diff --git a/.clang-tidy b/.clang-tidy index c9b77b5720..946f2950d8 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,9 +5,12 @@ Checks: >- -altera-*, -android-*, -boost-*, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, -bugprone-narrowing-conversions, -bugprone-signed-char-misuse, -cert-dcl50-cpp, + -cert-err33-c, -cert-err58-cpp, -cert-oop57-cpp, -cert-str34-c, @@ -15,6 +18,7 @@ Checks: >- -clang-analyzer-osx.*, -clang-diagnostic-delete-abstract-non-virtual-dtor, -clang-diagnostic-delete-non-abstract-non-virtual-dtor, + -clang-diagnostic-ignored-optimization-argument, -clang-diagnostic-shadow-field, -clang-diagnostic-unused-const-variable, -clang-diagnostic-unused-parameter, @@ -25,6 +29,7 @@ Checks: >- -cppcoreguidelines-macro-usage, -cppcoreguidelines-narrowing-conversions, -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-prefer-member-initializer, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-pro-bounds-pointer-arithmetic, @@ -36,6 +41,7 @@ Checks: >- -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-special-member-functions, + -cppcoreguidelines-virtual-class-destructor, -fuchsia-multiple-inheritance, -fuchsia-overloaded-operator, -fuchsia-statically-constructed-objects, @@ -68,6 +74,7 @@ Checks: >- -modernize-use-nodiscard, -mpi-*, -objc-*, + -readability-container-data-pointer, -readability-convert-member-functions-to-static, -readability-else-after-return, -readability-function-cognitive-complexity, @@ -82,8 +89,6 @@ WarningsAsErrors: '*' AnalyzeTemporaryDtors: false FormatStyle: google CheckOptions: - - key: google-readability-braces-around-statements.ShortStatementLines - value: '1' - key: google-readability-function-size.StatementThreshold value: '800' - key: google-runtime-int.TypeSuffix @@ -158,3 +163,9 @@ CheckOptions: value: '' - key: readability-qualified-auto.AddConstToQualified value: 0 + - key: readability-identifier-length.MinimumVariableNameLength + value: 0 + - key: readability-identifier-length.MinimumParameterNameLength + value: 0 + - key: readability-identifier-length.MinimumLoopCounterNameLength + value: 0 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b3fa6d4932..8d9565ad5f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,33 +5,59 @@ "script/devcontainer-post-create" ], "containerEnv": { - "DEVCONTAINER": "1" + "DEVCONTAINER": "1", + "PIP_BREAK_SYSTEM_PACKAGES": "1", + "PIP_ROOT_USER_ACTION": "ignore" }, "runArgs": [ "--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1" + // uncomment and edit the path in order to pass though local USB serial to the conatiner + // , "--device=/dev/ttyACM0" ], "appPort": 6052, + // if you are using avahi in the host device, uncomment these to allow the + // devcontainer to find devices via mdns + //"mounts": [ + // "type=bind,source=/dev/bus/usb,target=/dev/bus/usb", + // "type=bind,source=/var/run/dbus,target=/var/run/dbus", + // "type=bind,source=/var/run/avahi-daemon/socket,target=/var/run/avahi-daemon/socket" + //], "customizations": { "vscode": { "extensions": [ // python "ms-python.python", + "ms-python.pylint", + "ms-python.flake8", + "ms-python.black-formatter", "visualstudioexptteam.vscodeintellicode", // yaml "redhat.vscode-yaml", // cpp "ms-vscode.cpptools", // editorconfig - "editorconfig.editorconfig", + "editorconfig.editorconfig" ], "settings": { "python.languageServer": "Pylance", "python.pythonPath": "/usr/bin/python3", - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", + "pylint.args": [ + "--rcfile=${workspaceFolder}/pyproject.toml" + ], + "flake8.args": [ + "--config=${workspaceFolder}/.flake8" + ], + "black-formatter.args": [ + "--config", + "${workspaceFolder}/pyproject.toml" + ], + "[python]": { + // VS will say "Value is not accepted" before building the devcontainer, but the warning + // should go away after build is completed. + "editor.defaultFormatter": "ms-python.black-formatter" + }, "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, @@ -40,6 +66,8 @@ "yaml.customTags": [ "!secret scalar", "!lambda scalar", + "!extend scalar", + "!remove scalar", "!include_dir_named scalar", "!include_dir_list scalar", "!include_dir_merge_list scalar", diff --git a/.dockerignore b/.dockerignore index 9f14b98059..7998ff877f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -75,6 +75,9 @@ target/ # pyenv .python-version +# asdf +.tool-versions + # celery beat schedule file celerybeat-schedule diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..2724da06b6 --- /dev/null +++ b/.flake8 @@ -0,0 +1,42 @@ +[flake8] +max-line-length = 120 +# Following 4 for black compatibility +# E501: line too long +# W503: Line break occurred before a binary operator +# E203: Whitespace before ':' +# D202 No blank lines allowed after function docstring + +# TODO fix flake8 +# D100 Missing docstring in public module +# D101 Missing docstring in public class +# D102 Missing docstring in public method +# D103 Missing docstring in public function +# D104 Missing docstring in public package +# D105 Missing docstring in magic method +# D107 Missing docstring in __init__ +# D200 One-line docstring should fit on one line with quotes +# D205 1 blank line required between summary line and description +# D209 Multi-line docstring closing quotes should be on a separate line +# D400 First line should end with a period +# D401 First line should be in imperative mood + +ignore = + E501, + W503, + E203, + D202, + + D100, + D101, + D102, + D103, + D104, + D105, + D107, + D200, + D205, + D209, + D400, + D401, + +exclude = api_pb2.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3221b8ac5c..5703d39be1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,11 +7,16 @@ - [ ] 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) +- [ ] Code quality improvements to existing code or addition of tests - [ ] Other -**Related issue or feature (if applicable):** fixes +**Related issue or feature (if applicable):** -**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs# +- fixes + +**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** + +- esphome/esphome-docs# ## Test Environment @@ -19,14 +24,10 @@ - [ ] ESP32 IDF - [ ] ESP8266 - [ ] RP2040 +- [ ] BK72xx +- [ ] RTL87xx ## Example entry for `config.yaml`: - ```yaml # Example config.yaml diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml new file mode 100644 index 0000000000..5c686605c3 --- /dev/null +++ b/.github/actions/build-image/action.yaml @@ -0,0 +1,97 @@ +name: Build Image +inputs: + platform: + description: "Platform to build for" + required: true + example: "linux/amd64" + target: + description: "Target to build" + required: true + example: "docker" + baseimg: + description: "Base image type" + required: true + example: "docker" + suffix: + description: "Suffix to add to tags" + required: true + version: + description: "Version to build" + required: true + example: "2023.12.0" +runs: + using: "composite" + steps: + - name: Generate short tags + id: tags + shell: bash + run: | + output=$(docker/generate_tags.py \ + --tag "${{ inputs.version }}" \ + --suffix "${{ inputs.suffix }}") + echo $output + for l in $output; do + echo $l >> $GITHUB_OUTPUT + done + + # set cache-to only if dev branch + - id: cache-to + shell: bash + run: |- + if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then + echo "value=type=gha,mode=max" >> $GITHUB_OUTPUT + else + echo "value=" >> $GITHUB_OUTPUT + fi + + - name: Build and push to ghcr by digest + id: build-ghcr + uses: docker/build-push-action@v6.9.0 + env: + DOCKER_BUILD_SUMMARY: false + DOCKER_BUILD_RECORD_UPLOAD: false + with: + context: . + file: ./docker/Dockerfile + platforms: ${{ inputs.platform }} + target: ${{ inputs.target }} + cache-from: type=gha + cache-to: ${{ steps.cache-to.outputs.value }} + build-args: | + BASEIMGTYPE=${{ inputs.baseimg }} + BUILD_VERSION=${{ inputs.version }} + outputs: | + type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true + + - name: Export ghcr digests + shell: bash + run: | + mkdir -p /tmp/digests/${{ inputs.target }}/ghcr + digest="${{ steps.build-ghcr.outputs.digest }}" + touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}" + + - name: Build and push to dockerhub by digest + id: build-dockerhub + uses: docker/build-push-action@v6.9.0 + env: + DOCKER_BUILD_SUMMARY: false + DOCKER_BUILD_RECORD_UPLOAD: false + with: + context: . + file: ./docker/Dockerfile + platforms: ${{ inputs.platform }} + target: ${{ inputs.target }} + cache-from: type=gha + cache-to: ${{ steps.cache-to.outputs.value }} + build-args: | + BASEIMGTYPE=${{ inputs.baseimg }} + BUILD_VERSION=${{ inputs.version }} + outputs: | + type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true + + - name: Export dockerhub digests + shell: bash + run: | + mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub + digest="${{ steps.build-dockerhub.outputs.digest }}" + touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}" diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml new file mode 100644 index 0000000000..06c54578f5 --- /dev/null +++ b/.github/actions/restore-python/action.yml @@ -0,0 +1,47 @@ +name: Restore Python +inputs: + python-version: + description: Python version to restore + required: true + type: string + cache-key: + description: Cache key to use + required: true + type: string +outputs: + python-version: + description: Python version restored + value: ${{ steps.python.outputs.python-version }} +runs: + using: "composite" + steps: + - name: Set up Python ${{ inputs.python-version }} + id: python + uses: actions/setup-python@v5.3.0 + with: + python-version: ${{ inputs.python-version }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache/restore@v4.1.2 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }} + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows' + shell: bash + run: | + python -m venv venv + source venv/bin/activate + python --version + pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt + pip install -e . + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows' + shell: bash + run: | + python -m venv venv + ./venv/Scripts/activate + python --version + pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt + pip install -e . diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 666532f360..bb35f16048 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,3 +13,20 @@ updates: schedule: interval: daily open-pull-requests-limit: 10 + groups: + docker-actions: + applies-to: version-updates + patterns: + - "docker/setup-qemu-action" + - "docker/login-action" + - "docker/setup-buildx-action" + - package-ecosystem: github-actions + directory: "/.github/actions/build-image" + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: "/.github/actions/restore-python" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml new file mode 100644 index 0000000000..a6b2e2b2b3 --- /dev/null +++ b/.github/workflows/ci-api-proto.yml @@ -0,0 +1,80 @@ +name: API Proto CI + +on: + pull_request: + paths: + - "esphome/components/api/api.proto" + - "esphome/components/api/api_pb2.cpp" + - "esphome/components/api/api_pb2.h" + - "esphome/components/api/api_pb2_service.cpp" + - "esphome/components/api/api_pb2_service.h" + - "script/api_protobuf/api_protobuf.py" + - ".github/workflows/ci-api-proto.yml" + +permissions: + contents: read + pull-requests: write + +jobs: + check: + name: Check generated files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.7 + - name: Set up Python + uses: actions/setup-python@v5.3.0 + with: + python-version: "3.11" + + - name: Install apt dependencies + run: | + sudo apt update + sudo apt-cache show protobuf-compiler + sudo apt install -y protobuf-compiler + protoc --version + - name: Install python dependencies + run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt + - name: Generate files + run: script/api_protobuf/api_protobuf.py + - name: Check for changes + run: | + if ! git diff --quiet; then + echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY + echo "You have altered the generated proto files but they do not match what is expected." | tee -a $GITHUB_STEP_SUMMARY + echo "Please run 'script/api_protobuf/api_protobuf.py' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY + exit 1 + fi + - if: failure() + name: Review PR + uses: actions/github-script@v7.0.1 + with: + script: | + await github.rest.pulls.createReview({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + event: 'REQUEST_CHANGES', + body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.' + }) + - if: success() + name: Dismiss review + uses: actions/github-script@v7.0.1 + with: + script: | + let reviews = await github.rest.pulls.listReviews({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (let review of reviews.data) { + if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') { + await github.rest.pulls.dismissReview({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + review_id: review.id, + message: 'Files now match the expected proto files.' + }); + } + } diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index eb3a5a945c..435a58498e 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -2,13 +2,13 @@ name: CI for docker images # Only run when docker paths change -# yamllint disable-line rule:truthy + on: push: branches: [dev, beta, release] paths: - "docker/**" - - ".github/workflows/**" + - ".github/workflows/ci-docker.yml" - "requirements*.txt" - "platformio.ini" - "script/platformio_install_deps.py" @@ -16,7 +16,7 @@ on: pull_request: paths: - "docker/**" - - ".github/workflows/**" + - ".github/workflows/ci-docker.yml" - "requirements*.txt" - "platformio.ini" - "script/platformio_install_deps.py" @@ -40,15 +40,15 @@ jobs: arch: [amd64, armv7, aarch64] build_type: ["ha-addon", "docker", "lint"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5.3.0 with: python-version: "3.9" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3.7.1 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3.2.0 - name: Set TAG run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b04d04672..82a55d0e2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,18 @@ --- name: CI -# yamllint disable-line rule:truthy on: push: branches: [dev, beta, release] pull_request: + paths: + - "**" + - "!.github/workflows/*.yml" + - "!.github/actions/build-image/*" + - ".github/workflows/ci.yml" + - "!.yamllint" + - "!.github/dependabot.yml" merge_group: permissions: @@ -15,7 +21,6 @@ permissions: env: DEFAULT_PYTHON: "3.9" PYUPGRADE_TARGET: "--py39-plus" - CLANG_FORMAT_VERSION: "13.0.1" concurrency: # yamllint disable-line rule:line-length @@ -26,20 +31,26 @@ jobs: common: name: Create common environment runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v4.1.7 + - name: Generate cache-key + id: cache-key + run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.6.0 + id: python + uses: actions/setup-python@v5.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.3.1 + uses: actions/cache@v4.1.2 with: path: venv # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -49,15 +60,6 @@ jobs: pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -e . - yamllint: - name: yamllint - runs-on: ubuntu-latest - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Run yamllint - uses: frenck/action-yamllint@v1.4.0 - black: name: Check black runs-on: ubuntu-latest @@ -65,13 +67,12 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run black run: | . venv/bin/activate @@ -87,13 +88,12 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run flake8 run: | . venv/bin/activate @@ -109,13 +109,12 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run pylint run: | . venv/bin/activate @@ -131,13 +130,12 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run pyupgrade run: | . venv/bin/activate @@ -153,13 +151,12 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" - name: Run script/ci-custom @@ -170,24 +167,61 @@ jobs: pytest: name: Run pytest - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + os: + - ubuntu-latest + - macOS-latest + - windows-latest + exclude: + # Minimize CI resource usage + # by only running the Python version + # version used for docker images on Windows and macOS + - python-version: "3.12" + os: windows-latest + - python-version: "3.10" + os: windows-latest + - python-version: "3.9" + os: windows-latest + - python-version: "3.12" + os: macOS-latest + - python-version: "3.10" + os: macOS-latest + - python-version: "3.9" + os: macOS-latest + runs-on: ${{ matrix.os }} needs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ matrix.python-version }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - name: Run pytest + if: matrix.os == 'windows-latest' + run: | + ./venv/Scripts/activate + pytest -vv --cov-report=xml --tb=native tests + - name: Run pytest + if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' run: | . venv/bin/activate - pytest -vv --tb=native tests + pytest -vv --cov-report=xml --tb=native tests + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} clang-format: name: Check clang-format @@ -196,17 +230,16 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Install clang-format run: | . venv/bin/activate - pip install clang-format==${{ env.CLANG_FORMAT_VERSION }} + pip install clang-format -c requirements_dev.txt - name: Run clang-format run: | . venv/bin/activate @@ -216,44 +249,6 @@ jobs: run: script/ci-suggest-changes if: always() - compile-tests: - name: Run YAML test ${{ matrix.file }} - runs-on: ubuntu-latest - needs: - - common - - black - - ci-custom - - clang-format - - flake8 - - pylint - - pytest - - pyupgrade - - yamllint - strategy: - fail-fast: false - max-parallel: 2 - matrix: - file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8] - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 - with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} - - name: Cache platformio - uses: actions/cache@v3.3.1 - with: - path: ~/.platformio - # yamllint disable-line rule:line-length - key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }} - - name: Run esphome compile tests/test${{ matrix.file }}.yaml - run: | - . venv/bin/activate - esphome compile tests/test${{ matrix.file }}.yaml - clang-tidy: name: ${{ matrix.name }} runs-on: ubuntu-latest @@ -266,7 +261,6 @@ jobs: - pylint - pytest - pyupgrade - - yamllint strategy: fail-fast: false max-parallel: 2 @@ -299,29 +293,44 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} - # Use per check platformio cache because checks use different parts + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Cache platformio - uses: actions/cache@v3.3.1 + if: github.ref == 'refs/heads/dev' + uses: actions/cache@v4.1.2 with: path: ~/.platformio - # yamllint disable-line rule:line-length - key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} + key: platformio-${{ matrix.pio_cache_key }} + + - name: Cache platformio + if: github.ref != 'refs/heads/dev' + uses: actions/cache/restore@v4.1.2 + with: + path: ~/.platformio + key: platformio-${{ matrix.pio_cache_key }} - name: Install clang-tidy - run: sudo apt-get install clang-tidy-11 + run: | + sudo apt-get update + sudo apt-get install clang-tidy-14 - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + - name: Run 'pio run --list-targets -e esp32-idf-tidy' + if: matrix.name == 'Run script/clang-tidy for ESP32 IDF' + run: | + . venv/bin/activate + mkdir -p .temp + pio run --list-targets -e esp32-idf-tidy + - name: Run clang-tidy run: | . venv/bin/activate @@ -335,6 +344,143 @@ jobs: # yamllint disable-line rule:line-length if: always() + list-components: + runs-on: ubuntu-latest + needs: + - common + if: github.event_name == 'pull_request' + outputs: + components: ${{ steps.list-components.outputs.components }} + count: ${{ steps.list-components.outputs.count }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.7 + with: + # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works. + fetch-depth: 500 + - name: Get target branch + id: target-branch + run: | + echo "branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT + - name: Fetch ${{ steps.target-branch.outputs.branch }} branch + run: | + git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/${{ steps.target-branch.outputs.branch }}:refs/remotes/origin/${{ steps.target-branch.outputs.branch }} + git merge-base refs/remotes/origin/${{ steps.target-branch.outputs.branch }} HEAD + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Find changed components + id: list-components + run: | + . venv/bin/activate + components=$(script/list-components.py --changed --branch ${{ steps.target-branch.outputs.branch }}) + output_components=$(echo "$components" | jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))') + count=$(echo "$output_components" | jq length) + + echo "components=$output_components" >> $GITHUB_OUTPUT + echo "count=$count" >> $GITHUB_OUTPUT + + echo "$count Components:" + echo "$output_components" | jq + + test-build-components: + name: Component test ${{ matrix.file }} + runs-on: ubuntu-latest + needs: + - common + - list-components + if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100 + strategy: + fail-fast: false + max-parallel: 2 + matrix: + file: ${{ fromJson(needs.list-components.outputs.components) }} + steps: + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install libsdl2-dev + + - name: Check out code from GitHub + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: test_build_components -e config -c ${{ matrix.file }} + run: | + . venv/bin/activate + ./script/test_build_components -e config -c ${{ matrix.file }} + - name: test_build_components -e compile -c ${{ matrix.file }} + run: | + . venv/bin/activate + ./script/test_build_components -e compile -c ${{ matrix.file }} + + test-build-components-splitter: + name: Split components for testing into 20 groups maximum + runs-on: ubuntu-latest + needs: + - common + - list-components + if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100 + outputs: + matrix: ${{ steps.split.outputs.components }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.7 + - name: Split components into 20 groups + id: split + run: | + components=$(echo '${{ needs.list-components.outputs.components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]') + echo "components=$components" >> $GITHUB_OUTPUT + + test-build-components-split: + name: Test split components + runs-on: ubuntu-latest + needs: + - common + - list-components + - test-build-components-splitter + if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} + steps: + - name: List components + run: echo ${{ matrix.components }} + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install libsdl2-dev + + - name: Check out code from GitHub + uses: actions/checkout@v4.1.7 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Validate config + run: | + . venv/bin/activate + for component in ${{ matrix.components }}; do + ./script/test_build_components -e config -c $component + done + - name: Compile config + run: | + . venv/bin/activate + mkdir build_cache + export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache + for component in ${{ matrix.components }}; do + ./script/test_build_components -e compile -c $component + done + ci-status: name: CI Status runs-on: ubuntu-latest @@ -347,9 +493,11 @@ jobs: - pylint - pytest - pyupgrade - - yamllint - - compile-tests - clang-tidy + - list-components + - test-build-components + - test-build-components-splitter + - test-build-components-split if: always() steps: - name: Success @@ -357,4 +505,8 @@ jobs: run: exit 0 - name: Failure if: ${{ contains(needs.*.result, 'failure') }} - run: exit 1 + env: + JSON_DOC: ${{ toJSON(needs) }} + run: | + echo $JSON_DOC | jq + exit 1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..ddeb0a99d2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,91 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + workflow_dispatch: + schedule: + - cron: "30 18 * * 4" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + # - language: c-cpp + # build-mode: autobuild + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # 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@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index e762512ff6..ee10f49f61 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -1,7 +1,6 @@ --- name: Lock -# yamllint disable-line rule:truthy on: schedule: - cron: "30 0 * * *" @@ -18,7 +17,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5.0.1 with: pr-inactive-days: "1" pr-lock-reason: "" diff --git a/.github/workflows/needs-docs.yml b/.github/workflows/needs-docs.yml new file mode 100644 index 0000000000..628b5cc5e3 --- /dev/null +++ b/.github/workflows/needs-docs.yml @@ -0,0 +1,24 @@ +name: Needs Docs + +on: + pull_request: + types: [labeled, unlabeled] + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - name: Check for needs-docs label + uses: actions/github-script@v7.0.1 + with: + script: | + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const needsDocs = labels.find(label => label.name === 'needs-docs'); + if (needsDocs) { + core.setFailed('Pull request needs docs'); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ebd04e793..82d7ae5ee8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,6 @@ --- name: Publish Release -# yamllint disable-line rule:truthy on: workflow_dispatch: release: @@ -18,14 +17,16 @@ jobs: runs-on: ubuntu-latest outputs: tag: ${{ steps.tag.outputs.tag }} + branch_build: ${{ steps.tag.outputs.branch_build }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4.1.7 - name: Get tag id: tag # yamllint disable rule:line-length run: | - if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then - TAG="${GITHUB_REF#refs/tags/}" + if [[ "${{ github.event_name }}" = "release" ]]; then + TAG="${{ github.event.release.tag_name}}" + BRANCH_BUILD="false" else TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") today="$(date --utc '+%Y%m%d')" @@ -33,77 +34,167 @@ jobs: BRANCH=${GITHUB_REF#refs/heads/} if [[ "$BRANCH" != "dev" ]]; then TAG="${TAG}-${BRANCH}" + BRANCH_BUILD="true" + else + BRANCH_BUILD="false" fi fi echo "tag=${TAG}" >> $GITHUB_OUTPUT + echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT # yamllint enable rule:line-length deploy-pypi: name: Build and publish to PyPi if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5.3.0 with: python-version: "3.x" - name: Set up python environment - run: | - script/setup - pip install setuptools wheel twine - - name: Build - run: python setup.py sdist bdist_wheel - - name: Upload env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: twine upload dist/* + ESPHOME_NO_VENV: 1 + run: script/setup + - name: Build + run: |- + pip3 install build + python3 -m build + - name: Publish + uses: pypa/gh-action-pypi-publish@v1.11.0 deploy-docker: - name: Build and publish ESPHome ${{ matrix.image.title}} + name: Build ESPHome ${{ matrix.platform }} if: github.repository == 'esphome/esphome' permissions: contents: read packages: write runs-on: ubuntu-latest - continue-on-error: ${{ matrix.image.title == 'lint' }} needs: [init] + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm/v7 + - linux/arm64 + steps: + - uses: actions/checkout@v4.1.7 + - name: Set up Python + uses: actions/setup-python@v5.3.0 + with: + python-version: "3.9" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.7.1 + - name: Set up QEMU + if: matrix.platform != 'linux/amd64' + uses: docker/setup-qemu-action@v3.2.0 + + - name: Log in to docker hub + uses: docker/login-action@v3.3.0 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + uses: docker/login-action@v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build docker + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: docker + baseimg: docker + suffix: "" + version: ${{ needs.init.outputs.tag }} + + - name: Build ha-addon + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: hassio + baseimg: hassio + suffix: "hassio" + version: ${{ needs.init.outputs.tag }} + + - name: Build lint + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: lint + baseimg: docker + suffix: lint + version: ${{ needs.init.outputs.tag }} + + - name: Sanitize platform name + id: sanitize + run: | + echo "${{ matrix.platform }}" | sed 's|/|-|g' > /tmp/platform + echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT + + - name: Upload digests + uses: actions/upload-artifact@v4.4.3 + with: + name: digests-${{ steps.sanitize.outputs.name }} + path: /tmp/digests + retention-days: 1 + + deploy-manifest: + name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }} + runs-on: ubuntu-latest + needs: + - init + - deploy-docker + if: github.repository == 'esphome/esphome' + permissions: + contents: read + packages: write strategy: fail-fast: false matrix: image: - title: "ha-addon" - suffix: "hassio" target: "hassio" - baseimg: "hassio" + suffix: "hassio" - title: "docker" - suffix: "" target: "docker" - baseimg: "docker" + suffix: "" - title: "lint" - suffix: "lint" target: "lint" - baseimg: "docker" + suffix: "lint" + registry: + - ghcr + - dockerhub steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + - uses: actions/checkout@v4.1.7 + + - name: Download digests + uses: actions/download-artifact@v4.1.8 with: - python-version: "3.9" + pattern: digests-* + path: /tmp/digests + merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-buildx-action@v3.7.1 - name: Log in to docker hub - uses: docker/login-action@v2 + if: matrix.registry == 'dockerhub' + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@v2 + if: matrix.registry == 'ghcr' + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -112,44 +203,44 @@ jobs: - name: Generate short tags id: tags run: | - docker/generate_tags.py \ + output=$(docker/generate_tags.py \ --tag "${{ needs.init.outputs.tag }}" \ - --suffix "${{ matrix.image.suffix }}" + --suffix "${{ matrix.image.suffix }}" \ + --registry "${{ matrix.registry }}") + echo $output + for l in $output; do + echo $l >> $GITHUB_OUTPUT + done - - name: Build and push - uses: docker/build-push-action@v4 - with: - context: . - file: ./docker/Dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64 - target: ${{ matrix.image.target }} - push: true - # yamllint disable rule:line-length - cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }} - cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max - # yamllint enable rule:line-length - tags: ${{ steps.tags.outputs.tags }} - build-args: | - BASEIMGTYPE=${{ matrix.image.baseimg }} - BUILD_VERSION=${{ needs.init.outputs.tag }} + - name: Create manifest list and push + working-directory: /tmp/digests/${{ matrix.image.target }}/${{ matrix.registry }} + run: | + docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \ + $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *) deploy-ha-addon-repo: - if: github.repository == 'esphome/esphome' && github.event_name == 'release' + if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' runs-on: ubuntu-latest - needs: [deploy-docker] + needs: + - init + - deploy-manifest steps: - name: Trigger Workflow - uses: actions/github-script@v6 + uses: actions/github-script@v7.0.1 with: github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} script: | + let description = "ESPHome"; + if (context.eventName == "release") { + description = ${{ toJSON(github.event.release.body) }}; + } github.rest.actions.createWorkflowDispatch({ owner: "esphome", repo: "home-assistant-addon", workflow_id: "bump-version.yml", ref: "main", inputs: { - version: "${{ github.event.release.tag_name }}", - content: ${{ toJSON(github.event.release.body) }} + version: "${{ needs.init.outputs.tag }}", + content: description } }) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3a3e390eef..95f275e5a4 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,7 +1,6 @@ --- name: Stale -# yamllint disable-line rule:truthy on: schedule: - cron: "30 0 * * *" @@ -18,7 +17,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9.0.0 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -38,7 +37,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9.0.0 with: days-before-pr-stale: -1 days-before-pr-close: -1 diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 896a0369ac..7a46d596a1 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -4,30 +4,27 @@ name: Synchronise Device Classes from Home Assistant on: workflow_dispatch: schedule: - - cron: '45 6 * * *' - -permissions: - contents: write - pull-requests: write + - cron: "45 6 * * *" jobs: sync: name: Sync Device Classes runs-on: ubuntu-latest + if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.7 - name: Checkout Home Assistant - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.7 with: repository: home-assistant/core path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5.3.0 with: - python-version: 3.11 + python-version: 3.12 - name: Install Home Assistant run: | @@ -38,17 +35,8 @@ jobs: run: | python ./script/sync-device_class.py - - name: Get PR template - id: pr-template-body - run: | - body=$(cat .github/PULL_REQUEST_TEMPLATE.md) - delimiter="$(openssl rand -hex 8)" - echo "body<<$delimiter" >> $GITHUB_OUTPUT - echo "$body" >> $GITHUB_OUTPUT - echo "$delimiter" >> $GITHUB_OUTPUT - - name: Commit changes - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v7.0.5 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot @@ -56,5 +44,5 @@ jobs: branch: sync/device-classes delete-branch: true title: "Synchronise Device Classes from Home Assistant" - body: ${{ steps.pr-template-body.outputs.body }} + body-path: .github/PULL_REQUEST_TEMPLATE.md token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }} diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml new file mode 100644 index 0000000000..1c0b5f58ad --- /dev/null +++ b/.github/workflows/yaml-lint.yml @@ -0,0 +1,25 @@ +--- +name: YAML lint + +on: + push: + branches: [dev, beta, release] + paths: + - "**.yaml" + - "**.yml" + pull_request: + paths: + - "**.yaml" + - "**.yml" + +jobs: + yamllint: + name: yamllint + runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.7 + - name: Run yamllint + uses: frenck/action-yamllint@v1.5.0 + with: + strict: true diff --git a/.gitignore b/.gitignore index 71b66b2499..ad38e26fdd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,12 @@ __pycache__/ # Intellij Idea .idea +# Eclipse +.project +.cproject +.pydevproject +.settings/ + # Vim *.swp @@ -69,6 +75,9 @@ cov.xml # pyenv .python-version +# asdf +.tool-versions + # Environments .env .venv @@ -129,4 +138,8 @@ tests/.esphome/ sdkconfig.* !sdkconfig.defaults -.tests/ \ No newline at end of file +.tests/ + +/components +/managed_components + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 617d6f5d9f..aeb434167a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,8 +2,17 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - - repo: https://github.com/psf/black - rev: 23.3.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.5.4 + hooks: + # Run the linter. + - id: ruff + args: [--fix] + # Run the formatter. + - id: ruff-format + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.4.2 hooks: - id: black args: @@ -11,7 +20,7 @@ repos: - --quiet files: ^((esphome|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: @@ -27,7 +36,23 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py39-plus] + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.35.1 + hooks: + - id: yamllint + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v13.0.1 + hooks: + - id: clang-format + types_or: [c, c++] + - repo: local + hooks: + - id: pylint + name: pylint + entry: script/run-in-env.sh pylint + language: script + types: [python] diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 307dd496f0..acf1f29410 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -36,6 +36,24 @@ ] } ] + }, + { + "label": "Generate proto files", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "./script/api_protobuf/api_protobuf.py" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "never", + "close": true, + "panel": "new" + }, + "problemMatcher": [] } ] } diff --git a/.yamllint b/.yamllint index 4fea263214..22e9237f61 100644 --- a/.yamllint +++ b/.yamllint @@ -1,3 +1,19 @@ --- -ignore: | - venv/ +extends: default + +ignore-from-file: .gitignore + +rules: + document-start: disable + empty-lines: + level: error + max: 1 + max-start: 0 + max-end: 1 + indentation: + level: error + spaces: 2 + indent-sequences: true + check-multi-line-strings: false + line-length: disable + truthy: disable diff --git a/CODEOWNERS b/CODEOWNERS index 1b75203654..8fbbacef59 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,143 +6,243 @@ # the integration's code owner is automatically notified. # Core Code -setup.py @esphome/core +pyproject.toml @esphome/core esphome/*.py @esphome/core esphome/core/* @esphome/core # Integrations +esphome/components/a01nyub/* @MrSuicideParrot +esphome/components/a02yyuw/* @TH-Braemer esphome/components/absolute_humidity/* @DAVe3283 esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter +esphome/components/ade7880/* @kpfleming +esphome/components/ade7953/* @angelnu +esphome/components/ade7953_i2c/* @angelnu +esphome/components/ade7953_spi/* @angelnu +esphome/components/ads1118/* @solomondg1 +esphome/components/ags10/* @mak-42 +esphome/components/aic3204/* @kbx81 esphome/components/airthings_ble/* @jeromelaban +esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban +esphome/components/alarm_control_panel/* @grahambrown11 @hwstar +esphome/components/alpha3/* @jan-hofmeier +esphome/components/am2315c/* @swoboda1337 esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/am43/sensor/* @buxtronix esphome/components/analog_threshold/* @ianchi esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix +esphome/components/apds9306/* @aodrenah esphome/components/api/* @OttoWinter +esphome/components/as5600/* @ammmze +esphome/components/as5600/sensor/* @ammmze esphome/components/as7341/* @mrgnr esphome/components/async_tcp/* @OttoWinter +esphome/components/at581x/* @X-Ryl669 esphome/components/atc_mithermometer/* @ahpohl +esphome/components/atm90e26/* @danieltwagner +esphome/components/atm90e32/* @circuitsetup @descipher +esphome/components/audio/* @kahrendt +esphome/components/audio_dac/* @kbx81 +esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter esphome/components/bedjet/* @jhansche esphome/components/bedjet/climate/* @jhansche esphome/components/bedjet/fan/* @jhansche +esphome/components/bedjet/sensor/* @javawizard @jhansche +esphome/components/beken_spi_led_strip/* @Mat931 esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/bk72xx/* @kuba2k2 +esphome/components/bl0906/* @athom-tech @jesserockz @tarontop esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @tobias- -esphome/components/bl0942/* @dbuezas -esphome/components/ble_client/* @buxtronix +esphome/components/bl0942/* @dbuezas @dwmw2 +esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/bluetooth_proxy/* @jesserockz +esphome/components/bme280_base/* @esphome/core +esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth -esphome/components/bmp3xx/* @martgras +esphome/components/bme68x_bsec2/* @kbx81 @neffs +esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs +esphome/components/bmi160/* @flaviut +esphome/components/bmp280_base/* @ademuri +esphome/components/bmp280_i2c/* @ademuri +esphome/components/bmp280_spi/* @ademuri +esphome/components/bmp3xx/* @latonita +esphome/components/bmp3xx_base/* @latonita @martgras +esphome/components/bmp3xx_i2c/* @latonita +esphome/components/bmp3xx_spi/* @latonita +esphome/components/bmp581/* @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core +esphome/components/bytebuffer/* @clydebarrow esphome/components/canbus/* @danielschramm @mvturnho -esphome/components/cap1188/* @MrEditor97 +esphome/components/cap1188/* @mreditor97 esphome/components/captive_portal/* @OttoWinter esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke +esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz +esphome/components/combination/* @Cat-Ion @kahrendt esphome/components/coolix/* @glmnet esphome/components/copy/* @OttoWinter esphome/components/cover/* @esphome/core esphome/components/cs5460a/* @balrog-kun esphome/components/cse7761/* @berfenger +esphome/components/cst226/* @clydebarrow +esphome/components/cst816/* @clydebarrow esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx esphome/components/dac7678/* @NickB1 +esphome/components/daikin_arc/* @MagicBear esphome/components/daikin_brc/* @hagak +esphome/components/dallas_temp/* @ssieb esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core +esphome/components/datetime/* @jesserockz @rfdarter esphome/components/debug/* @OttoWinter esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet +esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dht/* @OttoWinter esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk +esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M -esphome/components/ektf2232/* @jesserockz +esphome/components/ektf2232/touchscreen/* @jesserockz +esphome/components/emc2101/* @ellull +esphome/components/emmeti/* @E440QF +esphome/components/ens160/* @latonita +esphome/components/ens160_base/* @latonita @vincentscode +esphome/components/ens160_i2c/* @latonita +esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 +esphome/components/es8311/* @kahrendt @kroimon esphome/components/esp32/* @esphome/core -esphome/components/esp32_ble/* @jesserockz +esphome/components/esp32_ble/* @Rapsssito @jesserockz esphome/components/esp32_ble_client/* @jesserockz -esphome/components/esp32_ble_server/* @jesserockz +esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_improv/* @jesserockz +esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/ethernet_info/* @gtjadsonsantos +esphome/components/event/* @nohat esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi -esphome/components/fingerprint_grow/* @OnFreund @loongyh +esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh +esphome/components/font/* @clydebarrow @esphome/core esphome/components/fs3000/* @kahrendt +esphome/components/ft5x06/* @clydebarrow +esphome/components/ft63x6/* @gpambrozio +esphome/components/gcja5/* @gcormier +esphome/components/gdk101/* @Szewcson esphome/components/globals/* @esphome/core +esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core +esphome/components/gpio/one_wire/* @ssieb esphome/components/gps/* @coogle esphome/components/graph/* @synco +esphome/components/graphical_display_menu/* @MrMDavidson +esphome/components/gree/* @orestismers +esphome/components/grove_gas_mc_v2/* @YorkshireIoT +esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte -esphome/components/haier/* @Yarikx +esphome/components/gt911/* @clydebarrow @jesserockz +esphome/components/haier/* @paveldn +esphome/components/haier/binary_sensor/* @paveldn +esphome/components/haier/button/* @paveldn +esphome/components/haier/sensor/* @paveldn +esphome/components/haier/switch/* @paveldn +esphome/components/haier/text_sensor/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann +esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode -esphome/components/homeassistant/* @OttoWinter +esphome/components/hmac_md5/* @dwmw2 +esphome/components/homeassistant/* @OttoWinter @esphome/core +esphome/components/homeassistant/number/* @landonr +esphome/components/homeassistant/switch/* @Links2004 +esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey -esphome/components/host/* @esphome/core +esphome/components/honeywellabp2_i2c/* @jpfaff +esphome/components/host/* @clydebarrow @esphome/core +esphome/components/host/time/* @clydebarrow esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hte501/* @Stock-M +esphome/components/http_request/ota/* @oarcher +esphome/components/http_request/update/* @jesserockz +esphome/components/htu31d/* @betterengineering esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hyt271/* @Philippe12 esphome/components/i2c/* @esphome/core +esphome/components/i2c_device/* @gabest11 esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz -esphome/components/i2s_audio/speaker/* @jesserockz -esphome/components/ili9xxx/* @nielsnl68 +esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt +esphome/components/iaqcore/* @yozik04 +esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core -esphome/components/ina260/* @MrEditor97 +esphome/components/ina226/* @Sergio303 @latonita +esphome/components/ina260/* @mreditor97 +esphome/components/ina2xx_base/* @latonita +esphome/components/ina2xx_i2c/* @latonita +esphome/components/ina2xx_spi/* @latonita esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkplate6/* @jesserockz esphome/components/integration/* @OttoWinter esphome/components/internal_temperature/* @Mat931 esphome/components/interval/* @esphome/core +esphome/components/jsn_sr04t/* @Mafus1 esphome/components/json/* @OttoWinter -esphome/components/kalman_combinator/* @Cat-Ion +esphome/components/kamstrup_kmp/* @cfeenstra1024 esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb esphome/components/kuntze/* @ssieb esphome/components/lcd_menu/* @numo68 -esphome/components/ld2410/* @sebcaps +esphome/components/ld2410/* @regevbr @sebcaps +esphome/components/ld2420/* @descipher esphome/components/ledc/* @OttoWinter +esphome/components/libretiny/* @kuba2k2 +esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/light/* @esphome/core +esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core -esphome/components/ltr390/* @sjtrny +esphome/components/ltr390/* @latonita @sjtrny +esphome/components/ltr501/* @latonita +esphome/components/ltr_als_ps/* @latonita +esphome/components/lvgl/* @clydebarrow +esphome/components/m5stack_8angle/* @rnauber esphome/components/matrix_keypad/* @ssieb +esphome/components/max17043/* @blacknell esphome/components/max31865/* @DAVe3283 esphome/components/max44009/* @berfenger esphome/components/max6956/* @looping40 @@ -159,11 +259,13 @@ esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp3204/* @rsumner esphome/components/mcp4728/* @berfenger esphome/components/mcp47a1/* @jesserockz -esphome/components/mcp9600/* @MrEditor97 +esphome/components/mcp9600/* @mreditor97 esphome/components/mcp9808/* @k7hpn 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/microphone/* @jesserockz esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov @@ -172,6 +274,7 @@ esphome/components/mitsubishi/* @RubyBailey esphome/components/mlx90393/* @functionpointer esphome/components/mlx90614/* @jesserockz esphome/components/mmc5603/* @benhoff +esphome/components/mmc5983/* @agoode esphome/components/modbus_controller/* @martgras esphome/components/modbus_controller/binary_sensor/* @martgras esphome/components/modbus_controller/number/* @martgras @@ -185,59 +288,84 @@ esphome/components/mopeka_pro_check/* @spbrogan esphome/components/mopeka_std_check/* @Fabian-Schmidt esphome/components/mpl3115a2/* @kbickar esphome/components/mpu6886/* @fabaff +esphome/components/ms8607/* @e28eta +esphome/components/nau7802/* @cujomalainey esphome/components/network/* @esphome/core -esphome/components/nextion/* @senexcrenshaw +esphome/components/nextion/* @edwardtfn @senexcrenshaw esphome/components/nextion/binary_sensor/* @senexcrenshaw esphome/components/nextion/sensor/* @senexcrenshaw esphome/components/nextion/switch/* @senexcrenshaw esphome/components/nextion/text_sensor/* @senexcrenshaw -esphome/components/nfc/* @jesserockz +esphome/components/nfc/* @jesserockz @kbx81 +esphome/components/noblex/* @AGalfra +esphome/components/npi19/* @bakerkj esphome/components/number/* @esphome/core +esphome/components/one_wire/* @ssieb +esphome/components/online_image/* @guillempages +esphome/components/opentherm/* @olegtarasov esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pca6416a/* @Mat931 -esphome/components/pca9554/* @hwstar +esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pcf85063/* @brogon +esphome/components/pcf8563/* @KoenBreeman esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie esphome/components/pmsa003i/* @sjtrny +esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn7150/* @jesserockz @kbx81 +esphome/components/pn7150_i2c/* @jesserockz @kbx81 +esphome/components/pn7160/* @jesserockz @kbx81 +esphome/components/pn7160_i2c/* @jesserockz @kbx81 +esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core -esphome/components/pulse_meter/* @cstaahl @stevebaxter +esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz +esphome/components/pylontech/* @functionpointer esphome/components/qmp6988/* @andrewpc esphome/components/qr_code/* @wjtje +esphome/components/qspi_dbi/* @clydebarrow +esphome/components/qwiic_pir/* @kahrendt esphome/components/radon_eye_ble/* @jeffeb3 esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet +esphome/components/resistance_sampler/* @jesserockz esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rp2040/* @jesserockz esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz +esphome/components/rpi_dpi_rgb/* @clydebarrow +esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet -esphome/components/safe_mode/* @jsuanet @paulmonigatti +esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core +esphome/components/sdl/* @clydebarrow esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath +esphome/components/seeed_mr24hpc1/* @limengdu esphome/components/selec_meter/* @sourabhjaiswal esphome/components/select/* @esphome/core +esphome/components/sen0321/* @notjj esphome/components/sen21231/* @shreyaskarnik esphome/components/sen5x/* @martgras esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core +esphome/components/sfa30/* @ghsensdev esphome/components/sgp40/* @SenexCrenshaw esphome/components/sgp4x/* @SenexCrenshaw @martgras esphome/components/shelly_dimmer/* @edge90 @rnauber +esphome/components/sht3xd/* @mrtoy-me esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sigma_delta_output/* @Cat-Ion @@ -251,8 +379,10 @@ esphome/components/smt100/* @piechade esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov -esphome/components/speaker/* @jesserockz -esphome/components/spi/* @esphome/core +esphome/components/speaker/* @jesserockz @kahrendt +esphome/components/spi/* @clydebarrow @esphome/core +esphome/components/spi_device/* @clydebarrow +esphome/components/spi_led_strip/* @clydebarrow esphome/components/sprinkler/* @kbx81 esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 @@ -266,20 +396,35 @@ esphome/components/ssd1331_base/* @kbx81 esphome/components/ssd1331_spi/* @kbx81 esphome/components/ssd1351_base/* @kbx81 esphome/components/ssd1351_spi/* @kbx81 +esphome/components/st7567_base/* @latonita +esphome/components/st7567_i2c/* @latonita +esphome/components/st7567_spi/* @latonita +esphome/components/st7701s/* @clydebarrow esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 esphome/components/st7920/* @marsjan155 +esphome/components/statsd/* @Links2004 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter +esphome/components/sun_gtil2/* @Mat931 esphome/components/switch/* @esphome/core esphome/components/t6615/* @tylermenezes +esphome/components/tc74/* @sethgirvan esphome/components/tca9548a/* @andreashergert1984 +esphome/components/tca9555/* @mobrembski esphome/components/tcl112/* @glmnet esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax +esphome/components/tem3200/* @bakerkj +esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar +esphome/components/template/datetime/* @rfdarter +esphome/components/template/event/* @nohat +esphome/components/template/fan/* @ssieb +esphome/components/text/* @mauritskorse esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter esphome/components/tlc5947/* @rnauber +esphome/components/tlc5971/* @IJIJI esphome/components/tm1621/* @Philippe12 esphome/components/tm1637/* @glmnet esphome/components/tm1638/* @skykingjwc @@ -289,8 +434,9 @@ esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka esphome/components/toshiba/* @kbx81 -esphome/components/touchscreen/* @jesserockz +esphome/components/touchscreen/* @jesserockz @nielsnl68 esphome/components/tsl2591/* @wjcarpenter +esphome/components/tt21100/* @kroimon esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/number/* @frankiboy1 @@ -299,21 +445,49 @@ esphome/components/tuya/sensor/* @jesserockz esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra esphome/components/uart/* @esphome/core +esphome/components/uart/button/* @ssieb +esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter +esphome/components/update/* @jesserockz +esphome/components/uponor_smatrix/* @kroimon +esphome/components/valve/* @esphome/core esphome/components/vbus/* @ssieb +esphome/components/veml3235/* @kbx81 +esphome/components/veml7700/* @latonita esphome/components/version/* @esphome/core esphome/components/voice_assistant/* @jesserockz -esphome/components/wake_on_lan/* @willwill2will54 +esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 +esphome/components/watchdog/* @oarcher +esphome/components/waveshare_epaper/* @clydebarrow esphome/components/web_server_base/* @OttoWinter +esphome/components/web_server_idf/* @dentra +esphome/components/weikai/* @DrCoolZic +esphome/components/weikai_i2c/* @DrCoolZic +esphome/components/weikai_spi/* @DrCoolZic esphome/components/whirlpool/* @glmnet esphome/components/whynter/* @aeonsablaze esphome/components/wiegand/* @ssieb +esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard +esphome/components/wk2132_i2c/* @DrCoolZic +esphome/components/wk2132_spi/* @DrCoolZic +esphome/components/wk2168_i2c/* @DrCoolZic +esphome/components/wk2168_spi/* @DrCoolZic +esphome/components/wk2204_i2c/* @DrCoolZic +esphome/components/wk2204_spi/* @DrCoolZic +esphome/components/wk2212_i2c/* @DrCoolZic +esphome/components/wk2212_spi/* @DrCoolZic esphome/components/wl_134/* @hobbypunk90 esphome/components/x9c/* @EtienneMD +esphome/components/xgzp68xx/* @gcormier +esphome/components/xiaomi_hhccjcy10/* @fariouche +esphome/components/xiaomi_lywsd02mmc/* @juanluss31 esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz -esphome/components/xpt2046/* @nielsnl68 @numo68 +esphome/components/xl9535/* @mreditor97 +esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 +esphome/components/zhlt01/* @cfeenstra1024 +esphome/components/zio_ultrasonic/* @kahrendt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec23656763..1c92d91159 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,5 +10,3 @@ Things to note when contributing: for more information. - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files which checks if your new feature compiles correctly. - - Sometimes I will let pull requests linger because I'm not 100% sure about them. Please feel free to ping - me after some time. diff --git a/README.md b/README.md index bb6fb37d3a..da1b2b3650 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,5 @@ For issues, please go to [the issue tracker](https://github.com/esphome/issues/issues). For feature requests, please see [feature requests](https://github.com/esphome/feature-requests/issues). + +[![ESPHome - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/esphome.png)](https://www.openhomefoundation.org/) diff --git a/docker/Dockerfile b/docker/Dockerfile index 95b6677815..44ee879a12 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,30 +5,41 @@ # One of "docker", "hassio" ARG BASEIMGTYPE=docker + # https://github.com/hassio-addons/addon-debian-base/releases -FROM ghcr.io/hassio-addons/debian-base:6.2.3 AS base-hassio -# https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye -FROM debian:bullseye-20230208-slim AS base-docker +FROM ghcr.io/hassio-addons/debian-base:7.2.0 AS base-hassio +# https://hub.docker.com/_/debian?tab=tags&page=1&name=bookworm +FROM debian:12.2-slim AS base-docker FROM base-${BASEIMGTYPE} AS base + ARG TARGETARCH ARG TARGETVARIANT + +# Note that --break-system-packages is used below because +# https://peps.python.org/pep-0668/ added a safety check that prevents +# installing packages with the same name as a system package. This is +# not a problem for us because we are not concerned about overwriting +# system packages because we are running in an isolated container. + RUN \ apt-get update \ # Use pinned versions so that we get updates with build caching && apt-get install -y --no-install-recommends \ - python3=3.9.2-3 \ - python3-pip=20.3.4-4+deb11u1 \ - python3-setuptools=52.0.0-4 \ - python3-pil=8.1.2+dfsg-0.3+deb11u1 \ - python3-cryptography=3.3.2-1 \ - python3-venv=3.9.2-3 \ - iputils-ping=3:20210202-1 \ - git=1:2.30.2-1+deb11u2 \ - curl=7.74.0-1.3+deb11u7 \ - openssh-client=1:8.4p1-5+deb11u1 \ + python3-pip=23.0.1+dfsg-1 \ + python3-setuptools=66.1.1-1 \ + python3-venv=3.11.2-1+b1 \ + python3-wheel=0.38.4-2 \ + iputils-ping=3:20221126-1 \ + git=1:2.39.5-0+deb12u1 \ + curl=7.88.1-10+deb12u7 \ + openssh-client=1:9.2p1-2+deb12u3 \ + python3-cffi=1.15.1-5 \ + libcairo2=1.16.0-7 \ + libmagic1=1:5.44-3 \ + patch=2.7.6-7 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ @@ -45,14 +56,18 @@ ENV \ # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian RUN \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ - ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ + ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \ fi RUN \ # Ubuntu python3-pip is missing wheel - pip3 install --no-cache-dir \ - wheel==0.37.1 \ - platformio==6.1.7 \ + if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ + export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ + fi; \ + pip3 install \ + --break-system-packages --no-cache-dir \ + # Keep platformio version in sync with requirements.txt + platformio==6.1.16 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_platformio_interval 1000000 \ @@ -60,10 +75,57 @@ RUN \ # First install requirements to leverage caching when requirements don't change -COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini / -RUN \ - pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ - && /platformio_install_deps.py /platformio.ini --libraries +# tmpfs is for https://github.com/rust-lang/cargo/issues/8719 + +COPY requirements.txt requirements_optional.txt / +RUN --mount=type=tmpfs,target=/root/.cargo <}``, and that dict is then handed off to the specified validators. + """ return maybe_conf(CONF_ID, *validators) def maybe_conf(conf, *validators): + """Allow a raw value to be specified in place of a config block. + If the value that's being validated is a dictionary, it's passed as-is to the specified validators. Otherwise, it's + wrapped in a dict that looks like ``{: }``, and that dict is then handed off to the specified + validators. + (This is a general case of ``maybe_simple_id`` that allows the wrapping key to be something other than ``id``.) + """ validator = cv.All(*validators) @schema_extractor("maybe") @@ -62,6 +75,13 @@ def validate_potentially_and_condition(value): return validate_condition(value) +def validate_potentially_or_condition(value): + if isinstance(value, list): + with cv.remove_prepend_path(["or"]): + return validate_condition({"or": value}) + return validate_condition(value) + + DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) @@ -69,6 +89,8 @@ WhileAction = cg.esphome_ns.class_("WhileAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component) UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action) +SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action) +ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) @@ -138,6 +160,7 @@ AUTOMATION_SCHEMA = cv.Schema( AndCondition = cg.esphome_ns.class_("AndCondition", Condition) OrCondition = cg.esphome_ns.class_("OrCondition", Condition) NotCondition = cg.esphome_ns.class_("NotCondition", Condition) +XorCondition = cg.esphome_ns.class_("XorCondition", Condition) @register_condition("and", AndCondition, validate_condition_list) @@ -152,12 +175,30 @@ async def or_condition_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg, conditions) +@register_condition("all", AndCondition, validate_condition_list) +async def all_condition_to_code(config, condition_id, template_arg, args): + conditions = await build_condition_list(config, template_arg, args) + return cg.new_Pvariable(condition_id, template_arg, conditions) + + +@register_condition("any", OrCondition, validate_condition_list) +async def any_condition_to_code(config, condition_id, template_arg, args): + conditions = await build_condition_list(config, template_arg, args) + return cg.new_Pvariable(condition_id, template_arg, conditions) + + @register_condition("not", NotCondition, validate_potentially_and_condition) async def not_condition_to_code(config, condition_id, template_arg, args): condition = await build_condition(config, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, condition) +@register_condition("xor", XorCondition, validate_condition_list) +async def xor_condition_to_code(config, condition_id, template_arg, args): + conditions = await build_condition_list(config, template_arg, args) + return cg.new_Pvariable(condition_id, template_arg, conditions) + + @register_condition("lambda", LambdaCondition, cv.returning_lambda) async def lambda_condition_to_code(config, condition_id, template_arg, args): lambda_ = await cg.process_lambda(config, args, return_type=bool) @@ -203,15 +244,21 @@ async def delay_action_to_code(config, action_id, template_arg, args): IfAction, cv.All( { - cv.Required(CONF_CONDITION): validate_potentially_and_condition, + cv.Exclusive( + CONF_CONDITION, CONF_CONDITION + ): validate_potentially_and_condition, + cv.Exclusive(CONF_ANY, CONF_CONDITION): validate_potentially_or_condition, + cv.Exclusive(CONF_ALL, CONF_CONDITION): validate_potentially_and_condition, cv.Optional(CONF_THEN): validate_action_list, cv.Optional(CONF_ELSE): validate_action_list, }, cv.has_at_least_one_key(CONF_THEN, CONF_ELSE), + cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL), ), ) async def if_action_to_code(config, action_id, template_arg, args): - conditions = await build_condition(config[CONF_CONDITION], template_arg, args) + cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION)) + conditions = await build_condition(config[cond_conf], template_arg, args) var = cg.new_Pvariable(action_id, template_arg, conditions) if CONF_THEN in config: actions = await build_action_list(config[CONF_THEN], template_arg, args) @@ -303,6 +350,41 @@ async def component_update_action_to_code(config, action_id, template_arg, args) return cg.new_Pvariable(action_id, template_arg, comp) +@register_action( + "component.suspend", + SuspendComponentAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), + } + ), +) +async def component_suspend_action_to_code(config, action_id, template_arg, args): + comp = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, comp) + + +@register_action( + "component.resume", + ResumeComponentAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), + cv.Optional(CONF_UPDATE_INTERVAL): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), +) +async def component_resume_action_to_code(config, action_id, template_arg, args): + comp = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, comp) + if CONF_UPDATE_INTERVAL in config: + template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int) + cg.add(var.set_update_interval(template_)) + return var + + async def build_action(full_config, template_arg, args): registry_entry, config = cg.extract_registry_entry_config( ACTION_REGISTRY, full_config diff --git a/esphome/codegen.py b/esphome/codegen.py index 43b44256e2..bfa1683ce7 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -8,83 +8,86 @@ # want to break suddenly due to a rename (this file will get backports for features). # pylint: disable=unused-import -from esphome.cpp_generator import ( # noqa +from esphome.cpp_generator import ( # noqa: F401 + ArrayInitializer, Expression, + LineComment, + MockObj, + MockObjClass, + Pvariable, RawExpression, RawStatement, - TemplateArguments, - StructInitializer, - ArrayInitializer, - safe_exp, Statement, - LineComment, - progmem_array, - static_const_array, - statement, - variable, - with_local_variable, - new_variable, - Pvariable, - new_Pvariable, + StructInitializer, + TemplateArguments, add, - add_global, - add_library, add_build_flag, add_define, + add_global, + add_library, add_platformio_option, get_variable, get_variable_with_full_id, - process_lambda, is_template, + new_Pvariable, + new_variable, + process_lambda, + progmem_array, + safe_exp, + statement, + static_const_array, templatable, - MockObj, - MockObjClass, + variable, + with_local_variable, ) -from esphome.cpp_helpers import ( # noqa - gpio_pin_expression, - register_component, +from esphome.cpp_helpers import ( # noqa: F401 build_registry_entry, build_registry_list, extract_registry_entry_config, - register_parented, + gpio_pin_expression, past_safe_mode, + register_component, + register_parented, ) -from esphome.cpp_types import ( # noqa - global_ns, - void, - nullptr, - float_, - double, +from esphome.cpp_types import ( # noqa: F401 + NAN, + App, + Application, + Component, + ComponentPtr, + Controller, + EntityBase, + EntityCategory, + ESPTime, + GPIOPin, + InternalGPIOPin, + JsonObject, + JsonObjectConst, + Parented, + PollingComponent, + arduino_json_ns, bool_, + const_char_ptr, + double, + esphome_ns, + float_, + global_ns, + gpio_Flags, + int16, + int32, + int64, int_, + nullptr, + optional, + size_t, std_ns, + std_shared_ptr, std_string, + std_string_ref, std_vector, uint8, uint16, uint32, uint64, - int16, - int32, - int64, - size_t, - const_char_ptr, - NAN, - esphome_ns, - App, - EntityBase, - Component, - ComponentPtr, - PollingComponent, - Application, - optional, - arduino_json_ns, - JsonObject, - JsonObjectConst, - Controller, - GPIOPin, - InternalGPIOPin, - gpio_Flags, - EntityCategory, - Parented, + void, ) diff --git a/esphome/components/a01nyub/__init__.py b/esphome/components/a01nyub/__init__.py new file mode 100644 index 0000000000..4c84847fb6 --- /dev/null +++ b/esphome/components/a01nyub/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@MrSuicideParrot"] diff --git a/esphome/components/a01nyub/a01nyub.cpp b/esphome/components/a01nyub/a01nyub.cpp new file mode 100644 index 0000000000..d0bc89a0c9 --- /dev/null +++ b/esphome/components/a01nyub/a01nyub.cpp @@ -0,0 +1,44 @@ +// Datasheet https://wiki.dfrobot.com/A01NYUB%20Waterproof%20Ultrasonic%20Sensor%20SKU:%20SEN0313 + +#include "a01nyub.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace a01nyub { + +static const char *const TAG = "a01nyub.sensor"; + +void A01nyubComponent::loop() { + uint8_t data; + while (this->available() > 0) { + this->read_byte(&data); + if (this->buffer_.empty() && (data != 0xff)) + continue; + buffer_.push_back(data); + if (this->buffer_.size() == 4) + this->check_buffer_(); + } +} + +void A01nyubComponent::check_buffer_() { + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; + if (this->buffer_[3] == checksum) { + float distance = (this->buffer_[1] << 8) + this->buffer_[2]; + if (distance > 280) { + float meters = distance / 1000.0; + 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()); + } + } else { + ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); + } + this->buffer_.clear(); +} + +void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); } + +} // namespace a01nyub +} // namespace esphome diff --git a/esphome/components/a01nyub/a01nyub.h b/esphome/components/a01nyub/a01nyub.h new file mode 100644 index 0000000000..6b22e9bcad --- /dev/null +++ b/esphome/components/a01nyub/a01nyub.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace a01nyub { + +class A01nyubComponent : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void loop() override; + void dump_config() override; + + protected: + void check_buffer_(); + + std::vector buffer_; +}; + +} // namespace a01nyub +} // namespace esphome diff --git a/esphome/components/a01nyub/sensor.py b/esphome/components/a01nyub/sensor.py new file mode 100644 index 0000000000..b57daa0357 --- /dev/null +++ b/esphome/components/a01nyub/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, + DEVICE_CLASS_DISTANCE, +) + +CODEOWNERS = ["@MrSuicideParrot"] +DEPENDENCIES = ["uart"] + +a01nyub_ns = cg.esphome_ns.namespace("a01nyub") +A01nyubComponent = a01nyub_ns.class_( + "A01nyubComponent", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = sensor.sensor_schema( + A01nyubComponent, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_DISTANCE, +).extend(uart.UART_DEVICE_SCHEMA) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "a01nyub", + baud_rate=9600, + require_tx=False, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/a02yyuw/__init__.py b/esphome/components/a02yyuw/__init__.py new file mode 100644 index 0000000000..6724dbb970 --- /dev/null +++ b/esphome/components/a02yyuw/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@TH-Braemer"] diff --git a/esphome/components/a02yyuw/a02yyuw.cpp b/esphome/components/a02yyuw/a02yyuw.cpp new file mode 100644 index 0000000000..ee378c3283 --- /dev/null +++ b/esphome/components/a02yyuw/a02yyuw.cpp @@ -0,0 +1,43 @@ +// Datasheet https://wiki.dfrobot.com/_A02YYUW_Waterproof_Ultrasonic_Sensor_SKU_SEN0311 + +#include "a02yyuw.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace a02yyuw { + +static const char *const TAG = "a02yyuw.sensor"; + +void A02yyuwComponent::loop() { + uint8_t data; + while (this->available() > 0) { + this->read_byte(&data); + if (this->buffer_.empty() && (data != 0xff)) + continue; + buffer_.push_back(data); + if (this->buffer_.size() == 4) + this->check_buffer_(); + } +} + +void A02yyuwComponent::check_buffer_() { + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; + if (this->buffer_[3] == checksum) { + float distance = (this->buffer_[1] << 8) + this->buffer_[2]; + if (distance > 30) { + 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()); + } + } else { + ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); + } + this->buffer_.clear(); +} + +void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); } + +} // namespace a02yyuw +} // namespace esphome diff --git a/esphome/components/a02yyuw/a02yyuw.h b/esphome/components/a02yyuw/a02yyuw.h new file mode 100644 index 0000000000..6ff370fdc3 --- /dev/null +++ b/esphome/components/a02yyuw/a02yyuw.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace a02yyuw { + +class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void loop() override; + void dump_config() override; + + protected: + void check_buffer_(); + + std::vector buffer_; +}; + +} // namespace a02yyuw +} // namespace esphome diff --git a/esphome/components/a02yyuw/sensor.py b/esphome/components/a02yyuw/sensor.py new file mode 100644 index 0000000000..d491a51be9 --- /dev/null +++ b/esphome/components/a02yyuw/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + ICON_ARROW_EXPAND_VERTICAL, + DEVICE_CLASS_DISTANCE, + UNIT_MILLIMETER, +) + +CODEOWNERS = ["@TH-Braemer"] +DEPENDENCIES = ["uart"] + +a02yyuw_ns = cg.esphome_ns.namespace("a02yyuw") +A02yyuwComponent = a02yyuw_ns.class_( + "A02yyuwComponent", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = sensor.sensor_schema( + A02yyuwComponent, + unit_of_measurement=UNIT_MILLIMETER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_DISTANCE, +).extend(uart.UART_DEVICE_SCHEMA) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "a02yyuw", + baud_rate=9600, + require_tx=False, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/a4988/stepper.py b/esphome/components/a4988/stepper.py index 7f53856c7b..744e9dc1cc 100644 --- a/esphome/components/a4988/stepper.py +++ b/esphome/components/a4988/stepper.py @@ -28,6 +28,6 @@ async def to_code(config): dir_pin = await cg.gpio_pin_expression(config[CONF_DIR_PIN]) cg.add(var.set_dir_pin(dir_pin)) - if CONF_SLEEP_PIN in config: - sleep_pin = await cg.gpio_pin_expression(config[CONF_SLEEP_PIN]) + if sleep_pin_config := config.get(CONF_SLEEP_PIN): + sleep_pin = await cg.gpio_pin_expression(sleep_pin_config) cg.add(var.set_sleep_pin(sleep_pin)) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index cceaa594ef..11b0ba2389 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,13 +1,16 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_INPUT +from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER from esphome.core import CORE from esphome.components.esp32 import get_esp32_variant +from esphome.const import PLATFORM_ESP8266 from esphome.components.esp32.const import ( VARIANT_ESP32, + VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C6, VARIANT_ESP32H2, VARIANT_ESP32S2, VARIANT_ESP32S3, @@ -15,15 +18,28 @@ from esphome.components.esp32.const import ( CODEOWNERS = ["@esphome/core"] +adc_ns = cg.esphome_ns.namespace("adc") + + +""" +From the below patch versions (and 5.2+) ADC_ATTEN_DB_11 is deprecated and replaced with ADC_ATTEN_DB_12. +4.4.7 +5.0.5 +5.1.3 +5.2+ +""" + ATTENUATION_MODES = { "0db": cg.global_ns.ADC_ATTEN_DB_0, "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, "6db": cg.global_ns.ADC_ATTEN_DB_6, - "11db": cg.global_ns.ADC_ATTEN_DB_11, + "11db": adc_ns.ADC_ATTEN_DB_12_COMPAT, + "12db": adc_ns.ADC_ATTEN_DB_12_COMPAT, "auto": "auto", } adc1_channel_t = cg.global_ns.enum("adc1_channel_t") +adc2_channel_t = cg.global_ns.enum("adc2_channel_t") # From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h # pin to adc1 channel mapping @@ -69,6 +85,22 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { 3: adc1_channel_t.ADC1_CHANNEL_3, 4: adc1_channel_t.ADC1_CHANNEL_4, }, + VARIANT_ESP32C2: { + 0: adc1_channel_t.ADC1_CHANNEL_0, + 1: adc1_channel_t.ADC1_CHANNEL_1, + 2: adc1_channel_t.ADC1_CHANNEL_2, + 3: adc1_channel_t.ADC1_CHANNEL_3, + 4: adc1_channel_t.ADC1_CHANNEL_4, + }, + VARIANT_ESP32C6: { + 0: adc1_channel_t.ADC1_CHANNEL_0, + 1: adc1_channel_t.ADC1_CHANNEL_1, + 2: adc1_channel_t.ADC1_CHANNEL_2, + 3: adc1_channel_t.ADC1_CHANNEL_3, + 4: adc1_channel_t.ADC1_CHANNEL_4, + 5: adc1_channel_t.ADC1_CHANNEL_5, + 6: adc1_channel_t.ADC1_CHANNEL_6, + }, VARIANT_ESP32H2: { 0: adc1_channel_t.ADC1_CHANNEL_0, 1: adc1_channel_t.ADC1_CHANNEL_1, @@ -78,41 +110,99 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { }, } +ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { + # TODO: add other variants + VARIANT_ESP32: { + 4: adc2_channel_t.ADC2_CHANNEL_0, + 0: adc2_channel_t.ADC2_CHANNEL_1, + 2: adc2_channel_t.ADC2_CHANNEL_2, + 15: adc2_channel_t.ADC2_CHANNEL_3, + 13: adc2_channel_t.ADC2_CHANNEL_4, + 12: adc2_channel_t.ADC2_CHANNEL_5, + 14: adc2_channel_t.ADC2_CHANNEL_6, + 27: adc2_channel_t.ADC2_CHANNEL_7, + 25: adc2_channel_t.ADC2_CHANNEL_8, + 26: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32S2: { + 11: adc2_channel_t.ADC2_CHANNEL_0, + 12: adc2_channel_t.ADC2_CHANNEL_1, + 13: adc2_channel_t.ADC2_CHANNEL_2, + 14: adc2_channel_t.ADC2_CHANNEL_3, + 15: adc2_channel_t.ADC2_CHANNEL_4, + 16: adc2_channel_t.ADC2_CHANNEL_5, + 17: adc2_channel_t.ADC2_CHANNEL_6, + 18: adc2_channel_t.ADC2_CHANNEL_7, + 19: adc2_channel_t.ADC2_CHANNEL_8, + 20: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32S3: { + 11: adc2_channel_t.ADC2_CHANNEL_0, + 12: adc2_channel_t.ADC2_CHANNEL_1, + 13: adc2_channel_t.ADC2_CHANNEL_2, + 14: adc2_channel_t.ADC2_CHANNEL_3, + 15: adc2_channel_t.ADC2_CHANNEL_4, + 16: adc2_channel_t.ADC2_CHANNEL_5, + 17: adc2_channel_t.ADC2_CHANNEL_6, + 18: adc2_channel_t.ADC2_CHANNEL_7, + 19: adc2_channel_t.ADC2_CHANNEL_8, + 20: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32C3: { + 5: adc2_channel_t.ADC2_CHANNEL_0, + }, + VARIANT_ESP32C2: {}, + VARIANT_ESP32C6: {}, + VARIANT_ESP32H2: {}, +} + def validate_adc_pin(value): if str(value).upper() == "VCC": - return cv.only_on_esp8266("VCC") + if CORE.is_rp2040: + return pins.internal_gpio_input_pin_schema(29) + return cv.only_on([PLATFORM_ESP8266])("VCC") if str(value).upper() == "TEMPERATURE": return cv.only_on_rp2040("TEMPERATURE") if CORE.is_esp32: - value = pins.internal_gpio_input_pin_number(value) + conf = pins.internal_gpio_input_pin_schema(value) + value = conf[CONF_NUMBER] variant = get_esp32_variant() - if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: + if ( + variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL + and variant not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL + ): raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") - if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: + if ( + value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] + and value not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] + ): raise cv.Invalid(f"{variant} doesn't support ADC on this pin") - return pins.internal_gpio_input_pin_schema(value) + + return conf if CORE.is_esp8266: - from esphome.components.esp8266.gpio import CONF_ANALOG + conf = pins.gpio_pin_schema( + {CONF_ANALOG: True, CONF_INPUT: True}, internal=True + )(value) - value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( - value - ) + if conf[CONF_NUMBER] != 17: # A0 + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") + return conf - if value != 17: # A0 - raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") + if CORE.is_rp2040: + conf = pins.internal_gpio_input_pin_schema(value) + number = conf[CONF_NUMBER] + if number not in (26, 27, 28, 29): + raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC") + return conf + + if CORE.is_libretiny: return pins.gpio_pin_schema( {CONF_ANALOG: True, CONF_INPUT: True}, internal=True )(value) - if CORE.is_rp2040: - value = pins.internal_gpio_input_pin_number(value) - if value not in (26, 27, 28, 29): - raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.") - return pins.internal_gpio_input_pin_schema(value) - raise NotImplementedError diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 9bfe0f5eed..7257793016 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -1,6 +1,6 @@ #include "adc_sensor.h" -#include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #ifdef USE_ESP8266 #ifdef USE_ADC_SENSOR_VCC @@ -12,6 +12,9 @@ ADC_MODE(ADC_VCC) #endif #ifdef USE_RP2040 +#ifdef CYW43_USES_VSYS_PIN +#include "pico/cyw43_arch.h" +#endif #include #endif @@ -20,15 +23,15 @@ namespace adc { static const char *const TAG = "adc"; -// 13bit for S2, and 12bit for all other esp32 variants +// 13-bit for S2, 12-bit for all other ESP32 variants #ifdef USE_ESP32 static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast(ADC_WIDTH_MAX - 1); #ifndef SOC_ADC_RTC_MAX_BITWIDTH #if USE_ESP32_VARIANT_ESP32S2 -static const int SOC_ADC_RTC_MAX_BITWIDTH = 13; +static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; #else -static const int SOC_ADC_RTC_MAX_BITWIDTH = 12; +static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; #endif #endif @@ -43,20 +46,27 @@ extern "C" ADCSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); #if !defined(USE_ADC_SENSOR_VCC) && !defined(USE_RP2040) - pin_->setup(); + this->pin_->setup(); #endif #ifdef USE_ESP32 - adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); - if (!autorange_) { - adc1_config_channel_atten(channel_, attenuation_); + if (this->channel1_ != ADC1_CHANNEL_MAX) { + adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); + if (!this->autorange_) { + adc1_config_channel_atten(this->channel1_, this->attenuation_); + } + } else if (this->channel2_ != ADC2_CHANNEL_MAX) { + if (!this->autorange_) { + adc2_config_channel_atten(this->channel2_, this->attenuation_); + } } // load characteristics for each attenuation - for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) { - auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, + for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { + auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; + auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, 1100, // default vref - &cal_characteristics_[i]); + &this->cal_characteristics_[i]); switch (cal_value) { case ESP_ADC_CAL_VAL_EFUSE_VREF: ESP_LOGV(TAG, "Using eFuse Vref for calibration"); @@ -85,44 +95,50 @@ extern "C" void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); -#ifdef USE_ESP8266 +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) #ifdef USE_ADC_SENSOR_VCC ESP_LOGCONFIG(TAG, " Pin: VCC"); #else - LOG_PIN(" Pin: ", pin_); + LOG_PIN(" Pin: ", this->pin_); #endif -#endif // USE_ESP8266 +#endif // USE_ESP8266 || USE_LIBRETINY #ifdef USE_ESP32 - LOG_PIN(" Pin: ", pin_); - if (autorange_) { - ESP_LOGCONFIG(TAG, " Attenuation: auto"); + LOG_PIN(" Pin: ", this->pin_); + if (this->autorange_) { + ESP_LOGCONFIG(TAG, " Attenuation: auto"); } else { switch (this->attenuation_) { case ADC_ATTEN_DB_0: - ESP_LOGCONFIG(TAG, " Attenuation: 0db"); + ESP_LOGCONFIG(TAG, " Attenuation: 0db"); break; case ADC_ATTEN_DB_2_5: - ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); + ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); break; case ADC_ATTEN_DB_6: - ESP_LOGCONFIG(TAG, " Attenuation: 6db"); + ESP_LOGCONFIG(TAG, " Attenuation: 6db"); break; - case ADC_ATTEN_DB_11: - ESP_LOGCONFIG(TAG, " Attenuation: 11db"); + case ADC_ATTEN_DB_12_COMPAT: + ESP_LOGCONFIG(TAG, " Attenuation: 12db"); break; default: // This is to satisfy the unused ADC_ATTEN_MAX break; } } #endif // USE_ESP32 + #ifdef USE_RP2040 if (this->is_temperature_) { ESP_LOGCONFIG(TAG, " Pin: Temperature"); } else { - LOG_PIN(" Pin: ", pin_); +#ifdef USE_ADC_SENSOR_VCC + ESP_LOGCONFIG(TAG, " Pin: VCC"); +#else + LOG_PIN(" Pin: ", this->pin_); +#endif // USE_ADC_SENSOR_VCC } -#endif +#endif // USE_RP2040 + ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); LOG_UPDATE_INTERVAL(this); } @@ -133,14 +149,24 @@ void ADCSensor::update() { this->publish_state(value_v); } +void ADCSensor::set_sample_count(uint8_t sample_count) { + if (sample_count != 0) { + this->sample_count_ = sample_count; + } +} + #ifdef USE_ESP8266 float ADCSensor::sample() { + uint32_t raw = 0; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { #ifdef USE_ADC_SENSOR_VCC - int raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) + raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) #else - int raw = analogRead(this->pin_->get_pin()); // NOLINT + raw += analogRead(this->pin_->get_pin()); // NOLINT #endif - if (output_raw_) { + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + if (this->output_raw_) { return raw; } return raw / 1024.0f; @@ -149,53 +175,81 @@ float ADCSensor::sample() { #ifdef USE_ESP32 float ADCSensor::sample() { - if (!autorange_) { - int raw = adc1_get_raw(channel_); - if (raw == -1) { - return NAN; + if (!this->autorange_) { + uint32_t sum = 0; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + int raw = -1; + if (this->channel1_ != ADC1_CHANNEL_MAX) { + raw = adc1_get_raw(this->channel1_); + } else if (this->channel2_ != ADC2_CHANNEL_MAX) { + adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); + } + if (raw == -1) { + return NAN; + } + sum += raw; } - if (output_raw_) { - return raw; + sum = (sum + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + if (this->output_raw_) { + return sum; } - uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]); + uint32_t mv = esp_adc_cal_raw_to_voltage(sum, &this->cal_characteristics_[(int32_t) this->attenuation_]); return mv / 1000.0f; } - int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); - raw11 = adc1_get_raw(channel_); - if (raw11 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); - raw6 = adc1_get_raw(channel_); - if (raw6 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); - raw2 = adc1_get_raw(channel_); - if (raw2 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); - raw0 = adc1_get_raw(channel_); + int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; + + if (this->channel1_ != ADC1_CHANNEL_MAX) { + adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); + raw12 = adc1_get_raw(this->channel1_); + if (raw12 < ADC_MAX) { + adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); + raw6 = adc1_get_raw(this->channel1_); + if (raw6 < ADC_MAX) { + adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); + raw2 = adc1_get_raw(this->channel1_); + if (raw2 < ADC_MAX) { + adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); + raw0 = adc1_get_raw(this->channel1_); + } + } + } + } else if (this->channel2_ != ADC2_CHANNEL_MAX) { + adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT); + adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); + if (raw12 < ADC_MAX) { + adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6); + adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); + if (raw6 < ADC_MAX) { + adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); + adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); + if (raw2 < ADC_MAX) { + adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); + adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); + } } } } - if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw11 == -1) { + if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw12 == -1) { return NAN; } - uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]); - uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int) ADC_ATTEN_DB_6]); - uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); - uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); + uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); + uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); + uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); + uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) - uint32_t c11 = std::min(raw11, ADC_HALF); + uint32_t c12 = std::min(raw12, ADC_HALF); uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); // max theoretical csum value is 4096*4 = 16384 - uint32_t csum = c11 + c6 + c2 + c0; + uint32_t csum = c12 + c6 + c2 + c0; // each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32 - uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); + uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); return mv_scaled / (float) (csum * 1000U); } #endif // USE_ESP32 @@ -206,23 +260,70 @@ float ADCSensor::sample() { adc_set_temp_sensor_enabled(true); delay(1); adc_select_input(4); + uint32_t raw = 0; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + raw += adc_read(); + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + adc_set_temp_sensor_enabled(false); + if (this->output_raw_) { + return raw; + } + return raw * 3.3f / 4096.0f; } else { uint8_t pin = this->pin_->get_pin(); +#ifdef CYW43_USES_VSYS_PIN + if (pin == PICO_VSYS_PIN) { + // Measuring VSYS on Raspberry Pico W needs to be wrapped with + // `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in + // https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and + // VSYS ADC both share GPIO29 + cyw43_thread_enter(); + } +#endif // CYW43_USES_VSYS_PIN + adc_gpio_init(pin); adc_select_input(pin - 26); - } - int raw = adc_read(); - if (this->is_temperature_) { - adc_set_temp_sensor_enabled(false); + uint32_t raw = 0; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + raw += adc_read(); + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + +#ifdef CYW43_USES_VSYS_PIN + if (pin == PICO_VSYS_PIN) { + cyw43_thread_exit(); + } +#endif // CYW43_USES_VSYS_PIN + + if (this->output_raw_) { + return raw; + } + float coeff = pin == PICO_VSYS_PIN ? 3.0 : 1.0; + return raw * 3.3f / 4096.0f * coeff; } - if (output_raw_) { - return raw; - } - return raw * 3.3f / 4096.0f; } #endif +#ifdef USE_LIBRETINY +float ADCSensor::sample() { + uint32_t raw = 0; + if (this->output_raw_) { + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + raw += analogRead(this->pin_->get_pin()); // NOLINT + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + return raw; + } + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + return raw / 1000.0f; +} +#endif // USE_LIBRETINY + #ifdef USE_ESP8266 std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } #endif diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 22cddde6f8..b697d6dd7e 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -1,37 +1,60 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/hal.h" -#include "esphome/core/defines.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" #ifdef USE_ESP32 -#include "driver/adc.h" #include +#include "driver/adc.h" #endif namespace esphome { namespace adc { +#ifdef USE_ESP32 +// clang-format off +#if (ESP_IDF_VERSION_MAJOR == 4 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 7)) || \ + (ESP_IDF_VERSION_MAJOR == 5 && \ + ((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \ + (ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \ + (ESP_IDF_VERSION_MINOR >= 2)) \ + ) +// clang-format on +static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_12; +#else +static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11; +#endif +#endif // USE_ESP32 + class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { public: #ifdef USE_ESP32 /// Set the attenuation for this pin. Only available on the ESP32. - void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; } - void set_channel(adc1_channel_t channel) { channel_ = channel; } - void set_autorange(bool autorange) { autorange_ = autorange; } + void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } + void set_channel1(adc1_channel_t channel) { + this->channel1_ = channel; + this->channel2_ = ADC2_CHANNEL_MAX; + } + void set_channel2(adc2_channel_t channel) { + this->channel2_ = channel; + this->channel1_ = ADC1_CHANNEL_MAX; + } + void set_autorange(bool autorange) { this->autorange_ = autorange; } #endif - /// Update adc values. + /// Update ADC values void update() override; - /// Setup ADc + /// Setup ADC void setup() override; void dump_config() override; - /// `HARDWARE_LATE` setup priority. + /// `HARDWARE_LATE` setup priority float get_setup_priority() const override; void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } - void set_output_raw(bool output_raw) { output_raw_ = output_raw; } + void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } + void set_sample_count(uint8_t sample_count); float sample() override; #ifdef USE_ESP8266 @@ -39,12 +62,13 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #endif #ifdef USE_RP2040 - void set_is_temperature() { is_temperature_ = true; } + void set_is_temperature() { this->is_temperature_ = true; } #endif protected: InternalGPIOPin *pin_; bool output_raw_{false}; + uint8_t sample_count_{1}; #ifdef USE_RP2040 bool is_temperature_{false}; @@ -52,9 +76,14 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #ifdef USE_ESP32 adc_atten_t attenuation_{ADC_ATTEN_DB_0}; - adc1_channel_t channel_{}; + adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; + adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; bool autorange_{false}; - esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {}; +#if ESP_IDF_VERSION_MAJOR >= 5 + esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; +#else + esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {}; +#endif #endif }; diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 4695e96570..59ea9e184c 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -1,5 +1,9 @@ +import logging + import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.core import CORE from esphome.components import sensor, voltage_sampler from esphome.components.esp32 import get_esp32_variant from esphome.const import ( @@ -8,28 +12,62 @@ from esphome.const import ( CONF_NUMBER, CONF_PIN, CONF_RAW, + CONF_WIFI, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) -from esphome.core import CORE - from . import ( ATTENUATION_MODES, ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, + ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, + adc_ns, validate_adc_pin, ) +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["voltage_sampler"] +CONF_SAMPLES = "samples" + + +_attenuation = cv.enum(ATTENUATION_MODES, lower=True) + def validate_config(config): if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": - raise cv.Invalid("Automatic attenuation cannot be used when raw output is set.") + raise cv.Invalid("Automatic attenuation cannot be used when raw output is set") + + if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1: + raise cv.Invalid( + "Automatic attenuation cannot be used when multisampling is set" + ) + if config.get(CONF_ATTENUATION) == "11db": + _LOGGER.warning( + "`attenuation: 11db` is deprecated, use `attenuation: 12db` instead" + ) + # Alter value here so `config` command prints the recommended change + config[CONF_ATTENUATION] = _attenuation("12db") + + return config + + +def final_validate_config(config): + if CORE.is_esp32: + variant = get_esp32_variant() + if ( + CONF_WIFI in fv.full_config.get() + and config[CONF_PIN][CONF_NUMBER] + in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] + ): + raise cv.Invalid( + f"{variant} doesn't support ADC on this pin when Wi-Fi is configured" + ) + return config -adc_ns = cg.esphome_ns.namespace("adc") ADCSensor = adc_ns.class_( "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) @@ -47,14 +85,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): validate_adc_pin, cv.Optional(CONF_RAW, default=False): cv.boolean, cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( - cv.only_on_esp32, cv.enum(ATTENUATION_MODES, lower=True) + cv.only_on_esp32, _attenuation ), + cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255), } ) .extend(cv.polling_component_schema("60s")), validate_config, ) +FINAL_VALIDATE_SCHEMA = final_validate_config + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -69,17 +110,27 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) - if CONF_RAW in config: - cg.add(var.set_output_raw(config[CONF_RAW])) + cg.add(var.set_output_raw(config[CONF_RAW])) + cg.add(var.set_sample_count(config[CONF_SAMPLES])) - if CONF_ATTENUATION in config: - if config[CONF_ATTENUATION] == "auto": + if attenuation := config.get(CONF_ATTENUATION): + if attenuation == "auto": cg.add(var.set_autorange(cg.global_ns.true)) else: - cg.add(var.set_attenuation(config[CONF_ATTENUATION])) + cg.add(var.set_attenuation(attenuation)) if CORE.is_esp32: variant = get_esp32_variant() pin_num = config[CONF_PIN][CONF_NUMBER] - chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] - cg.add(var.set_channel(chan)) + if ( + variant in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL + and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] + ): + chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_channel1(chan)) + elif ( + variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL + and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] + ): + chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_channel2(chan)) diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index 8893c39be6..f47389fd05 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -10,7 +10,7 @@ namespace esphome { namespace addressable_light { -class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent { +class AddressableLightDisplay : public display::DisplayBuffer { public: light::AddressableLight *get_light() const { return this->light_; } diff --git a/esphome/components/addressable_light/display.py b/esphome/components/addressable_light/display.py index 0684bf8dfc..327ec8296a 100644 --- a/esphome/components/addressable_light/display.py +++ b/esphome/components/addressable_light/display.py @@ -45,19 +45,18 @@ async def to_code(config): cg.add(var.set_height(config[CONF_HEIGHT])) cg.add(var.set_light(wrapped_light)) - await cg.register_component(var, config) await display.register_display(var, config) - if CONF_PIXEL_MAPPER in config: + if pixel_mapper := config.get(CONF_PIXEL_MAPPER): pixel_mapper_template_ = await cg.process_lambda( - config[CONF_PIXEL_MAPPER], + pixel_mapper, [(int, "x"), (int, "y")], return_type=cg.int_, ) cg.add(var.set_pixel_mapper(pixel_mapper_template_)) - if CONF_LAMBDA in config: + if lambda_config := config.get(CONF_LAMBDA): lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + lambda_config, [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ade7880/__init__.py b/esphome/components/ade7880/__init__.py new file mode 100644 index 0000000000..aed63c7dfa --- /dev/null +++ b/esphome/components/ade7880/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@kpfleming"] diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp new file mode 100644 index 0000000000..4a45b3b321 --- /dev/null +++ b/esphome/components/ade7880/ade7880.cpp @@ -0,0 +1,304 @@ +// This component was developed using knowledge gathered by a number +// of people who reverse-engineered the Shelly 3EM: +// +// @AndreKR on GitHub +// Axel (@Axel830 on GitHub) +// Marko (@goodkiller on GitHub) +// Michaël Piron (@michaelpiron on GitHub) +// Theo Arends (@arendst on GitHub) + +#include "ade7880.h" +#include "ade7880_registers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace ade7880 { + +static const char *const TAG = "ade7880"; + +void IRAM_ATTR ADE7880Store::gpio_intr(ADE7880Store *arg) { arg->reset_done = true; } + +void ADE7880::setup() { + if (this->irq0_pin_ != nullptr) { + this->irq0_pin_->setup(); + } + this->irq1_pin_->setup(); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + } + this->store_.irq1_pin = this->irq1_pin_->to_isr(); + this->irq1_pin_->attach_interrupt(ADE7880Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + + // if IRQ1 is already asserted, the cause must be determined + if (this->irq1_pin_->digital_read() == 0) { + ESP_LOGD(TAG, "IRQ1 found asserted during setup()"); + auto status1 = read_u32_register16_(STATUS1); + if ((status1 & ~STATUS1_RSTDONE) != 0) { + // not safe to proceed, must initiate reset + ESP_LOGD(TAG, "IRQ1 asserted for !RSTDONE, resetting device"); + this->reset_device_(); + return; + } + if ((status1 & STATUS1_RSTDONE) == STATUS1_RSTDONE) { + // safe to proceed, device has just completed reset cycle + ESP_LOGD(TAG, "Acknowledging RSTDONE"); + this->write_u32_register16_(STATUS0, 0xFFFF); + this->write_u32_register16_(STATUS1, 0xFFFF); + this->init_device_(); + return; + } + } + + this->reset_device_(); +} + +void ADE7880::loop() { + // check for completion of a reset cycle + if (!this->store_.reset_done) { + return; + } + + ESP_LOGD(TAG, "Acknowledging RSTDONE"); + this->write_u32_register16_(STATUS0, 0xFFFF); + this->write_u32_register16_(STATUS1, 0xFFFF); + this->init_device_(); + this->store_.reset_done = false; + this->store_.reset_pending = false; +} + +template +void ADE7880::update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) { + if (sensor == nullptr) { + return; + } + + float val = this->read_s24zp_register16_(a_register); + sensor->publish_state(f(val)); +} + +template +void ADE7880::update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) { + if (sensor == nullptr) { + return; + } + + float val = this->read_s16_register16_(a_register); + sensor->publish_state(f(val)); +} + +template +void ADE7880::update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) { + if (sensor == nullptr) { + return; + } + + float val = this->read_s32_register16_(a_register); + sensor->publish_state(f(val)); +} + +void ADE7880::update() { + if (this->store_.reset_pending) { + return; + } + + auto start = millis(); + + if (this->channel_n_ != nullptr) { + auto *chan = this->channel_n_; + this->update_sensor_from_s24zp_register16_(chan->current, NIRMS, [](float val) { return val / 100000.0f; }); + } + + if (this->channel_a_ != nullptr) { + auto *chan = this->channel_a_; + this->update_sensor_from_s24zp_register16_(chan->current, AIRMS, [](float val) { return val / 100000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->active_power, AWATT, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s16_register16_(chan->power_factor, APF, + [](float val) { return std::abs(val / -327.68f); }); + this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) { + return chan->forward_active_energy_total += val / 14400.0f; + }); + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) { + return chan->reverse_active_energy_total += val / 14400.0f; + }); + } + + if (this->channel_b_ != nullptr) { + auto *chan = this->channel_b_; + this->update_sensor_from_s24zp_register16_(chan->current, BIRMS, [](float val) { return val / 100000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->active_power, BWATT, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s16_register16_(chan->power_factor, BPF, + [](float val) { return std::abs(val / -327.68f); }); + this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) { + return chan->forward_active_energy_total += val / 14400.0f; + }); + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) { + return chan->reverse_active_energy_total += val / 14400.0f; + }); + } + + if (this->channel_c_ != nullptr) { + auto *chan = this->channel_c_; + this->update_sensor_from_s24zp_register16_(chan->current, CIRMS, [](float val) { return val / 100000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->voltage, CVRMS, [](float val) { return val / 10000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->active_power, CWATT, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; }); + this->update_sensor_from_s16_register16_(chan->power_factor, CPF, + [](float val) { return std::abs(val / -327.68f); }); + this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) { + return chan->forward_active_energy_total += val / 14400.0f; + }); + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) { + return chan->reverse_active_energy_total += val / 14400.0f; + }); + } + + ESP_LOGD(TAG, "update took %" PRIu32 " ms", millis() - start); +} + +void ADE7880::dump_config() { + ESP_LOGCONFIG(TAG, "ADE7880:"); + 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:"); + LOG_SENSOR(" ", "Current", this->channel_a_->current); + LOG_SENSOR(" ", "Voltage", this->channel_a_->voltage); + LOG_SENSOR(" ", "Active Power", this->channel_a_->active_power); + LOG_SENSOR(" ", "Apparent Power", this->channel_a_->apparent_power); + LOG_SENSOR(" ", "Power Factor", this->channel_a_->power_factor); + LOG_SENSOR(" ", "Forward Active Energy", this->channel_a_->forward_active_energy); + LOG_SENSOR(" ", "Reverse Active Energy", this->channel_a_->reverse_active_energy); + ESP_LOGCONFIG(TAG, " Calibration:"); + ESP_LOGCONFIG(TAG, " Current: %" PRId32, this->channel_a_->current_gain_calibration); + ESP_LOGCONFIG(TAG, " Voltage: %" PRId32, this->channel_a_->voltage_gain_calibration); + ESP_LOGCONFIG(TAG, " Power: %" PRId32, this->channel_a_->power_gain_calibration); + ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_a_->phase_angle_calibration); + } + + if (this->channel_b_ != nullptr) { + ESP_LOGCONFIG(TAG, " Phase B:"); + LOG_SENSOR(" ", "Current", this->channel_b_->current); + LOG_SENSOR(" ", "Voltage", this->channel_b_->voltage); + LOG_SENSOR(" ", "Active Power", this->channel_b_->active_power); + LOG_SENSOR(" ", "Apparent Power", this->channel_b_->apparent_power); + LOG_SENSOR(" ", "Power Factor", this->channel_b_->power_factor); + LOG_SENSOR(" ", "Forward Active Energy", this->channel_b_->forward_active_energy); + LOG_SENSOR(" ", "Reverse Active Energy", this->channel_b_->reverse_active_energy); + ESP_LOGCONFIG(TAG, " Calibration:"); + ESP_LOGCONFIG(TAG, " Current: %" PRId32, this->channel_b_->current_gain_calibration); + ESP_LOGCONFIG(TAG, " Voltage: %" PRId32, this->channel_b_->voltage_gain_calibration); + ESP_LOGCONFIG(TAG, " Power: %" PRId32, this->channel_b_->power_gain_calibration); + ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_b_->phase_angle_calibration); + } + + if (this->channel_c_ != nullptr) { + ESP_LOGCONFIG(TAG, " Phase C:"); + LOG_SENSOR(" ", "Current", this->channel_c_->current); + LOG_SENSOR(" ", "Voltage", this->channel_c_->voltage); + LOG_SENSOR(" ", "Active Power", this->channel_c_->active_power); + LOG_SENSOR(" ", "Apparent Power", this->channel_c_->apparent_power); + LOG_SENSOR(" ", "Power Factor", this->channel_c_->power_factor); + LOG_SENSOR(" ", "Forward Active Energy", this->channel_c_->forward_active_energy); + LOG_SENSOR(" ", "Reverse Active Energy", this->channel_c_->reverse_active_energy); + ESP_LOGCONFIG(TAG, " Calibration:"); + ESP_LOGCONFIG(TAG, " Current: %" PRId32, this->channel_c_->current_gain_calibration); + ESP_LOGCONFIG(TAG, " Voltage: %" PRId32, this->channel_c_->voltage_gain_calibration); + ESP_LOGCONFIG(TAG, " Power: %" PRId32, this->channel_c_->power_gain_calibration); + ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_c_->phase_angle_calibration); + } + + if (this->channel_n_ != nullptr) { + ESP_LOGCONFIG(TAG, " Neutral:"); + LOG_SENSOR(" ", "Current", this->channel_n_->current); + ESP_LOGCONFIG(TAG, " Calibration:"); + ESP_LOGCONFIG(TAG, " Current: %" PRId32, this->channel_n_->current_gain_calibration); + } + + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); +} + +void ADE7880::calibrate_s10zp_reading_(uint16_t a_register, int16_t calibration) { + if (calibration == 0) { + return; + } + + this->write_s10zp_register16_(a_register, calibration); +} + +void ADE7880::calibrate_s24zpse_reading_(uint16_t a_register, int32_t calibration) { + if (calibration == 0) { + return; + } + + this->write_s24zpse_register16_(a_register, calibration); +} + +void ADE7880::init_device_() { + this->write_u8_register16_(CONFIG2, CONFIG2_I2C_LOCK); + + this->write_u16_register16_(GAIN, 0); + + if (this->frequency_ > 55) { + this->write_u16_register16_(COMPMODE, COMPMODE_DEFAULT | COMPMODE_SELFREQ); + } + + if (this->channel_n_ != nullptr) { + this->calibrate_s24zpse_reading_(NIGAIN, this->channel_n_->current_gain_calibration); + } + + if (this->channel_a_ != nullptr) { + this->calibrate_s24zpse_reading_(AIGAIN, this->channel_a_->current_gain_calibration); + this->calibrate_s24zpse_reading_(AVGAIN, this->channel_a_->voltage_gain_calibration); + this->calibrate_s24zpse_reading_(APGAIN, this->channel_a_->power_gain_calibration); + this->calibrate_s10zp_reading_(APHCAL, this->channel_a_->phase_angle_calibration); + } + + if (this->channel_b_ != nullptr) { + this->calibrate_s24zpse_reading_(BIGAIN, this->channel_b_->current_gain_calibration); + this->calibrate_s24zpse_reading_(BVGAIN, this->channel_b_->voltage_gain_calibration); + this->calibrate_s24zpse_reading_(BPGAIN, this->channel_b_->power_gain_calibration); + this->calibrate_s10zp_reading_(BPHCAL, this->channel_b_->phase_angle_calibration); + } + + if (this->channel_c_ != nullptr) { + this->calibrate_s24zpse_reading_(CIGAIN, this->channel_c_->current_gain_calibration); + this->calibrate_s24zpse_reading_(CVGAIN, this->channel_c_->voltage_gain_calibration); + this->calibrate_s24zpse_reading_(CPGAIN, this->channel_c_->power_gain_calibration); + this->calibrate_s10zp_reading_(CPHCAL, this->channel_c_->phase_angle_calibration); + } + + // write three default values to data memory RAM to flush the I2C write queue + this->write_s32_register16_(VLEVEL, 0); + this->write_s32_register16_(VLEVEL, 0); + this->write_s32_register16_(VLEVEL, 0); + + this->write_u8_register16_(DSPWP_SEL, DSPWP_SEL_SET); + this->write_u8_register16_(DSPWP_SET, DSPWP_SET_RO); + this->write_u16_register16_(RUN, RUN_ENABLE); +} + +void ADE7880::reset_device_() { + if (this->reset_pin_ != nullptr) { + ESP_LOGD(TAG, "Reset device using RESET pin"); + this->reset_pin_->digital_write(false); + delay(1); + this->reset_pin_->digital_write(true); + } else { + ESP_LOGD(TAG, "Reset device using SWRST command"); + this->write_u16_register16_(CONFIG, CONFIG_SWRST); + } + this->store_.reset_pending = true; +} + +} // namespace ade7880 +} // namespace esphome diff --git a/esphome/components/ade7880/ade7880.h b/esphome/components/ade7880/ade7880.h new file mode 100644 index 0000000000..a565357dc5 --- /dev/null +++ b/esphome/components/ade7880/ade7880.h @@ -0,0 +1,131 @@ +#pragma once + +// This component was developed using knowledge gathered by a number +// of people who reverse-engineered the Shelly 3EM: +// +// @AndreKR on GitHub +// Axel (@Axel830 on GitHub) +// Marko (@goodkiller on GitHub) +// Michaël Piron (@michaelpiron on GitHub) +// Theo Arends (@arendst on GitHub) + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +#include "ade7880_registers.h" + +namespace esphome { +namespace ade7880 { + +struct NeutralChannel { + void set_current(sensor::Sensor *sens) { this->current = sens; } + + void set_current_gain_calibration(int32_t val) { this->current_gain_calibration = val; } + + sensor::Sensor *current{nullptr}; + int32_t current_gain_calibration{0}; +}; + +struct PowerChannel { + void set_current(sensor::Sensor *sens) { this->current = sens; } + void set_voltage(sensor::Sensor *sens) { this->voltage = sens; } + void set_active_power(sensor::Sensor *sens) { this->active_power = sens; } + void set_apparent_power(sensor::Sensor *sens) { this->apparent_power = sens; } + void set_power_factor(sensor::Sensor *sens) { this->power_factor = sens; } + void set_forward_active_energy(sensor::Sensor *sens) { this->forward_active_energy = sens; } + void set_reverse_active_energy(sensor::Sensor *sens) { this->reverse_active_energy = sens; } + + void set_current_gain_calibration(int32_t val) { this->current_gain_calibration = val; } + void set_voltage_gain_calibration(int32_t val) { this->voltage_gain_calibration = val; } + void set_power_gain_calibration(int32_t val) { this->power_gain_calibration = val; } + void set_phase_angle_calibration(int32_t val) { this->phase_angle_calibration = val; } + + sensor::Sensor *current{nullptr}; + sensor::Sensor *voltage{nullptr}; + sensor::Sensor *active_power{nullptr}; + sensor::Sensor *apparent_power{nullptr}; + sensor::Sensor *power_factor{nullptr}; + sensor::Sensor *forward_active_energy{nullptr}; + sensor::Sensor *reverse_active_energy{nullptr}; + int32_t current_gain_calibration{0}; + int32_t voltage_gain_calibration{0}; + int32_t power_gain_calibration{0}; + uint16_t phase_angle_calibration{0}; + float forward_active_energy_total{0}; + float reverse_active_energy_total{0}; +}; + +// Store data in a class that doesn't use multiple-inheritance (no vtables in flash!) +struct ADE7880Store { + volatile bool reset_done{false}; + bool reset_pending{false}; + ISRInternalGPIOPin irq1_pin; + + static void gpio_intr(ADE7880Store *arg); +}; + +class ADE7880 : public i2c::I2CDevice, public PollingComponent { + public: + void set_irq0_pin(InternalGPIOPin *pin) { this->irq0_pin_ = pin; } + void set_irq1_pin(InternalGPIOPin *pin) { this->irq1_pin_ = pin; } + void set_reset_pin(InternalGPIOPin *pin) { this->reset_pin_ = pin; } + void set_frequency(float frequency) { this->frequency_ = frequency; } + void set_channel_n(NeutralChannel *channel) { this->channel_n_ = channel; } + void set_channel_a(PowerChannel *channel) { this->channel_a_ = channel; } + void set_channel_b(PowerChannel *channel) { this->channel_b_ = channel; } + void set_channel_c(PowerChannel *channel) { this->channel_c_ = channel; } + + void setup() override; + + void loop() override; + + void update() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + ADE7880Store store_{}; + InternalGPIOPin *irq0_pin_{nullptr}; + InternalGPIOPin *irq1_pin_{nullptr}; + InternalGPIOPin *reset_pin_{nullptr}; + float frequency_; + NeutralChannel *channel_n_{nullptr}; + PowerChannel *channel_a_{nullptr}; + PowerChannel *channel_b_{nullptr}; + PowerChannel *channel_c_{nullptr}; + + void calibrate_s10zp_reading_(uint16_t a_register, int16_t calibration); + void calibrate_s24zpse_reading_(uint16_t a_register, int32_t calibration); + + void init_device_(); + + // each of these functions allow the caller to pass in a lambda (or any other callable) + // which modifies the value read from the register before it is passed to the sensor + // the callable will be passed a 'float' value and is expected to return a 'float' + template void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f); + template void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f); + template void update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f); + + void reset_device_(); + + uint8_t read_u8_register16_(uint16_t a_register); + int16_t read_s16_register16_(uint16_t a_register); + uint16_t read_u16_register16_(uint16_t a_register); + int32_t read_s24zp_register16_(uint16_t a_register); + int32_t read_s32_register16_(uint16_t a_register); + uint32_t read_u32_register16_(uint16_t a_register); + + void write_u8_register16_(uint16_t a_register, uint8_t value); + void write_s10zp_register16_(uint16_t a_register, int16_t value); + void write_u16_register16_(uint16_t a_register, uint16_t value); + void write_s24zpse_register16_(uint16_t a_register, int32_t value); + void write_s32_register16_(uint16_t a_register, int32_t value); + void write_u32_register16_(uint16_t a_register, uint32_t value); +}; + +} // namespace ade7880 +} // namespace esphome diff --git a/esphome/components/ade7880/ade7880_i2c.cpp b/esphome/components/ade7880/ade7880_i2c.cpp new file mode 100644 index 0000000000..fae20f175d --- /dev/null +++ b/esphome/components/ade7880/ade7880_i2c.cpp @@ -0,0 +1,101 @@ +// This component was developed using knowledge gathered by a number +// of people who reverse-engineered the Shelly 3EM: +// +// @AndreKR on GitHub +// Axel (@Axel830 on GitHub) +// Marko (@goodkiller on GitHub) +// Michaël Piron (@michaelpiron on GitHub) +// Theo Arends (@arendst on GitHub) + +#include "ade7880.h" + +namespace esphome { +namespace ade7880 { + +// adapted from https://stackoverflow.com/a/55912127/1886371 +template inline T sign_extend(const T &v) noexcept { + using S = struct { signed Val : Bits; }; + return reinterpret_cast(&v)->Val; +} + +// Register types +// unsigned 8-bit (uint8_t) +// signed 10-bit - 16-bit ZP on wire (int16_t, needs sign extension) +// unsigned 16-bit (uint16_t) +// unsigned 20-bit - 32-bit ZP on wire (uint32_t) +// signed 24-bit - 32-bit ZPSE on wire (int32_t, needs sign extension) +// signed 24-bit - 32-bit ZP on wire (int32_t, needs sign extension) +// signed 24-bit - 32-bit SE on wire (int32_t) +// signed 28-bit - 32-bit ZP on wire (int32_t, needs sign extension) +// unsigned 32-bit (uint32_t) +// signed 32-bit (int32_t) + +uint8_t ADE7880::read_u8_register16_(uint16_t a_register) { + uint8_t in; + this->read_register16(a_register, &in, sizeof(in)); + return in; +} + +int16_t ADE7880::read_s16_register16_(uint16_t a_register) { + int16_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return convert_big_endian(in); +} + +uint16_t ADE7880::read_u16_register16_(uint16_t a_register) { + uint16_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return convert_big_endian(in); +} + +int32_t ADE7880::read_s24zp_register16_(uint16_t a_register) { + // s24zp means 24 bit signed value in the lower 24 bits of a 32-bit register + int32_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return sign_extend<24>(convert_big_endian(in)); +} + +int32_t ADE7880::read_s32_register16_(uint16_t a_register) { + int32_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return convert_big_endian(in); +} + +uint32_t ADE7880::read_u32_register16_(uint16_t a_register) { + uint32_t in; + this->read_register16(a_register, reinterpret_cast(&in), sizeof(in)); + return convert_big_endian(in); +} + +void ADE7880::write_u8_register16_(uint16_t a_register, uint8_t value) { + this->write_register16(a_register, &value, sizeof(value)); +} + +void ADE7880::write_s10zp_register16_(uint16_t a_register, int16_t value) { + int16_t out = convert_big_endian(value & 0x03FF); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +void ADE7880::write_u16_register16_(uint16_t a_register, uint16_t value) { + uint16_t out = convert_big_endian(value); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +void ADE7880::write_s24zpse_register16_(uint16_t a_register, int32_t value) { + // s24zpse means a 24-bit signed value, sign-extended to 28 bits, in the lower 28 bits of a 32-bit register + int32_t out = convert_big_endian(value & 0x0FFFFFFF); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +void ADE7880::write_s32_register16_(uint16_t a_register, int32_t value) { + int32_t out = convert_big_endian(value); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +void ADE7880::write_u32_register16_(uint16_t a_register, uint32_t value) { + uint32_t out = convert_big_endian(value); + this->write_register16(a_register, reinterpret_cast(&out), sizeof(out)); +} + +} // namespace ade7880 +} // namespace esphome diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h new file mode 100644 index 0000000000..8b5b68abb0 --- /dev/null +++ b/esphome/components/ade7880/ade7880_registers.h @@ -0,0 +1,243 @@ +#pragma once + +// This file is a modified version of the one created by Michaël Piron (@michaelpiron on GitHub) + +// Source: https://www.analog.com/media/en/technical-documentation/application-notes/AN-1127.pdf + +namespace esphome { +namespace ade7880 { + +// DSP Data Memory RAM registers +constexpr uint16_t AIGAIN = 0x4380; +constexpr uint16_t AVGAIN = 0x4381; +constexpr uint16_t BIGAIN = 0x4382; +constexpr uint16_t BVGAIN = 0x4383; +constexpr uint16_t CIGAIN = 0x4384; +constexpr uint16_t CVGAIN = 0x4385; +constexpr uint16_t NIGAIN = 0x4386; + +constexpr uint16_t DICOEFF = 0x4388; + +constexpr uint16_t APGAIN = 0x4389; +constexpr uint16_t AWATTOS = 0x438A; +constexpr uint16_t BPGAIN = 0x438B; +constexpr uint16_t BWATTOS = 0x438C; +constexpr uint16_t CPGAIN = 0x438D; +constexpr uint16_t CWATTOS = 0x438E; +constexpr uint16_t AIRMSOS = 0x438F; +constexpr uint16_t AVRMSOS = 0x4390; +constexpr uint16_t BIRMSOS = 0x4391; +constexpr uint16_t BVRMSOS = 0x4392; +constexpr uint16_t CIRMSOS = 0x4393; +constexpr uint16_t CVRMSOS = 0x4394; +constexpr uint16_t NIRMSOS = 0x4395; +constexpr uint16_t HPGAIN = 0x4398; +constexpr uint16_t ISUMLVL = 0x4399; + +constexpr uint16_t VLEVEL = 0x439F; + +constexpr uint16_t AFWATTOS = 0x43A2; +constexpr uint16_t BFWATTOS = 0x43A3; +constexpr uint16_t CFWATTOS = 0x43A4; + +constexpr uint16_t AFVAROS = 0x43A5; +constexpr uint16_t BFVAROS = 0x43A6; +constexpr uint16_t CFVAROS = 0x43A7; + +constexpr uint16_t AFIRMSOS = 0x43A8; +constexpr uint16_t BFIRMSOS = 0x43A9; +constexpr uint16_t CFIRMSOS = 0x43AA; + +constexpr uint16_t AFVRMSOS = 0x43AB; +constexpr uint16_t BFVRMSOS = 0x43AC; +constexpr uint16_t CFVRMSOS = 0x43AD; + +constexpr uint16_t HXWATTOS = 0x43AE; +constexpr uint16_t HYWATTOS = 0x43AF; +constexpr uint16_t HZWATTOS = 0x43B0; +constexpr uint16_t HXVAROS = 0x43B1; +constexpr uint16_t HYVAROS = 0x43B2; +constexpr uint16_t HZVAROS = 0x43B3; + +constexpr uint16_t HXIRMSOS = 0x43B4; +constexpr uint16_t HYIRMSOS = 0x43B5; +constexpr uint16_t HZIRMSOS = 0x43B6; +constexpr uint16_t HXVRMSOS = 0x43B7; +constexpr uint16_t HYVRMSOS = 0x43B8; +constexpr uint16_t HZVRMSOS = 0x43B9; + +constexpr uint16_t AIRMS = 0x43C0; +constexpr uint16_t AVRMS = 0x43C1; +constexpr uint16_t BIRMS = 0x43C2; +constexpr uint16_t BVRMS = 0x43C3; +constexpr uint16_t CIRMS = 0x43C4; +constexpr uint16_t CVRMS = 0x43C5; +constexpr uint16_t NIRMS = 0x43C6; + +constexpr uint16_t ISUM = 0x43C7; + +// Internal DSP Memory RAM registers +constexpr uint16_t RUN = 0xE228; + +constexpr uint16_t AWATTHR = 0xE400; +constexpr uint16_t BWATTHR = 0xE401; +constexpr uint16_t CWATTHR = 0xE402; +constexpr uint16_t AFWATTHR = 0xE403; +constexpr uint16_t BFWATTHR = 0xE404; +constexpr uint16_t CFWATTHR = 0xE405; +constexpr uint16_t AFVARHR = 0xE409; +constexpr uint16_t BFVARHR = 0xE40A; +constexpr uint16_t CFVARHR = 0xE40B; + +constexpr uint16_t AVAHR = 0xE40C; +constexpr uint16_t BVAHR = 0xE40D; +constexpr uint16_t CVAHR = 0xE40E; + +constexpr uint16_t IPEAK = 0xE500; +constexpr uint16_t VPEAK = 0xE501; + +constexpr uint16_t STATUS0 = 0xE502; +constexpr uint16_t STATUS1 = 0xE503; + +constexpr uint16_t AIMAV = 0xE504; +constexpr uint16_t BIMAV = 0xE505; +constexpr uint16_t CIMAV = 0xE506; + +constexpr uint16_t OILVL = 0xE507; +constexpr uint16_t OVLVL = 0xE508; +constexpr uint16_t SAGLVL = 0xE509; +constexpr uint16_t MASK0 = 0xE50A; +constexpr uint16_t MASK1 = 0xE50B; + +constexpr uint16_t IAWV = 0xE50C; +constexpr uint16_t IBWV = 0xE50D; +constexpr uint16_t ICWV = 0xE50E; +constexpr uint16_t INWV = 0xE50F; +constexpr uint16_t VAWV = 0xE510; +constexpr uint16_t VBWV = 0xE511; +constexpr uint16_t VCWV = 0xE512; + +constexpr uint16_t AWATT = 0xE513; +constexpr uint16_t BWATT = 0xE514; +constexpr uint16_t CWATT = 0xE515; + +constexpr uint16_t AFVAR = 0xE516; +constexpr uint16_t BFVAR = 0xE517; +constexpr uint16_t CFVAR = 0xE518; + +constexpr uint16_t AVA = 0xE519; +constexpr uint16_t BVA = 0xE51A; +constexpr uint16_t CVA = 0xE51B; + +constexpr uint16_t CHECKSUM = 0xE51F; +constexpr uint16_t VNOM = 0xE520; +constexpr uint16_t LAST_RWDATA_24BIT = 0xE5FF; +constexpr uint16_t PHSTATUS = 0xE600; +constexpr uint16_t ANGLE0 = 0xE601; +constexpr uint16_t ANGLE1 = 0xE602; +constexpr uint16_t ANGLE2 = 0xE603; +constexpr uint16_t PHNOLOAD = 0xE608; +constexpr uint16_t LINECYC = 0xE60C; +constexpr uint16_t ZXTOUT = 0xE60D; +constexpr uint16_t COMPMODE = 0xE60E; +constexpr uint16_t GAIN = 0xE60F; +constexpr uint16_t CFMODE = 0xE610; +constexpr uint16_t CF1DEN = 0xE611; +constexpr uint16_t CF2DEN = 0xE612; +constexpr uint16_t CF3DEN = 0xE613; +constexpr uint16_t APHCAL = 0xE614; +constexpr uint16_t BPHCAL = 0xE615; +constexpr uint16_t CPHCAL = 0xE616; +constexpr uint16_t PHSIGN = 0xE617; +constexpr uint16_t CONFIG = 0xE618; +constexpr uint16_t MMODE = 0xE700; +constexpr uint16_t ACCMODE = 0xE701; +constexpr uint16_t LCYCMODE = 0xE702; +constexpr uint16_t PEAKCYC = 0xE703; +constexpr uint16_t SAGCYC = 0xE704; +constexpr uint16_t CFCYC = 0xE705; +constexpr uint16_t HSDC_CFG = 0xE706; +constexpr uint16_t VERSION = 0xE707; +constexpr uint16_t DSPWP_SET = 0xE7E3; +constexpr uint16_t LAST_RWDATA_8BIT = 0xE7FD; +constexpr uint16_t DSPWP_SEL = 0xE7FE; +constexpr uint16_t FVRMS = 0xE880; +constexpr uint16_t FIRMS = 0xE881; +constexpr uint16_t FWATT = 0xE882; +constexpr uint16_t FVAR = 0xE883; +constexpr uint16_t FVA = 0xE884; +constexpr uint16_t FPF = 0xE885; +constexpr uint16_t VTHDN = 0xE886; +constexpr uint16_t ITHDN = 0xE887; +constexpr uint16_t HXVRMS = 0xE888; +constexpr uint16_t HXIRMS = 0xE889; +constexpr uint16_t HXWATT = 0xE88A; +constexpr uint16_t HXVAR = 0xE88B; +constexpr uint16_t HXVA = 0xE88C; +constexpr uint16_t HXPF = 0xE88D; +constexpr uint16_t HXVHD = 0xE88E; +constexpr uint16_t HXIHD = 0xE88F; +constexpr uint16_t HYVRMS = 0xE890; +constexpr uint16_t HYIRMS = 0xE891; +constexpr uint16_t HYWATT = 0xE892; +constexpr uint16_t HYVAR = 0xE893; +constexpr uint16_t HYVA = 0xE894; +constexpr uint16_t HYPF = 0xE895; +constexpr uint16_t HYVHD = 0xE896; +constexpr uint16_t HYIHD = 0xE897; +constexpr uint16_t HZVRMS = 0xE898; +constexpr uint16_t HZIRMS = 0xE899; +constexpr uint16_t HZWATT = 0xE89A; +constexpr uint16_t HZVAR = 0xE89B; +constexpr uint16_t HZVA = 0xE89C; +constexpr uint16_t HZPF = 0xE89D; +constexpr uint16_t HZVHD = 0xE89E; +constexpr uint16_t HZIHD = 0xE89F; +constexpr uint16_t HCONFIG = 0xE900; +constexpr uint16_t APF = 0xE902; +constexpr uint16_t BPF = 0xE903; +constexpr uint16_t CPF = 0xE904; +constexpr uint16_t APERIOD = 0xE905; +constexpr uint16_t BPERIOD = 0xE906; +constexpr uint16_t CPERIOD = 0xE907; +constexpr uint16_t APNOLOAD = 0xE908; +constexpr uint16_t VARNOLOAD = 0xE909; +constexpr uint16_t VANOLOAD = 0xE90A; +constexpr uint16_t LAST_ADD = 0xE9FE; +constexpr uint16_t LAST_RWDATA_16BIT = 0xE9FF; +constexpr uint16_t CONFIG3 = 0xEA00; +constexpr uint16_t LAST_OP = 0xEA01; +constexpr uint16_t WTHR = 0xEA02; +constexpr uint16_t VARTHR = 0xEA03; +constexpr uint16_t VATHR = 0xEA04; + +constexpr uint16_t HX_REG = 0xEA08; +constexpr uint16_t HY_REG = 0xEA09; +constexpr uint16_t HZ_REG = 0xEA0A; +constexpr uint16_t LPOILVL = 0xEC00; +constexpr uint16_t CONFIG2 = 0xEC01; + +// STATUS1 Register Bits +constexpr uint32_t STATUS1_RSTDONE = (1 << 15); + +// CONFIG Register Bits +constexpr uint16_t CONFIG_SWRST = (1 << 7); + +// CONFIG2 Register Bits +constexpr uint8_t CONFIG2_I2C_LOCK = (1 << 1); + +// COMPMODE Register Bits +constexpr uint16_t COMPMODE_DEFAULT = 0x01FF; +constexpr uint16_t COMPMODE_SELFREQ = (1 << 14); + +// RUN Register Bits +constexpr uint16_t RUN_ENABLE = (1 << 0); + +// DSPWP_SET Register Bits +constexpr uint8_t DSPWP_SET_RO = (1 << 7); + +// DSPWP_SEL Register Bits +constexpr uint8_t DSPWP_SEL_SET = 0xAD; + +} // namespace ade7880 +} // namespace esphome diff --git a/esphome/components/ade7880/sensor.py b/esphome/components/ade7880/sensor.py new file mode 100644 index 0000000000..e075adb04c --- /dev/null +++ b/esphome/components/ade7880/sensor.py @@ -0,0 +1,290 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, i2c +from esphome import pins +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_CALIBRATION, + CONF_CURRENT, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_NAME, + CONF_PHASE_A, + CONF_PHASE_ANGLE, + CONF_PHASE_B, + CONF_PHASE_C, + CONF_POWER_FACTOR, + CONF_RESET_PIN, + CONF_REVERSE_ACTIVE_ENERGY, + CONF_VOLTAGE, + CONF_VOLTAGE_GAIN, + DEVICE_CLASS_APPARENT_POWER, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_PERCENT, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE_HOURS, + UNIT_WATT, + UNIT_WATT_HOURS, +) + +DEPENDENCIES = ["i2c"] + +ade7880_ns = cg.esphome_ns.namespace("ade7880") +ADE7880 = ade7880_ns.class_("ADE7880", cg.PollingComponent, i2c.I2CDevice) +NeutralChannel = ade7880_ns.struct("NeutralChannel") +PowerChannel = ade7880_ns.struct("PowerChannel") + +CONF_CURRENT_GAIN = "current_gain" +CONF_IRQ0_PIN = "irq0_pin" +CONF_IRQ1_PIN = "irq1_pin" +CONF_POWER_GAIN = "power_gain" + +CONF_NEUTRAL = "neutral" + +NEUTRAL_CHANNEL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(NeutralChannel), + cv.Optional(CONF_NAME): cv.string_strict, + cv.Required(CONF_CURRENT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Required(CONF_CALIBRATION): cv.Schema( + { + cv.Required(CONF_CURRENT_GAIN): cv.int_, + }, + ), + } +) + +POWER_CHANNEL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PowerChannel), + cv.Optional(CONF_NAME): cv.string_strict, + cv.Optional(CONF_VOLTAGE): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_CURRENT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTIVE_POWER): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_APPARENT_POWER): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_POWER_FACTOR): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + key=CONF_NAME, + ), + cv.Required(CONF_CALIBRATION): cv.Schema( + { + cv.Required(CONF_CURRENT_GAIN): cv.int_, + cv.Required(CONF_VOLTAGE_GAIN): cv.int_, + cv.Required(CONF_POWER_GAIN): cv.int_, + cv.Required(CONF_PHASE_ANGLE): cv.int_, + }, + ), + } +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + 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.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_PHASE_A): POWER_CHANNEL_SCHEMA, + cv.Optional(CONF_PHASE_B): POWER_CHANNEL_SCHEMA, + cv.Optional(CONF_PHASE_C): POWER_CHANNEL_SCHEMA, + cv.Optional(CONF_NEUTRAL): NEUTRAL_CHANNEL_SCHEMA, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x38)) +) + + +async def neutral_channel(config): + var = cg.new_Pvariable(config[CONF_ID]) + + current = config[CONF_CURRENT] + sens = await sensor.new_sensor(current) + cg.add(var.set_current(sens)) + + cg.add( + var.set_current_gain_calibration(config[CONF_CALIBRATION][CONF_CURRENT_GAIN]) + ) + + return var + + +async def power_channel(config): + var = cg.new_Pvariable(config[CONF_ID]) + + for sensor_type in [ + CONF_CURRENT, + CONF_VOLTAGE, + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_POWER_FACTOR, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, + ]: + if conf := config.get(sensor_type): + sens = await sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{sensor_type}")(sens)) + + for calib_type in [ + CONF_CURRENT_GAIN, + CONF_VOLTAGE_GAIN, + CONF_POWER_GAIN, + CONF_PHASE_ANGLE, + ]: + cg.add( + getattr(var, f"set_{calib_type}_calibration")( + config[CONF_CALIBRATION][calib_type] + ) + ) + + return var + + +def final_validate(config): + for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]: + if channel := config.get(channel): + channel_name = channel.get(CONF_NAME) + + for sensor_type in [ + CONF_CURRENT, + CONF_VOLTAGE, + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_POWER_FACTOR, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, + ]: + if conf := channel.get(sensor_type): + sensor_name = conf.get(CONF_NAME) + if ( + sensor_name + and channel_name + and not sensor_name.startswith(channel_name) + ): + conf[CONF_NAME] = f"{channel_name} {sensor_name}" + + if channel := config.get(CONF_NEUTRAL): + channel_name = channel.get(CONF_NAME) + if conf := channel.get(CONF_CURRENT): + sensor_name = conf.get(CONF_NAME) + if ( + sensor_name + and channel_name + and not sensor_name.startswith(channel_name) + ): + conf[CONF_NAME] = f"{channel_name} {sensor_name}" + + +FINAL_VALIDATE_SCHEMA = final_validate + + +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) + + if irq0_pin := config.get(CONF_IRQ0_PIN): + pin = await cg.gpio_pin_expression(irq0_pin) + cg.add(var.set_irq0_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ1_PIN]) + cg.add(var.set_irq1_pin(pin)) + + if reset_pin := config.get(CONF_RESET_PIN): + pin = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(pin)) + + if frequency := config.get(CONF_FREQUENCY): + cg.add(var.set_frequency(frequency)) + + if channel := config.get(CONF_PHASE_A): + chan = await power_channel(channel) + cg.add(var.set_channel_a(chan)) + + if channel := config.get(CONF_PHASE_B): + chan = await power_channel(channel) + cg.add(var.set_channel_b(chan)) + + if channel := config.get(CONF_PHASE_C): + chan = await power_channel(channel) + cg.add(var.set_channel_c(chan)) + + if channel := config.get(CONF_NEUTRAL): + chan = await neutral_channel(channel) + cg.add(var.set_channel_n(chan)) diff --git a/esphome/components/ade7953/__init__.py b/esphome/components/ade7953/__init__.py index e69de29bb2..d3078a0b67 100644 --- a/esphome/components/ade7953/__init__.py +++ b/esphome/components/ade7953/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@angelnu"] diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp deleted file mode 100644 index 2c61fc6a44..0000000000 --- a/esphome/components/ade7953/ade7953.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "ade7953.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace ade7953 { - -static const char *const TAG = "ade7953"; - -void ADE7953::dump_config() { - ESP_LOGCONFIG(TAG, "ADE7953:"); - LOG_PIN(" IRQ Pin: ", irq_pin_); - LOG_I2C_DEVICE(this); - LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "Voltage Sensor", this->voltage_sensor_); - LOG_SENSOR(" ", "Current A Sensor", this->current_a_sensor_); - LOG_SENSOR(" ", "Current B Sensor", this->current_b_sensor_); - LOG_SENSOR(" ", "Active Power A Sensor", this->active_power_a_sensor_); - LOG_SENSOR(" ", "Active Power B Sensor", this->active_power_b_sensor_); -} - -#define ADE_PUBLISH_(name, val, factor) \ - if (err == i2c::ERROR_OK && this->name##_sensor_) { \ - float value = (val) / (factor); \ - this->name##_sensor_->publish_state(value); \ - } -#define ADE_PUBLISH(name, val, factor) ADE_PUBLISH_(name, val, factor) - -void ADE7953::update() { - if (!this->is_setup_) - return; - - uint32_t val; - i2c::ErrorCode err = ade_read_32_(0x0312, &val); - ADE_PUBLISH(active_power_a, (int32_t) val, 154.0f); - err = ade_read_32_(0x0313, &val); - ADE_PUBLISH(active_power_b, (int32_t) val, 154.0f); - err = ade_read_32_(0x031A, &val); - ADE_PUBLISH(current_a, (uint32_t) val, 100000.0f); - err = ade_read_32_(0x031B, &val); - ADE_PUBLISH(current_b, (uint32_t) val, 100000.0f); - err = ade_read_32_(0x031C, &val); - ADE_PUBLISH(voltage, (uint32_t) val, 26000.0f); - - // auto apparent_power_a = this->ade_read_(0x0310); - // auto apparent_power_b = this->ade_read_(0x0311); - // auto reactive_power_a = this->ade_read_(0x0314); - // auto reactive_power_b = this->ade_read_(0x0315); - // auto power_factor_a = this->ade_read_(0x010A); - // auto power_factor_b = this->ade_read_(0x010B); -} - -} // namespace ade7953 -} // namespace esphome diff --git a/esphome/components/ade7953/ade7953.h b/esphome/components/ade7953/ade7953.h deleted file mode 100644 index c0c1cc4db8..0000000000 --- a/esphome/components/ade7953/ade7953.h +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/hal.h" -#include "esphome/components/i2c/i2c.h" -#include "esphome/components/sensor/sensor.h" - -#include - -namespace esphome { -namespace ade7953 { - -class ADE7953 : public i2c::I2CDevice, public PollingComponent { - public: - void set_irq_pin(InternalGPIOPin *irq_pin) { irq_pin_ = irq_pin; } - void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } - void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; } - void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; } - void set_active_power_a_sensor(sensor::Sensor *active_power_a_sensor) { - active_power_a_sensor_ = active_power_a_sensor; - } - void set_active_power_b_sensor(sensor::Sensor *active_power_b_sensor) { - active_power_b_sensor_ = active_power_b_sensor; - } - - void setup() override { - if (this->irq_pin_ != nullptr) { - this->irq_pin_->setup(); - } - this->set_timeout(100, [this]() { - this->ade_write_8_(0x0010, 0x04); - this->ade_write_8_(0x00FE, 0xAD); - this->ade_write_16_(0x0120, 0x0030); - this->is_setup_ = true; - }); - } - - void dump_config() override; - - void update() override; - - protected: - i2c::ErrorCode ade_write_8_(uint16_t reg, uint8_t value) { - std::vector data; - data.push_back(reg >> 8); - data.push_back(reg >> 0); - data.push_back(value); - return write(data.data(), data.size()); - } - i2c::ErrorCode ade_write_16_(uint16_t reg, uint16_t value) { - std::vector data; - data.push_back(reg >> 8); - data.push_back(reg >> 0); - data.push_back(value >> 8); - data.push_back(value >> 0); - return write(data.data(), data.size()); - } - i2c::ErrorCode ade_write_32_(uint16_t reg, uint32_t value) { - std::vector data; - data.push_back(reg >> 8); - data.push_back(reg >> 0); - data.push_back(value >> 24); - data.push_back(value >> 16); - data.push_back(value >> 8); - data.push_back(value >> 0); - return write(data.data(), data.size()); - } - i2c::ErrorCode ade_read_32_(uint16_t reg, uint32_t *value) { - uint8_t reg_data[2]; - reg_data[0] = reg >> 8; - reg_data[1] = reg >> 0; - i2c::ErrorCode err = write(reg_data, 2); - if (err != i2c::ERROR_OK) - return err; - uint8_t recv[4]; - err = read(recv, 4); - if (err != i2c::ERROR_OK) - return err; - *value = 0; - *value |= ((uint32_t) recv[0]) << 24; - *value |= ((uint32_t) recv[1]) << 16; - *value |= ((uint32_t) recv[2]) << 8; - *value |= ((uint32_t) recv[3]); - return i2c::ERROR_OK; - } - - InternalGPIOPin *irq_pin_{nullptr}; - bool is_setup_{false}; - sensor::Sensor *voltage_sensor_{nullptr}; - sensor::Sensor *current_a_sensor_{nullptr}; - sensor::Sensor *current_b_sensor_{nullptr}; - sensor::Sensor *active_power_a_sensor_{nullptr}; - sensor::Sensor *active_power_b_sensor_{nullptr}; -}; - -} // namespace ade7953 -} // namespace esphome diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index d02f466091..fc79888129 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -1,90 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, i2c -from esphome import pins -from esphome.const import ( - CONF_ID, - CONF_VOLTAGE, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, - UNIT_VOLT, - UNIT_AMPERE, - UNIT_WATT, + +CONFIG_SCHEMA = cv.invalid( + "The ade7953 sensor component has been renamed to ade7953_i2c." ) - -DEPENDENCIES = ["i2c"] - -ade7953_ns = cg.esphome_ns.namespace("ade7953") -ADE7953 = ade7953_ns.class_("ADE7953", cg.PollingComponent, i2c.I2CDevice) - -CONF_IRQ_PIN = "irq_pin" -CONF_CURRENT_A = "current_a" -CONF_CURRENT_B = "current_b" -CONF_ACTIVE_POWER_A = "active_power_a" -CONF_ACTIVE_POWER_B = "active_power_b" - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(ADE7953), - cv.Optional(CONF_IRQ_PIN): pins.internal_gpio_input_pin_schema, - 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_A): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CURRENT_B): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x38)) -) - - -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) - - if CONF_IRQ_PIN in config: - irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) - cg.add(var.set_irq_pin(irq_pin)) - - for key in [ - CONF_VOLTAGE, - CONF_CURRENT_A, - CONF_CURRENT_B, - CONF_ACTIVE_POWER_A, - CONF_ACTIVE_POWER_B, - ]: - if key not in config: - continue - conf = config[key] - sens = await sensor.new_sensor(conf) - cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/ade7953_base/__init__.py b/esphome/components/ade7953_base/__init__.py new file mode 100644 index 0000000000..af3f629ca8 --- /dev/null +++ b/esphome/components/ade7953_base/__init__.py @@ -0,0 +1,201 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome import pins +from esphome.const import ( + CONF_IRQ_PIN, + CONF_VOLTAGE, + CONF_FREQUENCY, + CONF_VOLTAGE_GAIN, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_APPARENT_POWER, + DEVICE_CLASS_POWER, + DEVICE_CLASS_REACTIVE_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_FREQUENCY, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_HERTZ, + UNIT_AMPERE, + UNIT_VOLT_AMPS, + UNIT_WATT, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_PERCENT, +) + +CONF_CURRENT_A = "current_a" +CONF_CURRENT_B = "current_b" +CONF_ACTIVE_POWER_A = "active_power_a" +CONF_ACTIVE_POWER_B = "active_power_b" +CONF_APPARENT_POWER_A = "apparent_power_a" +CONF_APPARENT_POWER_B = "apparent_power_b" +CONF_REACTIVE_POWER_A = "reactive_power_a" +CONF_REACTIVE_POWER_B = "reactive_power_b" +CONF_POWER_FACTOR_A = "power_factor_a" +CONF_POWER_FACTOR_B = "power_factor_b" +CONF_VOLTAGE_PGA_GAIN = "voltage_pga_gain" +CONF_CURRENT_PGA_GAIN_A = "current_pga_gain_a" +CONF_CURRENT_PGA_GAIN_B = "current_pga_gain_b" +CONF_CURRENT_GAIN_A = "current_gain_a" +CONF_CURRENT_GAIN_B = "current_gain_b" +CONF_ACTIVE_POWER_GAIN_A = "active_power_gain_a" +CONF_ACTIVE_POWER_GAIN_B = "active_power_gain_b" +CONF_USE_ACCUMULATED_ENERGY_REGISTERS = "use_accumulated_energy_registers" +PGA_GAINS = { + "1x": 0b000, + "2x": 0b001, + "4x": 0b010, + "8x": 0b011, + "16x": 0b100, + "22x": 0b101, +} + +ade7953_base_ns = cg.esphome_ns.namespace("ade7953_base") +ADE7953 = ade7953_base_ns.class_("ADE7953", cg.PollingComponent) + +ADE7953_CONFIG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_IRQ_PIN): pins.internal_gpio_input_pin_schema, + 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_FREQUENCY): sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + accuracy_decimals=2, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_A): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_B): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_B): 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_A): 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_APPARENT_POWER_B): 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_REACTIVE_POWER_A): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_REACTIVE_POWER_B): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR_A): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR_B): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + CONF_VOLTAGE_PGA_GAIN, + default="1x", + ): cv.one_of(*PGA_GAINS, lower=True), + cv.Optional( + CONF_CURRENT_PGA_GAIN_A, + default="1x", + ): cv.one_of(*PGA_GAINS, lower=True), + cv.Optional( + CONF_CURRENT_PGA_GAIN_B, + default="1x", + ): cv.one_of(*PGA_GAINS, lower=True), + cv.Optional(CONF_VOLTAGE_GAIN, default=0x400000): cv.hex_int_range( + min=0x100000, max=0x800000 + ), + cv.Optional(CONF_CURRENT_GAIN_A, default=0x400000): cv.hex_int_range( + min=0x100000, max=0x800000 + ), + cv.Optional(CONF_CURRENT_GAIN_B, default=0x400000): cv.hex_int_range( + min=0x100000, max=0x800000 + ), + cv.Optional(CONF_ACTIVE_POWER_GAIN_A, default=0x400000): cv.hex_int_range( + min=0x100000, max=0x800000 + ), + cv.Optional(CONF_ACTIVE_POWER_GAIN_B, default=0x400000): cv.hex_int_range( + min=0x100000, max=0x800000 + ), + cv.Optional(CONF_USE_ACCUMULATED_ENERGY_REGISTERS, default=False): cv.boolean, + } +).extend(cv.polling_component_schema("60s")) + + +async def register_ade7953(var, config): + await cg.register_component(var, config) + + if irq_pin_config := config.get(CONF_IRQ_PIN): + irq_pin = await cg.gpio_pin_expression(irq_pin_config) + cg.add(var.set_irq_pin(irq_pin)) + + cg.add(var.set_pga_v(PGA_GAINS[config.get(CONF_VOLTAGE_PGA_GAIN)])) + cg.add(var.set_pga_ia(PGA_GAINS[config.get(CONF_CURRENT_PGA_GAIN_A)])) + cg.add(var.set_pga_ib(PGA_GAINS[config.get(CONF_CURRENT_PGA_GAIN_B)])) + cg.add(var.set_vgain(config.get(CONF_VOLTAGE_GAIN))) + cg.add(var.set_aigain(config.get(CONF_CURRENT_GAIN_A))) + cg.add(var.set_bigain(config.get(CONF_CURRENT_GAIN_B))) + cg.add(var.set_awgain(config.get(CONF_ACTIVE_POWER_GAIN_A))) + cg.add(var.set_bwgain(config.get(CONF_ACTIVE_POWER_GAIN_B))) + cg.add( + var.set_use_acc_energy_regs(config.get(CONF_USE_ACCUMULATED_ENERGY_REGISTERS)) + ) + + for key in [ + CONF_VOLTAGE, + CONF_FREQUENCY, + CONF_CURRENT_A, + CONF_CURRENT_B, + CONF_POWER_FACTOR_A, + CONF_POWER_FACTOR_B, + CONF_APPARENT_POWER_A, + CONF_APPARENT_POWER_B, + CONF_ACTIVE_POWER_A, + CONF_ACTIVE_POWER_B, + CONF_REACTIVE_POWER_A, + CONF_REACTIVE_POWER_B, + ]: + if key not in config: + continue + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/ade7953_base/ade7953_base.cpp b/esphome/components/ade7953_base/ade7953_base.cpp new file mode 100644 index 0000000000..2511b4e04c --- /dev/null +++ b/esphome/components/ade7953_base/ade7953_base.cpp @@ -0,0 +1,150 @@ +#include "ade7953_base.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace ade7953_base { + +static const char *const TAG = "ade7953"; + +static const float ADE_POWER_FACTOR = 154.0f; +static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600; + +void ADE7953::setup() { + if (this->irq_pin_ != nullptr) { + this->irq_pin_->setup(); + } + + // The chip might take up to 100ms to initialise + this->set_timeout(100, [this]() { + // this->ade_write_8(0x0010, 0x04); + this->ade_write_8(0x00FE, 0xAD); + this->ade_write_16(0x0120, 0x0030); + // Set gains + 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(AIGAIN_32, aigain_); + this->ade_write_32(BIGAIN_32, bigain_); + this->ade_write_32(AWGAIN_32, awgain_); + this->ade_write_32(BWGAIN_32, bwgain_); + // Read back gains for debugging + 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(AIGAIN_32, &aigain_); + this->ade_read_32(BIGAIN_32, &bigain_); + this->ade_read_32(AWGAIN_32, &awgain_); + this->ade_read_32(BWGAIN_32, &bwgain_); + this->last_update_ = millis(); + this->is_setup_ = true; + }); +} + +void ADE7953::dump_config() { + LOG_PIN(" IRQ Pin: ", irq_pin_); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Voltage Sensor", this->voltage_sensor_); + LOG_SENSOR(" ", "Current A Sensor", this->current_a_sensor_); + LOG_SENSOR(" ", "Current B Sensor", this->current_b_sensor_); + LOG_SENSOR(" ", "Power Factor A Sensor", this->power_factor_a_sensor_); + LOG_SENSOR(" ", "Power Factor B Sensor", this->power_factor_b_sensor_); + LOG_SENSOR(" ", "Apparent Power A Sensor", this->apparent_power_a_sensor_); + LOG_SENSOR(" ", "Apparent Power B Sensor", this->apparent_power_b_sensor_); + LOG_SENSOR(" ", "Active Power A Sensor", this->active_power_a_sensor_); + LOG_SENSOR(" ", "Active Power B Sensor", this->active_power_b_sensor_); + LOG_SENSOR(" ", "Rective Power A Sensor", this->reactive_power_a_sensor_); + LOG_SENSOR(" ", "Reactive Power B Sensor", this->reactive_power_b_sensor_); + ESP_LOGCONFIG(TAG, " USE_ACC_ENERGY_REGS: %d", this->use_acc_energy_regs_); + ESP_LOGCONFIG(TAG, " PGA_V_8: 0x%X", pga_v_); + ESP_LOGCONFIG(TAG, " PGA_IA_8: 0x%X", pga_ia_); + ESP_LOGCONFIG(TAG, " PGA_IB_8: 0x%X", pga_ib_); + ESP_LOGCONFIG(TAG, " VGAIN_32: 0x%08jX", (uintmax_t) vgain_); + ESP_LOGCONFIG(TAG, " AIGAIN_32: 0x%08jX", (uintmax_t) aigain_); + ESP_LOGCONFIG(TAG, " BIGAIN_32: 0x%08jX", (uintmax_t) bigain_); + ESP_LOGCONFIG(TAG, " AWGAIN_32: 0x%08jX", (uintmax_t) awgain_); + ESP_LOGCONFIG(TAG, " BWGAIN_32: 0x%08jX", (uintmax_t) bwgain_); +} + +#define ADE_PUBLISH_(name, val, factor) \ + if (err == 0 && this->name##_sensor_) { \ + float value = (val) / (factor); \ + this->name##_sensor_->publish_state(value); \ + } +#define ADE_PUBLISH(name, val, factor) ADE_PUBLISH_(name, val, factor) + +void ADE7953::update() { + if (!this->is_setup_) + return; + + bool err; + + uint32_t interrupts_a = 0; + uint32_t interrupts_b = 0; + if (this->irq_pin_ != nullptr) { + // Read and reset interrupts + this->ade_read_32(0x032E, &interrupts_a); + this->ade_read_32(0x0331, &interrupts_b); + } + + uint32_t val; + uint16_t val_16; + uint16_t reg; + + // Power factor + err = this->ade_read_16(0x010A, &val_16); + ADE_PUBLISH(power_factor_a, (int16_t) val_16, (0x7FFF / 100.0f)); + err = this->ade_read_16(0x010B, &val_16); + ADE_PUBLISH(power_factor_b, (int16_t) val_16, (0x7FFF / 100.0f)); + + float pf = ADE_POWER_FACTOR; + if (this->use_acc_energy_regs_) { + const uint32_t now = millis(); + const auto diff = now - this->last_update_; + this->last_update_ = now; + // prevent DIV/0 + pf = ADE_WATTSEC_POWER_FACTOR * (diff < 10 ? 10 : diff) / 1000; + ESP_LOGVV(TAG, "ADE7953::update() diff=%" PRIu32 " pf=%f", diff, pf); + } + + // Apparent power + reg = this->use_acc_energy_regs_ ? 0x0322 : 0x0310; + err = this->ade_read_32(reg, &val); + ADE_PUBLISH(apparent_power_a, (int32_t) val, pf); + err = this->ade_read_32(reg + 1, &val); + ADE_PUBLISH(apparent_power_b, (int32_t) val, pf); + + // Active power + reg = this->use_acc_energy_regs_ ? 0x031E : 0x0312; + err = this->ade_read_32(reg, &val); + ADE_PUBLISH(active_power_a, (int32_t) val, pf); + err = this->ade_read_32(reg + 1, &val); + ADE_PUBLISH(active_power_b, (int32_t) val, pf); + + // Reactive power + reg = this->use_acc_energy_regs_ ? 0x0320 : 0x0314; + err = this->ade_read_32(reg, &val); + ADE_PUBLISH(reactive_power_a, (int32_t) val, pf); + err = this->ade_read_32(reg + 1, &val); + ADE_PUBLISH(reactive_power_b, (int32_t) val, pf); + + // Current + err = this->ade_read_32(0x031A, &val); + ADE_PUBLISH(current_a, (uint32_t) val, 100000.0f); + err = this->ade_read_32(0x031B, &val); + ADE_PUBLISH(current_b, (uint32_t) val, 100000.0f); + + // Voltage + err = this->ade_read_32(0x031C, &val); + ADE_PUBLISH(voltage, (uint32_t) val, 26000.0f); + + // Frequency + err = this->ade_read_16(0x010E, &val_16); + ADE_PUBLISH(frequency, 223750.0f, 1 + val_16); +} + +} // namespace ade7953_base +} // namespace esphome diff --git a/esphome/components/ade7953_base/ade7953_base.h b/esphome/components/ade7953_base/ade7953_base.h new file mode 100644 index 0000000000..d711a5c6be --- /dev/null +++ b/esphome/components/ade7953_base/ade7953_base.h @@ -0,0 +1,125 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" + +#include + +namespace esphome { +namespace ade7953_base { + +static const uint8_t PGA_V_8 = + 0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0]) +static const uint8_t PGA_IA_8 = + 0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0]) +static const uint8_t PGA_IB_8 = + 0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0]) + +static const uint32_t AIGAIN_32 = + 0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit) +static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit) +static const uint32_t AWGAIN_32 = + 0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit) +static const uint32_t AVARGAIN_32 = + 0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit) +static const uint32_t AVAGAIN_32 = + 0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit) + +static const uint32_t BIGAIN_32 = + 0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit) +static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit) +static const uint32_t BWGAIN_32 = + 0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit) +static const uint32_t BVARGAIN_32 = + 0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit) +static const uint32_t BVAGAIN_32 = + 0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit) + +class ADE7953 : public PollingComponent, public sensor::Sensor { + public: + void set_irq_pin(InternalGPIOPin *irq_pin) { irq_pin_ = irq_pin; } + + // Set PGA input gains: 0 1x, 1 2x, 0b10 4x + void set_pga_v(uint8_t pga_v) { pga_v_ = pga_v; } + void set_pga_ia(uint8_t pga_ia) { pga_ia_ = pga_ia; } + 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_aigain(uint32_t aigain) { aigain_ = aigain; } + void set_bigain(uint32_t bigain) { bigain_ = bigain; } + void set_awgain(uint32_t awgain) { awgain_ = awgain; } + void set_bwgain(uint32_t bwgain) { bwgain_ = bwgain; } + + void set_use_acc_energy_regs(bool use_acc_energy_regs) { use_acc_energy_regs_ = use_acc_energy_regs; } + + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + + void set_power_factor_a_sensor(sensor::Sensor *power_factor_a) { power_factor_a_sensor_ = power_factor_a; } + void set_power_factor_b_sensor(sensor::Sensor *power_factor_b) { power_factor_b_sensor_ = power_factor_b; } + + void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; } + void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; } + + void set_apparent_power_a_sensor(sensor::Sensor *apparent_power_a) { apparent_power_a_sensor_ = apparent_power_a; } + void set_apparent_power_b_sensor(sensor::Sensor *apparent_power_b) { apparent_power_b_sensor_ = apparent_power_b; } + + void set_active_power_a_sensor(sensor::Sensor *active_power_a_sensor) { + active_power_a_sensor_ = active_power_a_sensor; + } + void set_active_power_b_sensor(sensor::Sensor *active_power_b_sensor) { + active_power_b_sensor_ = active_power_b_sensor; + } + + void set_reactive_power_a_sensor(sensor::Sensor *reactive_power_a) { reactive_power_a_sensor_ = reactive_power_a; } + void set_reactive_power_b_sensor(sensor::Sensor *reactive_power_b) { reactive_power_b_sensor_ = reactive_power_b; } + + void setup() override; + + void dump_config() override; + + void update() override; + + protected: + InternalGPIOPin *irq_pin_{nullptr}; + bool is_setup_{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *frequency_sensor_{nullptr}; + sensor::Sensor *current_a_sensor_{nullptr}; + sensor::Sensor *current_b_sensor_{nullptr}; + sensor::Sensor *apparent_power_a_sensor_{nullptr}; + sensor::Sensor *apparent_power_b_sensor_{nullptr}; + sensor::Sensor *active_power_a_sensor_{nullptr}; + sensor::Sensor *active_power_b_sensor_{nullptr}; + sensor::Sensor *reactive_power_a_sensor_{nullptr}; + sensor::Sensor *reactive_power_b_sensor_{nullptr}; + sensor::Sensor *power_factor_a_sensor_{nullptr}; + sensor::Sensor *power_factor_b_sensor_{nullptr}; + uint8_t pga_v_; + uint8_t pga_ia_; + uint8_t pga_ib_; + uint32_t vgain_; + uint32_t aigain_; + uint32_t bigain_; + uint32_t awgain_; + uint32_t bwgain_; + bool use_acc_energy_regs_{false}; + uint32_t last_update_; + + virtual bool ade_write_8(uint16_t reg, uint8_t value) = 0; + + virtual bool ade_write_16(uint16_t reg, uint16_t value) = 0; + + virtual bool ade_write_32(uint16_t reg, uint32_t value) = 0; + + virtual bool ade_read_8(uint16_t reg, uint8_t *value) = 0; + + virtual bool ade_read_16(uint16_t reg, uint16_t *value) = 0; + + virtual bool ade_read_32(uint16_t reg, uint32_t *value) = 0; +}; + +} // namespace ade7953_base +} // namespace esphome diff --git a/esphome/components/ade7953_i2c/__init__.py b/esphome/components/ade7953_i2c/__init__.py new file mode 100644 index 0000000000..d3078a0b67 --- /dev/null +++ b/esphome/components/ade7953_i2c/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@angelnu"] diff --git a/esphome/components/ade7953_i2c/ade7953_i2c.cpp b/esphome/components/ade7953_i2c/ade7953_i2c.cpp new file mode 100644 index 0000000000..ae381824db --- /dev/null +++ b/esphome/components/ade7953_i2c/ade7953_i2c.cpp @@ -0,0 +1,80 @@ +#include "ade7953_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ade7953_i2c { + +static const char *const TAG = "ade7953"; + +void AdE7953I2c::dump_config() { + ESP_LOGCONFIG(TAG, "ADE7953_i2c:"); + LOG_I2C_DEVICE(this); + ade7953_base::ADE7953::dump_config(); +} +bool AdE7953I2c::ade_write_8(uint16_t reg, uint8_t value) { + uint8_t data[3]; + data[0] = reg >> 8; + data[1] = reg >> 0; + data[2] = value; + return this->write(data, 3) != i2c::ERROR_OK; +} +bool AdE7953I2c::ade_write_16(uint16_t reg, uint16_t value) { + uint8_t data[4]; + data[0] = reg >> 8; + data[1] = reg >> 0; + data[2] = value >> 8; + data[3] = value >> 0; + return this->write(data, 4) != i2c::ERROR_OK; +} +bool AdE7953I2c::ade_write_32(uint16_t reg, uint32_t value) { + uint8_t data[6]; + data[0] = reg >> 8; + data[1] = reg >> 0; + data[2] = value >> 24; + data[3] = value >> 16; + data[4] = value >> 8; + data[5] = value >> 0; + return this->write(data, 6) != i2c::ERROR_OK; +} +bool AdE7953I2c::ade_read_8(uint16_t reg, uint8_t *value) { + uint8_t reg_data[2]; + reg_data[0] = reg >> 8; + reg_data[1] = reg >> 0; + i2c::ErrorCode err = this->write(reg_data, 2); + if (err != i2c::ERROR_OK) + return true; + err = this->read(value, 1); + return (err != i2c::ERROR_OK); +} +bool AdE7953I2c::ade_read_16(uint16_t reg, uint16_t *value) { + uint8_t reg_data[2]; + reg_data[0] = reg >> 8; + reg_data[1] = reg >> 0; + i2c::ErrorCode err = this->write(reg_data, 2); + if (err != i2c::ERROR_OK) + return true; + uint8_t recv[2]; + err = this->read(recv, 2); + if (err != i2c::ERROR_OK) + return true; + *value = encode_uint16(recv[0], recv[1]); + return false; +} +bool AdE7953I2c::ade_read_32(uint16_t reg, uint32_t *value) { + uint8_t reg_data[2]; + reg_data[0] = reg >> 8; + reg_data[1] = reg >> 0; + i2c::ErrorCode err = this->write(reg_data, 2); + if (err != i2c::ERROR_OK) + return true; + uint8_t recv[4]; + err = this->read(recv, 4); + if (err != i2c::ERROR_OK) + return true; + *value = encode_uint32(recv[0], recv[1], recv[2], recv[3]); + return false; +} + +} // namespace ade7953_i2c +} // namespace esphome diff --git a/esphome/components/ade7953_i2c/ade7953_i2c.h b/esphome/components/ade7953_i2c/ade7953_i2c.h new file mode 100644 index 0000000000..65dc30dddb --- /dev/null +++ b/esphome/components/ade7953_i2c/ade7953_i2c.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/ade7953_base/ade7953_base.h" + +#include + +namespace esphome { +namespace ade7953_i2c { + +class AdE7953I2c : public ade7953_base::ADE7953, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + bool ade_write_8(uint16_t reg, uint8_t value) override; + bool ade_write_16(uint16_t reg, uint16_t value) override; + bool ade_write_32(uint16_t reg, uint32_t value) override; + bool ade_read_8(uint16_t reg, uint8_t *value) override; + bool ade_read_16(uint16_t reg, uint16_t *value) override; + bool ade_read_32(uint16_t reg, uint32_t *value) override; +}; + +} // namespace ade7953_i2c +} // namespace esphome diff --git a/esphome/components/ade7953_i2c/sensor.py b/esphome/components/ade7953_i2c/sensor.py new file mode 100644 index 0000000000..e52a44eced --- /dev/null +++ b/esphome/components/ade7953_i2c/sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, ade7953_base +from esphome.const import CONF_ID + + +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["ade7953_base"] + +ade7953_ns = cg.esphome_ns.namespace("ade7953_i2c") +ADE7953 = ade7953_ns.class_("AdE7953I2c", ade7953_base.ADE7953, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADE7953), + } + ) + .extend(ade7953_base.ADE7953_CONFIG_SCHEMA) + .extend(i2c.i2c_device_schema(0x38)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await i2c.register_i2c_device(var, config) + await ade7953_base.register_ade7953(var, config) diff --git a/esphome/components/ade7953_spi/__init__.py b/esphome/components/ade7953_spi/__init__.py new file mode 100644 index 0000000000..d3078a0b67 --- /dev/null +++ b/esphome/components/ade7953_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@angelnu"] diff --git a/esphome/components/ade7953_spi/ade7953_spi.cpp b/esphome/components/ade7953_spi/ade7953_spi.cpp new file mode 100644 index 0000000000..77a2a8adc7 --- /dev/null +++ b/esphome/components/ade7953_spi/ade7953_spi.cpp @@ -0,0 +1,81 @@ +#include "ade7953_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ade7953_spi { + +static const char *const TAG = "ade7953"; + +void AdE7953Spi::setup() { + this->spi_setup(); + ade7953_base::ADE7953::setup(); +} + +void AdE7953Spi::dump_config() { + ESP_LOGCONFIG(TAG, "ADE7953_spi:"); + LOG_PIN(" CS Pin: ", this->cs_); + ade7953_base::ADE7953::dump_config(); +} + +bool AdE7953Spi::ade_write_8(uint16_t reg, uint8_t value) { + this->enable(); + this->write_byte16(reg); + this->transfer_byte(0); + this->transfer_byte(value); + this->disable(); + return false; +} + +bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) { + this->enable(); + this->write_byte16(reg); + this->transfer_byte(0); + this->write_byte16(value); + this->disable(); + return false; +} + +bool AdE7953Spi::ade_write_32(uint16_t reg, uint32_t value) { + this->enable(); + this->write_byte16(reg); + this->transfer_byte(0); + this->write_byte16(value >> 16); + this->write_byte16(value & 0xFFFF); + this->disable(); + return false; +} + +bool AdE7953Spi::ade_read_8(uint16_t reg, uint8_t *value) { + this->enable(); + this->write_byte16(reg); + this->transfer_byte(0x80); + *value = this->read_byte(); + this->disable(); + return false; +} + +bool AdE7953Spi::ade_read_16(uint16_t reg, uint16_t *value) { + this->enable(); + this->write_byte16(reg); + this->transfer_byte(0x80); + uint8_t recv[2]; + this->read_array(recv, 2); + *value = encode_uint16(recv[0], recv[1]); + this->disable(); + return false; +} + +bool AdE7953Spi::ade_read_32(uint16_t reg, uint32_t *value) { + this->enable(); + this->write_byte16(reg); + this->transfer_byte(0x80); + uint8_t recv[4]; + this->read_array(recv, 4); + *value = encode_uint32(recv[0], recv[1], recv[2], recv[3]); + this->disable(); + return false; +} + +} // namespace ade7953_spi +} // namespace esphome diff --git a/esphome/components/ade7953_spi/ade7953_spi.h b/esphome/components/ade7953_spi/ade7953_spi.h new file mode 100644 index 0000000000..d96852b9bb --- /dev/null +++ b/esphome/components/ade7953_spi/ade7953_spi.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/ade7953_base/ade7953_base.h" + +#include + +namespace esphome { +namespace ade7953_spi { + +class AdE7953Spi : public ade7953_base::ADE7953, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + bool ade_write_8(uint16_t reg, uint8_t value) override; + bool ade_write_16(uint16_t reg, uint16_t value) override; + bool ade_write_32(uint16_t reg, uint32_t value) override; + bool ade_read_8(uint16_t reg, uint8_t *value) override; + bool ade_read_16(uint16_t reg, uint16_t *value) override; + bool ade_read_32(uint16_t reg, uint32_t *value) override; +}; + +} // namespace ade7953_spi +} // namespace esphome diff --git a/esphome/components/ade7953_spi/sensor.py b/esphome/components/ade7953_spi/sensor.py new file mode 100644 index 0000000000..5f9682c711 --- /dev/null +++ b/esphome/components/ade7953_spi/sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, ade7953_base +from esphome.const import CONF_ID + + +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["ade7953_base"] + +ade7953_ns = cg.esphome_ns.namespace("ade7953_spi") +ADE7953 = ade7953_ns.class_("AdE7953Spi", ade7953_base.ADE7953, spi.SPIDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADE7953), + } + ) + .extend(ade7953_base.ADE7953_CONFIG_SCHEMA) + .extend(spi.spi_device_schema()) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await spi.register_spi_device(var, config) + await ade7953_base.register_ade7953(var, config) diff --git a/esphome/components/ads1115/__init__.py b/esphome/components/ads1115/__init__.py index e8861a2f67..a463d8390d 100644 --- a/esphome/components/ads1115/__init__.py +++ b/esphome/components/ads1115/__init__.py @@ -4,13 +4,14 @@ from esphome.components import i2c from esphome.const import CONF_ID DEPENDENCIES = ["i2c"] -AUTO_LOAD = ["sensor", "voltage_sampler"] MULTI_CONF = True ads1115_ns = cg.esphome_ns.namespace("ads1115") ADS1115Component = ads1115_ns.class_("ADS1115Component", cg.Component, i2c.I2CDevice) CONF_CONTINUOUS_MODE = "continuous_mode" +CONF_ADS1115_ID = "ads1115_id" + CONFIG_SCHEMA = ( cv.Schema( { diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index c3f3c00c63..218edc4c81 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -1,6 +1,6 @@ #include "ads1115.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" namespace esphome { namespace ads1115 { @@ -75,25 +75,19 @@ void ADS1115Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Communication with ADS1115 failed!"); } - - for (auto *sensor : this->sensors_) { - LOG_SENSOR(" ", "Sensor", sensor); - ESP_LOGCONFIG(TAG, " Multiplexer: %u", sensor->get_multiplexer()); - ESP_LOGCONFIG(TAG, " Gain: %u", sensor->get_gain()); - ESP_LOGCONFIG(TAG, " Resolution: %u", sensor->get_resolution()); - } } -float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { +float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, + ADS1115Resolution resolution) { uint16_t config = this->prev_config_; // Multiplexer // 0bxBBBxxxxxxxxxxxx config &= 0b1000111111111111; - config |= (sensor->get_multiplexer() & 0b111) << 12; + config |= (multiplexer & 0b111) << 12; // Gain // 0bxxxxBBBxxxxxxxxx config &= 0b1111000111111111; - config |= (sensor->get_gain() & 0b111) << 9; + config |= (gain & 0b111) << 9; if (!this->continuous_mode_) { // Start conversion @@ -132,7 +126,7 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { return NAN; } - if (sensor->get_resolution() == ADS1015_12_BITS) { + if (resolution == ADS1015_12_BITS) { bool negative = (raw_conversion >> 15) == 1; // shift raw_conversion as it's only 12-bits, left justified @@ -151,8 +145,8 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { auto signed_conversion = static_cast(raw_conversion); float millivolts; - float divider = (sensor->get_resolution() == ADS1115_16_BITS) ? 32768.0f : 2048.0f; - switch (sensor->get_gain()) { + float divider = (resolution == ADS1115_16_BITS) ? 32768.0f : 2048.0f; + switch (gain) { case ADS1115_GAIN_6P144: millivolts = (signed_conversion * 6144) / divider; break; @@ -179,14 +173,5 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { return millivolts / 1e3f; } -float ADS1115Sensor::sample() { return this->parent_->request_measurement(this); } -void ADS1115Sensor::update() { - float v = this->parent_->request_measurement(this); - if (!std::isnan(v)) { - ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v); - this->publish_state(v); - } -} - } // namespace ads1115 } // namespace esphome diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index 0b8bfb339b..509333d2c8 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -1,9 +1,7 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -#include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/core/component.h" #include @@ -35,12 +33,8 @@ enum ADS1115Resolution { ADS1015_12_BITS = 12, }; -class ADS1115Sensor; - class ADS1115Component : public Component, public i2c::I2CDevice { public: - void register_sensor(ADS1115Sensor *obj) { this->sensors_.push_back(obj); } - /// Set up the internal sensor array. void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority @@ -48,33 +42,12 @@ class ADS1115Component : public Component, public i2c::I2CDevice { void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } /// Helper method to request a measurement from a sensor. - float request_measurement(ADS1115Sensor *sensor); + float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution); protected: - std::vector sensors_; uint16_t prev_config_{0}; bool continuous_mode_; }; -/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors. -class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { - public: - ADS1115Sensor(ADS1115Component *parent) : parent_(parent) {} - void update() override; - void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; } - void set_gain(ADS1115Gain gain) { gain_ = gain; } - void set_resolution(ADS1115Resolution resolution) { resolution_ = resolution; } - float sample() override; - uint8_t get_multiplexer() const { return multiplexer_; } - uint8_t get_gain() const { return gain_; } - uint8_t get_resolution() const { return resolution_; } - - protected: - ADS1115Component *parent_; - ADS1115Multiplexer multiplexer_; - ADS1115Gain gain_; - ADS1115Resolution resolution_; -}; - } // namespace ads1115 } // namespace esphome diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py deleted file mode 100644 index f0d894e2af..0000000000 --- a/esphome/components/ads1115/sensor.py +++ /dev/null @@ -1,92 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import sensor, voltage_sampler -from esphome.const import ( - CONF_GAIN, - CONF_MULTIPLEXER, - CONF_RESOLUTION, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, - UNIT_VOLT, - CONF_ID, -) -from . import ads1115_ns, ADS1115Component - -DEPENDENCIES = ["ads1115"] - -ADS1115Multiplexer = ads1115_ns.enum("ADS1115Multiplexer") -MUX = { - "A0_A1": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1, - "A0_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3, - "A1_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3, - "A2_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3, - "A0_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG, - "A1_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG, - "A2_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG, - "A3_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG, -} - -ADS1115Gain = ads1115_ns.enum("ADS1115Gain") -GAIN = { - "6.144": ADS1115Gain.ADS1115_GAIN_6P144, - "4.096": ADS1115Gain.ADS1115_GAIN_4P096, - "2.048": ADS1115Gain.ADS1115_GAIN_2P048, - "1.024": ADS1115Gain.ADS1115_GAIN_1P024, - "0.512": ADS1115Gain.ADS1115_GAIN_0P512, - "0.256": ADS1115Gain.ADS1115_GAIN_0P256, -} - -ADS1115Resolution = ads1115_ns.enum("ADS1115Resolution") -RESOLUTION = { - "16_BITS": ADS1115Resolution.ADS1115_16_BITS, - "12_BITS": ADS1115Resolution.ADS1015_12_BITS, -} - - -def validate_gain(value): - if isinstance(value, float): - value = f"{value:0.03f}" - elif not isinstance(value, str): - raise cv.Invalid(f'invalid gain "{value}"') - - return cv.enum(GAIN)(value) - - -ADS1115Sensor = ads1115_ns.class_( - "ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler -) - -CONF_ADS1115_ID = "ads1115_id" -CONFIG_SCHEMA = ( - sensor.sensor_schema( - ADS1115Sensor, - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=3, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ) - .extend( - { - cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), - cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), - cv.Required(CONF_GAIN): validate_gain, - cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( - RESOLUTION, upper=True, space="_" - ), - } - ) - .extend(cv.polling_component_schema("60s")) -) - - -async def to_code(config): - paren = await cg.get_variable(config[CONF_ADS1115_ID]) - var = cg.new_Pvariable(config[CONF_ID], paren) - await sensor.register_sensor(var, config) - await cg.register_component(var, config) - - cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) - cg.add(var.set_gain(config[CONF_GAIN])) - cg.add(var.set_resolution(config[CONF_RESOLUTION])) - - cg.add(paren.register_sensor(var)) diff --git a/esphome/components/ads1115/sensor/__init__.py b/esphome/components/ads1115/sensor/__init__.py new file mode 100644 index 0000000000..baec31d35c --- /dev/null +++ b/esphome/components/ads1115/sensor/__init__.py @@ -0,0 +1,81 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import ( + CONF_GAIN, + CONF_MULTIPLEXER, + CONF_RESOLUTION, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + CONF_ID, +) +from .. import ads1115_ns, ADS1115Component, CONF_ADS1115_ID + +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["ads1115"] + +ADS1115Multiplexer = ads1115_ns.enum("ADS1115Multiplexer") +MUX = { + "A0_A1": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1, + "A0_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3, + "A1_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3, + "A2_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3, + "A0_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG, + "A1_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG, + "A2_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG, + "A3_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG, +} + +ADS1115Gain = ads1115_ns.enum("ADS1115Gain") +GAIN = { + "6.144": ADS1115Gain.ADS1115_GAIN_6P144, + "4.096": ADS1115Gain.ADS1115_GAIN_4P096, + "2.048": ADS1115Gain.ADS1115_GAIN_2P048, + "1.024": ADS1115Gain.ADS1115_GAIN_1P024, + "0.512": ADS1115Gain.ADS1115_GAIN_0P512, + "0.256": ADS1115Gain.ADS1115_GAIN_0P256, +} + +ADS1115Resolution = ads1115_ns.enum("ADS1115Resolution") +RESOLUTION = { + "16_BITS": ADS1115Resolution.ADS1115_16_BITS, + "12_BITS": ADS1115Resolution.ADS1015_12_BITS, +} + + +ADS1115Sensor = ads1115_ns.class_( + "ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + ADS1115Sensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), + cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), + cv.Required(CONF_GAIN): cv.enum(GAIN, string=True), + cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( + RESOLUTION, upper=True, space="_" + ), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await sensor.register_sensor(var, config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_ADS1115_ID]) + + cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_resolution(config[CONF_RESOLUTION])) diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.cpp b/esphome/components/ads1115/sensor/ads1115_sensor.cpp new file mode 100644 index 0000000000..335fca4845 --- /dev/null +++ b/esphome/components/ads1115/sensor/ads1115_sensor.cpp @@ -0,0 +1,30 @@ +#include "ads1115_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1115 { + +static const char *const TAG = "ads1115.sensor"; + +float ADS1115Sensor::sample() { + return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_); +} + +void ADS1115Sensor::update() { + float v = this->sample(); + if (!std::isnan(v)) { + ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v); + this->publish_state(v); + } +} + +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_); +} + +} // namespace ads1115 +} // namespace esphome diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.h b/esphome/components/ads1115/sensor/ads1115_sensor.h new file mode 100644 index 0000000000..191afc3de6 --- /dev/null +++ b/esphome/components/ads1115/sensor/ads1115_sensor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include "../ads1115.h" + +namespace esphome { +namespace ads1115 { + +/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors. +class ADS1115Sensor : public sensor::Sensor, + public PollingComponent, + public voltage_sampler::VoltageSampler, + public Parented { + public: + void update() override; + void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } + void set_gain(ADS1115Gain gain) { this->gain_ = gain; } + void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; } + float sample() override; + + void dump_config() override; + + protected: + ADS1115Multiplexer multiplexer_; + ADS1115Gain gain_; + ADS1115Resolution resolution_; +}; + +} // namespace ads1115 +} // namespace esphome diff --git a/esphome/components/ads1118/__init__.py b/esphome/components/ads1118/__init__.py new file mode 100644 index 0000000000..f8d51101a6 --- /dev/null +++ b/esphome/components/ads1118/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID + +CODEOWNERS = ["@solomondg1"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +CONF_ADS1118_ID = "ads1118_id" + +ads1118_ns = cg.esphome_ns.namespace("ads1118") +ADS1118 = ads1118_ns.class_("ADS1118", cg.Component, spi.SPIDevice) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADS1118), + } +).extend(spi.spi_device_schema(cs_pin_required=True)) + + +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) diff --git a/esphome/components/ads1118/ads1118.cpp b/esphome/components/ads1118/ads1118.cpp new file mode 100644 index 0000000000..7b9d0cc36b --- /dev/null +++ b/esphome/components/ads1118/ads1118.cpp @@ -0,0 +1,126 @@ +#include "ads1118.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1118 { + +static const char *const TAG = "ads1118"; +static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111; + +void ADS1118::setup() { + ESP_LOGCONFIG(TAG, "Setting up ads1118"); + this->spi_setup(); + + this->config_ = 0; + // Setup multiplexer + // 0bx000xxxxxxxxxxxx + this->config_ |= ADS1118_MULTIPLEXER_P0_NG << 12; + + // Setup Gain + // 0bxxxx000xxxxxxxxx + this->config_ |= ADS1118_GAIN_6P144 << 9; + + // Set singleshot mode + // 0bxxxxxxx1xxxxxxxx + this->config_ |= 0b0000000100000000; + + // Set data rate - 860 samples per second (we're in singleshot mode) + // 0bxxxxxxxx100xxxxx + this->config_ |= ADS1118_DATA_RATE_860_SPS << 5; + + // Set temperature sensor mode - ADC + // 0bxxxxxxxxxxx0xxxx + this->config_ |= 0b0000000000000000; + + // Set DOUT pull up - enable + // 0bxxxxxxxxxxxx0xxx + this->config_ |= 0b0000000000001000; + + // NOP - must be 01 + // 0bxxxxxxxxxxxxx01x + this->config_ |= 0b0000000000000010; + + // Not used - can be 0 or 1, lets be positive + // 0bxxxxxxxxxxxxxxx1 + this->config_ |= 0b0000000000000001; +} + +void ADS1118::dump_config() { + ESP_LOGCONFIG(TAG, "ADS1118:"); + LOG_PIN(" CS Pin:", this->cs_); +} + +float ADS1118::request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode) { + uint16_t temp_config = this->config_; + // Multiplexer + // 0bxBBBxxxxxxxxxxxx + temp_config &= 0b1000111111111111; + temp_config |= (multiplexer & 0b111) << 12; + + // Gain + // 0bxxxxBBBxxxxxxxxx + temp_config &= 0b1111000111111111; + temp_config |= (gain & 0b111) << 9; + + if (temperature_mode) { + // Set temperature sensor mode + // 0bxxxxxxxxxxx1xxxx + temp_config |= 0b0000000000010000; + } else { + // Set ADC mode + // 0bxxxxxxxxxxx0xxxx + temp_config &= 0b1111111111101111; + } + + // Start conversion + temp_config |= 0b1000000000000000; + + this->enable(); + this->write_byte16(temp_config); + this->disable(); + + // about 1.2 ms with 860 samples per second + delay(2); + + this->enable(); + uint8_t adc_first_byte = this->read_byte(); + uint8_t adc_second_byte = this->read_byte(); + this->disable(); + uint16_t raw_conversion = encode_uint16(adc_first_byte, adc_second_byte); + + auto signed_conversion = static_cast(raw_conversion); + + if (temperature_mode) { + return (signed_conversion >> 2) * 0.03125f; + } else { + float millivolts; + float divider = 32768.0f; + switch (gain) { + case ADS1118_GAIN_6P144: + millivolts = (signed_conversion * 6144) / divider; + break; + case ADS1118_GAIN_4P096: + millivolts = (signed_conversion * 4096) / divider; + break; + case ADS1118_GAIN_2P048: + millivolts = (signed_conversion * 2048) / divider; + break; + case ADS1118_GAIN_1P024: + millivolts = (signed_conversion * 1024) / divider; + break; + case ADS1118_GAIN_0P512: + millivolts = (signed_conversion * 512) / divider; + break; + case ADS1118_GAIN_0P256: + millivolts = (signed_conversion * 256) / divider; + break; + default: + millivolts = NAN; + } + + return millivolts / 1e3f; + } +} + +} // namespace ads1118 +} // namespace esphome diff --git a/esphome/components/ads1118/ads1118.h b/esphome/components/ads1118/ads1118.h new file mode 100644 index 0000000000..8b9aa15cd2 --- /dev/null +++ b/esphome/components/ads1118/ads1118.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ads1118 { + +enum ADS1118Multiplexer { + ADS1118_MULTIPLEXER_P0_N1 = 0b000, + ADS1118_MULTIPLEXER_P0_N3 = 0b001, + ADS1118_MULTIPLEXER_P1_N3 = 0b010, + ADS1118_MULTIPLEXER_P2_N3 = 0b011, + ADS1118_MULTIPLEXER_P0_NG = 0b100, + ADS1118_MULTIPLEXER_P1_NG = 0b101, + ADS1118_MULTIPLEXER_P2_NG = 0b110, + ADS1118_MULTIPLEXER_P3_NG = 0b111, +}; + +enum ADS1118Gain { + ADS1118_GAIN_6P144 = 0b000, + ADS1118_GAIN_4P096 = 0b001, + ADS1118_GAIN_2P048 = 0b010, + ADS1118_GAIN_1P024 = 0b011, + ADS1118_GAIN_0P512 = 0b100, + ADS1118_GAIN_0P256 = 0b101, +}; + +class ADS1118 : public Component, + public spi::SPIDevice { + public: + ADS1118() = default; + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + /// Helper method to request a measurement from a sensor. + float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode); + + protected: + uint16_t config_{0}; +}; + +} // namespace ads1118 +} // namespace esphome diff --git a/esphome/components/ads1118/sensor/__init__.py b/esphome/components/ads1118/sensor/__init__.py new file mode 100644 index 0000000000..4e89115447 --- /dev/null +++ b/esphome/components/ads1118/sensor/__init__.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import ( + CONF_GAIN, + CONF_MULTIPLEXER, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_VOLT, + CONF_TYPE, +) +from .. import ads1118_ns, ADS1118, CONF_ADS1118_ID + +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["ads1118"] + +ADS1118Multiplexer = ads1118_ns.enum("ADS1118Multiplexer") +MUX = { + "A0_A1": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_N1, + "A0_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_N3, + "A1_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P1_N3, + "A2_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P2_N3, + "A0_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_NG, + "A1_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P1_NG, + "A2_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P2_NG, + "A3_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P3_NG, +} + +ADS1118Gain = ads1118_ns.enum("ADS1118Gain") +GAIN = { + "6.144": ADS1118Gain.ADS1118_GAIN_6P144, + "4.096": ADS1118Gain.ADS1118_GAIN_4P096, + "2.048": ADS1118Gain.ADS1118_GAIN_2P048, + "1.024": ADS1118Gain.ADS1118_GAIN_1P024, + "0.512": ADS1118Gain.ADS1118_GAIN_0P512, + "0.256": ADS1118Gain.ADS1118_GAIN_0P256, +} + + +ADS1118Sensor = ads1118_ns.class_( + "ADS1118Sensor", + cg.PollingComponent, + sensor.Sensor, + voltage_sampler.VoltageSampler, + cg.Parented.template(ADS1118), +) + +TYPE_ADC = "adc" +TYPE_TEMPERATURE = "temperature" + +CONFIG_SCHEMA = cv.typed_schema( + { + TYPE_ADC: sensor.sensor_schema( + ADS1118Sensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_ADS1118_ID): cv.use_id(ADS1118), + cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), + cv.Required(CONF_GAIN): cv.enum(GAIN, string=True), + } + ) + .extend(cv.polling_component_schema("60s")), + TYPE_TEMPERATURE: sensor.sensor_schema( + ADS1118Sensor, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_ADS1118_ID): cv.use_id(ADS1118), + } + ) + .extend(cv.polling_component_schema("60s")), + }, + default_type=TYPE_ADC, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_ADS1118_ID]) + + if config[CONF_TYPE] == TYPE_ADC: + cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) + cg.add(var.set_gain(config[CONF_GAIN])) + if config[CONF_TYPE] == TYPE_TEMPERATURE: + cg.add(var.set_temperature_mode(True)) diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.cpp b/esphome/components/ads1118/sensor/ads1118_sensor.cpp new file mode 100644 index 0000000000..c3ce3bdc9c --- /dev/null +++ b/esphome/components/ads1118/sensor/ads1118_sensor.cpp @@ -0,0 +1,29 @@ +#include "ads1118_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1118 { + +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_); +} + +float ADS1118Sensor::sample() { + return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->temperature_mode_); +} + +void ADS1118Sensor::update() { + float v = this->sample(); + if (!std::isnan(v)) { + ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v); + this->publish_state(v); + } +} + +} // namespace ads1118 +} // namespace esphome diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.h b/esphome/components/ads1118/sensor/ads1118_sensor.h new file mode 100644 index 0000000000..d2d7a03f59 --- /dev/null +++ b/esphome/components/ads1118/sensor/ads1118_sensor.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include "../ads1118.h" + +namespace esphome { +namespace ads1118 { + +class ADS1118Sensor : public PollingComponent, + public sensor::Sensor, + public voltage_sampler::VoltageSampler, + public Parented { + public: + void update() override; + + void set_multiplexer(ADS1118Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } + void set_gain(ADS1118Gain gain) { this->gain_ = gain; } + void set_temperature_mode(bool temp) { this->temperature_mode_ = temp; } + + float sample() override; + + void dump_config() override; + + protected: + ADS1118Multiplexer multiplexer_{ADS1118_MULTIPLEXER_P0_NG}; + ADS1118Gain gain_{ADS1118_GAIN_6P144}; + bool temperature_mode_; +}; + +} // namespace ads1118 +} // namespace esphome diff --git a/esphome/components/ags10/__init__.py b/esphome/components/ags10/__init__.py new file mode 100644 index 0000000000..37f34d06df --- /dev/null +++ b/esphome/components/ags10/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@mak-42"] diff --git a/esphome/components/ags10/ags10.cpp b/esphome/components/ags10/ags10.cpp new file mode 100644 index 0000000000..422380da83 --- /dev/null +++ b/esphome/components/ags10/ags10.cpp @@ -0,0 +1,214 @@ +#include "ags10.h" + +#include + +namespace esphome { +namespace ags10 { +static const char *const TAG = "ags10"; + +// Data acquisition. +static const uint8_t REG_TVOC = 0x00; +// Zero-point calibration. +static const uint8_t REG_CALIBRATION = 0x01; +// Read version. +static const uint8_t REG_VERSION = 0x11; +// Read current resistance. +static const uint8_t REG_RESISTANCE = 0x20; +// Modify target address. +static const uint8_t REG_ADDRESS = 0x21; + +// Zero-point calibration with current resistance. +static const uint16_t ZP_CURRENT = 0x0000; +// Zero-point reset. +static const uint16_t ZP_DEFAULT = 0xFFFF; + +void AGS10Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ags10..."); + + auto version = this->read_version_(); + if (version) { + ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version); + if (this->version_ != nullptr) { + this->version_->publish_state(*version); + } + } else { + ESP_LOGE(TAG, "AGS10 Sensor Version: unknown"); + } + + auto resistance = this->read_resistance_(); + if (resistance) { + ESP_LOGD(TAG, "AGS10 Sensor Resistance: 0x%08" PRIX32, *resistance); + if (this->resistance_ != nullptr) { + this->resistance_->publish_state(*resistance); + } + } else { + ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown"); + } + + ESP_LOGD(TAG, "Sensor initialized"); +} + +void AGS10Component::update() { + auto tvoc = this->read_tvoc_(); + if (tvoc) { + this->tvoc_->publish_state(*tvoc); + this->status_clear_warning(); + } else { + this->status_set_warning(); + } +} + +void AGS10Component::dump_config() { + ESP_LOGCONFIG(TAG, "AGS10:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case NONE: + break; + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with AGS10 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case ILLEGAL_STATUS: + ESP_LOGE(TAG, "AGS10 is not ready to return TVOC data or sensor in pre-heat stage."); + break; + case UNSUPPORTED_UNITS: + ESP_LOGE(TAG, "AGS10 returns TVOC data in unsupported units."); + break; + default: + ESP_LOGE(TAG, "Unknown error: %d", this->error_code_); + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_); + LOG_SENSOR(" ", "Firmware Version Sensor", this->version_); + LOG_SENSOR(" ", "Resistance Sensor", this->resistance_); +} + +/** + * Sets new I2C address of AGS10. + */ +bool AGS10Component::new_i2c_address(uint8_t newaddress) { + uint8_t rev_newaddress = ~newaddress; + std::array data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0}; + data[4] = calc_crc8_(data, 4); + if (!this->write_bytes(REG_ADDRESS, data)) { + this->error_code_ = COMMUNICATION_FAILED; + this->status_set_warning(); + ESP_LOGE(TAG, "couldn't write the new I2C address 0x%02X", newaddress); + return false; + } + this->set_i2c_address(newaddress); + ESP_LOGW(TAG, "changed I2C address to 0x%02X", newaddress); + this->error_code_ = NONE; + this->status_clear_warning(); + return true; +} + +bool AGS10Component::set_zero_point_with_factory_defaults() { return this->set_zero_point_with(ZP_DEFAULT); } + +bool AGS10Component::set_zero_point_with_current_resistance() { return this->set_zero_point_with(ZP_CURRENT); } + +bool AGS10Component::set_zero_point_with(uint16_t value) { + std::array data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0}; + data[4] = calc_crc8_(data, 4); + if (!this->write_bytes(REG_CALIBRATION, data)) { + this->error_code_ = COMMUNICATION_FAILED; + this->status_set_warning(); + ESP_LOGE(TAG, "unable to set zero-point calibration with 0x%02X", value); + return false; + } + if (value == ZP_CURRENT) { + ESP_LOGI(TAG, "zero-point calibration has been set with current resistance"); + } else if (value == ZP_DEFAULT) { + ESP_LOGI(TAG, "zero-point calibration has been reset to the factory defaults"); + } else { + ESP_LOGI(TAG, "zero-point calibration has been set with 0x%02X", value); + } + this->error_code_ = NONE; + this->status_clear_warning(); + return true; +} + +optional AGS10Component::read_tvoc_() { + auto data = this->read_and_check_<5>(REG_TVOC); + if (!data) { + return nullopt; + } + + auto res = *data; + auto status_byte = res[0]; + + int units = status_byte & 0x0e; + int status_bit = status_byte & 0x01; + + if (status_bit != 0) { + this->error_code_ = ILLEGAL_STATUS; + ESP_LOGW(TAG, "Reading AGS10 data failed: illegal status (not ready or sensor in pre-heat stage)!"); + return nullopt; + } + + if (units != 0) { + this->error_code_ = UNSUPPORTED_UNITS; + ESP_LOGE(TAG, "Reading AGS10 data failed: unsupported units (%d)!", units); + return nullopt; + } + + return encode_uint24(res[1], res[2], res[3]); +} + +optional AGS10Component::read_version_() { + auto data = this->read_and_check_<5>(REG_VERSION); + if (data) { + auto res = *data; + return res[3]; + } + return nullopt; +} + +optional AGS10Component::read_resistance_() { + auto data = this->read_and_check_<5>(REG_RESISTANCE); + if (data) { + auto res = *data; + return encode_uint32(res[0], res[1], res[2], res[3]); + } + return nullopt; +} + +template optional> AGS10Component::read_and_check_(uint8_t a_register) { + auto data = this->read_bytes(a_register); + if (!data.has_value()) { + this->error_code_ = COMMUNICATION_FAILED; + ESP_LOGE(TAG, "Reading AGS10 version failed!"); + return optional>(); + } + auto len = N - 1; + auto res = *data; + auto crc_byte = res[len]; + + if (crc_byte != calc_crc8_(res, len)) { + this->error_code_ = CRC_CHECK_FAILED; + ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!"); + return optional>(); + } + + return data; +} + +template uint8_t AGS10Component::calc_crc8_(std::array dat, uint8_t num) { + uint8_t i, byte1, crc = 0xFF; + for (byte1 = 0; byte1 < num; byte1++) { + crc ^= (dat[byte1]); + for (i = 0; i < 8; i++) { + if (crc & 0x80) { + crc = (crc << 1) ^ 0x31; + } else { + crc = (crc << 1); + } + } + } + return crc; +} +} // namespace ags10 +} // namespace esphome diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h new file mode 100644 index 0000000000..f2201fe70c --- /dev/null +++ b/esphome/components/ags10/ags10.h @@ -0,0 +1,152 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ags10 { + +class AGS10Component : public PollingComponent, public i2c::I2CDevice { + public: + /** + * Sets TVOC sensor. + */ + void set_tvoc(sensor::Sensor *tvoc) { this->tvoc_ = tvoc; } + + /** + * Sets version info sensor. + */ + void set_version(sensor::Sensor *version) { this->version_ = version; } + + /** + * Sets resistance info sensor. + */ + void set_resistance(sensor::Sensor *resistance) { this->resistance_ = resistance; } + + void setup() override; + + void update() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + /** + * Modifies target address of AGS10. + * + * New address is saved and takes effect immediately even after power-off. + */ + bool new_i2c_address(uint8_t newaddress); + + /** + * Sets zero-point with factory defaults. + */ + bool set_zero_point_with_factory_defaults(); + + /** + * Sets zero-point with current sensor resistance. + */ + bool set_zero_point_with_current_resistance(); + + /** + * Sets zero-point with the value. + */ + bool set_zero_point_with(uint16_t value); + + protected: + /** + * TVOC. + */ + sensor::Sensor *tvoc_{nullptr}; + + /** + * Firmvare version. + */ + sensor::Sensor *version_{nullptr}; + + /** + * Resistance. + */ + sensor::Sensor *resistance_{nullptr}; + + /** + * Last operation error code. + */ + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + CRC_CHECK_FAILED, + ILLEGAL_STATUS, + UNSUPPORTED_UNITS, + } error_code_{NONE}; + + /** + * Reads and returns value of TVOC. + */ + optional read_tvoc_(); + + /** + * Reads and returns a firmware version of AGS10. + */ + optional read_version_(); + + /** + * Reads and returns the resistance of AGS10. + */ + optional read_resistance_(); + + /** + * Read, checks and returns data from the sensor. + */ + template optional> read_and_check_(uint8_t a_register); + + /** + * Calculates CRC8 value. + * + * CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1) + * + * @param[in] dat the data buffer + * @param num number of bytes in the buffer + */ + template uint8_t calc_crc8_(std::array dat, uint8_t num); +}; + +template class AGS10NewI2cAddressAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, new_address) + + void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } +}; + +enum AGS10SetZeroPointActionMode { + // Zero-point reset. + FACTORY_DEFAULT, + // Zero-point calibration with current resistance. + CURRENT_VALUE, + // Zero-point calibration with custom resistance. + CUSTOM_VALUE, +}; + +template class AGS10SetZeroPointAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, value) + TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) + + void play(Ts... x) override { + switch (this->mode_.value(x...)) { + case FACTORY_DEFAULT: + this->parent_->set_zero_point_with_factory_defaults(); + break; + case CURRENT_VALUE: + this->parent_->set_zero_point_with_current_resistance(); + break; + case CUSTOM_VALUE: + this->parent_->set_zero_point_with(this->value_.value(x...)); + break; + } + } +}; +} // namespace ags10 +} // namespace esphome diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py new file mode 100644 index 0000000000..59aebd636b --- /dev/null +++ b/esphome/components/ags10/sensor.py @@ -0,0 +1,132 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + ICON_RADIATOR, + ICON_RESTART, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_OHM, + UNIT_PARTS_PER_BILLION, + CONF_ADDRESS, + CONF_TVOC, + CONF_VERSION, + CONF_MODE, + CONF_VALUE, +) + +CONF_RESISTANCE = "resistance" + +DEPENDENCIES = ["i2c"] + +ags10_ns = cg.esphome_ns.namespace("ags10") +AGS10Component = ags10_ns.class_("AGS10Component", cg.PollingComponent, i2c.I2CDevice) + +# Actions +AGS10NewI2cAddressAction = ags10_ns.class_( + "AGS10NewI2cAddressAction", automation.Action +) +AGS10SetZeroPointAction = ags10_ns.class_("AGS10SetZeroPointAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AGS10Component), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + icon=ICON_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_RESISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_OHM, + icon=ICON_RESTART, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x1A)) +) + +FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency="15khz") + + +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) + + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) + + if version_config := config.get(CONF_VERSION): + sens = await sensor.new_sensor(version_config) + cg.add(var.set_version(sens)) + + if resistance_config := config.get(CONF_RESISTANCE): + sens = await sensor.new_sensor(resistance_config) + cg.add(var.set_resistance(sens)) + + +AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(AGS10Component), + cv.Required(CONF_ADDRESS): cv.templatable(cv.i2c_address), + }, + key=CONF_ADDRESS, +) + + +@automation.register_action( + "ags10.new_i2c_address", + AGS10NewI2cAddressAction, + AGS10_NEW_I2C_ADDRESS_SCHEMA, +) +async def ags10newi2caddress_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + address = await cg.templatable(config[CONF_ADDRESS], args, int) + cg.add(var.set_new_address(address)) + return var + + +AGS10SetZeroPointActionMode = ags10_ns.enum("AGS10SetZeroPointActionMode") +AGS10_SET_ZERO_POINT_ACTION_MODE = { + "FACTORY_DEFAULT": AGS10SetZeroPointActionMode.FACTORY_DEFAULT, + "CURRENT_VALUE": AGS10SetZeroPointActionMode.CURRENT_VALUE, + "CUSTOM_VALUE": AGS10SetZeroPointActionMode.CUSTOM_VALUE, +} + +AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(AGS10Component), + cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True), + cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t), + }, +) + + +@automation.register_action( + "ags10.set_zero_point", + AGS10SetZeroPointAction, + AGS10_SET_ZERO_POINT_SCHEMA, +) +async def ags10setzeropoint_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + mode = await cg.templatable(config.get(CONF_MODE), args, enumerate) + cg.add(var.set_mode(mode)) + value = await cg.templatable(config[CONF_VALUE], args, int) + cg.add(var.set_value(value)) + return var diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index ad1a68498b..441c1ac9df 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -20,114 +20,140 @@ namespace esphome { namespace aht10 { static const char *const TAG = "aht10"; -static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; +static const uint8_t AHT10_INITIALIZE_CMD[] = {0xE1, 0x08, 0x00}; +static const uint8_t AHT20_INITIALIZE_CMD[] = {0xBE, 0x08, 0x00}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; -static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement -static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms -static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms +static const uint8_t AHT10_SOFTRESET_CMD[] = {0xBA}; + +static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for initialization and temperature measurement +static const uint8_t AHT10_READ_DELAY = 80; // ms, time to wait for conversion result +static const uint8_t AHT10_SOFTRESET_DELAY = 30; // ms + +static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms +static const uint8_t AHT10_INIT_ATTEMPTS = 10; + +static const uint8_t AHT10_STATUS_BUSY = 0x80; void AHT10Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up AHT10..."); + if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Reset AHT10 failed!"); + } + delay(AHT10_SOFTRESET_DELAY); - if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) { + i2c::ErrorCode error_code = i2c::ERROR_INVALID_ARGUMENT; + switch (this->variant_) { + case AHT10Variant::AHT20: + ESP_LOGCONFIG(TAG, "Setting up AHT20"); + error_code = this->write(AHT20_INITIALIZE_CMD, sizeof(AHT20_INITIALIZE_CMD)); + break; + case AHT10Variant::AHT10: + ESP_LOGCONFIG(TAG, "Setting up AHT10"); + error_code = this->write(AHT10_INITIALIZE_CMD, sizeof(AHT10_INITIALIZE_CMD)); + break; + } + if (error_code != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->mark_failed(); return; } - uint8_t data = 0; - if (this->write(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; - } - delay(AHT10_DEFAULT_DELAY); - if (this->read(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; - } - if (this->read(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; + uint8_t data = AHT10_STATUS_BUSY; + int cal_attempts = 0; + while (data & AHT10_STATUS_BUSY) { + delay(AHT10_DEFAULT_DELAY); + if (this->read(&data, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->mark_failed(); + return; + } + ++cal_attempts; + if (cal_attempts > AHT10_INIT_ATTEMPTS) { + ESP_LOGE(TAG, "AHT10 initialization timed out!"); + this->mark_failed(); + return; + } } if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED - ESP_LOGE(TAG, "AHT10 calibration failed!"); + ESP_LOGE(TAG, "AHT10 initialization failed!"); this->mark_failed(); return; } - ESP_LOGV(TAG, "AHT10 calibrated"); + ESP_LOGV(TAG, "AHT10 initialization"); } -void AHT10Component::update() { - if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { - ESP_LOGE(TAG, "Communication with AHT10 failed!"); - this->status_set_warning(); +void AHT10Component::restart_read_() { + if (this->read_count_ == AHT10_ATTEMPTS) { + this->read_count_ = 0; + this->status_set_error("Measurements reading timed-out!"); return; } + this->read_count_++; + this->set_timeout(AHT10_READ_DELAY, [this]() { this->read_data_(); }); +} + +void AHT10Component::read_data_() { uint8_t data[6]; - uint8_t delay_ms = AHT10_DEFAULT_DELAY; - if (this->humidity_sensor_ != nullptr) - delay_ms = AHT10_HUMIDITY_DELAY; - bool success = false; - for (int i = 0; i < AHT10_ATTEMPTS; ++i) { - ESP_LOGVV(TAG, "Attempt %d at %6u", i, millis()); - delay(delay_ms); - if (this->read(data, 6) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); - continue; - } - - if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy - ESP_LOGD(TAG, "AHT10 is busy, waiting..."); - } else if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) { - // Unrealistic humidity (0x0) - if (this->humidity_sensor_ == nullptr) { - ESP_LOGVV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required"); - break; - } else { - ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); - if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { - ESP_LOGE(TAG, "Communication with AHT10 failed!"); - this->status_set_warning(); - return; - } - } - } else { - // data is valid, we can break the loop - ESP_LOGVV(TAG, "Answer at %6u", millis()); - success = true; - break; - } + if (this->read_count_ > 1) { + ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_)); } - if (!success || (data[0] & 0x80) == 0x80) { - ESP_LOGE(TAG, "Measurements reading timed-out!"); - this->status_set_warning(); + if (this->read(data, 6) != i2c::ERROR_OK) { + this->status_set_warning("AHT10 read failed, retrying soon"); + this->restart_read_(); return; } + if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy + ESP_LOGD(TAG, "AHT10 is busy, waiting..."); + this->restart_read_(); + return; + } + if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) { + // Unrealistic humidity (0x0) + if (this->humidity_sensor_ == nullptr) { + ESP_LOGV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required"); + } else { + ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); + if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { + this->status_set_warning("Communication with AHT10 failed!"); + } + this->restart_read_(); + return; + } + } + if (this->read_count_ > 1) { + ESP_LOGD(TAG, "Success at %ums", (unsigned) (millis() - this->start_time_)); + } uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4; - float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f; - float humidity; - if (raw_humidity == 0) { // unrealistic value - humidity = NAN; - } else { - humidity = (float) raw_humidity * 100.0f / 1048576.0f; - } - if (this->temperature_sensor_ != nullptr) { + float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f; this->temperature_sensor_->publish_state(temperature); } if (this->humidity_sensor_ != nullptr) { + float humidity; + if (raw_humidity == 0) { // unrealistic value + humidity = NAN; + } else { + humidity = (float) raw_humidity * 100.0f / 1048576.0f; + } if (std::isnan(humidity)) { ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum"); } this->humidity_sensor_->publish_state(humidity); } this->status_clear_warning(); + this->read_count_ = 0; +} +void AHT10Component::update() { + if (this->read_count_ != 0) + return; + this->start_time_ = millis(); + if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { + this->status_set_warning("Communication with AHT10 failed!"); + return; + } + this->restart_read_(); } float AHT10Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index 4d0eaa5919..a3320c77e0 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" @@ -7,12 +9,15 @@ namespace esphome { namespace aht10 { +enum AHT10Variant { AHT10, AHT20 }; + class AHT10Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void update() override; void dump_config() override; float get_setup_priority() const override; + void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -20,6 +25,11 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { protected: sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; + AHT10Variant variant_{}; + unsigned read_count_{}; + void read_data_(); + void restart_read_(); + uint32_t start_time_{}; }; } // namespace aht10 diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py index 654d645966..31b07c0e73 100644 --- a/esphome/components/aht10/sensor.py +++ b/esphome/components/aht10/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, + CONF_VARIANT, ) DEPENDENCIES = ["i2c"] @@ -17,6 +18,12 @@ DEPENDENCIES = ["i2c"] aht10_ns = cg.esphome_ns.namespace("aht10") AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice) +AHT10Variant = aht10_ns.enum("AHT10Variant") +AHT10_VARIANTS = { + "AHT10": AHT10Variant.AHT10, + "AHT20": AHT10Variant.AHT20, +} + CONFIG_SCHEMA = ( cv.Schema( { @@ -33,6 +40,9 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum( + AHT10_VARIANTS, upper=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -44,11 +54,12 @@ 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) + cg.add(var.set_variant(config[CONF_VARIANT])) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature) cg.add(var.set_temperature_sensor(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/bme280/__init__.py b/esphome/components/aic3204/__init__.py similarity index 100% rename from esphome/components/bme280/__init__.py rename to esphome/components/aic3204/__init__.py diff --git a/esphome/components/aic3204/aic3204.cpp b/esphome/components/aic3204/aic3204.cpp new file mode 100644 index 0000000000..0560f2366b --- /dev/null +++ b/esphome/components/aic3204/aic3204.cpp @@ -0,0 +1,173 @@ +#include "aic3204.h" + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace aic3204 { + +static const char *const TAG = "aic3204"; + +#define ERROR_CHECK(err, msg) \ + if (!(err)) { \ + ESP_LOGE(TAG, msg); \ + this->mark_failed(); \ + return; \ + } + +void AIC3204::setup() { + ESP_LOGCONFIG(TAG, "Setting up AIC3204..."); + + // Set register page to 0 + ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed"); + // Initiate SW reset (PLL is powered off as part of reset) + ERROR_CHECK(this->write_byte(AIC3204_SW_RST, 0x01), "Software reset failed"); + // *** Program clock settings *** + // Default is CODEC_CLKIN is from MCLK pin. Don't need to change this. + // MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio) + // (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf) + // We do need MDAC*DOSR/32 >= the resource compute level for the processing block + // So here 2*128/32 = 8, which is equal to processing block 1 's resource compute + // See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow + // for determining these settings. + + // Power up NDAC and set to 2 + ERROR_CHECK(this->write_byte(AIC3204_NDAC, 0x82), "Set NDAC failed"); + // Power up MDAC and set to 2 + ERROR_CHECK(this->write_byte(AIC3204_MDAC, 0x82), "Set MDAC failed"); + // Program DOSR = 128 + ERROR_CHECK(this->write_byte(AIC3204_DOSR, 0x80), "Set DOSR failed"); + // Set Audio Interface Config: I2S, 32 bits, DOUT always driving + ERROR_CHECK(this->write_byte(AIC3204_CODEC_IF, 0x30), "Set CODEC_IF failed"); + // For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In + ERROR_CHECK(this->write_byte(AIC3204_SCLK_MFP3, 0x02), "Set SCLK/MFP3 failed"); + ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_4, 0x01), "Set AUDIO_IF_4 failed"); + ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_5, 0x01), "Set AUDIO_IF_5 failed"); + // Program the DAC processing block to be used - PRB_P1 + ERROR_CHECK(this->write_byte(AIC3204_DAC_SIG_PROC, 0x01), "Set DAC_SIG_PROC failed"); + + // *** Select Page 1 *** + ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x01), "Set page 1 failed"); + // Enable the internal AVDD_LDO: + ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x09), "Set LDO_CTRL failed"); + // *** Program Analog Blocks *** + // Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO + ERROR_CHECK(this->write_byte(AIC3204_PWR_CFG, 0x08), "Set PWR_CFG failed"); + // Enable Master Analog Power Control + ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x01), "Set LDO_CTRL failed"); + // Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v + // We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123) + // Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware + // We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47) + // (All pages refer to the TLV320AIC3204 Application Reference Guide) + ERROR_CHECK(this->write_byte(AIC3204_CM_CTRL, 0x40), "Set CM_CTRL failed"); + // *** Set PowerTune Modes *** + // Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver. + ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG1, 0x00), "Set PLAY_CFG1 failed"); + ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG2, 0x00), "Set PLAY_CFG2 failed"); + // Set the REF charging time to 40ms + ERROR_CHECK(this->write_byte(AIC3204_REF_STARTUP, 0x01), "Set REF_STARTUP failed"); + // HP soft stepping settings for optimal pop performance at power up + // Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling + // capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound. + ERROR_CHECK(this->write_byte(AIC3204_HP_START, 0x25), "Set HP_START failed"); + // Route Left DAC to HPL + ERROR_CHECK(this->write_byte(AIC3204_HPL_ROUTE, 0x08), "Set HPL_ROUTE failed"); + // Route Right DAC to HPR + ERROR_CHECK(this->write_byte(AIC3204_HPR_ROUTE, 0x08), "Set HPR_ROUTE failed"); + // Route Left DAC to LOL + ERROR_CHECK(this->write_byte(AIC3204_LOL_ROUTE, 0x08), "Set LOL_ROUTE failed"); + // Route Right DAC to LOR + ERROR_CHECK(this->write_byte(AIC3204_LOR_ROUTE, 0x08), "Set LOR_ROUTE failed"); + + // Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register) + ERROR_CHECK(this->write_byte(AIC3204_HPL_GAIN, 0x3e), "Set HPL_GAIN failed"); + // Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register) + ERROR_CHECK(this->write_byte(AIC3204_HPR_GAIN, 0x3e), "Set HPR_GAIN failed"); + // Unmute LOL and set gain to 0dB + ERROR_CHECK(this->write_byte(AIC3204_LOL_DRV_GAIN, 0x00), "Set LOL_DRV_GAIN failed"); + // Unmute LOR and set gain to 0dB + ERROR_CHECK(this->write_byte(AIC3204_LOR_DRV_GAIN, 0x00), "Set LOR_DRV_GAIN failed"); + + // Power up HPL and HPR, LOL and LOR drivers + ERROR_CHECK(this->write_byte(AIC3204_OP_PWR_CTRL, 0x3C), "Set OP_PWR_CTRL failed"); + + // Wait for 2.5 sec for soft stepping to take effect before attempting power-up + this->set_timeout(2500, [this]() { + // *** Power Up DAC *** + // Select Page 0 + ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set PAGE_CTRL failed"); + // Power up the Left and Right DAC Channels. Route Left data to Left DAC and Right data to Right DAC. + // DAC Vol control soft step 1 step per DAC word clock. + ERROR_CHECK(this->write_byte(AIC3204_DAC_CH_SET1, 0xd4), "Set DAC_CH_SET1 failed"); + // Set left and right DAC digital volume control + ERROR_CHECK(this->write_volume_(), "Set volume failed"); + // Unmute left and right channels + ERROR_CHECK(this->write_mute_(), "Set mute failed"); + }); +} + +void AIC3204::dump_config() { + ESP_LOGCONFIG(TAG, "AIC3204:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AIC3204 failed"); + } +} + +bool AIC3204::set_mute_off() { + this->is_muted_ = false; + return this->write_mute_(); +} + +bool AIC3204::set_mute_on() { + this->is_muted_ = true; + return this->write_mute_(); +} + +bool AIC3204::set_auto_mute_mode(uint8_t auto_mute_mode) { + this->auto_mute_mode_ = auto_mute_mode & 0x07; + ESP_LOGVV(TAG, "Setting auto_mute_mode to 0x%.2x", this->auto_mute_mode_); + return this->write_mute_(); +} + +bool AIC3204::set_volume(float volume) { + this->volume_ = clamp(volume, 0.0, 1.0); + return this->write_volume_(); +} + +bool AIC3204::is_muted() { return this->is_muted_; } + +float AIC3204::volume() { return this->volume_; } + +bool AIC3204::write_mute_() { + uint8_t mute_mode_byte = this->auto_mute_mode_ << 4; // auto-mute control is bits 4-6 + mute_mode_byte |= this->is_muted_ ? 0x0c : 0x00; // mute bits are 2-3 + if (!this->write_byte(AIC3204_PAGE_CTRL, 0x00) || !this->write_byte(AIC3204_DAC_CH_SET2, mute_mode_byte)) { + ESP_LOGE(TAG, "Writing mute modes failed"); + return false; + } + return true; +} + +bool AIC3204::write_volume_() { + const int8_t dvc_min_byte = -127; + const int8_t dvc_max_byte = 48; + + int8_t volume_byte = dvc_min_byte + (this->volume_ * (dvc_max_byte - dvc_min_byte)); + volume_byte = clamp(volume_byte, dvc_min_byte, dvc_max_byte); + + ESP_LOGVV(TAG, "Setting volume to 0x%.2x", volume_byte & 0xFF); + + if ((!this->write_byte(AIC3204_PAGE_CTRL, 0x00)) || (!this->write_byte(AIC3204_DACL_VOL_D, volume_byte)) || + (!this->write_byte(AIC3204_DACR_VOL_D, volume_byte))) { + ESP_LOGE(TAG, "Writing volume failed"); + return false; + } + return true; +} + +} // namespace aic3204 +} // namespace esphome diff --git a/esphome/components/aic3204/aic3204.h b/esphome/components/aic3204/aic3204.h new file mode 100644 index 0000000000..783a58a2b9 --- /dev/null +++ b/esphome/components/aic3204/aic3204.h @@ -0,0 +1,88 @@ +#pragma once + +#include "esphome/components/audio_dac/audio_dac.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace aic3204 { + +// TLV320AIC3204 Register Addresses +// Page 0 +static const uint8_t AIC3204_PAGE_CTRL = 0x00; // Register 0 - Page Control +static const uint8_t AIC3204_SW_RST = 0x01; // Register 1 - Software Reset +static const uint8_t AIC3204_CLK_PLL1 = 0x04; // Register 4 - Clock Setting Register 1, Multiplexers +static const uint8_t AIC3204_CLK_PLL2 = 0x05; // Register 5 - Clock Setting Register 2, P and R values +static const uint8_t AIC3204_CLK_PLL3 = 0x06; // Register 6 - Clock Setting Register 3, J values +static const uint8_t AIC3204_NDAC = 0x0B; // Register 11 - NDAC Divider Value +static const uint8_t AIC3204_MDAC = 0x0C; // Register 12 - MDAC Divider Value +static const uint8_t AIC3204_DOSR = 0x0E; // Register 14 - DOSR Divider Value (LS Byte) +static const uint8_t AIC3204_NADC = 0x12; // Register 18 - NADC Divider Value +static const uint8_t AIC3204_MADC = 0x13; // Register 19 - MADC Divider Value +static const uint8_t AIC3204_AOSR = 0x14; // Register 20 - AOSR Divider Value +static const uint8_t AIC3204_CODEC_IF = 0x1B; // Register 27 - CODEC Interface Control +static const uint8_t AIC3204_AUDIO_IF_4 = 0x1F; // Register 31 - Audio Interface Setting Register 4 +static const uint8_t AIC3204_AUDIO_IF_5 = 0x20; // Register 32 - Audio Interface Setting Register 5 +static const uint8_t AIC3204_SCLK_MFP3 = 0x38; // Register 56 - SCLK/MFP3 Function Control +static const uint8_t AIC3204_DAC_SIG_PROC = 0x3C; // Register 60 - DAC Sig Processing Block Control +static const uint8_t AIC3204_ADC_SIG_PROC = 0x3D; // Register 61 - ADC Sig Processing Block Control +static const uint8_t AIC3204_DAC_CH_SET1 = 0x3F; // Register 63 - DAC Channel Setup 1 +static const uint8_t AIC3204_DAC_CH_SET2 = 0x40; // Register 64 - DAC Channel Setup 2 +static const uint8_t AIC3204_DACL_VOL_D = 0x41; // Register 65 - DAC Left Digital Vol Control +static const uint8_t AIC3204_DACR_VOL_D = 0x42; // Register 66 - DAC Right Digital Vol Control +static const uint8_t AIC3204_DRC_ENABLE = 0x44; +static const uint8_t AIC3204_ADC_CH_SET = 0x51; // Register 81 - ADC Channel Setup +static const uint8_t AIC3204_ADC_FGA_MUTE = 0x52; // Register 82 - ADC Fine Gain Adjust/Mute + +// Page 1 +static const uint8_t AIC3204_PWR_CFG = 0x01; // Register 1 - Power Config +static const uint8_t AIC3204_LDO_CTRL = 0x02; // Register 2 - LDO Control +static const uint8_t AIC3204_PLAY_CFG1 = 0x03; // Register 3 - Playback Config 1 +static const uint8_t AIC3204_PLAY_CFG2 = 0x04; // Register 4 - Playback Config 2 +static const uint8_t AIC3204_OP_PWR_CTRL = 0x09; // Register 9 - Output Driver Power Control +static const uint8_t AIC3204_CM_CTRL = 0x0A; // Register 10 - Common Mode Control +static const uint8_t AIC3204_HPL_ROUTE = 0x0C; // Register 12 - HPL Routing Select +static const uint8_t AIC3204_HPR_ROUTE = 0x0D; // Register 13 - HPR Routing Select +static const uint8_t AIC3204_LOL_ROUTE = 0x0E; // Register 14 - LOL Routing Selection +static const uint8_t AIC3204_LOR_ROUTE = 0x0F; // Register 15 - LOR Routing Selection +static const uint8_t AIC3204_HPL_GAIN = 0x10; // Register 16 - HPL Driver Gain +static const uint8_t AIC3204_HPR_GAIN = 0x11; // Register 17 - HPR Driver Gain +static const uint8_t AIC3204_LOL_DRV_GAIN = 0x12; // Register 18 - LOL Driver Gain Setting +static const uint8_t AIC3204_LOR_DRV_GAIN = 0x13; // Register 19 - LOR Driver Gain Setting +static const uint8_t AIC3204_HP_START = 0x14; // Register 20 - Headphone Driver Startup +static const uint8_t AIC3204_LPGA_P_ROUTE = 0x34; // Register 52 - Left PGA Positive Input Route +static const uint8_t AIC3204_LPGA_N_ROUTE = 0x36; // Register 54 - Left PGA Negative Input Route +static const uint8_t AIC3204_RPGA_P_ROUTE = 0x37; // Register 55 - Right PGA Positive Input Route +static const uint8_t AIC3204_RPGA_N_ROUTE = 0x39; // Register 57 - Right PGA Negative Input Route +static const uint8_t AIC3204_LPGA_VOL = 0x3B; // Register 59 - Left PGA Volume +static const uint8_t AIC3204_RPGA_VOL = 0x3C; // Register 60 - Right PGA Volume +static const uint8_t AIC3204_ADC_PTM = 0x3D; // Register 61 - ADC Power Tune Config +static const uint8_t AIC3204_AN_IN_CHRG = 0x47; // Register 71 - Analog Input Quick Charging Config +static const uint8_t AIC3204_REF_STARTUP = 0x7B; // Register 123 - Reference Power Up Config + +class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + bool set_mute_off() override; + bool set_mute_on() override; + bool set_auto_mute_mode(uint8_t auto_mute_mode); + bool set_volume(float volume) override; + + bool is_muted() override; + float volume() override; + + protected: + bool write_mute_(); + bool write_volume_(); + + uint8_t auto_mute_mode_{0}; + float volume_{0}; +}; + +} // namespace aic3204 +} // namespace esphome diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py new file mode 100644 index 0000000000..da7a54df54 --- /dev/null +++ b/esphome/components/aic3204/audio_dac.py @@ -0,0 +1,52 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.audio_dac import AudioDac +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MODE + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["i2c"] + +aic3204_ns = cg.esphome_ns.namespace("aic3204") +AIC3204 = aic3204_ns.class_("AIC3204", AudioDac, cg.Component, i2c.I2CDevice) + +SetAutoMuteAction = aic3204_ns.class_("SetAutoMuteAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AIC3204), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x18)) +) + + +SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(AIC3204), + cv.Required(CONF_MODE): cv.templatable(cv.int_range(max=7, min=0)), + }, + key=CONF_MODE, +) + + +@automation.register_action( + "aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA +) +async def aic3204_set_volume_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config.get(CONF_MODE), args, int) + cg.add(var.set_auto_mute_mode(template_)) + + 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) diff --git a/esphome/components/aic3204/automation.h b/esphome/components/aic3204/automation.h new file mode 100644 index 0000000000..416a88fa12 --- /dev/null +++ b/esphome/components/aic3204/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "aic3204.h" + +namespace esphome { +namespace aic3204 { + +template class SetAutoMuteAction : public Action { + public: + explicit SetAutoMuteAction(AIC3204 *aic3204) : aic3204_(aic3204) {} + + TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) + + void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } + + protected: + AIC3204 *aic3204_; +}; + +} // namespace aic3204 +} // namespace esphome diff --git a/esphome/components/airthings_ble/airthings_listener.cpp b/esphome/components/airthings_ble/airthings_listener.cpp index 951961cb1b..a36d614df5 100644 --- a/esphome/components/airthings_ble/airthings_listener.cpp +++ b/esphome/components/airthings_ble/airthings_listener.cpp @@ -1,5 +1,6 @@ #include "airthings_listener.h" #include "esphome/core/log.h" +#include #ifdef USE_ESP32 @@ -19,7 +20,7 @@ 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:%u (MAC: %s)", sn, device.address_str().c_str()); + ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str().c_str()); return true; } } diff --git a/esphome/components/airthings_wave_base/__init__.py b/esphome/components/airthings_wave_base/__init__.py new file mode 100644 index 0000000000..d9b97f1c8d --- /dev/null +++ b/esphome/components/airthings_wave_base/__init__.py @@ -0,0 +1,103 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client + +from esphome.const import ( + CONF_BATTERY_VOLTAGE, + CONF_HUMIDITY, + CONF_PRESSURE, + CONF_TEMPERATURE, + CONF_TVOC, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_PARTS_PER_BILLION, + UNIT_PERCENT, + UNIT_VOLT, +) + +CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"] + +DEPENDENCIES = ["ble_client"] + +CONF_BATTERY_UPDATE_INTERVAL = "battery_update_interval" + +airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base") +AirthingsWaveBase = airthings_wave_base_ns.class_( + "AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode +) + + +BASE_SCHEMA = ( + sensor.SENSOR_SCHEMA.extend( + { + 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_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_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + state_class=STATE_CLASS_MEASUREMENT, + ), + 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, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional( + CONF_BATTERY_UPDATE_INTERVAL, + default="24h", + ): cv.update_interval, + } + ) + .extend(cv.polling_component_schema("5min")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +async def wave_base_to_code(var, config): + await cg.register_component(var, config) + + await ble_client.register_ble_node(var, config) + + if config_humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(config_humidity) + cg.add(var.set_humidity(sens)) + if config_temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(config_temperature) + cg.add(var.set_temperature(sens)) + if config_pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(config_pressure) + cg.add(var.set_pressure(sens)) + if config_tvoc := config.get(CONF_TVOC): + sens = await sensor.new_sensor(config_tvoc) + cg.add(var.set_tvoc(sens)) + if config_battery_voltage := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(config_battery_voltage) + cg.add(var.set_battery_voltage(sens)) + if config_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL): + cg.add(var.set_battery_update_interval(config_battery_update_interval)) diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.cpp b/esphome/components/airthings_wave_base/airthings_wave_base.cpp new file mode 100644 index 0000000000..16789ff454 --- /dev/null +++ b/esphome/components/airthings_wave_base/airthings_wave_base.cpp @@ -0,0 +1,211 @@ +#include "airthings_wave_base.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) + +#ifdef USE_ESP32 + +namespace esphome { +namespace airthings_wave_base { + +static const char *const TAG = "airthings_wave_base"; + +void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "Connected successfully!"); + } + break; + } + + case ESP_GATTC_DISCONNECT_EVT: { + this->handle_ = 0; + this->acp_handle_ = 0; + this->cccd_handle_ = 0; + ESP_LOGW(TAG, "Disconnected!"); + break; + } + + case ESP_GATTC_SEARCH_CMPL_EVT: { + if (this->request_read_values_()) { + if (!this->read_battery_next_update_) { + this->node_state = espbt::ClientState::ESTABLISHED; + } else { + // delay setting node_state to ESTABLISHED until confirmation of the notify registration + this->request_battery_(); + } + } + + // ensure that the client will be disconnected even if no responses arrive + this->set_response_timeout_(); + + 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); + break; + } + if (param->read.handle == this->handle_) { + this->read_sensors(param->read.value, param->read.value_len); + } + break; + } + + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->get_conn_id()) + break; + if (param->notify.handle == this->acp_handle_) { + this->read_battery_(param->notify.value, param->notify.value_len); + } + break; + } + + default: + break; + } +} + +bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return voc <= 16383; } + +void AirthingsWaveBase::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + if (!this->parent()->enabled) { + ESP_LOGW(TAG, "Reconnecting to device"); + this->parent()->set_enabled(true); + this->parent()->connect(); + } else { + ESP_LOGW(TAG, "Connection in progress"); + } + } +} + +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()); + return false; + } + + this->handle_ = chr->handle; + + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, + ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + return false; + } + + this->response_pending_(); + return true; +} + +bool AirthingsWaveBase::request_battery_() { + uint8_t battery_command = ACCESS_CONTROL_POINT_COMMAND; + uint8_t cccd_value[2] = {1, 0}; + + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_); + if (chr == nullptr) { + 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()); + 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()); + return false; + } + + auto reg_status = + esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), chr->handle); + if (reg_status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", reg_status); + return false; + } + + this->acp_handle_ = chr->handle; + this->cccd_handle_ = descr->handle; + + auto descr_status = + esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->cccd_handle_, + 2, cccd_value, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (descr_status) { + ESP_LOGW(TAG, "Error sending CCC descriptor write request, status=%d", descr_status); + return false; + } + + auto chr_status = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->acp_handle_, 1, + &battery_command, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (chr_status) { + ESP_LOGW(TAG, "Error sending read request for battery, status=%d", chr_status); + return false; + } + + this->response_pending_(); + return true; +} + +void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) { + auto *value = (AccessControlPointResponse *) (&raw_value[2]); + + if ((value_len >= (sizeof(AccessControlPointResponse) + 2)) && (raw_value[0] == ACCESS_CONTROL_POINT_COMMAND)) { + ESP_LOGD(TAG, "Battery received: %u mV", (unsigned int) value->battery); + + if (this->battery_voltage_ != nullptr) { + float voltage = value->battery / 1000.0f; + + this->battery_voltage_->publish_state(voltage); + } + + // read the battery again at the configured update interval + if (this->battery_update_interval_ != this->update_interval_) { + this->read_battery_next_update_ = false; + this->set_timeout("battery", this->battery_update_interval_, + [this]() { this->read_battery_next_update_ = true; }); + } + } + + this->response_received_(); +} + +void AirthingsWaveBase::response_pending_() { + this->responses_pending_++; + this->set_response_timeout_(); +} + +void AirthingsWaveBase::response_received_() { + if (--this->responses_pending_ == 0) { + // This instance must not stay connected + // so other clients can connect to it (e.g. the + // mobile app). + this->parent()->set_enabled(false); + } +} + +void AirthingsWaveBase::set_response_timeout_() { + this->set_timeout("response_timeout", 30 * 1000, [this]() { + this->responses_pending_ = 1; + this->response_received_(); + }); +} + +} // namespace airthings_wave_base +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.h b/esphome/components/airthings_wave_base/airthings_wave_base.h new file mode 100644 index 0000000000..1dc2e1f71f --- /dev/null +++ b/esphome/components/airthings_wave_base/airthings_wave_base.h @@ -0,0 +1,90 @@ +#pragma once + +// All information related to reading battery levels came from the sensors.airthings_wave +// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave) + +#ifdef USE_ESP32 + +#include +#include +#include +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace airthings_wave_base { + +namespace espbt = esphome::esp32_ble_tracker; + +static const uint8_t ACCESS_CONTROL_POINT_COMMAND = 0x6d; +static const auto CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID = espbt::ESPBTUUID::from_uint16(0x2902); + +class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode { + public: + AirthingsWaveBase() = default; + + void update() override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_battery_voltage(sensor::Sensor *voltage) { + battery_voltage_ = voltage; + this->read_battery_next_update_ = true; + } + void set_battery_update_interval(uint32_t interval) { battery_update_interval_ = interval; } + + protected: + bool is_valid_voc_value_(uint16_t voc); + + bool request_read_values_(); + virtual void read_sensors(uint8_t *raw_value, uint16_t value_len) = 0; + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; + + uint16_t handle_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID sensors_data_characteristic_uuid_; + + uint16_t acp_handle_{0}; + uint16_t cccd_handle_{0}; + espbt::ESPBTUUID access_control_point_characteristic_uuid_; + + uint8_t responses_pending_{0}; + void response_pending_(); + void response_received_(); + void set_response_timeout_(); + + // default to *not* reading battery voltage from the device; the + // set_* function for the battery sensor will set this to 'true' + bool read_battery_next_update_{false}; + bool request_battery_(); + void read_battery_(uint8_t *raw_value, uint16_t value_len); + uint32_t battery_update_interval_{}; + + struct AccessControlPointResponse { + uint32_t unused1; + uint8_t unused2; + uint8_t illuminance; + uint8_t unused3[10]; + uint16_t unused4[4]; + uint16_t battery; + uint16_t unused5; + }; +}; + +} // namespace airthings_wave_base +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp index 40873ec005..873826d06c 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp @@ -7,105 +7,47 @@ namespace airthings_wave_mini { static const char *const TAG = "airthings_wave_mini"; -void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - switch (event) { - case ESP_GATTC_OPEN_EVT: { - if (param->open.status == ESP_GATT_OK) { - ESP_LOGI(TAG, "Connected successfully!"); - } - break; - } - - case ESP_GATTC_DISCONNECT_EVT: { - ESP_LOGW(TAG, "Disconnected!"); - break; - } - - case ESP_GATTC_SEARCH_CMPL_EVT: { - this->handle_ = 0; - auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); - if (chr == nullptr) { - ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), - sensors_data_characteristic_uuid_.to_string().c_str()); - break; - } - this->handle_ = chr->handle; - this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; - - request_read_values_(); - 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); - break; - } - if (param->read.handle == this->handle_) { - read_sensors_(param->read.value, param->read.value_len); - } - break; - } - - default: - break; - } -} - -void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) { +void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) { auto *value = (WaveMiniReadings *) raw_value; if (sizeof(WaveMiniReadings) <= value_len) { - this->humidity_sensor_->publish_state(value->humidity / 100.0f); - this->pressure_sensor_->publish_state(value->pressure / 50.0f); - this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); - if (is_valid_voc_value_(value->voc)) { + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->publish_state(value->humidity / 100.0f); + } + + if (this->pressure_sensor_ != nullptr) { + this->pressure_sensor_->publish_state(value->pressure / 50.0f); + } + + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); + } + + if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { this->tvoc_sensor_->publish_state(value->voc); } - - // This instance must not stay connected - // so other clients can connect to it (e.g. the - // mobile app). - parent()->set_enabled(false); } -} -bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } - -void AirthingsWaveMini::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 AirthingsWaveMini::request_read_values_() { - auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, - ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); - } + this->response_received_(); } void AirthingsWaveMini::dump_config() { + // these really don't belong here, but there doesn't seem to be a + // practical way to have the base class use LOG_SENSOR and include + // the TAG from this component LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); } -AirthingsWaveMini::AirthingsWaveMini() - : PollingComponent(10000), - service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), - sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} +AirthingsWaveMini::AirthingsWaveMini() { + this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); + this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->access_control_point_characteristic_uuid_ = + espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); +} } // namespace airthings_wave_mini } // namespace esphome diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.h b/esphome/components/airthings_wave_mini/airthings_wave_mini.h index 128774f9cb..825ddbdc69 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.h +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.h @@ -2,50 +2,25 @@ #ifdef USE_ESP32 -#include -#include -#include -#include "esphome/components/ble_client/ble_client.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/core/component.h" -#include "esphome/core/log.h" +#include "esphome/components/airthings_wave_base/airthings_wave_base.h" namespace esphome { namespace airthings_wave_mini { +namespace espbt = esphome::esp32_ble_tracker; + static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; +static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e3ef4-ade7-11e4-89d3-123b93f75cba"; -class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode { +class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase { public: AirthingsWaveMini(); void dump_config() override; - void update() override; - - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override; - - void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } - void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } - void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } - void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } protected: - bool is_valid_voc_value_(uint16_t voc); - - void read_sensors_(uint8_t *value, uint16_t value_len); - void request_read_values_(); - - sensor::Sensor *temperature_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; - sensor::Sensor *pressure_sensor_{nullptr}; - sensor::Sensor *tvoc_sensor_{nullptr}; - - uint16_t handle_; - esp32_ble_tracker::ESPBTUUID service_uuid_; - esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; + void read_sensors(uint8_t *raw_value, uint16_t value_len) override; struct WaveMiniReadings { uint16_t unused01; diff --git a/esphome/components/airthings_wave_mini/sensor.py b/esphome/components/airthings_wave_mini/sensor.py index d38354fa84..0f4fd1a13a 100644 --- a/esphome/components/airthings_wave_mini/sensor.py +++ b/esphome/components/airthings_wave_mini/sensor.py @@ -1,82 +1,28 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, ble_client +from esphome.components import airthings_wave_base from esphome.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, - UNIT_PERCENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, CONF_ID, - CONF_HUMIDITY, - CONF_TVOC, - CONF_PRESSURE, - CONF_TEMPERATURE, - UNIT_PARTS_PER_BILLION, - ICON_RADIATOR, ) -DEPENDENCIES = ["ble_client"] +DEPENDENCIES = airthings_wave_base.DEPENDENCIES + +AUTO_LOAD = ["airthings_wave_base"] airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") AirthingsWaveMini = airthings_wave_mini_ns.class_( - "AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode + "AirthingsWaveMini", airthings_wave_base.AirthingsWaveBase ) -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(AirthingsWaveMini), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=2, - ), - 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_PRESSURE): sensor.sensor_schema( - unit_of_measurement=UNIT_HECTOPASCAL, - accuracy_decimals=2, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_TVOC): sensor.sensor_schema( - unit_of_measurement=UNIT_PARTS_PER_BILLION, - icon=ICON_RADIATOR, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - } - ) - .extend(cv.polling_component_schema("5min")) - .extend(ble_client.BLE_CLIENT_SCHEMA), +CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AirthingsWaveMini), + } ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - - await ble_client.register_ble_node(var, config) - - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) - cg.add(var.set_humidity(sens)) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) - cg.add(var.set_temperature(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) - cg.add(var.set_pressure(sens)) - if CONF_TVOC in config: - sens = await sensor.new_sensor(config[CONF_TVOC]) - cg.add(var.set_tvoc(sens)) + await airthings_wave_base.wave_base_to_code(var, config) diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index 11f86307fe..8c8c514fdb 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -7,129 +7,78 @@ namespace airthings_wave_plus { static const char *const TAG = "airthings_wave_plus"; -void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - switch (event) { - case ESP_GATTC_OPEN_EVT: { - if (param->open.status == ESP_GATT_OK) { - ESP_LOGI(TAG, "Connected successfully!"); - } - break; - } - - case ESP_GATTC_DISCONNECT_EVT: { - ESP_LOGW(TAG, "Disconnected!"); - break; - } - - case ESP_GATTC_SEARCH_CMPL_EVT: { - this->handle_ = 0; - auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); - if (chr == nullptr) { - ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), - sensors_data_characteristic_uuid_.to_string().c_str()); - break; - } - this->handle_ = chr->handle; - this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; - - request_read_values_(); - 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); - break; - } - if (param->read.handle == this->handle_) { - read_sensors_(param->read.value, param->read.value_len); - } - break; - } - - default: - break; - } -} - -void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { +void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { auto *value = (WavePlusReadings *) raw_value; if (sizeof(WavePlusReadings) <= value_len) { ESP_LOGD(TAG, "version = %d", value->version); if (value->version == 1) { - ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->publish_state(value->humidity / 2.0f); + } - this->humidity_sensor_->publish_state(value->humidity / 2.0f); - if (is_valid_radon_value_(value->radon)) { + if ((this->radon_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon)) { this->radon_sensor_->publish_state(value->radon); } - if (is_valid_radon_value_(value->radon_lt)) { + + if ((this->radon_long_term_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon_lt)) { this->radon_long_term_sensor_->publish_state(value->radon_lt); } - this->temperature_sensor_->publish_state(value->temperature / 100.0f); - this->pressure_sensor_->publish_state(value->pressure / 50.0f); - if (is_valid_co2_value_(value->co2)) { + + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(value->temperature / 100.0f); + } + + if (this->pressure_sensor_ != nullptr) { + this->pressure_sensor_->publish_state(value->pressure / 50.0f); + } + + if ((this->co2_sensor_ != nullptr) && this->is_valid_co2_value_(value->co2)) { this->co2_sensor_->publish_state(value->co2); } - if (is_valid_voc_value_(value->voc)) { + + if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { this->tvoc_sensor_->publish_state(value->voc); } - // This instance must not stay connected - // so other clients can connect to it (e.g. the - // mobile app). - parent()->set_enabled(false); + if (this->illuminance_sensor_ != nullptr) { + this->illuminance_sensor_->publish_state(value->ambientLight); + } } else { ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); } } + + this->response_received_(); } -bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } +bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return radon <= 16383; } -bool AirthingsWavePlus::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } - -bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; } - -void AirthingsWavePlus::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 AirthingsWavePlus::request_read_values_() { - auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, - ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); - } -} +bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return co2 <= 16383; } void AirthingsWavePlus::dump_config() { + // these really don't belong here, but there doesn't seem to be a + // practical way to have the base class use LOG_SENSOR and include + // the TAG from this component LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); - LOG_SENSOR(" ", "Radon", this->radon_sensor_); - LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); - LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); + + LOG_SENSOR(" ", "Radon", this->radon_sensor_); + LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); } -AirthingsWavePlus::AirthingsWavePlus() - : PollingComponent(10000), - service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), - sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} +AirthingsWavePlus::AirthingsWavePlus() { + this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); + this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->access_control_point_characteristic_uuid_ = + espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); +} } // namespace airthings_wave_plus } // namespace esphome diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h index 9dd6ed92d5..bd7a40ef8b 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.h +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -2,58 +2,38 @@ #ifdef USE_ESP32 -#include -#include -#include -#include "esphome/components/ble_client/ble_client.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/core/component.h" -#include "esphome/core/log.h" +#include "esphome/components/airthings_wave_base/airthings_wave_base.h" namespace esphome { namespace airthings_wave_plus { +namespace espbt = esphome::esp32_ble_tracker; + static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; +static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba"; -class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode { +class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { public: AirthingsWavePlus(); void dump_config() override; - void update() override; - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override; - - void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } - void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } - void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } - void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; } protected: bool is_valid_radon_value_(uint16_t radon); - bool is_valid_voc_value_(uint16_t voc); bool is_valid_co2_value_(uint16_t co2); - void read_sensors_(uint8_t *value, uint16_t value_len); - void request_read_values_(); + void read_sensors(uint8_t *raw_value, uint16_t value_len) override; - sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; - sensor::Sensor *pressure_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr}; - sensor::Sensor *tvoc_sensor_{nullptr}; - - uint16_t handle_; - esp32_ble_tracker::ESPBTUUID service_uuid_; - esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; + sensor::Sensor *illuminance_sensor_{nullptr}; struct WavePlusReadings { uint8_t version; diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py index 727fbe15fb..d28c7e2abc 100644 --- a/esphome/components/airthings_wave_plus/sensor.py +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -1,116 +1,76 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, ble_client +from esphome.components import sensor, airthings_wave_base from esphome.const import ( DEVICE_CLASS_CARBON_DIOXIDE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_PRESSURE, STATE_CLASS_MEASUREMENT, - UNIT_PERCENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, ICON_RADIOACTIVE, CONF_ID, CONF_RADON, CONF_RADON_LONG_TERM, - CONF_HUMIDITY, - CONF_TVOC, CONF_CO2, - CONF_PRESSURE, - CONF_TEMPERATURE, UNIT_BECQUEREL_PER_CUBIC_METER, UNIT_PARTS_PER_MILLION, - UNIT_PARTS_PER_BILLION, - ICON_RADIATOR, + CONF_ILLUMINANCE, + UNIT_LUX, + DEVICE_CLASS_ILLUMINANCE, ) -DEPENDENCIES = ["ble_client"] +DEPENDENCIES = airthings_wave_base.DEPENDENCIES + +AUTO_LOAD = ["airthings_wave_base"] airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") AirthingsWavePlus = airthings_wave_plus_ns.class_( - "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode + "AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase ) -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(AirthingsWavePlus), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=0, - ), - cv.Optional(CONF_RADON): sensor.sensor_schema( - unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, - icon=ICON_RADIOACTIVE, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( - unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, - icon=ICON_RADIOACTIVE, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - 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_PRESSURE): sensor.sensor_schema( - unit_of_measurement=UNIT_HECTOPASCAL, - accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CO2): sensor.sensor_schema( - unit_of_measurement=UNIT_PARTS_PER_MILLION, - accuracy_decimals=0, - device_class=DEVICE_CLASS_CARBON_DIOXIDE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_TVOC): sensor.sensor_schema( - unit_of_measurement=UNIT_PARTS_PER_BILLION, - icon=ICON_RADIATOR, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - } - ) - .extend(cv.polling_component_schema("5min")) - .extend(ble_client.BLE_CLIENT_SCHEMA), +CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AirthingsWavePlus), + cv.Optional(CONF_RADON): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) + await airthings_wave_base.wave_base_to_code(var, config) - await ble_client.register_ble_node(var, config) - - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) - cg.add(var.set_humidity(sens)) - if CONF_RADON in config: - sens = await sensor.new_sensor(config[CONF_RADON]) + if config_radon := config.get(CONF_RADON): + sens = await sensor.new_sensor(config_radon) cg.add(var.set_radon(sens)) - if CONF_RADON_LONG_TERM in config: - sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) + if config_radon_long_term := config.get(CONF_RADON_LONG_TERM): + sens = await sensor.new_sensor(config_radon_long_term) cg.add(var.set_radon_long_term(sens)) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) - cg.add(var.set_temperature(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) - cg.add(var.set_pressure(sens)) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if config_co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(config_co2) cg.add(var.set_co2(sens)) - if CONF_TVOC in config: - sens = await sensor.new_sensor(config[CONF_TVOC]) - cg.add(var.set_tvoc(sens)) + if config_illuminance := config.get(CONF_ILLUMINANCE): + sens = await sensor.new_sensor(config_illuminance) + cg.add(var.set_illuminance(sens)) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py new file mode 100644 index 0000000000..379fbf32f9 --- /dev/null +++ b/esphome/components/alarm_control_panel/__init__.py @@ -0,0 +1,316 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv +from esphome.const import ( + CONF_CODE, + CONF_ID, + CONF_MQTT_ID, + CONF_ON_STATE, + CONF_TRIGGER_ID, + CONF_WEB_SERVER, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@grahambrown11", "@hwstar"] +IS_PLATFORM_COMPONENT = True + +CONF_ON_TRIGGERED = "on_triggered" +CONF_ON_CLEARED = "on_cleared" +CONF_ON_ARMING = "on_arming" +CONF_ON_PENDING = "on_pending" +CONF_ON_ARMED_HOME = "on_armed_home" +CONF_ON_ARMED_NIGHT = "on_armed_night" +CONF_ON_ARMED_AWAY = "on_armed_away" +CONF_ON_DISARMED = "on_disarmed" +CONF_ON_CHIME = "on_chime" +CONF_ON_READY = "on_ready" + +alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") +AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) + +StateTrigger = alarm_control_panel_ns.class_( + "StateTrigger", automation.Trigger.template() +) +TriggeredTrigger = alarm_control_panel_ns.class_( + "TriggeredTrigger", automation.Trigger.template() +) +ClearedTrigger = alarm_control_panel_ns.class_( + "ClearedTrigger", automation.Trigger.template() +) +ArmingTrigger = alarm_control_panel_ns.class_( + "ArmingTrigger", automation.Trigger.template() +) +PendingTrigger = alarm_control_panel_ns.class_( + "PendingTrigger", automation.Trigger.template() +) +ArmedHomeTrigger = alarm_control_panel_ns.class_( + "ArmedHomeTrigger", automation.Trigger.template() +) +ArmedNightTrigger = alarm_control_panel_ns.class_( + "ArmedNightTrigger", automation.Trigger.template() +) +ArmedAwayTrigger = alarm_control_panel_ns.class_( + "ArmedAwayTrigger", automation.Trigger.template() +) +DisarmedTrigger = alarm_control_panel_ns.class_( + "DisarmedTrigger", automation.Trigger.template() +) +ChimeTrigger = alarm_control_panel_ns.class_( + "ChimeTrigger", automation.Trigger.template() +) +ReadyTrigger = alarm_control_panel_ns.class_( + "ReadyTrigger", automation.Trigger.template() +) + +ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) +ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) +ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action) +DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action) +PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action) +TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action) +ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action) +ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action) + +AlarmControlPanelCondition = alarm_control_panel_ns.class_( + "AlarmControlPanelCondition", automation.Condition +) + +ALARM_CONTROL_PANEL_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(AlarmControlPanel), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( + mqtt.MQTTAlarmControlPanelComponent + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + } + ), + cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), + } + ), + cv.Optional(CONF_ON_ARMING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), + } + ), + cv.Optional(CONF_ON_PENDING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), + } + ), + cv.Optional(CONF_ON_DISARMED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), + } + ), + cv.Optional(CONF_ON_CLEARED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), + } + ), + cv.Optional(CONF_ON_CHIME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), + } + ), + cv.Optional(CONF_ON_READY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), + } + ), + } + ) +) + +ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(AlarmControlPanel), + cv.Optional(CONF_CODE): cv.templatable(cv.string), + } +) + +ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(AlarmControlPanel), + } +) + + +async def setup_alarm_control_panel_core_(var, config): + await setup_entity(var, config) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TRIGGERED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ARMING, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PENDING, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ARMED_HOME, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ARMED_NIGHT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ARMED_AWAY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_DISARMED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CLEARED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CHIME, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_READY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + if mqtt_id := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id, var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_alarm_control_panel(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_alarm_control_panel(var)) + await setup_alarm_control_panel_core_(var, config) + + +@automation.register_action( + "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if code_config := config.get(CONF_CODE): + templatable_ = await cg.templatable(code_config, args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if code_config := config.get(CONF_CODE): + templatable_ = await cg.templatable(code_config, args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_arm_night_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_CODE in config: + templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_disarm_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if code_config := config.get(CONF_CODE): + templatable_ = await cg.templatable(code_config, args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_pending_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_trigger_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_chime_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +@automation.register_condition( + "alarm_control_panel.ready", + AlarmControlPanelCondition, + ALARM_CONTROL_PANEL_CONDITION_SCHEMA, +) +async def alarm_action_ready_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_condition( + "alarm_control_panel.is_armed", + AlarmControlPanelCondition, + ALARM_CONTROL_PANEL_CONDITION_SCHEMA, +) +async def alarm_control_panel_is_armed_to_code( + config, condition_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(alarm_control_panel_ns.using) + cg.add_define("USE_ALARM_CONTROL_PANEL") diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp new file mode 100644 index 0000000000..9f1485ee90 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -0,0 +1,156 @@ +#include + +#include "alarm_control_panel.h" + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +static const char *const TAG = "alarm_control_panel"; + +AlarmControlPanelCall AlarmControlPanel::make_call() { return AlarmControlPanelCall(this); } + +bool AlarmControlPanel::is_state_armed(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_ARMED_AWAY: + case ACP_STATE_ARMED_HOME: + case ACP_STATE_ARMED_NIGHT: + case ACP_STATE_ARMED_VACATION: + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return true; + default: + return false; + } +}; + +void AlarmControlPanel::publish_state(AlarmControlPanelState state) { + this->last_update_ = millis(); + if (state != this->current_state_) { + auto prev_state = this->current_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; + this->state_callback_.call(); + 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(); + } + + if (prev_state == ACP_STATE_TRIGGERED) { + this->cleared_callback_.call(); + } + if (state == this->desired_state_) { + // only store when in the desired state + this->pref_.save(&state); + } + } +} + +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)); +} + +void AlarmControlPanel::add_on_chime_callback(std::function &&callback) { + this->chime_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::add_on_ready_callback(std::function &&callback) { + this->ready_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::arm_away(optional code) { + auto call = this->make_call(); + call.arm_away(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_home(optional code) { + auto call = this->make_call(); + call.arm_home(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_night(optional code) { + auto call = this->make_call(); + call.arm_night(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_vacation(optional code) { + auto call = this->make_call(); + call.arm_vacation(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_custom_bypass(optional code) { + auto call = this->make_call(); + call.arm_custom_bypass(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::disarm(optional code) { + auto call = this->make_call(); + call.disarm(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h new file mode 100644 index 0000000000..85c2b2148e --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -0,0 +1,200 @@ +#pragma once + +#include + +#include "alarm_control_panel_call.h" +#include "alarm_control_panel_state.h" + +#include "esphome/core/automation.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +enum AlarmControlPanelFeature : uint8_t { + // Matches Home Assistant values + ACP_FEAT_ARM_HOME = 1 << 0, + ACP_FEAT_ARM_AWAY = 1 << 1, + ACP_FEAT_ARM_NIGHT = 1 << 2, + ACP_FEAT_TRIGGER = 1 << 3, + ACP_FEAT_ARM_CUSTOM_BYPASS = 1 << 4, + ACP_FEAT_ARM_VACATION = 1 << 5, +}; + +class AlarmControlPanel : public EntityBase { + public: + /** Make a AlarmControlPanelCall + * + */ + AlarmControlPanelCall make_call(); + + /** Set the state of the alarm_control_panel. + * + * @param state The AlarmControlPanelState. + */ + void publish_state(AlarmControlPanelState state); + + /** Add a callback for when the state of the alarm_control_panel changes + * + * @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 + */ + void add_on_cleared_callback(std::function &&callback); + + /** Add a callback for when a chime zone goes from closed to open + * + * @param callback The callback function + */ + void add_on_chime_callback(std::function &&callback); + + /** Add a callback for when a ready state changes + * + * @param callback The callback function + */ + void add_on_ready_callback(std::function &&callback); + + /** A numeric representation of the supported features as per HomeAssistant + * + */ + virtual uint32_t get_supported_features() const = 0; + + /** Returns if the alarm_control_panel has a code + * + */ + virtual bool get_requires_code() const = 0; + + /** Returns if the alarm_control_panel requires a code to arm + * + */ + virtual bool get_requires_code_to_arm() const = 0; + + /** arm the alarm in away mode + * + * @param code The code + */ + void arm_away(optional code = nullopt); + + /** arm the alarm in home mode + * + * @param code The code + */ + void arm_home(optional code = nullopt); + + /** arm the alarm in night mode + * + * @param code The code + */ + void arm_night(optional code = nullopt); + + /** arm the alarm in vacation mode + * + * @param code The code + */ + void arm_vacation(optional code = nullopt); + + /** arm the alarm in custom bypass mode + * + * @param code The code + */ + void arm_custom_bypass(optional code = nullopt); + + /** disarm the alarm + * + * @param code The code + */ + void disarm(optional code = nullopt); + + /** Get the state + * + */ + AlarmControlPanelState get_state() const { return this->current_state_; } + + // is the state one of the armed states + bool is_state_armed(AlarmControlPanelState state); + + protected: + friend AlarmControlPanelCall; + // in order to store last panel state in flash + ESPPreferenceObject pref_; + // current state + AlarmControlPanelState current_state_; + // the desired (or previous) state + AlarmControlPanelState desired_state_; + // last time the state was updated + 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_{}; + // chime callback + CallbackManager chime_callback_{}; + // ready callback + CallbackManager ready_callback_{}; +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp new file mode 100644 index 0000000000..b1d2b2a097 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -0,0 +1,104 @@ +#include "alarm_control_panel_call.h" + +#include "alarm_control_panel.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +static const char *const TAG = "alarm_control_panel"; + +AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {} + +AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) { + this->code_ = code; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_away() { + this->state_ = ACP_STATE_ARMED_AWAY; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_home() { + this->state_ = ACP_STATE_ARMED_HOME; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_night() { + this->state_ = ACP_STATE_ARMED_NIGHT; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_vacation() { + this->state_ = ACP_STATE_ARMED_VACATION; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_custom_bypass() { + this->state_ = ACP_STATE_ARMED_CUSTOM_BYPASS; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::disarm() { + this->state_ = ACP_STATE_DISARMED; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::pending() { + this->state_ = ACP_STATE_PENDING; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::triggered() { + this->state_ = ACP_STATE_TRIGGERED; + return *this; +} + +const optional &AlarmControlPanelCall::get_state() const { return this->state_; } +const optional &AlarmControlPanelCall::get_code() const { return this->code_; } + +void AlarmControlPanelCall::validate_() { + if (this->state_.has_value()) { + auto state = *this->state_; + if (this->parent_->is_state_armed(state) && this->parent_->get_state() != ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot arm when not disarmed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_PENDING && this->parent_->get_state() == ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot trip alarm when disarmed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_DISARMED && + !(this->parent_->is_state_armed(this->parent_->get_state()) || + this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_ARMING || + this->parent_->get_state() == ACP_STATE_TRIGGERED)) { + ESP_LOGW(TAG, "Cannot disarm when not armed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_ARMED_HOME && (this->parent_->get_supported_features() & ACP_FEAT_ARM_HOME) == 0) { + ESP_LOGW(TAG, "Cannot arm home when not supported"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_ARMED_NIGHT && (this->parent_->get_supported_features() & ACP_FEAT_ARM_NIGHT) == 0) { + ESP_LOGW(TAG, "Cannot arm night when not supported"); + this->state_.reset(); + return; + } + } +} + +void AlarmControlPanelCall::perform() { + this->validate_(); + if (this->state_) { + this->parent_->control(*this); + } +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h new file mode 100644 index 0000000000..034e3142da --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "alarm_control_panel_state.h" + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace alarm_control_panel { + +class AlarmControlPanel; + +class AlarmControlPanelCall { + public: + AlarmControlPanelCall(AlarmControlPanel *parent); + + AlarmControlPanelCall &set_code(const std::string &code); + AlarmControlPanelCall &arm_away(); + AlarmControlPanelCall &arm_home(); + AlarmControlPanelCall &arm_night(); + AlarmControlPanelCall &arm_vacation(); + AlarmControlPanelCall &arm_custom_bypass(); + AlarmControlPanelCall &disarm(); + AlarmControlPanelCall &pending(); + AlarmControlPanelCall &triggered(); + + void perform(); + const optional &get_state() const; + const optional &get_code() const; + + protected: + AlarmControlPanel *parent_; + optional code_{}; + optional state_{}; + void validate_(); +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp new file mode 100644 index 0000000000..abe6f51995 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -0,0 +1,34 @@ +#include "alarm_control_panel_state.h" + +namespace esphome { +namespace alarm_control_panel { + +const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_DISARMED: + return LOG_STR("DISARMED"); + case ACP_STATE_ARMED_HOME: + return LOG_STR("ARMED_HOME"); + case ACP_STATE_ARMED_AWAY: + return LOG_STR("ARMED_AWAY"); + case ACP_STATE_ARMED_NIGHT: + return LOG_STR("ARMED_NIGHT"); + case ACP_STATE_ARMED_VACATION: + return LOG_STR("ARMED_VACATION"); + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return LOG_STR("ARMED_CUSTOM_BYPASS"); + case ACP_STATE_PENDING: + return LOG_STR("PENDING"); + case ACP_STATE_ARMING: + return LOG_STR("ARMING"); + case ACP_STATE_DISARMING: + return LOG_STR("DISARMING"); + case ACP_STATE_TRIGGERED: + return LOG_STR("TRIGGERED"); + default: + return LOG_STR("UNKNOWN"); + } +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.h b/esphome/components/alarm_control_panel/alarm_control_panel_state.h new file mode 100644 index 0000000000..ad16222dc0 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +enum AlarmControlPanelState : uint8_t { + ACP_STATE_DISARMED = 0, + ACP_STATE_ARMED_HOME = 1, + ACP_STATE_ARMED_AWAY = 2, + ACP_STATE_ARMED_NIGHT = 3, + ACP_STATE_ARMED_VACATION = 4, + ACP_STATE_ARMED_CUSTOM_BYPASS = 5, + ACP_STATE_PENDING = 6, + ACP_STATE_ARMING = 7, + ACP_STATE_DISARMING = 8, + ACP_STATE_TRIGGERED = 9 +}; + +/** Returns a string representation of the state. + * + * @param state The AlarmControlPanelState. + */ +const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state); + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h new file mode 100644 index 0000000000..2177fb710f --- /dev/null +++ b/esphome/components/alarm_control_panel/automation.h @@ -0,0 +1,191 @@ + +#pragma once +#include "esphome/core/automation.h" +#include "alarm_control_panel.h" + +namespace esphome { +namespace alarm_control_panel { + +class StateTrigger : public Trigger<> { + public: + explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); + } +}; + +class TriggeredTrigger : public Trigger<> { + public: + explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); }); + } +}; + +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(); }); + } +}; + +class ClearedTrigger : public Trigger<> { + public: + explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); }); + } +}; + +class ChimeTrigger : public Trigger<> { + public: + explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); }); + } +}; + +class ReadyTrigger : public Trigger<> { + public: + explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); }); + } +}; + +template class ArmAwayAction : public Action { + public: + explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { + auto call = this->alarm_control_panel_->make_call(); + auto code = this->code_.optional_value(x...); + if (code.has_value()) { + call.set_code(code.value()); + } + call.arm_away(); + call.perform(); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class ArmHomeAction : public Action { + public: + explicit ArmHomeAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { + auto call = this->alarm_control_panel_->make_call(); + auto code = this->code_.optional_value(x...); + if (code.has_value()) { + call.set_code(code.value()); + } + call.arm_home(); + call.perform(); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class ArmNightAction : public Action { + public: + explicit ArmNightAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { + auto call = this->alarm_control_panel_->make_call(); + auto code = this->code_.optional_value(x...); + if (code.has_value()) { + call.set_code(code.value()); + } + call.arm_night(); + call.perform(); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class DisarmAction : public Action { + public: + explicit DisarmAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class PendingAction : public Action { + public: + explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class TriggeredAction : public Action { + public: + explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class AlarmControlPanelCondition : public Condition { + public: + AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} + bool check(Ts... x) override { + return this->parent_->is_state_armed(this->parent_->get_state()) || + this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; + } + + protected: + AlarmControlPanel *parent_; +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alpha3/__init__.py b/esphome/components/alpha3/__init__.py new file mode 100644 index 0000000000..7cd320c80f --- /dev/null +++ b/esphome/components/alpha3/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jan-hofmeier"] diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp new file mode 100644 index 0000000000..344f2d5a03 --- /dev/null +++ b/esphome/components/alpha3/alpha3.cpp @@ -0,0 +1,191 @@ +#include "alpha3.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include //gives ntohl + +#ifdef USE_ESP32 + +namespace esphome { +namespace alpha3 { + +static const char *const TAG = "alpha3"; + +void Alpha3::dump_config() { + ESP_LOGCONFIG(TAG, "ALPHA3"); + LOG_SENSOR(" ", "Flow", this->flow_sensor_); + LOG_SENSOR(" ", "Head", this->head_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Speed", this->speed_sensor_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); +} + +void Alpha3::setup() {} + +void Alpha3::extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset, + int16_t value_offset, sensor::Sensor *sensor, float factor) { + if (sensor == nullptr) + return; + // we need to handle cases where a value is split over two packets + const int16_t value_length = 4; // 32bit float + // offset inside current response packet + auto rel_offset = value_offset - response_offset; + if (rel_offset <= -value_length) + return; // aready passed the value completly + if (rel_offset >= length) + return; // value not in this packet + + auto start_offset = std::max(0, rel_offset); + auto end_offset = std::min((int16_t) (rel_offset + value_length), length); + auto copy_length = end_offset - start_offset; + auto buffer_offset = std::max(-rel_offset, 0); + std::memcpy(this->buffer_ + buffer_offset, response + start_offset, copy_length); + + if (rel_offset + value_length <= length) { + // we have the whole value + void *buffer = this->buffer_; // to prevent warnings when casting the pointer + *((int32_t *) buffer) = ntohl(*((int32_t *) buffer)); // values are big endian + float fvalue = *((float *) buffer); + sensor->publish_state(fvalue * factor); + } +} + +bool Alpha3::is_current_response_type_(const uint8_t *response_type) { + return !std::memcmp(this->response_type_, response_type, GENI_RESPONSE_TYPE_LENGTH); +} + +void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { + if (this->response_offset_ >= this->response_length_) { + ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str()); + if (length < GENI_RESPONSE_HEADER_LENGTH) { + ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_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(), + response[0], response[1], response[2], response[3], response[4]); + return; + } + this->response_length_ = response[1] - GENI_RESPONSE_HEADER_LENGTH + 2; // maybe 2 byte checksum + this->response_offset_ = -GENI_RESPONSE_HEADER_LENGTH; + std::memcpy(this->response_type_, response + 5, GENI_RESPONSE_TYPE_LENGTH); + } + + auto extract_publish_sensor_value = [response, length, this](int16_t value_offset, sensor::Sensor *sensor, + float factor) { + this->extract_publish_sensor_value_(response, length, this->response_offset_, value_offset, sensor, factor); + }; + + if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { + ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_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()); + 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); + extract_publish_sensor_value(GENI_RESPONSE_VOLTAGE_AC_OFFSET, this->voltage_sensor_, 1.0F); + } else { + ESP_LOGW(TAG, "unkown GENI response Type %d %d %d %d %d %d %d %d", this->response_type_[0], this->response_type_[1], + this->response_type_[2], this->response_type_[3], this->response_type_[4], this->response_type_[5], + this->response_type_[6], this->response_type_[7]); + } + this->response_offset_ += length; +} + +void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + 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()); + } + break; + } + case ESP_GATTC_CONNECT_EVT: { + if (std::memcmp(param->connect.remote_bda, this->parent_->get_remote_bda(), 6) != 0) + return; + auto ret = esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT); + if (ret) { + ESP_LOGW(TAG, "esp_ble_set_encryption failed, status=%x", ret); + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + this->node_state = espbt::ClientState::IDLE; + if (this->flow_sensor_ != nullptr) + this->flow_sensor_->publish_state(NAN); + if (this->head_sensor_ != nullptr) + this->head_sensor_->publish_state(NAN); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(NAN); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(NAN); + if (this->speed_sensor_ != nullptr) + this->speed_sensor_->publish_state(NAN); + if (this->speed_sensor_ != nullptr) + this->voltage_sensor_->publish_state(NAN); + break; + } + 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()); + break; + } + auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), + chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + this->geni_handle_ = chr->handle; + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + this->update(); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle == this->geni_handle_) { + this->handle_geni_response_(param->notify.value, param->notify.value_len); + } + break; + } + default: + break; + } +} + +void Alpha3::send_request_(uint8_t *request, size_t len) { + auto status = + 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); +} + +void Alpha3::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + return; + } + + if (this->flow_sensor_ != nullptr || this->head_sensor_ != nullptr) { + uint8_t geni_request_flow_head[] = {39, 7, 231, 248, 10, 3, 93, 1, 33, 82, 31}; + this->send_request_(geni_request_flow_head, sizeof(geni_request_flow_head)); + delay(25); // need to wait between requests + } + if (this->power_sensor_ != nullptr || this->current_sensor_ != nullptr || this->speed_sensor_ != nullptr || + this->voltage_sensor_ != nullptr) { + uint8_t geni_request_power[] = {39, 7, 231, 248, 10, 3, 87, 0, 69, 138, 205}; + this->send_request_(geni_request_power, sizeof(geni_request_power)); + delay(25); // need to wait between requests + } +} +} // namespace alpha3 +} // namespace esphome + +#endif diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h new file mode 100644 index 0000000000..325c70a538 --- /dev/null +++ b/esphome/components/alpha3/alpha3.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +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 int16_t GENI_RESPONSE_HEADER_LENGTH = 13; +static const size_t GENI_RESPONSE_TYPE_LENGTH = 8; + +static const uint8_t GENI_RESPONSE_TYPE_FLOW_HEAD[GENI_RESPONSE_TYPE_LENGTH] = {31, 0, 1, 48, 1, 0, 0, 24}; +static const int16_t GENI_RESPONSE_FLOW_OFFSET = 0; +static const int16_t GENI_RESPONSE_HEAD_OFFSET = 4; + +static const uint8_t GENI_RESPONSE_TYPE_POWER[GENI_RESPONSE_TYPE_LENGTH] = {44, 0, 1, 0, 1, 0, 0, 37}; +static const int16_t GENI_RESPONSE_VOLTAGE_AC_OFFSET = 0; +static const int16_t GENI_RESPONSE_VOLTAGE_DC_OFFSET = 4; +static const int16_t GENI_RESPONSE_CURRENT_OFFSET = 8; +static const int16_t GENI_RESPONSE_POWER_OFFSET = 12; +static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure +static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20; + +class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } + void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } + void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } + void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; } + void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; } + void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; } + + protected: + sensor::Sensor *flow_sensor_{nullptr}; + sensor::Sensor *head_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + uint16_t geni_handle_; + int16_t response_length_; + int16_t response_offset_; + uint8_t response_type_[GENI_RESPONSE_TYPE_LENGTH]; + uint8_t buffer_[4]; + void extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset, + int16_t value_offset, sensor::Sensor *sensor, float factor); + void handle_geni_response_(const uint8_t *response, uint16_t length); + void send_request_(uint8_t *request, size_t len); + bool is_current_response_type_(const uint8_t *response_type); +}; +} // namespace alpha3 +} // namespace esphome + +#endif diff --git a/esphome/components/alpha3/sensor.py b/esphome/components/alpha3/sensor.py new file mode 100644 index 0000000000..55a5d7c620 --- /dev/null +++ b/esphome/components/alpha3/sensor.py @@ -0,0 +1,85 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client +from esphome.const import ( + CONF_ID, + CONF_CURRENT, + CONF_FLOW, + CONF_HEAD, + CONF_POWER, + CONF_SPEED, + CONF_VOLTAGE, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_WATT, + UNIT_METER, + UNIT_CUBIC_METER_PER_HOUR, + UNIT_REVOLUTIONS_PER_MINUTE, +) + +alpha3_ns = cg.esphome_ns.namespace("alpha3") +Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Alpha3), + cv.Optional(CONF_FLOW): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR, + accuracy_decimals=2, + ), + cv.Optional(CONF_HEAD): sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + accuracy_decimals=2, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=2, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + ), + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("15s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + + if flow_config := config.get(CONF_FLOW): + sens = await sensor.new_sensor(flow_config) + cg.add(var.set_flow_sensor(sens)) + + if head_config := config.get(CONF_HEAD): + sens = await sensor.new_sensor(head_config) + cg.add(var.set_head_sensor(sens)) + + if power_config := config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) + cg.add(var.set_power_sensor(sens)) + + if current_config := config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) + cg.add(var.set_current_sensor(sens)) + + if speed_config := config.get(CONF_SPEED): + sens = await sensor.new_sensor(speed_config) + cg.add(var.set_speed_sensor(sens)) + + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) + cg.add(var.set_voltage_sensor(sens)) diff --git a/esphome/components/am2315c/__init__.py b/esphome/components/am2315c/__init__.py new file mode 100644 index 0000000000..2398e4fa23 --- /dev/null +++ b/esphome/components/am2315c/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@swoboda1337"] diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp new file mode 100644 index 0000000000..715251a9df --- /dev/null +++ b/esphome/components/am2315c/am2315c.cpp @@ -0,0 +1,200 @@ +// MIT License +// +// Copyright (c) 2023-2024 Rob Tillaart +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#include "am2315c.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace am2315c { + +static const char *const TAG = "am2315c"; + +uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) { + uint8_t crc = 0xFF; + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if (crc & 0x80) { + crc <<= 1; + crc ^= 0x31; + } else { + crc <<= 1; + } + } + } + return crc; +} + +bool AM2315C::reset_register_(uint8_t reg) { + // code based on demo code sent by www.aosong.com + // no further documentation. + // 0x1B returned 18, 0, 4 + // 0x1C returned 18, 65, 0 + // 0x1E returned 18, 8, 0 + // 18 seems to be status register + // other values unknown. + uint8_t data[3]; + data[0] = reg; + data[1] = 0; + data[2] = 0; + ESP_LOGD(TAG, "Reset register: 0x%02x", reg); + if (this->write(data, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Write failed!"); + this->mark_failed(); + return false; + } + delay(5); + if (this->read(data, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read failed!"); + this->mark_failed(); + return false; + } + delay(10); + data[0] = 0xB0 | reg; + if (this->write(data, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Write failed!"); + this->mark_failed(); + return false; + } + delay(5); + return true; +} + +bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) { + uint32_t raw; + raw = (data[1] << 12) | (data[2] << 4) | (data[3] >> 4); + humidity = raw * 9.5367431640625e-5; + raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; + temperature = raw * 1.9073486328125e-4 - 50; + return this->crc8_(data, 6) == data[6]; +} + +void AM2315C::setup() { + ESP_LOGCONFIG(TAG, "Setting up AM2315C..."); + + // get status + uint8_t status = 0; + if (this->read(&status, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read failed!"); + this->mark_failed(); + return; + } + + // reset registers if required, according to the datasheet + // this can be required after power on, although this was + // never required during testing + if ((status & 0x18) != 0x18) { + ESP_LOGD(TAG, "Resetting AM2315C registers"); + if (!this->reset_register_(0x1B)) { + this->mark_failed(); + return; + } + if (!this->reset_register_(0x1C)) { + this->mark_failed(); + return; + } + if (!this->reset_register_(0x1E)) { + this->mark_failed(); + return; + } + } +} + +void AM2315C::update() { + // request measurement + uint8_t data[3]; + data[0] = 0xAC; + data[1] = 0x33; + data[2] = 0x00; + if (this->write(data, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Write failed!"); + this->mark_failed(); + return; + } + + // wait for hw to complete measurement + set_timeout(160, [this]() { + // check status + uint8_t status = 0; + if (this->read(&status, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read failed!"); + this->mark_failed(); + return; + } + if ((status & 0x80) == 0x80) { + ESP_LOGE(TAG, "HW still busy!"); + this->mark_failed(); + return; + } + + // read + uint8_t data[7]; + if (this->read(data, 7) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read failed!"); + this->mark_failed(); + return; + } + + // check for all zeros + bool zeros = true; + for (uint8_t i : data) { + zeros = zeros && (i == 0); + } + if (zeros) { + ESP_LOGW(TAG, "Data all zeros!"); + this->status_set_warning(); + return; + } + + // convert + float temperature = 0.0; + float humidity = 0.0; + if (this->convert_(data, humidity, temperature)) { + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(temperature); + } + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->publish_state(humidity); + } + this->status_clear_warning(); + } else { + ESP_LOGW(TAG, "CRC failed!"); + this->status_set_warning(); + } + }); +} + +void AM2315C::dump_config() { + ESP_LOGCONFIG(TAG, "AM2315C:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AM2315C failed!"); + } + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +float AM2315C::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace am2315c +} // namespace esphome diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h new file mode 100644 index 0000000000..9cec40e4c2 --- /dev/null +++ b/esphome/components/am2315c/am2315c.h @@ -0,0 +1,51 @@ +// MIT License +// +// Copyright (c) 2023-2024 Rob Tillaart +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace am2315c { + +class AM2315C : public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + void update() override; + void setup() override; + float get_setup_priority() const override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + protected: + uint8_t crc8_(uint8_t *data, uint8_t len); + bool convert_(uint8_t *data, float &humidity, float &temperature); + bool reset_register_(uint8_t reg); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace am2315c +} // namespace esphome diff --git a/esphome/components/am2315c/sensor.py b/esphome/components/am2315c/sensor.py new file mode 100644 index 0000000000..f3201b05a2 --- /dev/null +++ b/esphome/components/am2315c/sensor.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +am2315c_ns = cg.esphome_ns.namespace("am2315c") +AM2315C = am2315c_ns.class_("AM2315C", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AM2315C), + 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_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x38)) +) + + +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) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/am2320/sensor.py b/esphome/components/am2320/sensor.py index 088978a8f1..ccd37d02c2 100644 --- a/esphome/components/am2320/sensor.py +++ b/esphome/components/am2320/sensor.py @@ -47,10 +47,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) cg.add(var.set_temperature_sensor(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/am43/sensor/__init__.py b/esphome/components/am43/sensor/__init__.py index 01588f2299..df068546cd 100644 --- a/esphome/components/am43/sensor/__init__.py +++ b/esphome/components/am43/sensor/__init__.py @@ -44,10 +44,10 @@ async def to_code(config): await cg.register_component(var, config) await ble_client.register_ble_node(var, config) - if CONF_BATTERY_LEVEL in config: - sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) cg.add(var.set_battery(sens)) - if CONF_ILLUMINANCE in config: - sens = await sensor.new_sensor(config[CONF_ILLUMINANCE]) + if illuminance_config := config.get(CONF_ILLUMINANCE): + sens = await sensor.new_sensor(illuminance_config) cg.add(var.set_illuminance(sens)) diff --git a/esphome/components/am43/sensor/am43_sensor.cpp b/esphome/components/am43/sensor/am43_sensor.cpp index 008c7768ed..4cc99001ae 100644 --- a/esphome/components/am43/sensor/am43_sensor.cpp +++ b/esphome/components/am43/sensor/am43_sensor.cpp @@ -26,7 +26,9 @@ void Am43::setup() { void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { case ESP_GATTC_OPEN_EVT: { - this->logged_in_ = false; + if (param->open.status == ESP_GATT_OK) { + this->logged_in_ = false; + } break; } case ESP_GATTC_DISCONNECT_EVT: { diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index f51d115d9e..5a308855de 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -1,31 +1,90 @@ import logging -from esphome import core -from esphome.components import display, font -import esphome.components.image as espImage -from esphome.components.image import CONF_USE_TRANSPARENCY -import esphome.config_validation as cv +from esphome import automation, core import esphome.codegen as cg +from esphome.components import font +import esphome.components.image as espImage +from esphome.components.image import ( + CONF_USE_TRANSPARENCY, + LOCAL_SCHEMA, + SOURCE_LOCAL, + SOURCE_WEB, + WEB_SCHEMA, +) +import esphome.config_validation as cv from esphome.const import ( CONF_FILE, CONF_ID, + CONF_PATH, CONF_RAW_DATA_ID, CONF_REPEAT, CONF_RESIZE, + CONF_SOURCE, CONF_TYPE, + CONF_URL, ) from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) +AUTO_LOAD = ["image"] +CODEOWNERS = ["@syndlex"] DEPENDENCIES = ["display"] MULTI_CONF = True CONF_LOOP = "loop" CONF_START_FRAME = "start_frame" CONF_END_FRAME = "end_frame" +CONF_FRAME = "frame" -Animation_ = display.display_ns.class_("Animation", espImage.Image_) +animation_ns = cg.esphome_ns.namespace("animation") + +Animation_ = animation_ns.class_("Animation", espImage.Image_) + +# Actions +NextFrameAction = animation_ns.class_( + "AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_) +) +PrevFrameAction = animation_ns.class_( + "AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_) +) +SetFrameAction = animation_ns.class_( + "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) +) + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + SOURCE_LOCAL: LOCAL_SCHEMA, + SOURCE_WEB: WEB_SCHEMA, + }, + key=CONF_SOURCE, +) + + +def _file_schema(value): + if isinstance(value, str): + return validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +FILE_SCHEMA = cv.Schema(_file_schema) + + +def validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("http://") or value.startswith("https://"): + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_WEB, + CONF_URL: value, + } + ) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_LOCAL, + CONF_PATH: value, + } + ) def validate_cross_dependencies(config): @@ -51,7 +110,7 @@ ANIMATION_SCHEMA = cv.Schema( cv.All( { cv.Required(CONF_ID): cv.declare_id(Animation_), - cv.Required(CONF_FILE): cv.file_, + cv.Required(CONF_FILE): FILE_SCHEMA, cv.Optional(CONF_RESIZE): cv.dimensions, cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( espImage.IMAGE_TYPE, upper=True @@ -74,13 +133,48 @@ ANIMATION_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) -CODEOWNERS = ["@syndlex"] +NEXT_FRAME_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(Animation_), + } +) +PREV_FRAME_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(Animation_), + } +) +SET_FRAME_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(Animation_), + cv.Required(CONF_FRAME): cv.uint16_t, + } +) + + +@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA) +@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA) +@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA) +async def animation_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if (frame := config.get(CONF_FRAME)) is not None: + template_ = await cg.templatable(frame, args, cg.uint16) + cg.add(var.set_frame(template_)) + return var async def to_code(config): from PIL import Image - path = CORE.relative_config_path(config[CONF_FILE]) + conf_file = config[CONF_FILE] + if conf_file[CONF_SOURCE] == SOURCE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + elif conf_file[CONF_SOURCE] == SOURCE_WEB: + path = espImage.compute_local_image_path(conf_file).as_posix() + else: + raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}") + try: image = Image.open(path) except Exception as e: @@ -92,13 +186,12 @@ async def to_code(config): new_width_max, new_height_max = config[CONF_RESIZE] ratio = min(new_width_max / width, new_height_max / height) width, height = int(width * ratio), int(height * ratio) - else: - if width > 500 or height > 500: - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, - ) + elif width > 500 or height > 500: + _LOGGER.warning( + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, + ) transparent = config[CONF_USE_TRANSPARENCY] @@ -107,13 +200,13 @@ async def to_code(config): pos = 0 for frameIndex in range(frames): image.seek(frameIndex) - frame = image.convert("LA", dither=Image.NONE) + frame = image.convert("LA", dither=Image.Dither.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) if len(pixels) != height * width: raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" ) for pix, a in pixels: if transparent: @@ -136,7 +229,7 @@ async def to_code(config): pixels = list(frame.getdata()) if len(pixels) != height * width: raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" ) for pix in pixels: data[pos] = pix[0] @@ -159,7 +252,7 @@ async def to_code(config): pixels = list(frame.getdata()) if len(pixels) != height * width: raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" ) for r, g, b, a in pixels: if transparent: @@ -178,7 +271,8 @@ async def to_code(config): pos += 1 elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: - data = [0 for _ in range(height * width * 2 * frames)] + bytes_per_pixel = 3 if transparent else 2 + data = [0 for _ in range(height * width * bytes_per_pixel * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) @@ -188,24 +282,20 @@ async def to_code(config): pixels = list(frame.getdata()) if len(pixels) != height * width: raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" ) for r, g, b, a in pixels: R = r >> 3 G = g >> 2 B = b >> 3 rgb = (R << 11) | (G << 5) | B - - if transparent: - if rgb == 0x0020: - rgb = 0 - if a < 0x80: - rgb = 0x0020 - data[pos] = rgb >> 8 pos += 1 data[pos] = rgb & 0xFF pos += 1 + if transparent: + data[pos] = a + pos += 1 elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: width8 = ((width + 7) // 8) * 8 @@ -215,7 +305,9 @@ async def to_code(config): if transparent: alpha = image.split()[-1] has_alpha = alpha.getextrema()[0] < 0xFF - frame = image.convert("1", dither=Image.NONE) + else: + has_alpha = False + frame = image.convert("1", dither=Image.Dither.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) if transparent: @@ -245,8 +337,8 @@ async def to_code(config): espImage.IMAGE_TYPE[config[CONF_TYPE]], ) cg.add(var.set_transparency(transparent)) - if CONF_LOOP in config: - start = config[CONF_LOOP][CONF_START_FRAME] - end = config[CONF_LOOP].get(CONF_END_FRAME, frames) - count = config[CONF_LOOP].get(CONF_REPEAT, -1) + if loop_config := config.get(CONF_LOOP): + start = loop_config[CONF_START_FRAME] + end = loop_config.get(CONF_END_FRAME, frames) + count = loop_config.get(CONF_REPEAT, -1) cg.add(var.set_loop(start, end, count)) diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp new file mode 100644 index 0000000000..1375dfe07e --- /dev/null +++ b/esphome/components/animation/animation.cpp @@ -0,0 +1,70 @@ +#include "animation.h" + +#include "esphome/core/hal.h" + +namespace esphome { +namespace animation { + +Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, + image::ImageType type) + : Image(data_start, width, height, type), + animation_data_start_(data_start), + current_frame_(0), + animation_frame_count_(animation_frame_count), + loop_start_frame_(0), + loop_end_frame_(animation_frame_count_), + loop_count_(0), + loop_current_iteration_(1) {} +void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) { + loop_start_frame_ = std::min(start_frame, animation_frame_count_); + loop_end_frame_ = std::min(end_frame, animation_frame_count_); + loop_count_ = count; + loop_current_iteration_ = 1; +} + +uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; } +int Animation::get_current_frame() const { return this->current_frame_; } +void Animation::next_frame() { + this->current_frame_++; + if (loop_count_ && this->current_frame_ == loop_end_frame_ && + (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { + this->current_frame_ = loop_start_frame_; + this->loop_current_iteration_++; + } + if (this->current_frame_ >= animation_frame_count_) { + this->loop_current_iteration_ = 1; + this->current_frame_ = 0; + } + + this->update_data_start_(); +} +void Animation::prev_frame() { + this->current_frame_--; + if (this->current_frame_ < 0) { + this->current_frame_ = this->animation_frame_count_ - 1; + } + + this->update_data_start_(); +} + +void Animation::set_frame(int frame) { + unsigned abs_frame = abs(frame); + + if (abs_frame < this->animation_frame_count_) { + if (frame >= 0) { + this->current_frame_ = frame; + } else { + this->current_frame_ = this->animation_frame_count_ - abs_frame; + } + } + + this->update_data_start_(); +} + +void Animation::update_data_start_() { + const uint32_t image_size = this->get_width_stride() * this->height_; + this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; +} + +} // namespace animation +} // namespace esphome diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h new file mode 100644 index 0000000000..272c5153d1 --- /dev/null +++ b/esphome/components/animation/animation.h @@ -0,0 +1,67 @@ +#pragma once +#include "esphome/components/image/image.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace animation { + +class Animation : public image::Image { + public: + Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type); + + uint32_t get_animation_frame_count() const; + int get_current_frame() const; + void next_frame(); + void prev_frame(); + + /** Selects a specific frame within the animation. + * + * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame. + */ + void set_frame(int frame); + + void set_loop(uint32_t start_frame, uint32_t end_frame, int count); + + protected: + void update_data_start_(); + + const uint8_t *animation_data_start_; + int current_frame_; + uint32_t animation_frame_count_; + uint32_t loop_start_frame_; + uint32_t loop_end_frame_; + int loop_count_; + int loop_current_iteration_; +}; + +template class AnimationNextFrameAction : public Action { + public: + AnimationNextFrameAction(Animation *parent) : parent_(parent) {} + void play(Ts... x) override { this->parent_->next_frame(); } + + protected: + Animation *parent_; +}; + +template class AnimationPrevFrameAction : public Action { + public: + AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} + void play(Ts... x) override { this->parent_->prev_frame(); } + + protected: + Animation *parent_; +}; + +template class AnimationSetFrameAction : public Action { + public: + AnimationSetFrameAction(Animation *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(uint16_t, frame) + void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); } + + protected: + Animation *parent_; +}; + +} // namespace animation +} // namespace esphome diff --git a/esphome/components/apds9306/__init__.py b/esphome/components/apds9306/__init__.py new file mode 100644 index 0000000000..3dc8fcf5ff --- /dev/null +++ b/esphome/components/apds9306/__init__.py @@ -0,0 +1,4 @@ +# Based on this datasheet: +# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf + +CODEOWNERS = ["@aodrenah"] diff --git a/esphome/components/apds9306/apds9306.cpp b/esphome/components/apds9306/apds9306.cpp new file mode 100644 index 0000000000..7b79b0964c --- /dev/null +++ b/esphome/components/apds9306/apds9306.cpp @@ -0,0 +1,151 @@ +// Based on this datasheet: +// https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf + +#include "apds9306.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace apds9306 { + +static const char *const TAG = "apds9306"; + +enum { // APDS9306 registers + APDS9306_MAIN_CTRL = 0x00, + APDS9306_ALS_MEAS_RATE = 0x04, + APDS9306_ALS_GAIN = 0x05, + APDS9306_PART_ID = 0x06, + APDS9306_MAIN_STATUS = 0x07, + APDS9306_CLEAR_DATA_0 = 0x0A, // LSB + APDS9306_CLEAR_DATA_1 = 0x0B, + APDS9306_CLEAR_DATA_2 = 0x0C, // MSB + APDS9306_ALS_DATA_0 = 0x0D, // LSB + APDS9306_ALS_DATA_1 = 0x0E, + APDS9306_ALS_DATA_2 = 0x0F, // MSB + APDS9306_INT_CFG = 0x19, + APDS9306_INT_PERSISTENCE = 0x1A, + APDS9306_ALS_THRES_UP_0 = 0x21, // LSB + APDS9306_ALS_THRES_UP_1 = 0x22, + APDS9306_ALS_THRES_UP_2 = 0x23, // MSB + APDS9306_ALS_THRES_LOW_0 = 0x24, // LSB + APDS9306_ALS_THRES_LOW_1 = 0x25, + APDS9306_ALS_THRES_LOW_2 = 0x26, // MSB + APDS9306_ALS_THRES_VAR = 0x27 +}; + +#define APDS9306_ERROR_CHECK(func, error) \ + if (!(func)) { \ + ESP_LOGE(TAG, error); \ + this->mark_failed(); \ + return; \ + } +#define APDS9306_WARNING_CHECK(func, warning) \ + if (!(func)) { \ + ESP_LOGW(TAG, warning); \ + this->status_set_warning(); \ + return; \ + } +#define APDS9306_WRITE_BYTE(reg, value) \ + ESP_LOGV(TAG, "Writing 0x%02x to 0x%02x", value, reg); \ + if (!this->write_byte(reg, value)) { \ + ESP_LOGE(TAG, "Failed writing 0x%02x to 0x%02x", value, reg); \ + this->mark_failed(); \ + return; \ + } + +void APDS9306::setup() { + ESP_LOGCONFIG(TAG, "Setting up APDS9306..."); + + uint8_t id; + if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + if (id != 0xB1 && id != 0xB3) { // 0xB1 for APDS9306 0xB3 for APDS9306-065 + this->error_code_ = WRONG_ID; + this->mark_failed(); + return; + } + + // ALS resolution and measurement, see datasheet or init.py for options + uint8_t als_meas_rate = ((this->bit_width_ & 0x07) << 4) | (this->measurement_rate_ & 0x07); + APDS9306_WRITE_BYTE(APDS9306_ALS_MEAS_RATE, als_meas_rate); + + // ALS gain, see datasheet or init.py for options + uint8_t als_gain = (this->gain_ & 0x07); + APDS9306_WRITE_BYTE(APDS9306_ALS_GAIN, als_gain); + + // Set to standby mode + APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00); + + // Check for data, clear main status + uint8_t status; + APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); + + // Set to active mode + APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); + + ESP_LOGCONFIG(TAG, "APDS9306 setup complete"); +} + +void APDS9306::dump_config() { + LOG_SENSOR("", "APDS9306", this); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with APDS9306 failed!"); + break; + case WRONG_ID: + ESP_LOGE(TAG, "APDS9306 has invalid id!"); + break; + default: + ESP_LOGE(TAG, "Setting up APDS9306 registers failed!"); + break; + } + } + + ESP_LOGCONFIG(TAG, " Gain: %u", AMBIENT_LIGHT_GAIN_VALUES[this->gain_]); + ESP_LOGCONFIG(TAG, " Measurement rate: %u", MEASUREMENT_RATE_VALUES[this->measurement_rate_]); + ESP_LOGCONFIG(TAG, " Measurement Resolution/Bit width: %d", MEASUREMENT_BIT_WIDTH_VALUES[this->bit_width_]); + + LOG_UPDATE_INTERVAL(this); +} + +void APDS9306::update() { + // Check for new data + uint8_t status; + APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); + + this->status_clear_warning(); + + if (!(status &= 0b00001000)) { // No new data + return; + } + + // Set to standby mode + APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00); + + // Clear MAIN STATUS + APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); + + uint8_t als_data[3]; + APDS9306_WARNING_CHECK(this->read_bytes(APDS9306_ALS_DATA_0, als_data, 3), "Reading ALS data has failed."); + + // Set to active mode + APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); + + uint32_t light_level = 0x00 | encode_uint24(als_data[2], als_data[1], als_data[0]); + + float lux = ((float) light_level / AMBIENT_LIGHT_GAIN_VALUES[this->gain_]) * + (100.0f / MEASUREMENT_RATE_VALUES[this->measurement_rate_]); + + ESP_LOGD(TAG, "Got illuminance=%.1flx from", lux); + this->publish_state(lux); +} + +} // namespace apds9306 +} // namespace esphome diff --git a/esphome/components/apds9306/apds9306.h b/esphome/components/apds9306/apds9306.h new file mode 100644 index 0000000000..44362908c8 --- /dev/null +++ b/esphome/components/apds9306/apds9306.h @@ -0,0 +1,66 @@ +// Based on this datasheet: +// https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf + +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace apds9306 { + +enum MeasurementBitWidth : uint8_t { + MEASUREMENT_BIT_WIDTH_20 = 0, + MEASUREMENT_BIT_WIDTH_19 = 1, + MEASUREMENT_BIT_WIDTH_18 = 2, + MEASUREMENT_BIT_WIDTH_17 = 3, + MEASUREMENT_BIT_WIDTH_16 = 4, + MEASUREMENT_BIT_WIDTH_13 = 5, +}; +static const uint8_t MEASUREMENT_BIT_WIDTH_VALUES[] = {20, 19, 18, 17, 16, 13}; + +enum MeasurementRate : uint8_t { + MEASUREMENT_RATE_25 = 0, + MEASUREMENT_RATE_50 = 1, + MEASUREMENT_RATE_100 = 2, + MEASUREMENT_RATE_200 = 3, + MEASUREMENT_RATE_500 = 4, + MEASUREMENT_RATE_1000 = 5, + MEASUREMENT_RATE_2000 = 6, +}; +static const uint16_t MEASUREMENT_RATE_VALUES[] = {25, 50, 100, 200, 500, 1000, 2000}; + +enum AmbientLightGain : uint8_t { + AMBIENT_LIGHT_GAIN_1 = 0, + AMBIENT_LIGHT_GAIN_3 = 1, + AMBIENT_LIGHT_GAIN_6 = 2, + AMBIENT_LIGHT_GAIN_9 = 3, + AMBIENT_LIGHT_GAIN_18 = 4, +}; +static const uint8_t AMBIENT_LIGHT_GAIN_VALUES[] = {1, 3, 6, 9, 18}; + +class APDS9306 : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::BUS; } + void dump_config() override; + void update() override; + void set_bit_width(MeasurementBitWidth bit_width) { this->bit_width_ = bit_width; } + void set_measurement_rate(MeasurementRate measurement_rate) { this->measurement_rate_ = measurement_rate; } + void set_ambient_light_gain(AmbientLightGain gain) { this->gain_ = gain; } + + protected: + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + WRONG_ID, + } error_code_{NONE}; + + MeasurementBitWidth bit_width_; + MeasurementRate measurement_rate_; + AmbientLightGain gain_; +}; + +} // namespace apds9306 +} // namespace esphome diff --git a/esphome/components/apds9306/sensor.py b/esphome/components/apds9306/sensor.py new file mode 100644 index 0000000000..25b301444f --- /dev/null +++ b/esphome/components/apds9306/sensor.py @@ -0,0 +1,95 @@ +# Based on this datasheet: +# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_GAIN, + DEVICE_CLASS_ILLUMINANCE, + ICON_LIGHTBULB, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) + +DEPENDENCIES = ["i2c"] + +CONF_APDS9306_ID = "apds9306_id" +CONF_BIT_WIDTH = "bit_width" +CONF_MEASUREMENT_RATE = "measurement_rate" + +apds9306_ns = cg.esphome_ns.namespace("apds9306") +APDS9306 = apds9306_ns.class_( + "APDS9306", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +MeasurementBitWidth = apds9306_ns.enum("MeasurementBitWidth") +MeasurementRate = apds9306_ns.enum("MeasurementRate") +AmbientLightGain = apds9306_ns.enum("AmbientLightGain") + +MEASUREMENT_BIT_WIDTHS = { + 20: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_20, + 19: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_19, + 18: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_18, + 17: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_17, + 16: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_16, + 13: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_13, +} + +MEASUREMENT_RATES = { + 25: MeasurementRate.MEASUREMENT_RATE_25, + 50: MeasurementRate.MEASUREMENT_RATE_50, + 100: MeasurementRate.MEASUREMENT_RATE_100, + 200: MeasurementRate.MEASUREMENT_RATE_200, + 500: MeasurementRate.MEASUREMENT_RATE_500, + 1000: MeasurementRate.MEASUREMENT_RATE_1000, + 2000: MeasurementRate.MEASUREMENT_RATE_2000, +} + +AMBIENT_LIGHT_GAINS = { + 1: AmbientLightGain.AMBIENT_LIGHT_GAIN_1, + 3: AmbientLightGain.AMBIENT_LIGHT_GAIN_3, + 6: AmbientLightGain.AMBIENT_LIGHT_GAIN_6, + 9: AmbientLightGain.AMBIENT_LIGHT_GAIN_9, + 18: AmbientLightGain.AMBIENT_LIGHT_GAIN_18, +} + + +def _validate_measurement_rate(value): + value = cv.positive_time_period_milliseconds(value) + return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + APDS9306, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_LIGHTBULB, + ) + .extend( + { + cv.Optional(CONF_GAIN, default="1"): cv.enum(AMBIENT_LIGHT_GAINS, int=True), + cv.Optional(CONF_BIT_WIDTH, default="18"): cv.enum( + MEASUREMENT_BIT_WIDTHS, int=True + ), + cv.Optional( + CONF_MEASUREMENT_RATE, default="100ms" + ): _validate_measurement_rate, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x52)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_bit_width(config[CONF_BIT_WIDTH])) + cg.add(var.set_measurement_rate(config[CONF_MEASUREMENT_RATE])) + cg.add(var.set_ambient_light_gain(config[CONF_GAIN])) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 6b2e7fd06b..27de5c873b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,23 +1,27 @@ import base64 -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_ACTION, + CONF_ACTIONS, CONF_DATA, CONF_DATA_TEMPLATE, + CONF_EVENT, CONF_ID, CONF_KEY, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, CONF_SERVICE, - CONF_VARIABLES, CONF_SERVICES, - CONF_TRIGGER_ID, - CONF_EVENT, CONF_TAG, + CONF_TRIGGER_ID, + CONF_VARIABLES, ) from esphome.core import coroutine_with_priority @@ -61,34 +65,51 @@ def validate_encryption_key(value): return value -CONFIG_SCHEMA = cv.Schema( +ACTIONS_SCHEMA = automation.validate_automation( { - cv.GenerateID(): cv.declare_id(APIServer), - cv.Optional(CONF_PORT, default=6053): cv.port, - cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, - cv.Optional( - CONF_REBOOT_TIMEOUT, default="15min" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SERVICES): automation.validate_automation( + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), + cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), - cv.Required(CONF_SERVICE): cv.valid_name, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema( - { - cv.validate_id_name: cv.one_of( - *SERVICE_ARG_NATIVE_TYPES, lower=True - ), - } - ), + cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), } ), - cv.Optional(CONF_ENCRYPTION): cv.Schema( - { - cv.Required(CONF_KEY): validate_encryption_key, - } - ), - } -).extend(cv.COMPONENT_SCHEMA) + }, + cv.All( + cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), + cv.rename_key(CONF_SERVICE, CONF_ACTION), + ), +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(APIServer), + cv.Optional(CONF_PORT, default=6053): cv.port, + cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="15min" + ): cv.positive_time_period_milliseconds, + cv.Exclusive( + CONF_SERVICES, group_of_exclusion=CONF_ACTIONS + ): ACTIONS_SCHEMA, + cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, + cv.Optional(CONF_ENCRYPTION): cv.Schema( + { + cv.Required(CONF_KEY): validate_encryption_key, + } + ), + cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.rename_key(CONF_SERVICES, CONF_ACTIONS), +) @coroutine_with_priority(40.0) @@ -100,7 +121,7 @@ async def to_code(config): cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - for conf in config.get(CONF_SERVICES, []): + for conf in config.get(CONF_ACTIONS, []): template_args = [] func_args = [] service_arg_names = [] @@ -111,17 +132,30 @@ async def to_code(config): service_arg_names.append(name) templ = cg.TemplateArguments(*template_args) trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names + conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names ) cg.add(var.register_user_service(trigger)) await automation.build_automation(trigger, func_args, conf) - if CONF_ENCRYPTION in config: - conf = config[CONF_ENCRYPTION] - decoded = base64.b64decode(conf[CONF_KEY]) + if CONF_ON_CLIENT_CONNECTED in config: + await automation.build_automation( + var.get_client_connected_trigger(), + [(cg.std_string, "client_info"), (cg.std_string, "client_address")], + config[CONF_ON_CLIENT_CONNECTED], + ) + + if CONF_ON_CLIENT_DISCONNECTED in config: + await automation.build_automation( + var.get_client_disconnected_trigger(), + [(cg.std_string, "client_info"), (cg.std_string, "client_address")], + config[CONF_ON_CLIENT_DISCONNECTED], + ) + + if encryption_config := config.get(CONF_ENCRYPTION): + decoded = base64.b64decode(encryption_config[CONF_KEY]) cg.add(var.set_noise_psk(list(decoded))) cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.4") + cg.add_library("esphome/noise-c", "0.1.6") else: cg.add_define("USE_API_PLAINTEXT") @@ -131,28 +165,43 @@ async def to_code(config): KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) -HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(APIServer), - cv.Required(CONF_SERVICE): cv.templatable(cv.string), - cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema( - {cv.string: cv.returning_lambda} - ), - } + +HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.templatable( + cv.string + ), + cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.templatable( + cv.string + ), + cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( + {cv.string: cv.returning_lambda} + ), + } + ), + cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), + cv.rename_key(CONF_SERVICE, CONF_ACTION), ) +@automation.register_action( + "homeassistant.action", + HomeAssistantServiceCallAction, + HOMEASSISTANT_ACTION_ACTION_SCHEMA, +) @automation.register_action( "homeassistant.service", HomeAssistantServiceCallAction, - HOMEASSISTANT_SERVICE_ACTION_SCHEMA, + HOMEASSISTANT_ACTION_ACTION_SCHEMA, ) async def homeassistant_service_to_code(config, action_id, template_arg, args): serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) - templ = await cg.templatable(config[CONF_SERVICE], args, None) + templ = await cg.templatable(config[CONF_ACTION], args, None) cg.add(var.set_service(templ)) for key, value in config[CONF_DATA].items(): templ = await cg.templatable(value, args, None) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4cc98c91d9..684540ffa6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -39,10 +39,16 @@ service APIConnection { rpc camera_image (CameraImageRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {} rpc number_command (NumberCommandRequest) returns (void) {} + rpc text_command (TextCommandRequest) returns (void) {} rpc select_command (SelectCommandRequest) returns (void) {} rpc button_command (ButtonCommandRequest) returns (void) {} rpc lock_command (LockCommandRequest) returns (void) {} + rpc valve_command (ValveCommandRequest) returns (void) {} rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} + rpc date_command (DateCommandRequest) returns (void) {} + rpc time_command (TimeCommandRequest) returns (void) {} + rpc datetime_command (DateTimeCommandRequest) returns (void) {} + rpc update_command (UpdateCommandRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} @@ -56,6 +62,10 @@ service APIConnection { rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} + rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} + rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {} + + rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {} } @@ -206,13 +216,17 @@ message DeviceInfoResponse { uint32 webserver_port = 10; - uint32 bluetooth_proxy_version = 11; + uint32 legacy_bluetooth_proxy_version = 11; + uint32 bluetooth_proxy_feature_flags = 15; string manufacturer = 12; string friendly_name = 13; - uint32 voice_assistant_version = 14; + uint32 legacy_voice_assistant_version = 14; + uint32 voice_assistant_feature_flags = 17; + + string suggested_area = 16; } message ListEntitiesRequest { @@ -359,6 +373,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10; EntityCategory entity_category = 11; + repeated string supported_preset_modes = 12; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -381,6 +396,7 @@ message FanStateResponse { FanSpeed speed = 4 [deprecated = true]; FanDirection direction = 5; int32 speed_level = 6; + string preset_mode = 7; } message FanCommandRequest { option (id) = 31; @@ -399,6 +415,8 @@ message FanCommandRequest { FanDirection direction = 9; bool has_speed_level = 10; int32 speed_level = 11; + bool has_preset_mode = 12; + string preset_mode = 13; } // ==================== LIGHT ==================== @@ -590,6 +608,7 @@ message ListEntitiesTextSensorResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + string device_class = 8; } message TextSensorStateResponse { option (id) = 27; @@ -669,6 +688,7 @@ message SubscribeHomeAssistantStateResponse { option (source) = SOURCE_SERVER; string entity_id = 1; string attribute = 2; + bool once = 3; } message HomeAssistantStateResponse { @@ -849,6 +869,10 @@ message ListEntitiesClimateResponse { string icon = 19; EntityCategory entity_category = 20; float visual_current_temperature_step = 21; + bool supports_current_humidity = 22; + bool supports_target_humidity = 23; + float visual_min_humidity = 24; + float visual_max_humidity = 25; } message ClimateStateResponse { option (id) = 47; @@ -869,6 +893,8 @@ message ClimateStateResponse { string custom_fan_mode = 11; ClimatePreset preset = 12; string custom_preset = 13; + float current_humidity = 14; + float target_humidity = 15; } message ClimateCommandRequest { option (id) = 48; @@ -897,6 +923,8 @@ message ClimateCommandRequest { ClimatePreset preset = 19; bool has_custom_preset = 20; string custom_preset = 21; + bool has_target_humidity = 22; + float target_humidity = 23; } // ==================== NUMBER ==================== @@ -1081,6 +1109,19 @@ enum MediaPlayerCommand { MEDIA_PLAYER_COMMAND_MUTE = 3; MEDIA_PLAYER_COMMAND_UNMUTE = 4; } +enum MediaPlayerFormatPurpose { + MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0; + MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1; +} +message MediaPlayerSupportedFormat { + option (ifdef) = "USE_MEDIA_PLAYER"; + + string format = 1; + uint32 sample_rate = 2; + uint32 num_channels = 3; + MediaPlayerFormatPurpose purpose = 4; + uint32 sample_bytes = 5; +} message ListEntitiesMediaPlayerResponse { option (id) = 63; option (source) = SOURCE_SERVER; @@ -1096,6 +1137,8 @@ message ListEntitiesMediaPlayerResponse { EntityCategory entity_category = 7; bool supports_pause = 8; + + repeated MediaPlayerSupportedFormat supported_formats = 9; } message MediaPlayerStateResponse { option (id) = 64; @@ -1123,6 +1166,9 @@ message MediaPlayerCommandRequest { bool has_media_url = 6; string media_url = 7; + + bool has_announcement = 8; + bool announcement = 9; } // ==================== BLUETOOTH ==================== @@ -1130,6 +1176,8 @@ message SubscribeBluetoothLEAdvertisementsRequest { option (id) = 66; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint32 flags = 1; } message BluetoothServiceData { @@ -1154,6 +1202,23 @@ message BluetoothLEAdvertisementResponse { uint32 address_type = 7; } +message BluetoothLERawAdvertisement { + uint64 address = 1; + sint32 rssi = 2; + uint32 address_type = 3; + + bytes data = 4; +} + +message BluetoothLERawAdvertisementsResponse { + option (id) = 93; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + option (no_delay) = true; + + repeated BluetoothLERawAdvertisement advertisements = 1; +} + enum BluetoothDeviceRequestType { BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; @@ -1383,12 +1448,30 @@ message BluetoothDeviceClearCacheResponse { } // ==================== PUSH TO TALK ==================== +enum VoiceAssistantSubscribeFlag { + VOICE_ASSISTANT_SUBSCRIBE_NONE = 0; + VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1; +} + message SubscribeVoiceAssistantRequest { option (id) = 89; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_VOICE_ASSISTANT"; bool subscribe = 1; + uint32 flags = 2; +} + +enum VoiceAssistantRequestFlag { + VOICE_ASSISTANT_REQUEST_NONE = 0; + VOICE_ASSISTANT_REQUEST_USE_VAD = 1; + VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD = 2; +} + +message VoiceAssistantAudioSettings { + uint32 noise_suppression_level = 1; + uint32 auto_gain = 2; + float volume_multiplier = 3; } message VoiceAssistantRequest { @@ -1397,6 +1480,10 @@ message VoiceAssistantRequest { option (ifdef) = "USE_VOICE_ASSISTANT"; bool start = 1; + string conversation_id = 2; + uint32 flags = 3; + VoiceAssistantAudioSettings audio_settings = 4; + string wake_word_phrase = 5; } message VoiceAssistantResponse { @@ -1418,6 +1505,12 @@ enum VoiceAssistantEvent { VOICE_ASSISTANT_INTENT_END = 6; VOICE_ASSISTANT_TTS_START = 7; VOICE_ASSISTANT_TTS_END = 8; + VOICE_ASSISTANT_WAKE_WORD_START = 9; + VOICE_ASSISTANT_WAKE_WORD_END = 10; + VOICE_ASSISTANT_STT_VAD_START = 11; + VOICE_ASSISTANT_STT_VAD_END = 12; + VOICE_ASSISTANT_TTS_STREAM_START = 98; + VOICE_ASSISTANT_TTS_STREAM_END = 99; } message VoiceAssistantEventData { @@ -1433,3 +1526,428 @@ message VoiceAssistantEventResponse { VoiceAssistantEvent event_type = 1; repeated VoiceAssistantEventData data = 2; } + +message VoiceAssistantAudio { + option (id) = 106; + option (source) = SOURCE_BOTH; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + bytes data = 1; + bool end = 2; +} + +enum VoiceAssistantTimerEvent { + VOICE_ASSISTANT_TIMER_STARTED = 0; + VOICE_ASSISTANT_TIMER_UPDATED = 1; + VOICE_ASSISTANT_TIMER_CANCELLED = 2; + VOICE_ASSISTANT_TIMER_FINISHED = 3; +} + +message VoiceAssistantTimerEventResponse { + option (id) = 115; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + VoiceAssistantTimerEvent event_type = 1; + string timer_id = 2; + string name = 3; + uint32 total_seconds = 4; + uint32 seconds_left = 5; + bool is_active = 6; +} + +message VoiceAssistantAnnounceRequest { + option (id) = 119; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + string media_id = 1; + string text = 2; +} + +message VoiceAssistantAnnounceFinished { + option (id) = 120; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + bool success = 1; +} + +message VoiceAssistantWakeWord { + string id = 1; + string wake_word = 2; + repeated string trained_languages = 3; +} + +message VoiceAssistantConfigurationRequest { + option (id) = 121; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; +} + +message VoiceAssistantConfigurationResponse { + option (id) = 122; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + repeated VoiceAssistantWakeWord available_wake_words = 1; + repeated string active_wake_words = 2; + uint32 max_active_wake_words = 3; +} + +message VoiceAssistantSetConfiguration { + option (id) = 123; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + repeated string active_wake_words = 1; +} + +// ==================== ALARM CONTROL PANEL ==================== +enum AlarmControlPanelState { + ALARM_STATE_DISARMED = 0; + ALARM_STATE_ARMED_HOME = 1; + ALARM_STATE_ARMED_AWAY = 2; + ALARM_STATE_ARMED_NIGHT = 3; + ALARM_STATE_ARMED_VACATION = 4; + ALARM_STATE_ARMED_CUSTOM_BYPASS = 5; + ALARM_STATE_PENDING = 6; + ALARM_STATE_ARMING = 7; + ALARM_STATE_DISARMING = 8; + ALARM_STATE_TRIGGERED = 9; +} + +enum AlarmControlPanelStateCommand { + ALARM_CONTROL_PANEL_DISARM = 0; + ALARM_CONTROL_PANEL_ARM_AWAY = 1; + ALARM_CONTROL_PANEL_ARM_HOME = 2; + ALARM_CONTROL_PANEL_ARM_NIGHT = 3; + ALARM_CONTROL_PANEL_ARM_VACATION = 4; + ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5; + ALARM_CONTROL_PANEL_TRIGGER = 6; +} + +message ListEntitiesAlarmControlPanelResponse { + option (id) = 94; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + uint32 supported_features = 8; + bool requires_code = 9; + bool requires_code_to_arm = 10; +} + +message AlarmControlPanelStateResponse { + option (id) = 95; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + option (no_delay) = true; + fixed32 key = 1; + AlarmControlPanelState state = 2; +} + +message AlarmControlPanelCommandRequest { + option (id) = 96; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + option (no_delay) = true; + fixed32 key = 1; + AlarmControlPanelStateCommand command = 2; + string code = 3; +} + +// ===================== TEXT ===================== +enum TextMode { + TEXT_MODE_TEXT = 0; + TEXT_MODE_PASSWORD = 1; +} +message ListEntitiesTextResponse { + option (id) = 97; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_TEXT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + + uint32 min_length = 8; + uint32 max_length = 9; + string pattern = 10; + TextMode mode = 11; +} +message TextStateResponse { + option (id) = 98; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_TEXT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; + // If the Text does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; +} +message TextCommandRequest { + option (id) = 99; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_TEXT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; +} + + +// ==================== DATETIME DATE ==================== +message ListEntitiesDateResponse { + option (id) = 100; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATE"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; +} +message DateStateResponse { + option (id) = 101; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATE"; + option (no_delay) = true; + + fixed32 key = 1; + // If the date does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 2; + uint32 year = 3; + uint32 month = 4; + uint32 day = 5; +} +message DateCommandRequest { + option (id) = 102; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_DATETIME_DATE"; + option (no_delay) = true; + + fixed32 key = 1; + uint32 year = 2; + uint32 month = 3; + uint32 day = 4; +} + +// ==================== DATETIME TIME ==================== +message ListEntitiesTimeResponse { + option (id) = 103; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_TIME"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; +} +message TimeStateResponse { + option (id) = 104; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_TIME"; + option (no_delay) = true; + + fixed32 key = 1; + // If the time does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 2; + uint32 hour = 3; + uint32 minute = 4; + uint32 second = 5; +} +message TimeCommandRequest { + option (id) = 105; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_DATETIME_TIME"; + option (no_delay) = true; + + fixed32 key = 1; + uint32 hour = 2; + uint32 minute = 3; + uint32 second = 4; +} + +// ==================== EVENT ==================== +message ListEntitiesEventResponse { + option (id) = 107; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_EVENT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; + + repeated string event_types = 9; +} +message EventResponse { + option (id) = 108; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_EVENT"; + + fixed32 key = 1; + string event_type = 2; +} + +// ==================== VALVE ==================== +message ListEntitiesValveResponse { + option (id) = 109; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VALVE"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; + + bool assumed_state = 9; + bool supports_position = 10; + bool supports_stop = 11; +} + +enum ValveOperation { + VALVE_OPERATION_IDLE = 0; + VALVE_OPERATION_IS_OPENING = 1; + VALVE_OPERATION_IS_CLOSING = 2; +} +message ValveStateResponse { + option (id) = 110; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VALVE"; + option (no_delay) = true; + + fixed32 key = 1; + float position = 2; + ValveOperation current_operation = 3; +} + +message ValveCommandRequest { + option (id) = 111; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VALVE"; + option (no_delay) = true; + + fixed32 key = 1; + bool has_position = 2; + float position = 3; + bool stop = 4; +} + +// ==================== DATETIME DATETIME ==================== +message ListEntitiesDateTimeResponse { + option (id) = 112; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATETIME"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; +} +message DateTimeStateResponse { + option (id) = 113; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATETIME"; + option (no_delay) = true; + + fixed32 key = 1; + // If the datetime does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 2; + fixed32 epoch_seconds = 3; +} +message DateTimeCommandRequest { + option (id) = 114; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_DATETIME_DATETIME"; + option (no_delay) = true; + + fixed32 key = 1; + fixed32 epoch_seconds = 2; +} + +// ==================== UPDATE ==================== +message ListEntitiesUpdateResponse { + option (id) = 116; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_UPDATE"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; +} +message UpdateStateResponse { + option (id) = 117; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_UPDATE"; + option (no_delay) = true; + + fixed32 key = 1; + bool missing_state = 2; + bool in_progress = 3; + bool has_progress = 4; + float progress = 5; + string current_version = 6; + string latest_version = 7; + string title = 8; + string release_summary = 9; + string release_url = 10; +} +enum UpdateCommand { + UPDATE_COMMAND_NONE = 0; + UPDATE_COMMAND_UPDATE = 1; + UPDATE_COMMAND_CHECK = 2; +} +message UpdateCommandRequest { + option (id) = 118; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_UPDATE"; + option (no_delay) = true; + + fixed32 key = 1; + UpdateCommand command = 2; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c350197e68..bb55a2ccf6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1,6 +1,8 @@ #include "api_connection.h" +#ifdef USE_API #include #include +#include #include "esphome/components/network/util.h" #include "esphome/core/entity_base.h" #include "esphome/core/hal.h" @@ -31,9 +33,9 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa this->proto_write_buffer_.reserve(64); #if defined(USE_API_PLAINTEXT) - helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif @@ -41,14 +43,29 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa void APIConnection::start() { this->last_traffic_ = millis(); - APIError err = helper_->init(); + APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), + errno); return; } - client_info_ = helper_->getpeername(); - helper_->set_log_info(client_info_); + this->client_info_ = helper_->getpeername(); + this->client_peername_ = this->client_info_; + this->helper_->set_log_info(this->client_info_); +} + +APIConnection::~APIConnection() { +#ifdef USE_BLUETOOTH_PROXY + if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) { + bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); + } +#endif +#ifdef USE_VOICE_ASSISTANT + if (voice_assistant::global_voice_assistant->get_api_connection() == this) { + voice_assistant::global_voice_assistant->client_subscription(this, false); + } +#endif } void APIConnection::loop() { @@ -59,7 +76,7 @@ void APIConnection::loop() { // when network is disconnected force disconnect immediately // don't wait for timeout this->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", this->client_combined_info_.c_str()); return; } if (this->next_close_) { @@ -69,24 +86,26 @@ void APIConnection::loop() { return; } - APIError err = helper_->loop(); + APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + api_error_to_str(err), errno); return; } ReadPacketBuffer buffer; - err = helper_->read_packet(&buffer); + err = this->helper_->read_packet(&buffer); if (err == APIError::WOULD_BLOCK) { // pass } else if (err != APIError::OK) { on_fatal_error(); if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); } else if (err == APIError::CONNECTION_CLOSED) { - ESP_LOGW(TAG, "%s: Connection closed", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str()); } else { - ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), + errno); } return; } else { @@ -100,18 +119,34 @@ void APIConnection::loop() { this->list_entities_iterator_.advance(); this->initial_state_iterator_.advance(); - const uint32_t keepalive = 60000; + static uint32_t keepalive = 60000; + static uint8_t max_ping_retries = 60; + static uint16_t ping_retry_interval = 1000; const uint32_t now = millis(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (keepalive * 5) / 2) { on_fatal_error(); - ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); + ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str()); } - } else if (now - this->last_traffic_ > keepalive) { + } else if (now - this->last_traffic_ > keepalive && now > this->next_ping_retry_) { ESP_LOGVV(TAG, "Sending keepalive PING..."); - this->sent_ping_ = true; - this->send_ping_request(PingRequest()); + this->sent_ping_ = this->send_ping_request(PingRequest()); + if (!this->sent_ping_) { + this->next_ping_retry_ = now + ping_retry_interval; + this->ping_retries_++; + if (this->ping_retries_ >= max_ping_retries) { + on_fatal_error(); + ESP_LOGE(TAG, "%s: Sending keepalive failed %d time(s). Disconnecting...", this->client_combined_info_.c_str(), + this->ping_retries_); + } else if (this->ping_retries_ >= 10) { + ESP_LOGW(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms", + this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval); + } else { + ESP_LOGD(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms", + this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval); + } + } } #ifdef USE_ESP32_CAMERA @@ -145,6 +180,7 @@ void APIConnection::loop() { SubscribeHomeAssistantStateResponse resp; resp.entity_id = it.entity_id; resp.attribute = it.attribute.value(); + resp.once = it.once; if (this->send_subscribe_home_assistant_state_response(resp)) { state_subs_at_++; } @@ -160,7 +196,7 @@ DisconnectResponse APIConnection::disconnect(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 requested disconnected", client_info_.c_str()); + ESP_LOGD(TAG, "%s requested disconnected", this->client_combined_info_.c_str()); this->next_close_ = true; DisconnectResponse resp; return resp; @@ -275,6 +311,8 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { } if (traits.supports_direction()) resp.direction = static_cast(fan->direction); + if (traits.supports_preset_modes()) + resp.preset_mode = fan->preset_mode; return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::Fan *fan) { @@ -289,6 +327,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) { msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); + for (auto const &preset : traits.supported_preset_modes()) + msg.supported_preset_modes.push_back(preset); msg.disabled_by_default = fan->is_disabled_by_default(); msg.icon = fan->get_icon(); msg.entity_category = static_cast(fan->get_entity_category()); @@ -310,6 +350,8 @@ 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.perform(); } #endif @@ -503,6 +545,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) msg.icon = text_sensor->get_icon(); msg.disabled_by_default = text_sensor->is_disabled_by_default(); msg.entity_category = static_cast(text_sensor->get_entity_category()); + msg.device_class = text_sensor->get_device_class(); return this->send_list_entities_text_sensor_response(msg); } #endif @@ -536,6 +579,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { resp.custom_preset = climate->custom_preset.value(); if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); + if (traits.get_supports_current_humidity()) + resp.current_humidity = climate->current_humidity; + if (traits.get_supports_target_humidity()) + resp.target_humidity = climate->target_humidity; return this->send_climate_state_response(resp); } bool APIConnection::send_climate_info(climate::Climate *climate) { @@ -552,7 +599,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.entity_category = static_cast(climate->get_entity_category()); msg.supports_current_temperature = traits.get_supports_current_temperature(); + msg.supports_current_humidity = traits.get_supports_current_humidity(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); + msg.supports_target_humidity = traits.get_supports_target_humidity(); for (auto mode : traits.get_supported_modes()) msg.supported_modes.push_back(static_cast(mode)); @@ -561,6 +610,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); + msg.visual_min_humidity = traits.get_visual_min_humidity(); + msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); @@ -591,6 +642,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { call.set_target_temperature_low(msg.target_temperature_low); if (msg.has_target_temperature_high) call.set_target_temperature_high(msg.target_temperature_high); + if (msg.has_target_humidity) + call.set_target_humidity(msg.target_humidity); if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) @@ -647,6 +700,156 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { } #endif +#ifdef USE_DATETIME_DATE +bool APIConnection::send_date_state(datetime::DateEntity *date) { + if (!this->state_subscription_) + return false; + + DateStateResponse resp{}; + resp.key = date->get_object_id_hash(); + resp.missing_state = !date->has_state(); + resp.year = date->year; + resp.month = date->month; + resp.day = date->day; + return this->send_date_state_response(resp); +} +bool APIConnection::send_date_info(datetime::DateEntity *date) { + ListEntitiesDateResponse msg; + msg.key = date->get_object_id_hash(); + msg.object_id = date->get_object_id(); + if (date->has_own_name()) + msg.name = date->get_name(); + msg.unique_id = get_default_unique_id("date", date); + msg.icon = date->get_icon(); + msg.disabled_by_default = date->is_disabled_by_default(); + msg.entity_category = static_cast(date->get_entity_category()); + + return this->send_list_entities_date_response(msg); +} +void APIConnection::date_command(const DateCommandRequest &msg) { + datetime::DateEntity *date = App.get_date_by_key(msg.key); + if (date == nullptr) + return; + + auto call = date->make_call(); + call.set_date(msg.year, msg.month, msg.day); + call.perform(); +} +#endif + +#ifdef USE_DATETIME_TIME +bool APIConnection::send_time_state(datetime::TimeEntity *time) { + if (!this->state_subscription_) + return false; + + TimeStateResponse resp{}; + resp.key = time->get_object_id_hash(); + resp.missing_state = !time->has_state(); + resp.hour = time->hour; + resp.minute = time->minute; + resp.second = time->second; + return this->send_time_state_response(resp); +} +bool APIConnection::send_time_info(datetime::TimeEntity *time) { + ListEntitiesTimeResponse msg; + msg.key = time->get_object_id_hash(); + msg.object_id = time->get_object_id(); + if (time->has_own_name()) + msg.name = time->get_name(); + msg.unique_id = get_default_unique_id("time", time); + msg.icon = time->get_icon(); + msg.disabled_by_default = time->is_disabled_by_default(); + msg.entity_category = static_cast(time->get_entity_category()); + + return this->send_list_entities_time_response(msg); +} +void APIConnection::time_command(const TimeCommandRequest &msg) { + datetime::TimeEntity *time = App.get_time_by_key(msg.key); + if (time == nullptr) + return; + + auto call = time->make_call(); + call.set_time(msg.hour, msg.minute, msg.second); + call.perform(); +} +#endif + +#ifdef USE_DATETIME_DATETIME +bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { + if (!this->state_subscription_) + return false; + + DateTimeStateResponse resp{}; + resp.key = datetime->get_object_id_hash(); + resp.missing_state = !datetime->has_state(); + if (datetime->has_state()) { + ESPTime state = datetime->state_as_esptime(); + resp.epoch_seconds = state.timestamp; + } + return this->send_date_time_state_response(resp); +} +bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { + ListEntitiesDateTimeResponse msg; + msg.key = datetime->get_object_id_hash(); + msg.object_id = datetime->get_object_id(); + if (datetime->has_own_name()) + msg.name = datetime->get_name(); + msg.unique_id = get_default_unique_id("datetime", datetime); + msg.icon = datetime->get_icon(); + msg.disabled_by_default = datetime->is_disabled_by_default(); + msg.entity_category = static_cast(datetime->get_entity_category()); + + return this->send_list_entities_date_time_response(msg); +} +void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { + datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key); + if (datetime == nullptr) + return; + + auto call = datetime->make_call(); + call.set_datetime(msg.epoch_seconds); + call.perform(); +} +#endif + +#ifdef USE_TEXT +bool APIConnection::send_text_state(text::Text *text, std::string state) { + if (!this->state_subscription_) + return false; + + TextStateResponse resp{}; + resp.key = text->get_object_id_hash(); + resp.state = std::move(state); + resp.missing_state = !text->has_state(); + return this->send_text_state_response(resp); +} +bool APIConnection::send_text_info(text::Text *text) { + ListEntitiesTextResponse msg; + msg.key = text->get_object_id_hash(); + msg.object_id = text->get_object_id(); + msg.name = text->get_name(); + msg.icon = text->get_icon(); + msg.disabled_by_default = text->is_disabled_by_default(); + msg.entity_category = static_cast(text->get_entity_category()); + 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.pattern = text->traits.get_pattern(); + + return this->send_list_entities_text_response(msg); +} +void APIConnection::text_command(const TextCommandRequest &msg) { + text::Text *text = App.get_text_by_key(msg.key); + if (text == nullptr) + return; + + auto call = text->make_call(); + call.set_value(msg.state); + call.perform(); +} +#endif + #ifdef USE_SELECT bool APIConnection::send_select_state(select::Select *select, std::string state) { if (!this->state_subscription_) @@ -752,6 +955,48 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { } #endif +#ifdef USE_VALVE +bool APIConnection::send_valve_state(valve::Valve *valve) { + if (!this->state_subscription_) + return false; + + ValveStateResponse resp{}; + resp.key = valve->get_object_id_hash(); + resp.position = valve->position; + resp.current_operation = static_cast(valve->current_operation); + return this->send_valve_state_response(resp); +} +bool APIConnection::send_valve_info(valve::Valve *valve) { + auto traits = valve->get_traits(); + ListEntitiesValveResponse msg; + msg.key = valve->get_object_id_hash(); + msg.object_id = valve->get_object_id(); + if (valve->has_own_name()) + msg.name = valve->get_name(); + msg.unique_id = get_default_unique_id("valve", valve); + msg.icon = valve->get_icon(); + msg.disabled_by_default = valve->is_disabled_by_default(); + msg.entity_category = static_cast(valve->get_entity_category()); + msg.device_class = valve->get_device_class(); + msg.assumed_state = traits.get_is_assumed_state(); + msg.supports_position = traits.get_supports_position(); + msg.supports_stop = traits.get_supports_stop(); + return this->send_list_entities_valve_response(msg); +} +void APIConnection::valve_command(const ValveCommandRequest &msg) { + valve::Valve *valve = App.get_valve_by_key(msg.key); + if (valve == nullptr) + return; + + auto call = valve->make_call(); + if (msg.has_position) + call.set_position(msg.position); + if (msg.stop) + call.set_command_stop(); + call.perform(); +} +#endif + #ifdef USE_MEDIA_PLAYER bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { if (!this->state_subscription_) @@ -759,7 +1004,11 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla MediaPlayerStateResponse resp{}; resp.key = media_player->get_object_id_hash(); - resp.state = static_cast(media_player->state); + + media_player::MediaPlayerState report_state = media_player->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING + ? media_player::MEDIA_PLAYER_STATE_PLAYING + : media_player->state; + resp.state = static_cast(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); return this->send_media_player_state_response(resp); @@ -778,6 +1027,16 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play auto traits = media_player->get_traits(); msg.supports_pause = traits.get_supports_pause(); + for (auto &supported_format : traits.get_supported_formats()) { + MediaPlayerSupportedFormat media_format; + media_format.format = 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); + media_format.sample_bytes = supported_format.sample_bytes; + msg.supported_formats.push_back(media_format); + } + return this->send_list_entities_media_player_response(msg); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { @@ -795,6 +1054,9 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { if (msg.has_media_url) { call.set_media_url(msg.media_url); } + if (msg.has_announcement) { + call.set_announcement(msg.announcement); + } call.perform(); } #endif @@ -845,9 +1107,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { #endif #ifdef USE_BLUETOOTH_PROXY +void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); +} +void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); +} bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { - if (!this->bluetooth_le_advertisement_subscription_) - return false; if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { BluetoothLEAdvertisementResponse resp = msg; for (auto &service : resp.service_data) { @@ -895,27 +1161,246 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_ #endif #ifdef USE_VOICE_ASSISTANT -bool APIConnection::request_voice_assistant(bool start) { - if (!this->voice_assistant_subscription_) - return false; - VoiceAssistantRequest msg; - msg.start = start; - return this->send_voice_assistant_request(msg); +void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { + if (voice_assistant::global_voice_assistant != nullptr) { + voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); + } } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { if (voice_assistant::global_voice_assistant != nullptr) { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - this->helper_->getpeername((struct sockaddr *) &storage, &len); - voice_assistant::global_voice_assistant->start(&storage, msg.port); + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + + if (msg.error) { + voice_assistant::global_voice_assistant->failed_to_start(); + return; + } + if (msg.port == 0) { + // Use API Audio + voice_assistant::global_voice_assistant->start_streaming(); + } else { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + this->helper_->getpeername((struct sockaddr *) &storage, &len); + voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port); + } } }; void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) { if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + voice_assistant::global_voice_assistant->on_event(msg); } } +void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) { + if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + voice_assistant::global_voice_assistant->on_audio(msg); + } +}; +void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) { + if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + + voice_assistant::global_voice_assistant->on_timer_event(msg); + } +}; + +void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) { + if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + + voice_assistant::global_voice_assistant->on_announce(msg); + } +} + +VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration( + const VoiceAssistantConfigurationRequest &msg) { + VoiceAssistantConfigurationResponse resp; + if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return resp; + } + + auto &config = voice_assistant::global_voice_assistant->get_configuration(); + for (auto &wake_word : config.available_wake_words) { + VoiceAssistantWakeWord resp_wake_word; + resp_wake_word.id = wake_word.id; + resp_wake_word.wake_word = wake_word.wake_word; + for (const auto &lang : wake_word.trained_languages) { + resp_wake_word.trained_languages.push_back(lang); + } + resp.available_wake_words.push_back(std::move(resp_wake_word)); + } + for (auto &wake_word_id : config.active_wake_words) { + resp.active_wake_words.push_back(wake_word_id); + } + resp.max_active_wake_words = config.max_active_wake_words; + } + return resp; +} + +void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { + if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + + voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words); + } +} + +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + if (!this->state_subscription_) + return false; + + AlarmControlPanelStateResponse resp{}; + resp.key = a_alarm_control_panel->get_object_id_hash(); + resp.state = static_cast(a_alarm_control_panel->get_state()); + return this->send_alarm_control_panel_state_response(resp); +} +bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + ListEntitiesAlarmControlPanelResponse msg; + msg.key = a_alarm_control_panel->get_object_id_hash(); + msg.object_id = a_alarm_control_panel->get_object_id(); + msg.name = a_alarm_control_panel->get_name(); + msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel); + msg.icon = a_alarm_control_panel->get_icon(); + msg.disabled_by_default = a_alarm_control_panel->is_disabled_by_default(); + msg.entity_category = static_cast(a_alarm_control_panel->get_entity_category()); + msg.supported_features = a_alarm_control_panel->get_supported_features(); + msg.requires_code = a_alarm_control_panel->get_requires_code(); + msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); + return this->send_list_entities_alarm_control_panel_response(msg); +} +void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { + alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); + if (a_alarm_control_panel == nullptr) + return; + + auto call = a_alarm_control_panel->make_call(); + switch (msg.command) { + case enums::ALARM_CONTROL_PANEL_DISARM: + call.disarm(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_AWAY: + call.arm_away(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_HOME: + call.arm_home(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: + call.arm_night(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_VACATION: + call.arm_vacation(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: + call.arm_custom_bypass(); + break; + case enums::ALARM_CONTROL_PANEL_TRIGGER: + call.pending(); + break; + } + call.set_code(msg.code); + call.perform(); +} +#endif + +#ifdef USE_EVENT +bool APIConnection::send_event(event::Event *event, std::string event_type) { + EventResponse resp{}; + resp.key = event->get_object_id_hash(); + resp.event_type = std::move(event_type); + return this->send_event_response(resp); +} +bool APIConnection::send_event_info(event::Event *event) { + ListEntitiesEventResponse msg; + msg.key = event->get_object_id_hash(); + msg.object_id = event->get_object_id(); + if (event->has_own_name()) + msg.name = event->get_name(); + msg.unique_id = get_default_unique_id("event", event); + msg.icon = event->get_icon(); + msg.disabled_by_default = event->is_disabled_by_default(); + msg.entity_category = static_cast(event->get_entity_category()); + msg.device_class = event->get_device_class(); + for (const auto &event_type : event->get_event_types()) + msg.event_types.push_back(event_type); + return this->send_list_entities_event_response(msg); +} +#endif + +#ifdef USE_UPDATE +bool APIConnection::send_update_state(update::UpdateEntity *update) { + if (!this->state_subscription_) + return false; + + UpdateStateResponse resp{}; + resp.key = update->get_object_id_hash(); + resp.missing_state = !update->has_state(); + if (update->has_state()) { + resp.in_progress = update->state == update::UpdateState::UPDATE_STATE_INSTALLING; + if (update->update_info.has_progress) { + resp.has_progress = true; + resp.progress = update->update_info.progress; + } + resp.current_version = update->update_info.current_version; + resp.latest_version = update->update_info.latest_version; + resp.title = update->update_info.title; + resp.release_summary = update->update_info.summary; + resp.release_url = update->update_info.release_url; + } + + return this->send_update_state_response(resp); +} +bool APIConnection::send_update_info(update::UpdateEntity *update) { + ListEntitiesUpdateResponse msg; + msg.key = update->get_object_id_hash(); + msg.object_id = update->get_object_id(); + if (update->has_own_name()) + msg.name = update->get_name(); + msg.unique_id = get_default_unique_id("update", update); + msg.icon = update->get_icon(); + msg.disabled_by_default = update->is_disabled_by_default(); + msg.entity_category = static_cast(update->get_entity_category()); + msg.device_class = update->get_device_class(); + return this->send_list_entities_update_response(msg); +} +void APIConnection::update_command(const UpdateCommandRequest &msg) { + update::UpdateEntity *update = App.get_update_by_key(msg.key); + if (update == nullptr) + return; + + switch (msg.command) { + case enums::UPDATE_COMMAND_UPDATE: + update->perform(); + break; + case enums::UPDATE_COMMAND_CHECK: + update->check(); + break; + case enums::UPDATE_COMMAND_NONE: + ESP_LOGE(TAG, "UPDATE_COMMAND_NONE not handled. Check client is sending the correct command"); + break; + default: + ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command); + break; + } +} #endif bool APIConnection::send_log_message(int level, const char *tag, const char *line) { @@ -933,16 +1418,18 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin } HelloResponse APIConnection::hello(const HelloRequest &msg) { - this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; - this->helper_->set_log_info(client_info_); + this->client_info_ = msg.client_info; + this->client_peername_ = this->helper_->getpeername(); + this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")"; + this->helper_->set_log_info(this->client_combined_info_); 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' | API Version %" PRIu32 ".%" PRIu32, this->client_info_.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->client_info_.c_str(), + this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 8; + resp.api_version_minor = 10; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); @@ -956,9 +1443,9 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: Connected successfully", this->client_combined_info_.c_str()); this->connection_state_ = ConnectionState::AUTHENTICATED; - + this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { this->send_time_request(); @@ -972,6 +1459,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.uses_password = this->parent_->uses_password(); resp.name = App.get_name(); resp.friendly_name = App.get_friendly_name(); + resp.suggested_area = App.get_area(); resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); @@ -979,6 +1467,10 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.manufacturer = "Espressif"; #elif defined(USE_RP2040) resp.manufacturer = "Raspberry Pi"; +#elif defined(USE_BK72XX) + resp.manufacturer = "Beken"; +#elif defined(USE_RTL87XX) + resp.manufacturer = "Realtek"; #elif defined(USE_HOST) resp.manufacturer = "Host"; #endif @@ -994,12 +1486,12 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY - resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() - ? bluetooth_proxy::ACTIVE_CONNECTIONS_VERSION - : bluetooth_proxy::PASSIVE_ONLY_VERSION; + resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version(); + resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); #endif #ifdef USE_VOICE_ASSISTANT - resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version(); + resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version(); + resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); #endif return resp; } @@ -1029,10 +1521,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) return false; if (!this->helper_->can_write_without_blocking()) { delay(0); - APIError err = helper_->loop(); + APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + api_error_to_str(err), errno); return false; } if (!this->helper_->can_write_without_blocking()) { @@ -1051,9 +1544,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) if (err != APIError::OK) { on_fatal_error(); if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); } else { - ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), + errno); } return false; } @@ -1062,11 +1556,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_combined_info_.c_str()); } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_combined_info_.c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -1075,3 +1569,4 @@ void APIConnection::on_fatal_error() { } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 78ecbb98e6..043aaee421 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -1,12 +1,13 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_API #include "api_frame_helper.h" #include "api_pb2.h" #include "api_pb2_service.h" #include "api_server.h" #include "esphome/core/application.h" #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include @@ -16,7 +17,7 @@ namespace api { class APIConnection : public APIServerConnection { public: APIConnection(std::unique_ptr socket, APIServer *parent); - virtual ~APIConnection() = default; + virtual ~APIConnection(); void start(); void loop(); @@ -72,6 +73,26 @@ class APIConnection : public APIServerConnection { bool send_number_info(number::Number *number); void number_command(const NumberCommandRequest &msg) override; #endif +#ifdef USE_DATETIME_DATE + bool send_date_state(datetime::DateEntity *date); + bool send_date_info(datetime::DateEntity *date); + void date_command(const DateCommandRequest &msg) override; +#endif +#ifdef USE_DATETIME_TIME + bool send_time_state(datetime::TimeEntity *time); + bool send_time_info(datetime::TimeEntity *time); + void time_command(const TimeCommandRequest &msg) override; +#endif +#ifdef USE_DATETIME_DATETIME + bool send_datetime_state(datetime::DateTimeEntity *datetime); + bool send_datetime_info(datetime::DateTimeEntity *datetime); + void datetime_command(const DateTimeCommandRequest &msg) override; +#endif +#ifdef USE_TEXT + bool send_text_state(text::Text *text, std::string state); + bool send_text_info(text::Text *text); + void text_command(const TextCommandRequest &msg) override; +#endif #ifdef USE_SELECT bool send_select_state(select::Select *select, std::string state); bool send_select_info(select::Select *select); @@ -86,6 +107,11 @@ class APIConnection : public APIServerConnection { bool send_lock_info(lock::Lock *a_lock); void lock_command(const LockCommandRequest &msg) override; #endif +#ifdef USE_VALVE + bool send_valve_state(valve::Valve *valve); + bool send_valve_info(valve::Valve *valve); + void valve_command(const ValveCommandRequest &msg) override; +#endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); bool send_media_player_info(media_player::MediaPlayer *media_player); @@ -98,12 +124,8 @@ class APIConnection : public APIServerConnection { this->send_homeassistant_service_response(call); } #ifdef USE_BLUETOOTH_PROXY - void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override { - this->bluetooth_le_advertisement_subscription_ = true; - } - void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override { - this->bluetooth_le_advertisement_subscription_ = false; - } + void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; + void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; @@ -125,17 +147,38 @@ class APIConnection : public APIServerConnection { #endif #ifdef USE_VOICE_ASSISTANT - void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { - this->voice_assistant_subscription_ = msg.subscribe; - } - bool request_voice_assistant(bool start); + void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override; void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; + void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; + VoiceAssistantConfigurationResponse voice_assistant_get_configuration( + const VoiceAssistantConfigurationRequest &msg) override; + void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); + bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); + void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; +#endif + +#ifdef USE_EVENT + bool send_event(event::Event *event, std::string event_type); + bool send_event_info(event::Event *event); +#endif + +#ifdef USE_UPDATE + bool send_update_state(update::UpdateEntity *update); + bool send_update_info(update::UpdateEntity *update); + void update_command(const UpdateCommandRequest &msg) override; #endif void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping + this->ping_retries_ = 0; this->sent_ping_ = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; @@ -181,6 +224,8 @@ class APIConnection : public APIServerConnection { } bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; + std::string get_client_combined_info() const { return this->client_combined_info_; } + protected: friend APIServer; @@ -200,6 +245,8 @@ class APIConnection : public APIServerConnection { std::unique_ptr helper_; std::string client_info_; + std::string client_peername_; + std::string client_combined_info_; uint32_t client_api_version_major_{0}; uint32_t client_api_version_minor_{0}; #ifdef USE_ESP32_CAMERA @@ -209,14 +256,10 @@ class APIConnection : public APIServerConnection { bool state_subscription_{false}; int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; uint32_t last_traffic_; + uint32_t next_ping_retry_{0}; + uint8_t ping_retries_{0}; bool sent_ping_{false}; bool service_call_subscription_{false}; -#ifdef USE_BLUETOOTH_PROXY - bool bluetooth_le_advertisement_subscription_{false}; -#endif -#ifdef USE_VOICE_ASSISTANT - bool voice_assistant_subscription_{false}; -#endif bool next_close_ = false; APIServer *parent_; InitialStateIterator initial_state_iterator_; @@ -226,3 +269,4 @@ class APIConnection : public APIServerConnection { } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index f4b18a1fd6..62f375508c 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,5 +1,5 @@ #include "api_frame_helper.h" - +#ifdef USE_API #include "esphome/core/log.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -1028,3 +1028,4 @@ APIError APIPlaintextFrameHelper::shutdown(int how) { } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index bf4872d2d6..56d8bf1973 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -5,7 +5,7 @@ #include #include "esphome/core/defines.h" - +#ifdef USE_API #ifdef USE_API_NOISE #include "noise/protocol.h" #endif @@ -190,3 +190,4 @@ class APIPlaintextFrameHelper : public APIFrameHelper { } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1dd8c82e00..8df152881c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3,6 +3,8 @@ #include "api_pb2.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace api { @@ -385,6 +387,18 @@ template<> const char *proto_enum_to_string(enums::Me } #endif #ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) { + switch (value) { + case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: + return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; + case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: + return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP template<> const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) { switch (value) { @@ -408,6 +422,33 @@ const char *proto_enum_to_string(enums::Bluet } #endif #ifdef HAS_PROTO_MESSAGE_DUMP +template<> +const char *proto_enum_to_string(enums::VoiceAssistantSubscribeFlag value) { + switch (value) { + case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE: + return "VOICE_ASSISTANT_SUBSCRIBE_NONE"; + case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO: + return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::VoiceAssistantRequestFlag value) { + switch (value) { + case enums::VOICE_ASSISTANT_REQUEST_NONE: + return "VOICE_ASSISTANT_REQUEST_NONE"; + case enums::VOICE_ASSISTANT_REQUEST_USE_VAD: + return "VOICE_ASSISTANT_REQUEST_USE_VAD"; + case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD: + return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP template<> const char *proto_enum_to_string(enums::VoiceAssistantEvent value) { switch (value) { case enums::VOICE_ASSISTANT_ERROR: @@ -428,6 +469,125 @@ template<> const char *proto_enum_to_string(enums::V return "VOICE_ASSISTANT_TTS_START"; case enums::VOICE_ASSISTANT_TTS_END: return "VOICE_ASSISTANT_TTS_END"; + case enums::VOICE_ASSISTANT_WAKE_WORD_START: + return "VOICE_ASSISTANT_WAKE_WORD_START"; + case enums::VOICE_ASSISTANT_WAKE_WORD_END: + return "VOICE_ASSISTANT_WAKE_WORD_END"; + case enums::VOICE_ASSISTANT_STT_VAD_START: + return "VOICE_ASSISTANT_STT_VAD_START"; + case enums::VOICE_ASSISTANT_STT_VAD_END: + return "VOICE_ASSISTANT_STT_VAD_END"; + case enums::VOICE_ASSISTANT_TTS_STREAM_START: + return "VOICE_ASSISTANT_TTS_STREAM_START"; + case enums::VOICE_ASSISTANT_TTS_STREAM_END: + return "VOICE_ASSISTANT_TTS_STREAM_END"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::VoiceAssistantTimerEvent value) { + switch (value) { + case enums::VOICE_ASSISTANT_TIMER_STARTED: + return "VOICE_ASSISTANT_TIMER_STARTED"; + case enums::VOICE_ASSISTANT_TIMER_UPDATED: + return "VOICE_ASSISTANT_TIMER_UPDATED"; + case enums::VOICE_ASSISTANT_TIMER_CANCELLED: + return "VOICE_ASSISTANT_TIMER_CANCELLED"; + case enums::VOICE_ASSISTANT_TIMER_FINISHED: + return "VOICE_ASSISTANT_TIMER_FINISHED"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) { + switch (value) { + case enums::ALARM_STATE_DISARMED: + return "ALARM_STATE_DISARMED"; + case enums::ALARM_STATE_ARMED_HOME: + return "ALARM_STATE_ARMED_HOME"; + case enums::ALARM_STATE_ARMED_AWAY: + return "ALARM_STATE_ARMED_AWAY"; + case enums::ALARM_STATE_ARMED_NIGHT: + return "ALARM_STATE_ARMED_NIGHT"; + case enums::ALARM_STATE_ARMED_VACATION: + return "ALARM_STATE_ARMED_VACATION"; + case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: + return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; + case enums::ALARM_STATE_PENDING: + return "ALARM_STATE_PENDING"; + case enums::ALARM_STATE_ARMING: + return "ALARM_STATE_ARMING"; + case enums::ALARM_STATE_DISARMING: + return "ALARM_STATE_DISARMING"; + case enums::ALARM_STATE_TRIGGERED: + return "ALARM_STATE_TRIGGERED"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> +const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) { + switch (value) { + case enums::ALARM_CONTROL_PANEL_DISARM: + return "ALARM_CONTROL_PANEL_DISARM"; + case enums::ALARM_CONTROL_PANEL_ARM_AWAY: + return "ALARM_CONTROL_PANEL_ARM_AWAY"; + case enums::ALARM_CONTROL_PANEL_ARM_HOME: + return "ALARM_CONTROL_PANEL_ARM_HOME"; + case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: + return "ALARM_CONTROL_PANEL_ARM_NIGHT"; + case enums::ALARM_CONTROL_PANEL_ARM_VACATION: + return "ALARM_CONTROL_PANEL_ARM_VACATION"; + case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: + return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; + case enums::ALARM_CONTROL_PANEL_TRIGGER: + return "ALARM_CONTROL_PANEL_TRIGGER"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::TextMode value) { + switch (value) { + case enums::TEXT_MODE_TEXT: + return "TEXT_MODE_TEXT"; + case enums::TEXT_MODE_PASSWORD: + return "TEXT_MODE_PASSWORD"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::ValveOperation value) { + switch (value) { + case enums::VALVE_OPERATION_IDLE: + return "VALVE_OPERATION_IDLE"; + case enums::VALVE_OPERATION_IS_OPENING: + return "VALVE_OPERATION_IS_OPENING"; + case enums::VALVE_OPERATION_IS_CLOSING: + return "VALVE_OPERATION_IS_CLOSING"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::UpdateCommand value) { + switch (value) { + case enums::UPDATE_COMMAND_NONE: + return "UPDATE_COMMAND_NONE"; + case enums::UPDATE_COMMAND_UPDATE: + return "UPDATE_COMMAND_UPDATE"; + case enums::UPDATE_COMMAND_CHECK: + return "UPDATE_COMMAND_CHECK"; default: return "UNKNOWN"; } @@ -471,12 +631,12 @@ void HelloRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" api_version_major: "); - sprintf(buffer, "%u", this->api_version_major); + sprintf(buffer, "%" PRIu32, this->api_version_major); out.append(buffer); out.append("\n"); out.append(" api_version_minor: "); - sprintf(buffer, "%u", this->api_version_minor); + sprintf(buffer, "%" PRIu32, this->api_version_minor); out.append(buffer); out.append("\n"); out.append("}"); @@ -521,12 +681,12 @@ void HelloResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("HelloResponse {\n"); out.append(" api_version_major: "); - sprintf(buffer, "%u", this->api_version_major); + sprintf(buffer, "%" PRIu32, this->api_version_major); out.append(buffer); out.append("\n"); out.append(" api_version_minor: "); - sprintf(buffer, "%u", this->api_version_minor); + sprintf(buffer, "%" PRIu32, this->api_version_minor); out.append(buffer); out.append("\n"); @@ -617,11 +777,19 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 11: { - this->bluetooth_proxy_version = value.as_uint32(); + this->legacy_bluetooth_proxy_version = value.as_uint32(); + return true; + } + case 15: { + this->bluetooth_proxy_feature_flags = value.as_uint32(); return true; } case 14: { - this->voice_assistant_version = value.as_uint32(); + this->legacy_voice_assistant_version = value.as_uint32(); + return true; + } + case 17: { + this->voice_assistant_feature_flags = value.as_uint32(); return true; } default: @@ -666,6 +834,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->friendly_name = value.as_string(); return true; } + case 16: { + this->suggested_area = value.as_string(); + return true; + } default: return false; } @@ -681,10 +853,13 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->project_name); buffer.encode_string(9, this->project_version); buffer.encode_uint32(10, this->webserver_port); - buffer.encode_uint32(11, this->bluetooth_proxy_version); + buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version); + buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); buffer.encode_string(12, this->manufacturer); buffer.encode_string(13, this->friendly_name); - buffer.encode_uint32(14, this->voice_assistant_version); + buffer.encode_uint32(14, this->legacy_voice_assistant_version); + buffer.encode_uint32(17, this->voice_assistant_feature_flags); + buffer.encode_string(16, this->suggested_area); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -727,12 +902,17 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" webserver_port: "); - sprintf(buffer, "%u", this->webserver_port); + sprintf(buffer, "%" PRIu32, this->webserver_port); out.append(buffer); out.append("\n"); - out.append(" bluetooth_proxy_version: "); - sprintf(buffer, "%u", this->bluetooth_proxy_version); + out.append(" legacy_bluetooth_proxy_version: "); + sprintf(buffer, "%" PRIu32, this->legacy_bluetooth_proxy_version); + out.append(buffer); + out.append("\n"); + + out.append(" bluetooth_proxy_feature_flags: "); + sprintf(buffer, "%" PRIu32, this->bluetooth_proxy_feature_flags); out.append(buffer); out.append("\n"); @@ -744,10 +924,19 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("'").append(this->friendly_name).append("'"); out.append("\n"); - out.append(" voice_assistant_version: "); - sprintf(buffer, "%u", this->voice_assistant_version); + out.append(" legacy_voice_assistant_version: "); + sprintf(buffer, "%" PRIu32, this->legacy_voice_assistant_version); out.append(buffer); out.append("\n"); + + out.append(" voice_assistant_feature_flags: "); + sprintf(buffer, "%" PRIu32, this->voice_assistant_feature_flags); + out.append(buffer); + out.append("\n"); + + out.append(" suggested_area: "); + out.append("'").append(this->suggested_area).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -837,7 +1026,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -905,7 +1094,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BinarySensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1008,7 +1197,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1098,7 +1287,7 @@ void CoverStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CoverStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1181,7 +1370,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CoverCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1265,6 +1454,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi this->icon = value.as_string(); return true; } + case 12: { + this->supported_preset_modes.push_back(value.as_string()); + return true; + } default: return false; } @@ -1291,6 +1484,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->disabled_by_default); buffer.encode_string(10, this->icon); buffer.encode_enum(11, this->entity_category); + for (auto &it : this->supported_preset_modes) { + buffer.encode_string(12, it, true); + } } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { @@ -1301,7 +1497,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1326,7 +1522,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" supported_speed_count: "); - sprintf(buffer, "%d", this->supported_speed_count); + sprintf(buffer, "%" PRId32, this->supported_speed_count); out.append(buffer); out.append("\n"); @@ -1341,6 +1537,12 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + for (const auto &it : this->supported_preset_modes) { + out.append(" supported_preset_modes: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } out.append("}"); } #endif @@ -1370,6 +1572,16 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } } +bool FanStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 7: { + this->preset_mode = value.as_string(); + return true; + } + default: + return false; + } +} bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -1387,13 +1599,14 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(4, this->speed); buffer.encode_enum(5, this->direction); buffer.encode_int32(6, this->speed_level); + buffer.encode_string(7, this->preset_mode); } #ifdef HAS_PROTO_MESSAGE_DUMP void FanStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("FanStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1414,9 +1627,13 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" speed_level: "); - sprintf(buffer, "%d", this->speed_level); + sprintf(buffer, "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -1462,6 +1679,20 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed_level = value.as_int32(); return true; } + case 12: { + this->has_preset_mode = value.as_bool(); + return true; + } + default: + return false; + } +} +bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 13: { + this->preset_mode = value.as_string(); + return true; + } default: return false; } @@ -1488,13 +1719,15 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(9, this->direction); buffer.encode_bool(10, this->has_speed_level); buffer.encode_int32(11, this->speed_level); + buffer.encode_bool(12, this->has_preset_mode); + buffer.encode_string(13, this->preset_mode); } #ifdef HAS_PROTO_MESSAGE_DUMP void FanCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("FanCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1535,9 +1768,17 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" speed_level: "); - sprintf(buffer, "%d", this->speed_level); + sprintf(buffer, "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); + + out.append(" has_preset_mode: "); + out.append(YESNO(this->has_preset_mode)); + out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -1649,7 +1890,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1803,7 +2044,7 @@ void LightStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LightStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2026,7 +2267,7 @@ void LightCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LightCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2124,7 +2365,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" transition_length: "); - sprintf(buffer, "%u", this->transition_length); + sprintf(buffer, "%" PRIu32, this->transition_length); out.append(buffer); out.append("\n"); @@ -2133,7 +2374,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" flash_length: "); - sprintf(buffer, "%u", this->flash_length); + sprintf(buffer, "%" PRIu32, this->flash_length); out.append(buffer); out.append("\n"); @@ -2241,7 +2482,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2262,7 +2503,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" accuracy_decimals: "); - sprintf(buffer, "%d", this->accuracy_decimals); + sprintf(buffer, "%" PRId32, this->accuracy_decimals); out.append(buffer); out.append("\n"); @@ -2326,7 +2567,7 @@ void SensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2415,7 +2656,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2478,7 +2719,7 @@ void SwitchStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SwitchStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2517,7 +2758,7 @@ void SwitchCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SwitchCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2559,6 +2800,10 @@ bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengt this->icon = value.as_string(); return true; } + case 8: { + this->device_class = value.as_string(); + return true; + } default: return false; } @@ -2581,6 +2826,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_string(8, this->device_class); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { @@ -2591,7 +2837,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2614,6 +2860,10 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -2657,7 +2907,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TextSensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2871,6 +3121,16 @@ void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); } #endif +bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->once = value.as_bool(); + return true; + } + default: + return false; + } +} bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2888,6 +3148,7 @@ bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, Proto void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); + buffer.encode_bool(3, this->once); } #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { @@ -2900,6 +3161,10 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { out.append(" attribute: "); out.append("'").append(this->attribute).append("'"); out.append("\n"); + + out.append(" once: "); + out.append(YESNO(this->once)); + out.append("\n"); out.append("}"); } #endif @@ -2964,7 +3229,7 @@ void GetTimeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("GetTimeResponse {\n"); out.append(" epoch_seconds: "); - sprintf(buffer, "%u", this->epoch_seconds); + sprintf(buffer, "%" PRIu32, this->epoch_seconds); out.append(buffer); out.append("\n"); out.append("}"); @@ -3048,7 +3313,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3142,7 +3407,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { out.append("\n"); out.append(" legacy_int: "); - sprintf(buffer, "%d", this->legacy_int); + sprintf(buffer, "%" PRId32, this->legacy_int); out.append(buffer); out.append("\n"); @@ -3156,7 +3421,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { out.append("\n"); out.append(" int_: "); - sprintf(buffer, "%d", this->int_); + sprintf(buffer, "%" PRId32, this->int_); out.append(buffer); out.append("\n"); @@ -3168,7 +3433,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { for (const auto &it : this->int_array) { out.append(" int_array: "); - sprintf(buffer, "%d", it); + sprintf(buffer, "%" PRId32, it); out.append(buffer); out.append("\n"); } @@ -3219,7 +3484,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ExecuteServiceRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3295,7 +3560,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3361,7 +3626,7 @@ void CameraImageResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CameraImageResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3449,6 +3714,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->entity_category = value.as_enum(); return true; } + case 22: { + this->supports_current_humidity = value.as_bool(); + return true; + } + case 23: { + this->supports_target_humidity = value.as_bool(); + return true; + } default: return false; } @@ -3505,6 +3778,14 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val this->visual_current_temperature_step = value.as_float(); return true; } + case 24: { + this->visual_min_humidity = value.as_float(); + return true; + } + case 25: { + this->visual_max_humidity = value.as_float(); + return true; + } default: return false; } @@ -3543,6 +3824,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(19, this->icon); buffer.encode_enum(20, this->entity_category); buffer.encode_float(21, this->visual_current_temperature_step); + buffer.encode_bool(22, this->supports_current_humidity); + buffer.encode_bool(23, this->supports_target_humidity); + buffer.encode_float(24, this->visual_min_humidity); + buffer.encode_float(25, this->visual_max_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -3553,7 +3838,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3648,6 +3933,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->visual_current_temperature_step); out.append(buffer); out.append("\n"); + + out.append(" supports_current_humidity: "); + out.append(YESNO(this->supports_current_humidity)); + out.append("\n"); + + out.append(" supports_target_humidity: "); + out.append(YESNO(this->supports_target_humidity)); + out.append("\n"); + + out.append(" visual_min_humidity: "); + sprintf(buffer, "%g", this->visual_min_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" visual_max_humidity: "); + sprintf(buffer, "%g", this->visual_max_humidity); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -3717,6 +4020,14 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->target_temperature_high = value.as_float(); return true; } + case 14: { + this->current_humidity = value.as_float(); + return true; + } + case 15: { + this->target_humidity = value.as_float(); + return true; + } default: return false; } @@ -3735,13 +4046,15 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, this->custom_fan_mode); buffer.encode_enum(12, this->preset); buffer.encode_string(13, this->custom_preset); + buffer.encode_float(14, this->current_humidity); + buffer.encode_float(15, this->target_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ClimateStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3796,6 +4109,16 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" custom_preset: "); out.append("'").append(this->custom_preset).append("'"); out.append("\n"); + + out.append(" current_humidity: "); + sprintf(buffer, "%g", this->current_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" target_humidity: "); + sprintf(buffer, "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -3861,6 +4184,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->has_custom_preset = value.as_bool(); return true; } + case 22: { + this->has_target_humidity = value.as_bool(); + return true; + } default: return false; } @@ -3897,6 +4224,10 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->target_temperature_high = value.as_float(); return true; } + case 23: { + this->target_humidity = value.as_float(); + return true; + } default: return false; } @@ -3923,13 +4254,15 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(19, this->preset); buffer.encode_bool(20, this->has_custom_preset); buffer.encode_string(21, this->custom_preset); + buffer.encode_bool(22, this->has_target_humidity); + buffer.encode_float(23, this->target_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ClimateCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4015,6 +4348,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(" custom_preset: "); out.append("'").append(this->custom_preset).append("'"); out.append("\n"); + + out.append(" has_target_humidity: "); + out.append(YESNO(this->has_target_humidity)); + out.append("\n"); + + out.append(" target_humidity: "); + sprintf(buffer, "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -4112,7 +4454,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4199,7 +4541,7 @@ void NumberStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("NumberStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4237,7 +4579,7 @@ void NumberCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("NumberCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4319,7 +4661,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4391,7 +4733,7 @@ void SelectStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SelectStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4434,7 +4776,7 @@ void SelectCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SelectCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4528,7 +4870,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4599,7 +4941,7 @@ void LockStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LockStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4654,7 +4996,7 @@ void LockCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LockCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4741,7 +5083,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4787,7 +5129,75 @@ void ButtonCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ButtonCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->sample_rate = value.as_uint32(); + return true; + } + case 3: { + this->num_channels = value.as_uint32(); + return true; + } + case 4: { + this->purpose = value.as_enum(); + return true; + } + case 5: { + this->sample_bytes = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool MediaPlayerSupportedFormat::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->format = value.as_string(); + return true; + } + default: + return false; + } +} +void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->format); + buffer.encode_uint32(2, this->sample_rate); + buffer.encode_uint32(3, this->num_channels); + buffer.encode_enum(4, this->purpose); + buffer.encode_uint32(5, this->sample_bytes); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void MediaPlayerSupportedFormat::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerSupportedFormat {\n"); + out.append(" format: "); + out.append("'").append(this->format).append("'"); + out.append("\n"); + + out.append(" sample_rate: "); + sprintf(buffer, "%" PRIu32, this->sample_rate); + out.append(buffer); + out.append("\n"); + + out.append(" num_channels: "); + sprintf(buffer, "%" PRIu32, this->num_channels); + out.append(buffer); + out.append("\n"); + + out.append(" purpose: "); + out.append(proto_enum_to_string(this->purpose)); + out.append("\n"); + + out.append(" sample_bytes: "); + sprintf(buffer, "%" PRIu32, this->sample_bytes); out.append(buffer); out.append("\n"); out.append("}"); @@ -4829,6 +5239,10 @@ bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLeng this->icon = value.as_string(); return true; } + case 9: { + this->supported_formats.push_back(value.as_message()); + return true; + } default: return false; } @@ -4852,6 +5266,9 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_bool(8, this->supports_pause); + for (auto &it : this->supported_formats) { + buffer.encode_message(9, it, true); + } } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { @@ -4862,7 +5279,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4889,6 +5306,12 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { out.append(" supports_pause: "); out.append(YESNO(this->supports_pause)); out.append("\n"); + + for (const auto &it : this->supported_formats) { + out.append(" supported_formats: "); + it.dump_to(out); + out.append("\n"); + } out.append("}"); } #endif @@ -4931,7 +5354,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("MediaPlayerStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4968,6 +5391,14 @@ bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt val this->has_media_url = value.as_bool(); return true; } + case 8: { + this->has_announcement = value.as_bool(); + return true; + } + case 9: { + this->announcement = value.as_bool(); + return true; + } default: return false; } @@ -5004,13 +5435,15 @@ void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(5, this->volume); buffer.encode_bool(6, this->has_media_url); buffer.encode_string(7, this->media_url); + buffer.encode_bool(8, this->has_announcement); + buffer.encode_bool(9, this->announcement); } #ifdef HAS_PROTO_MESSAGE_DUMP void MediaPlayerCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("MediaPlayerCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%u", this->key); + sprintf(buffer, "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -5038,13 +5471,39 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { out.append(" media_url: "); out.append("'").append(this->media_url).append("'"); out.append("\n"); + + out.append(" has_announcement: "); + out.append(YESNO(this->has_announcement)); + out.append("\n"); + + out.append(" announcement: "); + out.append(YESNO(this->announcement)); + out.append("\n"); out.append("}"); } #endif -void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} +bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->flags = value.as_uint32(); + return true; + } + default: + return false; + } +} +void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->flags); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { - out.append("SubscribeBluetoothLEAdvertisementsRequest {}"); + __attribute__((unused)) char buffer[64]; + out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); + out.append(" flags: "); + sprintf(buffer, "%" PRIu32, this->flags); + out.append(buffer); + out.append("\n"); + out.append("}"); } #endif bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -5088,7 +5547,7 @@ void BluetoothServiceData::dump_to(std::string &out) const { for (const auto &it : this->legacy_data) { out.append(" legacy_data: "); - sprintf(buffer, "%u", it); + sprintf(buffer, "%" PRIu32, it); out.append(buffer); out.append("\n"); } @@ -5168,7 +5627,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" rssi: "); - sprintf(buffer, "%d", this->rssi); + sprintf(buffer, "%" PRId32, this->rssi); out.append(buffer); out.append("\n"); @@ -5191,12 +5650,98 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { } out.append(" address_type: "); - sprintf(buffer, "%u", this->address_type); + sprintf(buffer, "%" PRIu32, this->address_type); out.append(buffer); out.append("\n"); out.append("}"); } #endif +bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->rssi = value.as_sint32(); + return true; + } + case 3: { + this->address_type = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool BluetoothLERawAdvertisement::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 4: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_sint32(2, this->rssi); + buffer.encode_uint32(3, this->address_type); + buffer.encode_string(4, this->data); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothLERawAdvertisement::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLERawAdvertisement {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" rssi: "); + sprintf(buffer, "%" PRId32, this->rssi); + out.append(buffer); + out.append("\n"); + + out.append(" address_type: "); + sprintf(buffer, "%" PRIu32, this->address_type); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothLERawAdvertisementsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->advertisements.push_back(value.as_message()); + return true; + } + default: + return false; + } +} +void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->advertisements) { + buffer.encode_message(1, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLERawAdvertisementsResponse {\n"); + for (const auto &it : this->advertisements) { + out.append(" advertisements: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +#endif bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -5243,7 +5788,7 @@ void BluetoothDeviceRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" address_type: "); - sprintf(buffer, "%u", this->address_type); + sprintf(buffer, "%" PRIu32, this->address_type); out.append(buffer); out.append("\n"); out.append("}"); @@ -5291,12 +5836,12 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" mtu: "); - sprintf(buffer, "%u", this->mtu); + sprintf(buffer, "%" PRIu32, this->mtu); out.append(buffer); out.append("\n"); out.append(" error: "); - sprintf(buffer, "%d", this->error); + sprintf(buffer, "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -5356,7 +5901,7 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const { } out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -5412,12 +5957,12 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const { } out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append(" properties: "); - sprintf(buffer, "%u", this->properties); + sprintf(buffer, "%" PRIu32, this->properties); out.append(buffer); out.append("\n"); @@ -5474,7 +6019,7 @@ void BluetoothGATTService::dump_to(std::string &out) const { } out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -5581,7 +6126,7 @@ void BluetoothGATTReadRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -5626,7 +6171,7 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -5680,7 +6225,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -5722,7 +6267,7 @@ void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -5767,7 +6312,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -5810,7 +6355,7 @@ void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -5859,7 +6404,7 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -5898,12 +6443,12 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothConnectionsFreeResponse {\n"); out.append(" free: "); - sprintf(buffer, "%u", this->free); + sprintf(buffer, "%" PRIu32, this->free); out.append(buffer); out.append("\n"); out.append(" limit: "); - sprintf(buffer, "%u", this->limit); + sprintf(buffer, "%" PRIu32, this->limit); out.append(buffer); out.append("\n"); out.append("}"); @@ -5942,12 +6487,12 @@ void BluetoothGATTErrorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append(" error: "); - sprintf(buffer, "%d", this->error); + sprintf(buffer, "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -5981,7 +6526,7 @@ void BluetoothGATTWriteResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -6015,7 +6560,7 @@ void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%u", this->handle); + sprintf(buffer, "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -6058,7 +6603,7 @@ void BluetoothDevicePairingResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%d", this->error); + sprintf(buffer, "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -6101,7 +6646,7 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%d", this->error); + sprintf(buffer, "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -6150,7 +6695,7 @@ void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%d", this->error); + sprintf(buffer, "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -6162,11 +6707,18 @@ bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarIn this->subscribe = value.as_bool(); return true; } + case 2: { + this->flags = value.as_uint32(); + return true; + } default: return false; } } -void SubscribeVoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->subscribe); } +void SubscribeVoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_bool(1, this->subscribe); + buffer.encode_uint32(2, this->flags); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6174,6 +6726,61 @@ void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { out.append(" subscribe: "); out.append(YESNO(this->subscribe)); out.append("\n"); + + out.append(" flags: "); + sprintf(buffer, "%" PRIu32, this->flags); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantAudioSettings::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->noise_suppression_level = value.as_uint32(); + return true; + } + case 2: { + this->auto_gain = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool VoiceAssistantAudioSettings::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 3: { + this->volume_multiplier = value.as_float(); + return true; + } + default: + return false; + } +} +void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->noise_suppression_level); + buffer.encode_uint32(2, this->auto_gain); + buffer.encode_float(3, this->volume_multiplier); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantAudioSettings::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAudioSettings {\n"); + out.append(" noise_suppression_level: "); + sprintf(buffer, "%" PRIu32, this->noise_suppression_level); + out.append(buffer); + out.append("\n"); + + out.append(" auto_gain: "); + sprintf(buffer, "%" PRIu32, this->auto_gain); + out.append(buffer); + out.append("\n"); + + out.append(" volume_multiplier: "); + sprintf(buffer, "%g", this->volume_multiplier); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -6183,11 +6790,39 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->start = value.as_bool(); return true; } + case 3: { + this->flags = value.as_uint32(); + return true; + } default: return false; } } -void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); } +bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->conversation_id = value.as_string(); + return true; + } + case 4: { + this->audio_settings = value.as_message(); + return true; + } + case 5: { + this->wake_word_phrase = value.as_string(); + return true; + } + default: + return false; + } +} +void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_bool(1, this->start); + 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); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6195,6 +6830,23 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append(" start: "); out.append(YESNO(this->start)); out.append("\n"); + + out.append(" conversation_id: "); + out.append("'").append(this->conversation_id).append("'"); + out.append("\n"); + + out.append(" flags: "); + sprintf(buffer, "%" PRIu32, this->flags); + out.append(buffer); + out.append("\n"); + + out.append(" audio_settings: "); + this->audio_settings.dump_to(out); + out.append("\n"); + + out.append(" wake_word_phrase: "); + out.append("'").append(this->wake_word_phrase).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -6221,7 +6873,7 @@ void VoiceAssistantResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("VoiceAssistantResponse {\n"); out.append(" port: "); - sprintf(buffer, "%u", this->port); + sprintf(buffer, "%" PRIu32, this->port); out.append(buffer); out.append("\n"); @@ -6305,6 +6957,1990 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->end = value.as_bool(); + return true; + } + default: + return false; + } +} +bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->data); + buffer.encode_bool(2, this->end); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantAudio::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAudio {\n"); + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + + out.append(" end: "); + out.append(YESNO(this->end)); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->event_type = value.as_enum(); + return true; + } + case 4: { + this->total_seconds = value.as_uint32(); + return true; + } + case 5: { + this->seconds_left = value.as_uint32(); + return true; + } + case 6: { + this->is_active = value.as_bool(); + return true; + } + default: + return false; + } +} +bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->timer_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + default: + return false; + } +} +void VoiceAssistantTimerEventResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_enum(1, this->event_type); + buffer.encode_string(2, this->timer_id); + buffer.encode_string(3, this->name); + buffer.encode_uint32(4, this->total_seconds); + buffer.encode_uint32(5, this->seconds_left); + buffer.encode_bool(6, this->is_active); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantTimerEventResponse {\n"); + out.append(" event_type: "); + out.append(proto_enum_to_string(this->event_type)); + out.append("\n"); + + out.append(" timer_id: "); + out.append("'").append(this->timer_id).append("'"); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" total_seconds: "); + sprintf(buffer, "%" PRIu32, this->total_seconds); + out.append(buffer); + out.append("\n"); + + out.append(" seconds_left: "); + sprintf(buffer, "%" PRIu32, this->seconds_left); + out.append(buffer); + out.append("\n"); + + out.append(" is_active: "); + out.append(YESNO(this->is_active)); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->media_id = value.as_string(); + return true; + } + case 2: { + this->text = value.as_string(); + return true; + } + default: + return false; + } +} +void VoiceAssistantAnnounceRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->media_id); + buffer.encode_string(2, this->text); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAnnounceRequest {\n"); + out.append(" media_id: "); + out.append("'").append(this->media_id).append("'"); + out.append("\n"); + + out.append(" text: "); + out.append("'").append(this->text).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantAnnounceFinished::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->success = value.as_bool(); + return true; + } + default: + return false; + } +} +void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAnnounceFinished {\n"); + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->id = value.as_string(); + return true; + } + case 2: { + this->wake_word = value.as_string(); + return true; + } + case 3: { + this->trained_languages.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { + 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); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantWakeWord::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantWakeWord {\n"); + out.append(" id: "); + out.append("'").append(this->id).append("'"); + out.append("\n"); + + out.append(" wake_word: "); + out.append("'").append(this->wake_word).append("'"); + out.append("\n"); + + for (const auto &it : this->trained_languages) { + out.append(" trained_languages: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +#endif +void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { + out.append("VoiceAssistantConfigurationRequest {}"); +} +#endif +bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->max_active_wake_words = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool VoiceAssistantConfigurationResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->available_wake_words.push_back(value.as_message()); + return true; + } + case 2: { + this->active_wake_words.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->available_wake_words) { + buffer.encode_message(1, it, true); + } + for (auto &it : this->active_wake_words) { + buffer.encode_string(2, it, true); + } + buffer.encode_uint32(3, this->max_active_wake_words); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantConfigurationResponse {\n"); + for (const auto &it : this->available_wake_words) { + out.append(" available_wake_words: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->active_wake_words) { + out.append(" active_wake_words: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" max_active_wake_words: "); + sprintf(buffer, "%" PRIu32, this->max_active_wake_words); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->active_wake_words.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->active_wake_words) { + buffer.encode_string(1, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantSetConfiguration {\n"); + for (const auto &it : this->active_wake_words) { + out.append(" active_wake_words: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +#endif +bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + case 8: { + this->supported_features = value.as_uint32(); + return true; + } + case 9: { + this->requires_code = value.as_bool(); + return true; + } + case 10: { + this->requires_code_to_arm = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesAlarmControlPanelResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesAlarmControlPanelResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->supported_features); + buffer.encode_bool(9, this->requires_code); + buffer.encode_bool(10, this->requires_code_to_arm); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesAlarmControlPanelResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" supported_features: "); + sprintf(buffer, "%" PRIu32, this->supported_features); + out.append(buffer); + out.append("\n"); + + out.append(" requires_code: "); + out.append(YESNO(this->requires_code)); + out.append("\n"); + + out.append(" requires_code_to_arm: "); + out.append(YESNO(this->requires_code_to_arm)); + out.append("\n"); + out.append("}"); +} +#endif +bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_enum(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void AlarmControlPanelStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AlarmControlPanelStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + out.append("}"); +} +#endif +bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->command = value.as_enum(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: { + this->code = value.as_string(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->command); + buffer.encode_string(3, this->code); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AlarmControlPanelCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" code: "); + out.append("'").append(this->code).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + case 8: { + this->min_length = value.as_uint32(); + return true; + } + case 9: { + this->max_length = value.as_uint32(); + return true; + } + case 11: { + this->mode = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesTextResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 10: { + this->pattern = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesTextResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->min_length); + buffer.encode_uint32(9, this->max_length); + buffer.encode_string(10, this->pattern); + buffer.encode_enum(11, this->mode); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesTextResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesTextResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" min_length: "); + sprintf(buffer, "%" PRIu32, this->min_length); + out.append(buffer); + out.append("\n"); + + out.append(" max_length: "); + sprintf(buffer, "%" PRIu32, this->max_length); + out.append(buffer); + out.append("\n"); + + out.append(" pattern: "); + out.append("'").append(this->pattern).append("'"); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string(this->mode)); + out.append("\n"); + out.append("}"); +} +#endif +bool TextStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->missing_state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool TextStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool TextStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void TextStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->state); + buffer.encode_bool(3, this->missing_state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void TextStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TextStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + out.append("}"); +} +#endif +bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void TextCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void TextCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TextCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesDateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesDateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesDateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesDateResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + out.append("}"); +} +#endif +bool DateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->missing_state = value.as_bool(); + return true; + } + case 3: { + this->year = value.as_uint32(); + return true; + } + case 4: { + this->month = value.as_uint32(); + return true; + } + case 5: { + this->day = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool DateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void DateStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->missing_state); + buffer.encode_uint32(3, this->year); + buffer.encode_uint32(4, this->month); + buffer.encode_uint32(5, this->day); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DateStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" year: "); + sprintf(buffer, "%" PRIu32, this->year); + out.append(buffer); + out.append("\n"); + + out.append(" month: "); + sprintf(buffer, "%" PRIu32, this->month); + out.append(buffer); + out.append("\n"); + + out.append(" day: "); + sprintf(buffer, "%" PRIu32, this->day); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->year = value.as_uint32(); + return true; + } + case 3: { + this->month = value.as_uint32(); + return true; + } + case 4: { + this->day = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void DateCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_uint32(2, this->year); + buffer.encode_uint32(3, this->month); + buffer.encode_uint32(4, this->day); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DateCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" year: "); + sprintf(buffer, "%" PRIu32, this->year); + out.append(buffer); + out.append("\n"); + + out.append(" month: "); + sprintf(buffer, "%" PRIu32, this->month); + out.append(buffer); + out.append("\n"); + + out.append(" day: "); + sprintf(buffer, "%" PRIu32, this->day); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesTimeResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesTimeResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + out.append("}"); +} +#endif +bool TimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->missing_state = value.as_bool(); + return true; + } + case 3: { + this->hour = value.as_uint32(); + return true; + } + case 4: { + this->minute = value.as_uint32(); + return true; + } + case 5: { + this->second = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool TimeStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->missing_state); + buffer.encode_uint32(3, this->hour); + buffer.encode_uint32(4, this->minute); + buffer.encode_uint32(5, this->second); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void TimeStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TimeStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" hour: "); + sprintf(buffer, "%" PRIu32, this->hour); + out.append(buffer); + out.append("\n"); + + out.append(" minute: "); + sprintf(buffer, "%" PRIu32, this->minute); + out.append(buffer); + out.append("\n"); + + out.append(" second: "); + sprintf(buffer, "%" PRIu32, this->second); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->hour = value.as_uint32(); + return true; + } + case 3: { + this->minute = value.as_uint32(); + return true; + } + case 4: { + this->second = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void TimeCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_uint32(2, this->hour); + buffer.encode_uint32(3, this->minute); + buffer.encode_uint32(4, this->second); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void TimeCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TimeCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" hour: "); + sprintf(buffer, "%" PRIu32, this->hour); + out.append(buffer); + out.append("\n"); + + out.append(" minute: "); + sprintf(buffer, "%" PRIu32, this->minute); + out.append(buffer); + out.append("\n"); + + out.append(" second: "); + sprintf(buffer, "%" PRIu32, this->second); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 8: { + this->device_class = value.as_string(); + return true; + } + case 9: { + this->event_types.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +bool ListEntitiesEventResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_string(8, this->device_class); + for (auto &it : this->event_types) { + buffer.encode_string(9, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesEventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesEventResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + for (const auto &it : this->event_types) { + out.append(" event_types: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +#endif +bool EventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->event_type = value.as_string(); + return true; + } + default: + return false; + } +} +bool EventResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void EventResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->event_type); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void EventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("EventResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" event_type: "); + out.append("'").append(this->event_type).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + case 9: { + this->assumed_state = value.as_bool(); + return true; + } + case 10: { + this->supports_position = value.as_bool(); + return true; + } + case 11: { + this->supports_stop = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesValveResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 8: { + this->device_class = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesValveResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + 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); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesValveResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesValveResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" assumed_state: "); + out.append(YESNO(this->assumed_state)); + out.append("\n"); + + out.append(" supports_position: "); + out.append(YESNO(this->supports_position)); + out.append("\n"); + + out.append(" supports_stop: "); + out.append(YESNO(this->supports_stop)); + out.append("\n"); + out.append("}"); +} +#endif +bool ValveStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->current_operation = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ValveStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 2: { + this->position = value.as_float(); + return true; + } + default: + return false; + } +} +void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_float(2, this->position); + buffer.encode_enum(3, this->current_operation); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ValveStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ValveStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" position: "); + sprintf(buffer, "%g", this->position); + out.append(buffer); + out.append("\n"); + + out.append(" current_operation: "); + out.append(proto_enum_to_string(this->current_operation)); + out.append("\n"); + out.append("}"); +} +#endif +bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->has_position = value.as_bool(); + return true; + } + case 4: { + this->stop = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 3: { + this->position = value.as_float(); + return true; + } + default: + return false; + } +} +void ValveCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->has_position); + buffer.encode_float(3, this->position); + buffer.encode_bool(4, this->stop); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ValveCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ValveCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_position: "); + out.append(YESNO(this->has_position)); + out.append("\n"); + + out.append(" position: "); + sprintf(buffer, "%g", this->position); + out.append(buffer); + out.append("\n"); + + out.append(" stop: "); + out.append(YESNO(this->stop)); + out.append("\n"); + out.append("}"); +} +#endif +bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesDateTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesDateTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesDateTimeResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + out.append("}"); +} +#endif +bool DateTimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->missing_state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool DateTimeStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 3: { + this->epoch_seconds = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->missing_state); + buffer.encode_fixed32(3, this->epoch_seconds); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DateTimeStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateTimeStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" epoch_seconds: "); + sprintf(buffer, "%" PRIu32, this->epoch_seconds); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 2: { + this->epoch_seconds = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_fixed32(2, this->epoch_seconds); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DateTimeCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateTimeCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" epoch_seconds: "); + sprintf(buffer, "%" PRIu32, this->epoch_seconds); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + default: + return false; + } +} +bool ListEntitiesUpdateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 8: { + this->device_class = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesUpdateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_string(8, this->device_class); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesUpdateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesUpdateResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->missing_state = value.as_bool(); + return true; + } + case 3: { + this->in_progress = value.as_bool(); + return true; + } + case 4: { + this->has_progress = value.as_bool(); + return true; + } + default: + return false; + } +} +bool UpdateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 6: { + this->current_version = value.as_string(); + return true; + } + case 7: { + this->latest_version = value.as_string(); + return true; + } + case 8: { + this->title = value.as_string(); + return true; + } + case 9: { + this->release_summary = value.as_string(); + return true; + } + case 10: { + this->release_url = value.as_string(); + return true; + } + default: + return false; + } +} +bool UpdateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 5: { + this->progress = value.as_float(); + return true; + } + default: + return false; + } +} +void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->missing_state); + 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); + 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 HAS_PROTO_MESSAGE_DUMP +void UpdateStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("UpdateStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" in_progress: "); + out.append(YESNO(this->in_progress)); + out.append("\n"); + + out.append(" has_progress: "); + out.append(YESNO(this->has_progress)); + out.append("\n"); + + out.append(" progress: "); + sprintf(buffer, "%g", this->progress); + out.append(buffer); + out.append("\n"); + + out.append(" current_version: "); + out.append("'").append(this->current_version).append("'"); + out.append("\n"); + + out.append(" latest_version: "); + out.append("'").append(this->latest_version).append("'"); + out.append("\n"); + + out.append(" title: "); + out.append("'").append(this->title).append("'"); + out.append("\n"); + + out.append(" release_summary: "); + out.append("'").append(this->release_summary).append("'"); + out.append("\n"); + + out.append(" release_url: "); + out.append("'").append(this->release_url).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->command = value.as_enum(); + return true; + } + default: + return false; + } +} +bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->command); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void UpdateCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("UpdateCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 0f4b79de19..063c217bf7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -156,6 +156,10 @@ enum MediaPlayerCommand : uint32_t { MEDIA_PLAYER_COMMAND_MUTE = 3, MEDIA_PLAYER_COMMAND_UNMUTE = 4, }; +enum MediaPlayerFormatPurpose : uint32_t { + MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0, + MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1, +}; enum BluetoothDeviceRequestType : uint32_t { BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0, BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1, @@ -165,6 +169,15 @@ enum BluetoothDeviceRequestType : uint32_t { BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE = 5, BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE = 6, }; +enum VoiceAssistantSubscribeFlag : uint32_t { + VOICE_ASSISTANT_SUBSCRIBE_NONE = 0, + VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1, +}; +enum VoiceAssistantRequestFlag : uint32_t { + VOICE_ASSISTANT_REQUEST_NONE = 0, + VOICE_ASSISTANT_REQUEST_USE_VAD = 1, + VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD = 2, +}; enum VoiceAssistantEvent : uint32_t { VOICE_ASSISTANT_ERROR = 0, VOICE_ASSISTANT_RUN_START = 1, @@ -175,6 +188,53 @@ enum VoiceAssistantEvent : uint32_t { VOICE_ASSISTANT_INTENT_END = 6, VOICE_ASSISTANT_TTS_START = 7, VOICE_ASSISTANT_TTS_END = 8, + VOICE_ASSISTANT_WAKE_WORD_START = 9, + VOICE_ASSISTANT_WAKE_WORD_END = 10, + VOICE_ASSISTANT_STT_VAD_START = 11, + VOICE_ASSISTANT_STT_VAD_END = 12, + VOICE_ASSISTANT_TTS_STREAM_START = 98, + VOICE_ASSISTANT_TTS_STREAM_END = 99, +}; +enum VoiceAssistantTimerEvent : uint32_t { + VOICE_ASSISTANT_TIMER_STARTED = 0, + VOICE_ASSISTANT_TIMER_UPDATED = 1, + VOICE_ASSISTANT_TIMER_CANCELLED = 2, + VOICE_ASSISTANT_TIMER_FINISHED = 3, +}; +enum AlarmControlPanelState : uint32_t { + ALARM_STATE_DISARMED = 0, + ALARM_STATE_ARMED_HOME = 1, + ALARM_STATE_ARMED_AWAY = 2, + ALARM_STATE_ARMED_NIGHT = 3, + ALARM_STATE_ARMED_VACATION = 4, + ALARM_STATE_ARMED_CUSTOM_BYPASS = 5, + ALARM_STATE_PENDING = 6, + ALARM_STATE_ARMING = 7, + ALARM_STATE_DISARMING = 8, + ALARM_STATE_TRIGGERED = 9, +}; +enum AlarmControlPanelStateCommand : uint32_t { + ALARM_CONTROL_PANEL_DISARM = 0, + ALARM_CONTROL_PANEL_ARM_AWAY = 1, + ALARM_CONTROL_PANEL_ARM_HOME = 2, + ALARM_CONTROL_PANEL_ARM_NIGHT = 3, + ALARM_CONTROL_PANEL_ARM_VACATION = 4, + ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5, + ALARM_CONTROL_PANEL_TRIGGER = 6, +}; +enum TextMode : uint32_t { + TEXT_MODE_TEXT = 0, + TEXT_MODE_PASSWORD = 1, +}; +enum ValveOperation : uint32_t { + VALVE_OPERATION_IDLE = 0, + VALVE_OPERATION_IS_OPENING = 1, + VALVE_OPERATION_IS_CLOSING = 2, +}; +enum UpdateCommand : uint32_t { + UPDATE_COMMAND_NONE = 0, + UPDATE_COMMAND_UPDATE = 1, + UPDATE_COMMAND_CHECK = 2, }; } // namespace enums @@ -287,10 +347,13 @@ class DeviceInfoResponse : public ProtoMessage { std::string project_name{}; std::string project_version{}; uint32_t webserver_port{0}; - uint32_t bluetooth_proxy_version{0}; + uint32_t legacy_bluetooth_proxy_version{0}; + uint32_t bluetooth_proxy_feature_flags{0}; std::string manufacturer{}; std::string friendly_name{}; - uint32_t voice_assistant_version{0}; + uint32_t legacy_voice_assistant_version{0}; + uint32_t voice_assistant_feature_flags{0}; + std::string suggested_area{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -434,6 +497,7 @@ class ListEntitiesFanResponse : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + std::vector supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -452,6 +516,7 @@ class FanStateResponse : public ProtoMessage { enums::FanSpeed speed{}; enums::FanDirection direction{}; int32_t speed_level{0}; + std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -459,6 +524,7 @@ class FanStateResponse : public ProtoMessage { 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; }; class FanCommandRequest : public ProtoMessage { @@ -474,6 +540,8 @@ class FanCommandRequest : public ProtoMessage { enums::FanDirection direction{}; bool has_speed_level{false}; int32_t speed_level{0}; + bool has_preset_mode{false}; + std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -481,6 +549,7 @@ class FanCommandRequest : public ProtoMessage { 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; }; class ListEntitiesLightResponse : public ProtoMessage { @@ -669,6 +738,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { std::string icon{}; bool disabled_by_default{false}; enums::EntityCategory entity_category{}; + std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -770,6 +840,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { public: std::string entity_id{}; std::string attribute{}; + bool once{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -777,6 +848,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class HomeAssistantStateResponse : public ProtoMessage { public: @@ -941,6 +1013,10 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::string icon{}; enums::EntityCategory entity_category{}; float visual_current_temperature_step{0.0f}; + bool supports_current_humidity{false}; + bool supports_target_humidity{false}; + float visual_min_humidity{0.0f}; + float visual_max_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -966,6 +1042,8 @@ class ClimateStateResponse : public ProtoMessage { std::string custom_fan_mode{}; enums::ClimatePreset preset{}; std::string custom_preset{}; + float current_humidity{0.0f}; + float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -999,6 +1077,8 @@ class ClimateCommandRequest : public ProtoMessage { enums::ClimatePreset preset{}; bool has_custom_preset{false}; std::string custom_preset{}; + bool has_target_humidity{false}; + float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1191,6 +1271,22 @@ class ButtonCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +class MediaPlayerSupportedFormat : public ProtoMessage { + public: + std::string format{}; + uint32_t sample_rate{0}; + uint32_t num_channels{0}; + enums::MediaPlayerFormatPurpose purpose{}; + uint32_t sample_bytes{0}; + void encode(ProtoWriteBuffer buffer) const override; +#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; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class ListEntitiesMediaPlayerResponse : public ProtoMessage { public: std::string object_id{}; @@ -1201,6 +1297,7 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage { bool disabled_by_default{false}; enums::EntityCategory entity_category{}; bool supports_pause{false}; + std::vector supported_formats{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1235,6 +1332,8 @@ class MediaPlayerCommandRequest : public ProtoMessage { float volume{0.0f}; bool has_media_url{false}; std::string media_url{}; + bool has_announcement{false}; + bool announcement{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1247,12 +1346,14 @@ class MediaPlayerCommandRequest : public ProtoMessage { }; class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { public: + uint32_t flags{0}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothServiceData : public ProtoMessage { public: @@ -1286,6 +1387,32 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class BluetoothLERawAdvertisement : public ProtoMessage { + public: + uint64_t address{0}; + int32_t rssi{0}; + uint32_t address_type{0}; + std::string data{}; + void encode(ProtoWriteBuffer buffer) const override; +#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; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothLERawAdvertisementsResponse : public ProtoMessage { + public: + std::vector advertisements{}; + void encode(ProtoWriteBuffer buffer) const override; +#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 BluetoothDeviceRequest : public ProtoMessage { public: uint64_t address{0}; @@ -1593,6 +1720,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { class SubscribeVoiceAssistantRequest : public ProtoMessage { public: bool subscribe{false}; + uint32_t flags{0}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1601,15 +1729,34 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantRequest : public ProtoMessage { +class VoiceAssistantAudioSettings : public ProtoMessage { public: - bool start{false}; + uint32_t noise_suppression_level{0}; + uint32_t auto_gain{0}; + float volume_multiplier{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #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; +}; +class VoiceAssistantRequest : public ProtoMessage { + public: + bool start{false}; + std::string conversation_id{}; + uint32_t flags{0}; + VoiceAssistantAudioSettings audio_settings{}; + std::string wake_word_phrase{}; + void encode(ProtoWriteBuffer buffer) const override; +#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; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class VoiceAssistantResponse : public ProtoMessage { @@ -1649,6 +1796,493 @@ class VoiceAssistantEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class VoiceAssistantAudio : public ProtoMessage { + public: + std::string data{}; + bool end{false}; + void encode(ProtoWriteBuffer buffer) const override; +#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; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class VoiceAssistantTimerEventResponse : public ProtoMessage { + public: + enums::VoiceAssistantTimerEvent event_type{}; + std::string timer_id{}; + std::string name{}; + uint32_t total_seconds{0}; + uint32_t seconds_left{0}; + bool is_active{false}; + void encode(ProtoWriteBuffer buffer) const override; +#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; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class VoiceAssistantAnnounceRequest : public ProtoMessage { + public: + std::string media_id{}; + std::string text{}; + void encode(ProtoWriteBuffer buffer) const override; +#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 VoiceAssistantAnnounceFinished : public ProtoMessage { + public: + bool success{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class VoiceAssistantWakeWord : public ProtoMessage { + public: + std::string id{}; + std::string wake_word{}; + std::vector trained_languages{}; + void encode(ProtoWriteBuffer buffer) const override; +#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 VoiceAssistantConfigurationRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +class VoiceAssistantConfigurationResponse : public ProtoMessage { + public: + std::vector available_wake_words{}; + std::vector active_wake_words{}; + uint32_t max_active_wake_words{0}; + void encode(ProtoWriteBuffer buffer) const override; +#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; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class VoiceAssistantSetConfiguration : public ProtoMessage { + public: + std::vector active_wake_words{}; + void encode(ProtoWriteBuffer buffer) const override; +#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 ListEntitiesAlarmControlPanelResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + uint32_t supported_features{0}; + bool requires_code{false}; + bool requires_code_to_arm{false}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class AlarmControlPanelStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + enums::AlarmControlPanelState state{}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class AlarmControlPanelCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + enums::AlarmControlPanelStateCommand command{}; + std::string code{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesTextResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + uint32_t min_length{0}; + uint32_t max_length{0}; + std::string pattern{}; + enums::TextMode mode{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class TextStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + std::string state{}; + bool missing_state{false}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class TextCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + std::string state{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class ListEntitiesDateResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DateStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + bool missing_state{false}; + uint32_t year{0}; + uint32_t month{0}; + uint32_t day{0}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class DateCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + uint32_t year{0}; + uint32_t month{0}; + uint32_t day{0}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class ListEntitiesTimeResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class TimeStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + bool missing_state{false}; + uint32_t hour{0}; + uint32_t minute{0}; + uint32_t second{0}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class TimeCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + uint32_t hour{0}; + uint32_t minute{0}; + uint32_t second{0}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class ListEntitiesEventResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + std::string device_class{}; + std::vector event_types{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class EventResponse : public ProtoMessage { + public: + uint32_t key{0}; + std::string event_type{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class ListEntitiesValveResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + std::string device_class{}; + bool assumed_state{false}; + bool supports_position{false}; + bool supports_stop{false}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ValveStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + float position{0.0f}; + enums::ValveOperation current_operation{}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class ValveCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + bool has_position{false}; + float position{0.0f}; + bool stop{false}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class ListEntitiesDateTimeResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DateTimeStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + bool missing_state{false}; + uint32_t epoch_seconds{0}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class DateTimeCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + uint32_t epoch_seconds{0}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; +class ListEntitiesUpdateResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + std::string device_class{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class UpdateStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + bool missing_state{false}; + bool in_progress{false}; + bool has_progress{false}; + float progress{0.0f}; + std::string current_version{}; + std::string latest_version{}; + std::string title{}; + std::string release_summary{}; + std::string release_url{}; + void encode(ProtoWriteBuffer buffer) const override; +#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_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class UpdateCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + enums::UpdateCommand command{}; + void encode(ProtoWriteBuffer buffer) const override; +#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; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index df36d0fdea..6e11d7169d 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -339,6 +339,15 @@ bool APIServerConnectionBase::send_bluetooth_le_advertisement_response(const Blu } #endif #ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_le_raw_advertisements_response( + const BluetoothLERawAdvertisementsResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_le_raw_advertisements_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 93); +} +#endif +#ifdef USE_BLUETOOTH_PROXY #endif #ifdef USE_BLUETOOTH_PROXY bool APIServerConnectionBase::send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg) { @@ -467,6 +476,182 @@ bool APIServerConnectionBase::send_voice_assistant_request(const VoiceAssistantR #endif #ifdef USE_VOICE_ASSISTANT #endif +#ifdef USE_VOICE_ASSISTANT +bool APIServerConnectionBase::send_voice_assistant_audio(const VoiceAssistantAudio &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_voice_assistant_audio: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 106); +} +#endif +#ifdef USE_VOICE_ASSISTANT +#endif +#ifdef USE_VOICE_ASSISTANT +#endif +#ifdef USE_VOICE_ASSISTANT +bool APIServerConnectionBase::send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_voice_assistant_announce_finished: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 120); +} +#endif +#ifdef USE_VOICE_ASSISTANT +#endif +#ifdef USE_VOICE_ASSISTANT +bool APIServerConnectionBase::send_voice_assistant_configuration_response( + const VoiceAssistantConfigurationResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_voice_assistant_configuration_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 122); +} +#endif +#ifdef USE_VOICE_ASSISTANT +#endif +#ifdef USE_ALARM_CONTROL_PANEL +bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response( + const ListEntitiesAlarmControlPanelResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_alarm_control_panel_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 94); +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +bool APIServerConnectionBase::send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_alarm_control_panel_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 95); +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +#endif +#ifdef USE_TEXT +bool APIServerConnectionBase::send_list_entities_text_response(const ListEntitiesTextResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_text_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 97); +} +#endif +#ifdef USE_TEXT +bool APIServerConnectionBase::send_text_state_response(const TextStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_text_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 98); +} +#endif +#ifdef USE_TEXT +#endif +#ifdef USE_DATETIME_DATE +bool APIServerConnectionBase::send_list_entities_date_response(const ListEntitiesDateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_date_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 100); +} +#endif +#ifdef USE_DATETIME_DATE +bool APIServerConnectionBase::send_date_state_response(const DateStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_date_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 101); +} +#endif +#ifdef USE_DATETIME_DATE +#endif +#ifdef USE_DATETIME_TIME +bool APIServerConnectionBase::send_list_entities_time_response(const ListEntitiesTimeResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_time_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 103); +} +#endif +#ifdef USE_DATETIME_TIME +bool APIServerConnectionBase::send_time_state_response(const TimeStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_time_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 104); +} +#endif +#ifdef USE_DATETIME_TIME +#endif +#ifdef USE_EVENT +bool APIServerConnectionBase::send_list_entities_event_response(const ListEntitiesEventResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_event_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 107); +} +#endif +#ifdef USE_EVENT +bool APIServerConnectionBase::send_event_response(const EventResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_event_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 108); +} +#endif +#ifdef USE_VALVE +bool APIServerConnectionBase::send_list_entities_valve_response(const ListEntitiesValveResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_valve_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 109); +} +#endif +#ifdef USE_VALVE +bool APIServerConnectionBase::send_valve_state_response(const ValveStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_valve_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 110); +} +#endif +#ifdef USE_VALVE +#endif +#ifdef USE_DATETIME_DATETIME +bool APIServerConnectionBase::send_list_entities_date_time_response(const ListEntitiesDateTimeResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_date_time_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 112); +} +#endif +#ifdef USE_DATETIME_DATETIME +bool APIServerConnectionBase::send_date_time_state_response(const DateTimeStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_date_time_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 113); +} +#endif +#ifdef USE_DATETIME_DATETIME +#endif +#ifdef USE_UPDATE +bool APIServerConnectionBase::send_list_entities_update_response(const ListEntitiesUpdateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_update_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 116); +} +#endif +#ifdef USE_UPDATE +bool APIServerConnectionBase::send_update_state_response(const UpdateStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_update_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 117); +} +#endif +#ifdef USE_UPDATE +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -874,6 +1059,138 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_event_response(msg); +#endif + break; + } + case 96: { +#ifdef USE_ALARM_CONTROL_PANEL + AlarmControlPanelCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str()); +#endif + this->on_alarm_control_panel_command_request(msg); +#endif + break; + } + case 99: { +#ifdef USE_TEXT + TextCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str()); +#endif + this->on_text_command_request(msg); +#endif + break; + } + case 102: { +#ifdef USE_DATETIME_DATE + DateCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str()); +#endif + this->on_date_command_request(msg); +#endif + break; + } + case 105: { +#ifdef USE_DATETIME_TIME + TimeCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_time_command_request: %s", msg.dump().c_str()); +#endif + this->on_time_command_request(msg); +#endif + break; + } + case 106: { +#ifdef USE_VOICE_ASSISTANT + VoiceAssistantAudio msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_voice_assistant_audio: %s", msg.dump().c_str()); +#endif + this->on_voice_assistant_audio(msg); +#endif + break; + } + case 111: { +#ifdef USE_VALVE + ValveCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str()); +#endif + this->on_valve_command_request(msg); +#endif + break; + } + case 114: { +#ifdef USE_DATETIME_DATETIME + DateTimeCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str()); +#endif + this->on_date_time_command_request(msg); +#endif + break; + } + case 115: { +#ifdef USE_VOICE_ASSISTANT + VoiceAssistantTimerEventResponse msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str()); +#endif + this->on_voice_assistant_timer_event_response(msg); +#endif + break; + } + case 118: { +#ifdef USE_UPDATE + UpdateCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); +#endif + this->on_update_command_request(msg); +#endif + break; + } + case 119: { +#ifdef USE_VOICE_ASSISTANT + VoiceAssistantAnnounceRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str()); +#endif + this->on_voice_assistant_announce_request(msg); +#endif + break; + } + case 121: { +#ifdef USE_VOICE_ASSISTANT + VoiceAssistantConfigurationRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); +#endif + this->on_voice_assistant_configuration_request(msg); +#endif + break; + } + case 123: { +#ifdef USE_VOICE_ASSISTANT + VoiceAssistantSetConfiguration msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); +#endif + this->on_voice_assistant_set_configuration(msg); #endif break; } @@ -1085,6 +1402,19 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest & this->number_command(msg); } #endif +#ifdef USE_TEXT +void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->text_command(msg); +} +#endif #ifdef USE_SELECT void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { if (!this->is_connection_setup()) { @@ -1124,6 +1454,19 @@ void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) this->lock_command(msg); } #endif +#ifdef USE_VALVE +void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->valve_command(msg); +} +#endif #ifdef USE_MEDIA_PLAYER void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { if (!this->is_connection_setup()) { @@ -1137,6 +1480,58 @@ void APIServerConnection::on_media_player_command_request(const MediaPlayerComma this->media_player_command(msg); } #endif +#ifdef USE_DATETIME_DATE +void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->date_command(msg); +} +#endif +#ifdef USE_DATETIME_TIME +void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->time_command(msg); +} +#endif +#ifdef USE_DATETIME_DATETIME +void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->datetime_command(msg); +} +#endif +#ifdef USE_UPDATE +void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->update_command(msg); +} +#endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { @@ -1286,6 +1681,48 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo this->subscribe_voice_assistant(msg); } #endif +#ifdef USE_VOICE_ASSISTANT +void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); + if (!this->send_voice_assistant_configuration_response(ret)) { + this->on_fatal_error(); + } +} +#endif +#ifdef USE_VOICE_ASSISTANT +void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->voice_assistant_set_configuration(msg); +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->alarm_control_panel_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 3808f128a4..51b94bf530 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -161,6 +161,9 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_BLUETOOTH_PROXY bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg); #endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_le_raw_advertisements_response(const BluetoothLERawAdvertisementsResponse &msg); +#endif #ifdef USE_BLUETOOTH_PROXY virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; #endif @@ -236,6 +239,97 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_VOICE_ASSISTANT virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; +#endif +#ifdef USE_VOICE_ASSISTANT + bool send_voice_assistant_audio(const VoiceAssistantAudio &msg); + virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; +#endif +#ifdef USE_VOICE_ASSISTANT + virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; +#endif +#ifdef USE_VOICE_ASSISTANT + virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; +#endif +#ifdef USE_VOICE_ASSISTANT + bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg); +#endif +#ifdef USE_VOICE_ASSISTANT + virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; +#endif +#ifdef USE_VOICE_ASSISTANT + bool send_voice_assistant_configuration_response(const VoiceAssistantConfigurationResponse &msg); +#endif +#ifdef USE_VOICE_ASSISTANT + virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; +#endif +#ifdef USE_TEXT + bool send_list_entities_text_response(const ListEntitiesTextResponse &msg); +#endif +#ifdef USE_TEXT + bool send_text_state_response(const TextStateResponse &msg); +#endif +#ifdef USE_TEXT + virtual void on_text_command_request(const TextCommandRequest &value){}; +#endif +#ifdef USE_DATETIME_DATE + bool send_list_entities_date_response(const ListEntitiesDateResponse &msg); +#endif +#ifdef USE_DATETIME_DATE + bool send_date_state_response(const DateStateResponse &msg); +#endif +#ifdef USE_DATETIME_DATE + virtual void on_date_command_request(const DateCommandRequest &value){}; +#endif +#ifdef USE_DATETIME_TIME + bool send_list_entities_time_response(const ListEntitiesTimeResponse &msg); +#endif +#ifdef USE_DATETIME_TIME + bool send_time_state_response(const TimeStateResponse &msg); +#endif +#ifdef USE_DATETIME_TIME + virtual void on_time_command_request(const TimeCommandRequest &value){}; +#endif +#ifdef USE_EVENT + bool send_list_entities_event_response(const ListEntitiesEventResponse &msg); +#endif +#ifdef USE_EVENT + bool send_event_response(const EventResponse &msg); +#endif +#ifdef USE_VALVE + bool send_list_entities_valve_response(const ListEntitiesValveResponse &msg); +#endif +#ifdef USE_VALVE + bool send_valve_state_response(const ValveStateResponse &msg); +#endif +#ifdef USE_VALVE + virtual void on_valve_command_request(const ValveCommandRequest &value){}; +#endif +#ifdef USE_DATETIME_DATETIME + bool send_list_entities_date_time_response(const ListEntitiesDateTimeResponse &msg); +#endif +#ifdef USE_DATETIME_DATETIME + bool send_date_time_state_response(const DateTimeStateResponse &msg); +#endif +#ifdef USE_DATETIME_DATETIME + virtual void on_date_time_command_request(const DateTimeCommandRequest &value){}; +#endif +#ifdef USE_UPDATE + bool send_list_entities_update_response(const ListEntitiesUpdateResponse &msg); +#endif +#ifdef USE_UPDATE + bool send_update_state_response(const UpdateStateResponse &msg); +#endif +#ifdef USE_UPDATE + virtual void on_update_command_request(const UpdateCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -276,6 +370,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_NUMBER virtual void number_command(const NumberCommandRequest &msg) = 0; #endif +#ifdef USE_TEXT + virtual void text_command(const TextCommandRequest &msg) = 0; +#endif #ifdef USE_SELECT virtual void select_command(const SelectCommandRequest &msg) = 0; #endif @@ -285,9 +382,24 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_LOCK virtual void lock_command(const LockCommandRequest &msg) = 0; #endif +#ifdef USE_VALVE + virtual void valve_command(const ValveCommandRequest &msg) = 0; +#endif #ifdef USE_MEDIA_PLAYER virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; #endif +#ifdef USE_DATETIME_DATE + virtual void date_command(const DateCommandRequest &msg) = 0; +#endif +#ifdef USE_DATETIME_TIME + virtual void time_command(const TimeCommandRequest &msg) = 0; +#endif +#ifdef USE_DATETIME_DATETIME + virtual void datetime_command(const DateTimeCommandRequest &msg) = 0; +#endif +#ifdef USE_UPDATE + virtual void update_command(const UpdateCommandRequest &msg) = 0; +#endif #ifdef USE_BLUETOOTH_PROXY virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; #endif @@ -321,6 +433,16 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_VOICE_ASSISTANT virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; +#endif +#ifdef USE_VOICE_ASSISTANT + virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration( + const VoiceAssistantConfigurationRequest &msg) = 0; +#endif +#ifdef USE_VOICE_ASSISTANT + virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -356,6 +478,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_NUMBER void on_number_command_request(const NumberCommandRequest &msg) override; #endif +#ifdef USE_TEXT + void on_text_command_request(const TextCommandRequest &msg) override; +#endif #ifdef USE_SELECT void on_select_command_request(const SelectCommandRequest &msg) override; #endif @@ -365,9 +490,24 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_LOCK void on_lock_command_request(const LockCommandRequest &msg) override; #endif +#ifdef USE_VALVE + void on_valve_command_request(const ValveCommandRequest &msg) override; +#endif #ifdef USE_MEDIA_PLAYER void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; #endif +#ifdef USE_DATETIME_DATE + void on_date_command_request(const DateCommandRequest &msg) override; +#endif +#ifdef USE_DATETIME_TIME + void on_time_command_request(const TimeCommandRequest &msg) override; +#endif +#ifdef USE_DATETIME_DATETIME + void on_date_time_command_request(const DateTimeCommandRequest &msg) override; +#endif +#ifdef USE_UPDATE + void on_update_command_request(const UpdateCommandRequest &msg) override; +#endif #ifdef USE_BLUETOOTH_PROXY void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; #endif @@ -402,6 +542,15 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_VOICE_ASSISTANT void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; #endif +#ifdef USE_VOICE_ASSISTANT + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override; +#endif +#ifdef USE_VOICE_ASSISTANT + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 068f74315c..f16b5a13cf 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -1,13 +1,14 @@ #include "api_server.h" +#ifdef USE_API +#include #include "api_connection.h" +#include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/util.h" #include "esphome/core/version.h" -#include "esphome/core/hal.h" -#include "esphome/components/network/util.h" -#include #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -111,6 +112,7 @@ void APIServer::loop() { [](const std::unique_ptr &conn) { return !conn->remove_; }); // print disconnection messages for (auto it = new_end; it != this->clients_.end(); ++it) { + this->client_disconnected_trigger_->trigger((*it)->client_info_, (*it)->client_peername_); ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str()); } // resize vector @@ -254,6 +256,42 @@ void APIServer::on_number_update(number::Number *obj, float state) { } #endif +#ifdef USE_DATETIME_DATE +void APIServer::on_date_update(datetime::DateEntity *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_date_state(obj); +} +#endif + +#ifdef USE_DATETIME_TIME +void APIServer::on_time_update(datetime::TimeEntity *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_time_state(obj); +} +#endif + +#ifdef USE_DATETIME_DATETIME +void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_datetime_state(obj); +} +#endif + +#ifdef USE_TEXT +void APIServer::on_text_update(text::Text *obj, const std::string &state) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_text_state(obj, state); +} +#endif + #ifdef USE_SELECT void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { if (obj->is_internal()) @@ -272,6 +310,15 @@ void APIServer::on_lock_update(lock::Lock *obj) { } #endif +#ifdef USE_VALVE +void APIServer::on_valve_update(valve::Valve *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_valve_state(obj); +} +#endif + #ifdef USE_MEDIA_PLAYER void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { if (obj->is_internal()) @@ -281,6 +328,20 @@ void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { } #endif +#ifdef USE_EVENT +void APIServer::on_event(event::Event *obj, const std::string &event_type) { + for (auto &c : this->clients_) + c->send_event(obj, event_type); +} +#endif + +#ifdef USE_UPDATE +void APIServer::on_update(update::UpdateEntity *obj) { + for (auto &c : this->clients_) + c->send_update_state(obj); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -291,112 +352,7 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon client->send_homeassistant_service_call(call); } } -#ifdef USE_BLUETOOTH_PROXY -void APIServer::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_le_advertisement(call); - } -} -void APIServer::send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { - BluetoothDeviceConnectionResponse call; - call.address = address; - call.connected = connected; - call.mtu = mtu; - call.error = error; - for (auto &client : this->clients_) { - client->send_bluetooth_device_connection_response(call); - } -} - -void APIServer::send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error) { - BluetoothDevicePairingResponse call; - call.address = address; - call.paired = paired; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_pairing_response(call); - } -} - -void APIServer::send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error) { - BluetoothDeviceUnpairingResponse call; - call.address = address; - call.success = success; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_unpairing_response(call); - } -} - -void APIServer::send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error) { - BluetoothDeviceClearCacheResponse call; - call.address = address; - call.success = success; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_clear_cache_response(call); - } -} - -void APIServer::send_bluetooth_connections_free(uint8_t free, uint8_t limit) { - BluetoothConnectionsFreeResponse call; - call.free = free; - call.limit = limit; - - for (auto &client : this->clients_) { - client->send_bluetooth_connections_free_response(call); - } -} - -void APIServer::send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_read_response(call); - } -} -void APIServer::send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_write_response(call); - } -} -void APIServer::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_notify_data_response(call); - } -} -void APIServer::send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_notify_response(call); - } -} -void APIServer::send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_get_services_response(call); - } -} -void APIServer::send_bluetooth_gatt_services_done(uint64_t address) { - BluetoothGATTGetServicesDoneResponse call; - call.address = address; - - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_get_services_done_response(call); - } -} -void APIServer::send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { - BluetoothGATTErrorResponse call; - call.address = address; - call.handle = handle; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_error_response(call); - } -} - -#endif APIServer::APIServer() { global_api_server = this; } void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f) { @@ -404,8 +360,18 @@ void APIServer::subscribe_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, + }); +}; const std::vector &APIServer::get_state_subs() const { return this->state_subs_; } @@ -414,7 +380,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->remove_ && client->connection_state_ == APIConnection::ConnectionState::CONNECTED) + if (!client->remove_ && client->is_authenticated()) client->send_time_request(); } } @@ -427,21 +393,15 @@ void APIServer::on_shutdown() { delay(10); } -#ifdef USE_VOICE_ASSISTANT -bool APIServer::start_voice_assistant() { - for (auto &c : this->clients_) { - if (c->request_voice_assistant(true)) - return true; - } - return false; -} -void APIServer::stop_voice_assistant() { - for (auto &c : this->clients_) { - if (c->request_voice_assistant(false)) - return; - } +#ifdef USE_ALARM_CONTROL_PANEL +void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_alarm_control_panel_state(obj); } #endif } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index a1bec2802f..42e0b1048a 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -1,16 +1,18 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/controller.h" #include "esphome/core/defines.h" -#include "esphome/core/log.h" -#include "esphome/components/socket/socket.h" +#ifdef USE_API +#include "api_noise_context.h" #include "api_pb2.h" #include "api_pb2_service.h" +#include "esphome/components/socket/socket.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/controller.h" +#include "esphome/core/log.h" #include "list_entities.h" #include "subscribe_state.h" #include "user_services.h" -#include "api_noise_context.h" #include @@ -65,39 +67,44 @@ class APIServer : public Component, public Controller { #ifdef USE_NUMBER void on_number_update(number::Number *obj, float state) override; #endif +#ifdef USE_DATETIME_DATE + void on_date_update(datetime::DateEntity *obj) override; +#endif +#ifdef USE_DATETIME_TIME + void on_time_update(datetime::TimeEntity *obj) override; +#endif +#ifdef USE_DATETIME_DATETIME + void on_datetime_update(datetime::DateTimeEntity *obj) override; +#endif +#ifdef USE_TEXT + void on_text_update(text::Text *obj, const std::string &state) override; +#endif #ifdef USE_SELECT void on_select_update(select::Select *obj, const std::string &state, size_t index) override; #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; #endif +#ifdef USE_VALVE + void on_valve_update(valve::Valve *obj) override; +#endif #ifdef USE_MEDIA_PLAYER void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); -#ifdef USE_BLUETOOTH_PROXY - void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call); - void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); - void send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); - void send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); - void send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); - void send_bluetooth_connections_free(uint8_t free, uint8_t limit); - void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call); - void send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call); - void send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call); - void send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call); - void send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call); - void send_bluetooth_gatt_services_done(uint64_t address); - void send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); -#endif void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif -#ifdef USE_VOICE_ASSISTANT - bool start_voice_assistant(); - void stop_voice_assistant(); +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; +#endif +#ifdef USE_EVENT + void on_event(event::Event *obj, const std::string &event_type) override; +#endif +#ifdef USE_UPDATE + void on_update(update::UpdateEntity *obj) override; #endif bool is_connected() const; @@ -106,13 +113,21 @@ class APIServer : public Component, public Controller { std::string entity_id; optional attribute; std::function callback; + bool once; }; 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; const std::vector &get_user_services() const { return this->user_services_; } + Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger *get_client_disconnected_trigger() const { + return this->client_disconnected_trigger_; + } + protected: std::unique_ptr socket_ = nullptr; uint16_t port_{6053}; @@ -122,6 +137,8 @@ class APIServer : public Component, public Controller { std::string password_; std::vector state_subs_; std::vector user_services_; + Trigger *client_connected_trigger_ = new Trigger(); + Trigger *client_disconnected_trigger_ = new Trigger(); #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); @@ -137,3 +154,4 @@ template class APIConnectedCondition : public Condition { } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index c777c3be9d..dd013c8c34 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -1,23 +1,29 @@ +from __future__ import annotations + import asyncio import logging from datetime import datetime -from typing import Optional +from typing import Any -from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel -import zeroconf +from aioesphomeapi import APIClient +from aioesphomeapi.api_pb2 import SubscribeLogsResponse +from aioesphomeapi.log_runner import async_run + +from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ +from esphome.core import CORE -from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__ -from esphome.util import safe_print from . import CONF_ENCRYPTION _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config, address): +async def async_run_logs(config: dict[str, Any], address: str) -> None: + """Run the logs command in the event loop.""" conf = config["api"] + name = config["esphome"]["name"] port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] - noise_psk: Optional[str] = None + noise_psk: str | None = None if CONF_ENCRYPTION in conf: noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) @@ -28,44 +34,27 @@ async def async_run_logs(config, address): client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, ) - first_connect = True + dashboard = CORE.dashboard - def on_log(msg): - time_ = datetime.now().time().strftime("[%H:%M:%S]") - text = msg.message.decode("utf8", "backslashreplace") - safe_print(time_ + text) - - async def on_connect(): - nonlocal first_connect - try: - await cli.subscribe_logs( - on_log, - log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE, - dump_config=first_connect, - ) - first_connect = False - except APIConnectionError: - cli.disconnect() - - async def on_disconnect(): - _LOGGER.warning("Disconnected from API") - - zc = zeroconf.Zeroconf() - reconnect = ReconnectLogic( - client=cli, - on_connect=on_connect, - on_disconnect=on_disconnect, - zeroconf_instance=zc, - ) - await reconnect.start() + def on_log(msg: SubscribeLogsResponse) -> None: + """Handle a new log message.""" + time_ = datetime.now() + message: bytes = msg.message + text = message.decode("utf8", "backslashreplace") + if dashboard: + text = text.replace("\033", "\\033") + print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") + stop = await async_run(cli, on_log, name=name) try: - while True: - await asyncio.sleep(60) + await asyncio.Event().wait() + finally: + await stop() + + +def run_logs(config: dict[str, Any], address: str) -> None: + """Run the logs command.""" + try: + asyncio.run(async_run_logs(config, address)) except KeyboardInterrupt: - await reconnect.stop() - zc.close() - - -def run_logs(config, address): - asyncio.run(async_run_logs(config, address)) + pass diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 9f125a6149..1a8e189f41 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -1,9 +1,9 @@ #pragma once #include -#include "user_services.h" #include "api_server.h" - +#ifdef USE_API +#include "user_services.h" namespace esphome { namespace api { @@ -105,7 +105,7 @@ class CustomAPIDevice { /** Subscribe to the state (or attribute state) of an entity from Home Assistant. * * Usage: - *å + * * ```cpp * void setup() override { * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); @@ -216,3 +216,4 @@ class CustomAPIDevice { } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index f04181e5b2..e91756c3c9 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -1,10 +1,10 @@ #pragma once +#include "api_server.h" +#ifdef USE_API +#include "api_pb2.h" #include "esphome/core/helpers.h" #include "esphome/core/automation.h" -#include "api_pb2.h" -#include "api_server.h" - #include namespace esphome { @@ -81,3 +81,4 @@ template class HomeAssistantServiceCallAction : public Actionclient_->send_lock_info(a_lock); } #endif +#ifdef USE_VALVE +bool ListEntitiesIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_info(valve); } +#endif bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} @@ -60,6 +64,24 @@ bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this-> bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); } #endif +#ifdef USE_DATETIME_DATE +bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_info(date); } +#endif + +#ifdef USE_DATETIME_TIME +bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); } +#endif + +#ifdef USE_DATETIME_DATETIME +bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { + return this->client_->send_datetime_info(datetime); +} +#endif + +#ifdef USE_TEXT +bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); } +#endif + #ifdef USE_SELECT bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } #endif @@ -69,6 +91,18 @@ bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_play return this->client_->send_media_player_info(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + return this->client_->send_alarm_control_panel_info(a_alarm_control_panel); +} +#endif +#ifdef USE_EVENT +bool ListEntitiesIterator::on_event(event::Event *event) { return this->client_->send_event_info(event); } +#endif +#ifdef USE_UPDATE +bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_info(update); } +#endif } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 4fbaa509a2..1821bbee4c 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -1,9 +1,9 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_API #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -#include "esphome/core/defines.h" - namespace esphome { namespace api { @@ -46,14 +46,38 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_NUMBER bool on_number(number::Number *number) override; #endif +#ifdef USE_DATETIME_DATE + bool on_date(datetime::DateEntity *date) override; +#endif +#ifdef USE_DATETIME_TIME + bool on_time(datetime::TimeEntity *time) override; +#endif +#ifdef USE_DATETIME_DATETIME + bool on_datetime(datetime::DateTimeEntity *datetime) override; +#endif +#ifdef USE_TEXT + bool on_text(text::Text *text) override; +#endif #ifdef USE_SELECT bool on_select(select::Select *select) override; #endif #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; #endif +#ifdef USE_VALVE + bool on_valve(valve::Valve *valve) override; +#endif #ifdef USE_MEDIA_PLAYER bool on_media_player(media_player::MediaPlayer *media_player) override; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; +#endif +#ifdef USE_EVENT + bool on_event(event::Event *event) override; +#endif +#ifdef USE_UPDATE + bool on_update(update::UpdateEntity *update) override; #endif bool on_end() override; @@ -63,3 +87,4 @@ class ListEntitiesIterator : public ComponentIterator { } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index fea219ecb9..ccc6c0d52c 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -160,8 +160,7 @@ class ProtoWriteBuffer { this->encode_field_raw(field_id, 2); this->encode_varint_raw(len); auto *data = reinterpret_cast(string); - for (size_t i = 0; i < len; i++) - this->write(data[i]); + this->buffer_->insert(this->buffer_->end(), data, data + len); } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size()); diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 1d1ba0245e..4b1d5ebc0d 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -1,4 +1,5 @@ #include "subscribe_state.h" +#ifdef USE_API #include "api_connection.h" #include "esphome/core/log.h" @@ -42,6 +43,20 @@ bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number, number->state); } #endif +#ifdef USE_DATETIME_DATE +bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); } +#endif +#ifdef USE_DATETIME_TIME +bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); } +#endif +#ifdef USE_DATETIME_DATETIME +bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) { + return this->client_->send_datetime_state(datetime); +} +#endif +#ifdef USE_TEXT +bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text, text->state); } +#endif #ifdef USE_SELECT bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select, select->state); @@ -50,12 +65,24 @@ bool InitialStateIterator::on_select(select::Select *select) { #ifdef USE_LOCK bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } #endif +#ifdef USE_VALVE +bool InitialStateIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_state(valve); } +#endif #ifdef USE_MEDIA_PLAYER bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { return this->client_->send_media_player_state(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); +} +#endif +#ifdef USE_UPDATE +bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); } +#endif InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 7a7ba697c0..efda32affb 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -1,10 +1,10 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_API #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" #include "esphome/core/controller.h" -#include "esphome/core/defines.h" - namespace esphome { namespace api { @@ -43,14 +43,38 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_NUMBER bool on_number(number::Number *number) override; #endif +#ifdef USE_DATETIME_DATE + bool on_date(datetime::DateEntity *date) override; +#endif +#ifdef USE_DATETIME_TIME + bool on_time(datetime::TimeEntity *time) override; +#endif +#ifdef USE_DATETIME_DATETIME + bool on_datetime(datetime::DateTimeEntity *datetime) override; +#endif +#ifdef USE_TEXT + bool on_text(text::Text *text) override; +#endif #ifdef USE_SELECT bool on_select(select::Select *select) override; #endif #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; #endif +#ifdef USE_VALVE + bool on_valve(valve::Valve *valve) override; +#endif #ifdef USE_MEDIA_PLAYER bool on_media_player(media_player::MediaPlayer *media_player) override; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; +#endif +#ifdef USE_EVENT + bool on_event(event::Event *event) override { return true; }; +#endif +#ifdef USE_UPDATE + bool on_update(update::UpdateEntity *update) override; #endif protected: APIConnection *client_; @@ -58,3 +82,4 @@ class InitialStateIterator : public ComponentIterator { } // namespace api } // namespace esphome +#endif diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 49618f5467..7e73722a92 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace api { template<> bool get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.bool_; } -template<> int get_execute_arg_value(const ExecuteServiceArgument &arg) { +template<> int32_t get_execute_arg_value(const ExecuteServiceArgument &arg) { if (arg.legacy_int != 0) return arg.legacy_int; return arg.int_; @@ -26,11 +26,13 @@ template<> std::vector get_execute_arg_value enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_BOOL; } -template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } +template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } -template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_INT_ARRAY; } +template<> enums::ServiceArgType to_service_arg_type>() { + return enums::SERVICE_ARG_TYPE_INT_ARRAY; +} template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; } diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py index cf0580ca62..2ec7c50859 100644 --- a/esphome/components/as3935/__init__.py +++ b/esphome/components/as3935/__init__.py @@ -2,14 +2,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import ( + CONF_CAPACITANCE, + CONF_DIV_RATIO, CONF_INDOOR, - CONF_WATCHDOG_THRESHOLD, - CONF_NOISE_LEVEL, - CONF_SPIKE_REJECTION, + CONF_IRQ_PIN, CONF_LIGHTNING_THRESHOLD, CONF_MASK_DISTURBER, - CONF_DIV_RATIO, - CONF_CAPACITANCE, + CONF_CALIBRATION, + CONF_TUNE_ANTENNA, + CONF_NOISE_LEVEL, + CONF_SPIKE_REJECTION, + CONF_WATCHDOG_THRESHOLD, ) MULTI_CONF = True @@ -19,7 +22,6 @@ CONF_AS3935_ID = "as3935_id" as3935_ns = cg.esphome_ns.namespace("as3935") AS3935 = as3935_ns.class_("AS3935Component", cg.Component) -CONF_IRQ_PIN = "irq_pin" AS3935_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(AS3935), @@ -34,6 +36,8 @@ AS3935_SCHEMA = cv.Schema( cv.Optional(CONF_MASK_DISTURBER, default=False): cv.boolean, cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 32, 64, 128, int=True), cv.Optional(CONF_CAPACITANCE, default=0): cv.int_range(min=0, max=15), + cv.Optional(CONF_TUNE_ANTENNA, default=False): cv.boolean, + cv.Optional(CONF_CALIBRATION, default=True): cv.boolean, } ) @@ -51,3 +55,5 @@ async def setup_as3935(var, config): cg.add(var.set_mask_disturber(config[CONF_MASK_DISTURBER])) cg.add(var.set_div_ratio(config[CONF_DIV_RATIO])) cg.add(var.set_capacitance(config[CONF_CAPACITANCE])) + cg.add(var.set_tune_antenna(config[CONF_TUNE_ANTENNA])) + cg.add(var.set_calibration(config[CONF_CALIBRATION])) diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index c5651caee3..29fc6ee685 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -21,6 +21,14 @@ void AS3935Component::setup() { this->write_mask_disturber(this->mask_disturber_); this->write_div_ratio(this->div_ratio_); this->write_capacitance(this->capacitance_); + + // Handle setting up tuning or auto-calibration + if (this->tune_antenna_) { + ESP_LOGCONFIG(TAG, " Antenna tuning: ENABLED - lightning detection will not function in this mode"); + this->tune_antenna(); + } else if (this->calibration_) { + this->calibrate_oscillator(); + } } void AS3935Component::dump_config() { @@ -227,6 +235,87 @@ uint32_t AS3935Component::get_lightning_energy_() { return pure_light; } +// REG0x03, bit [7:6], manufacturer default: 0 (16 division ratio). +// This function returns the current division ratio of the resonance frequency. +// The antenna resonance frequency should be within 3.5 percent of 500kHz, and +// so when modifying the resonance frequency with the internal capacitors +// (tuneCap()) it's important to keep in mind that the displayed frequency on +// the IRQ pin is divided by this number. +uint8_t AS3935Component::read_div_ratio() { + ESP_LOGV(TAG, "Calling read_div_ratio"); + uint8_t reg_val = this->read_register_(INT_MASK_ANT, DIV_MASK); + reg_val >>= 6; // Front of the line. + + if (reg_val == 0) { + return 16; + } else if (reg_val == 1) { + return 32; + } else if (reg_val == 2) { + return 64; + } else if (reg_val == 3) { + return 128; + } + ESP_LOGW(TAG, "Unknown response received for div_ratio"); + return 0; +} + +uint8_t AS3935Component::read_capacitance() { + ESP_LOGV(TAG, "Calling read_capacitance"); + uint8_t reg_val = this->read_register_(FREQ_DISP_IRQ, CAP_MASK) * 8; + return (reg_val); +} + +// REG0x08, bits [5,6,7], manufacturer default: 0. +// This will send the frequency of the oscillators to the IRQ pin. +// _osc 1, bit[5] = TRCO - System RCO at 32.768kHz +// _osc 2, bit[6] = SRCO - Timer RCO Oscillators 1.1MHz +// _osc 3, bit[7] = LCO - Frequency of the Antenna +void AS3935Component::display_oscillator(bool state, uint8_t osc) { + if ((osc < 1) || (osc > 3)) + return; + + this->write_register(FREQ_DISP_IRQ, OSC_MASK, state, 4 + osc); +} + +// REG0x3D, bits[7:0] +// This function calibrates both internal oscillators The oscillators are tuned +// based on the resonance frequency of the antenna and so it should be trimmed +// before the calibration is done. +bool AS3935Component::calibrate_oscillator() { + ESP_LOGI(TAG, "Starting oscillators calibration..."); + this->write_register(CALIB_RCO, WIPE_ALL, DIRECT_COMMAND, 0); // Send command to calibrate the oscillators + + this->display_oscillator(true, 2); + delay(2); // Give time for the internal oscillators to start up. + this->display_oscillator(false, 2); + + // Check it they were calibrated successfully. + uint8_t reg_val_srco = this->read_register_(CALIB_SRCO, CALIB_MASK_NOK); + uint8_t reg_val_trco = this->read_register_(CALIB_TRCO, CALIB_MASK_NOK); + + // reg_val_srco &= CALIB_MASK; + // reg_val_srco >>= 6; + // reg_val_trco &= CALIB_MASK; + // reg_val_trco >>= 6; + if (!reg_val_srco && !reg_val_trco) { // Zero upon success + ESP_LOGI(TAG, "Calibration was succesful"); + return true; + } else { + ESP_LOGW(TAG, "Calibration was NOT succesful"); + return false; + } +} + +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"); + this->display_oscillator(true, ANTFREQ); +} + uint8_t AS3935Component::read_register_(uint8_t reg, uint8_t mask) { uint8_t value = this->read_register(reg); value &= (~mask); diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index a8af703a59..dc590c268e 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -13,6 +13,9 @@ namespace esphome { namespace as3935 { +static const uint8_t DIRECT_COMMAND = 0x96; +static const uint8_t ANTFREQ = 3; + enum AS3935RegisterNames { AFE_GAIN = 0x00, THRESHOLD, @@ -30,6 +33,7 @@ enum AS3935RegisterNames { }; enum AS3935RegisterMasks { + WIPE_ALL = 0x0, GAIN_MASK = 0x3E, SPIKE_MASK = 0xF, IO_MASK = 0xC1, @@ -44,6 +48,7 @@ enum AS3935RegisterMasks { NOISE_FLOOR_MASK = 0x70, OSC_MASK = 0xE0, CALIB_MASK = 0x7F, + CALIB_MASK_NOK = 0xBF, DIV_MASK = 0x3F }; @@ -90,6 +95,13 @@ class AS3935Component : public Component { void write_div_ratio(uint8_t div_ratio); void set_capacitance(uint8_t capacitance) { capacitance_ = capacitance; } void write_capacitance(uint8_t capacitance); + uint8_t read_div_ratio(); + uint8_t read_capacitance(); + bool calibrate_oscillator(); + void display_oscillator(bool state, uint8_t osc); + void tune_antenna(); + void set_tune_antenna(bool tune_antenna) { tune_antenna_ = tune_antenna; } + void set_calibration(bool calibration) { calibration_ = calibration; } protected: uint8_t read_interrupt_register_(); @@ -112,6 +124,8 @@ class AS3935Component : public Component { bool mask_disturber_; uint8_t div_ratio_; uint8_t capacitance_; + bool tune_antenna_; + bool calibration_; }; } // namespace as3935 diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 5a3967ed7e..ff78b8a050 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -31,12 +31,10 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): hub = await cg.get_variable(config[CONF_AS3935_ID]) - if CONF_DISTANCE in config: - conf = config[CONF_DISTANCE] - distance_sensor = await sensor.new_sensor(conf) - cg.add(hub.set_distance_sensor(distance_sensor)) + if distance_config := config.get(CONF_DISTANCE): + sens = await sensor.new_sensor(distance_config) + cg.add(hub.set_distance_sensor(sens)) - if CONF_LIGHTNING_ENERGY in config: - conf = config[CONF_LIGHTNING_ENERGY] - lightning_energy_sensor = await sensor.new_sensor(conf) - cg.add(hub.set_energy_sensor(lightning_energy_sensor)) + if lightning_energy_config := config.get(CONF_LIGHTNING_ENERGY): + sens = await sensor.new_sensor(lightning_energy_config) + cg.add(hub.set_energy_sensor(sens)) diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py new file mode 100644 index 0000000000..feeae107a7 --- /dev/null +++ b/esphome/components/as5600/__init__.py @@ -0,0 +1,227 @@ +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_DIR_PIN, + CONF_DIRECTION, + CONF_HYSTERESIS, + CONF_RANGE, +) + +CODEOWNERS = ["@ammmze"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +as5600_ns = cg.esphome_ns.namespace("as5600") +AS5600Component = as5600_ns.class_("AS5600Component", cg.Component, i2c.I2CDevice) + +DIRECTION = { + "CLOCKWISE": 0, + "COUNTERCLOCKWISE": 1, +} + +POWER_MODE = { + "NOMINAL": 0, + "LOW1": 1, + "LOW2": 2, + "LOW3": 3, +} + +HYSTERESIS = { + "NONE": 0, + "LSB1": 1, + "LSB2": 2, + "LSB3": 3, +} + +SLOW_FILTER = { + "16X": 0, + "8X": 1, + "4X": 2, + "2X": 3, +} + +FAST_FILTER = { + "NONE": 0, + "LSB6": 1, + "LSB7": 2, + "LSB9": 3, + "LSB18": 4, + "LSB21": 5, + "LSB24": 6, + "LSB10": 7, +} + +CONF_RAW_ANGLE = "raw_angle" +CONF_RAW_POSITION = "raw_position" +CONF_WATCHDOG = "watchdog" +CONF_POWER_MODE = "power_mode" +CONF_SLOW_FILTER = "slow_filter" +CONF_FAST_FILTER = "fast_filter" +CONF_START_POSITION = "start_position" +CONF_END_POSITION = "end_position" + + +RESOLUTION = 4096 +MAX_POSITION = RESOLUTION - 1 +ANGLE_TO_POSITION = RESOLUTION / 360 +POSITION_TO_ANGLE = 360 / RESOLUTION +# validate min range of 18deg (per datasheet) ... though i seem to get valid values down to a range of 192steps (16.875deg) +MIN_RANGE = round(18 * ANGLE_TO_POSITION) + + +def angle(min=-360, max=360): + return cv.All( + cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max) + ) + + +def angle_to_position(value, min=-360, max=360): + try: + value = angle(min=min, max=max)(value) + return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION + except cv.Invalid as e: + raise cv.Invalid(f"When using angle, {e.error_message}") + + +def percent_to_position(value): + value = cv.possibly_negative_percentage(value) + return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION + + +def position(min=-MAX_POSITION, max=MAX_POSITION): + """Validate that the config option is a position. + Accepts integers, degrees, or percentage (of 360 degrees). + """ + + def validator(value): + if isinstance(value, str) and value.endswith("%"): + value = percent_to_position(value) + + if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")): + return angle_to_position( + value, + min=round(min * POSITION_TO_ANGLE), + max=round(max * POSITION_TO_ANGLE), + ) + + return cv.int_range(min=min, max=max)(value) + + return validator + + +def position_range(): + """Validate that value given is a valid range for the device. + A valid range is one of the following: + - a value of 0 (meaning full range) + - 18 thru 360 degrees + - negative 360 thru negative 18 degrees (notes: these are normalized to their positive values, accepting negatives is for convenience) + """ + zero_validator = position(min=0, max=0) + negative_validator = cv.Any( + position(min=-MAX_POSITION, max=-MIN_RANGE), + zero_validator, + ) + positive_validator = cv.Any( + position(min=MIN_RANGE, max=MAX_POSITION), + zero_validator, + ) + + def validator(value): + is_negative_str = isinstance(value, str) and value.startswith("-") + is_negative_num = isinstance(value, (float, int)) and value < 0 + if is_negative_str or is_negative_num: + return negative_validator(value) + return positive_validator(value) + + return validator + + +def has_valid_range_config(): + """Validate that that the config start + end position results in a valid + positional range, which must be >= 18degrees + """ + range_validator = position_range() + + def validator(config): + # if we don't have an end position, then there is nothing to do + if CONF_END_POSITION not in config: + return config + + # determine the range by taking the difference from the end and start + range = config[CONF_END_POSITION] - config[CONF_START_POSITION] + + # but need to account for start position being greater than end position + # where the range rolls back around the 0 position + if config[CONF_END_POSITION] < config[CONF_START_POSITION]: + range = RESOLUTION + config[CONF_END_POSITION] - config[CONF_START_POSITION] + + try: + range_validator(range) + return config + except cv.Invalid as e: + raise cv.Invalid( + f"The range between start and end position is invalid. It was was {range} but {e.error_message}" + ) + + return validator + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AS5600Component), + cv.Optional(CONF_DIR_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_DIRECTION, default="CLOCKWISE"): cv.enum( + DIRECTION, upper=True + ), + cv.Optional(CONF_WATCHDOG, default=False): cv.boolean, + cv.Optional(CONF_POWER_MODE, default="NOMINAL"): cv.enum( + POWER_MODE, upper=True, space="" + ), + cv.Optional(CONF_HYSTERESIS, default="NONE"): cv.enum( + HYSTERESIS, upper=True, space="" + ), + cv.Optional(CONF_SLOW_FILTER, default="16X"): cv.enum( + SLOW_FILTER, upper=True, space="" + ), + cv.Optional(CONF_FAST_FILTER, default="NONE"): cv.enum( + FAST_FILTER, upper=True, space="" + ), + cv.Optional(CONF_START_POSITION, default=0): position(), + cv.Optional(CONF_END_POSITION): position(), + cv.Optional(CONF_RANGE): position_range(), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x36)), + # ensure end_position and range are mutually exclusive + cv.has_at_most_one_key(CONF_END_POSITION, CONF_RANGE), + has_valid_range_config(), +) + + +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) + + cg.add(var.set_direction(config[CONF_DIRECTION])) + cg.add(var.set_watchdog(config[CONF_WATCHDOG])) + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + cg.add(var.set_slow_filter(config[CONF_SLOW_FILTER])) + cg.add(var.set_fast_filter(config[CONF_FAST_FILTER])) + cg.add(var.set_start_position(config[CONF_START_POSITION])) + + if dir_pin_config := config.get(CONF_DIR_PIN): + pin = await cg.gpio_pin_expression(dir_pin_config) + cg.add(var.set_dir_pin(pin)) + + if (end_position_config := config.get(CONF_END_POSITION, None)) is not None: + cg.add(var.set_end_position(end_position_config)) + + if (range_config := config.get(CONF_RANGE, None)) is not None: + cg.add(var.set_range(range_config)) diff --git a/esphome/components/as5600/as5600.cpp b/esphome/components/as5600/as5600.cpp new file mode 100644 index 0000000000..3fe7eab58d --- /dev/null +++ b/esphome/components/as5600/as5600.cpp @@ -0,0 +1,138 @@ +#include "as5600.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as5600 { + +static const char *const TAG = "as5600"; + +// Configuration registers +static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R +static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW +static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW +static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW +static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW + +// Output registers +static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R +static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R + +// Status registers +static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R +static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R +static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R + +void AS5600Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up AS5600..."); + + if (!this->read_byte(REGISTER_STATUS).has_value()) { + this->mark_failed(); + return; + } + + // configuration direction pin, if given + // the dir pin on the chip should be low for clockwise + // and high for counterclockwise. If the pin is left floating + // the reported positions will be erratic. + if (this->dir_pin_ != nullptr) { + this->dir_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->dir_pin_->digital_write(this->direction_ == 1); + } + + // build config register + // take the value, shift it left, and add mask to it to ensure we + // are only changing the bits appropriate for that setting in the + // off chance we somehow have bad value in there and it makes for + // a nice visual for the bit positions. + uint16_t config = 0; + // clang-format off + config |= (this->watchdog_ << 13) & 0b0010000000000000; + config |= (this->fast_filter_ << 10) & 0b0001110000000000; + config |= (this->slow_filter_ << 8) & 0b0000001100000000; + config |= (this->pwm_frequency_ << 6) & 0b0000000011000000; + config |= (this->output_mode_ << 4) & 0b0000000000110000; + config |= (this->hysteresis_ << 2) & 0b0000000000001100; + config |= (this->power_mode_ << 0) & 0b0000000000000011; + // clang-format on + + // write config to config register + if (!this->write_byte_16(REGISTER_CONF, config)) { + this->mark_failed(); + return; + } + + // configure the start position + this->write_byte_16(REGISTER_ZPOS, this->start_position_); + + // configure either end position or max angle + if (this->end_mode_ == END_MODE_POSITION) { + this->write_byte_16(REGISTER_MPOS, this->end_position_); + } else { + this->write_byte_16(REGISTER_MANG, this->end_position_); + } + + // calculate the raw max from end position or start + range + this->raw_max_ = this->end_mode_ == END_MODE_POSITION ? this->end_position_ & 4095 + : (this->start_position_ + this->end_position_) & 4095; + + // calculate allowed range of motion by taking the start from the end + // but only if the end is greater than the start. If the start is greater + // than the end position, then that means we take the start all the way to + // reset point (i.e. 0 deg raw) and then we that with the end position + uint16_t range = this->raw_max_ > this->start_position_ ? this->raw_max_ - this->start_position_ + : (4095 - this->start_position_) + this->raw_max_; + + // range scale is ratio of actual allowed range to the full range + this->range_scale_ = range / 4095.0f; +} + +void AS5600Component::dump_config() { + ESP_LOGCONFIG(TAG, "AS5600:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AS5600 failed!"); + return; + } + + ESP_LOGCONFIG(TAG, " Watchdog: %d", this->watchdog_); + ESP_LOGCONFIG(TAG, " Fast Filter: %d", this->fast_filter_); + ESP_LOGCONFIG(TAG, " Slow Filter: %d", this->slow_filter_); + ESP_LOGCONFIG(TAG, " Hysteresis: %d", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Start Position: %d", this->start_position_); + if (this->end_mode_ == END_MODE_POSITION) { + ESP_LOGCONFIG(TAG, " End Position: %d", this->end_position_); + } else { + ESP_LOGCONFIG(TAG, " Range: %d", this->end_position_); + } +} + +bool AS5600Component::in_range(uint16_t raw_position) { + return this->raw_max_ > this->start_position_ + ? raw_position >= this->start_position_ && raw_position <= this->raw_max_ + : raw_position >= this->start_position_ || raw_position <= this->raw_max_; +} + +AS5600MagnetStatus AS5600Component::read_magnet_status() { + uint8_t status = this->reg(REGISTER_STATUS).get() >> 3 & 0b000111; + return static_cast(status); +} + +optional AS5600Component::read_position() { + uint16_t pos = 0; + if (!this->read_byte_16(REGISTER_ANGLE, &pos)) { + return {}; + } + return pos; +} + +optional AS5600Component::read_raw_position() { + uint16_t pos = 0; + if (!this->read_byte_16(REGISTER_ANGLE_RAW, &pos)) { + return {}; + } + return pos; +} + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h new file mode 100644 index 0000000000..fbfd18db40 --- /dev/null +++ b/esphome/components/as5600/as5600.h @@ -0,0 +1,105 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/preferences.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace as5600 { + +static const uint16_t POSITION_COUNT = 4096; +static const float RAW_TO_DEGREES = 360.0 / POSITION_COUNT; +static const float DEGREES_TO_RAW = POSITION_COUNT / 360.0; + +enum EndPositionMode : uint8_t { + // In this mode, the end position is calculated by taking the start position + // and adding the range/positions. For example, you could say start at 90deg, + // and have a range of 180deg and effectively the sensor will report values + // from the physical 90deg thru 270deg. + END_MODE_RANGE, + // In this mode, the end position is explicitly set, and changing the start + // position will NOT change the end position. + END_MODE_POSITION, +}; + +enum OutRangeMode : uint8_t { + // In this mode, the AS5600 chip itself actually reports these values, but + // effectively it splits the out-of-range values in half, and when positioned + // over the half closest to the min/start position, it will report 0 and when + // positioned over the half closes to the max/end position, it will report the + // max/end value. + OUT_RANGE_MODE_MIN_MAX, + // In this mode, when the magnet is positioned outside the configured + // range, the sensor will report NAN, which translates to "Unknown" + // in Home Assistant. + OUT_RANGE_MODE_NAN, +}; + +enum AS5600MagnetStatus : uint8_t { + MAGNET_GONE = 2, // 0b010 / magnet not detected + MAGNET_OK = 4, // 0b100 / magnet just right + MAGNET_STRONG = 5, // 0b101 / magnet too strong + MAGNET_WEAK = 6, // 0b110 / magnet too weak +}; + +class AS5600Component : public Component, public i2c::I2CDevice { + public: + /// Set up the internal sensor array. + void setup() override; + void dump_config() override; + /// HARDWARE_LATE setup priority + float get_setup_priority() const override { return setup_priority::DATA; } + + // configuration setters + void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; } + void set_direction(uint8_t direction) { this->direction_ = direction; } + void set_fast_filter(uint8_t fast_filter) { this->fast_filter_ = fast_filter; } + void set_hysteresis(uint8_t hysteresis) { this->hysteresis_ = hysteresis; } + void set_power_mode(uint8_t power_mode) { this->power_mode_ = power_mode; } + void set_slow_filter(uint8_t slow_filter) { this->slow_filter_ = slow_filter; } + void set_watchdog(bool watchdog) { this->watchdog_ = watchdog; } + bool get_watchdog() { return this->watchdog_; } + void set_start_position(uint16_t start_position) { this->start_position_ = start_position % POSITION_COUNT; } + void set_end_position(uint16_t end_position) { + this->end_position_ = end_position % POSITION_COUNT; + this->end_mode_ = END_MODE_POSITION; + } + void set_range(uint16_t range) { + this->end_position_ = range % POSITION_COUNT; + this->end_mode_ = END_MODE_RANGE; + } + + // Gets the scale value for the configured range. + // For example, if configured to start at 0deg and end at 180deg, the + // range is 50% of the native/raw range, so the range scale would be 0.5. + // If configured to use the full 360deg, the range scale would be 1.0. + float get_range_scale() { return this->range_scale_; } + + // Indicates whether the given *raw* position is within the configured range + bool in_range(uint16_t raw_position); + + AS5600MagnetStatus read_magnet_status(); + optional read_position(); + optional read_raw_position(); + + protected: + InternalGPIOPin *dir_pin_{nullptr}; + uint8_t direction_; + uint8_t fast_filter_; + uint8_t hysteresis_; + uint8_t power_mode_; + uint8_t slow_filter_; + uint8_t pwm_frequency_{0}; + uint8_t output_mode_{0}; + bool watchdog_; + uint16_t start_position_; + uint16_t end_position_{0}; + uint16_t raw_max_; + EndPositionMode end_mode_{END_MODE_RANGE}; + float range_scale_{1.0}; +}; + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py new file mode 100644 index 0000000000..30337ab61b --- /dev/null +++ b/esphome/components/as5600/sensor/__init__.py @@ -0,0 +1,119 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + ICON_MAGNET, + ICON_ROTATE_RIGHT, + CONF_GAIN, + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_MAGNITUDE, + CONF_STATUS, + CONF_POSITION, + CONF_ANGLE, +) +from .. import as5600_ns, AS5600Component + +CODEOWNERS = ["@ammmze"] +DEPENDENCIES = ["as5600"] + +AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent) + +CONF_RAW_ANGLE = "raw_angle" +CONF_RAW_POSITION = "raw_position" +CONF_WATCHDOG = "watchdog" +CONF_POWER_MODE = "power_mode" +CONF_SLOW_FILTER = "slow_filter" +CONF_FAST_FILTER = "fast_filter" +CONF_PWM_FREQUENCY = "pwm_frequency" +CONF_BURN_COUNT = "burn_count" +CONF_START_POSITION = "start_position" +CONF_END_POSITION = "end_position" +CONF_OUT_OF_RANGE_MODE = "out_of_range_mode" + +OutOfRangeMode = as5600_ns.enum("OutRangeMode") +OUT_OF_RANGE_MODES = { + "MIN_MAX": OutOfRangeMode.OUT_RANGE_MODE_MIN_MAX, + "NAN": OutOfRangeMode.OUT_RANGE_MODE_NAN, +} + + +CONF_AS5600_ID = "as5600_id" +CONFIG_SCHEMA = ( + sensor.sensor_schema( + AS5600Sensor, + accuracy_decimals=0, + icon=ICON_ROTATE_RIGHT, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_AS5600_ID): cv.use_id(AS5600Component), + cv.Optional(CONF_OUT_OF_RANGE_MODE): cv.enum( + OUT_OF_RANGE_MODES, upper=True, space="_" + ), + cv.Optional(CONF_RAW_POSITION): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_ROTATE_RIGHT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_GAIN): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_MAGNITUDE): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_MAGNET, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_MAGNET, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_AS5600_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE): + cg.add(var.set_out_of_range_mode(out_of_range_mode_config)) + + if angle_config := config.get(CONF_ANGLE): + sens = await sensor.new_sensor(angle_config) + cg.add(var.set_angle_sensor(sens)) + + if raw_angle_config := config.get(CONF_RAW_ANGLE): + sens = await sensor.new_sensor(raw_angle_config) + cg.add(var.set_raw_angle_sensor(sens)) + + if position_config := config.get(CONF_POSITION): + sens = await sensor.new_sensor(position_config) + cg.add(var.set_position_sensor(sens)) + + if raw_position_config := config.get(CONF_RAW_POSITION): + sens = await sensor.new_sensor(raw_position_config) + cg.add(var.set_raw_position_sensor(sens)) + + if gain_config := config.get(CONF_GAIN): + sens = await sensor.new_sensor(gain_config) + cg.add(var.set_gain_sensor(sens)) + + if magnitude_config := config.get(CONF_MAGNITUDE): + sens = await sensor.new_sensor(magnitude_config) + cg.add(var.set_magnitude_sensor(sens)) + + if status_config := config.get(CONF_STATUS): + sens = await sensor.new_sensor(status_config) + cg.add(var.set_status_sensor(sens)) diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp new file mode 100644 index 0000000000..feb8f6cebf --- /dev/null +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -0,0 +1,98 @@ +#include "as5600_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as5600 { + +static const char *const TAG = "as5600.sensor"; + +// Configuration registers +static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R +static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW +static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW +static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW +static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW + +// Output registers +static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R +static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R + +// Status registers +static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R +static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R +static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R + +float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void AS5600Sensor::dump_config() { + LOG_SENSOR("", "AS5600 Sensor", this); + ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); + if (this->angle_sensor_ != nullptr) { + LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_); + } + if (this->raw_angle_sensor_ != nullptr) { + LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_); + } + if (this->position_sensor_ != nullptr) { + LOG_SENSOR(" ", "Position Sensor", this->position_sensor_); + } + if (this->raw_position_sensor_ != nullptr) { + LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); + } + if (this->gain_sensor_ != nullptr) { + LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); + } + if (this->magnitude_sensor_ != nullptr) { + LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); + } + if (this->status_sensor_ != nullptr) { + LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); + } + LOG_UPDATE_INTERVAL(this); +} + +void AS5600Sensor::update() { + if (this->gain_sensor_ != nullptr) { + this->gain_sensor_->publish_state(this->parent_->reg(REGISTER_AGC).get()); + } + + if (this->magnitude_sensor_ != nullptr) { + uint16_t value = 0; + this->parent_->read_byte_16(REGISTER_MAGNITUDE, &value); + this->magnitude_sensor_->publish_state(value); + } + + // 2 = magnet not detected + // 4 = magnet just right + // 5 = magnet too strong + // 6 = magnet too weak + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(this->parent_->read_magnet_status()); + } + + auto pos = this->parent_->read_position(); + if (!pos.has_value()) { + this->status_set_warning(); + return; + } + + auto raw = this->parent_->read_raw_position(); + if (!raw.has_value()) { + this->status_set_warning(); + return; + } + + if (this->out_of_range_mode_ == OUT_RANGE_MODE_NAN) { + this->publish_state(this->parent_->in_range(raw.value()) ? pos.value() : NAN); + } else { + this->publish_state(pos.value()); + } + + if (this->raw_position_sensor_ != nullptr) { + this->raw_position_sensor_->publish_state(raw.value()); + } + this->status_clear_warning(); +} + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h new file mode 100644 index 0000000000..0af9b01ae5 --- /dev/null +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/preferences.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/as5600/as5600.h" + +namespace esphome { +namespace as5600 { + +class AS5600Sensor : public PollingComponent, public Parented, public sensor::Sensor { + public: + void update() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } + void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } + void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; } + void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) { + this->raw_position_sensor_ = raw_position_sensor; + } + void set_gain_sensor(sensor::Sensor *gain_sensor) { this->gain_sensor_ = gain_sensor; } + void set_magnitude_sensor(sensor::Sensor *magnitude_sensor) { this->magnitude_sensor_ = magnitude_sensor; } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_out_of_range_mode(OutRangeMode oor_mode) { this->out_of_range_mode_ = oor_mode; } + OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; } + + protected: + sensor::Sensor *angle_sensor_{nullptr}; + sensor::Sensor *raw_angle_sensor_{nullptr}; + sensor::Sensor *position_sensor_{nullptr}; + sensor::Sensor *raw_position_sensor_{nullptr}; + sensor::Sensor *gain_sensor_{nullptr}; + sensor::Sensor *magnitude_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX}; +}; + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as7341/sensor.py b/esphome/components/as7341/sensor.py index 2424087c35..de60444aed 100644 --- a/esphome/components/as7341/sensor.py +++ b/esphome/components/as7341/sensor.py @@ -107,6 +107,6 @@ async def to_code(config): cg.add(var.set_astep(config[CONF_ASTEP])) for conf_id, set_sensor_func in SENSORS.items(): - if conf_id in config: - sens = await sensor.new_sensor(config[conf_id]) + if sens_config := config.get(conf_id): + sens = await sensor.new_sensor(sens_config) cg.add(getattr(var, set_sensor_func)(sens)) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 1d127623f1..99e250b6fc 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -1,6 +1,12 @@ # Dummy integration to allow relying on AsyncTCP import esphome.codegen as cg import esphome.config_validation as cv +from esphome.const import ( + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RTL87XX, +) from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@OttoWinter"] @@ -8,15 +14,15 @@ CODEOWNERS = ["@OttoWinter"] CONFIG_SCHEMA = cv.All( cv.Schema({}), cv.only_with_arduino, - cv.only_on(["esp32", "esp8266"]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), ) @coroutine_with_priority(200.0) async def to_code(config): - if CORE.is_esp32: + if CORE.is_esp32 or CORE.is_libretiny: # https://github.com/esphome/AsyncTCP/blob/master/library.json - cg.add_library("esphome/AsyncTCP-esphome", "1.2.2") + cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") elif CORE.is_esp8266: # https://github.com/esphome/ESPAsyncTCP - cg.add_library("esphome/ESPAsyncTCP-esphome", "1.2.3") + cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py new file mode 100644 index 0000000000..e636510a4b --- /dev/null +++ b/esphome/components/at581x/__init__.py @@ -0,0 +1,223 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation, core +from esphome.components import i2c +from esphome.automation import maybe_simple_id +from esphome.const import ( + CONF_ID, + CONF_FREQUENCY, +) + + +CODEOWNERS = ["@X-Ryl669"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + + +at581x_ns = cg.esphome_ns.namespace("at581x") +AT581XComponent = at581x_ns.class_("AT581XComponent", cg.Component, i2c.I2CDevice) + + +CONF_AT581X_ID = "at581x_id" + + +CONF_SENSING_DISTANCE = "sensing_distance" +CONF_POWERON_SELFCHECK_TIME = "poweron_selfcheck_time" +CONF_PROTECT_TIME = "protect_time" +CONF_TRIGGER_BASE = "trigger_base" +CONF_TRIGGER_KEEP = "trigger_keep" +CONF_STAGE_GAIN = "stage_gain" +CONF_POWER_CONSUMPTION = "power_consumption" +CONF_HW_FRONTEND_RESET = "hw_frontend_reset" + +RADAR_ALLOWED_FREQ = [ + 5696e6, + 5715e6, + 5730e6, + 5748e6, + 5765e6, + 5784e6, + 5800e6, + 5819e6, + 5836e6, + 5851e6, + 5869e6, + 5888e6, +] +RADAR_ALLOWED_CUR_CONSUMPTION = [ + 48e-6, + 56e-6, + 63e-6, + 70e-6, + 77e-6, + 91e-6, + 105e-6, + 115e-6, + 40e-6, + 44e-6, + 47e-6, + 51e-6, + 54e-6, + 61e-6, + 68e-6, + 78e-6, +] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(AT581XComponent), + } +) + +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA.extend(i2c.i2c_device_schema(0x28)).extend(cv.COMPONENT_SCHEMA) +) + + +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) + + +# Actions +AT581XResetAction = at581x_ns.class_("AT581XResetAction", automation.Action) +AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Action) + + +@automation.register_action( + "at581x.reset", + AT581XResetAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(AT581XComponent), + } + ), +) +async def at581x_reset_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 + + +RADAR_SETTINGS_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(AT581XComponent), + cv.Optional(CONF_HW_FRONTEND_RESET): cv.templatable(cv.boolean), + cv.Optional(CONF_FREQUENCY, default="5800MHz"): cv.templatable( + cv.All(cv.frequency, cv.one_of(*RADAR_ALLOWED_FREQ)) + ), + cv.Optional(CONF_SENSING_DISTANCE, default=823): cv.templatable( + cv.int_range(min=0, max=1023) + ), + cv.Optional(CONF_POWERON_SELFCHECK_TIME, default="2000ms"): cv.templatable( + cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=65535)), + ) + ), + cv.Optional(CONF_POWER_CONSUMPTION, default="70uA"): cv.templatable( + cv.All(cv.current, cv.one_of(*RADAR_ALLOWED_CUR_CONSUMPTION)) + ), + cv.Optional(CONF_PROTECT_TIME, default="1000ms"): cv.templatable( + cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=core.TimePeriod(milliseconds=1), + max=core.TimePeriod(milliseconds=65535), + ), + ) + ), + cv.Optional(CONF_TRIGGER_BASE, default="500ms"): cv.templatable( + cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=core.TimePeriod(milliseconds=1), + max=core.TimePeriod(milliseconds=65535), + ), + ) + ), + cv.Optional(CONF_TRIGGER_KEEP, default="1500ms"): cv.templatable( + cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=core.TimePeriod(milliseconds=1), + max=core.TimePeriod(milliseconds=65535), + ), + ) + ), + cv.Optional(CONF_STAGE_GAIN, default=3): cv.templatable( + cv.int_range(min=0, max=12) + ), + } +).add_extra( + cv.has_at_least_one_key( + CONF_HW_FRONTEND_RESET, + CONF_FREQUENCY, + CONF_SENSING_DISTANCE, + ) +) + + +@automation.register_action( + "at581x.settings", + AT581XSettingsAction, + RADAR_SETTINGS_SCHEMA, +) +async def at581x_settings_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + # Radar configuration + if frontend_reset := config.get(CONF_HW_FRONTEND_RESET): + template_ = await cg.templatable(frontend_reset, args, int) + cg.add(var.set_hw_frontend_reset(template_)) + + if freq := config.get(CONF_FREQUENCY): + template_ = await cg.templatable(freq, args, float) + template_ = int(template_ / 1000000) + cg.add(var.set_frequency(template_)) + + if sens_dist := config.get(CONF_SENSING_DISTANCE): + template_ = await cg.templatable(sens_dist, args, int) + cg.add(var.set_sensing_distance(template_)) + + if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME): + template_ = await cg.templatable(selfcheck, args, float) + if isinstance(template_, cv.TimePeriod): + template_ = template_.total_milliseconds + template_ = int(template_) + cg.add(var.set_poweron_selfcheck_time(template_)) + + if protect := config.get(CONF_PROTECT_TIME): + template_ = await cg.templatable(protect, args, float) + if isinstance(template_, cv.TimePeriod): + template_ = template_.total_milliseconds + template_ = int(template_) + cg.add(var.set_protect_time(template_)) + + if trig_base := config.get(CONF_TRIGGER_BASE): + template_ = await cg.templatable(trig_base, args, float) + if isinstance(template_, cv.TimePeriod): + template_ = template_.total_milliseconds + template_ = int(template_) + cg.add(var.set_trigger_base(template_)) + + if trig_keep := config.get(CONF_TRIGGER_KEEP): + template_ = await cg.templatable(trig_keep, args, float) + if isinstance(template_, cv.TimePeriod): + template_ = template_.total_milliseconds + template_ = int(template_) + cg.add(var.set_trigger_keep(template_)) + + if stage_gain := config.get(CONF_STAGE_GAIN): + template_ = await cg.templatable(stage_gain, args, int) + cg.add(var.set_stage_gain(template_)) + + if power := config.get(CONF_POWER_CONSUMPTION): + template_ = await cg.templatable(power, args, float) + template_ = int(template_ * 1000000) + cg.add(var.set_power_consumption(template_)) + + return var diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp new file mode 100644 index 0000000000..eef457f985 --- /dev/null +++ b/esphome/components/at581x/at581x.cpp @@ -0,0 +1,195 @@ +#include "at581x.h" +#include "esphome/core/log.h" + +/* Select gain for AT581X (3dB per step for level1, 6dB per step for level 2), high value = small gain. (p12) */ +const uint8_t GAIN_ADDR_TABLE[] = {0x5c, 0x63}; +const uint8_t GAIN5C_TABLE[] = {0x08, 0x18, 0x28, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xa8, 0xb8, 0xc8}; +const uint8_t GAIN63_TABLE[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; +const uint8_t GAIN61_VALUE = 0xCA; // 0xC0 | 0x02 (freq present) | 0x08 (gain present) + +/*!< Power consumption configuration table (p12). */ +const uint8_t POWER_TABLE[] = {48, 56, 63, 70, 77, 91, 105, 115, 40, 44, 47, 51, 54, 61, 68, 78}; +const uint8_t POWER67_TABLE[] = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7}; +const uint8_t POWER68_TABLE[] = {0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, + 24, 24, 24, 24, 24, 24, 24, 24}; // See Page 12, shift by 3 bits + +/*!< Frequency Configuration table (p14/15 of datasheet). */ +const uint8_t FREQ_ADDR = 0x61; +const uint16_t FREQ_TABLE[] = {5696, 5715, 5730, 5748, 5765, 5784, 5800, 5819, 5836, 5851, 5869, 5888}; +const uint8_t FREQ5F_TABLE[] = {0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x40, 0x41, 0x42, 0x43}; +const uint8_t FREQ60_TABLE[] = {0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9e, 0x9e, 0x9e, 0x9e}; + +/*!< Value for RF and analog modules switch (p10). */ +const uint8_t RF_OFF_TABLE[] = {0x46, 0xaa, 0x50}; +const uint8_t RF_ON_TABLE[] = {0x45, 0x55, 0xA0}; +const uint8_t RF_REG_ADDR[] = {0x5d, 0x62, 0x51}; + +/*!< Registers of Lighting delay time. Unit: ms, min 2s (p8) */ +const uint8_t HIGH_LEVEL_DELAY_CONTROL_ADDR = 0x41; /*!< Time_flag_out_ctrl 0x01 */ +const uint8_t HIGH_LEVEL_DELAY_VALUE_ADDR = 0x42; /*!< Time_flag_out_1 Bit<7:0> */ + +const uint8_t RESET_ADDR = 0x00; + +/*!< Sensing distance address */ +const uint8_t SIGNAL_DETECTION_THRESHOLD_ADDR_LO = 0x10; +const uint8_t SIGNAL_DETECTION_THRESHOLD_ADDR_HI = 0x11; + +/*!< Bit field value for power registers */ +const uint8_t POWER_THRESHOLD_ADDR_HI = 0x68; +const uint8_t POWER_THRESHOLD_ADDR_LO = 0x67; +const uint8_t PWR_WORK_TIME_EN = 8; // Reg 0x67 +const uint8_t PWR_BURST_TIME_EN = 32; // Reg 0x68 +const uint8_t PWR_THRESH_EN = 64; // Reg 0x68 +const uint8_t PWR_THRESH_VAL_EN = 128; // Reg 0x67 + +/*!< Times */ +const uint8_t TRIGGER_BASE_TIME_ADDR = 0x3D; // 4 bytes, so up to 0x40 +const uint8_t PROTECT_TIME_ADDR = 0x4E; // 2 bytes, up to 0x4F +const uint8_t TRIGGER_KEEP_TIME_ADDR = 0x42; // 4 bytes, so up to 0x45 +const uint8_t TIME41_VALUE = 1; +const uint8_t SELF_CHECK_TIME_ADDR = 0x38; // 2 bytes, up to 0x39 + +namespace esphome { +namespace at581x { + +static const char *const TAG = "at581x"; + +bool AT581XComponent::i2c_write_reg(uint8_t addr, uint8_t data) { + return this->write_register(addr, &data, 1) == esphome::i2c::NO_ERROR; +} +bool AT581XComponent::i2c_write_reg(uint8_t addr, uint32_t data) { + return this->i2c_write_reg(addr + 0, uint8_t(data & 0xFF)) && + this->i2c_write_reg(addr + 1, uint8_t((data >> 8) & 0xFF)) && + this->i2c_write_reg(addr + 2, uint8_t((data >> 16) & 0xFF)) && + this->i2c_write_reg(addr + 3, uint8_t((data >> 24) & 0xFF)); +} +bool AT581XComponent::i2c_write_reg(uint8_t addr, uint16_t data) { + return this->i2c_write_reg(addr, uint8_t(data & 0xFF)) && this->i2c_write_reg(addr + 1, uint8_t((data >> 8) & 0xFF)); +} + +bool AT581XComponent::i2c_read_reg(uint8_t addr, uint8_t &data) { + return this->read_register(addr, &data, 1) == esphome::i2c::NO_ERROR; +} + +void AT581XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up AT581X..."); } +void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); } +#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0])) +bool AT581XComponent::i2c_write_config() { + ESP_LOGCONFIG(TAG, "Writing new config for AT581X..."); + ESP_LOGCONFIG(TAG, "Frequency: %dMHz", this->freq_); + ESP_LOGCONFIG(TAG, "Sensing distance: %d", this->delta_); + ESP_LOGCONFIG(TAG, "Power: %dµA", this->power_); + ESP_LOGCONFIG(TAG, "Gain: %d", this->gain_); + ESP_LOGCONFIG(TAG, "Trigger base time: %dms", this->trigger_base_time_ms_); + ESP_LOGCONFIG(TAG, "Trigger keep time: %dms", this->trigger_keep_time_ms_); + ESP_LOGCONFIG(TAG, "Protect time: %dms", this->protect_time_ms_); + ESP_LOGCONFIG(TAG, "Self check time: %dms", this->self_check_time_ms_); + + // Set frequency point + if (!this->i2c_write_reg(FREQ_ADDR, GAIN61_VALUE)) { + ESP_LOGE(TAG, "Failed to write AT581X Freq mode"); + return false; + } + // Find the current frequency from the table to know what value to write + for (size_t i = 0; i < ARRAY_SIZE(FREQ_TABLE) + 1; i++) { + if (i == ARRAY_SIZE(FREQ_TABLE)) { + ESP_LOGE(TAG, "Set frequency not found"); + return false; + } + if (FREQ_TABLE[i] == this->freq_) { + if (!this->i2c_write_reg(0x5F, FREQ5F_TABLE[i]) || !this->i2c_write_reg(0x60, FREQ60_TABLE[i])) { + ESP_LOGE(TAG, "Failed to write AT581X Freq value"); + return false; + } + break; + } + } + + // Set distance + if (!this->i2c_write_reg(SIGNAL_DETECTION_THRESHOLD_ADDR_LO, (uint8_t) (this->delta_ & 0xFF)) || + !this->i2c_write_reg(SIGNAL_DETECTION_THRESHOLD_ADDR_HI, (uint8_t) (this->delta_ >> 8))) { + ESP_LOGE(TAG, "Failed to write AT581X sensing distance low"); + return false; + } + + // Set power setting + uint8_t pwr67 = PWR_THRESH_VAL_EN | PWR_WORK_TIME_EN, pwr68 = PWR_BURST_TIME_EN | PWR_THRESH_EN; + for (size_t i = 0; i < ARRAY_SIZE(POWER_TABLE) + 1; i++) { + if (i == ARRAY_SIZE(POWER_TABLE)) { + ESP_LOGE(TAG, "Set power not found"); + return false; + } + if (POWER_TABLE[i] == this->power_) { + pwr67 |= POWER67_TABLE[i]; + pwr68 |= POWER68_TABLE[i]; // See Page 12 + break; + } + } + + if (!this->i2c_write_reg(POWER_THRESHOLD_ADDR_LO, pwr67) || !this->i2c_write_reg(POWER_THRESHOLD_ADDR_HI, pwr68)) { + ESP_LOGE(TAG, "Failed to write AT581X power registers"); + return false; + } + + // Set gain + if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) || + !this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) { + ESP_LOGE(TAG, "Failed to write AT581X gain registers"); + return false; + } + + // Set times + if (!this->i2c_write_reg(TRIGGER_BASE_TIME_ADDR, (uint32_t) this->trigger_base_time_ms_)) { + ESP_LOGE(TAG, "Failed to write AT581X trigger base time registers"); + return false; + } + if (!this->i2c_write_reg(TRIGGER_KEEP_TIME_ADDR, (uint32_t) this->trigger_keep_time_ms_)) { + ESP_LOGE(TAG, "Failed to write AT581X trigger keep time registers"); + return false; + } + + if (!this->i2c_write_reg(PROTECT_TIME_ADDR, (uint16_t) this->protect_time_ms_)) { + ESP_LOGE(TAG, "Failed to write AT581X protect time registers"); + return false; + } + if (!this->i2c_write_reg(SELF_CHECK_TIME_ADDR, (uint16_t) this->self_check_time_ms_)) { + ESP_LOGE(TAG, "Failed to write AT581X self check time registers"); + return false; + } + + if (!this->i2c_write_reg(0x41, TIME41_VALUE)) { + ESP_LOGE(TAG, "Failed to enable AT581X time registers"); + return false; + } + + // Don't know why it's required in other code, it's not in datasheet + if (!this->i2c_write_reg(0x55, (uint8_t) 0x04)) { + ESP_LOGE(TAG, "Failed to enable AT581X"); + return false; + } + + // Ok, config is written, let's reset the chip so it's using the new config + return this->reset_hardware_frontend(); +} + +// float AT581XComponent::get_setup_priority() const { return 0; } +bool AT581XComponent::reset_hardware_frontend() { + if (!this->i2c_write_reg(RESET_ADDR, (uint8_t) 0) || !this->i2c_write_reg(RESET_ADDR, (uint8_t) 1)) { + ESP_LOGE(TAG, "Failed to reset AT581X hardware frontend"); + return false; + } + return true; +} + +void AT581XComponent::set_rf_mode(bool enable) { + const uint8_t *p = enable ? &RF_ON_TABLE[0] : &RF_OFF_TABLE[0]; + for (size_t i = 0; i < ARRAY_SIZE(RF_REG_ADDR); i++) { + if (!this->i2c_write_reg(RF_REG_ADDR[i], p[i])) { + ESP_LOGE(TAG, "Failed to write AT581X RF mode"); + return; + } + } +} + +} // namespace at581x +} // namespace esphome diff --git a/esphome/components/at581x/at581x.h b/esphome/components/at581x/at581x.h new file mode 100644 index 0000000000..6c637d08c5 --- /dev/null +++ b/esphome/components/at581x/at581x.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/defines.h" +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace at581x { + +class AT581XComponent : public Component, public i2c::I2CDevice { +#ifdef USE_SWITCH + protected: + switch_::Switch *rf_power_switch_{nullptr}; + + public: + void set_rf_power_switch(switch_::Switch *s) { + this->rf_power_switch_ = s; + s->turn_on(); + } +#endif + + void setup() override; + void dump_config() override; + // float get_setup_priority() const override; + + void set_sensing_distance(int distance) { this->delta_ = 1023 - distance; } + + void set_rf_mode(bool enabled); + void set_frequency(int frequency) { this->freq_ = frequency; } + void set_poweron_selfcheck_time(int value) { this->self_check_time_ms_ = value; } + void set_protect_time(int value) { this->protect_time_ms_ = value; } + void set_trigger_base(int value) { this->trigger_base_time_ms_ = value; } + void set_trigger_keep(int value) { this->trigger_keep_time_ms_ = value; } + void set_stage_gain(int value) { this->gain_ = value; } + void set_power_consumption(int value) { this->power_ = value; } + + bool i2c_write_config(); + bool reset_hardware_frontend(); + bool i2c_write_reg(uint8_t addr, uint8_t data); + bool i2c_write_reg(uint8_t addr, uint32_t data); + bool i2c_write_reg(uint8_t addr, uint16_t data); + bool i2c_read_reg(uint8_t addr, uint8_t &data); + + protected: + int freq_; + int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */ + int protect_time_ms_; /*!< Protection time, recommended 1000 ms */ + int trigger_base_time_ms_; /*!< Default: 500 ms */ + int trigger_keep_time_ms_; /*!< Total trig time = TRIGGER_BASE_TIME + DEF_TRIGGER_KEEP_TIME, minimum: 1 */ + int delta_; /*!< Delta value: 0 ~ 1023, the larger the value, the shorter the distance */ + int gain_; /*!< Default: 9dB */ + int power_; /*!< In µA */ +}; + +} // namespace at581x +} // namespace esphome diff --git a/esphome/components/at581x/automation.h b/esphome/components/at581x/automation.h new file mode 100644 index 0000000000..4863a87565 --- /dev/null +++ b/esphome/components/at581x/automation.h @@ -0,0 +1,71 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include "at581x.h" + +namespace esphome { +namespace at581x { + +template class AT581XResetAction : public Action, public Parented { + public: + void play(Ts... x) { this->parent_->reset_hardware_frontend(); } +}; + +template class AT581XSettingsAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(int8_t, hw_frontend_reset) + TEMPLATABLE_VALUE(int, frequency) + TEMPLATABLE_VALUE(int, sensing_distance) + TEMPLATABLE_VALUE(int, poweron_selfcheck_time) + TEMPLATABLE_VALUE(int, power_consumption) + TEMPLATABLE_VALUE(int, protect_time) + TEMPLATABLE_VALUE(int, trigger_base) + TEMPLATABLE_VALUE(int, trigger_keep) + TEMPLATABLE_VALUE(int, stage_gain) + + void play(Ts... x) { + if (this->frequency_.has_value()) { + int v = this->frequency_.value(x...); + this->parent_->set_frequency(v); + } + if (this->sensing_distance_.has_value()) { + int v = this->sensing_distance_.value(x...); + this->parent_->set_sensing_distance(v); + } + if (this->poweron_selfcheck_time_.has_value()) { + int v = this->poweron_selfcheck_time_.value(x...); + this->parent_->set_poweron_selfcheck_time(v); + } + if (this->power_consumption_.has_value()) { + int v = this->power_consumption_.value(x...); + this->parent_->set_power_consumption(v); + } + if (this->protect_time_.has_value()) { + int v = this->protect_time_.value(x...); + this->parent_->set_protect_time(v); + } + if (this->trigger_base_.has_value()) { + int v = this->trigger_base_.value(x...); + this->parent_->set_trigger_base(v); + } + if (this->trigger_keep_.has_value()) { + int v = this->trigger_keep_.value(x...); + this->parent_->set_trigger_keep(v); + } + if (this->stage_gain_.has_value()) { + int v = this->stage_gain_.value(x...); + this->parent_->set_stage_gain(v); + } + + // This actually perform all the modification on the system + this->parent_->i2c_write_config(); + + if (this->hw_frontend_reset_.has_value() && this->hw_frontend_reset_.value(x...) == true) { + this->parent_->reset_hardware_frontend(); + } + } +}; +} // namespace at581x +} // namespace esphome diff --git a/esphome/components/at581x/switch/__init__.py b/esphome/components/at581x/switch/__init__.py new file mode 100644 index 0000000000..c441b381a3 --- /dev/null +++ b/esphome/components/at581x/switch/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_SWITCH, + ICON_WIFI, +) +from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns + +DEPENDENCIES = ["at581x"] + +RFSwitch = at581x_ns.class_("RFSwitch", switch.Switch) + +CONFIG_SCHEMA = switch.switch_schema( + RFSwitch, + device_class=DEVICE_CLASS_SWITCH, + icon=ICON_WIFI, +).extend( + cv.Schema( + { + cv.GenerateID(CONF_AT581X_ID): cv.use_id(AT581XComponent), + } + ) +) + + +async def to_code(config): + at581x_component = await cg.get_variable(config[CONF_AT581X_ID]) + s = await switch.new_switch(config) + await cg.register_parented(s, config[CONF_AT581X_ID]) + cg.add(at581x_component.set_rf_power_switch(s)) diff --git a/esphome/components/at581x/switch/rf_switch.cpp b/esphome/components/at581x/switch/rf_switch.cpp new file mode 100644 index 0000000000..f1d03dc8a5 --- /dev/null +++ b/esphome/components/at581x/switch/rf_switch.cpp @@ -0,0 +1,12 @@ +#include "rf_switch.h" + +namespace esphome { +namespace at581x { + +void RFSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_rf_mode(state); +} + +} // namespace at581x +} // namespace esphome diff --git a/esphome/components/at581x/switch/rf_switch.h b/esphome/components/at581x/switch/rf_switch.h new file mode 100644 index 0000000000..920ddbb66a --- /dev/null +++ b/esphome/components/at581x/switch/rf_switch.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../at581x.h" + +namespace esphome { +namespace at581x { + +class RFSwitch : public switch_::Switch, public Parented { + protected: + void write_state(bool state) override; +}; + +} // namespace at581x +} // namespace esphome diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py index 7baab51944..e86afa500d 100644 --- a/esphome/components/atc_mithermometer/sensor.py +++ b/esphome/components/atc_mithermometer/sensor.py @@ -83,18 +83,18 @@ async def to_code(config): cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) cg.add(var.set_temperature(sens)) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) cg.add(var.set_humidity(sens)) - if CONF_BATTERY_LEVEL in config: - sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + 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 CONF_BATTERY_VOLTAGE in config: - sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) + if battery_voltage_config := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(battery_voltage_config) cg.add(var.set_battery_voltage(sens)) - if CONF_SIGNAL_STRENGTH in config: - sens = await sensor.new_sensor(config[CONF_SIGNAL_STRENGTH]) + 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/atm90e26/__init__.py b/esphome/components/atm90e26/__init__.py new file mode 100644 index 0000000000..ac441a9c2d --- /dev/null +++ b/esphome/components/atm90e26/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@danieltwagner"] diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp new file mode 100644 index 0000000000..6743f1a442 --- /dev/null +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -0,0 +1,235 @@ +#include "atm90e26.h" +#include "atm90e26_reg.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace atm90e26 { + +static const char *const TAG = "atm90e26"; + +void ATM90E26Component::update() { + if (this->read16_(ATM90E26_REGISTER_FUNCEN) != 0x0030) { + this->status_set_warning(); + return; + } + + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(this->get_line_voltage_()); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(this->get_line_current_()); + } + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(this->get_active_power_()); + } + if (this->reactive_power_sensor_ != nullptr) { + this->reactive_power_sensor_->publish_state(this->get_reactive_power_()); + } + if (this->power_factor_sensor_ != nullptr) { + this->power_factor_sensor_->publish_state(this->get_power_factor_()); + } + if (this->forward_active_energy_sensor_ != nullptr) { + this->forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_()); + } + if (this->reverse_active_energy_sensor_ != nullptr) { + this->reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_()); + } + if (this->freq_sensor_ != nullptr) { + this->freq_sensor_->publish_state(this->get_frequency_()); + } + this->status_clear_warning(); +} + +void ATM90E26Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ATM90E26 Component..."); + this->spi_setup(); + + uint16_t mmode = 0x422; // default values for everything but L/N line current gains + mmode |= (gain_pga_ & 0x7) << 13; + mmode |= (n_line_gain_ & 0x3) << 11; + + this->write16_(ATM90E26_REGISTER_SOFTRESET, 0x789A); // Perform soft reset + this->write16_(ATM90E26_REGISTER_FUNCEN, + 0x0030); // Voltage sag irq=1, report on warnout pin=1, energy dir change irq=0 + uint16_t read = this->read16_(ATM90E26_REGISTER_LASTDATA); + if (read != 0x0030) { + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC, check SPI settings: %d", read); + this->mark_failed(); + return; + } + // TODO: 100 * * sqrt(2) * / (4 * gain_voltage/32768) + this->write16_(ATM90E26_REGISTER_SAGTH, 0x17DD); // Voltage sag threshhold 0x1F2F + + // Set metering calibration values + this->write16_(ATM90E26_REGISTER_CALSTART, 0x5678); // CAL Metering calibration startup command + + // Configure + this->write16_(ATM90E26_REGISTER_MMODE, mmode); // Metering Mode Configuration (see above) + + this->write16_(ATM90E26_REGISTER_PLCONSTH, (pl_const_ >> 16)); // PL Constant MSB + this->write16_(ATM90E26_REGISTER_PLCONSTL, pl_const_ & 0xFFFF); // PL Constant LSB + + // Calibrate this to be 1 pulse per Wh + this->write16_(ATM90E26_REGISTER_LGAIN, gain_metering_); // L Line Calibration Gain (active power metering) + this->write16_(ATM90E26_REGISTER_LPHI, 0x0000); // L Line Calibration Angle + this->write16_(ATM90E26_REGISTER_NGAIN, 0x0000); // N Line Calibration Gain + this->write16_(ATM90E26_REGISTER_NPHI, 0x0000); // N Line Calibration Angle + this->write16_(ATM90E26_REGISTER_PSTARTTH, 0x08BD); // Active Startup Power Threshold (default) = 2237 + this->write16_(ATM90E26_REGISTER_PNOLTH, 0x0000); // Active No-Load Power Threshold + this->write16_(ATM90E26_REGISTER_QSTARTTH, 0x0AEC); // Reactive Startup Power Threshold (default) = 2796 + this->write16_(ATM90E26_REGISTER_QNOLTH, 0x0000); // Reactive No-Load Power Threshold + + // Compute Checksum for the registers we set above + // low byte = sum of all bytes + uint16_t cs = + ((mmode >> 8) + (mmode & 0xFF) + (pl_const_ >> 24) + ((pl_const_ >> 16) & 0xFF) + ((pl_const_ >> 8) & 0xFF) + + (pl_const_ & 0xFF) + (gain_metering_ >> 8) + (gain_metering_ & 0xFF) + 0x08 + 0xBD + 0x0A + 0xEC) & + 0xFF; + // high byte = XOR of all bytes + cs |= ((mmode >> 8) ^ (mmode & 0xFF) ^ (pl_const_ >> 24) ^ ((pl_const_ >> 16) & 0xFF) ^ ((pl_const_ >> 8) & 0xFF) ^ + (pl_const_ & 0xFF) ^ (gain_metering_ >> 8) ^ (gain_metering_ & 0xFF) ^ 0x08 ^ 0xBD ^ 0x0A ^ 0xEC) + << 8; + + this->write16_(ATM90E26_REGISTER_CS1, cs); + ESP_LOGVV(TAG, "Set CS1 to: 0x%04X", cs); + + // Set measurement calibration values + this->write16_(ATM90E26_REGISTER_ADJSTART, 0x5678); // Measurement calibration startup command, registers 31-3A + this->write16_(ATM90E26_REGISTER_UGAIN, gain_voltage_); // Voltage RMS gain + this->write16_(ATM90E26_REGISTER_IGAINL, gain_ct_); // L line current RMS gain + this->write16_(ATM90E26_REGISTER_IGAINN, 0x7530); // N Line Current RMS Gain + this->write16_(ATM90E26_REGISTER_UOFFSET, 0x0000); // Voltage Offset + this->write16_(ATM90E26_REGISTER_IOFFSETL, 0x0000); // L Line Current Offset + this->write16_(ATM90E26_REGISTER_IOFFSETN, 0x0000); // N Line Current Offse + this->write16_(ATM90E26_REGISTER_POFFSETL, 0x0000); // L Line Active Power Offset + this->write16_(ATM90E26_REGISTER_QOFFSETL, 0x0000); // L Line Reactive Power Offset + this->write16_(ATM90E26_REGISTER_POFFSETN, 0x0000); // N Line Active Power Offset + this->write16_(ATM90E26_REGISTER_QOFFSETN, 0x0000); // N Line Reactive Power Offset + + // Compute Checksum for the registers we set above + cs = ((gain_voltage_ >> 8) + (gain_voltage_ & 0xFF) + (gain_ct_ >> 8) + (gain_ct_ & 0xFF) + 0x75 + 0x30) & 0xFF; + cs |= ((gain_voltage_ >> 8) ^ (gain_voltage_ & 0xFF) ^ (gain_ct_ >> 8) ^ (gain_ct_ & 0xFF) ^ 0x75 ^ 0x30) << 8; + this->write16_(ATM90E26_REGISTER_CS2, cs); + ESP_LOGVV(TAG, "Set CS2 to: 0x%04X", cs); + + this->write16_(ATM90E26_REGISTER_CALSTART, + 0x8765); // Checks correctness of 21-2B registers and starts normal metering if ok + this->write16_(ATM90E26_REGISTER_ADJSTART, + 0x8765); // Checks correctness of 31-3A registers and starts normal measurement if ok + + const uint16_t sys_status = this->read16_(ATM90E26_REGISTER_SYSSTATUS); + if (sys_status & 0xC000) { // Checksum 1 Error + + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS1 was incorrect, expected: 0x%04X", + this->read16_(ATM90E26_REGISTER_CS1)); + this->mark_failed(); + } + if (sys_status & 0x3000) { // Checksum 2 Error + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS2 was incorrect, expected: 0x%04X", + this->read16_(ATM90E26_REGISTER_CS2)); + this->mark_failed(); + } +} + +void ATM90E26Component::dump_config() { + ESP_LOGCONFIG("", "ATM90E26:"); + LOG_PIN(" CS Pin: ", this->cs_); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with ATM90E26 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Voltage A", this->voltage_sensor_); + LOG_SENSOR(" ", "Current A", this->current_sensor_); + LOG_SENSOR(" ", "Power A", this->power_sensor_); + LOG_SENSOR(" ", "Reactive Power A", this->reactive_power_sensor_); + LOG_SENSOR(" ", "PF A", this->power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy A", this->forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_); + LOG_SENSOR(" ", "Frequency", this->freq_sensor_); +} +float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; } + +uint16_t ATM90E26Component::read16_(uint8_t a_register) { + uint8_t data[2]; + uint16_t output; + + this->enable(); + delayMicroseconds(4); + this->write_byte(a_register | 0x80); + delayMicroseconds(4); + this->read_array(data, 2); + this->disable(); + + output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); + ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output); + return output; +} + +void ATM90E26Component::write16_(uint8_t a_register, uint16_t val) { + ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val); + this->enable(); + delayMicroseconds(4); + this->write_byte(a_register & 0x7F); + delayMicroseconds(4); + this->write_byte((val >> 8) & 0xFF); + this->write_byte(val & 0xFF); + this->disable(); +} + +float ATM90E26Component::get_line_current_() { + const uint16_t current = this->read16_(ATM90E26_REGISTER_IRMS); + return current / 1000.0f; +} + +float ATM90E26Component::get_line_voltage_() { + const uint16_t voltage = this->read16_(ATM90E26_REGISTER_URMS); + return voltage / 100.0f; +} + +float ATM90E26Component::get_active_power_() { + const int16_t val = this->read16_(ATM90E26_REGISTER_PMEAN); // two's complement + return (float) val; +} + +float ATM90E26Component::get_reactive_power_() { + const int16_t val = this->read16_(ATM90E26_REGISTER_QMEAN); // two's complement + return (float) val; +} + +float ATM90E26Component::get_power_factor_() { + const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed + if (val & 0x8000) { + return -(val & 0x7FF) / 1000.0f; + } else { + return val / 1000.0f; + } +} + +float ATM90E26Component::get_forward_active_energy_() { + const uint16_t val = this->read16_(ATM90E26_REGISTER_APENERGY); + if ((UINT32_MAX - this->cumulative_forward_active_energy_) > val) { + this->cumulative_forward_active_energy_ += val; + } else { + this->cumulative_forward_active_energy_ = val; + } + // The register holds thenths of pulses, we want to output Wh + return (this->cumulative_forward_active_energy_ * 100.0f / meter_constant_); +} + +float ATM90E26Component::get_reverse_active_energy_() { + const uint16_t val = this->read16_(ATM90E26_REGISTER_ANENERGY); + if (UINT32_MAX - this->cumulative_reverse_active_energy_ > val) { + this->cumulative_reverse_active_energy_ += val; + } else { + this->cumulative_reverse_active_energy_ = val; + } + return (this->cumulative_reverse_active_energy_ * 100.0f / meter_constant_); +} + +float ATM90E26Component::get_frequency_() { + const uint16_t freq = this->read16_(ATM90E26_REGISTER_FREQ); + return freq / 100.0f; +} + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h new file mode 100644 index 0000000000..3c098d7e91 --- /dev/null +++ b/esphome/components/atm90e26/atm90e26.h @@ -0,0 +1,72 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace atm90e26 { + +class ATM90E26Component : public PollingComponent, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; } + void set_current_sensor(sensor::Sensor *obj) { this->current_sensor_ = obj; } + void set_power_sensor(sensor::Sensor *obj) { this->power_sensor_ = obj; } + void set_reactive_power_sensor(sensor::Sensor *obj) { this->reactive_power_sensor_ = obj; } + void set_forward_active_energy_sensor(sensor::Sensor *obj) { this->forward_active_energy_sensor_ = obj; } + void set_reverse_active_energy_sensor(sensor::Sensor *obj) { this->reverse_active_energy_sensor_ = obj; } + void set_power_factor_sensor(sensor::Sensor *obj) { this->power_factor_sensor_ = obj; } + void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } + void set_line_freq(int freq) { line_freq_ = freq; } + void set_meter_constant(float val) { meter_constant_ = val; } + void set_pl_const(uint32_t pl_const) { pl_const_ = pl_const; } + void set_gain_metering(uint16_t gain) { this->gain_metering_ = gain; } + void set_gain_voltage(uint16_t gain) { this->gain_voltage_ = gain; } + void set_gain_ct(uint16_t gain) { this->gain_ct_ = gain; } + void set_gain_pga(uint16_t gain) { gain_pga_ = gain; } + void set_n_line_gain(uint16_t gain) { n_line_gain_ = gain; } + + protected: + uint16_t read16_(uint8_t a_register); + int read32_(uint8_t addr_h, uint8_t addr_l); + void write16_(uint8_t a_register, uint16_t val); + + float get_line_voltage_(); + float get_line_current_(); + float get_active_power_(); + float get_reactive_power_(); + float get_power_factor_(); + float get_forward_active_energy_(); + float get_reverse_active_energy_(); + float get_frequency_(); + float get_chip_temperature_(); + + sensor::Sensor *freq_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *forward_active_energy_sensor_{nullptr}; + sensor::Sensor *reverse_active_energy_sensor_{nullptr}; + uint32_t cumulative_forward_active_energy_{0}; + uint32_t cumulative_reverse_active_energy_{0}; + uint16_t gain_metering_{7481}; + uint16_t gain_voltage_{26400}; + uint16_t gain_ct_{31251}; + uint16_t gain_pga_{0x4}; + uint16_t n_line_gain_{0x2}; + int line_freq_{60}; + float meter_constant_{3200.0f}; + uint32_t pl_const_{1429876}; +}; + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/atm90e26_reg.h b/esphome/components/atm90e26/atm90e26_reg.h new file mode 100644 index 0000000000..0a925f424e --- /dev/null +++ b/esphome/components/atm90e26/atm90e26_reg.h @@ -0,0 +1,70 @@ +#pragma once + +namespace esphome { +namespace atm90e26 { + +/* Status and Special Register */ +static const uint8_t ATM90E26_REGISTER_SOFTRESET = 0x00; // Software Reset +static const uint8_t ATM90E26_REGISTER_SYSSTATUS = 0x01; // System Status +static const uint8_t ATM90E26_REGISTER_FUNCEN = 0x02; // Function Enable +static const uint8_t ATM90E26_REGISTER_SAGTH = 0x03; // Voltage Sag Threshold +static const uint8_t ATM90E26_REGISTER_SMALLPMOD = 0x04; // Small-Power Mode +static const uint8_t ATM90E26_REGISTER_LASTDATA = 0x06; // Last Read/Write SPI/UART Value + +/* Metering Calibration and Configuration Register */ +static const uint8_t ATM90E26_REGISTER_LSB = 0x08; // RMS/Power 16-bit LSB +static const uint8_t ATM90E26_REGISTER_CALSTART = 0x20; // Calibration Start Command +static const uint8_t ATM90E26_REGISTER_PLCONSTH = 0x21; // High Word of PL_Constant +static const uint8_t ATM90E26_REGISTER_PLCONSTL = 0x22; // Low Word of PL_Constant +static const uint8_t ATM90E26_REGISTER_LGAIN = 0x23; // L Line Calibration Gain +static const uint8_t ATM90E26_REGISTER_LPHI = 0x24; // L Line Calibration Angle +static const uint8_t ATM90E26_REGISTER_NGAIN = 0x25; // N Line Calibration Gain +static const uint8_t ATM90E26_REGISTER_NPHI = 0x26; // N Line Calibration Angle +static const uint8_t ATM90E26_REGISTER_PSTARTTH = 0x27; // Active Startup Power Threshold +static const uint8_t ATM90E26_REGISTER_PNOLTH = 0x28; // Active No-Load Power Threshold +static const uint8_t ATM90E26_REGISTER_QSTARTTH = 0x29; // Reactive Startup Power Threshold +static const uint8_t ATM90E26_REGISTER_QNOLTH = 0x2A; // Reactive No-Load Power Threshold +static const uint8_t ATM90E26_REGISTER_MMODE = 0x2B; // Metering Mode Configuration +static const uint8_t ATM90E26_REGISTER_CS1 = 0x2C; // Checksum 1 + +/* Measurement Calibration Register */ +static const uint8_t ATM90E26_REGISTER_ADJSTART = 0x30; // Measurement Calibration Start Command +static const uint8_t ATM90E26_REGISTER_UGAIN = 0x31; // Voltage RMS Gain +static const uint8_t ATM90E26_REGISTER_IGAINL = 0x32; // L Line Current RMS Gain +static const uint8_t ATM90E26_REGISTER_IGAINN = 0x33; // N Line Current RMS Gain +static const uint8_t ATM90E26_REGISTER_UOFFSET = 0x34; // Voltage Offset +static const uint8_t ATM90E26_REGISTER_IOFFSETL = 0x35; // L Line Current Offset +static const uint8_t ATM90E26_REGISTER_IOFFSETN = 0x36; // N Line Current Offse +static const uint8_t ATM90E26_REGISTER_POFFSETL = 0x37; // L Line Active Power Offset +static const uint8_t ATM90E26_REGISTER_QOFFSETL = 0x38; // L Line Reactive Power Offset +static const uint8_t ATM90E26_REGISTER_POFFSETN = 0x39; // N Line Active Power Offset +static const uint8_t ATM90E26_REGISTER_QOFFSETN = 0x3A; // N Line Reactive Power Offset +static const uint8_t ATM90E26_REGISTER_CS2 = 0x3B; // Checksum 2 + +/* Energy Register */ +static const uint8_t ATM90E26_REGISTER_APENERGY = 0x40; // Forward Active Energy +static const uint8_t ATM90E26_REGISTER_ANENERGY = 0x41; // Reverse Active Energy +static const uint8_t ATM90E26_REGISTER_ATENERGY = 0x42; // Absolute Active Energy +static const uint8_t ATM90E26_REGISTER_RPENERGY = 0x43; // Forward (Inductive) Reactive Energy +static const uint8_t ATM90E26_REGISTER_RNENERG = 0x44; // Reverse (Capacitive) Reactive Energy +static const uint8_t ATM90E26_REGISTER_RTENERGY = 0x45; // Absolute Reactive Energy +static const uint8_t ATM90E26_REGISTER_ENSTATUS = 0x46; // Metering Status + +/* Measurement Register */ +static const uint8_t ATM90E26_REGISTER_IRMS = 0x48; // L Line Current RMS +static const uint8_t ATM90E26_REGISTER_URMS = 0x49; // Voltage RMS +static const uint8_t ATM90E26_REGISTER_PMEAN = 0x4A; // L Line Mean Active Power +static const uint8_t ATM90E26_REGISTER_QMEAN = 0x4B; // L Line Mean Reactive Power +static const uint8_t ATM90E26_REGISTER_FREQ = 0x4C; // Voltage Frequency +static const uint8_t ATM90E26_REGISTER_POWERF = 0x4D; // L Line Power Factor +static const uint8_t ATM90E26_REGISTER_PANGLE = 0x4E; // Phase Angle between Voltage and L Line Current +static const uint8_t ATM90E26_REGISTER_SMEAN = 0x4F; // L Line Mean Apparent Power +static const uint8_t ATM90E26_REGISTER_IRMS2 = 0x68; // N Line Current rms +static const uint8_t ATM90E26_REGISTER_PMEAN2 = 0x6A; // N Line Mean Active Power +static const uint8_t ATM90E26_REGISTER_QMEAN2 = 0x6B; // N Line Mean Reactive Power +static const uint8_t ATM90E26_REGISTER_POWERF2 = 0x6D; // N Line Power Factor +static const uint8_t ATM90E26_REGISTER_PANGLE2 = 0x6E; // Phase Angle between Voltage and N Line Current +static const uint8_t ATM90E26_REGISTER_SMEAN2 = 0x6F; // N Line Mean Apparent Power + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py new file mode 100644 index 0000000000..42ef259100 --- /dev/null +++ b/esphome/components/atm90e26/sensor.py @@ -0,0 +1,157 @@ +import esphome.codegen as cg +from esphome.components import sensor, spi +import esphome.config_validation as cv +from esphome.const import ( + CONF_CURRENT, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_LINE_FREQUENCY, + CONF_POWER, + CONF_POWER_FACTOR, + CONF_REACTIVE_POWER, + CONF_REVERSE_ACTIVE_ENERGY, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_LIGHTBULB, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, + UNIT_WATT_HOURS, +) + +CONF_METER_CONSTANT = "meter_constant" +CONF_PL_CONST = "pl_const" +CONF_GAIN_PGA = "gain_pga" +CONF_GAIN_METERING = "gain_metering" +CONF_GAIN_VOLTAGE = "gain_voltage" +CONF_GAIN_CT = "gain_ct" +LINE_FREQS = { + "50HZ": 50, + "60HZ": 60, +} +PGA_GAINS = { + "1X": 0x4, + "4X": 0x0, + "8X": 0x1, + "16X": 0x2, + "24X": 0x3, +} + +atm90e26_ns = cg.esphome_ns.namespace("atm90e26") +ATM90E26Component = atm90e26_ns.class_( + "ATM90E26Component", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ATM90E26Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + 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=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + icon=ICON_LIGHTBULB, + accuracy_decimals=2, + 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_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), + cv.Required(CONF_METER_CONSTANT): cv.positive_float, + cv.Optional(CONF_PL_CONST, default=1429876): cv.uint32_t, + cv.Optional(CONF_GAIN_METERING, default=7481): cv.uint16_t, + cv.Optional(CONF_GAIN_VOLTAGE, default=26400): cv.int_range( + min=0, max=32767 + ), + cv.Optional(CONF_GAIN_CT, default=31251): cv.uint16_t, + cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()) +) + + +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) + + 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 reactive_power_config := config.get(CONF_REACTIVE_POWER): + sens = await sensor.new_sensor(reactive_power_config) + cg.add(var.set_reactive_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)) + if forward_active_energy_config := config.get(CONF_FORWARD_ACTIVE_ENERGY): + sens = await sensor.new_sensor(forward_active_energy_config) + cg.add(var.set_forward_active_energy_sensor(sens)) + if reverse_active_energy_config := config.get(CONF_REVERSE_ACTIVE_ENERGY): + sens = await sensor.new_sensor(reverse_active_energy_config) + cg.add(var.set_reverse_active_energy_sensor(sens)) + if frequency_config := config.get(CONF_FREQUENCY): + sens = await sensor.new_sensor(frequency_config) + cg.add(var.set_freq_sensor(sens)) + cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) + cg.add(var.set_meter_constant(config[CONF_METER_CONSTANT])) + cg.add(var.set_pl_const(config[CONF_PL_CONST])) + cg.add(var.set_gain_metering(config[CONF_GAIN_METERING])) + cg.add(var.set_gain_voltage(config[CONF_GAIN_VOLTAGE])) + cg.add(var.set_gain_ct(config[CONF_GAIN_CT])) + cg.add(var.set_gain_pga(config[CONF_GAIN_PGA])) diff --git a/esphome/components/atm90e32/__init__.py b/esphome/components/atm90e32/__init__.py index e69de29bb2..8ce95be489 100644 --- a/esphome/components/atm90e32/__init__.py +++ b/esphome/components/atm90e32/__init__.py @@ -0,0 +1,7 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@circuitsetup", "@descipher"] + +atm90e32_ns = cg.esphome_ns.namespace("atm90e32") + +CONF_ATM90E32_ID = "atm90e32_id" diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index e4b8448da6..43647b1855 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -1,94 +1,208 @@ #include "atm90e32.h" #include "atm90e32_reg.h" #include "esphome/core/log.h" +#include namespace esphome { namespace atm90e32 { static const char *const TAG = "atm90e32"; +void ATM90E32Component::loop() { + if (this->get_publish_interval_flag_()) { + this->set_publish_interval_flag_(false); + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].voltage_sensor_ != nullptr) { + this->phase_[phase].voltage_ = this->get_phase_voltage_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].current_sensor_ != nullptr) { + this->phase_[phase].current_ = this->get_phase_current_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].power_sensor_ != nullptr) { + this->phase_[phase].active_power_ = this->get_phase_active_power_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].power_factor_sensor_ != nullptr) { + this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].reactive_power_sensor_ != nullptr) { + this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) { + this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) { + this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].phase_angle_sensor_ != nullptr) { + this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) { + this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].peak_current_sensor_ != nullptr) { + this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase); + } + } + // After the local store in collected we can publish them trusting they are withing +-1 haardware sampling + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].voltage_sensor_ != nullptr) { + this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].current_sensor_ != nullptr) { + this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].power_sensor_ != nullptr) { + this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].power_factor_sensor_ != nullptr) { + this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].reactive_power_sensor_ != nullptr) { + this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) { + this->phase_[phase].forward_active_energy_sensor_->publish_state( + this->get_local_phase_forward_active_energy_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) { + this->phase_[phase].reverse_active_energy_sensor_->publish_state( + this->get_local_phase_reverse_active_energy_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].phase_angle_sensor_ != nullptr) { + this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) { + this->phase_[phase].harmonic_active_power_sensor_->publish_state( + this->get_local_phase_harmonic_active_power_(phase)); + } + } + for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].peak_current_sensor_ != nullptr) { + this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase)); + } + } + if (this->freq_sensor_ != nullptr) { + this->freq_sensor_->publish_state(this->get_frequency_()); + } + if (this->chip_temperature_sensor_ != nullptr) { + this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_()); + } + } +} void ATM90E32Component::update() { if (this->read16_(ATM90E32_REGISTER_METEREN) != 1) { this->status_set_warning(); return; } - - if (this->phase_[0].voltage_sensor_ != nullptr) { - this->phase_[0].voltage_sensor_->publish_state(this->get_line_voltage_a_()); - } - if (this->phase_[1].voltage_sensor_ != nullptr) { - this->phase_[1].voltage_sensor_->publish_state(this->get_line_voltage_b_()); - } - if (this->phase_[2].voltage_sensor_ != nullptr) { - this->phase_[2].voltage_sensor_->publish_state(this->get_line_voltage_c_()); - } - if (this->phase_[0].current_sensor_ != nullptr) { - this->phase_[0].current_sensor_->publish_state(this->get_line_current_a_()); - } - if (this->phase_[1].current_sensor_ != nullptr) { - this->phase_[1].current_sensor_->publish_state(this->get_line_current_b_()); - } - if (this->phase_[2].current_sensor_ != nullptr) { - this->phase_[2].current_sensor_->publish_state(this->get_line_current_c_()); - } - if (this->phase_[0].power_sensor_ != nullptr) { - this->phase_[0].power_sensor_->publish_state(this->get_active_power_a_()); - } - if (this->phase_[1].power_sensor_ != nullptr) { - this->phase_[1].power_sensor_->publish_state(this->get_active_power_b_()); - } - if (this->phase_[2].power_sensor_ != nullptr) { - this->phase_[2].power_sensor_->publish_state(this->get_active_power_c_()); - } - if (this->phase_[0].reactive_power_sensor_ != nullptr) { - this->phase_[0].reactive_power_sensor_->publish_state(this->get_reactive_power_a_()); - } - if (this->phase_[1].reactive_power_sensor_ != nullptr) { - this->phase_[1].reactive_power_sensor_->publish_state(this->get_reactive_power_b_()); - } - if (this->phase_[2].reactive_power_sensor_ != nullptr) { - this->phase_[2].reactive_power_sensor_->publish_state(this->get_reactive_power_c_()); - } - if (this->phase_[0].power_factor_sensor_ != nullptr) { - this->phase_[0].power_factor_sensor_->publish_state(this->get_power_factor_a_()); - } - if (this->phase_[1].power_factor_sensor_ != nullptr) { - this->phase_[1].power_factor_sensor_->publish_state(this->get_power_factor_b_()); - } - if (this->phase_[2].power_factor_sensor_ != nullptr) { - this->phase_[2].power_factor_sensor_->publish_state(this->get_power_factor_c_()); - } - if (this->phase_[0].forward_active_energy_sensor_ != nullptr) { - this->phase_[0].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_a_()); - } - if (this->phase_[1].forward_active_energy_sensor_ != nullptr) { - this->phase_[1].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_b_()); - } - if (this->phase_[2].forward_active_energy_sensor_ != nullptr) { - this->phase_[2].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_c_()); - } - if (this->phase_[0].reverse_active_energy_sensor_ != nullptr) { - this->phase_[0].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_a_()); - } - if (this->phase_[1].reverse_active_energy_sensor_ != nullptr) { - this->phase_[1].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_b_()); - } - if (this->phase_[2].reverse_active_energy_sensor_ != nullptr) { - this->phase_[2].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_c_()); - } - if (this->freq_sensor_ != nullptr) { - this->freq_sensor_->publish_state(this->get_frequency_()); - } - if (this->chip_temperature_sensor_ != nullptr) { - this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_()); - } + this->set_publish_interval_flag_(true); this->status_clear_warning(); } +void ATM90E32Component::restore_calibrations_() { + if (enable_offset_calibration_) { + this->pref_.load(&this->offset_phase_); + } +}; + +void ATM90E32Component::run_offset_calibrations() { + // Run the calibrations and + // Setup voltage and current calibration offsets for PHASE A + this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); + this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); + this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset + // Setup voltage and current calibration offsets for PHASE B + this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); + this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); + this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset + // Setup voltage and current calibration offsets for PHASE C + this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); + this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); + this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset + this->pref_.save(&this->offset_phase_); + ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, + this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); + ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, + this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); +} + +void ATM90E32Component::clear_offset_calibrations() { + // Clear the calibrations and + this->offset_phase_[PHASEA].voltage_offset_ = 0; + this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEA].current_offset_ = 0; + this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset + this->offset_phase_[PHASEB].voltage_offset_ = 0; + this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEB].current_offset_ = 0; + this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset + this->offset_phase_[PHASEC].voltage_offset_ = 0; + this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEC].current_offset_ = 0; + this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset + this->pref_.save(&this->offset_phase_); + ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, + this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); + ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, + this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); +} + void ATM90E32Component::setup() { ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); this->spi_setup(); - + if (this->enable_offset_calibration_) { + uint32_t hash = fnv1_hash(App.get_friendly_name()); + this->pref_ = global_preferences->make_preference(hash, true); + this->restore_calibrations_(); + } uint16_t mmode0 = 0x87; // 3P4W 50Hz if (line_freq_ == 60) { mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz @@ -100,29 +214,36 @@ void ATM90E32Component::setup() { } this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset + delay(6); // Wait for the minimum 5ms + 1ms this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access - this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x0001) { + if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x55AA) { ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); this->mark_failed(); return; } - this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000 - this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default) - this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config - this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) - this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels - this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500 - this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% - this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 - this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% - this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[0].volt_gain_); // A Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[0].ct_gain_); // A line current gain - this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[1].volt_gain_); // B Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[1].ct_gain_); // B line current gain - this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[2].volt_gain_); // C Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[2].ct_gain_); // C line current gain - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration + + this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering + this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time ms (15:8), Sag Period ms (7:0) + this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000 + this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default) + this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config + this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) + this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels + this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500 + this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% + this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% + this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 + this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% + // Setup voltage and current gain for PHASE A + this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain + this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain + // Setup voltage and current gain for PHASE B + this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain + this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain + // Setup voltage and current gain for PHASE C + this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain + this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration } void ATM90E32Component::dump_config() { @@ -132,198 +253,244 @@ void ATM90E32Component::dump_config() { ESP_LOGE(TAG, "Communication with ATM90E32 failed!"); } LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "Voltage A", this->phase_[0].voltage_sensor_); - LOG_SENSOR(" ", "Current A", this->phase_[0].current_sensor_); - LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_); - LOG_SENSOR(" ", "Reactive Power A", this->phase_[0].reactive_power_sensor_); - LOG_SENSOR(" ", "PF A", this->phase_[0].power_factor_sensor_); - LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[0].forward_active_energy_sensor_); - LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[0].reverse_active_energy_sensor_); - LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_); - LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_); - LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_); - LOG_SENSOR(" ", "Reactive Power B", this->phase_[1].reactive_power_sensor_); - LOG_SENSOR(" ", "PF B", this->phase_[1].power_factor_sensor_); - LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[1].forward_active_energy_sensor_); - LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[1].reverse_active_energy_sensor_); - LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_); - LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_); - LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_); - LOG_SENSOR(" ", "Reactive Power C", this->phase_[2].reactive_power_sensor_); - LOG_SENSOR(" ", "PF C", this->phase_[2].power_factor_sensor_); - LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[2].forward_active_energy_sensor_); - LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[2].reverse_active_energy_sensor_); + LOG_SENSOR(" ", "Voltage A", this->phase_[PHASEA].voltage_sensor_); + LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_); + LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_); + LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_); + LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_); + LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEA].harmonic_active_power_sensor_); + LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEA].phase_angle_sensor_); + LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEA].peak_current_sensor_); + LOG_SENSOR(" ", "Voltage B", this->phase_[PHASEB].voltage_sensor_); + LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_); + LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_); + LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_); + LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_); + LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEB].harmonic_active_power_sensor_); + LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEB].phase_angle_sensor_); + LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEB].peak_current_sensor_); + LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_); + LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_); + LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_); + LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_); + LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_); + LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEC].harmonic_active_power_sensor_); + LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEC].phase_angle_sensor_); + LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEC].peak_current_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); } -float ATM90E32Component::get_setup_priority() const { return setup_priority::DATA; } +float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; } + +// R/C registers can conly be cleared after the LastSPIData register is updated (register 78H) +// Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period +// Default is 143FH (20ms, 63ms) uint16_t ATM90E32Component::read16_(uint16_t a_register) { uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03); uint8_t addrl = (a_register & 0xFF); uint8_t data[2]; uint16_t output; - this->enable(); - delayMicroseconds(10); + delay_microseconds_safe(10); this->write_byte(addrh); this->write_byte(addrl); - delayMicroseconds(4); this->read_array(data, 2); this->disable(); output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); - ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output); + ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output); return output; } int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { - uint16_t val_h = this->read16_(addr_h); - uint16_t val_l = this->read16_(addr_l); - int32_t val = (val_h << 16) | val_l; + const uint16_t val_h = this->read16_(addr_h); + const uint16_t val_l = this->read16_(addr_l); + const int32_t val = (val_h << 16) | val_l; - ESP_LOGVV(TAG, "read32_ addr_h 0x%04X val_h 0x%04X addr_l 0x%04X val_l 0x%04X = %d", addr_h, val_h, addr_l, val_l, - val); + ESP_LOGVV(TAG, + "read32_ addr_h 0x%04" PRIX16 " val_h 0x%04" PRIX16 " addr_l 0x%04" PRIX16 " val_l 0x%04" PRIX16 + " = %" PRId32, + addr_h, val_h, addr_l, val_l, val); return val; } void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) { - uint8_t addrh = (a_register >> 8) & 0x03; - uint8_t addrl = (a_register & 0xFF); - - ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val); + ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val); this->enable(); - delayMicroseconds(10); - this->write_byte(addrh); - this->write_byte(addrl); - delayMicroseconds(4); - this->write_byte((val >> 8) & 0xff); - this->write_byte(val & 0xFF); + this->write_byte16(a_register); + this->write_byte16(val); this->disable(); + if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != val) + ESP_LOGW(TAG, "SPI write error 0x%04X val 0x%04X", a_register, val); } -float ATM90E32Component::get_line_voltage_a_() { - uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSA); +float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } + +float ATM90E32Component::get_local_phase_current_(uint8_t phase) { return this->phase_[phase].current_; } + +float ATM90E32Component::get_local_phase_active_power_(uint8_t phase) { return this->phase_[phase].active_power_; } + +float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; } + +float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; } + +float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) { + return this->phase_[phase].forward_active_energy_; +} + +float ATM90E32Component::get_local_phase_reverse_active_energy_(uint8_t phase) { + return this->phase_[phase].reverse_active_energy_; +} + +float ATM90E32Component::get_local_phase_angle_(uint8_t phase) { return this->phase_[phase].phase_angle_; } + +float ATM90E32Component::get_local_phase_harmonic_active_power_(uint8_t phase) { + return this->phase_[phase].harmonic_active_power_; +} + +float ATM90E32Component::get_local_phase_peak_current_(uint8_t phase) { return this->phase_[phase].peak_current_; } + +float ATM90E32Component::get_phase_voltage_(uint8_t phase) { + const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); + if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) + ESP_LOGW(TAG, "SPI URMS voltage register read error."); return (float) voltage / 100; } -float ATM90E32Component::get_line_voltage_b_() { - uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSB); - return (float) voltage / 100; + +float ATM90E32Component::get_phase_voltage_avg_(uint8_t phase) { + const uint8_t reads = 10; + uint32_t accumulation = 0; + uint16_t voltage = 0; + for (uint8_t i = 0; i < reads; i++) { + voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); + if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) + ESP_LOGW(TAG, "SPI URMS voltage register read error."); + accumulation += voltage; + } + voltage = accumulation / reads; + this->phase_[phase].voltage_ = (float) voltage / 100; + return this->phase_[phase].voltage_; } -float ATM90E32Component::get_line_voltage_c_() { - uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSC); - return (float) voltage / 100; + +float ATM90E32Component::get_phase_current_avg_(uint8_t phase) { + const uint8_t reads = 10; + uint32_t accumulation = 0; + uint16_t current = 0; + for (uint8_t i = 0; i < reads; i++) { + current = this->read16_(ATM90E32_REGISTER_IRMS + phase); + if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) + ESP_LOGW(TAG, "SPI IRMS current register read error."); + accumulation += current; + } + current = accumulation / reads; + this->phase_[phase].current_ = (float) current / 1000; + return this->phase_[phase].current_; } -float ATM90E32Component::get_line_current_a_() { - uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSA); + +float ATM90E32Component::get_phase_current_(uint8_t phase) { + const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase); + if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) + ESP_LOGW(TAG, "SPI IRMS current register read error."); return (float) current / 1000; } -float ATM90E32Component::get_line_current_b_() { - uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSB); - return (float) current / 1000; -} -float ATM90E32Component::get_line_current_c_() { - uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSC); - return (float) current / 1000; -} -float ATM90E32Component::get_active_power_a_() { - int val = this->read32_(ATM90E32_REGISTER_PMEANA, ATM90E32_REGISTER_PMEANALSB); + +float ATM90E32Component::get_phase_active_power_(uint8_t phase) { + const int val = this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase); return val * 0.00032f; } -float ATM90E32Component::get_active_power_b_() { - int val = this->read32_(ATM90E32_REGISTER_PMEANB, ATM90E32_REGISTER_PMEANBLSB); + +float ATM90E32Component::get_phase_reactive_power_(uint8_t phase) { + const int val = this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase); return val * 0.00032f; } -float ATM90E32Component::get_active_power_c_() { - int val = this->read32_(ATM90E32_REGISTER_PMEANC, ATM90E32_REGISTER_PMEANCLSB); + +float ATM90E32Component::get_phase_power_factor_(uint8_t phase) { + const int16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); + if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != powerfactor) + ESP_LOGW(TAG, "SPI power factor read error."); + return (float) powerfactor / 1000; +} + +float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) { + const uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGY + phase); + if ((UINT32_MAX - this->phase_[phase].cumulative_forward_active_energy_) > val) { + this->phase_[phase].cumulative_forward_active_energy_ += val; + } else { + this->phase_[phase].cumulative_forward_active_energy_ = val; + } + return ((float) this->phase_[phase].cumulative_forward_active_energy_ * 10 / 3200); +} + +float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) { + const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY); + if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) { + this->phase_[phase].cumulative_reverse_active_energy_ += val; + } else { + this->phase_[phase].cumulative_reverse_active_energy_ = val; + } + return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * 10 / 3200); +} + +float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { + int val = this->read32_(ATM90E32_REGISTER_PMEANH + phase, ATM90E32_REGISTER_PMEANHLSB + phase); return val * 0.00032f; } -float ATM90E32Component::get_reactive_power_a_() { - int val = this->read32_(ATM90E32_REGISTER_QMEANA, ATM90E32_REGISTER_QMEANALSB); - return val * 0.00032f; + +float ATM90E32Component::get_phase_angle_(uint8_t phase) { + uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0; + return (float) (val > 180) ? val - 360.0 : val; } -float ATM90E32Component::get_reactive_power_b_() { - int val = this->read32_(ATM90E32_REGISTER_QMEANB, ATM90E32_REGISTER_QMEANBLSB); - return val * 0.00032f; -} -float ATM90E32Component::get_reactive_power_c_() { - int val = this->read32_(ATM90E32_REGISTER_QMEANC, ATM90E32_REGISTER_QMEANCLSB); - return val * 0.00032f; -} -float ATM90E32Component::get_power_factor_a_() { - int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANA); - return (float) pf / 1000; -} -float ATM90E32Component::get_power_factor_b_() { - int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANB); - return (float) pf / 1000; -} -float ATM90E32Component::get_power_factor_c_() { - int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANC); - return (float) pf / 1000; -} -float ATM90E32Component::get_forward_active_energy_a_() { - uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYA); - if ((UINT32_MAX - this->phase_[0].cumulative_forward_active_energy_) > val) { - this->phase_[0].cumulative_forward_active_energy_ += val; - } else { - this->phase_[0].cumulative_forward_active_energy_ = val; - } - return ((float) this->phase_[0].cumulative_forward_active_energy_ * 10 / 3200); -} -float ATM90E32Component::get_forward_active_energy_b_() { - uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYB); - if (UINT32_MAX - this->phase_[1].cumulative_forward_active_energy_ > val) { - this->phase_[1].cumulative_forward_active_energy_ += val; - } else { - this->phase_[1].cumulative_forward_active_energy_ = val; - } - return ((float) this->phase_[1].cumulative_forward_active_energy_ * 10 / 3200); -} -float ATM90E32Component::get_forward_active_energy_c_() { - uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYC); - if (UINT32_MAX - this->phase_[2].cumulative_forward_active_energy_ > val) { - this->phase_[2].cumulative_forward_active_energy_ += val; - } else { - this->phase_[2].cumulative_forward_active_energy_ = val; - } - return ((float) this->phase_[2].cumulative_forward_active_energy_ * 10 / 3200); -} -float ATM90E32Component::get_reverse_active_energy_a_() { - uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYA); - if (UINT32_MAX - this->phase_[0].cumulative_reverse_active_energy_ > val) { - this->phase_[0].cumulative_reverse_active_energy_ += val; - } else { - this->phase_[0].cumulative_reverse_active_energy_ = val; - } - return ((float) this->phase_[0].cumulative_reverse_active_energy_ * 10 / 3200); -} -float ATM90E32Component::get_reverse_active_energy_b_() { - uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYB); - if (UINT32_MAX - this->phase_[1].cumulative_reverse_active_energy_ > val) { - this->phase_[1].cumulative_reverse_active_energy_ += val; - } else { - this->phase_[1].cumulative_reverse_active_energy_ = val; - } - return ((float) this->phase_[1].cumulative_reverse_active_energy_ * 10 / 3200); -} -float ATM90E32Component::get_reverse_active_energy_c_() { - uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYC); - if (UINT32_MAX - this->phase_[2].cumulative_reverse_active_energy_ > val) { - this->phase_[2].cumulative_reverse_active_energy_ += val; - } else { - this->phase_[2].cumulative_reverse_active_energy_ = val; - } - return ((float) this->phase_[2].cumulative_reverse_active_energy_ * 10 / 3200); + +float ATM90E32Component::get_phase_peak_current_(uint8_t phase) { + int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase); + if (!this->peak_current_signed_) + val = abs(val); + // phase register * phase current gain value / 1000 * 2^13 + return (float) (val * this->phase_[phase].ct_gain_ / 8192000.0); } + float ATM90E32Component::get_frequency_() { - uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); + const uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); return (float) freq / 100; } + float ATM90E32Component::get_chip_temperature_() { - uint16_t ctemp = this->read16_(ATM90E32_REGISTER_TEMP); + const uint16_t ctemp = this->read16_(ATM90E32_REGISTER_TEMP); return (float) ctemp; } + +uint16_t ATM90E32Component::calibrate_voltage_offset_phase(uint8_t phase) { + const uint8_t num_reads = 5; + uint64_t total_value = 0; + for (int i = 0; i < num_reads; ++i) { + const uint32_t measurement_value = read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase); + total_value += measurement_value; + } + const uint32_t average_value = total_value / num_reads; + const uint32_t shifted_value = average_value >> 7; + const uint32_t voltage_offset = ~shifted_value + 1; + return voltage_offset & 0xFFFF; // Take the lower 16 bits +} + +uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) { + const uint8_t num_reads = 5; + uint64_t total_value = 0; + for (int i = 0; i < num_reads; ++i) { + const uint32_t measurement_value = read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase); + total_value += measurement_value; + } + const uint32_t average_value = total_value / num_reads; + const uint32_t current_offset = ~average_value + 1; + return current_offset & 0xFFFF; // Take the lower 16 bits +} + } // namespace atm90e32 } // namespace esphome diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index c9662df26e..35c61d1e05 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -1,25 +1,33 @@ #pragma once -#include "esphome/core/component.h" +#include "atm90e32_reg.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" +#include "esphome/core/application.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" namespace esphome { namespace atm90e32 { class ATM90E32Component : public PollingComponent, public spi::SPIDevice { + spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> { public: + static const uint8_t PHASEA = 0; + static const uint8_t PHASEB = 1; + static const uint8_t PHASEC = 2; + void loop() override; void setup() override; void dump_config() override; float get_setup_priority() const override; void update() override; - void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; } void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } void set_reactive_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].reactive_power_sensor_ = obj; } + void set_apparent_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].apparent_power_sensor_ = obj; } void set_forward_active_energy_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].forward_active_energy_sensor_ = obj; } @@ -27,64 +35,105 @@ class ATM90E32Component : public PollingComponent, this->phase_[phase].reverse_active_energy_sensor_ = obj; } void set_power_factor_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_factor_sensor_ = obj; } - void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; } + void set_phase_angle_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].phase_angle_sensor_ = obj; } + void set_harmonic_active_power_sensor(int phase, sensor::Sensor *obj) { + this->phase_[phase].harmonic_active_power_sensor_ = obj; + } + void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; } + void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } - void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } + void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) { chip_temperature_sensor_ = chip_temperature_sensor; } void set_line_freq(int freq) { line_freq_ = freq; } void set_current_phases(int phases) { current_phases_ = phases; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } + void run_offset_calibrations(); + void clear_offset_calibrations(); + void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } + uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); + uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); + int32_t last_periodic_millis = millis(); protected: uint16_t read16_(uint16_t a_register); int read32_(uint16_t addr_h, uint16_t addr_l); void write16_(uint16_t a_register, uint16_t val); - - float get_line_voltage_a_(); - float get_line_voltage_b_(); - float get_line_voltage_c_(); - float get_line_current_a_(); - float get_line_current_b_(); - float get_line_current_c_(); - float get_active_power_a_(); - float get_active_power_b_(); - float get_active_power_c_(); - float get_reactive_power_a_(); - float get_reactive_power_b_(); - float get_reactive_power_c_(); - float get_power_factor_a_(); - float get_power_factor_b_(); - float get_power_factor_c_(); - float get_forward_active_energy_a_(); - float get_forward_active_energy_b_(); - float get_forward_active_energy_c_(); - float get_reverse_active_energy_a_(); - float get_reverse_active_energy_b_(); - float get_reverse_active_energy_c_(); + float get_local_phase_voltage_(uint8_t /*phase*/); + float get_local_phase_current_(uint8_t /*phase*/); + float get_local_phase_active_power_(uint8_t /*phase*/); + float get_local_phase_reactive_power_(uint8_t /*phase*/); + float get_local_phase_power_factor_(uint8_t /*phase*/); + float get_local_phase_forward_active_energy_(uint8_t /*phase*/); + float get_local_phase_reverse_active_energy_(uint8_t /*phase*/); + float get_local_phase_angle_(uint8_t /*phase*/); + float get_local_phase_harmonic_active_power_(uint8_t /*phase*/); + float get_local_phase_peak_current_(uint8_t /*phase*/); + float get_phase_voltage_(uint8_t /*phase*/); + float get_phase_voltage_avg_(uint8_t /*phase*/); + float get_phase_current_(uint8_t /*phase*/); + float get_phase_current_avg_(uint8_t /*phase*/); + float get_phase_active_power_(uint8_t /*phase*/); + float get_phase_reactive_power_(uint8_t /*phase*/); + float get_phase_power_factor_(uint8_t /*phase*/); + float get_phase_forward_active_energy_(uint8_t /*phase*/); + float get_phase_reverse_active_energy_(uint8_t /*phase*/); + float get_phase_angle_(uint8_t /*phase*/); + float get_phase_harmonic_active_power_(uint8_t /*phase*/); + float get_phase_peak_current_(uint8_t /*phase*/); float get_frequency_(); float get_chip_temperature_(); + bool get_publish_interval_flag_() { return publish_interval_flag_; }; + void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; + void restore_calibrations_(); struct ATM90E32Phase { - uint16_t volt_gain_{7305}; - uint16_t ct_gain_{27961}; + uint16_t voltage_gain_{0}; + uint16_t ct_gain_{0}; + uint16_t voltage_offset_{0}; + uint16_t current_offset_{0}; + float voltage_{0}; + float current_{0}; + float active_power_{0}; + float reactive_power_{0}; + float power_factor_{0}; + float forward_active_energy_{0}; + float reverse_active_energy_{0}; + float phase_angle_{0}; + float harmonic_active_power_{0}; + float peak_current_{0}; sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *apparent_power_sensor_{nullptr}; sensor::Sensor *power_factor_sensor_{nullptr}; sensor::Sensor *forward_active_energy_sensor_{nullptr}; sensor::Sensor *reverse_active_energy_sensor_{nullptr}; + sensor::Sensor *phase_angle_sensor_{nullptr}; + sensor::Sensor *harmonic_active_power_sensor_{nullptr}; + sensor::Sensor *peak_current_sensor_{nullptr}; uint32_t cumulative_forward_active_energy_{0}; uint32_t cumulative_reverse_active_energy_{0}; } phase_[3]; + + struct Calibration { + uint16_t voltage_offset_{0}; + uint16_t current_offset_{0}; + } offset_phase_[3]; + + ESPPreferenceObject pref_; + sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr}; uint16_t pga_gain_{0x15}; int line_freq_{60}; int current_phases_{3}; + bool publish_interval_flag_{false}; + bool peak_current_signed_{false}; + bool enable_offset_calibration_{false}; }; } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index 7927a7fdfb..954fb42e79 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -1,5 +1,7 @@ #pragma once +#include + namespace esphome { namespace atm90e32 { @@ -131,10 +133,12 @@ static const uint16_t ATM90E32_REGISTER_IOFFSETN = 0x6E; // N Current Offset /* ENERGY REGISTERS */ static const uint16_t ATM90E32_REGISTER_APENERGYT = 0x80; // Total Forward Active +static const uint16_t ATM90E32_REGISTER_APENERGY = 0x81; // Forward Active Reg Base static const uint16_t ATM90E32_REGISTER_APENERGYA = 0x81; // A Forward Active static const uint16_t ATM90E32_REGISTER_APENERGYB = 0x82; // B Forward Active static const uint16_t ATM90E32_REGISTER_APENERGYC = 0x83; // C Forward Active static const uint16_t ATM90E32_REGISTER_ANENERGYT = 0x84; // Total Reverse Active +static const uint16_t ATM90E32_REGISTER_ANENERGY = 0x85; // Reverse Active Reg Base static const uint16_t ATM90E32_REGISTER_ANENERGYA = 0x85; // A Reverse Active static const uint16_t ATM90E32_REGISTER_ANENERGYB = 0x86; // B Reverse Active static const uint16_t ATM90E32_REGISTER_ANENERGYC = 0x87; // C Reverse Active @@ -172,10 +176,12 @@ static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF; // C Reverse Harm. E /* POWER & P.F. REGISTERS */ static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P) +static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Mean Power Reg Base (P) static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P) static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q) +static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Mean Power Reg Base (Q) static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q) @@ -184,15 +190,18 @@ static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S) static const uint16_t ATM90E32_REGISTER_PFMEANT = 0xBC; // Mean Power Factor +static const uint16_t ATM90E32_REGISTER_PFMEAN = 0xBD; // Power Factor Reg Base static const uint16_t ATM90E32_REGISTER_PFMEANA = 0xBD; // A Power Factor static const uint16_t ATM90E32_REGISTER_PFMEANB = 0xBE; // B Power Factor static const uint16_t ATM90E32_REGISTER_PFMEANC = 0xBF; // C Power Factor static const uint16_t ATM90E32_REGISTER_PMEANTLSB = 0xC0; // Lower Word (Tot. Act. Power) +static const uint16_t ATM90E32_REGISTER_PMEANLSB = 0xC1; // Lower Word Reg Base (Active Power) static const uint16_t ATM90E32_REGISTER_PMEANALSB = 0xC1; // Lower Word (A Act. Power) static const uint16_t ATM90E32_REGISTER_PMEANBLSB = 0xC2; // Lower Word (B Act. Power) static const uint16_t ATM90E32_REGISTER_PMEANCLSB = 0xC3; // Lower Word (C Act. Power) static const uint16_t ATM90E32_REGISTER_QMEANTLSB = 0xC4; // Lower Word (Tot. React. Power) +static const uint16_t ATM90E32_REGISTER_QMEANLSB = 0xC5; // Lower Word Reg Base (Reactive Power) static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5; // Lower Word (A React. Power) static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power) static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power) @@ -207,12 +216,15 @@ static const uint16_t ATM90E32_REGISTER_PMEANAF = 0xD1; // A Active Fund. Power static const uint16_t ATM90E32_REGISTER_PMEANBF = 0xD2; // B Active Fund. Power static const uint16_t ATM90E32_REGISTER_PMEANCF = 0xD3; // C Active Fund. Power static const uint16_t ATM90E32_REGISTER_PMEANTH = 0xD4; // Total Active Harm. Power +static const uint16_t ATM90E32_REGISTER_PMEANH = 0xD5; // Active Harm. Power Reg Base static const uint16_t ATM90E32_REGISTER_PMEANAH = 0xD5; // A Active Harm. Power static const uint16_t ATM90E32_REGISTER_PMEANBH = 0xD6; // B Active Harm. Power static const uint16_t ATM90E32_REGISTER_PMEANCH = 0xD7; // C Active Harm. Power +static const uint16_t ATM90E32_REGISTER_URMS = 0xD9; // RMS Voltage Reg Base static const uint16_t ATM90E32_REGISTER_URMSA = 0xD9; // A RMS Voltage static const uint16_t ATM90E32_REGISTER_URMSB = 0xDA; // B RMS Voltage static const uint16_t ATM90E32_REGISTER_URMSC = 0xDB; // C RMS Voltage +static const uint16_t ATM90E32_REGISTER_IRMS = 0xDD; // RMS Current Reg Base static const uint16_t ATM90E32_REGISTER_IRMSA = 0xDD; // A RMS Current static const uint16_t ATM90E32_REGISTER_IRMSB = 0xDE; // B RMS Current static const uint16_t ATM90E32_REGISTER_IRMSC = 0xDF; // C RMS Current @@ -223,12 +235,15 @@ static const uint16_t ATM90E32_REGISTER_PMEANAFLSB = 0xE1; // Lower Word (A Act static const uint16_t ATM90E32_REGISTER_PMEANBFLSB = 0xE2; // Lower Word (B Act. Fund. Power) static const uint16_t ATM90E32_REGISTER_PMEANCFLSB = 0xE3; // Lower Word (C Act. Fund. Power) static const uint16_t ATM90E32_REGISTER_PMEANTHLSB = 0xE4; // Lower Word (Tot. Act. Harm. Power) +static const uint16_t ATM90E32_REGISTER_PMEANHLSB = 0xE5; // Lower Word (A Act. Harm. Power) Reg Base static const uint16_t ATM90E32_REGISTER_PMEANAHLSB = 0xE5; // Lower Word (A Act. Harm. Power) static const uint16_t ATM90E32_REGISTER_PMEANBHLSB = 0xE6; // Lower Word (B Act. Harm. Power) static const uint16_t ATM90E32_REGISTER_PMEANCHLSB = 0xE7; // Lower Word (C Act. Harm. Power) +static const uint16_t ATM90E32_REGISTER_URMSLSB = 0xE9; // Lower Word RMS Voltage Reg Base static const uint16_t ATM90E32_REGISTER_URMSALSB = 0xE9; // Lower Word (A RMS Voltage) static const uint16_t ATM90E32_REGISTER_URMSBLSB = 0xEA; // Lower Word (B RMS Voltage) static const uint16_t ATM90E32_REGISTER_URMSCLSB = 0xEB; // Lower Word (C RMS Voltage) +static const uint16_t ATM90E32_REGISTER_IRMSLSB = 0xED; // Lower Word RMS Current Reg Base static const uint16_t ATM90E32_REGISTER_IRMSALSB = 0xED; // Lower Word (A RMS Current) static const uint16_t ATM90E32_REGISTER_IRMSBLSB = 0xEE; // Lower Word (B RMS Current) static const uint16_t ATM90E32_REGISTER_IRMSCLSB = 0xEF; // Lower Word (C RMS Current) @@ -237,10 +252,12 @@ static const uint16_t ATM90E32_REGISTER_IRMSCLSB = 0xEF; // Lower Word (C RMS static const uint16_t ATM90E32_REGISTER_UPEAKA = 0xF1; // A Voltage Peak static const uint16_t ATM90E32_REGISTER_UPEAKB = 0xF2; // B Voltage Peak static const uint16_t ATM90E32_REGISTER_UPEAKC = 0xF3; // C Voltage Peak +static const uint16_t ATM90E32_REGISTER_IPEAK = 0xF5; // Peak Current Reg Base static const uint16_t ATM90E32_REGISTER_IPEAKA = 0xF5; // A Current Peak static const uint16_t ATM90E32_REGISTER_IPEAKB = 0xF6; // B Current Peak static const uint16_t ATM90E32_REGISTER_IPEAKC = 0xF7; // C Current Peak static const uint16_t ATM90E32_REGISTER_FREQ = 0xF8; // Frequency +static const uint16_t ATM90E32_REGISTER_PANGLE = 0xF9; // Mean Phase Angle Reg Base static const uint16_t ATM90E32_REGISTER_PANGLEA = 0xF9; // A Mean Phase Angle static const uint16_t ATM90E32_REGISTER_PANGLEB = 0xFA; // B Mean Phase Angle static const uint16_t ATM90E32_REGISTER_PANGLEC = 0xFB; // C Mean Phase Angle diff --git a/esphome/components/atm90e32/button/__init__.py b/esphome/components/atm90e32/button/__init__.py new file mode 100644 index 0000000000..931346b386 --- /dev/null +++ b/esphome/components/atm90e32/button/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE + +from .. import atm90e32_ns +from ..sensor import ATM90E32Component + +CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration" +CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration" + +ATM90E32CalibrationButton = atm90e32_ns.class_( + "ATM90E32CalibrationButton", + button.Button, +) +ATM90E32ClearCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearCalibrationButton", + button.Button, +) + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), + cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema( + ATM90E32CalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema( + ATM90E32ClearCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_CHIP, + ), +} + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION): + b = await button.new_button(run_offset) + await cg.register_parented(b, parent) + if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION): + b = await button.new_button(clear_offset) + await cg.register_parented(b, parent) diff --git a/esphome/components/atm90e32/button/atm90e32_button.cpp b/esphome/components/atm90e32/button/atm90e32_button.cpp new file mode 100644 index 0000000000..00715b61dd --- /dev/null +++ b/esphome/components/atm90e32/button/atm90e32_button.cpp @@ -0,0 +1,20 @@ +#include "atm90e32_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace atm90e32 { + +static const char *const TAG = "atm90e32.button"; + +void ATM90E32CalibrationButton::press_action() { + ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process..."); + this->parent_->run_offset_calibrations(); +} + +void ATM90E32ClearCalibrationButton::press_action() { + ESP_LOGI(TAG, "Offset calibrations cleared."); + this->parent_->clear_offset_calibrations(); +} + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/button/atm90e32_button.h b/esphome/components/atm90e32/button/atm90e32_button.h new file mode 100644 index 0000000000..0617099457 --- /dev/null +++ b/esphome/components/atm90e32/button/atm90e32_button.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/atm90e32/atm90e32.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace atm90e32 { + +class ATM90E32CalibrationButton : public button::Button, public Parented { + public: + ATM90E32CalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32ClearCalibrationButton : public button::Button, public Parented { + public: + ATM90E32ClearCalibrationButton() = default; + + protected: + void press_action() override; +}; + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 9c876bb62c..0dc3bfdc4f 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -1,16 +1,22 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, spi +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_REACTIVE_POWER, - CONF_VOLTAGE, + CONF_APPARENT_POWER, CONF_CURRENT, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_LINE_FREQUENCY, + CONF_PHASE_A, + CONF_PHASE_ANGLE, + CONF_PHASE_B, + CONF_PHASE_C, CONF_POWER, CONF_POWER_FACTOR, - CONF_FREQUENCY, - CONF_FORWARD_ACTIVE_ENERGY, + CONF_REACTIVE_POWER, CONF_REVERSE_ACTIVE_ENERGY, + CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -18,29 +24,32 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, - ICON_LIGHTBULB, ICON_CURRENT_AC, + ICON_LIGHTBULB, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_DEGREES, UNIT_HERTZ, UNIT_VOLT, - UNIT_AMPERE, - UNIT_WATT, - UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, UNIT_WATT_HOURS, ) -CONF_PHASE_A = "phase_a" -CONF_PHASE_B = "phase_b" -CONF_PHASE_C = "phase_c" +from . import atm90e32_ns -CONF_LINE_FREQUENCY = "line_frequency" CONF_CHIP_TEMPERATURE = "chip_temperature" CONF_GAIN_PGA = "gain_pga" CONF_CURRENT_PHASES = "current_phases" CONF_GAIN_VOLTAGE = "gain_voltage" CONF_GAIN_CT = "gain_ct" +CONF_HARMONIC_POWER = "harmonic_power" +CONF_PEAK_CURRENT = "peak_current" +CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" +CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration" +UNIT_DEG = "degrees" LINE_FREQS = { "50HZ": 50, "60HZ": 60, @@ -55,7 +64,6 @@ PGA_GAINS = { "4X": 0x2A, } -atm90e32_ns = cg.esphome_ns.namespace("atm90e32") ATM90E32Component = atm90e32_ns.class_( "ATM90E32Component", cg.PollingComponent, spi.SPIDevice ) @@ -86,6 +94,12 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( accuracy_decimals=2, device_class=DEVICE_CLASS_POWER_FACTOR, @@ -103,6 +117,24 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional(CONF_PHASE_ANGLE): sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HARMONIC_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PEAK_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_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, } @@ -133,6 +165,8 @@ CONFIG_SCHEMA = ( CURRENT_PHASES, upper=True ), cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), + cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, + cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -151,33 +185,49 @@ async def to_code(config): conf = config[phase] cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE])) cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT])) - if CONF_VOLTAGE in conf: - sens = await sensor.new_sensor(conf[CONF_VOLTAGE]) + if voltage_config := conf.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) cg.add(var.set_voltage_sensor(i, sens)) - if CONF_CURRENT in conf: - sens = await sensor.new_sensor(conf[CONF_CURRENT]) + if current_config := conf.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) cg.add(var.set_current_sensor(i, sens)) - if CONF_POWER in conf: - sens = await sensor.new_sensor(conf[CONF_POWER]) + if power_config := conf.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) cg.add(var.set_power_sensor(i, sens)) - if CONF_REACTIVE_POWER in conf: - sens = await sensor.new_sensor(conf[CONF_REACTIVE_POWER]) + if reactive_power_config := conf.get(CONF_REACTIVE_POWER): + sens = await sensor.new_sensor(reactive_power_config) cg.add(var.set_reactive_power_sensor(i, sens)) - if CONF_POWER_FACTOR in conf: - sens = await sensor.new_sensor(conf[CONF_POWER_FACTOR]) + if apparent_power_config := conf.get(CONF_APPARENT_POWER): + sens = await sensor.new_sensor(apparent_power_config) + cg.add(var.set_apparent_power_sensor(i, sens)) + if power_factor_config := conf.get(CONF_POWER_FACTOR): + sens = await sensor.new_sensor(power_factor_config) cg.add(var.set_power_factor_sensor(i, sens)) - if CONF_FORWARD_ACTIVE_ENERGY in conf: - sens = await sensor.new_sensor(conf[CONF_FORWARD_ACTIVE_ENERGY]) + if forward_active_energy_config := conf.get(CONF_FORWARD_ACTIVE_ENERGY): + sens = await sensor.new_sensor(forward_active_energy_config) cg.add(var.set_forward_active_energy_sensor(i, sens)) - if CONF_REVERSE_ACTIVE_ENERGY in conf: - sens = await sensor.new_sensor(conf[CONF_REVERSE_ACTIVE_ENERGY]) + if reverse_active_energy_config := conf.get(CONF_REVERSE_ACTIVE_ENERGY): + sens = await sensor.new_sensor(reverse_active_energy_config) cg.add(var.set_reverse_active_energy_sensor(i, sens)) - if CONF_FREQUENCY in config: - sens = await sensor.new_sensor(config[CONF_FREQUENCY]) + if phase_angle_config := conf.get(CONF_PHASE_ANGLE): + sens = await sensor.new_sensor(phase_angle_config) + cg.add(var.set_phase_angle_sensor(i, sens)) + if harmonic_active_power_config := conf.get(CONF_HARMONIC_POWER): + sens = await sensor.new_sensor(harmonic_active_power_config) + cg.add(var.set_harmonic_active_power_sensor(i, sens)) + if peak_current_config := conf.get(CONF_PEAK_CURRENT): + sens = await sensor.new_sensor(peak_current_config) + cg.add(var.set_peak_current_sensor(i, sens)) + + if frequency_config := config.get(CONF_FREQUENCY): + sens = await sensor.new_sensor(frequency_config) cg.add(var.set_freq_sensor(sens)) - if CONF_CHIP_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_CHIP_TEMPERATURE]) + if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE): + sens = await sensor.new_sensor(chip_temperature_config) cg.add(var.set_chip_temperature_sensor(sens)) + cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) + cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) + cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION])) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py new file mode 100644 index 0000000000..4ffdc401dc --- /dev/null +++ b/esphome/components/audio/__init__.py @@ -0,0 +1,9 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +CODEOWNERS = ["@kahrendt"] +audio_ns = cg.esphome_ns.namespace("audio") + +CONFIG_SCHEMA = cv.All( + cv.Schema({}), +) diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h new file mode 100644 index 0000000000..b0968dc8da --- /dev/null +++ b/esphome/components/audio/audio.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace esphome { +namespace audio { + +struct AudioStreamInfo { + bool operator==(const AudioStreamInfo &rhs) const { + return (channels == rhs.channels) && (bits_per_sample == rhs.bits_per_sample) && (sample_rate == rhs.sample_rate); + } + bool operator!=(const AudioStreamInfo &rhs) const { return !operator==(rhs); } + size_t get_bytes_per_sample() const { return bits_per_sample / 8; } + uint8_t channels = 1; + uint8_t bits_per_sample = 16; + uint32_t sample_rate = 16000; +}; + +} // namespace audio +} // namespace esphome diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py new file mode 100644 index 0000000000..978ed195bd --- /dev/null +++ b/esphome/components/audio_dac/__init__.py @@ -0,0 +1,57 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_VOLUME +from esphome.core import coroutine_with_priority + +CODEOWNERS = ["@kbx81"] +IS_PLATFORM_COMPONENT = True + +audio_dac_ns = cg.esphome_ns.namespace("audio_dac") +AudioDac = audio_dac_ns.class_("AudioDac") + +MuteOffAction = audio_dac_ns.class_("MuteOffAction", automation.Action) +MuteOnAction = audio_dac_ns.class_("MuteOnAction", automation.Action) +SetVolumeAction = audio_dac_ns.class_("SetVolumeAction", automation.Action) + + +MUTE_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(AudioDac), + } +) + +SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(AudioDac), + cv.Required(CONF_VOLUME): cv.templatable(cv.percentage), + }, + key=CONF_VOLUME, +) + + +@automation.register_action("audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA) +@automation.register_action("audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA) +async def audio_dac_mute_action_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( + "audio_dac.set_volume", SetVolumeAction, SET_VOLUME_ACTION_SCHEMA +) +async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config.get(CONF_VOLUME), args, float) + cg.add(var.set_volume(template_)) + + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_AUDIO_DAC") + cg.add_global(audio_dac_ns.using) diff --git a/esphome/components/audio_dac/audio_dac.h b/esphome/components/audio_dac/audio_dac.h new file mode 100644 index 0000000000..a62d17b849 --- /dev/null +++ b/esphome/components/audio_dac/audio_dac.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace audio_dac { + +class AudioDac { + public: + virtual bool set_mute_off() = 0; + virtual bool set_mute_on() = 0; + virtual bool set_volume(float volume) = 0; + + virtual bool is_muted() = 0; + virtual float volume() = 0; + + protected: + bool is_muted_{false}; +}; + +} // namespace audio_dac +} // namespace esphome diff --git a/esphome/components/audio_dac/automation.h b/esphome/components/audio_dac/automation.h new file mode 100644 index 0000000000..b6cf2acaf4 --- /dev/null +++ b/esphome/components/audio_dac/automation.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "audio_dac.h" + +namespace esphome { +namespace audio_dac { + +template class MuteOffAction : public Action { + public: + explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} + + void play(Ts... x) override { this->audio_dac_->set_mute_off(); } + + protected: + AudioDac *audio_dac_; +}; + +template class MuteOnAction : public Action { + public: + explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} + + void play(Ts... x) override { this->audio_dac_->set_mute_on(); } + + protected: + AudioDac *audio_dac_; +}; + +template class SetVolumeAction : public Action { + public: + explicit SetVolumeAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} + + TEMPLATABLE_VALUE(float, volume) + + void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } + + protected: + AudioDac *audio_dac_; +}; + +} // namespace audio_dac +} // namespace esphome diff --git a/esphome/components/axs15231/__init__.py b/esphome/components/axs15231/__init__.py new file mode 100644 index 0000000000..3246dbed24 --- /dev/null +++ b/esphome/components/axs15231/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +axs15231_ns = cg.esphome_ns.namespace("axs15231") diff --git a/esphome/components/axs15231/touchscreen/__init__.py b/esphome/components/axs15231/touchscreen/__init__.py new file mode 100644 index 0000000000..8c18d8ca75 --- /dev/null +++ b/esphome/components/axs15231/touchscreen/__init__.py @@ -0,0 +1,36 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c, touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +from .. import axs15231_ns + +AXS15231Touchscreen = axs15231_ns.class_( + "AXS15231Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONFIG_SCHEMA = ( + touchscreen.touchscreen_schema("50ms") + .extend( + { + cv.GenerateID(): cv.declare_id(AXS15231Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x3B)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp new file mode 100644 index 0000000000..54b39a6bb9 --- /dev/null +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -0,0 +1,64 @@ +#include "axs15231_touchscreen.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace axs15231 { + +static const char *const TAG = "ax15231.touchscreen"; + +constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a, 0x0, 0x0, 0x0, 0x8}; + +#define ERROR_CHECK(err) \ + if ((err) != i2c::ERROR_OK) { \ + this->status_set_warning("Failed to communicate"); \ + return; \ + } + +void AXS15231Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up AXS15231 Touchscreen..."); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + delay(10); + } + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + this->x_raw_max_ = this->display_->get_native_width(); + this->y_raw_max_ = this->display_->get_native_height(); + ESP_LOGCONFIG(TAG, "AXS15231 Touchscreen setup complete"); +} + +void AXS15231Touchscreen::update_touches() { + i2c::ErrorCode err; + uint8_t data[8]{}; + + err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD), false); + ERROR_CHECK(err); + err = this->read(data, sizeof(data)); + ERROR_CHECK(err); + this->status_clear_warning(); + if (data[0] != 0) // no touches + return; + uint16_t x = encode_uint16(data[2] & 0xF, data[3]); + uint16_t y = encode_uint16(data[4] & 0xF, data[5]); + this->add_raw_touch_position_(0, x, y); +} + +void AXS15231Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "AXS15231 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Width: %d", this->x_raw_max_); + ESP_LOGCONFIG(TAG, " Height: %d", this->y_raw_max_); +} + +} // namespace axs15231 +} // namespace esphome diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h new file mode 100644 index 0000000000..a55c5c0d32 --- /dev/null +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace axs15231 { + +class AXS15231Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{}; +}; + +} // namespace axs15231 +} // namespace esphome diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py index 1b65bf7f1d..86eef29b14 100644 --- a/esphome/components/b_parasite/sensor.py +++ b/esphome/components/b_parasite/sensor.py @@ -87,6 +87,6 @@ async def to_code(config): (CONF_MOISTURE, var.set_soil_moisture), (CONF_ILLUMINANCE, var.set_illuminance), ]: - if config_key in config: - sens = await sensor.new_sensor(config[config_key]) + if sensor_config := config.get(config_key): + sens = await sensor.new_sensor(sensor_config) cg.add(setter(sens)) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 20cb87025a..aed97b2890 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -15,6 +15,16 @@ void BangBangClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; + + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { @@ -47,6 +57,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, }); @@ -145,8 +157,11 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { default: trig = nullptr; } - assert(trig != nullptr); - trig->trigger(); + if (trig != nullptr) { + trig->trigger(); + } else { + ESP_LOGW(TAG, "trig not set - unsupported action"); + } this->action = action; this->prev_trigger_ = trig; this->publish_state(); @@ -171,6 +186,7 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa BangBangClimate::BangBangClimate() : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } @@ -181,8 +197,8 @@ void BangBangClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.2f°C", this->normal_config_.default_temperature_low); + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.2f°C", this->normal_config_.default_temperature_high); } BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default; diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 84bcd51f34..96368af34c 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -24,6 +24,7 @@ class BangBangClimate : public climate::Climate, public Component { void dump_config() override; void set_sensor(sensor::Sensor *sensor); + void set_humidity_sensor(sensor::Sensor *humidity_sensor); Trigger<> *get_idle_trigger() const; Trigger<> *get_cool_trigger() const; void set_supports_cool(bool supports_cool); @@ -48,6 +49,9 @@ class BangBangClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; + /** The trigger to call when the controller should switch to idle mode. * * In idle mode, the controller is assumed to have both heating and cooling disabled. diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index 5c935987de..9dde0ae1ac 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, + CONF_HUMIDITY_SENSOR, CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR, @@ -22,6 +23,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), @@ -47,6 +49,10 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + normal_config = BangBangClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], @@ -57,19 +63,18 @@ async def to_code(config): var.get_idle_trigger(), [], config[CONF_IDLE_ACTION] ) - if CONF_COOL_ACTION in config: + if cool_action_config := config.get(CONF_COOL_ACTION): await automation.build_automation( - var.get_cool_trigger(), [], config[CONF_COOL_ACTION] + var.get_cool_trigger(), [], cool_action_config ) cg.add(var.set_supports_cool(True)) - if CONF_HEAT_ACTION in config: + if heat_action_config := config.get(CONF_HEAT_ACTION): await automation.build_automation( - var.get_heat_trigger(), [], config[CONF_HEAT_ACTION] + var.get_heat_trigger(), [], heat_action_config ) cg.add(var.set_supports_heat(True)) - if CONF_AWAY_CONFIG in config: - away = config[CONF_AWAY_CONFIG] + if away := config.get(CONF_AWAY_CONFIG): away_config = BangBangClimateTargetTempConfig( away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], diff --git a/esphome/components/bedjet/__init__.py b/esphome/components/bedjet/__init__.py index 1697c549b3..a4b8a50eab 100644 --- a/esphome/components/bedjet/__init__.py +++ b/esphome/components/bedjet/__init__.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = ( BEDJET_CLIENT_SCHEMA = cv.Schema( { - cv.Required(CONF_BEDJET_ID): cv.use_id(BedJetHub), + cv.GenerateID(CONF_BEDJET_ID): cv.use_id(BedJetHub), } ) @@ -45,8 +45,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await ble_client.register_ble_node(var, config) - if CONF_TIME_ID in config: - time_ = await cg.get_variable(config[CONF_TIME_ID]) + if time_id := config.get(CONF_TIME_ID): + time_ = await cg.get_variable(time_id) cg.add(var.set_time_id(time_)) - if CONF_RECEIVE_TIMEOUT in config: - cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) + if (receive_timeout := config.get(CONF_RECEIVE_TIMEOUT)) is not None: + cg.add(var.set_status_timeout(receive_timeout)) diff --git a/esphome/components/bedjet/bedjet_codec.cpp b/esphome/components/bedjet/bedjet_codec.cpp index 735393ffcb..9a312e226c 100644 --- a/esphome/components/bedjet/bedjet_codec.cpp +++ b/esphome/components/bedjet/bedjet_codec.cpp @@ -13,8 +13,10 @@ float bedjet_temp_to_f(const uint8_t temp) { /** Cleans up the packet before sending. */ BedjetPacket *BedjetCodec::clean_packet_() { - // So far no commands require more than 2 bytes of data. - assert(this->packet_.data_length <= 2); + // So far no commands require more than 2 bytes of data + if (this->packet_.data_length > 2) { + ESP_LOGW(TAG, "Packet may be malformed"); + } for (int i = this->packet_.data_length; i < 2; i++) { this->packet_.data[i] = '\0'; } @@ -157,5 +159,11 @@ bool BedjetCodec::compare(const uint8_t *data, uint16_t length) { return explicit_fields_changed; } +/// Converts a BedJet temp step into degrees Celsius. +float bedjet_temp_to_c(uint8_t temp) { + // BedJet temp is "C*2"; to get C, divide by 2. + return temp / 2.0f; +} + } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/bedjet_codec.h b/esphome/components/bedjet/bedjet_codec.h index 3a41313ada..07aee32d54 100644 --- a/esphome/components/bedjet/bedjet_codec.h +++ b/esphome/components/bedjet/bedjet_codec.h @@ -90,7 +90,7 @@ struct BedjetStatusPacket { int unused_6 : 1; // 0x4 bool is_dual_zone : 1; /// Is part of a Dual Zone configuration int unused_7 : 1; // 0x1 - } dual_zone_flags; + } dual_zone_flags; // NOLINT(clang-diagnostic-unaligned-access) uint8_t unused_4 : 8; // Unknown 23-24 = 0x1310 uint8_t unused_5 : 8; // Unknown 23-24 = 0x1310 @@ -187,5 +187,8 @@ class BedjetCodec { BedjetStatusPacket buf_; }; +/// Converts a BedJet temp step into degrees Celsius. +float bedjet_temp_to_c(uint8_t temp); + } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 27a75b2671..7cac1b61ff 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -40,6 +40,14 @@ enum BedjetHeatMode { HEAT_MODE_EXTENDED, }; +// Which temperature to use as the climate entity's current temperature reading +enum BedjetTemperatureSource { + // Use the temperature of the air the BedJet is putting out + TEMPERATURE_SOURCE_OUTLET, + // Use the ambient temperature of the room the BedJet is in + TEMPERATURE_SOURCE_AMBIENT +}; + enum BedjetButton : uint8_t { /// Turn BedJet off BTN_OFF = 0x1, diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index fbd2876dc9..6404298697 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -3,6 +3,7 @@ #include "bedjet_hub.h" #include "bedjet_child.h" #include "bedjet_const.h" +#include namespace esphome { namespace bedjet { @@ -241,7 +242,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga this->set_notify_(true); #ifdef USE_TIME - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time(); } #endif @@ -373,7 +374,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { // Set reentrant flag to prevent processing multiple packets. this->processing_ = true; - ESP_LOGVV(TAG, "[%s] Decoding packet: last=%d, delta=%d, force=%s", this->get_name().c_str(), + ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name().c_str(), this->last_notify_, delta, this->force_refresh_ ? "y" : "n"); bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); @@ -440,9 +441,8 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { #ifdef USE_TIME void BedJetHub::send_local_time() { - if (this->time_id_.has_value()) { - auto *time_id = *this->time_id_; - time::ESPTime now = time_id->now(); + if (this->time_id_ != nullptr) { + ESPTime now = this->time_id_->now(); if (now.is_valid()) { this->set_clock(now.hour, now.minute); ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); @@ -453,10 +453,9 @@ void BedJetHub::send_local_time() { } void BedJetHub::setup_time_() { - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time(); - auto *time_id = *this->time_id_; - time_id->add_on_time_sync_callback([this] { this->send_local_time(); }); + this->time_id_->add_on_time_sync_callback([this] { this->send_local_time(); }); } else { ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); } @@ -523,11 +522,11 @@ void BedJetHub::dispatch_status_() { ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); } else if (diff > NOTIFY_WARN_THRESHOLD) { - ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); + ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name().c_str(), diff / 1000); } if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { - ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); + ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying...", this->get_name().c_str(), this->timeout_); // set_enabled(false) will only close the connection if state != IDLE. this->parent()->set_state(espbt::ClientState::CONNECTING); this->parent()->set_enabled(false); diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 5809827cfa..6258795b02 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -13,6 +13,7 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif #include @@ -140,7 +141,7 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo #ifdef USE_TIME /** Initializes time sync callbacks to support syncing current time to the BedJet. */ void setup_time_(); - optional time_id_{}; + time::RealTimeClock *time_id_{nullptr}; #endif uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; diff --git a/esphome/components/bedjet/climate/__init__.py b/esphome/components/bedjet/climate/__init__.py index b12622868f..e454b0922b 100644 --- a/esphome/components/bedjet/climate/__init__.py +++ b/esphome/components/bedjet/climate/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_HEAT_MODE, CONF_ID, CONF_RECEIVE_TIMEOUT, + CONF_TEMPERATURE_SOURCE, CONF_TIME_ID, ) from .. import ( @@ -21,10 +22,15 @@ DEPENDENCIES = ["bedjet"] BedJetClimate = bedjet_ns.class_("BedJetClimate", climate.Climate, cg.PollingComponent) BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode") +BedjetTemperatureSource = bedjet_ns.enum("BedjetTemperatureSource") BEDJET_HEAT_MODES = { "heat": BedjetHeatMode.HEAT_MODE_HEAT, "extended": BedjetHeatMode.HEAT_MODE_EXTENDED, } +BEDJET_TEMPERATURE_SOURCES = { + "outlet": BedjetTemperatureSource.TEMPERATURE_SOURCE_OUTLET, + "ambient": BedjetTemperatureSource.TEMPERATURE_SOURCE_AMBIENT, +} CONFIG_SCHEMA = ( climate.CLIMATE_SCHEMA.extend( @@ -33,6 +39,9 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( BEDJET_HEAT_MODES, lower=True ), + cv.Optional(CONF_TEMPERATURE_SOURCE, default="ambient"): cv.enum( + BEDJET_TEMPERATURE_SOURCES, lower=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -63,3 +72,4 @@ async def to_code(config): await register_bedjet_child(var, config) cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) + cg.add(var.set_temperature_source(config[CONF_TEMPERATURE_SOURCE])) diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 431cf614e9..854129f816 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -8,12 +8,6 @@ namespace bedjet { using namespace esphome::climate; -/// Converts a BedJet temp step into degrees Celsius. -float bedjet_temp_to_c(const uint8_t temp) { - // BedJet temp is "C*2"; to get C, divide by 2. - return temp / 2.0f; -} - static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { if (fan_step < BEDJET_FAN_SPEED_COUNT) return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; @@ -236,9 +230,14 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { if (converted_temp > 0) this->target_temperature = converted_temp; - converted_temp = bedjet_temp_to_c(data->ambient_temp_step); - if (converted_temp > 0) + if (this->temperature_source_ == TEMPERATURE_SOURCE_OUTLET) { + converted_temp = bedjet_temp_to_c(data->actual_temp_step); + } else { + converted_temp = bedjet_temp_to_c(data->ambient_temp_step); + } + if (converted_temp > 0) { this->current_temperature = converted_temp; + } const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); if (fan_mode_name != nullptr) { diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index 48c50d842f..7eaa735a3f 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -28,6 +28,8 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli /** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */ void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; } + /** Sets the temperature source to use for the climate entity's current temperature */ + void set_temperature_source(BedjetTemperatureSource source) { this->temperature_source_ = source; } climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); @@ -74,6 +76,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli void control(const climate::ClimateCall &call) override; BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT; + BedjetTemperatureSource temperature_source_ = TEMPERATURE_SOURCE_AMBIENT; void reset_state_(); bool update_status_(); diff --git a/esphome/components/bedjet/sensor/__init__.py b/esphome/components/bedjet/sensor/__init__.py new file mode 100644 index 0000000000..756b31de53 --- /dev/null +++ b/esphome/components/bedjet/sensor/__init__.py @@ -0,0 +1,55 @@ +import logging + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) +from .. import ( + BEDJET_CLIENT_SCHEMA, + bedjet_ns, + register_bedjet_child, +) + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@jhansche", "@javawizard"] +DEPENDENCIES = ["bedjet"] + +CONF_OUTLET_TEMPERATURE = "outlet_temperature" +CONF_AMBIENT_TEMPERATURE = "ambient_temperature" + +BedjetSensor = bedjet_ns.class_("BedjetSensor", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BedjetSensor), + cv.Optional(CONF_OUTLET_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_AMBIENT_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +).extend(BEDJET_CLIENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await register_bedjet_child(var, config) + + if outlet_temperature_sensor := config.get(CONF_OUTLET_TEMPERATURE): + sensor_var = await sensor.new_sensor(outlet_temperature_sensor) + cg.add(var.set_outlet_temperature_sensor(sensor_var)) + + if ambient_temperature_sensor := config.get(CONF_AMBIENT_TEMPERATURE): + sensor_var = await sensor.new_sensor(ambient_temperature_sensor) + cg.add(var.set_ambient_temperature_sensor(sensor_var)) diff --git a/esphome/components/bedjet/sensor/bedjet_sensor.cpp b/esphome/components/bedjet/sensor/bedjet_sensor.cpp new file mode 100644 index 0000000000..2fda8c927f --- /dev/null +++ b/esphome/components/bedjet/sensor/bedjet_sensor.cpp @@ -0,0 +1,34 @@ +#include "bedjet_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bedjet { + +std::string BedjetSensor::describe() { return "BedJet Sensor"; } + +void BedjetSensor::dump_config() { + ESP_LOGCONFIG(TAG, "BedJet Sensor:"); + LOG_SENSOR(" ", "Outlet Temperature", this->outlet_temperature_sensor_); + LOG_SENSOR(" ", "Ambient Temperature", this->ambient_temperature_sensor_); +} + +void BedjetSensor::on_bedjet_state(bool is_ready) {} + +void BedjetSensor::on_status(const BedjetStatusPacket *data) { + if (this->outlet_temperature_sensor_ != nullptr) { + float converted_temp = bedjet_temp_to_c(data->actual_temp_step); + if (converted_temp > 0) { + this->outlet_temperature_sensor_->publish_state(converted_temp); + } + } + + if (this->ambient_temperature_sensor_ != nullptr) { + float converted_temp = bedjet_temp_to_c(data->ambient_temp_step); + if (converted_temp > 0) { + this->ambient_temperature_sensor_->publish_state(converted_temp); + } + } +} + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/sensor/bedjet_sensor.h b/esphome/components/bedjet/sensor/bedjet_sensor.h new file mode 100644 index 0000000000..8cbaa863ee --- /dev/null +++ b/esphome/components/bedjet/sensor/bedjet_sensor.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/bedjet/bedjet_child.h" +#include "esphome/components/bedjet/bedjet_codec.h" + +namespace esphome { +namespace bedjet { + +class BedjetSensor : public BedJetClient, public Component { + public: + void dump_config() override; + + void on_status(const BedjetStatusPacket *data) override; + void on_bedjet_state(bool is_ready) override; + std::string describe() override; + + void set_outlet_temperature_sensor(sensor::Sensor *outlet_temperature_sensor) { + this->outlet_temperature_sensor_ = outlet_temperature_sensor; + } + void set_ambient_temperature_sensor(sensor::Sensor *ambient_temperature_sensor) { + this->ambient_temperature_sensor_ = ambient_temperature_sensor; + } + + protected: + sensor::Sensor *outlet_temperature_sensor_{nullptr}; + sensor::Sensor *ambient_temperature_sensor_{nullptr}; +}; + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/beken_spi_led_strip/__init__.py b/esphome/components/beken_spi_led_strip/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp new file mode 100644 index 0000000000..04c8649b90 --- /dev/null +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -0,0 +1,384 @@ +#include "led_strip.h" + +#ifdef USE_BK72XX + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +extern "C" { +#include "rtos_pub.h" +#include "spi.h" +#include "arm_arch.h" +#include "general_dma_pub.h" +#include "gpio_pub.h" +#include "icu_pub.h" +#undef SPI_DAT +#undef SPI_BASE +}; + +static const uint32_t SPI_TX_DMA_CHANNEL = GDMA_CHANNEL_3; + +// TODO: Check if SPI_PERI_CLK_DCO depends on the chip variant +static const uint32_t SPI_PERI_CLK_26M = 26000000; +static const uint32_t SPI_PERI_CLK_DCO = 120000000; + +static const uint32_t SPI_BASE = 0x00802700; +static const uint32_t SPI_DAT = SPI_BASE + 3 * 4; +static const uint32_t SPI_CONFIG = SPI_BASE + 1 * 4; + +static const uint32_t SPI_TX_EN = 1 << 0; +static const uint32_t CTRL_NSSMD_3 = 1 << 17; +static const uint32_t SPI_TX_FINISH_EN = 1 << 2; +static const uint32_t SPI_RX_FINISH_EN = 1 << 3; + +namespace esphome { +namespace beken_spi_led_strip { + +static const char *const TAG = "beken_spi_led_strip"; + +struct spi_data_t { + SemaphoreHandle_t dma_tx_semaphore; + volatile bool tx_in_progress; + bool first_run; +}; + +static spi_data_t *spi_data = nullptr; + +static void set_spi_ctrl_register(unsigned long bit, bool val) { + uint32_t value = REG_READ(SPI_CTRL); + if (val == 0) { + value &= ~bit; + } else if (val == 1) { + value |= bit; + } + REG_WRITE(SPI_CTRL, value); +} + +static void set_spi_config_register(unsigned long bit, bool val) { + uint32_t value = REG_READ(SPI_CONFIG); + if (val == 0) { + value &= ~bit; + } else if (val == 1) { + value |= bit; + } + REG_WRITE(SPI_CONFIG, value); +} + +void spi_dma_tx_enable(bool enable) { + GDMA_CFG_ST en_cfg; + set_spi_config_register(SPI_TX_EN, enable ? 1 : 0); + en_cfg.channel = SPI_TX_DMA_CHANNEL; + en_cfg.param = enable ? 1 : 0; + sddev_control(GDMA_DEV_NAME, CMD_GDMA_SET_DMA_ENABLE, &en_cfg); +} + +static void spi_set_clock(uint32_t max_hz) { + int source_clk = 0; + int spi_clk = 0; + int div = 0; + uint32_t param; + if (max_hz > 4333000) { + if (max_hz > 30000000) { + spi_clk = 30000000; + } else { + spi_clk = max_hz; + } + sddev_control(ICU_DEV_NAME, CMD_CLK_PWR_DOWN, ¶m); + source_clk = SPI_PERI_CLK_DCO; + param = PCLK_POSI_SPI; + sddev_control(ICU_DEV_NAME, CMD_CONF_PCLK_DCO, ¶m); + param = PWD_SPI_CLK_BIT; + sddev_control(ICU_DEV_NAME, CMD_CLK_PWR_UP, ¶m); + } else { + spi_clk = max_hz; +#if CFG_XTAL_FREQUENCE + source_clk = CFG_XTAL_FREQUENCE; +#else + source_clk = SPI_PERI_CLK_26M; +#endif + param = PCLK_POSI_SPI; + sddev_control(ICU_DEV_NAME, CMD_CONF_PCLK_26M, ¶m); + } + div = ((source_clk >> 1) / spi_clk); + if (div < 2) { + div = 2; + } else if (div >= 255) { + div = 255; + } + param = REG_READ(SPI_CTRL); + param &= ~(SPI_CKR_MASK << SPI_CKR_POSI); + param |= (div << SPI_CKR_POSI); + REG_WRITE(SPI_CTRL, param); + ESP_LOGD(TAG, "target frequency: %d, actual frequency: %d", max_hz, source_clk / 2 / div); +} + +void spi_dma_tx_finish_callback(unsigned int param) { + spi_data->tx_in_progress = false; + xSemaphoreGive(spi_data->dma_tx_semaphore); + spi_dma_tx_enable(0); +} + +void BekenSPILEDStripLightOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up Beken SPI LED Strip..."); + + size_t buffer_size = this->get_buffer_size_(); + size_t dma_buffer_size = (buffer_size * 8) + (2 * 64); + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buf_ = allocator.allocate(buffer_size); + if (this->buf_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate LED buffer!"); + this->mark_failed(); + return; + } + + this->effect_data_ = allocator.allocate(this->num_leds_); + if (this->effect_data_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate effect data!"); + this->mark_failed(); + return; + } + + this->dma_buf_ = allocator.allocate(dma_buffer_size); + if (this->dma_buf_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate DMA buffer!"); + this->mark_failed(); + return; + } + + memset(this->buf_, 0, buffer_size); + memset(this->effect_data_, 0, this->num_leds_); + memset(this->dma_buf_, 0, dma_buffer_size); + + uint32_t value = PCLK_POSI_SPI; + sddev_control(ICU_DEV_NAME, CMD_CONF_PCLK_26M, &value); + + value = PWD_SPI_CLK_BIT; + sddev_control(ICU_DEV_NAME, CMD_CLK_PWR_UP, &value); + + if (spi_data != nullptr) { + ESP_LOGE(TAG, "SPI device already initialized!"); + this->mark_failed(); + return; + } + + spi_data = (spi_data_t *) calloc(1, sizeof(spi_data_t)); + if (spi_data == nullptr) { + ESP_LOGE(TAG, "Cannot allocate spi_data!"); + this->mark_failed(); + return; + } + + spi_data->dma_tx_semaphore = xSemaphoreCreateBinary(); + if (spi_data->dma_tx_semaphore == nullptr) { + ESP_LOGE(TAG, "TX Semaphore init faild!"); + this->mark_failed(); + return; + } + + spi_data->first_run = true; + + set_spi_ctrl_register(MSTEN, 0); + set_spi_ctrl_register(BIT_WDTH, 0); + spi_set_clock(this->spi_frequency_); + set_spi_ctrl_register(CKPOL, 0); + set_spi_ctrl_register(CKPHA, 0); + set_spi_ctrl_register(MSTEN, 1); + set_spi_ctrl_register(SPIEN, 1); + + set_spi_ctrl_register(TXINT_EN, 0); + set_spi_ctrl_register(RXINT_EN, 0); + set_spi_config_register(SPI_TX_FINISH_EN, 1); + set_spi_config_register(SPI_RX_FINISH_EN, 1); + set_spi_ctrl_register(RXOVR_EN, 0); + set_spi_ctrl_register(TXOVR_EN, 0); + + value = REG_READ(SPI_CTRL); + value &= ~CTRL_NSSMD_3; + value |= (1 << 17); + REG_WRITE(SPI_CTRL, value); + + value = GFUNC_MODE_SPI_DMA; + sddev_control(GPIO_DEV_NAME, CMD_GPIO_ENABLE_SECOND, &value); + set_spi_ctrl_register(SPI_S_CS_UP_INT_EN, 0); + + GDMA_CFG_ST en_cfg; + GDMACFG_TPYES_ST init_cfg; + memset(&init_cfg, 0, sizeof(GDMACFG_TPYES_ST)); + + init_cfg.dstdat_width = 8; + init_cfg.srcdat_width = 32; + init_cfg.dstptr_incr = 0; + init_cfg.srcptr_incr = 1; + init_cfg.src_start_addr = this->dma_buf_; + init_cfg.dst_start_addr = (void *) SPI_DAT; // SPI_DMA_REG4_TXFIFO + init_cfg.channel = SPI_TX_DMA_CHANNEL; + init_cfg.prio = 0; // 10 + init_cfg.u.type4.src_loop_start_addr = this->dma_buf_; + init_cfg.u.type4.src_loop_end_addr = this->dma_buf_ + dma_buffer_size; + init_cfg.half_fin_handler = nullptr; + init_cfg.fin_handler = spi_dma_tx_finish_callback; + init_cfg.src_module = GDMA_X_SRC_DTCM_RD_REQ; + init_cfg.dst_module = GDMA_X_DST_GSPI_TX_REQ; // GDMA_X_DST_HSSPI_TX_REQ + sddev_control(GDMA_DEV_NAME, CMD_GDMA_CFG_TYPE4, (void *) &init_cfg); + en_cfg.channel = SPI_TX_DMA_CHANNEL; + en_cfg.param = dma_buffer_size; + sddev_control(GDMA_DEV_NAME, CMD_GDMA_SET_TRANS_LENGTH, (void *) &en_cfg); + en_cfg.channel = SPI_TX_DMA_CHANNEL; + en_cfg.param = 0; + sddev_control(GDMA_DEV_NAME, CMD_GDMA_CFG_WORK_MODE, (void *) &en_cfg); + en_cfg.channel = SPI_TX_DMA_CHANNEL; + en_cfg.param = 0; + sddev_control(GDMA_DEV_NAME, CMD_GDMA_CFG_SRCADDR_LOOP, &en_cfg); + + spi_dma_tx_enable(0); + + value = REG_READ(SPI_CONFIG); + value &= ~(0xFFF << 8); + value |= ((dma_buffer_size & 0xFFF) << 8); + REG_WRITE(SPI_CONFIG, value); +} + +void BekenSPILEDStripLightOutput::set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency) { + this->bit0_ = bit0; + this->bit1_ = bit1; + this->spi_frequency_ = spi_frequency; +} + +void BekenSPILEDStripLightOutput::write_state(light::LightState *state) { + // protect from refreshing too often + uint32_t now = micros(); + if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + // try again next loop iteration, so that this change won't get lost + this->schedule_show(); + return; + } + this->last_refresh_ = now; + this->mark_shown_(); + + ESP_LOGVV(TAG, "Writing RGB values to bus..."); + + if (spi_data == nullptr) { + ESP_LOGE(TAG, "SPI not initialized"); + this->status_set_warning(); + return; + } + + if (!spi_data->first_run && !xSemaphoreTake(spi_data->dma_tx_semaphore, 10 / portTICK_PERIOD_MS)) { + ESP_LOGE(TAG, "Timed out waiting for semaphore"); + return; + } + + if (spi_data->tx_in_progress) { + ESP_LOGE(TAG, "tx_in_progress is set"); + this->status_set_warning(); + return; + } + + spi_data->tx_in_progress = true; + + size_t buffer_size = this->get_buffer_size_(); + size_t size = 0; + uint8_t *psrc = this->buf_; + uint8_t *pdest = this->dma_buf_ + 64; + // The 64 byte padding is a workaround for a SPI DMA bug where the + // output doesn't exactly start at the beginning of dma_buf_ + + while (size < buffer_size) { + uint8_t b = *psrc; + for (int i = 0; i < 8; i++) { + *pdest++ = b & (1 << (7 - i)) ? this->bit1_ : this->bit0_; + } + size++; + psrc++; + } + + spi_data->first_run = false; + spi_dma_tx_enable(1); + + this->status_clear_warning(); +} + +light::ESPColorView BekenSPILEDStripLightOutput::get_view_internal(int32_t index) const { + int32_t r = 0, g = 0, b = 0; + switch (this->rgb_order_) { + case ORDER_RGB: + r = 0; + g = 1; + b = 2; + break; + case ORDER_RBG: + r = 0; + g = 2; + b = 1; + break; + case ORDER_GRB: + r = 1; + g = 0; + b = 2; + break; + case ORDER_GBR: + r = 2; + g = 0; + b = 1; + break; + case ORDER_BGR: + r = 2; + g = 1; + b = 0; + break; + case ORDER_BRG: + r = 1; + g = 2; + b = 0; + break; + } + uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3; + uint8_t white = this->is_wrgb_ ? 0 : 3; + + return {this->buf_ + (index * multiplier) + r + this->is_wrgb_, + this->buf_ + (index * multiplier) + g + this->is_wrgb_, + this->buf_ + (index * multiplier) + b + this->is_wrgb_, + this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr, + &this->effect_data_[index], + &this->correction_}; +} + +void BekenSPILEDStripLightOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Beken SPI LED Strip:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + const char *rgb_order; + switch (this->rgb_order_) { + case ORDER_RGB: + rgb_order = "RGB"; + break; + case ORDER_RBG: + rgb_order = "RBG"; + break; + case ORDER_GRB: + rgb_order = "GRB"; + break; + case ORDER_GBR: + rgb_order = "GBR"; + break; + case ORDER_BGR: + rgb_order = "BGR"; + break; + case ORDER_BRG: + rgb_order = "BRG"; + break; + default: + rgb_order = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order); + ESP_LOGCONFIG(TAG, " Max refresh rate: %" PRIu32, *this->max_refresh_rate_); + ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_); +} + +float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } + +} // namespace beken_spi_led_strip +} // namespace esphome + +#endif // USE_BK72XX diff --git a/esphome/components/beken_spi_led_strip/led_strip.h b/esphome/components/beken_spi_led_strip/led_strip.h new file mode 100644 index 0000000000..705f9102a9 --- /dev/null +++ b/esphome/components/beken_spi_led_strip/led_strip.h @@ -0,0 +1,85 @@ +#pragma once + +#ifdef USE_BK72XX + +#include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/light_output.h" +#include "esphome/core/color.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace beken_spi_led_strip { + +enum RGBOrder : uint8_t { + ORDER_RGB, + ORDER_RBG, + ORDER_GRB, + ORDER_GBR, + ORDER_BGR, + ORDER_BRG, +}; + +class BekenSPILEDStripLightOutput : public light::AddressableLight { + public: + void setup() override; + void write_state(light::LightState *state) override; + float get_setup_priority() const override; + + int32_t size() const override { return this->num_leds_; } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + if (this->is_rgbw_ || this->is_wrgb_) { + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}); + } else { + traits.set_supported_color_modes({light::ColorMode::RGB}); + } + return traits; + } + + void set_pin(uint8_t pin) { this->pin_ = pin; } + 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; } + + /// Set a maximum refresh rate in µs as some lights do not like being updated too often. + void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } + + void set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency); + + void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } + + void clear_effect_data() override { + for (int i = 0; i < this->size(); i++) + this->effect_data_[i] = 0; + } + + void dump_config() override; + + protected: + light::ESPColorView get_view_internal(int32_t index) const override; + + size_t get_buffer_size_() const { return this->num_leds_ * (this->is_rgbw_ || this->is_wrgb_ ? 4 : 3); } + + uint8_t *buf_{nullptr}; + uint8_t *effect_data_{nullptr}; + uint8_t *dma_buf_{nullptr}; + + uint8_t pin_; + uint16_t num_leds_; + bool is_rgbw_; + bool is_wrgb_; + + uint32_t spi_frequency_{6666666}; + uint8_t bit0_{0xE0}; + uint8_t bit1_{0xFC}; + RGBOrder rgb_order_; + + uint32_t last_refresh_{0}; + optional max_refresh_rate_{}; +}; + +} // namespace beken_spi_led_strip +} // namespace esphome + +#endif // USE_BK72XX diff --git a/esphome/components/beken_spi_led_strip/light.py b/esphome/components/beken_spi_led_strip/light.py new file mode 100644 index 0000000000..2a1aa05c79 --- /dev/null +++ b/esphome/components/beken_spi_led_strip/light.py @@ -0,0 +1,134 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import libretiny, light +from esphome.const import ( + CONF_CHIPSET, + CONF_IS_RGBW, + CONF_MAX_REFRESH_RATE, + CONF_NUM_LEDS, + CONF_OUTPUT_ID, + CONF_PIN, + CONF_RGB_ORDER, +) + +CODEOWNERS = ["@Mat931"] +DEPENDENCIES = ["libretiny"] + +beken_spi_led_strip_ns = cg.esphome_ns.namespace("beken_spi_led_strip") +BekenSPILEDStripLightOutput = beken_spi_led_strip_ns.class_( + "BekenSPILEDStripLightOutput", light.AddressableLight +) + +RGBOrder = beken_spi_led_strip_ns.enum("RGBOrder") + +RGB_ORDERS = { + "RGB": RGBOrder.ORDER_RGB, + "RBG": RGBOrder.ORDER_RBG, + "GRB": RGBOrder.ORDER_GRB, + "GBR": RGBOrder.ORDER_GBR, + "BGR": RGBOrder.ORDER_BGR, + "BRG": RGBOrder.ORDER_BRG, +} + + +@dataclass +class LEDStripTimings: + bit0: int + bit1: int + spi_frequency: int + + +CHIPSETS = { + "WS2812": LEDStripTimings( + 0b11100000, 0b11111100, 6666666 + ), # Clock divider: 9, Bit time: 1350ns + "SK6812": LEDStripTimings( + 0b11000000, 0b11111000, 7500000 + ), # Clock divider: 8, Bit time: 1200ns + "APA106": LEDStripTimings( + 0b11000000, 0b11111110, 5454545 + ), # Clock divider: 11, Bit time: 1650ns + "SM16703": LEDStripTimings( + 0b11000000, 0b11111110, 7500000 + ), # Clock divider: 8, Bit time: 1200ns +} + + +CONF_IS_WRGB = "is_wrgb" + +SUPPORTED_PINS = { + libretiny.const.FAMILY_BK7231N: [16], + libretiny.const.FAMILY_BK7231T: [16], + libretiny.const.FAMILY_BK7251: [16], +} + + +def _validate_pin(value): + family = libretiny.get_libretiny_family() + if family not in SUPPORTED_PINS: + raise cv.Invalid(f"Chip family {family} is not supported.") + if value not in SUPPORTED_PINS[family]: + supported_pin_info = ", ".join(f"{x}" for x in SUPPORTED_PINS[family]) + raise cv.Invalid( + f"Pin {value} is not supported on the {family}. Supported pins: {supported_pin_info}" + ) + return value + + +def _validate_num_leds(value): + max_num_leds = 165 # 170 + if value[CONF_IS_RGBW] or value[CONF_IS_WRGB]: + max_num_leds = 123 # 127 + if value[CONF_NUM_LEDS] > max_num_leds: + raise cv.Invalid( + f"The maximum number of LEDs for this configuration is {max_num_leds}.", + path=CONF_NUM_LEDS, + ) + return value + + +CONFIG_SCHEMA = cv.All( + light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BekenSPILEDStripLightOutput), + cv.Required(CONF_PIN): cv.All( + pins.internal_gpio_output_pin_number, _validate_pin + ), + cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, + cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), + cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, + cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), + cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, + cv.Optional(CONF_IS_WRGB, default=False): cv.boolean, + } + ), + _validate_num_leds, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, 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])) + + if CONF_MAX_REFRESH_RATE in config: + cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) + + chipset = CHIPSETS[config[CONF_CHIPSET]] + cg.add( + var.set_led_params( + chipset.bit0, + chipset.bit1, + chipset.spi_frequency, + ) + ) + + cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) + cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) + cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index 6edfa885c9..73d6b9339f 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -29,10 +29,10 @@ async def to_code(config): output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) - if CONF_OSCILLATION_OUTPUT in config: - oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) + if oscillation_output_id := config.get(CONF_OSCILLATION_OUTPUT): + oscillation_output = await cg.get_variable(oscillation_output_id) cg.add(var.set_oscillating(oscillation_output)) - if CONF_DIRECTION_OUTPUT in config: - direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + if direction_output_id := config.get(CONF_DIRECTION_OUTPUT): + direction_output = await cg.get_variable(direction_output_id) cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 86c83aff5c..8346a82cf0 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -18,10 +18,11 @@ class BinaryLightOutput : public light::LightOutput { void write_state(light::LightState *state) override { bool binary; state->current_values_as_binary(&binary); - if (binary) + if (binary) { this->output_->turn_on(); - else + } else { this->output_->turn_off(); + } } protected: diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index f4a5c95b12..d947c2aba6 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -1,10 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome import automation, core from esphome.automation import Condition, maybe_simple_id -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DELAY, CONF_DEVICE_CLASS, @@ -16,6 +14,7 @@ from esphome.const import ( CONF_INVERTED, CONF_MAX_LENGTH, CONF_MIN_LENGTH, + CONF_MQTT_ID, CONF_ON_CLICK, CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, @@ -26,7 +25,7 @@ from esphome.const import ( CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, - CONF_MQTT_ID, + CONF_WEB_SERVER, DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_CARBON_MONOXIDE, @@ -58,6 +57,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -95,6 +96,14 @@ DEVICE_CLASSES = [ IS_PLATFORM_COMPONENT = True +CONF_TIME_OFF = "time_off" +CONF_TIME_ON = "time_on" + +DEFAULT_DELAY = "1s" +DEFAULT_TIME_OFF = "100ms" +DEFAULT_TIME_ON = "900ms" + + binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor") BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase) BinarySensorInitiallyOff = binary_sensor_ns.class_( @@ -133,52 +142,81 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) +SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) FILTER_REGISTRY = Registry() validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) -@FILTER_REGISTRY.register("invert", InvertFilter, {}) +def register_filter(name, filter_type, schema): + return FILTER_REGISTRY.register(name, filter_type, schema) + + +@register_filter("invert", InvertFilter, {}) async def invert_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id) -@FILTER_REGISTRY.register( - "delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds +@register_filter( + "delayed_on_off", + DelayedOnOffFilter, + cv.Any( + cv.templatable(cv.positive_time_period_milliseconds), + cv.Schema( + { + cv.Required(CONF_TIME_ON): cv.templatable( + cv.positive_time_period_milliseconds + ), + cv.Required(CONF_TIME_OFF): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), + msg="'delayed_on_off' filter requires either a delay time to be used for both " + "turn-on and turn-off delays, or two parameters 'time_on' and 'time_off' if " + "different delay times are required.", + ), ) async def delayed_on_off_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) + var = cg.new_Pvariable(filter_id) await cg.register_component(var, {}) + if isinstance(config, dict): + template_ = await cg.templatable(config[CONF_TIME_ON], [], cg.uint32) + cg.add(var.set_on_delay(template_)) + template_ = await cg.templatable(config[CONF_TIME_OFF], [], cg.uint32) + cg.add(var.set_off_delay(template_)) + else: + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_on_delay(template_)) + cg.add(var.set_off_delay(template_)) return var -@FILTER_REGISTRY.register( - "delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds +@register_filter( + "delayed_on", DelayedOnFilter, cv.templatable(cv.positive_time_period_milliseconds) ) async def delayed_on_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) + var = cg.new_Pvariable(filter_id) await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_delay(template_)) return var -@FILTER_REGISTRY.register( - "delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds +@register_filter( + "delayed_off", + DelayedOffFilter, + cv.templatable(cv.positive_time_period_milliseconds), ) async def delayed_off_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) + var = cg.new_Pvariable(filter_id) await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_delay(template_)) return var -CONF_TIME_OFF = "time_off" -CONF_TIME_ON = "time_on" - -DEFAULT_DELAY = "1s" -DEFAULT_TIME_OFF = "100ms" -DEFAULT_TIME_ON = "900ms" - - -@FILTER_REGISTRY.register( +@register_filter( "autorepeat", AutorepeatFilter, cv.All( @@ -215,7 +253,7 @@ async def autorepeat_filter_to_code(config, filter_id): return var -@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda) +@register_filter("lambda", LambdaFilter, cv.returning_lambda) async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) @@ -223,6 +261,19 @@ async def lambda_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, lambda_) +@register_filter( + "settle", + SettleFilter, + cv.templatable(cv.positive_time_period_milliseconds), +) +async def settle_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id) + await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_delay(template_)) + return var + + MULTI_CLICK_TIMING_SCHEMA = cv.Schema( { cv.Optional(CONF_STATE): cv.boolean, @@ -323,64 +374,88 @@ def validate_multi_click_timing(value): validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( - { - cv.GenerateID(): cv.declare_id(BinarySensor), - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( - mqtt.MQTTBinarySensorComponent - ), - cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean, - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_PRESS): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger), - } - ), - cv.Optional(CONF_ON_RELEASE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), - } - ), - cv.Optional(CONF_ON_CLICK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), - cv.Optional( - CONF_MIN_LENGTH, default="50ms" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_MAX_LENGTH, default="350ms" - ): cv.positive_time_period_milliseconds, - } - ), - cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), - cv.Optional( - CONF_MIN_LENGTH, default="50ms" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_MAX_LENGTH, default="350ms" - ): cv.positive_time_period_milliseconds, - } - ), - cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger), - cv.Required(CONF_TIMING): cv.All( - [parse_multi_click_timing_str], validate_multi_click_timing +def validate_click_timing(value): + for v in value: + min_length = v.get(CONF_MIN_LENGTH) + max_length = v.get(CONF_MAX_LENGTH) + if max_length < min_length: + raise cv.Invalid( + f"Max length ({max_length}) must be larger than min length ({min_length})." + ) + + return value + + +BINARY_SENSOR_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(BinarySensor), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( + mqtt.MQTTBinarySensorComponent + ), + cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean, + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional(CONF_ON_PRESS): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger), + } + ), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), + } + ), + cv.Optional(CONF_ON_CLICK): cv.All( + automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), + cv.Optional( + CONF_MIN_LENGTH, default="50ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MAX_LENGTH, default="350ms" + ): cv.positive_time_period_milliseconds, + } ), - cv.Optional( - CONF_INVALID_COOLDOWN, default="1s" - ): cv.positive_time_period_milliseconds, - } - ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - } + validate_click_timing, + ), + cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All( + automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DoubleClickTrigger + ), + cv.Optional( + CONF_MIN_LENGTH, default="50ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MAX_LENGTH, default="350ms" + ): cv.positive_time_period_milliseconds, + } + ), + validate_click_timing, + ), + cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger), + cv.Required(CONF_TIMING): cv.All( + [parse_multi_click_timing_str], validate_multi_click_timing + ), + cv.Optional( + CONF_INVALID_COOLDOWN, default="1s" + ): cv.positive_time_period_milliseconds, + } + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + } + ), + } + ) ) _UNDEF = object() @@ -413,14 +488,14 @@ def binary_sensor_schema( async def setup_binary_sensor_core_(var, config): await setup_entity(var, config) - if CONF_DEVICE_CLASS in config: - cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) - if CONF_PUBLISH_INITIAL_STATE in config: - cg.add(var.set_publish_initial_state(config[CONF_PUBLISH_INITIAL_STATE])) - if CONF_INVERTED in config: - cg.add(var.set_inverted(config[CONF_INVERTED])) - if CONF_FILTERS in config: - filters = await cg.build_registry_list(FILTER_REGISTRY, config[CONF_FILTERS]) + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + if publish_initial_state := config.get(CONF_PUBLISH_INITIAL_STATE): + cg.add(var.set_publish_initial_state(publish_initial_state)) + if inverted := config.get(CONF_INVERTED): + cg.add(var.set_inverted(inverted)) + if filters_config := config.get(CONF_FILTERS): + filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) cg.add(var.add_filters(filters)) for conf in config.get(CONF_ON_PRESS, []): @@ -464,10 +539,13 @@ async def setup_binary_sensor_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "x")], conf) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if mqtt_id := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_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 ce082aafb3..c2e76246aa 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -22,7 +22,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) { // Start matching MultiClickTriggerEvent evt = this->timing_[0]; if (evt.state == state) { - ESP_LOGV(TAG, "START min=%u max=%u", evt.min_length, evt.max_length); + ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); this->at_index_ = 1; if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { @@ -51,15 +51,15 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) { MultiClickTriggerEvent evt = this->timing_[*this->at_index_]; if (evt.max_length != 4294967294UL) { - ESP_LOGV(TAG, "A i=%u min=%u max=%u", *this->at_index_, evt.min_length, evt.max_length); // NOLINT + ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT this->schedule_is_valid_(evt.min_length); this->schedule_is_not_valid_(evt.max_length); } else if (*this->at_index_ + 1 != this->timing_.size()) { - ESP_LOGV(TAG, "B i=%u min=%u", *this->at_index_, evt.min_length); // NOLINT + ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT this->cancel_timeout("is_not_valid"); this->schedule_is_valid_(evt.min_length); } else { - ESP_LOGV(TAG, "C i=%u min=%u", *this->at_index_, evt.min_length); // NOLINT + ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT this->is_valid_ = false; this->cancel_timeout("is_not_valid"); this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); @@ -68,7 +68,8 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) { *this->at_index_ = *this->at_index_ + 1; } void binary_sensor::MultiClickTrigger::schedule_cooldown_() { - ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %u ms...", this->invalid_cooldown_); + ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms...", + this->invalid_cooldown_); this->is_in_cooldown_ = true; this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again."); @@ -97,6 +98,11 @@ void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_lengt this->schedule_cooldown_(); }); } +void binary_sensor::MultiClickTrigger::cancel() { + ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); + this->is_valid_ = false; + this->schedule_cooldown_(); +} void binary_sensor::MultiClickTrigger::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index de527d1070..12b07a05e3 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -104,6 +105,8 @@ class MultiClickTrigger : public Trigger<>, public Component { void set_invalid_cooldown(uint32_t invalid_cooldown) { this->invalid_cooldown_ = invalid_cooldown; } + void cancel(); + protected: void on_state_(bool state); void schedule_cooldown_(); diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 53c2daf42d..8f94b108ac 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -26,22 +26,20 @@ void Filter::input(bool value, bool is_initial) { } } -DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {} optional DelayedOnOffFilter::new_value(bool value, bool is_initial) { if (value) { - this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); }); + this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); } else { - this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); }); + this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); } return {}; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {} optional DelayedOnFilter::new_value(bool value, bool is_initial) { if (value) { - this->set_timeout("ON", this->delay_, [this, is_initial]() { this->output(true, is_initial); }); + this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); return {}; } else { this->cancel_timeout("ON"); @@ -51,10 +49,9 @@ optional DelayedOnFilter::new_value(bool value, bool is_initial) { float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -DelayedOffFilter::DelayedOffFilter(uint32_t delay) : delay_(delay) {} optional DelayedOffFilter::new_value(bool value, bool is_initial) { if (!value) { - this->set_timeout("OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); }); + this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); return {}; } else { this->cancel_timeout("OFF"); @@ -114,15 +111,23 @@ LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } -optional UniqueFilter::new_value(bool value, bool is_initial) { - if (this->last_value_.has_value() && *this->last_value_ == value) { +optional SettleFilter::new_value(bool value, bool is_initial) { + if (!this->steady_) { + this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() { + this->steady_ = true; + this->output(value, is_initial); + }); return {}; } else { - this->last_value_ = value; + this->steady_ = false; + this->output(value, is_initial); + this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); return value; } } +float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } + } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 64a33f6e34..f7342db2fb 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -29,38 +30,40 @@ class Filter { class DelayedOnOffFilter : public Filter, public Component { public: - explicit DelayedOnOffFilter(uint32_t delay); - optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; + template void set_on_delay(T delay) { this->on_delay_ = delay; } + template void set_off_delay(T delay) { this->off_delay_ = delay; } + protected: - uint32_t delay_; + TemplatableValue on_delay_{}; + TemplatableValue off_delay_{}; }; class DelayedOnFilter : public Filter, public Component { public: - explicit DelayedOnFilter(uint32_t delay); - optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; + template void set_delay(T delay) { this->delay_ = delay; } + protected: - uint32_t delay_; + TemplatableValue delay_{}; }; class DelayedOffFilter : public Filter, public Component { public: - explicit DelayedOffFilter(uint32_t delay); - optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; + template void set_delay(T delay) { this->delay_ = delay; } + protected: - uint32_t delay_; + TemplatableValue delay_{}; }; class InvertFilter : public Filter { @@ -105,12 +108,17 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; -class UniqueFilter : public Filter { +class SettleFilter : public Filter, public Component { public: optional new_value(bool value, bool is_initial) override; + float get_setup_priority() const override; + + template void set_delay(T delay) { this->delay_ = delay; } + protected: - optional last_value_{}; + TemplatableValue delay_{}; + bool steady_{true}; }; } // namespace binary_sensor diff --git a/esphome/components/bk72xx/__init__.py b/esphome/components/bk72xx/__init__.py new file mode 100644 index 0000000000..6737631ac7 --- /dev/null +++ b/esphome/components/bk72xx/__init__.py @@ -0,0 +1,51 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. +# For custom pin validators, put validate_pin() or validate_usage() +# in gpio.py file in this directory. +# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA +# in schema.py file in this directory. + +from esphome import pins +from esphome.components import libretiny +from esphome.components.libretiny.const import ( + COMPONENT_BK72XX, + KEY_COMPONENT_DATA, + KEY_LIBRETINY, + LibreTinyComponent, +) +from esphome.core import CORE + +from .boards import BK72XX_BOARDS, BK72XX_BOARD_PINS + +CODEOWNERS = ["@kuba2k2"] +AUTO_LOAD = ["libretiny"] + +COMPONENT_DATA = LibreTinyComponent( + name=COMPONENT_BK72XX, + boards=BK72XX_BOARDS, + board_pins=BK72XX_BOARD_PINS, + pin_validation=None, + usage_validation=None, +) + + +def _set_core_data(config): + CORE.data[KEY_LIBRETINY] = {} + CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA + return config + + +CONFIG_SCHEMA = libretiny.BASE_SCHEMA + +PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA + +CONFIG_SCHEMA.prepend_extra(_set_core_data) + + +async def to_code(config): + return await libretiny.component_to_code(config) + + +@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA) +async def pin_to_code(config): + return await libretiny.gpio.component_pin_to_code(config) diff --git a/esphome/components/bk72xx/boards.py b/esphome/components/bk72xx/boards.py new file mode 100644 index 0000000000..8e3e8a97a2 --- /dev/null +++ b/esphome/components/bk72xx/boards.py @@ -0,0 +1,1264 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. + +from esphome.components.libretiny.const import ( + FAMILY_BK7231N, + FAMILY_BK7231Q, + FAMILY_BK7231T, + FAMILY_BK7251, +) + +BK72XX_BOARDS = { + "cb1s": { + "name": "CB1S Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "cb2l": { + "name": "CB2L Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "cb2s": { + "name": "CB2S Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "cb3l": { + "name": "CB3L Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "cb3s": { + "name": "CB3S Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "cb3se": { + "name": "CB3SE Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "cblc5": { + "name": "CBLC5 Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "cbu": { + "name": "CBU Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "generic-bk7231n-qfn32-tuya": { + "name": "Generic - BK7231N (Tuya QFN32)", + "family": FAMILY_BK7231N, + }, + "generic-bk7231t-qfn32-tuya": { + "name": "Generic - BK7231T (Tuya QFN32)", + "family": FAMILY_BK7231T, + }, + "generic-bk7252": { + "name": "Generic - BK7252", + "family": FAMILY_BK7251, + }, + "lsc-lma35-t": { + "name": "LSC LMA35 BK7231T", + "family": FAMILY_BK7231T, + }, + "lsc-lma35": { + "name": "LSC LMA35 BK7231N", + "family": FAMILY_BK7231N, + }, + "wa2": { + "name": "WA2 Wi-Fi Module", + "family": FAMILY_BK7231Q, + }, + "wb1s": { + "name": "WB1S Wi-Fi Module", + "family": FAMILY_BK7231T, + }, + "wb2l-m1": { + "name": "WB2L_M1 Wi-Fi Module", + "family": FAMILY_BK7231N, + }, + "wb2l": { + "name": "WB2L Wi-Fi Module", + "family": FAMILY_BK7231T, + }, + "wb2s": { + "name": "WB2S Wi-Fi Module", + "family": FAMILY_BK7231T, + }, + "wb3l": { + "name": "WB3L Wi-Fi Module", + "family": FAMILY_BK7231T, + }, + "wb3s": { + "name": "WB3S Wi-Fi Module", + "family": FAMILY_BK7231T, + }, + "wblc5": { + "name": "WBLC5 Wi-Fi Module", + "family": FAMILY_BK7231T, + }, +} + +BK72XX_BOARD_PINS = { + "cb1s": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 11, + "D1": 10, + "D2": 6, + "D3": 7, + "D4": 0, + "D5": 9, + "D6": 8, + "D7": 1, + "D8": 24, + "D9": 26, + "D10": 23, + "D11": 20, + "D12": 21, + "D13": 22, + "A0": 23, + }, + "cb2l": { + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_TX": 0, + "P0": 0, + "P6": 6, + "P7": 7, + "P8": 8, + "P10": 10, + "P11": 11, + "P21": 21, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "SCL2": 0, + "SDA1": 21, + "TX1": 11, + "TX2": 0, + "D0": 8, + "D1": 7, + "D2": 6, + "D3": 26, + "D4": 24, + "D5": 10, + "D6": 0, + "D7": 11, + "D8": 21, + }, + "cb2s": { + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P10": 10, + "P11": 11, + "P21": 21, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 6, + "D1": 7, + "D2": 8, + "D3": 23, + "D4": 10, + "D5": 11, + "D6": 24, + "D7": 26, + "D8": 0, + "D9": 1, + "D10": 21, + "A0": 23, + }, + "cb3l": { + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P21": 21, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "SCK": 14, + "SCL2": 0, + "SDA1": 21, + "TX1": 11, + "TX2": 0, + "D0": 23, + "D1": 14, + "D2": 26, + "D3": 24, + "D4": 6, + "D5": 9, + "D6": 0, + "D7": 21, + "D8": 8, + "D9": 7, + "D10": 10, + "D11": 11, + "A0": 23, + }, + "cb3s": { + "WIRE1_SCL": 20, + "WIRE1_SDA_0": 21, + "WIRE1_SDA_1": 21, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "SCK": 14, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "TX1": 11, + "TX2": 0, + "D0": 23, + "D1": 14, + "D2": 26, + "D3": 24, + "D4": 6, + "D5": 9, + "D6": 0, + "D7": 21, + "D8": 8, + "D9": 7, + "D10": 10, + "D11": 11, + "D12": 22, + "D13": 20, + "A0": 23, + }, + "cb3se": { + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL1": 20, + "SCL2": 0, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 23, + "D1": 14, + "D2": 26, + "D3": 24, + "D4": 6, + "D5": 9, + "D6": 0, + "D7": 1, + "D8": 8, + "D9": 7, + "D10": 10, + "D11": 11, + "D12": 15, + "D13": 22, + "D14": 20, + "D15": 17, + "D16": 16, + "A0": 23, + }, + "cblc5": { + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "P0": 0, + "P1": 1, + "P6": 6, + "P10": 10, + "P11": 11, + "P21": 21, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 24, + "D1": 6, + "D2": 26, + "D3": 11, + "D4": 10, + "D5": 1, + "D6": 0, + "D7": 21, + }, + "cbu": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "P28": 28, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 14, + "D1": 16, + "D2": 20, + "D3": 22, + "D4": 23, + "D5": 1, + "D6": 0, + "D7": 8, + "D8": 7, + "D9": 6, + "D10": 26, + "D11": 24, + "D12": 11, + "D13": 10, + "D14": 28, + "D15": 9, + "D16": 17, + "D17": 15, + "D18": 21, + "A0": 23, + }, + "generic-bk7231n-qfn32-tuya": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "P28": 28, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 0, + "D1": 1, + "D2": 6, + "D3": 7, + "D4": 8, + "D5": 9, + "D6": 10, + "D7": 11, + "D8": 14, + "D9": 15, + "D10": 16, + "D11": 17, + "D12": 20, + "D13": 21, + "D14": 22, + "D15": 23, + "D16": 24, + "D17": 26, + "D18": 28, + "A0": 23, + }, + "generic-bk7231t-qfn32-tuya": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "P28": 28, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 0, + "D1": 1, + "D2": 6, + "D3": 7, + "D4": 8, + "D5": 9, + "D6": 10, + "D7": 11, + "D8": 14, + "D9": 15, + "D10": 16, + "D11": 17, + "D12": 20, + "D13": 21, + "D14": 22, + "D15": 23, + "D16": 24, + "D17": 26, + "D18": 28, + "A0": 23, + }, + "generic-bk7252": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_CTS": 12, + "SERIAL1_RTS": 13, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC1": 4, + "ADC2": 5, + "ADC3": 23, + "ADC4": 2, + "ADC5": 3, + "ADC6": 12, + "ADC7": 13, + "CS": 15, + "CTS1": 12, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P2": 2, + "P3": 3, + "P4": 4, + "P5": 5, + "P6": 6, + "P7": 7, + "P10": 10, + "P11": 11, + "P12": 12, + "P13": 13, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P18": 18, + "P19": 19, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P25": 25, + "P26": 26, + "P27": 27, + "P28": 28, + "P29": 29, + "P30": 30, + "P31": 31, + "P32": 32, + "P33": 33, + "P34": 34, + "P35": 35, + "P36": 36, + "P37": 37, + "P38": 38, + "P39": 39, + "PWM0": 6, + "PWM1": 7, + "PWM4": 24, + "PWM5": 26, + "RTS1": 13, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 0, + "D1": 1, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 5, + "D6": 6, + "D7": 7, + "D8": 10, + "D9": 11, + "D10": 12, + "D11": 13, + "D12": 14, + "D13": 15, + "D14": 16, + "D15": 17, + "D16": 18, + "D17": 19, + "D18": 20, + "D19": 21, + "D20": 22, + "D21": 23, + "D22": 24, + "D23": 25, + "D24": 26, + "D25": 27, + "D26": 28, + "D27": 29, + "D28": 30, + "D29": 31, + "D30": 32, + "D31": 33, + "D32": 34, + "D33": 35, + "D34": 36, + "D35": 37, + "D36": 38, + "D37": 39, + "A1": 4, + "A2": 5, + "A3": 23, + "A4": 3, + "A5": 2, + "A6": 12, + "A7": 13, + }, + "lsc-lma35-t": { + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P16": 16, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 26, + "D1": 14, + "D2": 16, + "D3": 24, + "D4": 22, + "D5": 0, + "D6": 23, + "D7": 8, + "D8": 9, + "D9": 21, + "D10": 6, + "D11": 7, + "D12": 10, + "D13": 11, + "D14": 1, + "A0": 23, + }, + "lsc-lma35": { + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P16": 16, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 26, + "D1": 14, + "D2": 16, + "D3": 24, + "D4": 22, + "D5": 0, + "D6": 23, + "D7": 8, + "D8": 9, + "D9": 21, + "D10": 6, + "D11": 7, + "D12": 10, + "D13": 11, + "D14": 1, + "A0": 23, + }, + "wa2": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_TX": 0, + "ADC1": 4, + "ADC3": 23, + "P0": 0, + "P4": 4, + "P6": 6, + "P7": 7, + "P8": 8, + "P10": 10, + "P11": 11, + "P18": 18, + "P19": 19, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM4": 18, + "PWM5": 19, + "RX1": 10, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "TX1": 11, + "TX2": 0, + "D0": 8, + "D1": 7, + "D2": 6, + "D3": 23, + "D4": 10, + "D5": 11, + "D6": 18, + "D7": 19, + "D8": 20, + "D9": 4, + "D10": 0, + "D11": 21, + "D12": 22, + "A0": 23, + }, + "wb1s": { + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL2": 0, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 11, + "D1": 10, + "D2": 26, + "D3": 24, + "D4": 0, + "D5": 8, + "D6": 7, + "D7": 1, + "D8": 9, + "D9": 6, + "D10": 23, + "A0": 23, + }, + "wb2l-m1": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P10": 10, + "P11": 11, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 8, + "D1": 7, + "D2": 6, + "D3": 26, + "D4": 24, + "D5": 10, + "D6": 11, + "D7": 1, + "D8": 0, + "D9": 20, + "D10": 21, + "D11": 23, + "D12": 22, + "A0": 23, + }, + "wb2l": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P10": 10, + "P11": 11, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 8, + "D1": 7, + "D2": 6, + "D3": 26, + "D4": 24, + "D5": 10, + "D6": 11, + "D7": 1, + "D8": 0, + "D9": 20, + "D10": 21, + "D11": 23, + "D12": 22, + "A0": 23, + }, + "wb2s": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 8, + "D1": 7, + "D2": 6, + "D3": 23, + "D4": 10, + "D5": 11, + "D6": 24, + "D7": 26, + "D8": 20, + "D9": 9, + "D10": 1, + "D11": 0, + "D12": 21, + "D13": 22, + "A0": 23, + }, + "wb3l": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P16": 16, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 23, + "D1": 14, + "D2": 26, + "D3": 24, + "D4": 6, + "D5": 9, + "D6": 0, + "D7": 16, + "D8": 8, + "D9": 7, + "D10": 10, + "D11": 11, + "D12": 22, + "D13": 21, + "D14": 20, + "D15": 1, + "A0": 23, + }, + "wb3s": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 23, + "D1": 14, + "D2": 26, + "D3": 24, + "D4": 6, + "D5": 7, + "D6": 0, + "D7": 1, + "D8": 9, + "D9": 8, + "D10": 10, + "D11": 11, + "D12": 22, + "D13": 21, + "D14": 20, + "A0": 23, + }, + "wblc5": { + "WIRE1_SCL": 20, + "WIRE1_SDA": 21, + "WIRE2_SCL": 0, + "WIRE2_SDA": 1, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC3": 23, + "P0": 0, + "P1": 1, + "P6": 6, + "P10": 10, + "P11": 11, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL1": 20, + "SCL2": 0, + "SDA1": 21, + "SDA2": 1, + "TX1": 11, + "TX2": 0, + "D0": 24, + "D1": 6, + "D2": 26, + "D3": 10, + "D4": 11, + "D5": 1, + "D6": 0, + "D7": 20, + "D8": 21, + "D9": 22, + "D10": 23, + "A0": 23, + }, +} + +BOARDS = BK72XX_BOARDS diff --git a/esphome/components/bl0906/__init__.py b/esphome/components/bl0906/__init__.py new file mode 100644 index 0000000000..b077792604 --- /dev/null +++ b/esphome/components/bl0906/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@athom-tech", "@tarontop", "@jesserockz"] diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp new file mode 100644 index 0000000000..bddb62ff64 --- /dev/null +++ b/esphome/components/bl0906/bl0906.cpp @@ -0,0 +1,238 @@ +#include "bl0906.h" +#include "constants.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0906 { + +static const char *const TAG = "bl0906"; + +constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +// The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; +constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) { + return (address + data->l + data->m + data->h) ^ 0xFF; +} + +void BL0906::loop() { + if (this->current_channel_ == UINT8_MAX) { + return; + } + + while (this->available()) + this->flush(); + + if (this->current_channel_ == 0) { + // Temperature + this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_); + } else if (this->current_channel_ == 1) { + this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_); + this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_); + this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_); + } else if (this->current_channel_ == 2) { + this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_); + this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_); + this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_); + } else if (this->current_channel_ == 3) { + this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_); + this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_); + this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_); + } else if (this->current_channel_ == 4) { + this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_); + this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_); + this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_); + } else if (this->current_channel_ == 5) { + this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_); + this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_); + this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_); + } else if (this->current_channel_ == 6) { + this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_); + this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_); + this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_); + } else if (this->current_channel_ == UINT8_MAX - 2) { + // Frequency + this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_); + // Voltage + this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_); + } else if (this->current_channel_ == UINT8_MAX - 1) { + // Total power + this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_); + // Total Energy + this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_); + } else { + this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage + return; + } + this->current_channel_++; + this->handle_actions_(); +} + +void BL0906::setup() { + while (this->available()) + this->flush(); + this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE)); + // Calibration (1: register address; 2: value before calibration; 3: value after calibration) + this->bias_correction_(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1 + this->bias_correction_(BL0906_RMSOS_2, 0.01500, 0); + this->bias_correction_(BL0906_RMSOS_3, 0.01400, 0); + this->bias_correction_(BL0906_RMSOS_4, 0.01300, 0); + this->bias_correction_(BL0906_RMSOS_5, 0.01200, 0); + this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6 + + this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD)); +} + +void BL0906::update() { this->current_channel_ = 0; } + +size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) { + this->action_queue_.push_back(function); + return this->action_queue_.size(); +} + +void BL0906::handle_actions_() { + if (this->action_queue_.empty()) { + return; + } + ActionCallbackFuncPtr ptr_func = nullptr; + for (int i = 0; i < this->action_queue_.size(); i++) { + ptr_func = this->action_queue_[i]; + if (ptr_func) { + ESP_LOGI(TAG, "HandleActionCallback[%d]...", i); + (this->*ptr_func)(); + } + } + + while (this->available()) { + this->read(); + } + + this->action_queue_.clear(); +} + +// Reset energy +void BL0906::reset_energy_() { + this->write_array(BL0906_INIT[0], 6); + delay(1); + this->flush(); + + ESP_LOGW(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_INIT[0][0], BL0906_INIT[0][1], BL0906_INIT[0][2], + BL0906_INIT[0][3], BL0906_INIT[0][4], BL0906_INIT[0][5]); +} + +// Read data +void BL0906::read_data_(const uint8_t address, const float reference, sensor::Sensor *sensor) { + if (sensor == nullptr) { + return; + } + DataPacket buffer; + ube24_t data_u24; + sbe24_t data_s24; + float value = 0; + + bool signed_result = reference == BL0906_TREF || reference == BL0906_WATT || reference == BL0906_PREF; + + this->write_byte(BL0906_READ_COMMAND); + this->write_byte(address); + if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { + if (bl0906_checksum(address, &buffer) == buffer.checksum) { + if (signed_result) { + data_s24.l = buffer.l; + data_s24.m = buffer.m; + data_s24.h = buffer.h; + } else { + data_u24.l = buffer.l; + data_u24.m = buffer.m; + data_u24.h = buffer.h; + } + } else { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + return; + } + } + // Power + if (reference == BL0906_PREF) { + value = (float) to_int32_t(data_s24) * reference; + } + + // Total power + if (reference == BL0906_WATT) { + value = (float) to_int32_t(data_s24) * reference; + } + + // Voltage, current, power, total power + if (reference == BL0906_UREF || reference == BL0906_IREF || reference == BL0906_EREF || reference == BL0906_CF) { + value = (float) to_uint32_t(data_u24) * reference; + } + + // Frequency + if (reference == BL0906_FREF) { + value = reference / (float) to_uint32_t(data_u24); + } + // Chip temperature + if (reference == BL0906_TREF) { + value = (float) to_int32_t(data_s24); + value = (value - 64) * 12.5 / 59 - 40; + } + sensor->publish_state(value); +} + +// RMS offset correction +void BL0906::bias_correction_(uint8_t address, float measurements, float correction) { + DataPacket data; + float ki = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097; // Current coefficient + float i_rms0 = measurements * ki; + float i_rms = correction * ki; + int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256; + data.l = value << 24 >> 24; + data.m = value << 16 >> 24; + if (value < 0) { + data.h = (value << 8 >> 24) | 0b10000000; + } + data.address = bl0906_checksum(address, &data); + ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); + this->write_byte(BL0906_WRITE_COMMAND); + this->write_byte(address); + this->write_byte(data.l); + this->write_byte(data.m); + this->write_byte(data.h); + this->write_byte(data.address); +} + +void BL0906::dump_config() { + ESP_LOGCONFIG(TAG, "BL0906:"); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + + LOG_SENSOR(" ", "Current1", this->current_1_sensor_); + LOG_SENSOR(" ", "Current2", this->current_2_sensor_); + LOG_SENSOR(" ", "Current3", this->current_3_sensor_); + LOG_SENSOR(" ", "Current4", this->current_4_sensor_); + LOG_SENSOR(" ", "Current5", this->current_5_sensor_); + LOG_SENSOR(" ", "Current6", this->current_6_sensor_); + + LOG_SENSOR(" ", "Power1", this->power_1_sensor_); + LOG_SENSOR(" ", "Power2", this->power_2_sensor_); + LOG_SENSOR(" ", "Power3", this->power_3_sensor_); + LOG_SENSOR(" ", "Power4", this->power_4_sensor_); + LOG_SENSOR(" ", "Power5", this->power_5_sensor_); + LOG_SENSOR(" ", "Power6", this->power_6_sensor_); + + LOG_SENSOR(" ", "Energy1", this->energy_1_sensor_); + LOG_SENSOR(" ", "Energy2", this->energy_2_sensor_); + LOG_SENSOR(" ", "Energy3", this->energy_3_sensor_); + LOG_SENSOR(" ", "Energy4", this->energy_4_sensor_); + LOG_SENSOR(" ", "Energy5", this->energy_5_sensor_); + LOG_SENSOR(" ", "Energy6", this->energy_6_sensor_); + + LOG_SENSOR(" ", "Total Power", this->total_power_sensor_); + LOG_SENSOR(" ", "Total Energy", this->total_energy_sensor_); + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); +} + +} // namespace bl0906 +} // namespace esphome diff --git a/esphome/components/bl0906/bl0906.h b/esphome/components/bl0906/bl0906.h new file mode 100644 index 0000000000..5a9ad0f028 --- /dev/null +++ b/esphome/components/bl0906/bl0906.h @@ -0,0 +1,96 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/datatypes.h" + +// https://www.belling.com.cn/media/file_object/bel_product/BL0906/datasheet/BL0906_V1.02_cn.pdf +// https://www.belling.com.cn/media/file_object/bel_product/BL0906/guide/BL0906%20APP%20Note_V1.02.pdf + +namespace esphome { +namespace bl0906 { + +struct DataPacket { // NOLINT(altera-struct-pack-align) + uint8_t l{0}; + uint8_t m{0}; + uint8_t h{0}; + uint8_t checksum; // checksum + uint8_t address; +} __attribute__((packed)); + +struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l{0}; + uint8_t m{0}; + uint8_t h{0}; +} __attribute__((packed)); + +struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l{0}; + uint8_t m{0}; + int8_t h{0}; +} __attribute__((packed)); + +template class ResetEnergyAction; + +class BL0906; + +using ActionCallbackFuncPtr = void (BL0906::*)(); + +class BL0906 : public PollingComponent, public uart::UARTDevice { + SUB_SENSOR(voltage) + SUB_SENSOR(current_1) + SUB_SENSOR(current_2) + SUB_SENSOR(current_3) + SUB_SENSOR(current_4) + SUB_SENSOR(current_5) + SUB_SENSOR(current_6) + SUB_SENSOR(power_1) + SUB_SENSOR(power_2) + SUB_SENSOR(power_3) + SUB_SENSOR(power_4) + SUB_SENSOR(power_5) + SUB_SENSOR(power_6) + SUB_SENSOR(total_power) + SUB_SENSOR(energy_1) + SUB_SENSOR(energy_2) + SUB_SENSOR(energy_3) + SUB_SENSOR(energy_4) + SUB_SENSOR(energy_5) + SUB_SENSOR(energy_6) + SUB_SENSOR(total_energy) + SUB_SENSOR(frequency) + SUB_SENSOR(temperature) + + public: + void loop() override; + + void update() override; + void setup() override; + void dump_config() override; + + protected: + template friend class ResetEnergyAction; + + void reset_energy_(); + + void read_data_(uint8_t address, float reference, sensor::Sensor *sensor); + + void bias_correction_(uint8_t address, float measurements, float correction); + + uint8_t current_channel_{0}; + size_t enqueue_action_(ActionCallbackFuncPtr function); + void handle_actions_(); + + private: + std::vector action_queue_{}; +}; + +template class ResetEnergyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } +}; + +} // namespace bl0906 +} // namespace esphome diff --git a/esphome/components/bl0906/const.py b/esphome/components/bl0906/const.py new file mode 100644 index 0000000000..67f21d35b0 --- /dev/null +++ b/esphome/components/bl0906/const.py @@ -0,0 +1,4 @@ +# const.py +ICON_ENERGY = "mdi:lightning-bolt" +ICON_FREQUENCY = "mdi:cosine-wave" +ICON_VOLTAGE = "mdi:sine-wave" diff --git a/esphome/components/bl0906/constants.h b/esphome/components/bl0906/constants.h new file mode 100644 index 0000000000..546916aa3c --- /dev/null +++ b/esphome/components/bl0906/constants.h @@ -0,0 +1,122 @@ +#pragma once +#include + +namespace esphome { +namespace bl0906 { + +// Total power conversion +static const float BL0906_WATT = 16 * 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / + (40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000); +// Total Energy conversion +static const float BL0906_CF = 16 * 4194304 * 0.032768 * 16 / + (3600000 * 16 * + (40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / + (1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000)))); +// Frequency conversion +static const float BL0906_FREF = 10000000; +// Temperature conversion +static const float BL0906_TREF = 12.5 / 59 - 40; +// Current conversion +static const float BL0906_IREF = 1.097 / (12875 * 1 * (5.1 + 5.1) * 1000 / 2000); +// Voltage conversion +static const float BL0906_UREF = 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / (13162 * 1 * 100 * 1000); +// Power conversion +static const float BL0906_PREF = 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / + (40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000); +// Energy conversion +static const float BL0906_EREF = 4194304 * 0.032768 * 16 / + (3600000 * 16 * + (40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / + (1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000)))); +// Current coefficient +static const float BL0906_KI = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097; +// Power coefficient +static const float BL0906_KP = 40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / 1.097 / 1.097 / + (20000 + 20000 + 20000 + 20000 + 20000); + +static const uint8_t USR_WRPROT_WITABLE[6] = {0xCA, 0x9E, 0x55, 0x55, 0x00, 0xB7}; +static const uint8_t USR_WRPROT_ONLYREAD[6] = {0xCA, 0x9E, 0x00, 0x00, 0x00, 0x61}; + +static const uint8_t BL0906_READ_COMMAND = 0x35; +static const uint8_t BL0906_WRITE_COMMAND = 0xCA; + +// Register address +// Voltage +static const uint8_t BL0906_V_RMS = 0x16; + +// Total power +static const uint8_t BL0906_WATT_SUM = 0X2C; + +// Current1~6 +static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1 +static const uint8_t BL0906_I_2_RMS = 0x0E; +static const uint8_t BL0906_I_3_RMS = 0x0F; +static const uint8_t BL0906_I_4_RMS = 0x10; +static const uint8_t BL0906_I_5_RMS = 0x13; +static const uint8_t BL0906_I_6_RMS = 0x14; // current_6 + +// Power1~6 +static const uint8_t BL0906_WATT_1 = 0X23; // power_1 +static const uint8_t BL0906_WATT_2 = 0X24; +static const uint8_t BL0906_WATT_3 = 0X25; +static const uint8_t BL0906_WATT_4 = 0X26; +static const uint8_t BL0906_WATT_5 = 0X29; +static const uint8_t BL0906_WATT_6 = 0X2A; // power_6 + +// Active pulse count, unsigned +static const uint8_t BL0906_CF_1_CNT = 0X30; // Channel_1 +static const uint8_t BL0906_CF_2_CNT = 0X31; +static const uint8_t BL0906_CF_3_CNT = 0X32; +static const uint8_t BL0906_CF_4_CNT = 0X33; +static const uint8_t BL0906_CF_5_CNT = 0X36; +static const uint8_t BL0906_CF_6_CNT = 0X37; // Channel_6 + +// Total active pulse count, unsigned +static const uint8_t BL0906_CF_SUM_CNT = 0X39; + +// Voltage frequency cycle +static const uint8_t BL0906_FREQUENCY = 0X4E; + +// Internal temperature +static const uint8_t BL0906_TEMPERATURE = 0X5E; + +// Calibration register +// RMS gain adjustment register +static const uint8_t BL0906_RMSGN_1 = 0x6D; // Channel_1 +static const uint8_t BL0906_RMSGN_2 = 0x6E; +static const uint8_t BL0906_RMSGN_3 = 0x6F; +static const uint8_t BL0906_RMSGN_4 = 0x70; +static const uint8_t BL0906_RMSGN_5 = 0x73; +static const uint8_t BL0906_RMSGN_6 = 0x74; // Channel_6 + +// RMS offset correction register +static const uint8_t BL0906_RMSOS_1 = 0x78; // Channel_1 +static const uint8_t BL0906_RMSOS_2 = 0x79; +static const uint8_t BL0906_RMSOS_3 = 0x7A; +static const uint8_t BL0906_RMSOS_4 = 0x7B; +static const uint8_t BL0906_RMSOS_5 = 0x7E; +static const uint8_t BL0906_RMSOS_6 = 0x7F; // Channel_6 + +// Active power gain adjustment register +static const uint8_t BL0906_WATTGN_1 = 0xB7; // Channel_1 +static const uint8_t BL0906_WATTGN_2 = 0xB8; +static const uint8_t BL0906_WATTGN_3 = 0xB9; +static const uint8_t BL0906_WATTGN_4 = 0xBA; +static const uint8_t BL0906_WATTGN_5 = 0xBD; +static const uint8_t BL0906_WATTGN_6 = 0xBE; // Channel_6 + +// User write protection setting register, +// You must first write 0x5555 to the write protection setting register before writing to other registers. +static const uint8_t BL0906_USR_WRPROT = 0x9E; + +// Reset Register +static const uint8_t BL0906_SOFT_RESET = 0x9F; + +const uint8_t BL0906_INIT[2][6] = { + // Reset to default + {BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52}, + // Enable User Operation Write + {BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}}; + +} // namespace bl0906 +} // namespace esphome diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py new file mode 100644 index 0000000000..42c6f06092 --- /dev/null +++ b/esphome/components/bl0906/sensor.py @@ -0,0 +1,185 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_CHANNEL, + CONF_CURRENT, + CONF_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_NAME, + CONF_POWER, + CONF_TEMPERATURE, + CONF_TOTAL_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_POWER, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_KILOWATT_HOURS, + UNIT_VOLT, + UNIT_WATT, +) + +# Import ICONS not included in esphome's const.py, from the local components const.py +from .const import ICON_ENERGY, ICON_FREQUENCY, ICON_VOLTAGE + +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["bl0906"] +CONF_TOTAL_ENERGY = "total_energy" + +bl0906_ns = cg.esphome_ns.namespace("bl0906") +BL0906 = bl0906_ns.class_("BL0906", cg.PollingComponent, uart.UARTDevice) +ResetEnergyAction = bl0906_ns.class_("ResetEnergyAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0906), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + icon=ICON_FREQUENCY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_FREQUENCY, + unit_of_measurement=UNIT_HERTZ, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=UNIT_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + icon=ICON_VOLTAGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=UNIT_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TOTAL_POWER): sensor.sensor_schema( + icon=ICON_POWER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=UNIT_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TOTAL_ENERGY): sensor.sensor_schema( + icon=ICON_ENERGY, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + ), + } + ) + .extend( + cv.Schema( + { + cv.Optional(f"{CONF_CHANNEL}_{i + 1}"): cv.Schema( + { + cv.Optional(CONF_CURRENT): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_CURRENT_AC, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=UNIT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_POWER): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_POWER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=UNIT_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ENERGY): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_ENERGY, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=UNIT_KILOWATT_HOURS, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + key=CONF_NAME, + ), + } + ) + for i in range(6) + } + ) + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.polling_component_schema("60s")) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "bl0906", baud_rate=19200, require_tx=True, require_rx=True +) + + +@automation.register_action( + "bl0906.reset_energy", + ResetEnergyAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(BL0906), + } + ), +) +async def reset_energy_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 uart.register_uart_device(var, config) + if frequency_config := config.get(CONF_FREQUENCY): + sens = await sensor.new_sensor(frequency_config) + cg.add(var.set_frequency_sensor(sens)) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) + cg.add(var.set_voltage_sensor(sens)) + + for i in range(6): + if channel_config := config.get(f"{CONF_CHANNEL}_{i + 1}"): + if current_config := channel_config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) + cg.add(getattr(var, f"set_current_{i + 1}_sensor")(sens)) + if power_config := channel_config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) + cg.add(getattr(var, f"set_power_{i + 1}_sensor")(sens)) + if energy_config := channel_config.get(CONF_ENERGY): + sens = await sensor.new_sensor(energy_config) + cg.add(getattr(var, f"set_energy_{i + 1}_sensor")(sens)) + + if total_power_config := config.get(CONF_TOTAL_POWER): + sens = await sensor.new_sensor(total_power_config) + cg.add(var.set_total_power_sensor(sens)) + + if total_energy_config := config.get(CONF_TOTAL_ENERGY): + sens = await sensor.new_sensor(total_energy_config) + cg.add(var.set_total_energy_sensor(sens)) diff --git a/esphome/components/bl0939/bl0939.cpp b/esphome/components/bl0939/bl0939.cpp index 0575507c46..7428e48740 100644 --- a/esphome/components/bl0939/bl0939.cpp +++ b/esphome/components/bl0939/bl0939.cpp @@ -1,5 +1,6 @@ #include "bl0939.h" #include "esphome/core/log.h" +#include namespace esphome { namespace bl0939 { @@ -80,7 +81,7 @@ void BL0939::setup() { void BL0939::received_package_(const DataPacket *data) const { // Bad header if (data->frame_header != BL0939_PACKET_HEADER) { - ESP_LOGI("bl0939", "Invalid data. Header mismatch: %d", data->frame_header); + ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); return; } @@ -120,8 +121,9 @@ void BL0939::received_package_(const DataPacket *data) const { energy_sensor_sum_->publish_state(total_energy_consumption); } - ESP_LOGV("bl0939", "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %d, CntB %d, ∫P1 %fkWh, ∫P2 %fkWh", v_rms, - ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption); + ESP_LOGV(TAG, + "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %" PRId32 ", CntB %" PRId32 ", ∫P1 %fkWh, ∫P2 %fkWh", + v_rms, ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption); } void BL0939::dump_config() { // NOLINT(readability-function-cognitive-complexity) diff --git a/esphome/components/bl0939/sensor.py b/esphome/components/bl0939/sensor.py index 4c6e3ea4d9..2a85b34567 100644 --- a/esphome/components/bl0939/sensor.py +++ b/esphome/components/bl0939/sensor.py @@ -93,35 +93,27 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_VOLTAGE in config: - conf = config[CONF_VOLTAGE] - sens = await sensor.new_sensor(conf) + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) cg.add(var.set_voltage_sensor(sens)) - if CONF_CURRENT_1 in config: - conf = config[CONF_CURRENT_1] - sens = await sensor.new_sensor(conf) + if current_1_config := config.get(CONF_CURRENT_1): + sens = await sensor.new_sensor(current_1_config) cg.add(var.set_current_sensor_1(sens)) - if CONF_CURRENT_2 in config: - conf = config[CONF_CURRENT_2] - sens = await sensor.new_sensor(conf) + if current_2_config := config.get(CONF_CURRENT_2): + sens = await sensor.new_sensor(current_2_config) cg.add(var.set_current_sensor_2(sens)) - if CONF_ACTIVE_POWER_1 in config: - conf = config[CONF_ACTIVE_POWER_1] - sens = await sensor.new_sensor(conf) + if active_power_1_config := config.get(CONF_ACTIVE_POWER_1): + sens = await sensor.new_sensor(active_power_1_config) cg.add(var.set_power_sensor_1(sens)) - if CONF_ACTIVE_POWER_2 in config: - conf = config[CONF_ACTIVE_POWER_2] - sens = await sensor.new_sensor(conf) + if active_power_2_config := config.get(CONF_ACTIVE_POWER_2): + sens = await sensor.new_sensor(active_power_2_config) cg.add(var.set_power_sensor_2(sens)) - if CONF_ENERGY_1 in config: - conf = config[CONF_ENERGY_1] - sens = await sensor.new_sensor(conf) + if energy_1_config := config.get(CONF_ENERGY_1): + sens = await sensor.new_sensor(energy_1_config) cg.add(var.set_energy_sensor_1(sens)) - if CONF_ENERGY_2 in config: - conf = config[CONF_ENERGY_2] - sens = await sensor.new_sensor(conf) + if energy_2_config := config.get(CONF_ENERGY_2): + sens = await sensor.new_sensor(energy_2_config) cg.add(var.set_energy_sensor_2(sens)) - if CONF_ENERGY_TOTAL in config: - conf = config[CONF_ENERGY_TOTAL] - sens = await sensor.new_sensor(conf) + if energy_total_config := config.get(CONF_ENERGY_TOTAL): + sens = await sensor.new_sensor(energy_total_config) cg.add(var.set_energy_sensor_sum(sens)) diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index ed193b23f3..24990d5482 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -1,5 +1,6 @@ #include "bl0940.h" #include "esphome/core/log.h" +#include namespace esphome { namespace bl0940 { @@ -77,7 +78,7 @@ float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; if (sensor != nullptr) { if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { - ESP_LOGD("bl0940", "Invalid temperature change. Sensor: '%s', Old temperature: %f, New temperature: %f", + ESP_LOGD(TAG, "Invalid temperature change. Sensor: '%s', Old temperature: %f, New temperature: %f", sensor->get_name().c_str(), sensor->get_state(), converted_temp); return 0.0f; } @@ -89,7 +90,7 @@ float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { void BL0940::received_package_(const DataPacket *data) const { // Bad header if (data->frame_header != BL0940_PACKET_HEADER) { - ESP_LOGI("bl0940", "Invalid data. Header mismatch: %d", data->frame_header); + ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); return; } @@ -115,7 +116,7 @@ void BL0940::received_package_(const DataPacket *data) const { energy_sensor_->publish_state(total_energy_consumption); } - ESP_LOGV("bl0940", "BL0940: U %fV, I %fA, P %fW, Cnt %d, ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, + ESP_LOGV(TAG, "BL0940: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, total_energy_consumption, tps1, tps2); } diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index 9f516a8691..5cb1472d76 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -4,7 +4,9 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_CURRENT, CONF_ENERGY, + CONF_EXTERNAL_TEMPERATURE, CONF_ID, + CONF_INTERNAL_TEMPERATURE, CONF_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, @@ -18,12 +20,11 @@ from esphome.const import ( UNIT_KILOWATT_HOURS, UNIT_VOLT, UNIT_WATT, + STATE_CLASS_TOTAL_INCREASING, ) DEPENDENCIES = ["uart"] -CONF_INTERNAL_TEMPERATURE = "internal_temperature" -CONF_EXTERNAL_TEMPERATURE = "external_temperature" bl0940_ns = cg.esphome_ns.namespace("bl0940") BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) @@ -54,6 +55,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, @@ -79,27 +81,21 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_VOLTAGE in config: - conf = config[CONF_VOLTAGE] - sens = await sensor.new_sensor(conf) + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) cg.add(var.set_voltage_sensor(sens)) - if CONF_CURRENT in config: - conf = config[CONF_CURRENT] - sens = await sensor.new_sensor(conf) + if current_config := config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) cg.add(var.set_current_sensor(sens)) - if CONF_POWER in config: - conf = config[CONF_POWER] - sens = await sensor.new_sensor(conf) + if power_config := config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) cg.add(var.set_power_sensor(sens)) - if CONF_ENERGY in config: - conf = config[CONF_ENERGY] - sens = await sensor.new_sensor(conf) + if energy_config := config.get(CONF_ENERGY): + sens = await sensor.new_sensor(energy_config) cg.add(var.set_energy_sensor(sens)) - if CONF_INTERNAL_TEMPERATURE in config: - conf = config[CONF_INTERNAL_TEMPERATURE] - sens = await sensor.new_sensor(conf) + if internal_temperature_config := config.get(CONF_INTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(internal_temperature_config) cg.add(var.set_internal_temperature_sensor(sens)) - if CONF_EXTERNAL_TEMPERATURE in config: - conf = config[CONF_EXTERNAL_TEMPERATURE] - sens = await sensor.new_sensor(conf) + if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(external_temperature_config) cg.add(var.set_external_temperature_sensor(sens)) diff --git a/esphome/components/bl0942/__init__.py b/esphome/components/bl0942/__init__.py index 8ef7857b7b..38b68d84b5 100644 --- a/esphome/components/bl0942/__init__.py +++ b/esphome/components/bl0942/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@dbuezas"] +CODEOWNERS = ["@dbuezas", "@dwmw2"] diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index e6d18a82a7..e6f96c1b19 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -1,5 +1,8 @@ #include "bl0942.h" #include "esphome/core/log.h" +#include + +// Datasheet: https://www.belling.com.cn/media/file_object/bel_product/BL0942/datasheet/BL0942_V1.06_en.pdf namespace esphome { namespace bl0942 { @@ -11,43 +14,64 @@ static const uint8_t BL0942_FULL_PACKET = 0xAA; static const uint8_t BL0942_PACKET_HEADER = 0x55; static const uint8_t BL0942_WRITE_COMMAND = 0xA8; -static const uint8_t BL0942_REG_I_FAST_RMS_CTRL = 0x10; -static const uint8_t BL0942_REG_MODE = 0x18; -static const uint8_t BL0942_REG_SOFT_RESET = 0x19; -static const uint8_t BL0942_REG_USR_WRPROT = 0x1A; + +static const uint8_t BL0942_REG_I_RMSOS = 0x12; +static const uint8_t BL0942_REG_WA_CREEP = 0x14; +static const uint8_t BL0942_REG_I_FAST_RMS_TH = 0x15; +static const uint8_t BL0942_REG_I_FAST_RMS_CYC = 0x16; +static const uint8_t BL0942_REG_FREQ_CYC = 0x17; +static const uint8_t BL0942_REG_OT_FUNX = 0x18; +static const uint8_t BL0942_REG_MODE = 0x19; +static const uint8_t BL0942_REG_SOFT_RESET = 0x1C; +static const uint8_t BL0942_REG_USR_WRPROT = 0x1D; static const uint8_t BL0942_REG_TPS_CTRL = 0x1B; -// TODO: Confirm insialisation works as intended -const uint8_t BL0942_INIT[5][6] = { - // Reset to default - {BL0942_WRITE_COMMAND, BL0942_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, - // Enable User Operation Write - {BL0942_WRITE_COMMAND, BL0942_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, - // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS - {BL0942_WRITE_COMMAND, BL0942_REG_MODE, 0x00, 0x10, 0x00, 0x37}, - // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS - {BL0942_WRITE_COMMAND, BL0942_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, - // 0x181C = Half cycle, Fast RMS threshold 6172 - {BL0942_WRITE_COMMAND, BL0942_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; +static const uint32_t BL0942_REG_MODE_RESV = 0x03; +static const uint32_t BL0942_REG_MODE_CF_EN = 0x04; +static const uint32_t BL0942_REG_MODE_RMS_UPDATE_SEL = 0x08; +static const uint32_t BL0942_REG_MODE_FAST_RMS_SEL = 0x10; +static const uint32_t BL0942_REG_MODE_AC_FREQ_SEL = 0x20; +static const uint32_t BL0942_REG_MODE_CF_CNT_CLR_SEL = 0x40; +static const uint32_t BL0942_REG_MODE_CF_CNT_ADD_SEL = 0x80; +static const uint32_t BL0942_REG_MODE_UART_RATE_19200 = 0x200; +static const uint32_t BL0942_REG_MODE_UART_RATE_38400 = 0x300; +static const uint32_t BL0942_REG_MODE_DEFAULT = + BL0942_REG_MODE_RESV | BL0942_REG_MODE_CF_EN | BL0942_REG_MODE_CF_CNT_ADD_SEL; + +static const uint32_t BL0942_REG_SOFT_RESET_MAGIC = 0x5a5a5a; +static const uint32_t BL0942_REG_USR_WRPROT_MAGIC = 0x55; + +// 23-byte packet, 11 bits per byte, 2400 baud: about 105ms +static const uint32_t PKT_TIMEOUT_MS = 200; void BL0942::loop() { DataPacket buffer; - if (!this->available()) { + int avail = this->available(); + + if (!avail) { return; } - if (read_array((uint8_t *) &buffer, sizeof(buffer))) { - if (validate_checksum(&buffer)) { - received_package_(&buffer); + if (avail < sizeof(buffer)) { + if (!this->rx_start_) { + this->rx_start_ = millis(); + } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail); + this->read_array((uint8_t *) &buffer, avail); + this->rx_start_ = 0; } - } else { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); - while (read() >= 0) - ; + return; } + + if (this->read_array((uint8_t *) &buffer, sizeof(buffer))) { + if (this->validate_checksum_(&buffer)) { + this->received_package_(&buffer); + } + } + this->rx_start_ = 0; } -bool BL0942::validate_checksum(DataPacket *data) { - uint8_t checksum = BL0942_READ_COMMAND; +bool BL0942::validate_checksum_(DataPacket *data) { + uint8_t checksum = BL0942_READ_COMMAND | this->address_; // Whole package but checksum uint8_t *raw = (uint8_t *) data; for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { @@ -60,17 +84,73 @@ bool BL0942::validate_checksum(DataPacket *data) { return checksum == data->checksum; } -void BL0942::update() { +void BL0942::write_reg_(uint8_t reg, uint32_t val) { + uint8_t pkt[6]; + this->flush(); - this->write_byte(BL0942_READ_COMMAND); + pkt[0] = BL0942_WRITE_COMMAND | this->address_; + pkt[1] = reg; + pkt[2] = (val & 0xff); + pkt[3] = (val >> 8) & 0xff; + pkt[4] = (val >> 16) & 0xff; + pkt[5] = (pkt[0] + pkt[1] + pkt[2] + pkt[3] + pkt[4]) ^ 0xff; + this->write_array(pkt, 6); + delay(1); +} + +int BL0942::read_reg_(uint8_t reg) { + union { + uint8_t b[4]; + uint32_le_t le32; + } resp; + + this->write_byte(BL0942_READ_COMMAND | this->address_); + this->write_byte(reg); + this->flush(); + if (this->read_array(resp.b, 4) && + resp.b[3] == + (uint8_t) ((BL0942_READ_COMMAND + this->address_ + reg + resp.b[0] + resp.b[1] + resp.b[2]) ^ 0xff)) { + resp.b[3] = 0; + return resp.le32; + } + return -1; +} + +void BL0942::update() { + this->write_byte(BL0942_READ_COMMAND | this->address_); this->write_byte(BL0942_FULL_PACKET); } void BL0942::setup() { - for (auto *i : BL0942_INIT) { - this->write_array(i, 6); - delay(1); + // If either current or voltage references are set explicitly by the user, + // calculate the power reference from it unless that is also explicitly set. + if ((this->current_reference_set_ || this->voltage_reference_set_) && !this->power_reference_set_) { + this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0 / 305978.0) / 73989.0; + this->power_reference_set_ = true; } + + // Similarly for energy reference, if the power reference was set by the user + // either implicitly or explicitly. + if (this->power_reference_set_ && !this->energy_reference_set_) { + this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4; + this->energy_reference_set_ = true; + } + + this->write_reg_(BL0942_REG_USR_WRPROT, BL0942_REG_USR_WRPROT_MAGIC); + if (this->reset_) + this->write_reg_(BL0942_REG_SOFT_RESET, BL0942_REG_SOFT_RESET_MAGIC); + + uint32_t mode = BL0942_REG_MODE_DEFAULT; + mode |= BL0942_REG_MODE_RMS_UPDATE_SEL; /* 800ms refresh time */ + if (this->line_freq_ == LINE_FREQUENCY_60HZ) + mode |= BL0942_REG_MODE_AC_FREQ_SEL; + this->write_reg_(BL0942_REG_MODE, mode); + + this->write_reg_(BL0942_REG_USR_WRPROT, 0); + + if (this->read_reg_(BL0942_REG_MODE) != mode) + this->status_set_warning("BL0942 setup failed!"); + this->flush(); } @@ -81,10 +161,17 @@ void BL0942::received_package_(DataPacket *data) { return; } + // cf_cnt is only 24 bits, so track overflows + uint32_t cf_cnt = (uint24_t) data->cf_cnt; + cf_cnt |= this->prev_cf_cnt_ & 0xff000000; + if (cf_cnt < this->prev_cf_cnt_) { + cf_cnt += 0x1000000; + } + this->prev_cf_cnt_ = cf_cnt; + float v_rms = (uint24_t) data->v_rms / voltage_reference_; float i_rms = (uint24_t) data->i_rms / current_reference_; float watt = (int24_t) data->watt / power_reference_; - uint32_t cf_cnt = (uint24_t) data->cf_cnt; float total_energy_consumption = cf_cnt / energy_reference_; float frequency = 1000000.0f / data->frequency; @@ -103,18 +190,25 @@ void BL0942::received_package_(DataPacket *data) { if (frequency_sensor_ != nullptr) { frequency_sensor_->publish_state(frequency); } - - ESP_LOGV(TAG, "BL0942: U %fV, I %fA, P %fW, Cnt %d, ∫P %fkWh, frequency %f°Hz, status 0x%08X", v_rms, i_rms, watt, - cf_cnt, total_energy_consumption, frequency, data->status); + this->status_clear_warning(); + ESP_LOGV(TAG, "BL0942: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, frequency %fHz, status 0x%08X", v_rms, i_rms, + watt, cf_cnt, total_energy_consumption, frequency, data->status); } void BL0942::dump_config() { // NOLINT(readability-function-cognitive-complexity) ESP_LOGCONFIG(TAG, "BL0942:"); + ESP_LOGCONFIG(TAG, " Reset: %s", TRUEFALSE(this->reset_)); + ESP_LOGCONFIG(TAG, " Address: %d", this->address_); + ESP_LOGCONFIG(TAG, " Nominal line frequency: %d Hz", this->line_freq_); + ESP_LOGCONFIG(TAG, " Current reference: %f", this->current_reference_); + ESP_LOGCONFIG(TAG, " Energy reference: %f", this->energy_reference_); + ESP_LOGCONFIG(TAG, " Power reference: %f", this->power_reference_); + ESP_LOGCONFIG(TAG, " Voltage reference: %f", this->voltage_reference_); LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Power", this->power_sensor_); LOG_SENSOR("", "Energy", this->energy_sensor_); - LOG_SENSOR("", "frequency", this->frequency_sensor_); + LOG_SENSOR("", "Frequency", this->frequency_sensor_); } } // namespace bl0942 diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 12489915e1..37b884e6ca 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -8,6 +8,57 @@ namespace esphome { namespace bl0942 { +// The BL0942 IC is "calibration-free", which means that it doesn't care +// at all about calibration, and that's left to software. It measures a +// voltage differential on its IP/IN pins which linearly proportional to +// the current flow, and another on its VP pin which is proportional to +// the line voltage. It never knows the actual calibration; the values +// it reports are solely in terms of those inputs. +// +// The datasheet refers to the input voltages as I(A) and V(V), both +// in millivolts. It measures them against a reference voltage Vref, +// which is typically 1.218V (but that absolute value is meaningless +// without the actual calibration anyway). +// +// The reported I_RMS value is 305978 I(A)/Vref, and the reported V_RMS +// value is 73989 V(V)/Vref. So we can calibrate those by applying a +// simple meter with a resistive load. +// +// The chip also measures the phase difference between voltage and +// current, and uses it to calculate the power factor (cos φ). It +// reports the WATT value of 3537 * I_RMS * V_RMS * cos φ). +// +// It also integrates total energy based on the WATT value. The time for +// one CF_CNT pulse is 1638.4*256 / WATT. +// +// So... how do we calibrate that? +// +// Using a simple resistive load and an external meter, we can measure +// the true voltage and current for a given V_RMS and I_RMS reading, +// to calculate BL0942_UREF and BL0942_IREF. Those are in units of +// "305978 counts per amp" or "73989 counts per volt" respectively. +// +// We can derive BL0942_PREF from those. Let's eliminate the weird +// factors and express the calibration in plain counts per volt/amp: +// UREF1 = UREF/73989, IREF1 = IREF/305978. +// +// Next... the true power in Watts is V * I * cos φ, so that's equal +// to WATT/3537 * IREF1 * UREF1. Which means +// BL0942_PREF = BL0942_UREF * BL0942_IREF * 3537 / 305978 / 73989. +// +// Finally the accumulated energy. The period of a CF_CNT count is +// 1638.4*256 / WATT seconds, or 419230.4 / WATT seconds. Which means +// the energy represented by a CN_CNT pulse is 419230.4 WATT-seconds. +// Factoring in the calibration, that's 419230.4 / BL0942_PREF actual +// Watt-seconds (or Joules, as the physicists like to call them). +// +// But we're not being physicists today; we we're being engineers, so +// we want to convert to kWh instead. Which we do by dividing by 1000 +// and then by 3600, so the energy in kWh is +// CF_CNT * 419230.4 / BL0942_PREF / 3600000 +// +// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4 + static const float BL0942_PREF = 596; // taken from tasmota static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218 static const float BL0942_IREF = 251213.46469622; // 305978/1.218 @@ -28,6 +79,11 @@ struct DataPacket { uint8_t checksum; } __attribute__((packed)); +enum LineFrequency : uint8_t { + LINE_FREQUENCY_50HZ = 50, + LINE_FREQUENCY_60HZ = 60, +}; + class BL0942 : public PollingComponent, public uart::UARTDevice { public: void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } @@ -35,9 +91,27 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; } + void set_address(uint8_t address) { this->address_ = address; } + void set_reset(bool reset) { this->reset_ = reset; } + void set_current_reference(float current_ref) { + this->current_reference_ = current_ref; + this->current_reference_set_ = true; + } + void set_energy_reference(float energy_ref) { + this->energy_reference_ = energy_ref; + this->energy_reference_set_ = true; + } + void set_power_reference(float power_ref) { + this->power_reference_ = power_ref; + this->power_reference_set_ = true; + } + void set_voltage_reference(float voltage_ref) { + this->voltage_reference_ = voltage_ref; + this->voltage_reference_set_ = true; + } void loop() override; - void update() override; void setup() override; void dump_config() override; @@ -53,15 +127,25 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { // Divide by this to turn into Watt float power_reference_ = BL0942_PREF; + bool power_reference_set_ = false; // Divide by this to turn into Volt float voltage_reference_ = BL0942_UREF; + bool voltage_reference_set_ = false; // Divide by this to turn into Ampere float current_reference_ = BL0942_IREF; + bool current_reference_set_ = false; // Divide by this to turn into kWh float energy_reference_ = BL0942_EREF; + bool energy_reference_set_ = false; + uint8_t address_ = 0; + bool reset_ = false; + LineFrequency line_freq_ = LINE_FREQUENCY_50HZ; + uint32_t rx_start_ = 0; + uint32_t prev_cf_cnt_ = 0; - static bool validate_checksum(DataPacket *data); - + bool validate_checksum_(DataPacket *data); + int read_reg_(uint8_t reg); + void write_reg_(uint8_t reg, uint32_t val); void received_package_(DataPacket *data); }; } // namespace bl0942 diff --git a/esphome/components/bl0942/sensor.py b/esphome/components/bl0942/sensor.py index f23375b309..550f534b74 100644 --- a/esphome/components/bl0942/sensor.py +++ b/esphome/components/bl0942/sensor.py @@ -1,31 +1,46 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, uart +import esphome.config_validation as cv from esphome.const import ( + CONF_ADDRESS, CONF_CURRENT, CONF_ENERGY, + CONF_FREQUENCY, CONF_ID, + CONF_LINE_FREQUENCY, CONF_POWER, CONF_VOLTAGE, - CONF_FREQUENCY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - DEVICE_CLASS_FREQUENCY, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, + UNIT_HERTZ, UNIT_KILOWATT_HOURS, UNIT_VOLT, UNIT_WATT, - UNIT_HERTZ, ) +CONF_CURRENT_REFERENCE = "current_reference" +CONF_ENERGY_REFERENCE = "energy_reference" +CONF_POWER_REFERENCE = "power_reference" +CONF_RESET = "reset" +CONF_VOLTAGE_REFERENCE = "voltage_reference" + DEPENDENCIES = ["uart"] bl0942_ns = cg.esphome_ns.namespace("bl0942") BL0942 = bl0942_ns.class_("BL0942", cg.PollingComponent, uart.UARTDevice) +LineFrequency = bl0942_ns.enum("LineFrequency") +LINE_FREQS = { + 50: LineFrequency.LINE_FREQUENCY_50HZ, + 60: LineFrequency.LINE_FREQUENCY_60HZ, +} + CONFIG_SCHEMA = ( cv.Schema( { @@ -44,21 +59,35 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, - accuracy_decimals=0, + accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, - accuracy_decimals=0, + accuracy_decimals=3, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, - accuracy_decimals=0, + accuracy_decimals=2, device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_LINE_FREQUENCY, default="50HZ"): cv.All( + cv.frequency, + cv.enum( + LINE_FREQS, + int=True, + ), + ), + cv.Optional(CONF_ADDRESS, default=0): cv.int_range(min=0, max=3), + cv.Optional(CONF_RESET, default=True): cv.boolean, + cv.Optional(CONF_CURRENT_REFERENCE): cv.float_, + cv.Optional(CONF_ENERGY_REFERENCE): cv.float_, + cv.Optional(CONF_POWER_REFERENCE): cv.float_, + cv.Optional(CONF_VOLTAGE_REFERENCE): cv.float_, } ) .extend(cv.polling_component_schema("60s")) @@ -71,23 +100,29 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_VOLTAGE in config: - conf = config[CONF_VOLTAGE] - sens = await sensor.new_sensor(conf) + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) cg.add(var.set_voltage_sensor(sens)) - if CONF_CURRENT in config: - conf = config[CONF_CURRENT] - sens = await sensor.new_sensor(conf) + if current_config := config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) cg.add(var.set_current_sensor(sens)) - if CONF_POWER in config: - conf = config[CONF_POWER] - sens = await sensor.new_sensor(conf) + if power_config := config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) cg.add(var.set_power_sensor(sens)) - if CONF_ENERGY in config: - conf = config[CONF_ENERGY] - sens = await sensor.new_sensor(conf) + if energy_config := config.get(CONF_ENERGY): + sens = await sensor.new_sensor(energy_config) cg.add(var.set_energy_sensor(sens)) - if CONF_FREQUENCY in config: - conf = config[CONF_FREQUENCY] - sens = await sensor.new_sensor(conf) + if frequency_config := config.get(CONF_FREQUENCY): + sens = await sensor.new_sensor(frequency_config) cg.add(var.set_frequency_sensor(sens)) + cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) + cg.add(var.set_address(config[CONF_ADDRESS])) + cg.add(var.set_reset(config[CONF_RESET])) + if (current_reference := config.get(CONF_CURRENT_REFERENCE, None)) is not None: + cg.add(var.set_current_reference(current_reference)) + if (voltage_reference := config.get(CONF_VOLTAGE_REFERENCE, None)) is not None: + cg.add(var.set_voltage_reference(voltage_reference)) + if (power_reference := config.get(CONF_POWER_REFERENCE, None)) is not None: + cg.add(var.set_power_reference(power_reference)) + if (energy_reference := config.get(CONF_ENERGY_REFERENCE, None)) is not None: + cg.add(var.set_energy_reference(energy_reference)) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 8f70ad3417..bc7d517695 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -1,6 +1,8 @@ +from esphome import automation +from esphome.automation import maybe_simple_id import esphome.codegen as cg +from esphome.components import esp32_ble_client, esp32_ble_tracker import esphome.config_validation as cv -from esphome.components import esp32_ble_tracker, esp32_ble_client from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_ID, @@ -12,10 +14,9 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VALUE, ) -from esphome import automation AUTO_LOAD = ["esp32_ble_client"] -CODEOWNERS = ["@buxtronix"] +CODEOWNERS = ["@buxtronix", "@clydebarrow"] DEPENDENCIES = ["esp32_ble_tracker"] ble_client_ns = cg.esphome_ns.namespace("ble_client") @@ -43,6 +44,10 @@ BLEClientNumericComparisonRequestTrigger = ble_client_ns.class_( # Actions BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) +BLEConnectAction = ble_client_ns.class_("BLEClientConnectAction", automation.Action) +BLEDisconnectAction = ble_client_ns.class_( + "BLEClientDisconnectAction", automation.Action +) BLEPasskeyReplyAction = ble_client_ns.class_( "BLEClientPasskeyReplyAction", automation.Action ) @@ -58,10 +63,9 @@ CONF_ACCEPT = "accept" CONF_ON_PASSKEY_REQUEST = "on_passkey_request" CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification" CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request" +CONF_AUTO_CONNECT = "auto_connect" -# Espressif platformio framework is built with MAX_BLE_CONN to 3, so -# enforce this in yaml checks. -MULTI_CONF = 3 +MULTI_CONF = True CONFIG_SCHEMA = ( cv.Schema( @@ -69,6 +73,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(BLEClient), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_NAME): cv.string, + cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean, cv.Optional(CONF_ON_CONNECT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -135,6 +140,12 @@ BLE_WRITE_ACTION_SCHEMA = cv.Schema( } ) +BLE_CONNECT_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(CONF_ID): cv.use_id(BLEClient), + } +) + BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.use_id(BLEClient), @@ -157,6 +168,24 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( ) +@automation.register_action( + "ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA +) +async def ble_disconnect_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + return var + + +@automation.register_action( + "ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA +) +async def ble_connect_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + return var + + @automation.register_action( "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA ) @@ -261,6 +290,7 @@ async def to_code(config): await cg.register_component(var, config) await esp32_ble_tracker.register_client(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_auto_connect(config[CONF_AUTO_CONNECT])) for conf in config.get(CONF_ON_CONNECT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp index 429f906a5f..9a0233eb70 100644 --- a/esphome/components/ble_client/automation.cpp +++ b/esphome/components/ble_client/automation.cpp @@ -2,76 +2,10 @@ #include "automation.h" -#include -#include -#include - -#include "esphome/core/log.h" - namespace esphome { namespace ble_client { -static const char *const TAG = "ble_client.automation"; -void BLEWriterClientNode::write(const std::vector &value) { - if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected"); - return; - } else if (this->ble_char_handle_ == 0) { - ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found"); - return; - } - esp_gatt_write_type_t write_type; - if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { - write_type = ESP_GATT_WRITE_TYPE_RSP; - ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); - } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { - 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", this->char_uuid_.to_string().c_str()); - return; - } - ESP_LOGVV(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->ble_char_handle_, - value.size(), const_cast(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); - } -} - -void BLEWriterClientNode::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - switch (event) { - case ESP_GATTC_REG_EVT: - break; - case ESP_GATTC_OPEN_EVT: - this->node_state = espbt::ClientState::ESTABLISHED; - ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str()); - break; - case ESP_GATTC_SEARCH_CMPL_EVT: { - auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); - if (chr == nullptr) { - ESP_LOGW("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()); - break; - } - this->ble_char_handle_ = chr->handle; - this->char_props_ = chr->properties; - this->node_state = espbt::ClientState::ESTABLISHED; - ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - ble_client_->address_str().c_str()); - break; - } - case ESP_GATTC_DISCONNECT_EVT: - this->node_state = espbt::ClientState::IDLE; - this->ble_char_handle_ = 0; - ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str()); - break; - default: - break; - } -} +const char *const Automation::TAG = "ble_client.automation"; } // namespace ble_client } // namespace esphome diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 423f74b85a..a5c661e2f5 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -7,9 +7,19 @@ #include "esphome/core/automation.h" #include "esphome/components/ble_client/ble_client.h" +#include "esphome/core/log.h" namespace esphome { namespace ble_client { + +// placeholder class for static TAG . +class Automation { + public: + // could be made inline with C++17 + static const char *const TAG; +}; + +// implement on_connect automation. class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { public: explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } @@ -23,17 +33,28 @@ class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { } }; +// on_disconnect automation class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { public: explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { - if (event == ESP_GATTC_DISCONNECT_EVT && - memcmp(param->disconnect.remote_bda, this->parent_->get_remote_bda(), 6) == 0) - this->trigger(); - if (event == ESP_GATTC_SEARCH_CMPL_EVT) - this->node_state = espbt::ClientState::ESTABLISHED; + // test for CLOSE and not DISCONNECT - DISCONNECT can occur even if no virtual connection (OPEN event) occurred. + // So this will not trigger unless a complete open has previously succeeded. + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_CLOSE_EVT: { + this->trigger(); + break; + } + default: { + break; + } + } } }; @@ -42,10 +63,8 @@ class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode { explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { - if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && - memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { + if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) this->trigger(); - } } }; @@ -54,10 +73,8 @@ class BLEClientPasskeyNotificationTrigger : public Trigger, public BLE explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { - if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && - memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { - uint32_t passkey = param->ble_security.key_notif.passkey; - this->trigger(passkey); + if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) { + this->trigger(param->ble_security.key_notif.passkey); } } }; @@ -67,24 +84,20 @@ class BLEClientNumericComparisonRequestTrigger : public Trigger, publi explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { - if (event == ESP_GAP_BLE_NC_REQ_EVT && - memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { - uint32_t passkey = param->ble_security.key_notif.passkey; - this->trigger(passkey); + if (event == ESP_GAP_BLE_NC_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) { + this->trigger(param->ble_security.key_notif.passkey); } } }; -class BLEWriterClientNode : public BLEClientNode { +// implement the ble_client.ble_write action. +template class BLEClientWriteAction : public Action, public BLEClientNode { public: - BLEWriterClientNode(BLEClient *ble_client) { + BLEClientWriteAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; } - // Attempts to write the contents of value to char_uuid_. - void write(const std::vector &value); - void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } @@ -93,29 +106,6 @@ class BLEWriterClientNode : public BLEClientNode { void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override; - - private: - BLEClient *ble_client_; - int ble_char_handle_ = 0; - esp_gatt_char_prop_t char_props_; - espbt::ESPBTUUID service_uuid_; - espbt::ESPBTUUID char_uuid_; -}; - -template class BLEClientWriteAction : public Action, public BLEWriterClientNode { - public: - BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {} - - void play(Ts... x) override { - if (has_simple_value_) { - return write(this->value_simple_); - } else { - return write(this->value_template_(x...)); - } - } - void set_value_template(std::function(Ts...)> func) { this->value_template_ = std::move(func); has_simple_value_ = false; @@ -126,10 +116,94 @@ template class BLEClientWriteAction : public Action, publ has_simple_value_ = true; } + void play(Ts... x) override {} + + void play_complex(Ts... x) override { + this->num_running_++; + this->var_ = std::make_tuple(x...); + auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...); + // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. + if (!write(value)) + this->play_next_(x...); + } + + /** + * Note about logging: the esph_log_X macros are used here because the CI checks complain about use of the ESP LOG + * macros in header files (Can't even write it in a comment!) + * Not sure why, because they seem to work just fine. + * The problem is that the implementation of a templated class can't be placed in a .cpp file when using C++ less than + * 17, so the methods have to be here. The esph_log_X macros are equivalent in function, but don't trigger the CI + * errors. + */ + // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event. + bool write(const std::vector &value) { + 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 (err != ESP_OK) { + esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); + return false; + } + return true; + } + + 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) { + case ESP_GATTC_WRITE_CHAR_EVT: + // upstream code checked the MAC address, verify the characteristic. + if (param->write.handle == this->char_handle_) + this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); + break; + case ESP_GATTC_DISCONNECT_EVT: + if (this->num_running_ != 0) + this->stop_complex(); + break; + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + 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()); + break; + } + this->char_handle_ = chr->handle; + this->char_props_ = chr->properties; + if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { + this->write_type_ = ESP_GATT_WRITE_TYPE_RSP; + esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); + } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { + 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()); + 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()); + break; + } + default: + break; + } + } + private: + BLEClient *ble_client_; bool has_simple_value_ = true; std::vector value_simple_; std::function(Ts...)> value_template_{}; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + std::tuple var_{}; + uint16_t char_handle_{}; + esp_gatt_char_prop_t char_props_{}; + esp_gatt_write_type_t write_type_{}; }; template class BLEClientPasskeyReplyAction : public Action { @@ -212,6 +286,92 @@ template class BLEClientRemoveBondAction : public Action BLEClient *parent_{nullptr}; }; +template class BLEClientConnectAction : public Action, public BLEClientNode { + public: + BLEClientConnectAction(BLEClient *ble_client) { + ble_client->register_ble_node(this); + ble_client_ = ble_client; + } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + if (this->num_running_ == 0) + return; + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: + this->node_state = espbt::ClientState::ESTABLISHED; + this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); + break; + // if the connection is closed, terminate the automation chain. + case ESP_GATTC_DISCONNECT_EVT: + this->stop_complex(); + break; + default: + break; + } + } + + // not used since we override play_complex_ + void play(Ts... x) override {} + + void play_complex(Ts... x) override { + // it makes no sense to have multiple instances of this running at the same time. + // this would occur only if the same automation was re-triggered while still + // running. So just cancel the second chain if this is detected. + if (this->num_running_ != 0) { + this->stop_complex(); + return; + } + this->num_running_++; + if (this->node_state == espbt::ClientState::ESTABLISHED) { + this->play_next_(x...); + } else { + this->var_ = std::make_tuple(x...); + this->ble_client_->connect(); + } + } + + private: + BLEClient *ble_client_; + std::tuple var_{}; +}; + +template class BLEClientDisconnectAction : public Action, public BLEClientNode { + public: + BLEClientDisconnectAction(BLEClient *ble_client) { + ble_client->register_ble_node(this); + ble_client_ = ble_client; + } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + if (this->num_running_ == 0) + return; + switch (event) { + case ESP_GATTC_CLOSE_EVT: + case ESP_GATTC_DISCONNECT_EVT: + this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); + break; + default: + break; + } + } + + // not used since we override play_complex_ + void play(Ts... x) override {} + + void play_complex(Ts... x) override { + this->num_running_++; + if (this->node_state == espbt::ClientState::IDLE) { + this->play_next_(x...); + } else { + this->var_ = std::make_tuple(x...); + this->ble_client_->disconnect(); + } + } + + private: + BLEClient *ble_client_; + std::tuple var_{}; +}; } // namespace ble_client } // namespace esphome diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index f3a9f01a1a..19cf2bc1f3 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -26,6 +26,7 @@ void BLEClient::loop() { void BLEClient::dump_config() { ESP_LOGCONFIG(TAG, "BLE Client:"); ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_)); } bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { @@ -37,31 +38,24 @@ bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { void BLEClient::set_enabled(bool enabled) { if (enabled == this->enabled) return; - if (!enabled && this->state() != espbt::ClientState::IDLE) { - ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); - auto ret = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); - if (ret) { - ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret); - } - } this->enabled = enabled; + if (!enabled) { + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + this->disconnect(); + } } bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, esp_ble_gattc_cb_param_t *param) { - bool all_established = this->all_nodes_established_(); - if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param)) return false; for (auto *node : this->nodes_) node->gattc_event_handler(event, esp_gattc_if, param); - // Delete characteristics after clients have used them to save RAM. - if (!all_established && this->all_nodes_established_()) { - for (auto &svc : this->services_) - delete svc; // NOLINT(cppcoreguidelines-owning-memory) - this->services_.clear(); + if (!this->services_.empty() && this->all_nodes_established_()) { + this->release_services(); + ESP_LOGD(TAG, "All clients established, services released"); } return true; } diff --git a/esphome/components/ble_client/output/__init__.py b/esphome/components/ble_client/output/__init__.py index fd847d80b8..729885eb8b 100644 --- a/esphome/components/ble_client/output/__init__.py +++ b/esphome/components/ble_client/output/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import ble_client, esp32_ble_tracker, output +import esphome.config_validation as cv from esphome.const import CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_SERVICE_UUID from .. import ble_client_ns diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index 6709803936..3f05bc4b84 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -19,26 +19,36 @@ void BLEBinaryOutput::dump_config() { void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { - case ESP_GATTC_OPEN_EVT: - this->client_state_ = espbt::ClientState::ESTABLISHED; - ESP_LOGW(TAG, "[%s] Connected successfully!", this->char_uuid_.to_string().c_str()); - break; - case ESP_GATTC_DISCONNECT_EVT: - ESP_LOGW(TAG, "[%s] Disconnected", this->char_uuid_.to_string().c_str()); - this->client_state_ = espbt::ClientState::IDLE; - break; - case ESP_GATTC_WRITE_CHAR_EVT: { - if (param->write.status == 0) { - break; - } - + 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->char_uuid_.to_string().c_str()); + 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()); break; } - if (param->write.handle == chr->handle) { - ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status); + this->char_handle_ = chr->handle; + this->char_props_ = chr->properties; + if (this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { + this->write_type_ = ESP_GATT_WRITE_TYPE_RSP; + ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); + } else if (!this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { + 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(), + 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()); + 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); } break; } @@ -48,26 +58,18 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i } void BLEBinaryOutput::write_state(bool state) { - if (this->client_state_ != espbt::ClientState::ESTABLISHED) { + 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()); return; } - - auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); - if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] Characteristic not found. State update can not be written.", - this->char_uuid_.to_string().c_str()); - 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); - if (this->require_response_) { - chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_RSP); - } else { - chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_NO_RSP); - } + 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); } } // namespace ble_client diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 83eabcf5f2..0a1e186b26 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -32,7 +32,9 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi bool require_response_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; - espbt::ClientState client_state_; + uint16_t char_handle_{}; + esp_gatt_char_prop_t char_props_{}; + esp_gatt_write_type_t write_type_{}; }; } // namespace ble_client diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index c9bf2995b1..0c48902a90 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -1,17 +1,18 @@ +from esphome import automation import esphome.codegen as cg +from esphome.components import ble_client, esp32_ble_tracker, sensor import esphome.config_validation as cv -from esphome.components import sensor, ble_client, esp32_ble_tracker from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_LAMBDA, + CONF_SERVICE_UUID, CONF_TRIGGER_ID, CONF_TYPE, - CONF_SERVICE_UUID, DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, ) -from esphome import automation + from .. import ble_client_ns DEPENDENCIES = ["ble_client"] @@ -129,32 +130,18 @@ async def characteristic_sensor_to_code(config): ) cg.add(var.set_char_uuid128(uuid128)) - if CONF_DESCRIPTOR_UUID in config: - if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): - cg.add( - var.set_descr_uuid16( - esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) - ) - ) - elif len(config[CONF_DESCRIPTOR_UUID]) == len( - esp32_ble_tracker.bt_uuid32_format - ): - cg.add( - var.set_descr_uuid32( - esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) - ) - ) - elif len(config[CONF_DESCRIPTOR_UUID]) == len( - esp32_ble_tracker.bt_uuid128_format - ): - uuid128 = esp32_ble_tracker.as_reversed_hex_array( - config[CONF_DESCRIPTOR_UUID] - ) + if descriptor_uuid := config.get(CONF_DESCRIPTOR_UUID): + if len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add(var.set_descr_uuid16(esp32_ble_tracker.as_hex(descriptor_uuid))) + elif len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add(var.set_descr_uuid32(esp32_ble_tracker.as_hex(descriptor_uuid))) + elif len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(descriptor_uuid) cg.add(var.set_descr_uuid128(uuid128)) - if CONF_LAMBDA in config: + if lambda_config := config.get(CONF_LAMBDA): lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(adv_data_t_const_ref, "x")], return_type=cg.float_ + lambda_config, [(adv_data_t_const_ref, "x")], return_type=cg.float_ ) cg.add(var.set_data_to_value(lambda_)) diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h index d830165d30..56ab7ba4c9 100644 --- a/esphome/components/ble_client/sensor/automation.h +++ b/esphome/components/ble_client/sensor/automation.h @@ -14,15 +14,17 @@ class BLESensorNotifyTrigger : public Trigger, public BLESensor { 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) { - case ESP_GATTC_SEARCH_CMPL_EVT: { - this->sensor_->node_state = espbt::ClientState::ESTABLISHED; + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle == this->sensor_->handle) + this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); break; } - case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() || - param->notify.handle != this->sensor_->handle) - break; - this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + // confirms notifications are being listened for. While enabling of notifications may still be in + // progress by the parent, we assume it will happen. + if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->sensor_->handle) + this->node_state = espbt::ClientState::ESTABLISHED; + break; } default: break; diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 13e51ed5b3..81d244ce6d 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -1,8 +1,8 @@ #include "ble_rssi_sensor.h" -#include "esphome/core/log.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/log.h" #ifdef USE_ESP32 @@ -22,22 +22,19 @@ void BLEClientRSSISensor::dump_config() { void BLEClientRSSISensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { - case ESP_GATTC_OPEN_EVT: { - if (param->open.status == ESP_GATT_OK) { - ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); - break; - } - break; - } - case ESP_GATTC_DISCONNECT_EVT: { - ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + case ESP_GATTC_CLOSE_EVT: { this->status_set_warning(); this->publish_state(NAN); break; } - case ESP_GATTC_SEARCH_CMPL_EVT: + case ESP_GATTC_SEARCH_CMPL_EVT: { this->node_state = espbt::ClientState::ESTABLISHED; + if (this->should_update_) { + this->should_update_ = false; + this->get_rssi_(); + } break; + } default: break; } @@ -50,6 +47,7 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) { int8_t rssi = param->read_rssi_cmpl.rssi; ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi); + this->status_clear_warning(); this->publish_state(rssi); } break; @@ -61,9 +59,12 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl void BLEClientRSSISensor::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + this->should_update_ = true; return; } - + this->get_rssi_(); +} +void BLEClientRSSISensor::get_rssi_() { ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str()); auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda()); if (status != ESP_OK) { diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 028df83832..5dd3fc7af9 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -1,9 +1,9 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/ble_client/ble_client.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" #ifdef USE_ESP32 #include @@ -24,6 +24,10 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; + + protected: + void get_rssi_(); + bool should_update_{false}; }; } // namespace ble_client diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index a05efad60b..43f61f5304 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -33,7 +33,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } break; } - case ESP_GATTC_DISCONNECT_EVT: { + case ESP_GATTC_CLOSE_EVT: { ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); this->status_set_warning(); this->publish_state(NAN); @@ -74,8 +74,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga 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); break; @@ -87,15 +85,23 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) - break; - ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); + if (param->notify.handle != this->handle) + break; this->publish_state(this->parse_data_(param->notify.value, param->notify.value_len)); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->node_state = espbt::ClientState::ESTABLISHED; + if (param->reg_for_notify.handle == this->handle) { + if (param->reg_for_notify.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error registering for notifications at handle %d, status=%d", param->reg_for_notify.handle, + param->reg_for_notify.status); + break; + } + this->node_state = espbt::ClientState::ESTABLISHED; + ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_string().c_str()); + } break; } default: diff --git a/esphome/components/ble_client/switch/__init__.py b/esphome/components/ble_client/switch/__init__.py index 2304d65c01..70314e8f30 100644 --- a/esphome/components/ble_client/switch/__init__.py +++ b/esphome/components/ble_client/switch/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg +from esphome.components import ble_client, switch import esphome.config_validation as cv -from esphome.components import switch, ble_client from esphome.const import ICON_BLUETOOTH + from .. import ble_client_ns BLEClientSwitch = ble_client_ns.class_( diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp index 6de5252404..9d92b1b2b5 100644 --- a/esphome/components/ble_client/switch/ble_switch.cpp +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -17,14 +17,11 @@ void BLEClientSwitch::write_state(bool state) { void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { - case ESP_GATTC_REG_EVT: + case ESP_GATTC_CLOSE_EVT: this->publish_state(this->parent_->enabled); break; - case ESP_GATTC_OPEN_EVT: + case ESP_GATTC_SEARCH_CMPL_EVT: this->node_state = espbt::ClientState::ESTABLISHED; - break; - case ESP_GATTC_DISCONNECT_EVT: - this->node_state = espbt::ClientState::IDLE; this->publish_state(this->parent_->enabled); break; default: diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py index 66f00c551b..479af1a57e 100644 --- a/esphome/components/ble_client/text_sensor/__init__.py +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -1,13 +1,14 @@ +from esphome import automation import esphome.codegen as cg +from esphome.components import ble_client, esp32_ble_tracker, text_sensor import esphome.config_validation as cv -from esphome.components import text_sensor, ble_client, esp32_ble_tracker from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_ID, - CONF_TRIGGER_ID, CONF_SERVICE_UUID, + CONF_TRIGGER_ID, ) -from esphome import automation + from .. import ble_client_ns DEPENDENCIES = ["ble_client"] @@ -88,27 +89,13 @@ async def to_code(config): ) cg.add(var.set_char_uuid128(uuid128)) - if CONF_DESCRIPTOR_UUID in config: - if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): - cg.add( - var.set_descr_uuid16( - esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) - ) - ) - elif len(config[CONF_DESCRIPTOR_UUID]) == len( - esp32_ble_tracker.bt_uuid32_format - ): - cg.add( - var.set_descr_uuid32( - esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) - ) - ) - elif len(config[CONF_DESCRIPTOR_UUID]) == len( - esp32_ble_tracker.bt_uuid128_format - ): - uuid128 = esp32_ble_tracker.as_reversed_hex_array( - config[CONF_DESCRIPTOR_UUID] - ) + if descriptor_uuid := config: + if len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add(var.set_descr_uuid16(esp32_ble_tracker.as_hex(descriptor_uuid))) + elif len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add(var.set_descr_uuid32(esp32_ble_tracker.as_hex(descriptor_uuid))) + elif len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(descriptor_uuid) cg.add(var.set_descr_uuid128(uuid128)) await cg.register_component(var, config) 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 1a304593c7..33938ee7b7 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -36,8 +36,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } break; } - case ESP_GATTC_DISCONNECT_EVT: { - ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + case ESP_GATTC_CLOSE_EVT: { this->status_set_warning(); this->publish_state(EMPTY); break; @@ -77,20 +76,18 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ 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); - break; - } if (param->read.handle == this->handle) { + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } this->status_clear_warning(); this->publish_state(this->parse_data(param->read.value, param->read.value_len)); } break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) + if (param->notify.handle != this->handle) 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]); @@ -98,7 +95,8 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->node_state = espbt::ClientState::ESTABLISHED; + if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->handle) + this->node_state = espbt::ClientState::ESTABLISHED; break; } default: diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index d54b7678e1..3a0f1ade98 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -1,15 +1,18 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor, esp32_ble_tracker +import esphome.config_validation as cv from esphome.const import ( - CONF_MAC_ADDRESS, - CONF_SERVICE_UUID, CONF_IBEACON_MAJOR, CONF_IBEACON_MINOR, CONF_IBEACON_UUID, + CONF_MAC_ADDRESS, CONF_MIN_RSSI, + CONF_SERVICE_UUID, + CONF_TIMEOUT, ) +CONF_IRK = "irk" + DEPENDENCIES = ["esp32_ble_tracker"] ble_presence_ns = cg.esphome_ns.namespace("ble_presence") @@ -34,18 +37,22 @@ CONFIG_SCHEMA = cv.All( .extend( { cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_IRK): cv.uuid, cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, - cv.Optional(CONF_IBEACON_UUID): cv.uuid, + cv.Optional(CONF_IBEACON_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_TIMEOUT, default="5min"): cv.positive_time_period, cv.Optional(CONF_MIN_RSSI): cv.All( - cv.decibel, cv.int_range(min=-90, max=-30) + cv.decibel, cv.int_range(min=-100, max=-30) ), } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA), - cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID), + cv.has_exactly_one_key( + CONF_MAC_ADDRESS, CONF_IRK, CONF_SERVICE_UUID, CONF_IBEACON_UUID + ), _validate, ) @@ -55,35 +62,32 @@ async def to_code(config): await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) - if CONF_MIN_RSSI in config: - cg.add(var.set_minimum_rssi(config[CONF_MIN_RSSI])) + cg.add(var.set_timeout(config[CONF_TIMEOUT].total_milliseconds)) + if min_rssi := config.get(CONF_MIN_RSSI): + cg.add(var.set_minimum_rssi(min_rssi)) - if CONF_MAC_ADDRESS in config: - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if mac_address := config.get(CONF_MAC_ADDRESS): + cg.add(var.set_address(mac_address.as_hex)) - if CONF_SERVICE_UUID in config: - if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): - cg.add( - var.set_service_uuid16( - esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]) - ) - ) - elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): - cg.add( - var.set_service_uuid32( - esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]) - ) - ) - elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + if irk := config.get(CONF_IRK): + irk = esp32_ble_tracker.as_hex_array(str(irk)) + cg.add(var.set_irk(irk)) + + if service_uuid := config.get(CONF_SERVICE_UUID): + if len(service_uuid) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add(var.set_service_uuid16(esp32_ble_tracker.as_hex(service_uuid))) + elif len(service_uuid) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add(var.set_service_uuid32(esp32_ble_tracker.as_hex(service_uuid))) + elif len(service_uuid) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(service_uuid) cg.add(var.set_service_uuid128(uuid128)) - if CONF_IBEACON_UUID in config: - ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID])) + if ibeacon_uuid := config.get(CONF_IBEACON_UUID): + ibeacon_uuid = esp32_ble_tracker.as_reversed_hex_array(ibeacon_uuid) cg.add(var.set_ibeacon_uuid(ibeacon_uuid)) - if CONF_IBEACON_MAJOR in config: - cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR])) + if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None: + cg.add(var.set_ibeacon_major(ibeacon_major)) - if CONF_IBEACON_MINOR in config: - cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR])) + if (ibeacon_minor := config.get(CONF_IBEACON_MINOR)) is not None: + cg.add(var.set_ibeacon_minor(ibeacon_minor)) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 953ea460a8..3ed60d1b49 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -17,6 +17,10 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->match_by_ = MATCH_BY_MAC_ADDRESS; this->address_ = address; } + void set_irk(uint8_t *irk) { + this->match_by_ = MATCH_BY_IRK; + this->irk_ = irk; + } void set_service_uuid16(uint16_t uuid) { this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); @@ -45,28 +49,28 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->check_minimum_rssi_ = true; this->minimum_rssi_ = rssi; } - void on_scan_end() override { - if (!this->found_) - this->publish_state(false); - this->found_ = false; - } + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (this->check_minimum_rssi_ && this->minimum_rssi_ <= device.get_rssi()) { + if (this->check_minimum_rssi_ && this->minimum_rssi_ > device.get_rssi()) { return false; } switch (this->match_by_) { case MATCH_BY_MAC_ADDRESS: if (device.address_uint64() == this->address_) { - this->publish_state(true); - this->found_ = true; + this->set_found_(true); + return true; + } + break; + case MATCH_BY_IRK: + if (device.resolve_irk(this->irk_)) { + this->set_found_(true); return true; } break; case MATCH_BY_SERVICE_UUID: for (auto uuid : device.get_service_uuids()) { if (this->uuid_ == uuid) { - this->publish_state(true); - this->found_ = true; + this->set_found_(true); return true; } } @@ -90,20 +94,31 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, return false; } - this->publish_state(true); - this->found_ = true; + this->set_found_(true); return true; } return false; } + + void loop() override { + if (this->found_ && this->last_seen_ + this->timeout_ < millis()) + this->set_found_(false); + } void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } protected: - enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; + void set_found_(bool state) { + this->found_ = state; + if (state) + this->last_seen_ = millis(); + this->publish_state(state); + } + enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; MatchType match_by_; uint64_t address_; + uint8_t *irk_; esp32_ble_tracker::ESPBTUUID uuid_; @@ -118,6 +133,8 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, bool check_minimum_rssi_{false}; bool found_{false}; + uint32_t last_seen_{}; + uint32_t timeout_{}; }; } // namespace ble_presence diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 79aebce7d3..89e4f33aca 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -15,6 +15,10 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi this->match_by_ = MATCH_BY_MAC_ADDRESS; this->address_ = address; } + void set_irk(uint8_t *irk) { + this->match_by_ = MATCH_BY_IRK; + this->irk_ = irk; + } void set_service_uuid16(uint16_t uuid) { this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); @@ -53,6 +57,13 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi return true; } break; + case MATCH_BY_IRK: + if (device.resolve_irk(this->irk_)) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + break; case MATCH_BY_SERVICE_UUID: for (auto uuid : device.get_service_uuids()) { if (this->uuid_ == uuid) { @@ -91,12 +102,13 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi float get_setup_priority() const override { return setup_priority::DATA; } protected: - enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; + enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; MatchType match_by_; bool found_{false}; uint64_t address_; + uint8_t *irk_; esp32_ble_tracker::ESPBTUUID uuid_; diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index 7c7bfc58a7..c4e767aa21 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -1,17 +1,19 @@ import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, sensor import esphome.config_validation as cv -from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_IBEACON_MAJOR, CONF_IBEACON_MINOR, CONF_IBEACON_UUID, - CONF_SERVICE_UUID, CONF_MAC_ADDRESS, + CONF_SERVICE_UUID, DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, ) +CONF_IRK = "irk" + DEPENDENCIES = ["esp32_ble_tracker"] ble_rssi_ns = cg.esphome_ns.namespace("ble_rssi") @@ -39,15 +41,18 @@ CONFIG_SCHEMA = cv.All( .extend( { cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_IRK): cv.uuid, cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, - cv.Optional(CONF_IBEACON_UUID): cv.uuid, + cv.Optional(CONF_IBEACON_UUID): esp32_ble_tracker.bt_uuid, } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA), - cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID), + cv.has_exactly_one_key( + CONF_MAC_ADDRESS, CONF_IRK, CONF_SERVICE_UUID, CONF_IBEACON_UUID + ), _validate, ) @@ -57,32 +62,28 @@ async def to_code(config): await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) - if CONF_MAC_ADDRESS in config: - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if mac_address := config.get(CONF_MAC_ADDRESS): + cg.add(var.set_address(mac_address.as_hex)) - if CONF_SERVICE_UUID in config: - if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): - cg.add( - var.set_service_uuid16( - esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]) - ) - ) - elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): - cg.add( - var.set_service_uuid32( - esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]) - ) - ) - elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + if irk := config.get(CONF_IRK): + irk = esp32_ble_tracker.as_hex_array(str(irk)) + cg.add(var.set_irk(irk)) + + if service_uuid := config.get(CONF_SERVICE_UUID): + if len(service_uuid) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add(var.set_service_uuid16(esp32_ble_tracker.as_hex(service_uuid))) + elif len(service_uuid) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add(var.set_service_uuid32(esp32_ble_tracker.as_hex(service_uuid))) + elif len(service_uuid) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(service_uuid) cg.add(var.set_service_uuid128(uuid128)) - if CONF_IBEACON_UUID in config: - ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID])) + if ibeacon_uuid := config.get(CONF_IBEACON_UUID): + ibeacon_uuid = esp32_ble_tracker.as_reversed_hex_array(ibeacon_uuid) cg.add(var.set_ibeacon_uuid(ibeacon_uuid)) - if CONF_IBEACON_MAJOR in config: - cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR])) + if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None: + cg.add(var.set_ibeacon_major(ibeacon_major)) - if CONF_IBEACON_MINOR in config: - cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR])) + if (ibeacon_minor := config.get(CONF_IBEACON_MINOR)) is not None: + cg.add(var.set_ibeacon_minor(ibeacon_minor)) diff --git a/esphome/components/ble_scanner/text_sensor.py b/esphome/components/ble_scanner/text_sensor.py index 743403c6a4..96d71a0399 100644 --- a/esphome/components/ble_scanner/text_sensor.py +++ b/esphome/components/ble_scanner/text_sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, text_sensor import esphome.config_validation as cv -from esphome.components import text_sensor, esp32_ble_tracker DEPENDENCIES = ["esp32_ble_tracker"] diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index bec1579d8e..5a4ba36666 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -1,8 +1,8 @@ -from esphome.components import esp32_ble_tracker, esp32_ble_client -import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ACTIVE, CONF_ID +from esphome.components import esp32_ble_client, esp32_ble_tracker from esphome.components.esp32 import add_idf_sdkconfig_option +import esphome.config_validation as cv +from esphome.const import CONF_ACTIVE, CONF_ID AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] DEPENDENCIES = ["api", "esp32"] diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 9354ab36d6..543752853e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -1,6 +1,6 @@ #include "bluetooth_connection.h" -#include "esphome/components/api/api_server.h" +#include "esphome/components/api/api_pb2.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -20,31 +20,31 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga switch (event) { case ESP_GATTC_DISCONNECT_EVT: { - api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->disconnect.reason); + this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); this->set_address(0); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_connections_free(); + break; + } + case ESP_GATTC_CLOSE_EVT: { + this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason); + this->set_address(0); + this->proxy_->send_connections_free(); break; } case ESP_GATTC_OPEN_EVT: { - if (param->open.conn_id != this->conn_id_) - break; if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { - api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->open.status); + this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); this->set_address(0); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_connections_free(); } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_device_connection(this->address_, true, this->mtu_); + this->proxy_->send_connections_free(); } this->seen_mtu_or_services_ = false; break; } - case ESP_GATTC_CFG_MTU_EVT: { - if (param->cfg_mtu.conn_id != this->conn_id_) - break; + case ESP_GATTC_CFG_MTU_EVT: + case ESP_GATTC_SEARCH_CMPL_EVT: { if (!this->seen_mtu_or_services_) { // We don't know if we will get the MTU or the services first, so // only send the device connection true if we have already received @@ -52,34 +52,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->seen_mtu_or_services_ = true; break; } - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); - break; - } - case ESP_GATTC_SEARCH_CMPL_EVT: { - if (param->search_cmpl.conn_id != this->conn_id_) - break; - if (!this->seen_mtu_or_services_) { - // We don't know if we will get the MTU or the services first, so - // only send the device connection true if we have already received - // the mtu. - this->seen_mtu_or_services_ = true; - break; - } - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_device_connection(this->address_, true, this->mtu_); + this->proxy_->send_connections_free(); break; } case ESP_GATTC_READ_DESCR_EVT: case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->conn_id_) - break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->read.handle, param->read.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->read.handle, param->read.status); + this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } api::BluetoothGATTReadResponse resp; @@ -89,23 +71,21 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga for (uint16_t i = 0; i < param->read.value_len; i++) { resp.data.push_back(param->read.value[i]); } - api::global_api_server->send_bluetooth_gatt_read_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp); break; } case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_DESCR_EVT: { - if (param->write.conn_id != this->conn_id_) - break; if (param->write.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->write.handle, param->write.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->write.handle, param->write.status); + this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - api::global_api_server->send_bluetooth_gatt_write_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_write_response(resp); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -113,33 +93,29 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle, param->unreg_for_notify.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->unreg_for_notify.handle, - param->unreg_for_notify.status); + this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - api::global_api_server->send_bluetooth_gatt_notify_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { if (param->reg_for_notify.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->reg_for_notify.handle, - param->reg_for_notify.status); + this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - api::global_api_server->send_bluetooth_gatt_notify_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp); break; } case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.conn_id != this->conn_id_) - break; ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(), param->notify.handle); api::BluetoothGATTNotifyDataResponse resp; @@ -149,7 +125,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga for (uint16_t i = 0; i < param->notify.value_len; i++) { resp.data.push_back(param->notify.value[i]); } - api::global_api_server->send_bluetooth_gatt_notify_data_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp); break; } default: @@ -166,10 +142,9 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) break; if (param->ble_security.auth_cmpl.success) { - api::global_api_server->send_bluetooth_device_pairing(this->address_, true); + this->proxy_->send_device_pairing(this->address_, true); } else { - api::global_api_server->send_bluetooth_device_pairing(this->address_, false, - param->ble_security.auth_cmpl.fail_reason); + this->proxy_->send_device_pairing(this->address_, false, param->ble_security.auth_cmpl.fail_reason); } break; default: @@ -283,6 +258,10 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl return ESP_OK; } +esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() { + return this->proxy_->get_advertisement_parser_type(); +} + } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 8b13f4d1c2..e6ab3cbccc 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -14,6 +14,7 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp_err_t read_characteristic(uint16_t handle); esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 76950c944e..bd1c8b7ea4 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -1,11 +1,10 @@ #include "bluetooth_proxy.h" #include "esphome/core/log.h" +#include "esphome/core/macros.h" #ifdef USE_ESP32 -#include "esphome/components/api/api_server.h" - namespace esphome { namespace bluetooth_proxy { @@ -27,15 +26,42 @@ std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - if (!api::global_api_server->is_connected()) + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) return false; + ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), device.get_rssi()); this->send_api_packet_(device); - return true; } +bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) + return false; + + api::BluetoothLERawAdvertisementsResponse resp; + for (size_t i = 0; i < count; i++) { + auto &result = advertisements[i]; + api::BluetoothLERawAdvertisement adv; + adv.address = esp32_ble::ble_addr_to_uint64(result.bda); + adv.rssi = result.rssi; + adv.address_type = result.ble_addr_type; + + uint8_t length = result.adv_data_len + result.scan_rsp_len; + adv.data.reserve(length); + for (uint16_t i = 0; i < length; i++) { + adv.data.push_back(result.ble_adv[i]); + } + + resp.advertisements.push_back(std::move(adv)); + + ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], + result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); + } + ESP_LOGV(TAG, "Proxying %d packets", count); + this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); + return true; +} void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); @@ -58,12 +84,14 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi manufacturer_data.data.assign(data.data.begin(), data.data.end()); resp.manufacturer_data.push_back(std::move(manufacturer_data)); } - api::global_api_server->send_bluetooth_le_advertisement(resp); + this->api_connection_->send_bluetooth_le_advertisement(resp); } void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, " Active: %s", YESNO(this->active_)); + ESP_LOGCONFIG(TAG, " Connections: %d", this->connections_.size()); + ESP_LOGCONFIG(TAG, " Raw advertisements: %s", YESNO(this->raw_advertisements_)); } int BluetoothProxy::get_bluetooth_connections_free() { @@ -81,7 +109,7 @@ int BluetoothProxy::get_bluetooth_connections_free() { } void BluetoothProxy::loop() { - if (!api::global_api_server->is_connected()) { + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { for (auto *connection : this->connections_) { if (connection->get_address() != 0) { connection->disconnect(); @@ -92,7 +120,7 @@ void BluetoothProxy::loop() { for (auto *connection : this->connections_) { if (connection->send_service_ == connection->service_count_) { connection->send_service_ = DONE_SENDING_SERVICES; - api::global_api_server->send_bluetooth_gatt_services_done(connection->get_address()); + this->send_gatt_services_done(connection->get_address()); if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { connection->release_services(); @@ -170,11 +198,17 @@ void BluetoothProxy::loop() { service_resp.characteristics.push_back(std::move(characteristic_resp)); } resp.services.push_back(std::move(service_resp)); - api::global_api_server->send_bluetooth_gatt_services(resp); + this->api_connection_->send_bluetooth_gatt_get_services_response(resp); } } } +esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { + if (this->raw_advertisements_) + return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; + return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS; +} + BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { for (auto *connection : this->connections_) { if (connection->get_address() == address) @@ -208,16 +242,15 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest auto *connection = this->get_connection_(msg.address, true); if (connection == nullptr) { ESP_LOGW(TAG, "No free connections available"); - api::global_api_server->send_bluetooth_device_connection(msg.address, false); + this->send_device_connection(msg.address, false); return; } if (connection->state() == espbt::ClientState::CONNECTED || connection->state() == espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(), connection->address_str().c_str()); - api::global_api_server->send_bluetooth_device_connection(msg.address, true); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, true); + this->send_connections_free(); return; } else if (connection->state() == espbt::ClientState::SEARCHING) { ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device", @@ -263,25 +296,22 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } else { connection->set_state(espbt::ClientState::SEARCHING); } - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_connections_free(); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - api::global_api_server->send_bluetooth_device_connection(msg.address, false); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, false); + this->send_connections_free(); return; } if (connection->state() != espbt::ClientState::IDLE) { connection->disconnect(); } else { connection->set_address(0); - api::global_api_server->send_bluetooth_device_connection(msg.address, false); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, false); + this->send_connections_free(); } break; } @@ -291,10 +321,10 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest if (!connection->is_paired()) { auto err = connection->pair(); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_device_pairing(msg.address, false, err); + this->send_device_pairing(msg.address, false, err); } } else { - api::global_api_server->send_bluetooth_device_pairing(msg.address, true); + this->send_device_pairing(msg.address, true); } } break; @@ -303,14 +333,20 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest esp_bd_addr_t address; uint64_to_bd_addr(msg.address, address); esp_err_t ret = esp_ble_remove_bond_device(address); - api::global_api_server->send_bluetooth_device_unpairing(msg.address, ret == ESP_OK, ret); + this->send_device_pairing(msg.address, ret == ESP_OK, ret); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: { esp_bd_addr_t address; uint64_to_bd_addr(msg.address, address); esp_err_t ret = esp_ble_gattc_cache_clean(address); - api::global_api_server->send_bluetooth_device_clear_cache(msg.address, ret == ESP_OK, ret); + api::BluetoothDeviceClearCacheResponse call; + call.address = msg.address; + call.success = ret == ESP_OK; + call.error = ret; + + this->api_connection_->send_bluetooth_device_clear_cache_response(call); + break; } } @@ -320,13 +356,13 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->read_characteristic(msg.handle); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -334,13 +370,13 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest & auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->write_characteristic(msg.handle, msg.data, msg.response); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -348,13 +384,13 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->read_descriptor(msg.handle); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -362,13 +398,13 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->write_descriptor(msg.handle, msg.data, true); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -376,12 +412,12 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr || !connection->connected()) { ESP_LOGW(TAG, "Cannot get GATT services, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); return; } if (!connection->service_count_) { ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); - api::global_api_server->send_bluetooth_gatt_services_done(msg.address); + this->send_gatt_services_done(msg.address); return; } if (connection->send_service_ == @@ -393,16 +429,91 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->notify_characteristic(msg.handle, msg.enable); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } +void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { + if (this->api_connection_ != nullptr) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + this->api_connection_ = api_connection; + this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; + this->parent_->recalculate_advertisement_parser_types(); +} + +void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) { + if (this->api_connection_ != api_connection) { + ESP_LOGV(TAG, "API connection is not subscribed"); + return; + } + this->api_connection_ = nullptr; + this->raw_advertisements_ = false; + this->parent_->recalculate_advertisement_parser_types(); +} + +void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothDeviceConnectionResponse call; + call.address = address; + call.connected = connected; + call.mtu = mtu; + call.error = error; + this->api_connection_->send_bluetooth_device_connection_response(call); +} +void BluetoothProxy::send_connections_free() { + if (this->api_connection_ == nullptr) + return; + api::BluetoothConnectionsFreeResponse call; + call.free = this->get_bluetooth_connections_free(); + call.limit = this->get_bluetooth_connections_limit(); + this->api_connection_->send_bluetooth_connections_free_response(call); +} + +void BluetoothProxy::send_gatt_services_done(uint64_t address) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothGATTGetServicesDoneResponse call; + call.address = address; + this->api_connection_->send_bluetooth_gatt_get_services_done_response(call); +} + +void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothGATTErrorResponse call; + call.address = address; + call.handle = handle; + call.error = error; + this->api_connection_->send_bluetooth_gatt_error_response(call); +} + +void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { + api::BluetoothDevicePairingResponse call; + call.address = address; + call.paired = paired; + call.error = error; + + this->api_connection_->send_bluetooth_device_pairing_response(call); +} + +void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { + api::BluetoothDeviceUnpairingResponse call; + call.address = address; + call.success = success; + call.error = error; + + this->api_connection_->send_bluetooth_device_unpairing_response(call); +} + BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace bluetooth_proxy diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index a582abc8a3..35a37f934a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" #include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -21,12 +22,36 @@ static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; using namespace esp32_ble_client; +// Legacy versions: +// Version 1: Initial version without active connections +// Version 2: Support for active connections +// Version 3: New connection API +// Version 4: Pairing support +// Version 5: Cache clear support +static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; +static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; + +enum BluetoothProxyFeature : uint32_t { + FEATURE_PASSIVE_SCAN = 1 << 0, + FEATURE_ACTIVE_CONNECTIONS = 1 << 1, + FEATURE_REMOTE_CACHING = 1 << 2, + FEATURE_PAIRING = 1 << 3, + FEATURE_CACHE_CLEARING = 1 << 4, + FEATURE_RAW_ADVERTISEMENTS = 1 << 5, +}; + +enum BluetoothProxySubscriptionFlag : uint32_t { + SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, +}; + class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: BluetoothProxy(); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; void dump_config() override; void loop() override; + esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { this->connections_.push_back(connection); @@ -44,6 +69,18 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com int get_bluetooth_connections_free(); int get_bluetooth_connections_limit() { return this->connections_.size(); } + void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); + void unsubscribe_api_connection(api::APIConnection *api_connection); + api::APIConnection *get_api_connection() { return this->api_connection_; } + + void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); + void send_connections_free(); + void send_gatt_services_done(uint64_t address); + void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); + void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); + void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); + void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); + static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) { bd_addr[0] = (address >> 40) & 0xff; bd_addr[1] = (address >> 32) & 0xff; @@ -56,6 +93,27 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } + uint32_t get_legacy_version() const { + if (this->active_) { + return LEGACY_ACTIVE_CONNECTIONS_VERSION; + } + return LEGACY_PASSIVE_ONLY_VERSION; + } + + uint32_t get_feature_flags() const { + uint32_t flags = 0; + flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN; + flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS; + if (this->active_) { + flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS; + flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; + flags |= BluetoothProxyFeature::FEATURE_PAIRING; + flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; + } + + return flags; + } + protected: void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); @@ -64,18 +122,12 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com bool active_; std::vector connections_{}; + api::APIConnection *api_connection_{nullptr}; + bool raw_advertisements_{false}; }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -// Version 1: Initial version without active connections -// Version 2: Support for active connections -// Version 3: New connection API -// Version 4: Pairing support -// Version 5: Cache clear support -static const uint32_t ACTIVE_CONNECTIONS_VERSION = 5; -static const uint32_t PASSIVE_ONLY_VERSION = 1; - } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp deleted file mode 100644 index 786fc01d28..0000000000 --- a/esphome/components/bme280/bme280.cpp +++ /dev/null @@ -1,355 +0,0 @@ -#include "bme280.h" -#include "esphome/core/hal.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace bme280 { - -static const char *const TAG = "bme280.sensor"; - -static const uint8_t BME280_REGISTER_DIG_T1 = 0x88; -static const uint8_t BME280_REGISTER_DIG_T2 = 0x8A; -static const uint8_t BME280_REGISTER_DIG_T3 = 0x8C; - -static const uint8_t BME280_REGISTER_DIG_P1 = 0x8E; -static const uint8_t BME280_REGISTER_DIG_P2 = 0x90; -static const uint8_t BME280_REGISTER_DIG_P3 = 0x92; -static const uint8_t BME280_REGISTER_DIG_P4 = 0x94; -static const uint8_t BME280_REGISTER_DIG_P5 = 0x96; -static const uint8_t BME280_REGISTER_DIG_P6 = 0x98; -static const uint8_t BME280_REGISTER_DIG_P7 = 0x9A; -static const uint8_t BME280_REGISTER_DIG_P8 = 0x9C; -static const uint8_t BME280_REGISTER_DIG_P9 = 0x9E; - -static const uint8_t BME280_REGISTER_DIG_H1 = 0xA1; -static const uint8_t BME280_REGISTER_DIG_H2 = 0xE1; -static const uint8_t BME280_REGISTER_DIG_H3 = 0xE3; -static const uint8_t BME280_REGISTER_DIG_H4 = 0xE4; -static const uint8_t BME280_REGISTER_DIG_H5 = 0xE5; -static const uint8_t BME280_REGISTER_DIG_H6 = 0xE7; - -static const uint8_t BME280_REGISTER_CHIPID = 0xD0; -static const uint8_t BME280_REGISTER_RESET = 0xE0; - -static const uint8_t BME280_REGISTER_CONTROLHUMID = 0xF2; -static const uint8_t BME280_REGISTER_STATUS = 0xF3; -static const uint8_t BME280_REGISTER_CONTROL = 0xF4; -static const uint8_t BME280_REGISTER_CONFIG = 0xF5; -static const uint8_t BME280_REGISTER_MEASUREMENTS = 0xF7; -static const uint8_t BME280_REGISTER_PRESSUREDATA = 0xF7; -static const uint8_t BME280_REGISTER_TEMPDATA = 0xFA; -static const uint8_t BME280_REGISTER_HUMIDDATA = 0xFD; - -static const uint8_t BME280_MODE_FORCED = 0b01; -static const uint8_t BME280_SOFT_RESET = 0xB6; -static const uint8_t BME280_STATUS_IM_UPDATE = 0b01; - -inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } - -static const char *oversampling_to_str(BME280Oversampling oversampling) { - switch (oversampling) { - case BME280_OVERSAMPLING_NONE: - return "None"; - case BME280_OVERSAMPLING_1X: - return "1x"; - case BME280_OVERSAMPLING_2X: - return "2x"; - case BME280_OVERSAMPLING_4X: - return "4x"; - case BME280_OVERSAMPLING_8X: - return "8x"; - case BME280_OVERSAMPLING_16X: - return "16x"; - default: - return "UNKNOWN"; - } -} - -static const char *iir_filter_to_str(BME280IIRFilter filter) { - switch (filter) { - case BME280_IIR_FILTER_OFF: - return "OFF"; - case BME280_IIR_FILTER_2X: - return "2x"; - case BME280_IIR_FILTER_4X: - return "4x"; - case BME280_IIR_FILTER_8X: - return "8x"; - case BME280_IIR_FILTER_16X: - return "16x"; - default: - return "UNKNOWN"; - } -} - -void BME280Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up BME280..."); - uint8_t chip_id = 0; - - // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries - // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. - if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; - } - - if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } - if (chip_id != 0x60) { - this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(); - return; - } - - // Send a soft reset. - if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { - this->mark_failed(); - return; - } - // Wait until the NVM data has finished loading. - uint8_t status; - uint8_t retry = 5; - do { - delay(2); - if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { - ESP_LOGW(TAG, "Error reading status register."); - this->mark_failed(); - return; - } - } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); - if (status & BME280_STATUS_IM_UPDATE) { - ESP_LOGW(TAG, "Timeout loading NVM."); - this->mark_failed(); - return; - } - - // Read calibration - this->calibration_.t1 = read_u16_le_(BME280_REGISTER_DIG_T1); - this->calibration_.t2 = read_s16_le_(BME280_REGISTER_DIG_T2); - this->calibration_.t3 = read_s16_le_(BME280_REGISTER_DIG_T3); - - this->calibration_.p1 = read_u16_le_(BME280_REGISTER_DIG_P1); - this->calibration_.p2 = read_s16_le_(BME280_REGISTER_DIG_P2); - this->calibration_.p3 = read_s16_le_(BME280_REGISTER_DIG_P3); - this->calibration_.p4 = read_s16_le_(BME280_REGISTER_DIG_P4); - this->calibration_.p5 = read_s16_le_(BME280_REGISTER_DIG_P5); - this->calibration_.p6 = read_s16_le_(BME280_REGISTER_DIG_P6); - this->calibration_.p7 = read_s16_le_(BME280_REGISTER_DIG_P7); - this->calibration_.p8 = read_s16_le_(BME280_REGISTER_DIG_P8); - this->calibration_.p9 = read_s16_le_(BME280_REGISTER_DIG_P9); - - this->calibration_.h1 = read_u8_(BME280_REGISTER_DIG_H1); - this->calibration_.h2 = read_s16_le_(BME280_REGISTER_DIG_H2); - this->calibration_.h3 = read_u8_(BME280_REGISTER_DIG_H3); - this->calibration_.h4 = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); - this->calibration_.h5 = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); - this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6); - - uint8_t humid_control_val = 0; - if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { - this->mark_failed(); - 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(); - return; - } - - uint8_t config_register = 0; - if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { - this->mark_failed(); - 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(); - return; - } -} -void BME280Component::dump_config() { - ESP_LOGCONFIG(TAG, "BME280:"); - LOG_I2C_DEVICE(this); - switch (this->error_code_) { - case COMMUNICATION_FAILED: - ESP_LOGE(TAG, "Communication with BME280 failed!"); - break; - case WRONG_CHIP_ID: - ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?"); - break; - case NONE: - default: - break; - } - ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_)); - LOG_UPDATE_INTERVAL(this); - - LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); - ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->temperature_oversampling_)); - LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); - ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); - LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); - ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_)); -} -float BME280Component::get_setup_priority() const { return setup_priority::DATA; } - -inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } - -void BME280Component::update() { - // Enable sensor - ESP_LOGV(TAG, "Sending conversion request..."); - uint8_t meas_value = 0; - meas_value |= (this->temperature_oversampling_ & 0b111) << 5; - meas_value |= (this->pressure_oversampling_ & 0b111) << 2; - meas_value |= BME280_MODE_FORCED; - if (!this->write_byte(BME280_REGISTER_CONTROL, meas_value)) { - this->status_set_warning(); - return; - } - - float meas_time = 1.5f; - meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_); - meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; - meas_time += 2.3f * oversampling_to_time(this->humidity_oversampling_) + 0.575f; - - this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { - uint8_t data[8]; - if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) { - ESP_LOGW(TAG, "Error reading registers."); - this->status_set_warning(); - return; - } - int32_t t_fine = 0; - float temperature = this->read_temperature_(data, &t_fine); - if (std::isnan(temperature)) { - ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); - this->status_set_warning(); - return; - } - float pressure = this->read_pressure_(data, t_fine); - float humidity = this->read_humidity_(data, t_fine); - - ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->pressure_sensor_ != nullptr) - this->pressure_sensor_->publish_state(pressure); - if (this->humidity_sensor_ != nullptr) - this->humidity_sensor_->publish_state(humidity); - this->status_clear_warning(); - }); -} -float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) { - int32_t adc = ((data[3] & 0xFF) << 16) | ((data[4] & 0xFF) << 8) | (data[5] & 0xFF); - adc >>= 4; - if (adc == 0x80000) { - // temperature was disabled - return NAN; - } - - const int32_t t1 = this->calibration_.t1; - const int32_t t2 = this->calibration_.t2; - const int32_t t3 = this->calibration_.t3; - - int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; - int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; - *t_fine = var1 + var2; - - float temperature = (*t_fine * 5 + 128) >> 8; - return temperature / 100.0f; -} - -float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) { - int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); - adc >>= 4; - if (adc == 0x80000) { - // pressure was disabled - return NAN; - } - const int64_t p1 = this->calibration_.p1; - const int64_t p2 = this->calibration_.p2; - const int64_t p3 = this->calibration_.p3; - const int64_t p4 = this->calibration_.p4; - const int64_t p5 = this->calibration_.p5; - const int64_t p6 = this->calibration_.p6; - const int64_t p7 = this->calibration_.p7; - const int64_t p8 = this->calibration_.p8; - const int64_t p9 = this->calibration_.p9; - - int64_t var1, var2, p; - var1 = int64_t(t_fine) - 128000; - var2 = var1 * var1 * p6; - var2 = var2 + ((var1 * p5) << 17); - var2 = var2 + (p4 << 35); - var1 = ((var1 * var1 * p3) >> 8) + ((var1 * p2) << 12); - var1 = ((int64_t(1) << 47) + var1) * p1 >> 33; - - if (var1 == 0) - return NAN; - - p = 1048576 - adc; - p = (((p << 31) - var2) * 3125) / var1; - var1 = (p9 * (p >> 13) * (p >> 13)) >> 25; - var2 = (p8 * p) >> 19; - - p = ((p + var1 + var2) >> 8) + (p7 << 4); - return (p / 256.0f) / 100.0f; -} - -float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) { - uint16_t raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); - if (raw_adc == 0x8000) - return NAN; - - int32_t adc = raw_adc; - - const int32_t h1 = this->calibration_.h1; - const int32_t h2 = this->calibration_.h2; - const int32_t h3 = this->calibration_.h3; - const int32_t h4 = this->calibration_.h4; - const int32_t h5 = this->calibration_.h5; - const int32_t h6 = this->calibration_.h6; - - int32_t v_x1_u32r = t_fine - 76800; - - v_x1_u32r = ((((adc << 14) - (h4 << 20) - (h5 * v_x1_u32r)) + 16384) >> 15) * - (((((((v_x1_u32r * h6) >> 10) * (((v_x1_u32r * h3) >> 11) + 32768)) >> 10) + 2097152) * h2 + 8192) >> 14); - - v_x1_u32r = v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * h1) >> 4); - - v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r; - v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r; - float h = v_x1_u32r >> 12; - - return h / 1024.0f; -} -void BME280Component::set_temperature_oversampling(BME280Oversampling temperature_over_sampling) { - this->temperature_oversampling_ = temperature_over_sampling; -} -void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over_sampling) { - this->pressure_oversampling_ = pressure_over_sampling; -} -void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) { - this->humidity_oversampling_ = humidity_over_sampling; -} -void BME280Component::set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } -uint8_t BME280Component::read_u8_(uint8_t a_register) { - uint8_t data = 0; - this->read_byte(a_register, &data); - return data; -} -uint16_t BME280Component::read_u16_le_(uint8_t a_register) { - uint16_t data = 0; - this->read_byte_16(a_register, &data); - return (data >> 8) | (data << 8); -} -int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } - -} // namespace bme280 -} // namespace esphome diff --git a/esphome/components/bme280/bme280.h b/esphome/components/bme280/bme280.h deleted file mode 100644 index 50d398c40f..0000000000 --- a/esphome/components/bme280/bme280.h +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" - -namespace esphome { -namespace bme280 { - -/// Internal struct storing the calibration values of an BME280. -struct BME280CalibrationData { - uint16_t t1; // 0x88 - 0x89 - int16_t t2; // 0x8A - 0x8B - int16_t t3; // 0x8C - 0x8D - - uint16_t p1; // 0x8E - 0x8F - int16_t p2; // 0x90 - 0x91 - int16_t p3; // 0x92 - 0x93 - int16_t p4; // 0x94 - 0x95 - int16_t p5; // 0x96 - 0x97 - int16_t p6; // 0x98 - 0x99 - int16_t p7; // 0x9A - 0x9B - int16_t p8; // 0x9C - 0x9D - int16_t p9; // 0x9E - 0x9F - - uint8_t h1; // 0xA1 - int16_t h2; // 0xE1 - 0xE2 - uint8_t h3; // 0xE3 - int16_t h4; // 0xE4 - 0xE5[3:0] - int16_t h5; // 0xE5[7:4] - 0xE6 - int8_t h6; // 0xE7 -}; - -/** Enum listing all Oversampling values for the BME280. - * - * Oversampling basically means measuring a condition multiple times. Higher oversampling - * values therefore increase the time required to read sensor values but increase accuracy. - */ -enum BME280Oversampling { - BME280_OVERSAMPLING_NONE = 0b000, - BME280_OVERSAMPLING_1X = 0b001, - BME280_OVERSAMPLING_2X = 0b010, - BME280_OVERSAMPLING_4X = 0b011, - BME280_OVERSAMPLING_8X = 0b100, - BME280_OVERSAMPLING_16X = 0b101, -}; - -/** Enum listing all Infinite Impulse Filter values for the BME280. - * - * Higher values increase accuracy, but decrease response time. - */ -enum BME280IIRFilter { - BME280_IIR_FILTER_OFF = 0b000, - BME280_IIR_FILTER_2X = 0b001, - BME280_IIR_FILTER_4X = 0b010, - BME280_IIR_FILTER_8X = 0b011, - BME280_IIR_FILTER_16X = 0b100, -}; - -/// This class implements support for the BME280 Temperature+Pressure+Humidity i2c sensor. -class BME280Component : public PollingComponent, public i2c::I2CDevice { - public: - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } - void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } - void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } - - /// Set the oversampling value for the temperature sensor. Default is 16x. - void set_temperature_oversampling(BME280Oversampling temperature_over_sampling); - /// Set the oversampling value for the pressure sensor. Default is 16x. - void set_pressure_oversampling(BME280Oversampling pressure_over_sampling); - /// Set the oversampling value for the humidity sensor. Default is 16x. - void set_humidity_oversampling(BME280Oversampling humidity_over_sampling); - /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. - void set_iir_filter(BME280IIRFilter iir_filter); - - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - void update() override; - - protected: - /// Read the temperature value and store the calculated ambient temperature in t_fine. - float read_temperature_(const uint8_t *data, int32_t *t_fine); - /// Read the pressure value in hPa using the provided t_fine value. - float read_pressure_(const uint8_t *data, int32_t t_fine); - /// Read the humidity value in % using the provided t_fine value. - float read_humidity_(const uint8_t *data, int32_t t_fine); - uint8_t read_u8_(uint8_t a_register); - uint16_t read_u16_le_(uint8_t a_register); - int16_t read_s16_le_(uint8_t a_register); - - BME280CalibrationData calibration_; - BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X}; - BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X}; - BME280Oversampling humidity_oversampling_{BME280_OVERSAMPLING_16X}; - BME280IIRFilter iir_filter_{BME280_IIR_FILTER_OFF}; - sensor::Sensor *temperature_sensor_{nullptr}; - sensor::Sensor *pressure_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; - enum ErrorCode { - NONE = 0, - COMMUNICATION_FAILED, - WRONG_CHIP_ID, - } error_code_{NONE}; -}; - -} // namespace bme280 -} // namespace esphome diff --git a/esphome/components/bme280/sensor.py b/esphome/components/bme280/sensor.py deleted file mode 100644 index dcb842d879..0000000000 --- a/esphome/components/bme280/sensor.py +++ /dev/null @@ -1,119 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import i2c, sensor -from esphome.const import ( - CONF_HUMIDITY, - CONF_ID, - CONF_IIR_FILTER, - CONF_OVERSAMPLING, - CONF_PRESSURE, - CONF_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, - UNIT_PERCENT, -) - -DEPENDENCIES = ["i2c"] - -bme280_ns = cg.esphome_ns.namespace("bme280") -BME280Oversampling = bme280_ns.enum("BME280Oversampling") -OVERSAMPLING_OPTIONS = { - "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE, - "1X": BME280Oversampling.BME280_OVERSAMPLING_1X, - "2X": BME280Oversampling.BME280_OVERSAMPLING_2X, - "4X": BME280Oversampling.BME280_OVERSAMPLING_4X, - "8X": BME280Oversampling.BME280_OVERSAMPLING_8X, - "16X": BME280Oversampling.BME280_OVERSAMPLING_16X, -} - -BME280IIRFilter = bme280_ns.enum("BME280IIRFilter") -IIR_FILTER_OPTIONS = { - "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF, - "2X": BME280IIRFilter.BME280_IIR_FILTER_2X, - "4X": BME280IIRFilter.BME280_IIR_FILTER_4X, - "8X": BME280IIRFilter.BME280_IIR_FILTER_8X, - "16X": BME280IIRFilter.BME280_IIR_FILTER_16X, -} - -BME280Component = bme280_ns.class_( - "BME280Component", cg.PollingComponent, i2c.I2CDevice -) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(BME280Component), - 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, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - unit_of_measurement=UNIT_HECTOPASCAL, - accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( - IIR_FILTER_OPTIONS, upper=True - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x77)) -) - - -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) - - if CONF_TEMPERATURE in config: - conf = config[CONF_TEMPERATURE] - sens = await sensor.new_sensor(conf) - cg.add(var.set_temperature_sensor(sens)) - cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING])) - - if CONF_PRESSURE in config: - conf = config[CONF_PRESSURE] - sens = await sensor.new_sensor(conf) - cg.add(var.set_pressure_sensor(sens)) - cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) - - if CONF_HUMIDITY in config: - conf = config[CONF_HUMIDITY] - sens = await sensor.new_sensor(conf) - cg.add(var.set_humidity_sensor(sens)) - cg.add(var.set_humidity_oversampling(conf[CONF_OVERSAMPLING])) - - cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) diff --git a/esphome/components/bme280_base/__init__.py b/esphome/components/bme280_base/__init__.py new file mode 100644 index 0000000000..6a5f7e1127 --- /dev/null +++ b/esphome/components/bme280_base/__init__.py @@ -0,0 +1,108 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_PERCENT, +) + +CODEOWNERS = ["@esphome/core"] + +bme280_ns = cg.esphome_ns.namespace("bme280_base") +BME280Oversampling = bme280_ns.enum("BME280Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE, + "1X": BME280Oversampling.BME280_OVERSAMPLING_1X, + "2X": BME280Oversampling.BME280_OVERSAMPLING_2X, + "4X": BME280Oversampling.BME280_OVERSAMPLING_4X, + "8X": BME280Oversampling.BME280_OVERSAMPLING_8X, + "16X": BME280Oversampling.BME280_OVERSAMPLING_16X, +} + +BME280IIRFilter = bme280_ns.enum("BME280IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF, + "2X": BME280IIRFilter.BME280_IIR_FILTER_2X, + "4X": BME280IIRFilter.BME280_IIR_FILTER_4X, + "8X": BME280IIRFilter.BME280_IIR_FILTER_8X, + "16X": BME280IIRFilter.BME280_IIR_FILTER_16X, +} + +CONFIG_SCHEMA_BASE = cv.Schema( + { + 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, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING])) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING])) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING])) + + cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) + + return var diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp new file mode 100644 index 0000000000..76e20836c7 --- /dev/null +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -0,0 +1,359 @@ +#include +#include + +#include "bme280_base.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome { +namespace bme280_base { + +static const char *const TAG = "bme280.sensor"; + +static const uint8_t BME280_REGISTER_DIG_T1 = 0x88; +static const uint8_t BME280_REGISTER_DIG_T2 = 0x8A; +static const uint8_t BME280_REGISTER_DIG_T3 = 0x8C; + +static const uint8_t BME280_REGISTER_DIG_P1 = 0x8E; +static const uint8_t BME280_REGISTER_DIG_P2 = 0x90; +static const uint8_t BME280_REGISTER_DIG_P3 = 0x92; +static const uint8_t BME280_REGISTER_DIG_P4 = 0x94; +static const uint8_t BME280_REGISTER_DIG_P5 = 0x96; +static const uint8_t BME280_REGISTER_DIG_P6 = 0x98; +static const uint8_t BME280_REGISTER_DIG_P7 = 0x9A; +static const uint8_t BME280_REGISTER_DIG_P8 = 0x9C; +static const uint8_t BME280_REGISTER_DIG_P9 = 0x9E; + +static const uint8_t BME280_REGISTER_DIG_H1 = 0xA1; +static const uint8_t BME280_REGISTER_DIG_H2 = 0xE1; +static const uint8_t BME280_REGISTER_DIG_H3 = 0xE3; +static const uint8_t BME280_REGISTER_DIG_H4 = 0xE4; +static const uint8_t BME280_REGISTER_DIG_H5 = 0xE5; +static const uint8_t BME280_REGISTER_DIG_H6 = 0xE7; + +static const uint8_t BME280_REGISTER_CHIPID = 0xD0; +static const uint8_t BME280_REGISTER_RESET = 0xE0; + +static const uint8_t BME280_REGISTER_CONTROLHUMID = 0xF2; +static const uint8_t BME280_REGISTER_STATUS = 0xF3; +static const uint8_t BME280_REGISTER_CONTROL = 0xF4; +static const uint8_t BME280_REGISTER_CONFIG = 0xF5; +static const uint8_t BME280_REGISTER_MEASUREMENTS = 0xF7; +static const uint8_t BME280_REGISTER_PRESSUREDATA = 0xF7; +static const uint8_t BME280_REGISTER_TEMPDATA = 0xFA; +static const uint8_t BME280_REGISTER_HUMIDDATA = 0xFD; + +static const uint8_t BME280_MODE_FORCED = 0b01; +static const uint8_t BME280_SOFT_RESET = 0xB6; +static const uint8_t BME280_STATUS_IM_UPDATE = 0b01; + +inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } + +const char *iir_filter_to_str(BME280IIRFilter filter) { // NOLINT + switch (filter) { + case BME280_IIR_FILTER_OFF: + return "OFF"; + case BME280_IIR_FILTER_2X: + return "2x"; + case BME280_IIR_FILTER_4X: + return "4x"; + case BME280_IIR_FILTER_8X: + return "8x"; + case BME280_IIR_FILTER_16X: + return "16x"; + default: + return "UNKNOWN"; + } +} + +const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT + switch (oversampling) { + case BME280_OVERSAMPLING_NONE: + return "None"; + case BME280_OVERSAMPLING_1X: + return "1x"; + case BME280_OVERSAMPLING_2X: + return "2x"; + case BME280_OVERSAMPLING_4X: + return "4x"; + case BME280_OVERSAMPLING_8X: + return "8x"; + case BME280_OVERSAMPLING_16X: + return "16x"; + default: + return "UNKNOWN"; + } +} + +void BME280Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up BME280..."); + uint8_t chip_id = 0; + + // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries + // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. + if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + } + + if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + if (chip_id != 0x60) { + this->error_code_ = WRONG_CHIP_ID; + this->mark_failed(); + return; + } + + // Send a soft reset. + if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { + this->mark_failed(); + return; + } + // Wait until the NVM data has finished loading. + uint8_t status; + uint8_t retry = 5; + do { // NOLINT + delay(2); + if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { + ESP_LOGW(TAG, "Error reading status register."); + this->mark_failed(); + return; + } + } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); + if (status & BME280_STATUS_IM_UPDATE) { + ESP_LOGW(TAG, "Timeout loading NVM."); + this->mark_failed(); + return; + } + + // Read calibration + this->calibration_.t1 = read_u16_le_(BME280_REGISTER_DIG_T1); + this->calibration_.t2 = read_s16_le_(BME280_REGISTER_DIG_T2); + this->calibration_.t3 = read_s16_le_(BME280_REGISTER_DIG_T3); + + this->calibration_.p1 = read_u16_le_(BME280_REGISTER_DIG_P1); + this->calibration_.p2 = read_s16_le_(BME280_REGISTER_DIG_P2); + this->calibration_.p3 = read_s16_le_(BME280_REGISTER_DIG_P3); + this->calibration_.p4 = read_s16_le_(BME280_REGISTER_DIG_P4); + this->calibration_.p5 = read_s16_le_(BME280_REGISTER_DIG_P5); + this->calibration_.p6 = read_s16_le_(BME280_REGISTER_DIG_P6); + this->calibration_.p7 = read_s16_le_(BME280_REGISTER_DIG_P7); + this->calibration_.p8 = read_s16_le_(BME280_REGISTER_DIG_P8); + this->calibration_.p9 = read_s16_le_(BME280_REGISTER_DIG_P9); + + this->calibration_.h1 = read_u8_(BME280_REGISTER_DIG_H1); + this->calibration_.h2 = read_s16_le_(BME280_REGISTER_DIG_H2); + this->calibration_.h3 = read_u8_(BME280_REGISTER_DIG_H3); + this->calibration_.h4 = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); + this->calibration_.h5 = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6); + + uint8_t humid_control_val = 0; + if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { + this->mark_failed(); + 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(); + return; + } + + uint8_t config_register = 0; + if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { + this->mark_failed(); + 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(); + return; + } +} +void BME280Component::dump_config() { + ESP_LOGCONFIG(TAG, "BME280:"); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with BME280 failed!"); + break; + case WRONG_CHIP_ID: + ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?"); + break; + case NONE: + default: + break; + } + ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_)); + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->temperature_oversampling_)); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_)); +} +float BME280Component::get_setup_priority() const { return setup_priority::DATA; } + +inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } + +void BME280Component::update() { + // Enable sensor + ESP_LOGV(TAG, "Sending conversion request..."); + uint8_t meas_value = 0; + meas_value |= (this->temperature_oversampling_ & 0b111) << 5; + meas_value |= (this->pressure_oversampling_ & 0b111) << 2; + meas_value |= BME280_MODE_FORCED; + if (!this->write_byte(BME280_REGISTER_CONTROL, meas_value)) { + this->status_set_warning(); + return; + } + + float meas_time = 1.5f; + meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_); + meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; + meas_time += 2.3f * oversampling_to_time(this->humidity_oversampling_) + 0.575f; + + this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { + uint8_t data[8]; + if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) { + ESP_LOGW(TAG, "Error reading registers."); + this->status_set_warning(); + return; + } + int32_t t_fine = 0; + float const temperature = this->read_temperature_(data, &t_fine); + if (std::isnan(temperature)) { + ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); + this->status_set_warning(); + return; + } + float const pressure = this->read_pressure_(data, t_fine); + float const humidity = this->read_humidity_(data, t_fine); + + ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + this->status_clear_warning(); + }); +} +float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) { + int32_t adc = ((data[3] & 0xFF) << 16) | ((data[4] & 0xFF) << 8) | (data[5] & 0xFF); + adc >>= 4; + if (adc == 0x80000) { + // temperature was disabled + return NAN; + } + + const int32_t t1 = this->calibration_.t1; + const int32_t t2 = this->calibration_.t2; + const int32_t t3 = this->calibration_.t3; + + int32_t const var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; + int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; + *t_fine = var1 + var2; + + float const temperature = (*t_fine * 5 + 128); + return temperature / 25600.0f; +} + +float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) { + int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); + adc >>= 4; + if (adc == 0x80000) { + // pressure was disabled + return NAN; + } + const int64_t p1 = this->calibration_.p1; + const int64_t p2 = this->calibration_.p2; + const int64_t p3 = this->calibration_.p3; + const int64_t p4 = this->calibration_.p4; + const int64_t p5 = this->calibration_.p5; + const int64_t p6 = this->calibration_.p6; + const int64_t p7 = this->calibration_.p7; + const int64_t p8 = this->calibration_.p8; + const int64_t p9 = this->calibration_.p9; + + int64_t var1, var2, p; + var1 = int64_t(t_fine) - 128000; + var2 = var1 * var1 * p6; + var2 = var2 + ((var1 * p5) << 17); + var2 = var2 + (p4 << 35); + var1 = ((var1 * var1 * p3) >> 8) + ((var1 * p2) << 12); + var1 = ((int64_t(1) << 47) + var1) * p1 >> 33; + + if (var1 == 0) + return NAN; + + p = 1048576 - adc; + p = (((p << 31) - var2) * 3125) / var1; + var1 = (p9 * (p >> 13) * (p >> 13)) >> 25; + var2 = (p8 * p) >> 19; + + p = ((p + var1 + var2) >> 8) + (p7 << 4); + return (p / 256.0f) / 100.0f; +} + +float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) { + uint16_t const raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); + if (raw_adc == 0x8000) + return NAN; + + int32_t const adc = raw_adc; + + const int32_t h1 = this->calibration_.h1; + const int32_t h2 = this->calibration_.h2; + const int32_t h3 = this->calibration_.h3; + const int32_t h4 = this->calibration_.h4; + const int32_t h5 = this->calibration_.h5; + const int32_t h6 = this->calibration_.h6; + + int32_t v_x1_u32r = t_fine - 76800; + + v_x1_u32r = ((((adc << 14) - (h4 << 20) - (h5 * v_x1_u32r)) + 16384) >> 15) * + (((((((v_x1_u32r * h6) >> 10) * (((v_x1_u32r * h3) >> 11) + 32768)) >> 10) + 2097152) * h2 + 8192) >> 14); + + v_x1_u32r = v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * h1) >> 4); + + v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r; + v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r; + float const h = v_x1_u32r >> 12; + + return h / 1024.0f; +} +void BME280Component::set_temperature_oversampling(BME280Oversampling temperature_over_sampling) { + this->temperature_oversampling_ = temperature_over_sampling; +} +void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over_sampling) { + this->pressure_oversampling_ = pressure_over_sampling; +} +void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) { + this->humidity_oversampling_ = humidity_over_sampling; +} +void BME280Component::set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } +uint8_t BME280Component::read_u8_(uint8_t a_register) { + uint8_t data = 0; + this->read_byte(a_register, &data); + return data; +} +uint16_t BME280Component::read_u16_le_(uint8_t a_register) { + uint16_t data = 0; + this->read_byte_16(a_register, &data); + return (data >> 8) | (data << 8); +} +int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } + +} // namespace bme280_base +} // namespace esphome diff --git a/esphome/components/bme280_base/bme280_base.h b/esphome/components/bme280_base/bme280_base.h new file mode 100644 index 0000000000..0f55ad0101 --- /dev/null +++ b/esphome/components/bme280_base/bme280_base.h @@ -0,0 +1,114 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bme280_base { + +/// Internal struct storing the calibration values of an BME280. +struct BME280CalibrationData { + uint16_t t1; // 0x88 - 0x89 + int16_t t2; // 0x8A - 0x8B + int16_t t3; // 0x8C - 0x8D + + uint16_t p1; // 0x8E - 0x8F + int16_t p2; // 0x90 - 0x91 + int16_t p3; // 0x92 - 0x93 + int16_t p4; // 0x94 - 0x95 + int16_t p5; // 0x96 - 0x97 + int16_t p6; // 0x98 - 0x99 + int16_t p7; // 0x9A - 0x9B + int16_t p8; // 0x9C - 0x9D + int16_t p9; // 0x9E - 0x9F + + uint8_t h1; // 0xA1 + int16_t h2; // 0xE1 - 0xE2 + uint8_t h3; // 0xE3 + int16_t h4; // 0xE4 - 0xE5[3:0] + int16_t h5; // 0xE5[7:4] - 0xE6 + int8_t h6; // 0xE7 +}; + +/** Enum listing all Oversampling values for the BME280. + * + * Oversampling basically means measuring a condition multiple times. Higher oversampling + * values therefore increase the time required to read sensor values but increase accuracy. + */ +enum BME280Oversampling { + BME280_OVERSAMPLING_NONE = 0b000, + BME280_OVERSAMPLING_1X = 0b001, + BME280_OVERSAMPLING_2X = 0b010, + BME280_OVERSAMPLING_4X = 0b011, + BME280_OVERSAMPLING_8X = 0b100, + BME280_OVERSAMPLING_16X = 0b101, +}; + +/** Enum listing all Infinite Impulse Filter values for the BME280. + * + * Higher values increase accuracy, but decrease response time. + */ +enum BME280IIRFilter { + BME280_IIR_FILTER_OFF = 0b000, + BME280_IIR_FILTER_2X = 0b001, + BME280_IIR_FILTER_4X = 0b010, + BME280_IIR_FILTER_8X = 0b011, + BME280_IIR_FILTER_16X = 0b100, +}; + +/// This class implements support for the BME280 Temperature+Pressure+Humidity sensor. +class BME280Component : public PollingComponent { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + /// Set the oversampling value for the temperature sensor. Default is 16x. + void set_temperature_oversampling(BME280Oversampling temperature_over_sampling); + /// Set the oversampling value for the pressure sensor. Default is 16x. + void set_pressure_oversampling(BME280Oversampling pressure_over_sampling); + /// Set the oversampling value for the humidity sensor. Default is 16x. + void set_humidity_oversampling(BME280Oversampling humidity_over_sampling); + /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. + void set_iir_filter(BME280IIRFilter iir_filter); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + /// Read the temperature value and store the calculated ambient temperature in t_fine. + float read_temperature_(const uint8_t *data, int32_t *t_fine); + /// Read the pressure value in hPa using the provided t_fine value. + float read_pressure_(const uint8_t *data, int32_t t_fine); + /// Read the humidity value in % using the provided t_fine value. + float read_humidity_(const uint8_t *data, int32_t t_fine); + uint8_t read_u8_(uint8_t a_register); + uint16_t read_u16_le_(uint8_t a_register); + int16_t read_s16_le_(uint8_t a_register); + + virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0; + + BME280CalibrationData calibration_; + BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X}; + BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X}; + BME280Oversampling humidity_oversampling_{BME280_OVERSAMPLING_16X}; + BME280IIRFilter iir_filter_{BME280_IIR_FILTER_OFF}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + WRONG_CHIP_ID, + } error_code_{NONE}; +}; + +} // namespace bme280_base +} // namespace esphome diff --git a/esphome/components/bme280_i2c/__init__.py b/esphome/components/bme280_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bme280_i2c/bme280_i2c.cpp b/esphome/components/bme280_i2c/bme280_i2c.cpp new file mode 100644 index 0000000000..e29675b5b7 --- /dev/null +++ b/esphome/components/bme280_i2c/bme280_i2c.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "bme280_i2c.h" +#include "esphome/components/i2c/i2c.h" +#include "../bme280_base/bme280_base.h" + +namespace esphome { +namespace bme280_i2c { + +bool BME280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { + return I2CDevice::read_byte(a_register, data); +}; +bool BME280I2CComponent::write_byte(uint8_t a_register, uint8_t data) { + return I2CDevice::write_byte(a_register, data); +}; +bool BME280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::read_bytes(a_register, data, len); +}; +bool BME280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + return I2CDevice::read_byte_16(a_register, data); +}; + +void BME280I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BME280Component::dump_config(); +} + +} // namespace bme280_i2c +} // namespace esphome diff --git a/esphome/components/bme280_i2c/bme280_i2c.h b/esphome/components/bme280_i2c/bme280_i2c.h new file mode 100644 index 0000000000..c5e2f7e342 --- /dev/null +++ b/esphome/components/bme280_i2c/bme280_i2c.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/bme280_base/bme280_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bme280_i2c { + +static const char *const TAG = "bme280_i2c.sensor"; + +class BME280I2CComponent : public esphome::bme280_base::BME280Component, public i2c::I2CDevice { + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool read_byte_16(uint8_t a_register, uint16_t *data) override; + void dump_config() override; +}; + +} // namespace bme280_i2c +} // namespace esphome diff --git a/esphome/components/bme280_i2c/sensor.py b/esphome/components/bme280_i2c/sensor.py new file mode 100644 index 0000000000..f3007ebaac --- /dev/null +++ b/esphome/components/bme280_i2c/sensor.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from ..bme280_base import to_code_base, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["bme280_base"] +DEPENDENCIES = ["i2c"] + +bme280_ns = cg.esphome_ns.namespace("bme280_i2c") +BME280I2CComponent = bme280_ns.class_( + "BME280I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(BME280I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bme280_spi/__init__.py b/esphome/components/bme280_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bme280_spi/bme280_spi.cpp b/esphome/components/bme280_spi/bme280_spi.cpp new file mode 100644 index 0000000000..c6ebfdfd0b --- /dev/null +++ b/esphome/components/bme280_spi/bme280_spi.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include "bme280_spi.h" +#include + +namespace esphome { +namespace bme280_spi { + +uint8_t set_bit(uint8_t num, int position) { + int mask = 1 << position; + return num | mask; +} + +uint8_t clear_bit(uint8_t num, int position) { + int mask = 1 << position; + return num & ~mask; +} + +void BME280SPIComponent::setup() { + this->spi_setup(); + BME280Component::setup(); +}; + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used +// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte +// 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf + +bool BME280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool BME280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(clear_bit(a_register, 7)); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool BME280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + this->read_array(data, len); + this->disable(); + return true; +} + +bool BME280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + ((uint8_t *) data)[1] = this->transfer_byte(0); + ((uint8_t *) data)[0] = this->transfer_byte(0); + this->disable(); + return true; +} + +} // namespace bme280_spi +} // namespace esphome diff --git a/esphome/components/bme280_spi/bme280_spi.h b/esphome/components/bme280_spi/bme280_spi.h new file mode 100644 index 0000000000..b6b8997fa7 --- /dev/null +++ b/esphome/components/bme280_spi/bme280_spi.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/bme280_base/bme280_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace bme280_spi { + +class BME280SPIComponent : public esphome::bme280_base::BME280Component, + public spi::SPIDevice { + void setup() override; + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool read_byte_16(uint8_t a_register, uint16_t *data) override; +}; + +} // namespace bme280_spi +} // namespace esphome diff --git a/esphome/components/bme280_spi/sensor.py b/esphome/components/bme280_spi/sensor.py new file mode 100644 index 0000000000..33a12318a5 --- /dev/null +++ b/esphome/components/bme280_spi/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from ..bme280_base import to_code_base, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["bme280_base"] +CODEOWNERS = ["@apbodrov"] +DEPENDENCIES = ["spi"] + + +bme280_spi_ns = cg.esphome_ns.namespace("bme280_spi") +BME280SPIComponent = bme280_spi_ns.class_( + "BME280SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend( + {cv.GenerateID(): cv.declare_id(BME280SPIComponent)} +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/bme680/sensor.py b/esphome/components/bme680/sensor.py index 76472c7562..586b454697 100644 --- a/esphome/components/bme680/sensor.py +++ b/esphome/components/bme680/sensor.py @@ -130,27 +130,23 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - conf = config[CONF_TEMPERATURE] - sens = await sensor.new_sensor(conf) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) cg.add(var.set_temperature_sensor(sens)) - cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING])) + cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING])) - if CONF_PRESSURE in config: - conf = config[CONF_PRESSURE] - sens = await sensor.new_sensor(conf) + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) cg.add(var.set_pressure_sensor(sens)) - cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) + cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING])) - if CONF_HUMIDITY in config: - conf = config[CONF_HUMIDITY] - sens = await sensor.new_sensor(conf) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) cg.add(var.set_humidity_sensor(sens)) - cg.add(var.set_humidity_oversampling(conf[CONF_OVERSAMPLING])) + cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING])) - if CONF_GAS_RESISTANCE in config: - conf = config[CONF_GAS_RESISTANCE] - sens = await sensor.new_sensor(conf) + if gas_resistance_config := config.get(CONF_GAS_RESISTANCE): + sens = await sensor.new_sensor(gas_resistance_config) cg.add(var.set_gas_resistance_sensor(sens)) cg.add(var.set_iir_filter(IIR_FILTER_OPTIONS[config[CONF_IIR_FILTER]])) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 085d2a574b..743ef6e85d 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, esp32 -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] @@ -9,9 +9,8 @@ AUTO_LOAD = ["sensor", "text_sensor"] MULTI_CONF = True CONF_BME680_BSEC_ID = "bme680_bsec_id" -CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_IAQ_MODE = "iaq_mode" -CONF_SAMPLE_RATE = "sample_rate" +CONF_SUPPLY_VOLTAGE = "supply_voltage" CONF_STATE_SAVE_INTERVAL = "state_save_interval" bme680_bsec_ns = cg.esphome_ns.namespace("bme680_bsec") @@ -22,6 +21,12 @@ IAQ_MODE_OPTIONS = { "MOBILE": IAQMode.IAQ_MODE_MOBILE, } +SupplyVoltage = bme680_bsec_ns.enum("SupplyVoltage") +SUPPLY_VOLTAGE_OPTIONS = { + "1.8V": SupplyVoltage.SUPPLY_VOLTAGE_1V8, + "3.3V": SupplyVoltage.SUPPLY_VOLTAGE_3V3, +} + SampleRate = bme680_bsec_ns.enum("SampleRate") SAMPLE_RATE_OPTIONS = { "LP": SampleRate.SAMPLE_RATE_LP, @@ -40,6 +45,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( IAQ_MODE_OPTIONS, upper=True ), + cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum( + SUPPLY_VOLTAGE_OPTIONS, upper=True + ), cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( SAMPLE_RATE_OPTIONS, upper=True ), @@ -67,6 +75,7 @@ async def to_code(config): cg.add(var.set_device_id(str(config[CONF_ID]))) cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE])) + cg.add(var.set_supply_voltage(config[CONF_SUPPLY_VOLTAGE])) cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) cg.add( var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 2b1b0dc948..17dae35b5c 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -52,17 +52,33 @@ void BME680BSECComponent::setup() { void BME680BSECComponent::set_config_() { if (this->sample_rate_ == SAMPLE_RATE_ULP) { - const uint8_t config[] = { + if (this->supply_voltage_ == SUPPLY_VOLTAGE_3V3) { + const uint8_t config[] = { #include "config/generic_33v_300s_28d/bsec_iaq.txt" - }; - this->bsec_status_ = - bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); - } else { - const uint8_t config[] = { + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } else { // SUPPLY_VOLTAGE_1V8 + const uint8_t config[] = { +#include "config/generic_18v_300s_28d/bsec_iaq.txt" + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } + } else { // SAMPLE_RATE_LP + if (this->supply_voltage_ == SUPPLY_VOLTAGE_3V3) { + const uint8_t config[] = { #include "config/generic_33v_3s_28d/bsec_iaq.txt" - }; - this->bsec_status_ = - bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } else { // SUPPLY_VOLTAGE_1V8 + const uint8_t config[] = { +#include "config/generic_18v_3s_28d/bsec_iaq.txt" + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } } } @@ -145,6 +161,7 @@ void BME680BSECComponent::dump_config() { ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_); ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile"); + ESP_LOGCONFIG(TAG, " Supply Voltage: %sV", this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8"); ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_)); ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_); diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index a97ad2f53e..e52dbe964b 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -21,6 +21,11 @@ enum IAQMode { IAQ_MODE_MOBILE = 1, }; +enum SupplyVoltage { + SUPPLY_VOLTAGE_3V3 = 0, + SUPPLY_VOLTAGE_1V8 = 1, +}; + enum SampleRate { SAMPLE_RATE_LP = 0, SAMPLE_RATE_ULP = 1, @@ -35,6 +40,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; } void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + void set_supply_voltage(SupplyVoltage supply_voltage) { this->supply_voltage_ = supply_voltage; } void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; } @@ -109,6 +115,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { std::string device_id_; float temperature_offset_{0}; IAQMode iaq_mode_{IAQ_MODE_STATIC}; + SupplyVoltage supply_voltage_; SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py index 8d00012150..aa96998232 100644 --- a/esphome/components/bme680_bsec/sensor.py +++ b/esphome/components/bme680_bsec/sensor.py @@ -4,38 +4,37 @@ from esphome.components import sensor from esphome.const import ( CONF_GAS_RESISTANCE, CONF_HUMIDITY, + CONF_IAQ_ACCURACY, CONF_PRESSURE, + CONF_SAMPLE_RATE, CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ICON_GAS_CYLINDER, + ICON_GAUGE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_HECTOPASCAL, UNIT_OHM, UNIT_PARTS_PER_MILLION, UNIT_PERCENT, - ICON_GAS_CYLINDER, - ICON_GAUGE, - ICON_THERMOMETER, - ICON_WATER_PERCENT, ) from . import ( BME680BSECComponent, CONF_BME680_BSEC_ID, - CONF_SAMPLE_RATE, SAMPLE_RATE_OPTIONS, ) DEPENDENCIES = ["bme680_bsec"] CONF_IAQ = "iaq" -CONF_IAQ_ACCURACY = "iaq_accuracy" CONF_CO2_EQUIVALENT = "co2_equivalent" CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" UNIT_IAQ = "IAQ" ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" -ICON_TEST_TUBE = "mdi:test-tube" TYPES = [ CONF_TEMPERATURE, @@ -53,7 +52,6 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - icon=ICON_THERMOMETER, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -62,16 +60,14 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, - icon=ICON_GAUGE, accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, - icon=ICON_WATER_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, @@ -97,14 +93,14 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, - icon=ICON_TEST_TUBE, accuracy_decimals=1, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, - icon=ICON_TEST_TUBE, accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), } @@ -112,12 +108,13 @@ CONFIG_SCHEMA = cv.Schema( async def setup_conf(config, key, hub): - if key in config: - conf = config[key] - sens = await sensor.new_sensor(conf) + if sensor_config := config.get(key): + sens = await sensor.new_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_sensor")(sens)) - if CONF_SAMPLE_RATE in conf: - cg.add(getattr(hub, f"set_{key}_sample_rate")(conf[CONF_SAMPLE_RATE])) + if CONF_SAMPLE_RATE in sensor_config: + cg.add( + getattr(hub, f"set_{key}_sample_rate")(sensor_config[CONF_SAMPLE_RATE]) + ) async def to_code(config): diff --git a/esphome/components/bme680_bsec/text_sensor.py b/esphome/components/bme680_bsec/text_sensor.py index 2d93c90818..6b46e501da 100644 --- a/esphome/components/bme680_bsec/text_sensor.py +++ b/esphome/components/bme680_bsec/text_sensor.py @@ -1,11 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor +from esphome.const import CONF_IAQ_ACCURACY from . import BME680BSECComponent, CONF_BME680_BSEC_ID DEPENDENCIES = ["bme680_bsec"] -CONF_IAQ_ACCURACY = "iaq_accuracy" ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" TYPES = [CONF_IAQ_ACCURACY] @@ -21,9 +21,8 @@ CONFIG_SCHEMA = cv.Schema( async def setup_conf(config, key, hub): - if key in config: - conf = config[key] - sens = await text_sensor.new_text_sensor(conf) + if sensor_config := config.get(key): + sens = await text_sensor.new_text_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py new file mode 100644 index 0000000000..d6dbb52f18 --- /dev/null +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -0,0 +1,196 @@ +import hashlib +from pathlib import Path + +from esphome import core, external_files +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODEL, + CONF_RAW_DATA_ID, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE_OFFSET, +) + +CODEOWNERS = ["@neffs", "@kbx81"] + +DOMAIN = "bme68x_bsec2" + +BSEC2_LIBRARY_VERSION = "v1.8.2610" + +CONF_ALGORITHM_OUTPUT = "algorithm_output" +CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id" +CONF_IAQ_MODE = "iaq_mode" +CONF_OPERATING_AGE = "operating_age" +CONF_STATE_SAVE_INTERVAL = "state_save_interval" +CONF_SUPPLY_VOLTAGE = "supply_voltage" + +bme68x_bsec2_ns = cg.esphome_ns.namespace("bme68x_bsec2") +BME68xBSEC2Component = bme68x_bsec2_ns.class_("BME68xBSEC2Component", cg.Component) + + +MODEL_OPTIONS = ["bme680", "bme688"] + +AlgorithmOutput = bme68x_bsec2_ns.enum("AlgorithmOutput") +ALGORITHM_OUTPUT_OPTIONS = { + "classification": AlgorithmOutput.ALGORITHM_OUTPUT_CLASSIFICATION, + "regression": AlgorithmOutput.ALGORITHM_OUTPUT_REGRESSION, +} + +OperatingAge = bme68x_bsec2_ns.enum("OperatingAge") +OPERATING_AGE_OPTIONS = { + "4d": OperatingAge.OPERATING_AGE_4D, + "28d": OperatingAge.OPERATING_AGE_28D, +} + +SampleRate = bme68x_bsec2_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "LP": SampleRate.SAMPLE_RATE_LP, + "ULP": SampleRate.SAMPLE_RATE_ULP, +} + +Voltage = bme68x_bsec2_ns.enum("Voltage") +VOLTAGE_OPTIONS = { + "1.8V": Voltage.VOLTAGE_1_8V, + "3.3V": Voltage.VOLTAGE_3_3V, +} + +ALGORITHM_OUTPUT_FILE_NAME = { + "classification": "sel", + "regression": "reg", +} + +SAMPLE_RATE_FILE_NAME = { + "LP": "3s", + "ULP": "300s", +} + +VOLTAGE_FILE_NAME = { + "1.8V": "18v", + "3.3V": "33v", +} + + +def _compute_local_file_path(url: str) -> Path: + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + return base_dir / key + + +def _compute_url(config: dict) -> str: + model = config.get(CONF_MODEL) + operating_age = config.get(CONF_OPERATING_AGE) + sample_rate = SAMPLE_RATE_FILE_NAME[config.get(CONF_SAMPLE_RATE)] + volts = VOLTAGE_FILE_NAME[config.get(CONF_SUPPLY_VOLTAGE)] + if model == "bme688": + algo = ALGORITHM_OUTPUT_FILE_NAME[ + config.get(CONF_ALGORITHM_OUTPUT, "classification") + ] + filename = "bsec_selectivity" + else: + algo = "iaq" + filename = "bsec_iaq" + return f"https://raw.githubusercontent.com/boschsensortec/Bosch-BSEC2-Library/{BSEC2_LIBRARY_VERSION}/src/config/{model}/{model}_{algo}_{volts}_{sample_rate}_{operating_age}/{filename}.txt" + + +def download_bme68x_blob(config): + url = _compute_url(config) + path = _compute_local_file_path(url) + external_files.download_content(url, path) + + return config + + +def validate_bme68x(config): + if CONF_ALGORITHM_OUTPUT not in config: + return config + + if config[CONF_MODEL] != "bme688": + raise cv.Invalid(f"{CONF_ALGORITHM_OUTPUT} is only valid for BME688") + + if config[CONF_ALGORITHM_OUTPUT] == "regression" and ( + config[CONF_OPERATING_AGE] != "4d" + or config[CONF_SAMPLE_RATE] != "ULP" + or config[CONF_SUPPLY_VOLTAGE] != "1.8V" + ): + raise cv.Invalid( + f" To use '{CONF_ALGORITHM_OUTPUT}: regression', {CONF_OPERATING_AGE} must be '4d', {CONF_SAMPLE_RATE} must be 'ULP' and {CONF_SUPPLY_VOLTAGE} must be '1.8V'" + ) + return config + + +CONFIG_SCHEMA_BASE = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME68xBSEC2Component), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_MODEL): cv.one_of(*MODEL_OPTIONS, lower=True), + cv.Optional(CONF_ALGORITHM_OUTPUT): cv.enum( + ALGORITHM_OUTPUT_OPTIONS, lower=True + ), + cv.Optional(CONF_OPERATING_AGE, default="28d"): cv.enum( + OPERATING_AGE_OPTIONS, lower=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( + SAMPLE_RATE_OPTIONS, upper=True + ), + cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum( + VOLTAGE_OPTIONS, upper=True + ), + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional( + CONF_STATE_SAVE_INTERVAL, default="6hours" + ): cv.positive_time_period_minutes, + }, + ) + .add_extra(cv.only_with_arduino) + .add_extra(validate_bme68x) + .add_extra(download_bme68x_blob) +) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if algo_output := config.get(CONF_ALGORITHM_OUTPUT): + cg.add(var.set_algorithm_output(algo_output)) + cg.add(var.set_operating_age(config[CONF_OPERATING_AGE])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add(var.set_voltage(config[CONF_SUPPLY_VOLTAGE])) + cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + cg.add( + var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) + ) + + path = _compute_local_file_path(_compute_url(config)) + + try: + with open(path, encoding="utf-8") as f: + bsec2_iaq_config = f.read() + except Exception as e: + raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}") + + # Convert retrieved BSEC2 config to an array of ints + rhs = [int(x) for x in bsec2_iaq_config.split(",")] + # Create an array which will reside in program memory and configure the sensor instance to use it + bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) + + # Although this component does not use SPI, the BSEC2 library requires the SPI library + cg.add_library("SPI", None) + cg.add_library( + "BME68x Sensor library", + "1.1.40407", + ) + cg.add_library( + "BSEC2 Software Library", + None, + f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}", + ) + + cg.add_define("USE_BSEC2") + + return var diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp new file mode 100644 index 0000000000..5425bbd5b7 --- /dev/null +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -0,0 +1,523 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_BSEC2 +#include "bme68x_bsec2.h" + +#include + +namespace esphome { +namespace bme68x_bsec2 { + +#define BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(a) (a == ALGORITHM_OUTPUT_CLASSIFICATION ? "Classification" : "Regression") +#define BME68X_BSEC2_OPERATING_AGE_LOG(o) (o == OPERATING_AGE_4D ? "4 days" : "28 days") +#define BME68X_BSEC2_SAMPLE_RATE_LOG(r) (r == SAMPLE_RATE_DEFAULT ? "Default" : (r == SAMPLE_RATE_ULP ? "ULP" : "LP")) +#define BME68X_BSEC2_VOLTAGE_LOG(v) (v == VOLTAGE_3_3V ? "3.3V" : "1.8V") + +static const char *const TAG = "bme68x_bsec2.sensor"; + +static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +void BME68xBSEC2Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up BME68X via BSEC2..."); + + this->bsec_status_ = bsec_init_m(&this->bsec_instance_); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_init_m failed: status %d", this->bsec_status_); + return; + } + + bsec_get_version_m(&this->bsec_instance_, &this->version_); + + this->bme68x_status_ = bme68x_init(&this->bme68x_); + if (this->bme68x_status_ != BME68X_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bme68x_init failed: status %d", this->bme68x_status_); + return; + } + if (this->bsec2_configuration_ != nullptr && this->bsec2_configuration_length_) { + this->set_config_(this->bsec2_configuration_, this->bsec2_configuration_length_); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_set_configuration_m failed: status %d", this->bsec_status_); + return; + } + } + + this->update_subscription_(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_update_subscription_m failed: status %d", this->bsec_status_); + return; + } + + this->load_state_(); +} + +void BME68xBSEC2Component::dump_config() { + ESP_LOGCONFIG(TAG, "BME68X via BSEC2:"); + + ESP_LOGCONFIG(TAG, " BSEC2 version: %d.%d.%d.%d", this->version_.major, this->version_.minor, + this->version_.major_bugfix, this->version_.minor_bugfix); + + ESP_LOGCONFIG(TAG, " BSEC2 configuration blob:"); + ESP_LOGCONFIG(TAG, " Configured: %s", YESNO(this->bsec2_blob_configured_)); + if (this->bsec2_configuration_ != nullptr && this->bsec2_configuration_length_) { + ESP_LOGCONFIG(TAG, " Size: %" PRIu32, this->bsec2_configuration_length_); + } + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_, + this->bme68x_status_); + } + + if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) { + ESP_LOGCONFIG(TAG, " Algorithm output: %s", BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(this->algorithm_output_)); + } + ESP_LOGCONFIG(TAG, " Operating age: %s", BME68X_BSEC2_OPERATING_AGE_LOG(this->operating_age_)); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->sample_rate_)); + ESP_LOGCONFIG(TAG, " Voltage: %s", BME68X_BSEC2_VOLTAGE_LOG(this->voltage_)); + ESP_LOGCONFIG(TAG, " State save interval: %ims", this->state_save_interval_ms_); + ESP_LOGCONFIG(TAG, " Temperature offset: %.2f", this->temperature_offset_); + +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->temperature_sample_rate_)); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->pressure_sample_rate_)); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->humidity_sample_rate_)); + LOG_SENSOR(" ", "Gas resistance", this->gas_resistance_sensor_); + LOG_SENSOR(" ", "CO2 equivalent", this->co2_equivalent_sensor_); + LOG_SENSOR(" ", "Breath VOC equivalent", this->breath_voc_equivalent_sensor_); + LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); + LOG_SENSOR(" ", "IAQ static", this->iaq_static_sensor_); + LOG_SENSOR(" ", "Numeric IAQ accuracy", this->iaq_accuracy_sensor_); +#endif +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "IAQ accuracy", this->iaq_accuracy_text_sensor_); +#endif +} + +float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; } + +void BME68xBSEC2Component::loop() { + this->run_(); + + if (this->bsec_status_ < BSEC_OK || this->bme68x_status_ < BME68X_OK) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + if (this->bsec_status_ > BSEC_OK || this->bme68x_status_ > BME68X_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + // Process a single action from the queue. These are primarily sensor state publishes + // that in totality take too long to send in a single call. + if (this->queue_.size()) { + auto action = std::move(this->queue_.front()); + this->queue_.pop(); + action(); + } +} + +void BME68xBSEC2Component::set_config_(const uint8_t *config, uint32_t len) { + if (len > BSEC_MAX_PROPERTY_BLOB_SIZE) { + ESP_LOGE(TAG, "Configuration is larger than BSEC_MAX_PROPERTY_BLOB_SIZE"); + this->mark_failed(); + return; + } + uint8_t work_buffer[BSEC_MAX_PROPERTY_BLOB_SIZE]; + this->bsec_status_ = bsec_set_configuration_m(&this->bsec_instance_, config, len, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ == BSEC_OK) { + this->bsec2_blob_configured_ = true; + } +} + +float BME68xBSEC2Component::calc_sensor_sample_rate_(SampleRate sample_rate) { + if (sample_rate == SAMPLE_RATE_DEFAULT) { + sample_rate = this->sample_rate_; + } + return sample_rate == SAMPLE_RATE_ULP ? BSEC_SAMPLE_RATE_ULP : BSEC_SAMPLE_RATE_LP; +} + +void BME68xBSEC2Component::update_subscription_() { + bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; + uint8_t num_virtual_sensors = 0; +#ifdef USE_SENSOR + if (this->iaq_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->iaq_static_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_STATIC_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->co2_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->breath_voc_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->pressure_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->pressure_sample_rate_); + num_virtual_sensors++; + } + + if (this->gas_resistance_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->temperature_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->temperature_sample_rate_); + num_virtual_sensors++; + } + + if (this->humidity_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->humidity_sample_rate_); + num_virtual_sensors++; + } +#endif + bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR]; + uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; + this->bsec_status_ = bsec_update_subscription_m(&this->bsec_instance_, virtual_sensors, num_virtual_sensors, + sensor_settings, &num_sensor_settings); +} + +void BME68xBSEC2Component::run_() { + int64_t curr_time_ns = this->get_time_ns_(); + if (curr_time_ns < this->next_call_ns_) { + return; + } + this->op_mode_ = this->bsec_settings_.op_mode; + uint8_t status; + + ESP_LOGV(TAG, "Performing sensor run"); + + struct bme68x_conf bme68x_conf; + this->bsec_status_ = bsec_sensor_control_m(&this->bsec_instance_, curr_time_ns, &this->bsec_settings_); + if (this->bsec_status_ < BSEC_OK) { + ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_); + return; + } + this->next_call_ns_ = this->bsec_settings_.next_call; + + if (this->bsec_settings_.trigger_measurement) { + bme68x_get_conf(&bme68x_conf, &this->bme68x_); + + bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling; + bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling; + bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling; + bme68x_set_conf(&bme68x_conf, &this->bme68x_); + + switch (this->bsec_settings_.op_mode) { + case BME68X_FORCED_MODE: + this->bme68x_heatr_conf_.enable = BME68X_ENABLE; + this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature; + this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration; + + status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); + status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); + this->op_mode_ = BME68X_FORCED_MODE; + this->sleep_mode_ = false; + ESP_LOGV(TAG, "Using forced mode"); + + break; + case BME68X_PARALLEL_MODE: + if (this->op_mode_ != this->bsec_settings_.op_mode) { + this->bme68x_heatr_conf_.enable = BME68X_ENABLE; + this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile; + this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile; + this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len; + this->bme68x_heatr_conf_.shared_heatr_dur = + BSEC_TOTAL_HEAT_DUR - + (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000)); + + status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + + status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); + this->op_mode_ = BME68X_PARALLEL_MODE; + this->sleep_mode_ = false; + ESP_LOGV(TAG, "Using parallel mode"); + } + break; + case BME68X_SLEEP_MODE: + if (!this->sleep_mode_) { + bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_); + this->sleep_mode_ = true; + ESP_LOGV(TAG, "Using sleep mode"); + } + break; + } + + uint32_t meas_dur = 0; + meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); + ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); + this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); }); + } else { + ESP_LOGV(TAG, "Measurement not required"); + this->read_(curr_time_ns); + } +} + +void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { + ESP_LOGV(TAG, "Reading data"); + + if (this->bsec_settings_.trigger_measurement) { + uint8_t current_op_mode; + this->bme68x_status_ = bme68x_get_op_mode(¤t_op_mode, &this->bme68x_); + + if (current_op_mode == BME68X_SLEEP_MODE) { + ESP_LOGV(TAG, "Still in sleep mode, doing nothing"); + return; + } + } + + if (!this->bsec_settings_.process_data) { + ESP_LOGV(TAG, "Data processing not required"); + return; + } + + struct bme68x_data data[3]; + uint8_t nFields = 0; + this->bme68x_status_ = bme68x_get_data(this->op_mode_, &data[0], &nFields, &this->bme68x_); + + if (this->bme68x_status_ != BME68X_OK) { + ESP_LOGW(TAG, "Failed to get sensor data (BME68X error code %d)", this->bme68x_status_); + return; + } + if (nFields < 1) { + ESP_LOGD(TAG, "BME68X did not provide new data"); + return; + } + + for (uint8_t i = 0; i < nFields; i++) { + bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance + uint8_t num_inputs = 0; + + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_TEMPERATURE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; + inputs[num_inputs].signal = data[i].temperature; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_HEATSOURCE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; + inputs[num_inputs].signal = this->temperature_offset_; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_HUMIDITY)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; + inputs[num_inputs].signal = data[i].humidity; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_PRESSURE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; + inputs[num_inputs].signal = data[i].pressure; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_GASRESISTOR)) { + if (data[i].status & BME68X_GASM_VALID_MSK) { + inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; + inputs[num_inputs].signal = data[i].gas_resistance; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } else { + ESP_LOGD(TAG, "BME68X did not report gas data"); + } + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_PROFILE_PART) && + (data[i].status & BME68X_GASM_VALID_MSK)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PROFILE_PART; + inputs[num_inputs].signal = (this->op_mode_ == BME68X_FORCED_MODE) ? 0 : data[i].gas_index; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + + if (num_inputs < 1) { + ESP_LOGD(TAG, "No signal inputs available for BSEC2"); + return; + } + + bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; + uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; + this->bsec_status_ = bsec_do_steps_m(&this->bsec_instance_, inputs, num_inputs, outputs, &num_outputs); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "BSEC2 failed to process signals (BSEC2 error code %d)", this->bsec_status_); + return; + } + if (num_outputs < 1) { + ESP_LOGD(TAG, "No signal outputs provided by BSEC2"); + return; + } + + this->publish_(outputs, num_outputs); + } +} + +void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { + ESP_LOGV(TAG, "Publishing sensor states"); + bool update_accuracy = false; + uint8_t max_accuracy = 0; + for (uint8_t i = 0; i < num_outputs; i++) { + float signal = outputs[i].signal; + switch (outputs[i].sensor_id) { + case BSEC_OUTPUT_IAQ: + max_accuracy = std::max(outputs[i].accuracy, max_accuracy); + update_accuracy = true; +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_STATIC_IAQ: + max_accuracy = std::max(outputs[i].accuracy, max_accuracy); + update_accuracy = true; +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_static_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_CO2_EQUIVALENT: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_RAW_PRESSURE: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); }); +#endif + break; + case BSEC_OUTPUT_RAW_GAS: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); }); +#endif + break; + } + } + if (update_accuracy) { +#ifdef USE_SENSOR + this->queue_push_( + [this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); }); +#endif +#ifdef USE_TEXT_SENSOR + this->queue_push_([this, max_accuracy]() { + this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[max_accuracy]); + }); +#endif + // Queue up an opportunity to save state + this->queue_push_([this, max_accuracy]() { this->save_state_(max_accuracy); }); + } +} + +int64_t BME68xBSEC2Component::get_time_ns_() { + int64_t time_ms = millis(); + if (this->last_time_ms_ > time_ms) { + this->millis_overflow_counter_++; + } + this->last_time_ms_ = time_ms; + + return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); +} + +#ifdef USE_SENSOR +void BME68xBSEC2Component::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) { + if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} +#endif + +#ifdef USE_TEXT_SENSOR +void BME68xBSEC2Component::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { + if (!sensor || (sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} +#endif + +void BME68xBSEC2Component::load_state_() { + uint32_t hash = this->get_hash(); + this->bsec_state_ = global_preferences->make_preference(hash, true); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + if (this->bsec_state_.load(&state)) { + ESP_LOGV(TAG, "Loading state"); + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = + bsec_set_state_m(&this->bsec_instance_, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed to load state (BSEC2 error code %d)", this->bsec_status_); + } + ESP_LOGI(TAG, "Loaded state"); + } +} + +void BME68xBSEC2Component::save_state_(uint8_t accuracy) { + if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { + return; + } + + ESP_LOGV(TAG, "Saving state"); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; + uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; + + this->bsec_status_ = bsec_get_state_m(&this->bsec_instance_, 0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, + BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed fetch state for save (BSEC2 error code %d)", this->bsec_status_); + return; + } + + if (!this->bsec_state_.save(&state)) { + ESP_LOGW(TAG, "Failed to save state"); + return; + } + this->last_state_save_ms_ = millis(); + + ESP_LOGI(TAG, "Saved state"); +} + +} // namespace bme68x_bsec2 +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h new file mode 100644 index 0000000000..7b9db2b7bf --- /dev/null +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -0,0 +1,163 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" + +#ifdef USE_BSEC2 + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif + +#include +#include + +#include + +namespace esphome { +namespace bme68x_bsec2 { + +enum AlgorithmOutput { + ALGORITHM_OUTPUT_IAQ, + ALGORITHM_OUTPUT_CLASSIFICATION, + ALGORITHM_OUTPUT_REGRESSION, +}; + +enum OperatingAge { + OPERATING_AGE_4D, + OPERATING_AGE_28D, +}; + +enum SampleRate { + SAMPLE_RATE_LP = 0, + SAMPLE_RATE_ULP = 1, + SAMPLE_RATE_DEFAULT = 2, +}; + +enum Voltage { + VOLTAGE_1_8V, + VOLTAGE_3_3V, +}; + +class BME68xBSEC2Component : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; } + void set_operating_age(OperatingAge operating_age) { this->operating_age_ = operating_age; } + void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } + void set_voltage(Voltage voltage) { this->voltage_ = voltage; } + + void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } + void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; } + void set_pressure_sample_rate(SampleRate sample_rate) { this->pressure_sample_rate_ = sample_rate; } + void set_humidity_sample_rate(SampleRate sample_rate) { this->humidity_sample_rate_ = sample_rate; } + + void set_bsec2_configuration(const uint8_t *data, const uint32_t len) { + this->bsec2_configuration_ = data; + this->bsec2_configuration_length_ = len; + } + + void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + +#ifdef USE_SENSOR + void set_temperature_sensor(sensor::Sensor *sensor) { this->temperature_sensor_ = sensor; } + void set_pressure_sensor(sensor::Sensor *sensor) { this->pressure_sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } + void set_gas_resistance_sensor(sensor::Sensor *sensor) { this->gas_resistance_sensor_ = sensor; } + void set_iaq_sensor(sensor::Sensor *sensor) { this->iaq_sensor_ = sensor; } + void set_iaq_static_sensor(sensor::Sensor *sensor) { this->iaq_static_sensor_ = sensor; } + void set_iaq_accuracy_sensor(sensor::Sensor *sensor) { this->iaq_accuracy_sensor_ = sensor; } + void set_co2_equivalent_sensor(sensor::Sensor *sensor) { this->co2_equivalent_sensor_ = sensor; } + void set_breath_voc_equivalent_sensor(sensor::Sensor *sensor) { this->breath_voc_equivalent_sensor_ = sensor; } +#endif +#ifdef USE_TEXT_SENSOR + void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *sensor) { this->iaq_accuracy_text_sensor_ = sensor; } +#endif + virtual uint32_t get_hash() = 0; + + protected: + void set_config_(const uint8_t *config, u_int32_t len); + float calc_sensor_sample_rate_(SampleRate sample_rate); + void update_subscription_(); + + void run_(); + void read_(int64_t trigger_time_ns); + void publish_(const bsec_output_t *outputs, uint8_t num_outputs); + int64_t get_time_ns_(); + +#ifdef USE_SENSOR + void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); +#endif +#ifdef USE_TEXT_SENSOR + void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); +#endif + + void load_state_(); + void save_state_(uint8_t accuracy); + + void queue_push_(std::function &&f) { this->queue_.push(std::move(f)); } + + struct bme68x_dev bme68x_; + bsec_bme_settings_t bsec_settings_; + bsec_version_t version_; + uint8_t bsec_instance_[BSEC_INSTANCE_SIZE]; + + struct bme68x_heatr_conf bme68x_heatr_conf_; + uint8_t op_mode_; // operating mode of sensor + bool sleep_mode_; + bsec_library_return_t bsec_status_{BSEC_OK}; + int8_t bme68x_status_{BME68X_OK}; + + int64_t last_time_ms_{0}; + uint32_t millis_overflow_counter_{0}; + int64_t next_call_ns_{0}; + + std::queue> queue_; + + uint8_t const *bsec2_configuration_{nullptr}; + uint32_t bsec2_configuration_length_{0}; + bool bsec2_blob_configured_{false}; + + ESPPreferenceObject bsec_state_; + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_ = 0; + + float temperature_offset_{0}; + + AlgorithmOutput algorithm_output_{ALGORITHM_OUTPUT_IAQ}; + OperatingAge operating_age_{OPERATING_AGE_28D}; + Voltage voltage_{VOLTAGE_3_3V}; + + SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate + SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; + +#ifdef USE_SENSOR + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *gas_resistance_sensor_{nullptr}; + sensor::Sensor *iaq_sensor_{nullptr}; + sensor::Sensor *iaq_static_sensor_{nullptr}; + sensor::Sensor *iaq_accuracy_sensor_{nullptr}; + sensor::Sensor *co2_equivalent_sensor_{nullptr}; + sensor::Sensor *breath_voc_equivalent_sensor_{nullptr}; +#endif +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *iaq_accuracy_text_sensor_{nullptr}; +#endif +}; + +} // namespace bme68x_bsec2 +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2/sensor.py b/esphome/components/bme68x_bsec2/sensor.py new file mode 100644 index 0000000000..419f47b248 --- /dev/null +++ b/esphome/components/bme68x_bsec2/sensor.py @@ -0,0 +1,130 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_GAS_RESISTANCE, + CONF_HUMIDITY, + CONF_IAQ_ACCURACY, + CONF_PRESSURE, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + ICON_GAS_CYLINDER, + ICON_GAUGE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_OHM, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, +) + +from . import CONF_BME68X_BSEC2_ID, SAMPLE_RATE_OPTIONS, BME68xBSEC2Component + +DEPENDENCIES = ["bme68x_bsec2"] + +CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" +CONF_CO2_EQUIVALENT = "co2_equivalent" +CONF_IAQ = "iaq" +CONF_IAQ_STATIC = "iaq_static" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" +ICON_TEST_TUBE = "mdi:test-tube" +UNIT_IAQ = "IAQ" + +TYPES = [ + CONF_TEMPERATURE, + CONF_PRESSURE, + CONF_HUMIDITY, + CONF_GAS_RESISTANCE, + CONF_IAQ, + CONF_IAQ_STATIC, + CONF_IAQ_ACCURACY, + CONF_CO2_EQUIVALENT, + CONF_BREATH_VOC_EQUIVALENT, +] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ): sensor.sensor_schema( + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ_STATIC): sensor.sensor_schema( + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( + icon=ICON_ACCURACY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +) + + +async def setup_conf(config, key, hub): + if conf := config.get(key): + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) + if sample_rate := conf.get(CONF_SAMPLE_RATE): + cg.add(getattr(hub, f"set_{key}_sample_rate")(sample_rate)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2/text_sensor.py b/esphome/components/bme68x_bsec2/text_sensor.py new file mode 100644 index 0000000000..fce00afe34 --- /dev/null +++ b/esphome/components/bme68x_bsec2/text_sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_IAQ_ACCURACY + +from . import CONF_BME68X_BSEC2_ID, BME68xBSEC2Component + +DEPENDENCIES = ["bme68x_bsec2"] + +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" + +TYPES = [CONF_IAQ_ACCURACY] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), + cv.Optional(CONF_IAQ_ACCURACY): text_sensor.text_sensor_schema( + icon=ICON_ACCURACY + ), + } +) + + +async def setup_conf(config, key, hub): + if conf := config.get(key): + sens = await text_sensor.new_text_sensor(conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2_i2c/__init__.py b/esphome/components/bme68x_bsec2_i2c/__init__.py new file mode 100644 index 0000000000..d6fb7fa9be --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.bme68x_bsec2 import ( + CONFIG_SCHEMA_BASE, + BME68xBSEC2Component, + to_code_base, +) +import esphome.config_validation as cv + +CODEOWNERS = ["@neffs", "@kbx81"] + +AUTO_LOAD = ["bme68x_bsec2"] +DEPENDENCIES = ["i2c"] + +bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c") +BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_( + "BME68xBSEC2I2CComponent", BME68xBSEC2Component, i2c.I2CDevice +) + + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + cv.Schema({cv.GenerateID(): cv.declare_id(BME68xBSEC2I2CComponent)}) +).extend(i2c.i2c_device_schema(0x76)) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp new file mode 100644 index 0000000000..874c8bf388 --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp @@ -0,0 +1,53 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_BSEC2 +#include "bme68x_bsec2_i2c.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace bme68x_bsec2_i2c { + +static const char *const TAG = "bme68x_bsec2_i2c.sensor"; + +void BME68xBSEC2I2CComponent::setup() { + // must set up our bme68x_dev instance before calling setup() + this->bme68x_.intf_ptr = (void *) this; + this->bme68x_.intf = BME68X_I2C_INTF; + this->bme68x_.read = BME68xBSEC2I2CComponent::read_bytes_wrapper; + this->bme68x_.write = BME68xBSEC2I2CComponent::write_bytes_wrapper; + this->bme68x_.delay_us = BME68xBSEC2I2CComponent::delay_us; + this->bme68x_.amb_temp = 25; + + BME68xBSEC2Component::setup(); +} + +void BME68xBSEC2I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BME68xBSEC2Component::dump_config(); +} + +uint32_t BME68xBSEC2I2CComponent::get_hash() { return fnv1_hash("bme68x_bsec_state_" + to_string(this->address_)); } + +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); + return static_cast(intfPtr)->read_bytes(a_register, data, len) ? 0 : -1; +} + +int8_t BME68xBSEC2I2CComponent::write_bytes_wrapper(uint8_t a_register, const uint8_t *data, uint32_t len, + void *intfPtr) { + ESP_LOGVV(TAG, "write_bytes_wrapper: reg = %u", a_register); + return static_cast(intfPtr)->write_bytes(a_register, data, len) ? 0 : -1; +} + +void BME68xBSEC2I2CComponent::delay_us(uint32_t period, void *intfPtr) { + ESP_LOGVV(TAG, "Delaying for %" PRIu32 "us", period); + delayMicroseconds(period); +} + +} // namespace bme68x_bsec2_i2c +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h new file mode 100644 index 0000000000..a21a123f7b --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" + +#ifdef USE_BSEC2 + +#include "esphome/components/bme68x_bsec2/bme68x_bsec2.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bme68x_bsec2_i2c { + +class BME68xBSEC2I2CComponent : public bme68x_bsec2::BME68xBSEC2Component, public i2c::I2CDevice { + void setup() override; + void dump_config() override; + + uint32_t get_hash() override; + + static int8_t read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr); + static int8_t write_bytes_wrapper(uint8_t a_register, const uint8_t *data, uint32_t len, void *intfPtr); + static void delay_us(uint32_t period, void *intfPtr); +}; + +} // namespace bme68x_bsec2_i2c +} // namespace esphome +#endif diff --git a/esphome/components/bmi160/__init__.py b/esphome/components/bmi160/__init__.py new file mode 100644 index 0000000000..49b6d0252a --- /dev/null +++ b/esphome/components/bmi160/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@flaviut"] diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp new file mode 100644 index 0000000000..69b4694345 --- /dev/null +++ b/esphome/components/bmi160/bmi160.cpp @@ -0,0 +1,270 @@ +#include "bmi160.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bmi160 { + +static const char *const TAG = "bmi160"; + +const uint8_t BMI160_REGISTER_CHIPID = 0x00; + +const uint8_t BMI160_REGISTER_CMD = 0x7E; +enum class Cmd : uint8_t { + START_FOC = 0x03, + ACCL_SET_PMU_MODE = 0b00010000, // last 2 bits are mode + GYRO_SET_PMU_MODE = 0b00010100, // last 2 bits are mode + MAG_SET_PMU_MODE = 0b00011000, // last 2 bits are mode + PROG_NVM = 0xA0, + FIFO_FLUSH = 0xB0, + INT_RESET = 0xB1, + SOFT_RESET = 0xB6, + STEP_CNT_CLR = 0xB2, +}; +enum class GyroPmuMode : uint8_t { + SUSPEND = 0b00, + NORMAL = 0b01, + LOW_POWER = 0b10, +}; +enum class AcclPmuMode : uint8_t { + SUSPEND = 0b00, + NORMAL = 0b01, + FAST_STARTUP = 0b11, +}; +enum class MagPmuMode : uint8_t { + SUSPEND = 0b00, + NORMAL = 0b01, + LOW_POWER = 0b10, +}; + +const uint8_t BMI160_REGISTER_ACCEL_CONFIG = 0x40; +enum class AcclFilterMode : uint8_t { + POWER_SAVING = 0b00000000, + PERF = 0b10000000, +}; +enum class AcclBandwidth : uint8_t { + OSR4_AVG1 = 0b00000000, + OSR2_AVG2 = 0b00010000, + NORMAL_AVG4 = 0b00100000, + RES_AVG8 = 0b00110000, + RES_AVG16 = 0b01000000, + RES_AVG32 = 0b01010000, + RES_AVG64 = 0b01100000, + RES_AVG128 = 0b01110000, +}; +enum class AccelOutputDataRate : uint8_t { + HZ_25_32 = 0b0001, // 25/32 Hz + HZ_25_16 = 0b0010, // 25/16 Hz + HZ_25_8 = 0b0011, // 25/8 Hz + HZ_25_4 = 0b0100, // 25/4 Hz + HZ_25_2 = 0b0101, // 25/2 Hz + HZ_25 = 0b0110, // 25 Hz + HZ_50 = 0b0111, // 50 Hz + HZ_100 = 0b1000, // 100 Hz + HZ_200 = 0b1001, // 200 Hz + HZ_400 = 0b1010, // 400 Hz + HZ_800 = 0b1011, // 800 Hz + HZ_1600 = 0b1100, // 1600 Hz +}; +const uint8_t BMI160_REGISTER_ACCEL_RANGE = 0x41; +enum class AccelRange : uint8_t { + RANGE_2G = 0b0011, + RANGE_4G = 0b0101, + RANGE_8G = 0b1000, + RANGE_16G = 0b1100, +}; + +const uint8_t BMI160_REGISTER_GYRO_CONFIG = 0x42; +enum class GyroBandwidth : uint8_t { + OSR4 = 0x00, + OSR2 = 0x10, + NORMAL = 0x20, +}; +enum class GyroOuputDataRate : uint8_t { + HZ_25 = 0x06, + HZ_50 = 0x07, + HZ_100 = 0x08, + HZ_200 = 0x09, + HZ_400 = 0x0A, + HZ_800 = 0x0B, + HZ_1600 = 0x0C, + HZ_3200 = 0x0D, +}; +const uint8_t BMI160_REGISTER_GYRO_RANGE = 0x43; +enum class GyroRange : uint8_t { + RANGE_2000_DPS = 0x0, // ±2000 °/s + RANGE_1000_DPS = 0x1, + RANGE_500_DPS = 0x2, + RANGE_250_DPS = 0x3, + RANGE_125_DPS = 0x4, +}; + +const uint8_t BMI160_REGISTER_DATA_GYRO_X_LSB = 0x0C; +const uint8_t BMI160_REGISTER_DATA_GYRO_X_MSB = 0x0D; +const uint8_t BMI160_REGISTER_DATA_GYRO_Y_LSB = 0x0E; +const uint8_t BMI160_REGISTER_DATA_GYRO_Y_MSB = 0x0F; +const uint8_t BMI160_REGISTER_DATA_GYRO_Z_LSB = 0x10; +const uint8_t BMI160_REGISTER_DATA_GYRO_Z_MSB = 0x11; +const uint8_t BMI160_REGISTER_DATA_ACCEL_X_LSB = 0x12; +const uint8_t BMI160_REGISTER_DATA_ACCEL_X_MSB = 0x13; +const uint8_t BMI160_REGISTER_DATA_ACCEL_Y_LSB = 0x14; +const uint8_t BMI160_REGISTER_DATA_ACCEL_Y_MSB = 0x15; +const uint8_t BMI160_REGISTER_DATA_ACCEL_Z_LSB = 0x16; +const uint8_t BMI160_REGISTER_DATA_ACCEL_Z_MSB = 0x17; +const uint8_t BMI160_REGISTER_DATA_TEMP_LSB = 0x20; +const uint8_t BMI160_REGISTER_DATA_TEMP_MSB = 0x21; + +const float GRAVITY_EARTH = 9.80665f; + +void BMI160Component::internal_setup_(int stage) { + switch (stage) { + case 0: + ESP_LOGCONFIG(TAG, "Setting up BMI160..."); + uint8_t chipid; + if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) { + this->mark_failed(); + return; + } + + ESP_LOGV(TAG, " Bringing accelerometer out of sleep..."); + if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::ACCL_SET_PMU_MODE | (uint8_t) AcclPmuMode::NORMAL)) { + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Waiting for accelerometer to wake up..."); + // need to wait (max delay in datasheet) because we can't send commands while another is in progress + // min 5ms, 10ms + this->set_timeout(10, [this]() { this->internal_setup_(1); }); + break; + + case 1: + ESP_LOGV(TAG, " Bringing gyroscope out of sleep..."); + if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::GYRO_SET_PMU_MODE | (uint8_t) GyroPmuMode::NORMAL)) { + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Waiting for gyroscope to wake up..."); + // wait between 51 & 81ms, doing 100 to be safe + this->set_timeout(10, [this]() { this->internal_setup_(2); }); + break; + + case 2: + ESP_LOGV(TAG, " Setting up Gyro Config..."); + uint8_t gyro_config = (uint8_t) GyroBandwidth::OSR4 | (uint8_t) GyroOuputDataRate::HZ_25; + ESP_LOGV(TAG, " Output gyro_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config)); + if (!this->write_byte(BMI160_REGISTER_GYRO_CONFIG, gyro_config)) { + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Setting up Gyro Range..."); + uint8_t gyro_range = (uint8_t) GyroRange::RANGE_2000_DPS; + ESP_LOGV(TAG, " Output gyro_range: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_range)); + if (!this->write_byte(BMI160_REGISTER_GYRO_RANGE, gyro_range)) { + this->mark_failed(); + return; + } + + ESP_LOGV(TAG, " Setting up Accel Config..."); + uint8_t accel_config = + (uint8_t) AcclFilterMode::PERF | (uint8_t) AcclBandwidth::RES_AVG16 | (uint8_t) AccelOutputDataRate::HZ_25; + ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config)); + if (!this->write_byte(BMI160_REGISTER_ACCEL_CONFIG, accel_config)) { + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Setting up Accel Range..."); + uint8_t accel_range = (uint8_t) AccelRange::RANGE_16G; + ESP_LOGV(TAG, " Output accel_range: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_range)); + if (!this->write_byte(BMI160_REGISTER_ACCEL_RANGE, accel_range)) { + this->mark_failed(); + return; + } + + this->setup_complete_ = true; + } +} + +void BMI160Component::setup() { this->internal_setup_(0); } +void BMI160Component::dump_config() { + ESP_LOGCONFIG(TAG, "BMI160:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with BMI160 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Acceleration X", this->accel_x_sensor_); + LOG_SENSOR(" ", "Acceleration Y", this->accel_y_sensor_); + LOG_SENSOR(" ", "Acceleration Z", this->accel_z_sensor_); + LOG_SENSOR(" ", "Gyro X", this->gyro_x_sensor_); + LOG_SENSOR(" ", "Gyro Y", this->gyro_y_sensor_); + LOG_SENSOR(" ", "Gyro Z", this->gyro_z_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); +} + +i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) { + uint8_t raw_data[len * 2]; + // read using read_register because we have little-endian data, and read_bytes_16 will swap it + i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2, true); + if (err != i2c::ERROR_OK) { + return err; + } + for (int i = 0; i < len; i++) { + value[i] = (int16_t) ((uint16_t) raw_data[i * 2] | ((uint16_t) raw_data[i * 2 + 1] << 8)); + } + return err; +} + +void BMI160Component::update() { + if (!this->setup_complete_) { + return; + } + + ESP_LOGV(TAG, " Updating BMI160..."); + int16_t data[6]; + if (this->read_le_int16_(BMI160_REGISTER_DATA_GYRO_X_LSB, data, 6) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + float gyro_x = (float) data[0] / (float) INT16_MAX * 2000.f; + float gyro_y = (float) data[1] / (float) INT16_MAX * 2000.f; + float gyro_z = (float) data[2] / (float) INT16_MAX * 2000.f; + float accel_x = (float) data[3] / (float) INT16_MAX * 16 * GRAVITY_EARTH; + float accel_y = (float) data[4] / (float) INT16_MAX * 16 * GRAVITY_EARTH; + float accel_z = (float) data[5] / (float) INT16_MAX * 16 * GRAVITY_EARTH; + + int16_t raw_temperature; + if (this->read_le_int16_(BMI160_REGISTER_DATA_TEMP_LSB, &raw_temperature, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + float temperature = (float) raw_temperature / (float) INT16_MAX * 64.5f + 23.f; + + ESP_LOGD(TAG, + "Got accel={x=%.3f m/s², y=%.3f m/s², z=%.3f m/s²}, " + "gyro={x=%.3f °/s, y=%.3f °/s, z=%.3f °/s}, temp=%.3f°C", + accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z, temperature); + + if (this->accel_x_sensor_ != nullptr) + this->accel_x_sensor_->publish_state(accel_x); + if (this->accel_y_sensor_ != nullptr) + this->accel_y_sensor_->publish_state(accel_y); + if (this->accel_z_sensor_ != nullptr) + this->accel_z_sensor_->publish_state(accel_z); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + + if (this->gyro_x_sensor_ != nullptr) + this->gyro_x_sensor_->publish_state(gyro_x); + if (this->gyro_y_sensor_ != nullptr) + this->gyro_y_sensor_->publish_state(gyro_y); + if (this->gyro_z_sensor_ != nullptr) + this->gyro_z_sensor_->publish_state(gyro_z); + + this->status_clear_warning(); +} +float BMI160Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace bmi160 +} // namespace esphome diff --git a/esphome/components/bmi160/bmi160.h b/esphome/components/bmi160/bmi160.h new file mode 100644 index 0000000000..47691a4de9 --- /dev/null +++ b/esphome/components/bmi160/bmi160.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bmi160 { + +class BMI160Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void update() override; + + float get_setup_priority() const override; + + void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } + void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } + void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_gyro_x_sensor(sensor::Sensor *gyro_x_sensor) { gyro_x_sensor_ = gyro_x_sensor; } + void set_gyro_y_sensor(sensor::Sensor *gyro_y_sensor) { gyro_y_sensor_ = gyro_y_sensor; } + void set_gyro_z_sensor(sensor::Sensor *gyro_z_sensor) { gyro_z_sensor_ = gyro_z_sensor; } + + protected: + sensor::Sensor *accel_x_sensor_{nullptr}; + sensor::Sensor *accel_y_sensor_{nullptr}; + sensor::Sensor *accel_z_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *gyro_x_sensor_{nullptr}; + sensor::Sensor *gyro_y_sensor_{nullptr}; + sensor::Sensor *gyro_z_sensor_{nullptr}; + + void internal_setup_(int stage); + bool setup_complete_{false}; + + /** reads `len` 16-bit little-endian integers from the given i2c register */ + i2c::ErrorCode read_le_int16_(uint8_t reg, int16_t *value, uint8_t len); +}; + +} // namespace bmi160 +} // namespace esphome diff --git a/esphome/components/bmi160/sensor.py b/esphome/components/bmi160/sensor.py new file mode 100644 index 0000000000..baf185f95a --- /dev/null +++ b/esphome/components/bmi160/sensor.py @@ -0,0 +1,102 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + CONF_ACCELERATION_X, + CONF_ACCELERATION_Y, + CONF_ACCELERATION_Z, + CONF_GYROSCOPE_X, + CONF_GYROSCOPE_Y, + CONF_GYROSCOPE_Z, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_METER_PER_SECOND_SQUARED, + ICON_ACCELERATION_X, + ICON_ACCELERATION_Y, + ICON_ACCELERATION_Z, + ICON_GYROSCOPE_X, + ICON_GYROSCOPE_Y, + ICON_GYROSCOPE_Z, + UNIT_DEGREE_PER_SECOND, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["i2c"] + +bmi160_ns = cg.esphome_ns.namespace("bmi160") +BMI160Component = bmi160_ns.class_( + "BMI160Component", cg.PollingComponent, i2c.I2CDevice +) + +accel_schema = { + "unit_of_measurement": UNIT_METER_PER_SECOND_SQUARED, + "accuracy_decimals": 2, + "state_class": STATE_CLASS_MEASUREMENT, +} +gyro_schema = { + "unit_of_measurement": UNIT_DEGREE_PER_SECOND, + "accuracy_decimals": 2, + "state_class": STATE_CLASS_MEASUREMENT, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BMI160Component), + cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema( + icon=ICON_ACCELERATION_X, + **accel_schema, + ), + cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema( + icon=ICON_ACCELERATION_Y, + **accel_schema, + ), + cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema( + icon=ICON_ACCELERATION_Z, + **accel_schema, + ), + cv.Optional(CONF_GYROSCOPE_X): sensor.sensor_schema( + icon=ICON_GYROSCOPE_X, + **gyro_schema, + ), + cv.Optional(CONF_GYROSCOPE_Y): sensor.sensor_schema( + icon=ICON_GYROSCOPE_Y, + **gyro_schema, + ), + cv.Optional(CONF_GYROSCOPE_Z): sensor.sensor_schema( + icon=ICON_GYROSCOPE_Z, + **gyro_schema, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x68)) +) + + +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) + + for d in ["x", "y", "z"]: + accel_key = f"acceleration_{d}" + if accel_key in config: + sens = await sensor.new_sensor(config[accel_key]) + cg.add(getattr(var, f"set_accel_{d}_sensor")(sens)) + accel_key = f"gyroscope_{d}" + if accel_key in config: + sens = await sensor.new_sensor(config[accel_key]) + cg.add(getattr(var, f"set_gyro_{d}_sensor")(sens)) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/bmp085/sensor.py b/esphome/components/bmp085/sensor.py index 52f554120a..83f5a0c821 100644 --- a/esphome/components/bmp085/sensor.py +++ b/esphome/components/bmp085/sensor.py @@ -47,12 +47,10 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_TEMPERATURE in config: - conf = config[CONF_TEMPERATURE] - sens = await sensor.new_sensor(conf) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) cg.add(var.set_temperature(sens)) - if CONF_PRESSURE in config: - conf = config[CONF_PRESSURE] - sens = await sensor.new_sensor(conf) + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) cg.add(var.set_pressure(sens)) diff --git a/esphome/components/bmp280/bmp280.cpp b/esphome/components/bmp280/bmp280.cpp deleted file mode 100644 index a5b2517893..0000000000 --- a/esphome/components/bmp280/bmp280.cpp +++ /dev/null @@ -1,266 +0,0 @@ -#include "bmp280.h" -#include "esphome/core/hal.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace bmp280 { - -static const char *const TAG = "bmp280.sensor"; - -static const uint8_t BMP280_REGISTER_STATUS = 0xF3; -static const uint8_t BMP280_REGISTER_CONTROL = 0xF4; -static const uint8_t BMP280_REGISTER_CONFIG = 0xF5; -static const uint8_t BMP280_REGISTER_PRESSUREDATA = 0xF7; -static const uint8_t BMP280_REGISTER_TEMPDATA = 0xFA; -static const uint8_t BMP280_REGISTER_RESET = 0xE0; - -static const uint8_t BMP280_MODE_FORCED = 0b01; -static const uint8_t BMP280_SOFT_RESET = 0xB6; -static const uint8_t BMP280_STATUS_IM_UPDATE = 0b01; - -inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } - -static const char *oversampling_to_str(BMP280Oversampling oversampling) { - switch (oversampling) { - case BMP280_OVERSAMPLING_NONE: - return "None"; - case BMP280_OVERSAMPLING_1X: - return "1x"; - case BMP280_OVERSAMPLING_2X: - return "2x"; - case BMP280_OVERSAMPLING_4X: - return "4x"; - case BMP280_OVERSAMPLING_8X: - return "8x"; - case BMP280_OVERSAMPLING_16X: - return "16x"; - default: - return "UNKNOWN"; - } -} - -static const char *iir_filter_to_str(BMP280IIRFilter filter) { - switch (filter) { - case BMP280_IIR_FILTER_OFF: - return "OFF"; - case BMP280_IIR_FILTER_2X: - return "2x"; - case BMP280_IIR_FILTER_4X: - return "4x"; - case BMP280_IIR_FILTER_8X: - return "8x"; - case BMP280_IIR_FILTER_16X: - return "16x"; - default: - return "UNKNOWN"; - } -} - -void BMP280Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up BMP280..."); - uint8_t chip_id = 0; - if (!this->read_byte(0xD0, &chip_id)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } - if (chip_id != 0x58) { - this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(); - return; - } - - // Send a soft reset. - if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { - this->mark_failed(); - return; - } - // Wait until the NVM data has finished loading. - uint8_t status; - uint8_t retry = 5; - do { - delay(2); - if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { - ESP_LOGW(TAG, "Error reading status register."); - this->mark_failed(); - return; - } - } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); - if (status & BMP280_STATUS_IM_UPDATE) { - ESP_LOGW(TAG, "Timeout loading NVM."); - this->mark_failed(); - return; - } - - // Read calibration - this->calibration_.t1 = this->read_u16_le_(0x88); - this->calibration_.t2 = this->read_s16_le_(0x8A); - this->calibration_.t3 = this->read_s16_le_(0x8C); - - this->calibration_.p1 = this->read_u16_le_(0x8E); - this->calibration_.p2 = this->read_s16_le_(0x90); - this->calibration_.p3 = this->read_s16_le_(0x92); - this->calibration_.p4 = this->read_s16_le_(0x94); - this->calibration_.p5 = this->read_s16_le_(0x96); - this->calibration_.p6 = this->read_s16_le_(0x98); - this->calibration_.p7 = this->read_s16_le_(0x9A); - this->calibration_.p8 = this->read_s16_le_(0x9C); - this->calibration_.p9 = this->read_s16_le_(0x9E); - - uint8_t config_register = 0; - if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) { - this->mark_failed(); - return; - } - config_register &= ~0b11111100; - config_register |= 0b000 << 5; // 0.5 ms standby time - config_register |= (this->iir_filter_ & 0b111) << 2; - if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) { - this->mark_failed(); - return; - } -} -void BMP280Component::dump_config() { - ESP_LOGCONFIG(TAG, "BMP280:"); - LOG_I2C_DEVICE(this); - switch (this->error_code_) { - case COMMUNICATION_FAILED: - ESP_LOGE(TAG, "Communication with BMP280 failed!"); - break; - case WRONG_CHIP_ID: - ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BME280?"); - break; - case NONE: - default: - break; - } - ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_)); - LOG_UPDATE_INTERVAL(this); - - LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); - ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->temperature_oversampling_)); - LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); - ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); -} -float BMP280Component::get_setup_priority() const { return setup_priority::DATA; } - -inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } - -void BMP280Component::update() { - // Enable sensor - ESP_LOGV(TAG, "Sending conversion request..."); - uint8_t meas_value = 0; - meas_value |= (this->temperature_oversampling_ & 0b111) << 5; - meas_value |= (this->pressure_oversampling_ & 0b111) << 2; - meas_value |= 0b01; // Forced mode - if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_value)) { - this->status_set_warning(); - return; - } - - float meas_time = 1; - meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_); - meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; - - this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { - int32_t t_fine = 0; - float temperature = this->read_temperature_(&t_fine); - if (std::isnan(temperature)) { - ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values."); - this->status_set_warning(); - return; - } - float pressure = this->read_pressure_(t_fine); - - ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->pressure_sensor_ != nullptr) - this->pressure_sensor_->publish_state(pressure); - this->status_clear_warning(); - }); -} - -float BMP280Component::read_temperature_(int32_t *t_fine) { - uint8_t data[3]; - if (!this->read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) - return NAN; - int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); - adc >>= 4; - if (adc == 0x80000) { - // temperature was disabled - return NAN; - } - - const int32_t t1 = this->calibration_.t1; - const int32_t t2 = this->calibration_.t2; - const int32_t t3 = this->calibration_.t3; - - int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; - int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; - *t_fine = var1 + var2; - - float temperature = (*t_fine * 5 + 128) >> 8; - return temperature / 100.0f; -} - -float BMP280Component::read_pressure_(int32_t t_fine) { - uint8_t data[3]; - if (!this->read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) - return NAN; - int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); - adc >>= 4; - if (adc == 0x80000) { - // pressure was disabled - return NAN; - } - const int64_t p1 = this->calibration_.p1; - const int64_t p2 = this->calibration_.p2; - const int64_t p3 = this->calibration_.p3; - const int64_t p4 = this->calibration_.p4; - const int64_t p5 = this->calibration_.p5; - const int64_t p6 = this->calibration_.p6; - const int64_t p7 = this->calibration_.p7; - const int64_t p8 = this->calibration_.p8; - const int64_t p9 = this->calibration_.p9; - - int64_t var1, var2, p; - var1 = int64_t(t_fine) - 128000; - var2 = var1 * var1 * p6; - var2 = var2 + ((var1 * p5) << 17); - var2 = var2 + (p4 << 35); - var1 = ((var1 * var1 * p3) >> 8) + ((var1 * p2) << 12); - var1 = ((int64_t(1) << 47) + var1) * p1 >> 33; - - if (var1 == 0) - return NAN; - - p = 1048576 - adc; - p = (((p << 31) - var2) * 3125) / var1; - var1 = (p9 * (p >> 13) * (p >> 13)) >> 25; - var2 = (p8 * p) >> 19; - - p = ((p + var1 + var2) >> 8) + (p7 << 4); - return (p / 256.0f) / 100.0f; -} -void BMP280Component::set_temperature_oversampling(BMP280Oversampling temperature_over_sampling) { - this->temperature_oversampling_ = temperature_over_sampling; -} -void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over_sampling) { - this->pressure_oversampling_ = pressure_over_sampling; -} -void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } -uint8_t BMP280Component::read_u8_(uint8_t a_register) { - uint8_t data = 0; - this->read_byte(a_register, &data); - return data; -} -uint16_t BMP280Component::read_u16_le_(uint8_t a_register) { - uint16_t data = 0; - this->read_byte_16(a_register, &data); - return (data >> 8) | (data << 8); -} -int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } - -} // namespace bmp280 -} // namespace esphome diff --git a/esphome/components/bmp280/bmp280.h b/esphome/components/bmp280/bmp280.h deleted file mode 100644 index 96eb470155..0000000000 --- a/esphome/components/bmp280/bmp280.h +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" - -namespace esphome { -namespace bmp280 { - -/// Internal struct storing the calibration values of an BMP280. -struct BMP280CalibrationData { - uint16_t t1; // 0x88 - 0x89 - int16_t t2; // 0x8A - 0x8B - int16_t t3; // 0x8C - 0x8D - - uint16_t p1; // 0x8E - 0x8F - int16_t p2; // 0x90 - 0x91 - int16_t p3; // 0x92 - 0x93 - int16_t p4; // 0x94 - 0x95 - int16_t p5; // 0x96 - 0x97 - int16_t p6; // 0x98 - 0x99 - int16_t p7; // 0x9A - 0x9B - int16_t p8; // 0x9C - 0x9D - int16_t p9; // 0x9E - 0x9F -}; - -/** Enum listing all Oversampling values for the BMP280. - * - * Oversampling basically means measuring a condition multiple times. Higher oversampling - * values therefore increase the time required to read sensor values but increase accuracy. - */ -enum BMP280Oversampling { - BMP280_OVERSAMPLING_NONE = 0b000, - BMP280_OVERSAMPLING_1X = 0b001, - BMP280_OVERSAMPLING_2X = 0b010, - BMP280_OVERSAMPLING_4X = 0b011, - BMP280_OVERSAMPLING_8X = 0b100, - BMP280_OVERSAMPLING_16X = 0b101, -}; - -/** Enum listing all Infinite Impulse Filter values for the BMP280. - * - * Higher values increase accuracy, but decrease response time. - */ -enum BMP280IIRFilter { - BMP280_IIR_FILTER_OFF = 0b000, - BMP280_IIR_FILTER_2X = 0b001, - BMP280_IIR_FILTER_4X = 0b010, - BMP280_IIR_FILTER_8X = 0b011, - BMP280_IIR_FILTER_16X = 0b100, -}; - -/// This class implements support for the BMP280 Temperature+Pressure i2c sensor. -class BMP280Component : public PollingComponent, public i2c::I2CDevice { - public: - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } - void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } - - /// Set the oversampling value for the temperature sensor. Default is 16x. - void set_temperature_oversampling(BMP280Oversampling temperature_over_sampling); - /// Set the oversampling value for the pressure sensor. Default is 16x. - void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling); - /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. - void set_iir_filter(BMP280IIRFilter iir_filter); - - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - void update() override; - - protected: - /// Read the temperature value and store the calculated ambient temperature in t_fine. - float read_temperature_(int32_t *t_fine); - /// Read the pressure value in hPa using the provided t_fine value. - float read_pressure_(int32_t t_fine); - uint8_t read_u8_(uint8_t a_register); - uint16_t read_u16_le_(uint8_t a_register); - int16_t read_s16_le_(uint8_t a_register); - - BMP280CalibrationData calibration_; - BMP280Oversampling temperature_oversampling_{BMP280_OVERSAMPLING_16X}; - BMP280Oversampling pressure_oversampling_{BMP280_OVERSAMPLING_16X}; - BMP280IIRFilter iir_filter_{BMP280_IIR_FILTER_OFF}; - sensor::Sensor *temperature_sensor_{nullptr}; - sensor::Sensor *pressure_sensor_{nullptr}; - enum ErrorCode { - NONE = 0, - COMMUNICATION_FAILED, - WRONG_CHIP_ID, - } error_code_{NONE}; -}; - -} // namespace bmp280 -} // namespace esphome diff --git a/esphome/components/bmp280/sensor.py b/esphome/components/bmp280/sensor.py index 95a9577f7e..a624889982 100644 --- a/esphome/components/bmp280/sensor.py +++ b/esphome/components/bmp280/sensor.py @@ -1,96 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor -from esphome.const import ( - CONF_ID, - CONF_PRESSURE, - CONF_TEMPERATURE, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, - CONF_IIR_FILTER, - CONF_OVERSAMPLING, + +CONFIG_SCHEMA = cv.invalid( + "The bmp280 sensor component has been renamed to bmp280_i2c." ) - -DEPENDENCIES = ["i2c"] - -bmp280_ns = cg.esphome_ns.namespace("bmp280") -BMP280Oversampling = bmp280_ns.enum("BMP280Oversampling") -OVERSAMPLING_OPTIONS = { - "NONE": BMP280Oversampling.BMP280_OVERSAMPLING_NONE, - "1X": BMP280Oversampling.BMP280_OVERSAMPLING_1X, - "2X": BMP280Oversampling.BMP280_OVERSAMPLING_2X, - "4X": BMP280Oversampling.BMP280_OVERSAMPLING_4X, - "8X": BMP280Oversampling.BMP280_OVERSAMPLING_8X, - "16X": BMP280Oversampling.BMP280_OVERSAMPLING_16X, -} - -BMP280IIRFilter = bmp280_ns.enum("BMP280IIRFilter") -IIR_FILTER_OPTIONS = { - "OFF": BMP280IIRFilter.BMP280_IIR_FILTER_OFF, - "2X": BMP280IIRFilter.BMP280_IIR_FILTER_2X, - "4X": BMP280IIRFilter.BMP280_IIR_FILTER_4X, - "8X": BMP280IIRFilter.BMP280_IIR_FILTER_8X, - "16X": BMP280IIRFilter.BMP280_IIR_FILTER_16X, -} - -BMP280Component = bmp280_ns.class_( - "BMP280Component", cg.PollingComponent, i2c.I2CDevice -) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(BMP280Component), - 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, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - unit_of_measurement=UNIT_HECTOPASCAL, - accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( - IIR_FILTER_OPTIONS, upper=True - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x77)) -) - - -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) - - if CONF_TEMPERATURE in config: - conf = config[CONF_TEMPERATURE] - sens = await sensor.new_sensor(conf) - cg.add(var.set_temperature_sensor(sens)) - cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING])) - - if CONF_PRESSURE in config: - conf = config[CONF_PRESSURE] - sens = await sensor.new_sensor(conf) - cg.add(var.set_pressure_sensor(sens)) - cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) diff --git a/esphome/components/bmp280_base/__init__.py b/esphome/components/bmp280_base/__init__.py new file mode 100644 index 0000000000..c0f9af9dd7 --- /dev/null +++ b/esphome/components/bmp280_base/__init__.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, +) + +CODEOWNERS = ["@ademuri"] + +bmp280_ns = cg.esphome_ns.namespace("bmp280_base") +BMP280Oversampling = bmp280_ns.enum("BMP280Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": BMP280Oversampling.BMP280_OVERSAMPLING_NONE, + "1X": BMP280Oversampling.BMP280_OVERSAMPLING_1X, + "2X": BMP280Oversampling.BMP280_OVERSAMPLING_2X, + "4X": BMP280Oversampling.BMP280_OVERSAMPLING_4X, + "8X": BMP280Oversampling.BMP280_OVERSAMPLING_8X, + "16X": BMP280Oversampling.BMP280_OVERSAMPLING_16X, +} + +BMP280IIRFilter = bmp280_ns.enum("BMP280IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": BMP280IIRFilter.BMP280_IIR_FILTER_OFF, + "2X": BMP280IIRFilter.BMP280_IIR_FILTER_2X, + "4X": BMP280IIRFilter.BMP280_IIR_FILTER_4X, + "8X": BMP280IIRFilter.BMP280_IIR_FILTER_8X, + "16X": BMP280IIRFilter.BMP280_IIR_FILTER_16X, +} + +CONFIG_SCHEMA_BASE = cv.Schema( + { + 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, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING])) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING])) + + cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) + + return var diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp new file mode 100644 index 0000000000..f94456f6e6 --- /dev/null +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -0,0 +1,273 @@ +#include "bmp280_base.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bmp280_base { + +static const char *const TAG = "bmp280.sensor"; + +static const uint8_t BMP280_REGISTER_STATUS = 0xF3; +static const uint8_t BMP280_REGISTER_CONTROL = 0xF4; +static const uint8_t BMP280_REGISTER_CONFIG = 0xF5; +static const uint8_t BMP280_REGISTER_PRESSUREDATA = 0xF7; +static const uint8_t BMP280_REGISTER_TEMPDATA = 0xFA; +static const uint8_t BMP280_REGISTER_RESET = 0xE0; + +static const uint8_t BMP280_MODE_FORCED = 0b01; +static const uint8_t BMP280_SOFT_RESET = 0xB6; +static const uint8_t BMP280_STATUS_IM_UPDATE = 0b01; + +inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } + +static const char *oversampling_to_str(BMP280Oversampling oversampling) { + switch (oversampling) { + case BMP280_OVERSAMPLING_NONE: + return "None"; + case BMP280_OVERSAMPLING_1X: + return "1x"; + case BMP280_OVERSAMPLING_2X: + return "2x"; + case BMP280_OVERSAMPLING_4X: + return "4x"; + case BMP280_OVERSAMPLING_8X: + return "8x"; + case BMP280_OVERSAMPLING_16X: + return "16x"; + default: + return "UNKNOWN"; + } +} + +static const char *iir_filter_to_str(BMP280IIRFilter filter) { + switch (filter) { + case BMP280_IIR_FILTER_OFF: + return "OFF"; + case BMP280_IIR_FILTER_2X: + return "2x"; + case BMP280_IIR_FILTER_4X: + return "4x"; + case BMP280_IIR_FILTER_8X: + return "8x"; + case BMP280_IIR_FILTER_16X: + return "16x"; + default: + return "UNKNOWN"; + } +} + +void BMP280Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up BMP280..."); + uint8_t chip_id = 0; + + // Read the chip id twice, to work around a bug where the first read is 0. + // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 + if (!this->read_byte(0xD0, &chip_id)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + if (!this->read_byte(0xD0, &chip_id)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + if (chip_id != 0x58) { + this->error_code_ = WRONG_CHIP_ID; + this->mark_failed(); + return; + } + + // Send a soft reset. + if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { + this->mark_failed(); + return; + } + // Wait until the NVM data has finished loading. + uint8_t status; + uint8_t retry = 5; + do { + delay(2); + if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { + ESP_LOGW(TAG, "Error reading status register."); + this->mark_failed(); + return; + } + } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); + if (status & BMP280_STATUS_IM_UPDATE) { + ESP_LOGW(TAG, "Timeout loading NVM."); + this->mark_failed(); + return; + } + + // Read calibration + this->calibration_.t1 = this->read_u16_le_(0x88); + this->calibration_.t2 = this->read_s16_le_(0x8A); + this->calibration_.t3 = this->read_s16_le_(0x8C); + + this->calibration_.p1 = this->read_u16_le_(0x8E); + this->calibration_.p2 = this->read_s16_le_(0x90); + this->calibration_.p3 = this->read_s16_le_(0x92); + this->calibration_.p4 = this->read_s16_le_(0x94); + this->calibration_.p5 = this->read_s16_le_(0x96); + this->calibration_.p6 = this->read_s16_le_(0x98); + this->calibration_.p7 = this->read_s16_le_(0x9A); + this->calibration_.p8 = this->read_s16_le_(0x9C); + this->calibration_.p9 = this->read_s16_le_(0x9E); + + uint8_t config_register = 0; + if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) { + this->mark_failed(); + return; + } + config_register &= ~0b11111100; + config_register |= 0b000 << 5; // 0.5 ms standby time + config_register |= (this->iir_filter_ & 0b111) << 2; + if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) { + this->mark_failed(); + return; + } +} +void BMP280Component::dump_config() { + ESP_LOGCONFIG(TAG, "BMP280:"); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with BMP280 failed!"); + break; + case WRONG_CHIP_ID: + ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BME280?"); + break; + case NONE: + default: + break; + } + ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_)); + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->temperature_oversampling_)); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); +} +float BMP280Component::get_setup_priority() const { return setup_priority::DATA; } + +inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } + +void BMP280Component::update() { + // Enable sensor + ESP_LOGV(TAG, "Sending conversion request..."); + uint8_t meas_value = 0; + meas_value |= (this->temperature_oversampling_ & 0b111) << 5; + meas_value |= (this->pressure_oversampling_ & 0b111) << 2; + meas_value |= 0b01; // Forced mode + if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_value)) { + this->status_set_warning(); + return; + } + + float meas_time = 1; + meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_); + meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; + + this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { + int32_t t_fine = 0; + float temperature = this->read_temperature_(&t_fine); + if (std::isnan(temperature)) { + ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values."); + this->status_set_warning(); + return; + } + float pressure = this->read_pressure_(t_fine); + + ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure); + this->status_clear_warning(); + }); +} + +float BMP280Component::read_temperature_(int32_t *t_fine) { + uint8_t data[3]; + if (!this->read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) + return NAN; + int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); + adc >>= 4; + if (adc == 0x80000) { + // temperature was disabled + return NAN; + } + + const int32_t t1 = this->calibration_.t1; + const int32_t t2 = this->calibration_.t2; + const int32_t t3 = this->calibration_.t3; + + int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; + int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; + *t_fine = var1 + var2; + + float temperature = (*t_fine * 5 + 128); + return temperature / 25600.0f; +} + +float BMP280Component::read_pressure_(int32_t t_fine) { + uint8_t data[3]; + if (!this->read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) + return NAN; + int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); + adc >>= 4; + if (adc == 0x80000) { + // pressure was disabled + return NAN; + } + const int64_t p1 = this->calibration_.p1; + const int64_t p2 = this->calibration_.p2; + const int64_t p3 = this->calibration_.p3; + const int64_t p4 = this->calibration_.p4; + const int64_t p5 = this->calibration_.p5; + const int64_t p6 = this->calibration_.p6; + const int64_t p7 = this->calibration_.p7; + const int64_t p8 = this->calibration_.p8; + const int64_t p9 = this->calibration_.p9; + + int64_t var1, var2, p; + var1 = int64_t(t_fine) - 128000; + var2 = var1 * var1 * p6; + var2 = var2 + ((var1 * p5) << 17); + var2 = var2 + (p4 << 35); + var1 = ((var1 * var1 * p3) >> 8) + ((var1 * p2) << 12); + var1 = ((int64_t(1) << 47) + var1) * p1 >> 33; + + if (var1 == 0) + return NAN; + + p = 1048576 - adc; + p = (((p << 31) - var2) * 3125) / var1; + var1 = (p9 * (p >> 13) * (p >> 13)) >> 25; + var2 = (p8 * p) >> 19; + + p = ((p + var1 + var2) >> 8) + (p7 << 4); + return (p / 256.0f) / 100.0f; +} +void BMP280Component::set_temperature_oversampling(BMP280Oversampling temperature_over_sampling) { + this->temperature_oversampling_ = temperature_over_sampling; +} +void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over_sampling) { + this->pressure_oversampling_ = pressure_over_sampling; +} +void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } +uint8_t BMP280Component::read_u8_(uint8_t a_register) { + uint8_t data = 0; + this->read_byte(a_register, &data); + return data; +} +uint16_t BMP280Component::read_u16_le_(uint8_t a_register) { + uint16_t data = 0; + this->read_byte_16(a_register, &data); + return (data >> 8) | (data << 8); +} +int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } + +} // namespace bmp280_base +} // namespace esphome diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h new file mode 100644 index 0000000000..4b22e98f13 --- /dev/null +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -0,0 +1,98 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bmp280_base { + +/// Internal struct storing the calibration values of an BMP280. +struct BMP280CalibrationData { + uint16_t t1; // 0x88 - 0x89 + int16_t t2; // 0x8A - 0x8B + int16_t t3; // 0x8C - 0x8D + + uint16_t p1; // 0x8E - 0x8F + int16_t p2; // 0x90 - 0x91 + int16_t p3; // 0x92 - 0x93 + int16_t p4; // 0x94 - 0x95 + int16_t p5; // 0x96 - 0x97 + int16_t p6; // 0x98 - 0x99 + int16_t p7; // 0x9A - 0x9B + int16_t p8; // 0x9C - 0x9D + int16_t p9; // 0x9E - 0x9F +}; + +/** Enum listing all Oversampling values for the BMP280. + * + * Oversampling basically means measuring a condition multiple times. Higher oversampling + * values therefore increase the time required to read sensor values but increase accuracy. + */ +enum BMP280Oversampling { + BMP280_OVERSAMPLING_NONE = 0b000, + BMP280_OVERSAMPLING_1X = 0b001, + BMP280_OVERSAMPLING_2X = 0b010, + BMP280_OVERSAMPLING_4X = 0b011, + BMP280_OVERSAMPLING_8X = 0b100, + BMP280_OVERSAMPLING_16X = 0b101, +}; + +/** Enum listing all Infinite Impulse Filter values for the BMP280. + * + * Higher values increase accuracy, but decrease response time. + */ +enum BMP280IIRFilter { + BMP280_IIR_FILTER_OFF = 0b000, + BMP280_IIR_FILTER_2X = 0b001, + BMP280_IIR_FILTER_4X = 0b010, + BMP280_IIR_FILTER_8X = 0b011, + BMP280_IIR_FILTER_16X = 0b100, +}; + +/// This class implements support for the BMP280 Temperature+Pressure sensor. +class BMP280Component : public PollingComponent { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + /// Set the oversampling value for the temperature sensor. Default is 16x. + void set_temperature_oversampling(BMP280Oversampling temperature_over_sampling); + /// Set the oversampling value for the pressure sensor. Default is 16x. + void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling); + /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. + void set_iir_filter(BMP280IIRFilter iir_filter); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0; + + protected: + /// Read the temperature value and store the calculated ambient temperature in t_fine. + float read_temperature_(int32_t *t_fine); + /// Read the pressure value in hPa using the provided t_fine value. + float read_pressure_(int32_t t_fine); + uint8_t read_u8_(uint8_t a_register); + uint16_t read_u16_le_(uint8_t a_register); + int16_t read_s16_le_(uint8_t a_register); + + BMP280CalibrationData calibration_; + BMP280Oversampling temperature_oversampling_{BMP280_OVERSAMPLING_16X}; + BMP280Oversampling pressure_oversampling_{BMP280_OVERSAMPLING_16X}; + BMP280IIRFilter iir_filter_{BMP280_IIR_FILTER_OFF}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + WRONG_CHIP_ID, + } error_code_{NONE}; +}; + +} // namespace bmp280_base +} // namespace esphome diff --git a/esphome/components/bmp280_i2c/__init__.py b/esphome/components/bmp280_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.cpp b/esphome/components/bmp280_i2c/bmp280_i2c.cpp new file mode 100644 index 0000000000..04b8bd8b10 --- /dev/null +++ b/esphome/components/bmp280_i2c/bmp280_i2c.cpp @@ -0,0 +1,27 @@ +#include "bmp280_i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bmp280_i2c { + +bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { + return I2CDevice::read_byte(a_register, data); +}; +bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) { + return I2CDevice::write_byte(a_register, data); +}; +bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::read_bytes(a_register, data, len); +}; +bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + return I2CDevice::read_byte_16(a_register, data); +}; + +void BMP280I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BMP280Component::dump_config(); +} + +} // namespace bmp280_i2c +} // namespace esphome diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.h b/esphome/components/bmp280_i2c/bmp280_i2c.h new file mode 100644 index 0000000000..66d78d788b --- /dev/null +++ b/esphome/components/bmp280_i2c/bmp280_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/bmp280_base/bmp280_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bmp280_i2c { + +static const char *const TAG = "bmp280_i2c.sensor"; + +/// This class implements support for the BMP280 Temperature+Pressure i2c sensor. +class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice { + public: + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool read_byte_16(uint8_t a_register, uint16_t *data) override; + void dump_config() override; +}; + +} // namespace bmp280_i2c +} // namespace esphome diff --git a/esphome/components/bmp280_i2c/sensor.py b/esphome/components/bmp280_i2c/sensor.py new file mode 100644 index 0000000000..991bb827a3 --- /dev/null +++ b/esphome/components/bmp280_i2c/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from ..bmp280_base import to_code_base, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["bmp280_base"] +CODEOWNERS = ["@ademuri"] +DEPENDENCIES = ["i2c"] + +bmp280_ns = cg.esphome_ns.namespace("bmp280_i2c") +BMP280I2CComponent = bmp280_ns.class_( + "BMP280I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(BMP280I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bmp280_spi/__init__.py b/esphome/components/bmp280_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp280_spi/bmp280_spi.cpp b/esphome/components/bmp280_spi/bmp280_spi.cpp new file mode 100644 index 0000000000..a35e829432 --- /dev/null +++ b/esphome/components/bmp280_spi/bmp280_spi.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include "bmp280_spi.h" +#include + +namespace esphome { +namespace bmp280_spi { + +uint8_t set_bit(uint8_t num, uint8_t position) { + uint8_t mask = 1 << position; + return num | mask; +} + +uint8_t clear_bit(uint8_t num, uint8_t position) { + uint8_t mask = 1 << position; + return num & ~mask; +} + +void BMP280SPIComponent::setup() { + this->spi_setup(); + BMP280Component::setup(); +}; + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used +// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte +// 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf + +bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(clear_bit(a_register, 7)); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + this->read_array(data, len); + this->disable(); + return true; +} + +bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + ((uint8_t *) data)[1] = this->transfer_byte(0); + ((uint8_t *) data)[0] = this->transfer_byte(0); + this->disable(); + return true; +} + +} // namespace bmp280_spi +} // namespace esphome diff --git a/esphome/components/bmp280_spi/bmp280_spi.h b/esphome/components/bmp280_spi/bmp280_spi.h new file mode 100644 index 0000000000..dd226502f6 --- /dev/null +++ b/esphome/components/bmp280_spi/bmp280_spi.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/bmp280_base/bmp280_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace bmp280_spi { + +class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component, + public spi::SPIDevice { + void setup() override; + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool read_byte_16(uint8_t a_register, uint16_t *data) override; +}; + +} // namespace bmp280_spi +} // namespace esphome diff --git a/esphome/components/bmp280_spi/sensor.py b/esphome/components/bmp280_spi/sensor.py new file mode 100644 index 0000000000..511d45b24e --- /dev/null +++ b/esphome/components/bmp280_spi/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from ..bmp280_base import to_code_base, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["bmp280_base"] +CODEOWNERS = ["@ademuri"] +DEPENDENCIES = ["spi"] + +bmp280_ns = cg.esphome_ns.namespace("bmp280_spi") +BMP280SPIComponent = bmp280_ns.class_( + "BMP280SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + spi.spi_device_schema(default_mode="mode3") +).extend({cv.GenerateID(): cv.declare_id(BMP280SPIComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/bmp3xx/bmp3xx.cpp b/esphome/components/bmp3xx/bmp3xx.cpp deleted file mode 100644 index 410b7a3173..0000000000 --- a/esphome/components/bmp3xx/bmp3xx.cpp +++ /dev/null @@ -1,388 +0,0 @@ -/* - based on BMP388_DEV by Martin Lindupp - under MIT License (MIT) - Copyright (C) Martin Lindupp 2020 - http://github.com/MartinL1/BMP388_DEV -*/ - -#include "bmp3xx.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace bmp3xx { - -static const char *const TAG = "bmp3xx.sensor"; - -static const LogString *chip_type_to_str(uint8_t chip_type) { - switch (chip_type) { - case BMP388_ID: - return LOG_STR("BMP 388"); - case BMP390_ID: - return LOG_STR("BMP 390"); - default: - return LOG_STR("Unknown Chip Type"); - } -} - -static const LogString *oversampling_to_str(Oversampling oversampling) { - switch (oversampling) { - case Oversampling::OVERSAMPLING_NONE: - return LOG_STR("None"); - case Oversampling::OVERSAMPLING_X2: - return LOG_STR("2x"); - case Oversampling::OVERSAMPLING_X4: - return LOG_STR("4x"); - case Oversampling::OVERSAMPLING_X8: - return LOG_STR("8x"); - case Oversampling::OVERSAMPLING_X16: - return LOG_STR("16x"); - case Oversampling::OVERSAMPLING_X32: - return LOG_STR("32x"); - default: - return LOG_STR(""); - } -} - -static const LogString *iir_filter_to_str(IIRFilter filter) { - switch (filter) { - case IIRFilter::IIR_FILTER_OFF: - return LOG_STR("OFF"); - case IIRFilter::IIR_FILTER_2: - return LOG_STR("2x"); - case IIRFilter::IIR_FILTER_4: - return LOG_STR("4x"); - case IIRFilter::IIR_FILTER_8: - return LOG_STR("8x"); - case IIRFilter::IIR_FILTER_16: - return LOG_STR("16x"); - case IIRFilter::IIR_FILTER_32: - return LOG_STR("32x"); - case IIRFilter::IIR_FILTER_64: - return LOG_STR("64x"); - case IIRFilter::IIR_FILTER_128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } -} - -void BMP3XXComponent::setup() { - this->error_code_ = NONE; - ESP_LOGCONFIG(TAG, "Setting up BMP3XX..."); - // Call the Device base class "initialise" function - if (!reset()) { - ESP_LOGE(TAG, "Failed to reset BMP3XX..."); - this->error_code_ = ERROR_SENSOR_RESET; - this->mark_failed(); - } - - if (!read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)) { - ESP_LOGE(TAG, "Can't read chip id"); - this->error_code_ = ERROR_COMMUNICATION_FAILED; - this->mark_failed(); - return; - } - ESP_LOGCONFIG(TAG, "Chip %s Id 0x%X", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg); - - if (chip_id_.reg != BMP388_ID && chip_id_.reg != BMP390_ID) { - ESP_LOGE(TAG, "Unknown chip id - is this really a BMP388 or BMP390?"); - this->error_code_ = ERROR_WRONG_CHIP_ID; - this->mark_failed(); - return; - } - // set sensor in sleep mode - stop_conversion(); - // Read the calibration parameters into the params structure - if (!read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_))) { - ESP_LOGE(TAG, "Can't read calibration data"); - this->error_code_ = ERROR_COMMUNICATION_FAILED; - this->mark_failed(); - return; - } - compensation_float_params_.param_T1 = - (float) compensation_params_.param_T1 / powf(2.0f, -8.0f); // Calculate the floating point trim parameters - compensation_float_params_.param_T2 = (float) compensation_params_.param_T2 / powf(2.0f, 30.0f); - compensation_float_params_.param_T3 = (float) compensation_params_.param_T3 / powf(2.0f, 48.0f); - compensation_float_params_.param_P1 = ((float) compensation_params_.param_P1 - powf(2.0f, 14.0f)) / powf(2.0f, 20.0f); - compensation_float_params_.param_P2 = ((float) compensation_params_.param_P2 - powf(2.0f, 14.0f)) / powf(2.0f, 29.0f); - compensation_float_params_.param_P3 = (float) compensation_params_.param_P3 / powf(2.0f, 32.0f); - compensation_float_params_.param_P4 = (float) compensation_params_.param_P4 / powf(2.0f, 37.0f); - compensation_float_params_.param_P5 = (float) compensation_params_.param_P5 / powf(2.0f, -3.0f); - compensation_float_params_.param_P6 = (float) compensation_params_.param_P6 / powf(2.0f, 6.0f); - compensation_float_params_.param_P7 = (float) compensation_params_.param_P7 / powf(2.0f, 8.0f); - compensation_float_params_.param_P8 = (float) compensation_params_.param_P8 / powf(2.0f, 15.0f); - compensation_float_params_.param_P9 = (float) compensation_params_.param_P9 / powf(2.0f, 48.0f); - compensation_float_params_.param_P10 = (float) compensation_params_.param_P10 / powf(2.0f, 48.0f); - compensation_float_params_.param_P11 = (float) compensation_params_.param_P11 / powf(2.0f, 65.0f); - - // Initialise the BMP388 IIR filter register - if (!set_iir_filter(this->iir_filter_)) { - ESP_LOGE(TAG, "Failed to set IIR filter"); - this->error_code_ = ERROR_COMMUNICATION_FAILED; - this->mark_failed(); - return; - } - - // Set power control registers - pwr_ctrl_.bit.press_en = 1; - pwr_ctrl_.bit.temp_en = 1; - // Disable pressure if no sensor defined - // keep temperature enabled since it's needed for compensation - if (this->pressure_sensor_ == nullptr) { - pwr_ctrl_.bit.press_en = 0; - this->pressure_oversampling_ = OVERSAMPLING_NONE; - } - // just disable oeversampling for temp if not used - if (this->temperature_sensor_ == nullptr) { - this->temperature_oversampling_ = OVERSAMPLING_NONE; - } - // Initialise the BMP388 oversampling register - if (!set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_)) { - ESP_LOGE(TAG, "Failed to set oversampling register"); - this->error_code_ = ERROR_COMMUNICATION_FAILED; - this->mark_failed(); - return; - } -} - -void BMP3XXComponent::dump_config() { - ESP_LOGCONFIG(TAG, "BMP3XX:"); - ESP_LOGCONFIG(TAG, " Type: %s (0x%X)", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg); - LOG_I2C_DEVICE(this); - switch (this->error_code_) { - case NONE: - break; - case ERROR_COMMUNICATION_FAILED: - ESP_LOGE(TAG, "Communication with BMP3XX failed!"); - break; - case ERROR_WRONG_CHIP_ID: - ESP_LOGE( - TAG, - "BMP3XX has wrong chip ID (reported id: 0x%X) - please check if you are really using a BMP 388 or BMP 390", - this->chip_id_.reg); - break; - case ERROR_SENSOR_RESET: - ESP_LOGE(TAG, "BMP3XX failed to reset"); - break; - default: - ESP_LOGE(TAG, "BMP3XX error code %d", (int) this->error_code_); - break; - } - ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_filter_))); - LOG_UPDATE_INTERVAL(this); - if (this->temperature_sensor_) { - LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); - ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_))); - } - if (this->pressure_sensor_) { - LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); - ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); - } -} -float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; } - -inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); } - -void BMP3XXComponent::update() { - // Enable sensor - ESP_LOGV(TAG, "Sending conversion request..."); - float meas_time = 1.0f; - // Ref: https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp390-ds002.pdf 3.9.2 - meas_time += 2.02f * oversampling_to_time(this->temperature_oversampling_) + 0.163f; - meas_time += 2.02f * oversampling_to_time(this->pressure_oversampling_) + 0.392f; - meas_time += 0.234f; - if (!set_mode(FORCED_MODE)) { - ESP_LOGE(TAG, "Failed start forced mode"); - this->mark_failed(); - return; - } - - ESP_LOGVV(TAG, "measurement time %d", uint32_t(ceilf(meas_time))); - this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { - float temperature = 0.0f; - float pressure = 0.0f; - if (this->pressure_sensor_ != nullptr) { - if (!get_measurements(temperature, pressure)) { - ESP_LOGW(TAG, "Failed to read pressure and temperature - skipping update"); - this->status_set_warning(); - return; - } - ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); - } else { - if (!get_temperature(temperature)) { - ESP_LOGW(TAG, "Failed to read temperature - skipping update"); - this->status_set_warning(); - return; - } - ESP_LOGD(TAG, "Got temperature=%.1f°C", temperature); - } - - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->pressure_sensor_ != nullptr) - this->pressure_sensor_->publish_state(pressure); - this->status_clear_warning(); - set_mode(SLEEP_MODE); - }); -} - -// Reset the BMP3XX -uint8_t BMP3XXComponent::reset() { - write_byte(BMP388_CMD, RESET_CODE); // Write the reset code to the command register - // Wait for 10ms - delay(10); - this->read_byte(BMP388_EVENT, &event_.reg); // Read the BMP388's event register - return event_.bit.por_detected; // Return if device reset is complete -} - -// Start a one shot measurement in FORCED_MODE -bool BMP3XXComponent::start_forced_conversion() { - // Only set FORCED_MODE if we're already in SLEEP_MODE - if (pwr_ctrl_.bit.mode == SLEEP_MODE) { - return set_mode(FORCED_MODE); - } - return true; -} - -// Stop the conversion and return to SLEEP_MODE -bool BMP3XXComponent::stop_conversion() { return set_mode(SLEEP_MODE); } - -// Set the pressure oversampling rate -bool BMP3XXComponent::set_pressure_oversampling(Oversampling oversampling) { - osr_.bit.osr_p = oversampling; - return this->write_byte(BMP388_OSR, osr_.reg); -} - -// Set the temperature oversampling rate -bool BMP3XXComponent::set_temperature_oversampling(Oversampling oversampling) { - osr_.bit.osr_t = oversampling; - return this->write_byte(BMP388_OSR, osr_.reg); -} - -// Set the IIR filter setting -bool BMP3XXComponent::set_iir_filter(IIRFilter iir_filter) { - config_.bit.iir_filter = iir_filter; - return this->write_byte(BMP388_CONFIG, config_.reg); -} - -// Get temperature -bool BMP3XXComponent::get_temperature(float &temperature) { - // Check if a measurement is ready - if (!data_ready()) { - return false; - } - uint8_t data[3]; - // Read the temperature - if (!this->read_bytes(BMP388_DATA_3, &data[0], 3)) { - ESP_LOGE(TAG, "Failed to read temperature"); - return false; - } - // Copy the temperature data into the adc variables - int32_t adc_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; - // Temperature compensation (function from BMP388 datasheet) - temperature = bmp388_compensate_temperature_((float) adc_temp); - return true; -} - -// Get the pressure -bool BMP3XXComponent::get_pressure(float &pressure) { - float temperature; - return get_measurements(temperature, pressure); -} - -// Get temperature and pressure -bool BMP3XXComponent::get_measurements(float &temperature, float &pressure) { - // Check if a measurement is ready - if (!data_ready()) { - ESP_LOGD(TAG, "BMP3XX Get measurement - data not ready skipping update"); - return false; - } - - uint8_t data[6]; - // Read the temperature and pressure data - if (!this->read_bytes(BMP388_DATA_0, &data[0], 6)) { - ESP_LOGE(TAG, "Failed to read measurements"); - return false; - } - // Copy the temperature and pressure data into the adc variables - int32_t adc_pres = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; - int32_t adc_temp = (int32_t) data[5] << 16 | (int32_t) data[4] << 8 | (int32_t) data[3]; - - // Temperature compensation (function from BMP388 datasheet) - temperature = bmp388_compensate_temperature_((float) adc_temp); - // Pressure compensation (function from BMP388 datasheet) - pressure = bmp388_compensate_pressure_((float) adc_pres, temperature); - // Calculate the pressure in millibar/hPa - pressure /= 100.0f; - return true; -} - -// Set the BMP388's mode in the power control register -bool BMP3XXComponent::set_mode(OperationMode mode) { - pwr_ctrl_.bit.mode = mode; - return this->write_byte(BMP388_PWR_CTRL, pwr_ctrl_.reg); -} - -// Set the BMP388 oversampling register -bool BMP3XXComponent::set_oversampling_register(Oversampling pressure_oversampling, - Oversampling temperature_oversampling) { - osr_.reg = temperature_oversampling << 3 | pressure_oversampling; - return this->write_byte(BMP388_OSR, osr_.reg); -} - -// Check if measurement data is ready -bool BMP3XXComponent::data_ready() { - // If we're in SLEEP_MODE return immediately - if (pwr_ctrl_.bit.mode == SLEEP_MODE) { - ESP_LOGD(TAG, "Not ready - sensor is in sleep mode"); - return false; - } - // Read the interrupt status register - uint8_t status; - if (!this->read_byte(BMP388_INT_STATUS, &status)) { - ESP_LOGE(TAG, "Failed to read status register"); - return false; - } - int_status_.reg = status; - ESP_LOGVV(TAG, "data ready status %d", status); - // If we're in FORCED_MODE switch back to SLEEP_MODE - if (int_status_.bit.drdy) { - if (pwr_ctrl_.bit.mode == FORCED_MODE) { - pwr_ctrl_.bit.mode = SLEEP_MODE; - } - return true; // The measurement is ready - } - return false; // The measurement is still pending -} - -//////////////////////////////////////////////////////////////////////////////// -// Bosch BMP3XXComponent (Private) Member Functions -//////////////////////////////////////////////////////////////////////////////// - -float BMP3XXComponent::bmp388_compensate_temperature_(float uncomp_temp) { - float partial_data1 = uncomp_temp - compensation_float_params_.param_T1; - float partial_data2 = partial_data1 * compensation_float_params_.param_T2; - return partial_data2 + partial_data1 * partial_data1 * compensation_float_params_.param_T3; -} - -float BMP3XXComponent::bmp388_compensate_pressure_(float uncomp_press, float t_lin) { - float partial_data1 = compensation_float_params_.param_P6 * t_lin; - float partial_data2 = compensation_float_params_.param_P7 * t_lin * t_lin; - float partial_data3 = compensation_float_params_.param_P8 * t_lin * t_lin * t_lin; - float partial_out1 = compensation_float_params_.param_P5 + partial_data1 + partial_data2 + partial_data3; - partial_data1 = compensation_float_params_.param_P2 * t_lin; - partial_data2 = compensation_float_params_.param_P3 * t_lin * t_lin; - partial_data3 = compensation_float_params_.param_P4 * t_lin * t_lin * t_lin; - float partial_out2 = - uncomp_press * (compensation_float_params_.param_P1 + partial_data1 + partial_data2 + partial_data3); - partial_data1 = uncomp_press * uncomp_press; - partial_data2 = compensation_float_params_.param_P9 + compensation_float_params_.param_P10 * t_lin; - partial_data3 = partial_data1 * partial_data2; - float partial_data4 = - partial_data3 + uncomp_press * uncomp_press * uncomp_press * compensation_float_params_.param_P11; - return partial_out1 + partial_out2 + partial_data4; -} - -} // namespace bmp3xx -} // namespace esphome diff --git a/esphome/components/bmp3xx/bmp3xx.h b/esphome/components/bmp3xx/bmp3xx.h deleted file mode 100644 index d3b15f601d..0000000000 --- a/esphome/components/bmp3xx/bmp3xx.h +++ /dev/null @@ -1,237 +0,0 @@ -/* - based on BMP388_DEV by Martin Lindupp - under MIT License (MIT) - Copyright (C) Martin Lindupp 2020 - http://github.com/MartinL1/BMP388_DEV -*/ - -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" - -namespace esphome { -namespace bmp3xx { - -static const uint8_t BMP388_ID = 0x50; // The BMP388 device ID -static const uint8_t BMP390_ID = 0x60; // The BMP390 device ID -static const uint8_t RESET_CODE = 0xB6; // The BMP388 reset code - -/// BMP388_DEV Registers -enum { - BMP388_CHIP_ID = 0x00, // Chip ID register sub-address - BMP388_ERR_REG = 0x02, // Error register sub-address - BMP388_STATUS = 0x03, // Status register sub-address - BMP388_DATA_0 = 0x04, // Pressure eXtended Least Significant Byte (XLSB) register sub-address - BMP388_DATA_1 = 0x05, // Pressure Least Significant Byte (LSB) register sub-address - BMP388_DATA_2 = 0x06, // Pressure Most Significant Byte (MSB) register sub-address - BMP388_DATA_3 = 0x07, // Temperature eXtended Least Significant Byte (XLSB) register sub-address - BMP388_DATA_4 = 0x08, // Temperature Least Significant Byte (LSB) register sub-address - BMP388_DATA_5 = 0x09, // Temperature Most Significant Byte (MSB) register sub-address - BMP388_SENSORTIME_0 = 0x0C, // Sensor time register 0 sub-address - BMP388_SENSORTIME_1 = 0x0D, // Sensor time register 1 sub-address - BMP388_SENSORTIME_2 = 0x0E, // Sensor time register 2 sub-address - BMP388_EVENT = 0x10, // Event register sub-address - BMP388_INT_STATUS = 0x11, // Interrupt Status register sub-address - BMP388_INT_CTRL = 0x19, // Interrupt Control register sub-address - BMP388_IF_CONFIG = 0x1A, // Interface Configuration register sub-address - BMP388_PWR_CTRL = 0x1B, // Power Control register sub-address - BMP388_OSR = 0x1C, // Oversampling register sub-address - BMP388_ODR = 0x1D, // Output Data Rate register sub-address - BMP388_CONFIG = 0x1F, // Configuration register sub-address - BMP388_TRIM_PARAMS = 0x31, // Trim parameter registers' base sub-address - BMP388_CMD = 0x7E // Command register sub-address -}; - -/// Device mode bitfield in the control and measurement register -enum OperationMode { SLEEP_MODE = 0x00, FORCED_MODE = 0x01, NORMAL_MODE = 0x03 }; - -/// Oversampling bit fields in the control and measurement register -enum Oversampling { - OVERSAMPLING_NONE = 0x00, - OVERSAMPLING_X2 = 0x01, - OVERSAMPLING_X4 = 0x02, - OVERSAMPLING_X8 = 0x03, - OVERSAMPLING_X16 = 0x04, - OVERSAMPLING_X32 = 0x05 -}; - -/// Infinite Impulse Response (IIR) filter bit field in the configuration register -enum IIRFilter { - IIR_FILTER_OFF = 0x00, - IIR_FILTER_2 = 0x01, - IIR_FILTER_4 = 0x02, - IIR_FILTER_8 = 0x03, - IIR_FILTER_16 = 0x04, - IIR_FILTER_32 = 0x05, - IIR_FILTER_64 = 0x06, - IIR_FILTER_128 = 0x07 -}; - -/// This class implements support for the BMP3XX Temperature+Pressure i2c sensor. -class BMP3XXComponent : public PollingComponent, public i2c::I2CDevice { - public: - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - void update() override; - - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } - void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } - - /// Set the oversampling value for the temperature sensor. Default is 16x. - void set_temperature_oversampling_config(Oversampling temperature_oversampling) { - this->temperature_oversampling_ = temperature_oversampling; - } - /// Set the oversampling value for the pressure sensor. Default is 16x. - void set_pressure_oversampling_config(Oversampling pressure_oversampling) { - this->pressure_oversampling_ = pressure_oversampling; - } - /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. - void set_iir_filter_config(IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } - - /// Soft reset the sensor - uint8_t reset(); - /// Start continuous measurement in NORMAL_MODE - bool start_normal_conversion(); - /// Start a one shot measurement in FORCED_MODE - bool start_forced_conversion(); - /// Stop the conversion and return to SLEEP_MODE - bool stop_conversion(); - /// Set the pressure oversampling: OFF, X1, X2, X4, X8, X16, X32 - bool set_pressure_oversampling(Oversampling pressure_oversampling); - /// Set the temperature oversampling: OFF, X1, X2, X4, X8, X16, X32 - bool set_temperature_oversampling(Oversampling temperature_oversampling); - /// Set the IIR filter setting: OFF, 2, 3, 8, 16, 32 - bool set_iir_filter(IIRFilter iir_filter); - /// Get a temperature measurement - bool get_temperature(float &temperature); - /// Get a pressure measurement - bool get_pressure(float &pressure); - /// Get a temperature and pressure measurement - bool get_measurements(float &temperature, float &pressure); - /// Get a temperature and pressure measurement - bool get_measurement(); - /// Set the barometer mode - bool set_mode(OperationMode mode); - /// Set the BMP388 oversampling register - bool set_oversampling_register(Oversampling pressure_oversampling, Oversampling temperature_oversampling); - /// Checks if a measurement is ready - bool data_ready(); - - protected: - Oversampling temperature_oversampling_{OVERSAMPLING_X16}; - Oversampling pressure_oversampling_{OVERSAMPLING_X16}; - IIRFilter iir_filter_{IIR_FILTER_OFF}; - OperationMode operation_mode_{FORCED_MODE}; - sensor::Sensor *temperature_sensor_{nullptr}; - sensor::Sensor *pressure_sensor_{nullptr}; - enum ErrorCode { - NONE = 0, - ERROR_COMMUNICATION_FAILED, - ERROR_WRONG_CHIP_ID, - ERROR_SENSOR_STATUS, - ERROR_SENSOR_RESET, - } error_code_{NONE}; - - struct { // The BMP388 compensation trim parameters (coefficients) - uint16_t param_T1; - uint16_t param_T2; - int8_t param_T3; - int16_t param_P1; - int16_t param_P2; - int8_t param_P3; - int8_t param_P4; - uint16_t param_P5; - uint16_t param_P6; - int8_t param_P7; - int8_t param_P8; - int16_t param_P9; - int8_t param_P10; - int8_t param_P11; - } __attribute__((packed)) compensation_params_; - - struct FloatParams { // The BMP388 float point compensation trim parameters - float param_T1; - float param_T2; - float param_T3; - float param_P1; - float param_P2; - float param_P3; - float param_P4; - float param_P5; - float param_P6; - float param_P7; - float param_P8; - float param_P9; - float param_P10; - float param_P11; - } compensation_float_params_; - - union { // Copy of the BMP388's chip id register - struct { - uint8_t chip_id_nvm : 4; - uint8_t chip_id_fixed : 4; - } bit; - uint8_t reg; - } chip_id_ = {.reg = 0}; - - union { // Copy of the BMP388's event register - struct { - uint8_t por_detected : 1; - } bit; - uint8_t reg; - } event_ = {.reg = 0}; - - union { // Copy of the BMP388's interrupt status register - struct { - uint8_t fwm_int : 1; - uint8_t ffull_int : 1; - uint8_t : 1; - uint8_t drdy : 1; - } bit; - uint8_t reg; - } int_status_ = {.reg = 0}; - - union { // Copy of the BMP388's power control register - struct { - uint8_t press_en : 1; - uint8_t temp_en : 1; - uint8_t : 2; - uint8_t mode : 2; - } bit; - uint8_t reg; - } pwr_ctrl_ = {.reg = 0}; - - union { // Copy of the BMP388's oversampling register - struct { - uint8_t osr_p : 3; - uint8_t osr_t : 3; - } bit; - uint8_t reg; - } osr_ = {.reg = 0}; - - union { // Copy of the BMP388's output data rate register - struct { - uint8_t odr_sel : 5; - } bit; - uint8_t reg; - } odr_ = {.reg = 0}; - - union { // Copy of the BMP388's configuration register - struct { - uint8_t : 1; - uint8_t iir_filter : 3; - } bit; - uint8_t reg; - } config_ = {.reg = 0}; - - // Bosch temperature compensation function - float bmp388_compensate_temperature_(float uncomp_temp); - // Bosch pressure compensation function - float bmp388_compensate_pressure_(float uncomp_press, float t_lin); -}; - -} // namespace bmp3xx -} // namespace esphome diff --git a/esphome/components/bmp3xx/sensor.py b/esphome/components/bmp3xx/sensor.py index f0da1c3c24..3c7927f1a8 100644 --- a/esphome/components/bmp3xx/sensor.py +++ b/esphome/components/bmp3xx/sensor.py @@ -1,100 +1,7 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor -from esphome.const import ( - CONF_ID, - CONF_IIR_FILTER, - CONF_OVERSAMPLING, - CONF_PRESSURE, - CONF_TEMPERATURE, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, + +CODEOWNERS = ["@latonita"] + +CONFIG_SCHEMA = cv.invalid( + "The bmp3xx sensor component has been renamed to bmp3xx_i2c." ) - -CODEOWNERS = ["@martgras"] -DEPENDENCIES = ["i2c"] - -bmp3xx_ns = cg.esphome_ns.namespace("bmp3xx") -Oversampling = bmp3xx_ns.enum("Oversampling") -OVERSAMPLING_OPTIONS = { - "NONE": Oversampling.OVERSAMPLING_NONE, - "2X": Oversampling.OVERSAMPLING_X2, - "4X": Oversampling.OVERSAMPLING_X4, - "8X": Oversampling.OVERSAMPLING_X8, - "16X": Oversampling.OVERSAMPLING_X16, - "32X": Oversampling.OVERSAMPLING_X32, -} - -IIRFilter = bmp3xx_ns.enum("IIRFilter") -IIR_FILTER_OPTIONS = { - "OFF": IIRFilter.IIR_FILTER_OFF, - "2X": IIRFilter.IIR_FILTER_2, - "4X": IIRFilter.IIR_FILTER_4, - "8X": IIRFilter.IIR_FILTER_8, - "16X": IIRFilter.IIR_FILTER_16, - "32X": IIRFilter.IIR_FILTER_32, - "64X": IIRFilter.IIR_FILTER_64, - "128X": IIRFilter.IIR_FILTER_128, -} - -BMP3XXComponent = bmp3xx_ns.class_( - "BMP3XXComponent", cg.PollingComponent, i2c.I2CDevice -) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(BMP3XXComponent), - 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, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="2X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - unit_of_measurement=UNIT_HECTOPASCAL, - accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( - IIR_FILTER_OPTIONS, upper=True - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x77)) -) - - -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) - cg.add(var.set_iir_filter_config(config[CONF_IIR_FILTER])) - if CONF_TEMPERATURE in config: - conf = config[CONF_TEMPERATURE] - sens = await sensor.new_sensor(conf) - cg.add(var.set_temperature_sensor(sens)) - cg.add(var.set_temperature_oversampling_config(conf[CONF_OVERSAMPLING])) - - if CONF_PRESSURE in config: - conf = config[CONF_PRESSURE] - sens = await sensor.new_sensor(conf) - cg.add(var.set_pressure_sensor(sens)) - cg.add(var.set_pressure_oversampling_config(conf[CONF_OVERSAMPLING])) diff --git a/esphome/components/bmp3xx_base/__init__.py b/esphome/components/bmp3xx_base/__init__.py new file mode 100644 index 0000000000..589d170907 --- /dev/null +++ b/esphome/components/bmp3xx_base/__init__.py @@ -0,0 +1,95 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, +) + +CODEOWNERS = ["@martgras", "@latonita"] + +bmp3xx_ns = cg.esphome_ns.namespace("bmp3xx_base") +Oversampling = bmp3xx_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32X": Oversampling.OVERSAMPLING_X32, +} + +IIRFilter = bmp3xx_ns.enum("IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": IIRFilter.IIR_FILTER_OFF, + "2X": IIRFilter.IIR_FILTER_2, + "4X": IIRFilter.IIR_FILTER_4, + "8X": IIRFilter.IIR_FILTER_8, + "16X": IIRFilter.IIR_FILTER_16, + "32X": IIRFilter.IIR_FILTER_32, + "64X": IIRFilter.IIR_FILTER_64, + "128X": IIRFilter.IIR_FILTER_128, +} + + +CONFIG_SCHEMA_BASE = cv.Schema( + { + 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, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="2X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_iir_filter_config(config[CONF_IIR_FILTER])) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add( + var.set_temperature_oversampling_config( + temperature_config[CONF_OVERSAMPLING] + ) + ) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING])) + + return var diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp new file mode 100644 index 0000000000..75b6812f81 --- /dev/null +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -0,0 +1,389 @@ +/* + based on BMP388_DEV by Martin Lindupp + under MIT License (MIT) + Copyright (C) Martin Lindupp 2020 + http://github.com/MartinL1/BMP388_DEV +*/ + +#include "bmp3xx_base.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace bmp3xx_base { + +static const char *const TAG = "bmp3xx.sensor"; + +static const LogString *chip_type_to_str(uint8_t chip_type) { + switch (chip_type) { + case BMP388_ID: + return LOG_STR("BMP 388"); + case BMP390_ID: + return LOG_STR("BMP 390"); + default: + return LOG_STR("Unknown Chip Type"); + } +} + +static const LogString *oversampling_to_str(Oversampling oversampling) { + switch (oversampling) { + case Oversampling::OVERSAMPLING_NONE: + return LOG_STR("None"); + case Oversampling::OVERSAMPLING_X2: + return LOG_STR("2x"); + case Oversampling::OVERSAMPLING_X4: + return LOG_STR("4x"); + case Oversampling::OVERSAMPLING_X8: + return LOG_STR("8x"); + case Oversampling::OVERSAMPLING_X16: + return LOG_STR("16x"); + case Oversampling::OVERSAMPLING_X32: + return LOG_STR("32x"); + default: + return LOG_STR(""); + } +} + +static const LogString *iir_filter_to_str(IIRFilter filter) { + switch (filter) { + case IIRFilter::IIR_FILTER_OFF: + return LOG_STR("OFF"); + case IIRFilter::IIR_FILTER_2: + return LOG_STR("2x"); + case IIRFilter::IIR_FILTER_4: + return LOG_STR("4x"); + case IIRFilter::IIR_FILTER_8: + return LOG_STR("8x"); + case IIRFilter::IIR_FILTER_16: + return LOG_STR("16x"); + case IIRFilter::IIR_FILTER_32: + return LOG_STR("32x"); + case IIRFilter::IIR_FILTER_64: + return LOG_STR("64x"); + case IIRFilter::IIR_FILTER_128: + return LOG_STR("128x"); + default: + return LOG_STR(""); + } +} + +void BMP3XXComponent::setup() { + this->error_code_ = NONE; + ESP_LOGCONFIG(TAG, "Setting up BMP3XX..."); + // Call the Device base class "initialise" function + if (!reset()) { + ESP_LOGE(TAG, "Failed to reset BMP3XX..."); + this->error_code_ = ERROR_SENSOR_RESET; + this->mark_failed(); + } + + if (!read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)) { + ESP_LOGE(TAG, "Can't read chip id"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "Chip %s Id 0x%X", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg); + + if (chip_id_.reg != BMP388_ID && chip_id_.reg != BMP390_ID) { + ESP_LOGE(TAG, "Unknown chip id - is this really a BMP388 or BMP390?"); + this->error_code_ = ERROR_WRONG_CHIP_ID; + this->mark_failed(); + return; + } + // set sensor in sleep mode + stop_conversion(); + // Read the calibration parameters into the params structure + if (!read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_))) { + ESP_LOGE(TAG, "Can't read calibration data"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + compensation_float_params_.param_T1 = + (float) compensation_params_.param_T1 / powf(2.0f, -8.0f); // Calculate the floating point trim parameters + compensation_float_params_.param_T2 = (float) compensation_params_.param_T2 / powf(2.0f, 30.0f); + compensation_float_params_.param_T3 = (float) compensation_params_.param_T3 / powf(2.0f, 48.0f); + compensation_float_params_.param_P1 = ((float) compensation_params_.param_P1 - powf(2.0f, 14.0f)) / powf(2.0f, 20.0f); + compensation_float_params_.param_P2 = ((float) compensation_params_.param_P2 - powf(2.0f, 14.0f)) / powf(2.0f, 29.0f); + compensation_float_params_.param_P3 = (float) compensation_params_.param_P3 / powf(2.0f, 32.0f); + compensation_float_params_.param_P4 = (float) compensation_params_.param_P4 / powf(2.0f, 37.0f); + compensation_float_params_.param_P5 = (float) compensation_params_.param_P5 / powf(2.0f, -3.0f); + compensation_float_params_.param_P6 = (float) compensation_params_.param_P6 / powf(2.0f, 6.0f); + compensation_float_params_.param_P7 = (float) compensation_params_.param_P7 / powf(2.0f, 8.0f); + compensation_float_params_.param_P8 = (float) compensation_params_.param_P8 / powf(2.0f, 15.0f); + compensation_float_params_.param_P9 = (float) compensation_params_.param_P9 / powf(2.0f, 48.0f); + compensation_float_params_.param_P10 = (float) compensation_params_.param_P10 / powf(2.0f, 48.0f); + compensation_float_params_.param_P11 = (float) compensation_params_.param_P11 / powf(2.0f, 65.0f); + + // Initialise the BMP388 IIR filter register + if (!set_iir_filter(this->iir_filter_)) { + ESP_LOGE(TAG, "Failed to set IIR filter"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + // Set power control registers + pwr_ctrl_.bit.press_en = 1; + pwr_ctrl_.bit.temp_en = 1; + // Disable pressure if no sensor defined + // keep temperature enabled since it's needed for compensation + if (this->pressure_sensor_ == nullptr) { + pwr_ctrl_.bit.press_en = 0; + this->pressure_oversampling_ = OVERSAMPLING_NONE; + } + // just disable oeversampling for temp if not used + if (this->temperature_sensor_ == nullptr) { + this->temperature_oversampling_ = OVERSAMPLING_NONE; + } + // Initialise the BMP388 oversampling register + if (!set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_)) { + ESP_LOGE(TAG, "Failed to set oversampling register"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } +} + +void BMP3XXComponent::dump_config() { + ESP_LOGCONFIG(TAG, "BMP3XX:"); + ESP_LOGCONFIG(TAG, " Type: %s (0x%X)", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg); + switch (this->error_code_) { + case NONE: + break; + case ERROR_COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with BMP3XX failed!"); + break; + case ERROR_WRONG_CHIP_ID: + ESP_LOGE( + TAG, + "BMP3XX has wrong chip ID (reported id: 0x%X) - please check if you are really using a BMP 388 or BMP 390", + this->chip_id_.reg); + break; + case ERROR_SENSOR_RESET: + ESP_LOGE(TAG, "BMP3XX failed to reset"); + break; + default: + ESP_LOGE(TAG, "BMP3XX error code %d", (int) this->error_code_); + break; + } + ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_filter_))); + LOG_UPDATE_INTERVAL(this); + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_))); + } + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); + } +} +float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; } + +inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); } + +void BMP3XXComponent::update() { + // Enable sensor + ESP_LOGV(TAG, "Sending conversion request..."); + float meas_time = 1.0f; + // Ref: https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp390-ds002.pdf 3.9.2 + meas_time += 2.02f * oversampling_to_time(this->temperature_oversampling_) + 0.163f; + meas_time += 2.02f * oversampling_to_time(this->pressure_oversampling_) + 0.392f; + meas_time += 0.234f; + if (!set_mode(FORCED_MODE)) { + ESP_LOGE(TAG, "Failed start forced mode"); + this->mark_failed(); + return; + } + + const uint32_t meas_timeout = uint32_t(ceilf(meas_time)); + ESP_LOGVV(TAG, "measurement time %" PRIu32, meas_timeout); + this->set_timeout("data", meas_timeout, [this]() { + float temperature = 0.0f; + float pressure = 0.0f; + if (this->pressure_sensor_ != nullptr) { + if (!get_measurements(temperature, pressure)) { + ESP_LOGW(TAG, "Failed to read pressure and temperature - skipping update"); + this->status_set_warning(); + return; + } + ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); + } else { + if (!get_temperature(temperature)) { + ESP_LOGW(TAG, "Failed to read temperature - skipping update"); + this->status_set_warning(); + return; + } + ESP_LOGD(TAG, "Got temperature=%.1f°C", temperature); + } + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure); + this->status_clear_warning(); + set_mode(SLEEP_MODE); + }); +} + +// Reset the BMP3XX +uint8_t BMP3XXComponent::reset() { + write_byte(BMP388_CMD, RESET_CODE); // Write the reset code to the command register + // Wait for 10ms + delay(10); + this->read_byte(BMP388_EVENT, &event_.reg); // Read the BMP388's event register + return event_.bit.por_detected; // Return if device reset is complete +} + +// Start a one shot measurement in FORCED_MODE +bool BMP3XXComponent::start_forced_conversion() { + // Only set FORCED_MODE if we're already in SLEEP_MODE + if (pwr_ctrl_.bit.mode == SLEEP_MODE) { + return set_mode(FORCED_MODE); + } + return true; +} + +// Stop the conversion and return to SLEEP_MODE +bool BMP3XXComponent::stop_conversion() { return set_mode(SLEEP_MODE); } + +// Set the pressure oversampling rate +bool BMP3XXComponent::set_pressure_oversampling(Oversampling oversampling) { + osr_.bit.osr_p = oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Set the temperature oversampling rate +bool BMP3XXComponent::set_temperature_oversampling(Oversampling oversampling) { + osr_.bit.osr_t = oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Set the IIR filter setting +bool BMP3XXComponent::set_iir_filter(IIRFilter iir_filter) { + config_.bit.iir_filter = iir_filter; + return this->write_byte(BMP388_CONFIG, config_.reg); +} + +// Get temperature +bool BMP3XXComponent::get_temperature(float &temperature) { + // Check if a measurement is ready + if (!data_ready()) { + return false; + } + uint8_t data[3]; + // Read the temperature + if (!this->read_bytes(BMP388_DATA_3, &data[0], 3)) { + ESP_LOGE(TAG, "Failed to read temperature"); + return false; + } + // Copy the temperature data into the adc variables + int32_t adc_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + // Temperature compensation (function from BMP388 datasheet) + temperature = bmp388_compensate_temperature_((float) adc_temp); + return true; +} + +// Get the pressure +bool BMP3XXComponent::get_pressure(float &pressure) { + float temperature; + return get_measurements(temperature, pressure); +} + +// Get temperature and pressure +bool BMP3XXComponent::get_measurements(float &temperature, float &pressure) { + // Check if a measurement is ready + if (!data_ready()) { + ESP_LOGD(TAG, "BMP3XX Get measurement - data not ready skipping update"); + return false; + } + + uint8_t data[6]; + // Read the temperature and pressure data + if (!this->read_bytes(BMP388_DATA_0, &data[0], 6)) { + ESP_LOGE(TAG, "Failed to read measurements"); + return false; + } + // Copy the temperature and pressure data into the adc variables + int32_t adc_pres = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t adc_temp = (int32_t) data[5] << 16 | (int32_t) data[4] << 8 | (int32_t) data[3]; + + // Temperature compensation (function from BMP388 datasheet) + temperature = bmp388_compensate_temperature_((float) adc_temp); + // Pressure compensation (function from BMP388 datasheet) + pressure = bmp388_compensate_pressure_((float) adc_pres, temperature); + // Calculate the pressure in millibar/hPa + pressure /= 100.0f; + return true; +} + +// Set the BMP388's mode in the power control register +bool BMP3XXComponent::set_mode(OperationMode mode) { + pwr_ctrl_.bit.mode = mode; + return this->write_byte(BMP388_PWR_CTRL, pwr_ctrl_.reg); +} + +// Set the BMP388 oversampling register +bool BMP3XXComponent::set_oversampling_register(Oversampling pressure_oversampling, + Oversampling temperature_oversampling) { + osr_.reg = temperature_oversampling << 3 | pressure_oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Check if measurement data is ready +bool BMP3XXComponent::data_ready() { + // If we're in SLEEP_MODE return immediately + if (pwr_ctrl_.bit.mode == SLEEP_MODE) { + ESP_LOGD(TAG, "Not ready - sensor is in sleep mode"); + return false; + } + // Read the interrupt status register + uint8_t status; + if (!this->read_byte(BMP388_INT_STATUS, &status)) { + ESP_LOGE(TAG, "Failed to read status register"); + return false; + } + int_status_.reg = status; + ESP_LOGVV(TAG, "data ready status %d", status); + // If we're in FORCED_MODE switch back to SLEEP_MODE + if (int_status_.bit.drdy) { + if (pwr_ctrl_.bit.mode == FORCED_MODE) { + pwr_ctrl_.bit.mode = SLEEP_MODE; + } + return true; // The measurement is ready + } + return false; // The measurement is still pending +} + +//////////////////////////////////////////////////////////////////////////////// +// Bosch BMP3XXComponent (Private) Member Functions +//////////////////////////////////////////////////////////////////////////////// + +float BMP3XXComponent::bmp388_compensate_temperature_(float uncomp_temp) { + float partial_data1 = uncomp_temp - compensation_float_params_.param_T1; + float partial_data2 = partial_data1 * compensation_float_params_.param_T2; + return partial_data2 + partial_data1 * partial_data1 * compensation_float_params_.param_T3; +} + +float BMP3XXComponent::bmp388_compensate_pressure_(float uncomp_press, float t_lin) { + float partial_data1 = compensation_float_params_.param_P6 * t_lin; + float partial_data2 = compensation_float_params_.param_P7 * t_lin * t_lin; + float partial_data3 = compensation_float_params_.param_P8 * t_lin * t_lin * t_lin; + float partial_out1 = compensation_float_params_.param_P5 + partial_data1 + partial_data2 + partial_data3; + partial_data1 = compensation_float_params_.param_P2 * t_lin; + partial_data2 = compensation_float_params_.param_P3 * t_lin * t_lin; + partial_data3 = compensation_float_params_.param_P4 * t_lin * t_lin * t_lin; + float partial_out2 = + uncomp_press * (compensation_float_params_.param_P1 + partial_data1 + partial_data2 + partial_data3); + partial_data1 = uncomp_press * uncomp_press; + partial_data2 = compensation_float_params_.param_P9 + compensation_float_params_.param_P10 * t_lin; + partial_data3 = partial_data1 * partial_data2; + float partial_data4 = + partial_data3 + uncomp_press * uncomp_press * uncomp_press * compensation_float_params_.param_P11; + return partial_out1 + partial_out2 + partial_data4; +} + +} // namespace bmp3xx_base +} // namespace esphome diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.h b/esphome/components/bmp3xx_base/bmp3xx_base.h new file mode 100644 index 0000000000..50f92e04c1 --- /dev/null +++ b/esphome/components/bmp3xx_base/bmp3xx_base.h @@ -0,0 +1,242 @@ +/* + based on BMP388_DEV by Martin Lindupp + under MIT License (MIT) + Copyright (C) Martin Lindupp 2020 + http://github.com/MartinL1/BMP388_DEV +*/ + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bmp3xx_base { + +static const uint8_t BMP388_ID = 0x50; // The BMP388 device ID +static const uint8_t BMP390_ID = 0x60; // The BMP390 device ID +static const uint8_t RESET_CODE = 0xB6; // The BMP388 reset code + +/// BMP388_DEV Registers +enum { + BMP388_CHIP_ID = 0x00, // Chip ID register sub-address + BMP388_ERR_REG = 0x02, // Error register sub-address + BMP388_STATUS = 0x03, // Status register sub-address + BMP388_DATA_0 = 0x04, // Pressure eXtended Least Significant Byte (XLSB) register sub-address + BMP388_DATA_1 = 0x05, // Pressure Least Significant Byte (LSB) register sub-address + BMP388_DATA_2 = 0x06, // Pressure Most Significant Byte (MSB) register sub-address + BMP388_DATA_3 = 0x07, // Temperature eXtended Least Significant Byte (XLSB) register sub-address + BMP388_DATA_4 = 0x08, // Temperature Least Significant Byte (LSB) register sub-address + BMP388_DATA_5 = 0x09, // Temperature Most Significant Byte (MSB) register sub-address + BMP388_SENSORTIME_0 = 0x0C, // Sensor time register 0 sub-address + BMP388_SENSORTIME_1 = 0x0D, // Sensor time register 1 sub-address + BMP388_SENSORTIME_2 = 0x0E, // Sensor time register 2 sub-address + BMP388_EVENT = 0x10, // Event register sub-address + BMP388_INT_STATUS = 0x11, // Interrupt Status register sub-address + BMP388_INT_CTRL = 0x19, // Interrupt Control register sub-address + BMP388_IF_CONFIG = 0x1A, // Interface Configuration register sub-address + BMP388_PWR_CTRL = 0x1B, // Power Control register sub-address + BMP388_OSR = 0x1C, // Oversampling register sub-address + BMP388_ODR = 0x1D, // Output Data Rate register sub-address + BMP388_CONFIG = 0x1F, // Configuration register sub-address + BMP388_TRIM_PARAMS = 0x31, // Trim parameter registers' base sub-address + BMP388_CMD = 0x7E // Command register sub-address +}; + +/// Device mode bitfield in the control and measurement register +enum OperationMode { SLEEP_MODE = 0x00, FORCED_MODE = 0x01, NORMAL_MODE = 0x03 }; + +/// Oversampling bit fields in the control and measurement register +enum Oversampling { + OVERSAMPLING_NONE = 0x00, + OVERSAMPLING_X2 = 0x01, + OVERSAMPLING_X4 = 0x02, + OVERSAMPLING_X8 = 0x03, + OVERSAMPLING_X16 = 0x04, + OVERSAMPLING_X32 = 0x05 +}; + +/// Infinite Impulse Response (IIR) filter bit field in the configuration register +enum IIRFilter { + IIR_FILTER_OFF = 0x00, + IIR_FILTER_2 = 0x01, + IIR_FILTER_4 = 0x02, + IIR_FILTER_8 = 0x03, + IIR_FILTER_16 = 0x04, + IIR_FILTER_32 = 0x05, + IIR_FILTER_64 = 0x06, + IIR_FILTER_128 = 0x07 +}; + +/// This class implements support for the BMP3XX Temperature+Pressure sensor. +class BMP3XXComponent : public PollingComponent { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + /// Set the oversampling value for the temperature sensor. Default is 16x. + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + } + /// Set the oversampling value for the pressure sensor. Default is 16x. + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + } + /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. + void set_iir_filter_config(IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } + + /// Soft reset the sensor + uint8_t reset(); + /// Start continuous measurement in NORMAL_MODE + bool start_normal_conversion(); + /// Start a one shot measurement in FORCED_MODE + bool start_forced_conversion(); + /// Stop the conversion and return to SLEEP_MODE + bool stop_conversion(); + /// Set the pressure oversampling: OFF, X1, X2, X4, X8, X16, X32 + bool set_pressure_oversampling(Oversampling pressure_oversampling); + /// Set the temperature oversampling: OFF, X1, X2, X4, X8, X16, X32 + bool set_temperature_oversampling(Oversampling temperature_oversampling); + /// Set the IIR filter setting: OFF, 2, 3, 8, 16, 32 + bool set_iir_filter(IIRFilter iir_filter); + /// Get a temperature measurement + bool get_temperature(float &temperature); + /// Get a pressure measurement + bool get_pressure(float &pressure); + /// Get a temperature and pressure measurement + bool get_measurements(float &temperature, float &pressure); + /// Get a temperature and pressure measurement + bool get_measurement(); + /// Set the barometer mode + bool set_mode(OperationMode mode); + /// Set the BMP388 oversampling register + bool set_oversampling_register(Oversampling pressure_oversampling, Oversampling temperature_oversampling); + /// Checks if a measurement is ready + bool data_ready(); + + protected: + Oversampling temperature_oversampling_{OVERSAMPLING_X16}; + Oversampling pressure_oversampling_{OVERSAMPLING_X16}; + IIRFilter iir_filter_{IIR_FILTER_OFF}; + OperationMode operation_mode_{FORCED_MODE}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + enum ErrorCode { + NONE = 0, + ERROR_COMMUNICATION_FAILED, + ERROR_WRONG_CHIP_ID, + ERROR_SENSOR_STATUS, + ERROR_SENSOR_RESET, + } error_code_{NONE}; + + struct { // The BMP388 compensation trim parameters (coefficients) + uint16_t param_T1; + uint16_t param_T2; + int8_t param_T3; + int16_t param_P1; + int16_t param_P2; + int8_t param_P3; + int8_t param_P4; + uint16_t param_P5; + uint16_t param_P6; + int8_t param_P7; + int8_t param_P8; + int16_t param_P9; + int8_t param_P10; + int8_t param_P11; + } __attribute__((packed)) compensation_params_; + + struct FloatParams { // The BMP388 float point compensation trim parameters + float param_T1; + float param_T2; + float param_T3; + float param_P1; + float param_P2; + float param_P3; + float param_P4; + float param_P5; + float param_P6; + float param_P7; + float param_P8; + float param_P9; + float param_P10; + float param_P11; + } compensation_float_params_; + + union { // Copy of the BMP388's chip id register + struct { + uint8_t chip_id_nvm : 4; + uint8_t chip_id_fixed : 4; + } bit; + uint8_t reg; + } chip_id_ = {.reg = 0}; + + union { // Copy of the BMP388's event register + struct { + uint8_t por_detected : 1; + } bit; + uint8_t reg; + } event_ = {.reg = 0}; + + union { // Copy of the BMP388's interrupt status register + struct { + uint8_t fwm_int : 1; + uint8_t ffull_int : 1; + uint8_t : 1; + uint8_t drdy : 1; + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; + + union { // Copy of the BMP388's power control register + struct { + uint8_t press_en : 1; + uint8_t temp_en : 1; + uint8_t : 2; + uint8_t mode : 2; + } bit; + uint8_t reg; + } pwr_ctrl_ = {.reg = 0}; + + union { // Copy of the BMP388's oversampling register + struct { + uint8_t osr_p : 3; + uint8_t osr_t : 3; + } bit; + uint8_t reg; + } osr_ = {.reg = 0}; + + union { // Copy of the BMP388's output data rate register + struct { + uint8_t odr_sel : 5; + } bit; + uint8_t reg; + } odr_ = {.reg = 0}; + + union { // Copy of the BMP388's configuration register + struct { + uint8_t : 1; + uint8_t iir_filter : 3; + } bit; + uint8_t reg; + } config_ = {.reg = 0}; + + // Bosch temperature compensation function + float bmp388_compensate_temperature_(float uncomp_temp); + // Bosch pressure compensation function + float bmp388_compensate_pressure_(float uncomp_press, float t_lin); + + // interface specific functions + virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; +}; + +} // namespace bmp3xx_base +} // namespace esphome diff --git a/esphome/components/bmp3xx_i2c/__init__.py b/esphome/components/bmp3xx_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp3xx_i2c/bmp3xx_i2c.cpp b/esphome/components/bmp3xx_i2c/bmp3xx_i2c.cpp new file mode 100644 index 0000000000..7531090185 --- /dev/null +++ b/esphome/components/bmp3xx_i2c/bmp3xx_i2c.cpp @@ -0,0 +1,29 @@ +#include "esphome/components/i2c/i2c.h" +#include "bmp3xx_i2c.h" +#include + +namespace esphome { +namespace bmp3xx_i2c { + +static const char *const TAG = "bmp3xx_i2c.sensor"; + +bool BMP3XXI2CComponent::read_byte(uint8_t a_register, uint8_t *data) { + return I2CDevice::read_byte(a_register, data); +}; +bool BMP3XXI2CComponent::write_byte(uint8_t a_register, uint8_t data) { + return I2CDevice::write_byte(a_register, data); +}; +bool BMP3XXI2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::read_bytes(a_register, data, len); +}; +bool BMP3XXI2CComponent::write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::write_bytes(a_register, data, len); +}; + +void BMP3XXI2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BMP3XXComponent::dump_config(); +} + +} // namespace bmp3xx_i2c +} // namespace esphome diff --git a/esphome/components/bmp3xx_i2c/bmp3xx_i2c.h b/esphome/components/bmp3xx_i2c/bmp3xx_i2c.h new file mode 100644 index 0000000000..d8b95cf843 --- /dev/null +++ b/esphome/components/bmp3xx_i2c/bmp3xx_i2c.h @@ -0,0 +1,17 @@ +#pragma once +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/bmp3xx_base/bmp3xx_base.h" + +namespace esphome { +namespace bmp3xx_i2c { + +class BMP3XXI2CComponent : public bmp3xx_base::BMP3XXComponent, public i2c::I2CDevice { + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + void dump_config() override; +}; + +} // namespace bmp3xx_i2c +} // namespace esphome diff --git a/esphome/components/bmp3xx_i2c/sensor.py b/esphome/components/bmp3xx_i2c/sensor.py new file mode 100644 index 0000000000..ae59d29e89 --- /dev/null +++ b/esphome/components/bmp3xx_i2c/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +from esphome.components import i2c +from ..bmp3xx_base import to_code_base, cv, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["bmp3xx_base"] +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +bmp3xx_ns = cg.esphome_ns.namespace("bmp3xx_i2c") + +BMP3XXI2CComponent = bmp3xx_ns.class_( + "BMP3XXI2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(BMP3XXI2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bmp3xx_spi/__init__.py b/esphome/components/bmp3xx_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp3xx_spi/bmp3xx_spi.cpp b/esphome/components/bmp3xx_spi/bmp3xx_spi.cpp new file mode 100644 index 0000000000..2084530125 --- /dev/null +++ b/esphome/components/bmp3xx_spi/bmp3xx_spi.cpp @@ -0,0 +1,57 @@ +#include "bmp3xx_spi.h" +#include + +namespace esphome { +namespace bmp3xx_spi { + +static const char *const TAG = "bmp3xx_spi.sensor"; + +uint8_t set_bit(uint8_t num, int position) { + int mask = 1 << position; + return num | mask; +} + +uint8_t clear_bit(uint8_t num, int position) { + int mask = 1 << position; + return num & ~mask; +} + +void BMP3XXSPIComponent::setup() { + this->spi_setup(); + BMP3XXComponent::setup(); +} + +bool BMP3XXSPIComponent::read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool BMP3XXSPIComponent::write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(clear_bit(a_register, 7)); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool BMP3XXSPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + this->read_array(data, len); + this->disable(); + return true; +} + +bool BMP3XXSPIComponent::write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(clear_bit(a_register, 7)); + this->transfer_array(data, len); + this->disable(); + return true; +} + +} // namespace bmp3xx_spi +} // namespace esphome diff --git a/esphome/components/bmp3xx_spi/bmp3xx_spi.h b/esphome/components/bmp3xx_spi/bmp3xx_spi.h new file mode 100644 index 0000000000..2183994abe --- /dev/null +++ b/esphome/components/bmp3xx_spi/bmp3xx_spi.h @@ -0,0 +1,19 @@ +#pragma once +#include "esphome/components/bmp3xx_base/bmp3xx_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace bmp3xx_spi { + +class BMP3XXSPIComponent : public bmp3xx_base::BMP3XXComponent, + public spi::SPIDevice { + void setup() override; + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; +}; + +} // namespace bmp3xx_spi +} // namespace esphome diff --git a/esphome/components/bmp3xx_spi/sensor.py b/esphome/components/bmp3xx_spi/sensor.py new file mode 100644 index 0000000000..3d1acd3c1b --- /dev/null +++ b/esphome/components/bmp3xx_spi/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +from esphome.components import spi +from ..bmp3xx_base import to_code_base, cv, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["bmp3xx_base"] +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["spi"] + +bmp3xx_ns = cg.esphome_ns.namespace("bmp3xx_spi") + +BMP3XXSPIComponent = bmp3xx_ns.class_( + "BMP3XXSPIComponent", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend( + {cv.GenerateID(): cv.declare_id(BMP3XXSPIComponent)} +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/bmp581/__init__.py b/esphome/components/bmp581/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp581/bmp581.cpp b/esphome/components/bmp581/bmp581.cpp new file mode 100644 index 0000000000..0308da0bcb --- /dev/null +++ b/esphome/components/bmp581/bmp581.cpp @@ -0,0 +1,596 @@ +/* + * Adds support for Bosch's BMP581 high accuracy pressure and temperature sensor + * - Component structure based on ESPHome's BMP3XX component (as of March, 2023) + * - Implementation is easier as the sensor itself automatically compensates pressure for the temperature + * - Temperature and pressure data is converted via simple divison operations in this component + * - IIR filter level can independently be applied to temperature and pressure measurements + * - Bosch's BMP5-Sensor-API was consulted to verify that sensor configuration is done correctly + * - Copyright (c) 2022 Bosch Sensortec Gmbh, SPDX-License-Identifier: BSD-3-Clause + * - This component uses forced power mode only so measurements are synchronized by the host + * - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4) + */ + +#include "bmp581.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace bmp581 { + +static const char *const TAG = "bmp581"; + +static const LogString *oversampling_to_str(Oversampling oversampling) { + switch (oversampling) { + case Oversampling::OVERSAMPLING_NONE: + return LOG_STR("None"); + case Oversampling::OVERSAMPLING_X2: + return LOG_STR("2x"); + case Oversampling::OVERSAMPLING_X4: + return LOG_STR("4x"); + case Oversampling::OVERSAMPLING_X8: + return LOG_STR("8x"); + case Oversampling::OVERSAMPLING_X16: + return LOG_STR("16x"); + case Oversampling::OVERSAMPLING_X32: + return LOG_STR("32x"); + case Oversampling::OVERSAMPLING_X64: + return LOG_STR("64x"); + case Oversampling::OVERSAMPLING_X128: + return LOG_STR("128x"); + default: + return LOG_STR(""); + } +} + +static const LogString *iir_filter_to_str(IIRFilter filter) { + switch (filter) { + case IIRFilter::IIR_FILTER_OFF: + return LOG_STR("OFF"); + case IIRFilter::IIR_FILTER_2: + return LOG_STR("2x"); + case IIRFilter::IIR_FILTER_4: + return LOG_STR("4x"); + case IIRFilter::IIR_FILTER_8: + return LOG_STR("8x"); + case IIRFilter::IIR_FILTER_16: + return LOG_STR("16x"); + case IIRFilter::IIR_FILTER_32: + return LOG_STR("32x"); + case IIRFilter::IIR_FILTER_64: + return LOG_STR("64x"); + case IIRFilter::IIR_FILTER_128: + return LOG_STR("128x"); + default: + return LOG_STR(""); + } +} + +void BMP581Component::dump_config() { + ESP_LOGCONFIG(TAG, "BMP581:"); + + switch (this->error_code_) { + case NONE: + break; + case ERROR_COMMUNICATION_FAILED: + ESP_LOGE(TAG, " Communication with BMP581 failed!"); + break; + case ERROR_WRONG_CHIP_ID: + ESP_LOGE(TAG, " BMP581 has wrong chip ID - please verify you are using a BMP 581"); + break; + case ERROR_SENSOR_RESET: + ESP_LOGE(TAG, " BMP581 failed to reset"); + break; + case ERROR_SENSOR_STATUS: + ESP_LOGE(TAG, " BMP581 sensor status failed, there were NVM problems"); + break; + case ERROR_PRIME_IIR_FAILED: + ESP_LOGE(TAG, " BMP581's IIR Filter failed to prime with an initial measurement"); + break; + default: + ESP_LOGE(TAG, " BMP581 error code %d", (int) this->error_code_); + break; + } + + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + + ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_); + + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_temperature_level_))); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_))); + } + + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_pressure_level_))); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); + } +} + +void BMP581Component::setup() { + /* + * Setup goes through several stages, which follows the post-power-up procedure (page 18 of datasheet) and then sets + * configured options + * 1) Soft reboot + * 2) Verify ASIC chip ID matches BMP581 + * 3) Verify sensor status (check if NVM is okay) + * 4) Enable data ready interrupt + * 5) Write oversampling settings and set internal configuration values + * 6) Configure and prime IIR Filter(s), if enabled + */ + + this->error_code_ = NONE; + ESP_LOGCONFIG(TAG, "Setting up BMP581..."); + + //////////////////// + // 1) Soft reboot // + //////////////////// + + // Power-On-Reboot bit is asserted if sensor successfully reset + if (!this->reset_()) { + ESP_LOGE(TAG, "BMP581 failed to reset"); + + this->error_code_ = ERROR_SENSOR_RESET; + this->mark_failed(); + + return; + } + + /////////////////////////////////////////// + // 2) Verify ASIC chip ID matches BMP581 // + /////////////////////////////////////////// + + uint8_t chip_id; + + // read chip id from sensor + if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) { + ESP_LOGE(TAG, "Failed to read chip id"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + // verify id + if (chip_id != BMP581_ASIC_ID) { + ESP_LOGE(TAG, "Unknown chip ID, is this a BMP581?"); + + this->error_code_ = ERROR_WRONG_CHIP_ID; + this->mark_failed(); + + return; + } + + //////////////////////////////////////////////////// + // 3) Verify sensor status (check if NVM is okay) // + //////////////////////////////////////////////////// + + if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) { + ESP_LOGE(TAG, "Failed to read status register"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + // verify status_nvm_rdy bit (it is asserted if boot was successful) + if (!(this->status_.bit.status_nvm_rdy)) { + ESP_LOGE(TAG, "NVM not ready after boot"); + + this->error_code_ = ERROR_SENSOR_STATUS; + this->mark_failed(); + + return; + } + + // verify status_nvm_err bit (it is asserted if an error is detected) + if (this->status_.bit.status_nvm_err) { + ESP_LOGE(TAG, "NVM error detected on boot"); + + this->error_code_ = ERROR_SENSOR_STATUS; + this->mark_failed(); + + return; + } + + //////////////////////////////////// + // 4) Enable data ready interrupt // + //////////////////////////////////// + + // enable the data ready interrupt source + if (!this->write_interrupt_source_settings_(true)) { + ESP_LOGE(TAG, "Failed to write interrupt source register"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + ////////////////////////////////////////////////////////////////////////// + // 5) Write oversampling settings and set internal configuration values // + ////////////////////////////////////////////////////////////////////////// + + // configure pressure readings, if sensor is defined + // otherwise, disable pressure oversampling + if (this->pressure_sensor_) { + this->osr_config_.bit.press_en = true; + } else { + this->pressure_oversampling_ = OVERSAMPLING_NONE; + } + + // write oversampling settings + if (!this->write_oversampling_settings_(this->temperature_oversampling_, this->pressure_oversampling_)) { + ESP_LOGE(TAG, "Failed to write oversampling register"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + // set output data rate to 4 Hz=0x19 (page 65 of datasheet) + // - ?shouldn't? matter as this component only uses FORCED_MODE - datasheet is ambiguous + // - If in NORMAL_MODE or NONSTOP_MODE, then this would still allow deep standby to save power + // - will be written to BMP581 at next requested measurement + this->odr_config_.bit.odr = 0x19; + + /////////////////////////////////////////////////////// + /// 6) Configure and prime IIR Filter(s), if enabled // + /////////////////////////////////////////////////////// + + if ((this->iir_temperature_level_ != IIR_FILTER_OFF) || (this->iir_pressure_level_ != IIR_FILTER_OFF)) { + if (!this->write_iir_settings_(this->iir_temperature_level_, this->iir_pressure_level_)) { + ESP_LOGE(TAG, "Failed to write IIR configuration registers"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + if (!this->prime_iir_filter_()) { + ESP_LOGE(TAG, "Failed to prime the IIR filter with an intiial measurement"); + + this->error_code_ = ERROR_PRIME_IIR_FAILED; + this->mark_failed(); + + return; + } + } +} + +void BMP581Component::update() { + /* + * Each update goes through several stages + * 0) Verify either a temperature or pressure sensor is defined before proceeding + * 1) Request a measurement + * 2) Wait for measurement to finish (based on oversampling rates) + * 3) Read data registers for temperature and pressure, if applicable + * 4) Publish measurements to sensor(s), if applicable + */ + + //////////////////////////////////////////////////////////////////////////////////// + // 0) Verify either a temperature or pressure sensor is defined before proceeding // + //////////////////////////////////////////////////////////////////////////////////// + + if ((!this->temperature_sensor_) && (!this->pressure_sensor_)) { + return; + } + + ////////////////////////////// + // 1) Request a measurement // + ////////////////////////////// + + ESP_LOGVV(TAG, "Requesting a measurement from sensor"); + + if (!this->start_measurement_()) { + ESP_LOGW(TAG, "Failed to request forced measurement of sensor"); + this->status_set_warning(); + + return; + } + + ////////////////////////////////////////////////////////////////////// + // 2) Wait for measurement to finish (based on oversampling rates) // + ////////////////////////////////////////////////////////////////////// + + ESP_LOGVV(TAG, "Measurement is expected to take %d ms to complete", this->conversion_time_); + + this->set_timeout("measurement", this->conversion_time_, [this]() { + float temperature = 0.0; + float pressure = 0.0; + + //////////////////////////////////////////////////////////////////////// + // 3) Read data registers for temperature and pressure, if applicable // + //////////////////////////////////////////////////////////////////////// + + if (this->pressure_sensor_) { + if (!this->read_temperature_and_pressure_(temperature, pressure)) { + ESP_LOGW(TAG, "Failed to read temperature and pressure measurements, skipping update"); + this->status_set_warning(); + + return; + } + } else { + if (!this->read_temperature_(temperature)) { + ESP_LOGW(TAG, "Failed to read temperature measurement, skipping update"); + this->status_set_warning(); + + return; + } + } + + ///////////////////////////////////////////////////////// + // 4) Publish measurements to sensor(s), if applicable // + ///////////////////////////////////////////////////////// + + if (this->temperature_sensor_) { + this->temperature_sensor_->publish_state(temperature); + } + + if (this->pressure_sensor_) { + this->pressure_sensor_->publish_state(pressure); + } + + this->status_clear_warning(); + }); +} + +bool BMP581Component::check_data_readiness_() { + // - verifies component is not internally in standby mode + // - reads interrupt status register + // - checks if data ready bit is asserted + // - If true, then internally sets component to standby mode if in forced mode + // - returns data readiness state + + if (this->odr_config_.bit.pwr_mode == STANDBY_MODE) { + ESP_LOGD(TAG, "Data is not ready, sensor is in standby mode"); + return false; + } + + uint8_t status; + + if (!this->read_byte(BMP581_INT_STATUS, &status)) { + ESP_LOGE(TAG, "Failed to read interrupt status register"); + return false; + } + + this->int_status_.reg = status; + + if (this->int_status_.bit.drdy_data_reg) { + // If in forced mode, then set internal record of the power mode to STANDBY_MODE + // - sensor automatically returns to standby mode after completing a forced measurement + if (this->odr_config_.bit.pwr_mode == FORCED_MODE) { + this->odr_config_.bit.pwr_mode = STANDBY_MODE; + } + + return true; + } + + return false; +} + +bool BMP581Component::prime_iir_filter_() { + // - temporarily disables oversampling for a fast initial measurement; avoids slowing down ESPHome's startup process + // - enables IIR filter flushing with forced measurements + // - forces a measurement; flushing the IIR filter and priming it with a current value + // - disables IIR filter flushing with forced measurements + // - reverts to internally configured oversampling rates + // - returns success of all register writes/priming + + // store current internal oversampling settings to revert to after priming + Oversampling current_temperature_oversampling = (Oversampling) this->osr_config_.bit.osr_t; + Oversampling current_pressure_oversampling = (Oversampling) this->osr_config_.bit.osr_p; + + // temporarily disables oversampling for temperature and pressure for a fast priming measurement + if (!this->write_oversampling_settings_(OVERSAMPLING_NONE, OVERSAMPLING_NONE)) { + ESP_LOGE(TAG, "Failed to write oversampling register"); + + return false; + } + + // flush the IIR filter with forced measurements (we will only flush once) + this->dsp_config_.bit.iir_flush_forced_en = true; + if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) { + ESP_LOGE(TAG, "Failed to write IIR source register"); + + return false; + } + + // forces an intial measurement + // - this measurements flushes the IIR filter reflecting written DSP settings + // - flushing with this initial reading avoids having the internal previous data aquisition being 0, which + // (I)nfinitely affects future values + if (!this->start_measurement_()) { + ESP_LOGE(TAG, "Failed to request a forced measurement"); + + return false; + } + + // wait for priming measurement to complete + // - with oversampling disabled, the conversion time for a single measurement for pressure and temperature is + // ceilf(1.05*(1.0+1.0)) = 3ms + // - see page 12 of datasheet for details + delay(3); + + if (!this->check_data_readiness_()) { + ESP_LOGE(TAG, "IIR priming measurement was not ready"); + + return false; + } + + // disable IIR filter flushings on future forced measurements + this->dsp_config_.bit.iir_flush_forced_en = false; + if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) { + ESP_LOGE(TAG, "Failed to write IIR source register"); + + return false; + } + + // revert oversampling rates to original settings + return this->write_oversampling_settings_(current_temperature_oversampling, current_pressure_oversampling); +} + +bool BMP581Component::read_temperature_(float &temperature) { + // - verifies data is ready to be read + // - reads in 3 bytes of temperature data + // - returns whether successful, where the the variable parameter contains + // - the measured temperature (in degrees Celsius) + + if (!this->check_data_readiness_()) { + ESP_LOGW(TAG, "Data from sensor isn't ready, skipping this update"); + this->status_set_warning(); + + return false; + } + + uint8_t data[3]; + if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) { + ESP_LOGW(TAG, "Failed to read sensor's measurement data"); + this->status_set_warning(); + + return false; + } + + // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] + int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) + + return true; +} + +bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &pressure) { + // - verifies data is ready to be read + // - reads in 6 bytes of temperature data (3 for temeperature, 3 for pressure) + // - returns whether successful, where the variable parameters contain + // - the measured temperature (in degrees Celsius) + // - the measured pressure (in Pa) + + if (!this->check_data_readiness_()) { + ESP_LOGW(TAG, "Data from sensor isn't ready, skipping this update"); + this->status_set_warning(); + + return false; + } + + uint8_t data[6]; + if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) { + ESP_LOGW(TAG, "Failed to read sensor's measurement data"); + this->status_set_warning(); + + return false; + } + + // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] + int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) + + // pressure MSB is in data[5], LSB is in data[4], XLSB in data[3] + int32_t raw_press = (int32_t) data[5] << 16 | (int32_t) data[4] << 8 | (int32_t) data[3]; + pressure = (float) (raw_press / 64.0); // Divide by 2^6=64 for Pa (page 22 of datasheet) + + return true; +} + +bool BMP581Component::reset_() { + // - writes reset command to the command register + // - waits for sensor to complete reset + // - returns the Power-On-Reboot interrupt status, which is asserted if successful + + // writes reset command to BMP's command register + if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) { + ESP_LOGE(TAG, "Failed to write reset command"); + + return false; + } + + // t_{soft_res} = 2ms (page 11 of datasheet); time it takes to enter standby mode + // - round up to 3 ms + delay(3); + + // read interrupt status register + if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) { + ESP_LOGE(TAG, "Failed to read interrupt status register"); + + return false; + } + + // Power-On-Reboot bit is asserted if sensor successfully reset + return this->int_status_.bit.por; +} + +bool BMP581Component::start_measurement_() { + // - only pushes the sensor into FORCED_MODE for a reading if already in STANDBY_MODE + // - returns whether a measurement is in progress or has been initiated + + if (this->odr_config_.bit.pwr_mode == STANDBY_MODE) { + return this->write_power_mode_(FORCED_MODE); + } else { + return true; + } +} + +bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter pressure_iir) { + // - ensures data registers store filtered values + // - sets IIR filter levels on sensor + // - matches other default settings on sensor + // - writes configuration to the two relevant registers + // - returns success or failure of write to the registers + + // If the temperature/pressure IIR filter is configured, then ensure data registers store the filtered measurement + this->dsp_config_.bit.shdw_sel_iir_t = (temperature_iir != IIR_FILTER_OFF); + this->dsp_config_.bit.shdw_sel_iir_p = (pressure_iir != IIR_FILTER_OFF); + + // set temperature and pressure IIR filter level to configured values + this->iir_config_.bit.set_iir_t = temperature_iir; + this->iir_config_.bit.set_iir_p = pressure_iir; + + // enable pressure and temperature compensation (page 61 of datasheet) + // - ?only relevant if IIR filter is applied?; the datasheet is ambiguous + // - matches BMP's default setting + this->dsp_config_.bit.comp_pt_en = 0x3; + + // BMP581_DSP register and BMP581_DSP_IIR registers are successive + // - allows us to write the IIR configuration with one command to both registers + uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg}; + return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data)); +} + +bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) { + // - updates component's internal setting + // - returns success or failure of write to interrupt source register + + this->int_source_.bit.drdy_data_reg_en = data_ready_enable; + + // write interrupt source register + return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg); +} + +bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling, + Oversampling pressure_oversampling) { + // - updates component's internal setting + // - returns success or failure of write to Over-Sampling Rate register + + this->osr_config_.bit.osr_t = temperature_oversampling; + this->osr_config_.bit.osr_p = pressure_oversampling; + + return this->write_byte(BMP581_OSR, this->osr_config_.reg); +} + +bool BMP581Component::write_power_mode_(OperationMode mode) { + // - updates the component's internal power mode + // - returns success or failure of write to Output Data Rate register + + this->odr_config_.bit.pwr_mode = mode; + + // write odr register + return this->write_byte(BMP581_ODR, this->odr_config_.reg); +} + +} // namespace bmp581 +} // namespace esphome diff --git a/esphome/components/bmp581/bmp581.h b/esphome/components/bmp581/bmp581.h new file mode 100644 index 0000000000..7327be44ae --- /dev/null +++ b/esphome/components/bmp581/bmp581.h @@ -0,0 +1,222 @@ +// All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4) + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bmp581 { + +static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet) +static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command + +// BMP581 Register Addresses +enum { + BMP581_CHIP_ID = 0x01, // read chip ID + BMP581_INT_SOURCE = 0x15, // write interrupt sources + BMP581_MEASUREMENT_DATA = + 0x1D, // read measurement registers, 0x1D-0x1F are temperature XLSB to MSB and 0x20-0x22 are pressure XLSB to MSB + BMP581_INT_STATUS = 0x27, // read interrupt statuses + BMP581_STATUS = 0x28, // read sensor status + BMP581_DSP = 0x30, // write sensor configuration + BMP581_DSP_IIR = 0x31, // write IIR filter configuration + BMP581_OSR = 0x36, // write oversampling configuration + BMP581_ODR = 0x37, // write data rate and power mode configuration + BMP581_COMMAND = 0x7E // write sensor command +}; + +// BMP581 Power mode operations +enum OperationMode { + STANDBY_MODE = 0x0, // no active readings + NORMAL_MODE = 0x1, // read continuously at ODR configured rate and standby between + FORCED_MODE = 0x2, // read sensor once (only reading mode used by this component) + NONSTOP_MODE = 0x3 // read continuously with no standby +}; + +// Temperature and pressure sensors can be oversampled to reduce noise +enum Oversampling { + OVERSAMPLING_NONE = 0x0, + OVERSAMPLING_X2 = 0x1, + OVERSAMPLING_X4 = 0x2, + OVERSAMPLING_X8 = 0x3, + OVERSAMPLING_X16 = 0x4, + OVERSAMPLING_X32 = 0x5, + OVERSAMPLING_X64 = 0x6, + OVERSAMPLING_X128 = 0x7 +}; + +// Infinite Impulse Response filter reduces noise caused by ambient disturbances +enum IIRFilter { + IIR_FILTER_OFF = 0x0, + IIR_FILTER_2 = 0x1, + IIR_FILTER_4 = 0x2, + IIR_FILTER_8 = 0x3, + IIR_FILTER_16 = 0x4, + IIR_FILTER_32 = 0x5, + IIR_FILTER_64 = 0x6, + IIR_FILTER_128 = 0x7 +}; + +class BMP581Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + + void dump_config() override; + + void setup() override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } + + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + } + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + } + + void set_temperature_iir_filter_config(IIRFilter iir_temperature_level) { + this->iir_temperature_level_ = iir_temperature_level; + } + void set_pressure_iir_filter_config(IIRFilter iir_pressure_level) { this->iir_pressure_level_ = iir_pressure_level; } + + void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; } + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + + Oversampling temperature_oversampling_; + Oversampling pressure_oversampling_; + + IIRFilter iir_temperature_level_; + IIRFilter iir_pressure_level_; + + // Stores the sensors conversion time needed for a measurement based on oversampling settings and datasheet (page 12) + // Computed in Python during codegen + uint8_t conversion_time_; + + // Checks if the BMP581 has measurement data ready by checking the sensor's interrupts + bool check_data_readiness_(); + + // Flushes the IIR filter and primes an initial reading + bool prime_iir_filter_(); + + // Reads temperature data from sensor and converts data to measurement in degrees Celsius + bool read_temperature_(float &temperature); + // Reads temperature and pressure data from sensor and converts data to measurements in degrees Celsius and Pa + bool read_temperature_and_pressure_(float &temperature, float &pressure); + + // Soft resets the BMP581 + bool reset_(); + + // Initiates a measurement on sensor by switching to FORCED_MODE + bool start_measurement_(); + + // Writes the IIR filter configuration to the DSP and DSP_IIR registers + bool write_iir_settings_(IIRFilter temperature_iir, IIRFilter pressure_iir); + + // Writes whether to enable the data ready interrupt to the interrupt source register + bool write_interrupt_source_settings_(bool data_ready_enable); + + // Writes the oversampling settings to the OSR register + bool write_oversampling_settings_(Oversampling temperature_oversampling, Oversampling pressure_oversampling); + + // Sets the power mode on the BMP581 by writing to the ODR register + bool write_power_mode_(OperationMode mode); + + enum ErrorCode { + NONE = 0, + ERROR_COMMUNICATION_FAILED, + ERROR_WRONG_CHIP_ID, + ERROR_SENSOR_STATUS, + ERROR_SENSOR_RESET, + ERROR_PRIME_IIR_FAILED + } error_code_{NONE}; + + // BMP581's interrupt source register (address 0x15) to configure which interrupts are enabled (page 54 of datasheet) + union { + struct { + uint8_t drdy_data_reg_en : 1; // Data ready interrupt enable + uint8_t fifo_full_en : 1; // FIFO full interrupt enable + uint8_t fifo_ths_en : 1; // FIFO threshold/watermark interrupt enable + uint8_t oor_p_en : 1; // Pressure data out-of-range interrupt enable + } bit; + uint8_t reg; + } int_source_ = {.reg = 0}; + + // BMP581's interrupt status register (address 0x27) to determine ensor's current state (page 58 of datasheet) + union { + struct { + uint8_t drdy_data_reg : 1; // Data ready + uint8_t fifo_full : 1; // FIFO full + uint8_t fifo_ths : 1; // FIFO fhreshold/watermark + uint8_t oor_p : 1; // Pressure data out-of-range + uint8_t por : 1; // Power-On-Reset complete + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; + + // BMP581's status register (address 0x28) to determine if sensor has setup correctly (page 58 of datasheet) + union { + struct { + uint8_t status_core_rdy : 1; + uint8_t status_nvm_rdy : 1; // asserted if NVM is ready of operations + uint8_t status_nvm_err : 1; // asserted if NVM error + uint8_t status_nvm_cmd_err : 1; // asserted if boot command error + uint8_t status_boot_err_corrected : 1; // asserted if a boot error has been corrected + uint8_t : 2; + uint8_t st_crack_pass : 1; // asserted if crack check has executed without detecting a crack + } bit; + uint8_t reg; + } status_ = {.reg = 0}; + + // BMP581's dsp register (address 0x30) to configure data registers iir selection (page 61 of datasheet) + union { + struct { + uint8_t comp_pt_en : 2; // enable temperature and pressure compensation + uint8_t iir_flush_forced_en : 1; // IIR filter is flushed in forced mode + uint8_t shdw_sel_iir_t : 1; // temperature data register value selected before or after iir + uint8_t fifo_sel_iir_t : 1; // FIFO temperature data register value secected before or after iir + uint8_t shdw_sel_iir_p : 1; // pressure data register value selected before or after iir + uint8_t fifo_sel_iir_p : 1; // FIFO pressure data register value selected before or after iir + uint8_t oor_sel_iir_p : 1; // pressure out-of-range value selected before or after iir + } bit; + uint8_t reg; + } dsp_config_ = {.reg = 0}; + + // BMP581's iir register (address 0x31) to configure iir filtering(page 62 of datasheet) + union { + struct { + uint8_t set_iir_t : 3; // Temperature IIR filter coefficient + uint8_t set_iir_p : 3; // Pressure IIR filter coefficient + } bit; + uint8_t reg; + } iir_config_ = {.reg = 0}; + + // BMP581's OSR register (address 0x36) to configure Over-Sampling Rates (page 64 of datasheet) + union { + struct { + uint8_t osr_t : 3; // Temperature oversampling + uint8_t osr_p : 3; // Pressure oversampling + uint8_t press_en : 1; // Enables pressure measurement + } bit; + uint8_t reg; + } osr_config_ = {.reg = 0}; + + // BMP581's odr register (address 0x37) to configure output data rate and power mode (page 64 of datasheet) + union { + struct { + uint8_t pwr_mode : 2; // power mode of sensor + uint8_t odr : 5; // output data rate + uint8_t deep_dis : 1; // deep standby disabled if asserted + } bit; + uint8_t reg; + } odr_config_ = {.reg = 0}; +}; + +} // namespace bmp581 +} // namespace esphome diff --git a/esphome/components/bmp581/sensor.py b/esphome/components/bmp581/sensor.py new file mode 100644 index 0000000000..1e0350075a --- /dev/null +++ b/esphome/components/bmp581/sensor.py @@ -0,0 +1,163 @@ +import math +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PASCAL, +) + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["i2c"] + +bmp581_ns = cg.esphome_ns.namespace("bmp581") + +Oversampling = bmp581_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32X": Oversampling.OVERSAMPLING_X32, + "64X": Oversampling.OVERSAMPLING_X64, + "128X": Oversampling.OVERSAMPLING_X128, +} + +IIRFilter = bmp581_ns.enum("IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": IIRFilter.IIR_FILTER_OFF, + "2X": IIRFilter.IIR_FILTER_2, + "4X": IIRFilter.IIR_FILTER_4, + "8X": IIRFilter.IIR_FILTER_8, + "16X": IIRFilter.IIR_FILTER_16, + "32X": IIRFilter.IIR_FILTER_32, + "64X": IIRFilter.IIR_FILTER_64, + "128X": IIRFilter.IIR_FILTER_128, +} + +BMP581Component = bmp581_ns.class_( + "BMP581Component", cg.PollingComponent, i2c.I2CDevice +) + + +def compute_measurement_conversion_time(config): + # - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet + # - returns a rounded up time in ms + + # Page 12 of datasheet + PRESSURE_OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 1.0, + "2X": 1.7, + "4X": 2.9, + "8X": 5.4, + "16X": 10.4, + "32X": 20.4, + "64X": 40.4, + "128X": 80.4, + } + + # Page 12 of datasheet + TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 1.0, + "2X": 1.1, + "4X": 1.5, + "8X": 2.1, + "16X": 3.3, + "32X": 5.8, + "64X": 10.8, + "128X": 20.8, + } + + pressure_conversion_time = ( + 0.0 # No conversion time necessary without a pressure sensor + ) + if pressure_config := config.get(CONF_PRESSURE): + pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[ + pressure_config.get(CONF_OVERSAMPLING) + ] + + temperature_conversion_time = ( + 1.0 # BMP581 always samples the temperature even if only reading pressure + ) + if temperature_config := config.get(CONF_TEMPERATURE): + temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[ + temperature_config.get(CONF_OVERSAMPLING) + ] + + # Datasheet indicates a 5% possible error in each conversion time listed + return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BMP581Component), + 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, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PASCAL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x46)) +) + + +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) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add( + var.set_temperature_oversampling_config( + temperature_config[CONF_OVERSAMPLING] + ) + ) + cg.add( + var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER]) + ) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING])) + cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER])) + + cg.add(var.set_conversion_time(compute_measurement_conversion_time(config))) diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index 5b9e4a5a2c..4b74cc85f5 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -12,6 +12,8 @@ static const uint8_t BP1658CJ_ADDR_START_3CH = 0x10; static const uint8_t BP1658CJ_ADDR_START_2CH = 0x20; static const uint8_t BP1658CJ_ADDR_START_5CH = 0x30; +static const uint8_t BP1658CJ_DELAY = 2; + void BP1658CJ::setup() { ESP_LOGCONFIG(TAG, "Setting up BP1658CJ Output Component..."); this->data_pin_->setup(); @@ -35,10 +37,14 @@ void BP1658CJ::loop() { uint8_t data[12]; if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { - // Off / Sleep - data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_STANDBY; for (int i = 1; i < 12; i++) data[i] = 0; + + // First turn all channels off + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_5CH; + this->write_buffer_(data, 12); + // Then sleep + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_STANDBY; this->write_buffer_(data, 12); } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { @@ -81,29 +87,44 @@ void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) { } this->pwm_amounts_[channel] = value; } + void BP1658CJ::write_bit_(bool value) { - this->clock_pin_->digital_write(false); this->data_pin_->digital_write(value); + delayMicroseconds(BP1658CJ_DELAY); this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); } void BP1658CJ::write_byte_(uint8_t data) { for (uint8_t mask = 0x80; mask; mask >>= 1) { this->write_bit_(data & mask); } - this->clock_pin_->digital_write(false); - this->data_pin_->digital_write(true); + + // ack bit + this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); + this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); } void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { this->data_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); + for (uint32_t i = 0; i < size; i++) { this->write_byte_(buffer[i]); } - this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); this->data_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); } } // namespace bp1658cj diff --git a/esphome/components/bp5758d/bp5758d.cpp b/esphome/components/bp5758d/bp5758d.cpp index 111fd6b68e..87c4165275 100644 --- a/esphome/components/bp5758d/bp5758d.cpp +++ b/esphome/components/bp5758d/bp5758d.cpp @@ -17,12 +17,16 @@ static const uint8_t BP5758D_ADDR_START_2CH = 0b00100000; static const uint8_t BP5758D_ADDR_START_5CH = 0b00110000; static const uint8_t BP5758D_ALL_DATA_CHANNEL_ENABLEMENT = 0b00011111; +static const uint8_t BP5758D_DELAY = 2; + void BP5758D::setup() { ESP_LOGCONFIG(TAG, "Setting up BP5758D Output Component..."); this->data_pin_->setup(); this->data_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); this->clock_pin_->setup(); this->clock_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); this->channel_current_.resize(5, 0); this->pwm_amounts_.resize(5, 0); } @@ -39,10 +43,14 @@ void BP5758D::loop() { uint8_t data[17]; if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { - // Off / Sleep - data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_STANDBY; - for (int i = 1; i < 16; i++) + for (int i = 1; i < 17; i++) data[i] = 0; + + // First turn all channels off + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_5CH; + this->write_buffer_(data, 17); + // Then sleep + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_STANDBY; this->write_buffer_(data, 17); } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { @@ -119,28 +127,42 @@ void BP5758D::set_channel_value_(uint8_t channel, uint16_t value) { void BP5758D::set_channel_current_(uint8_t channel, uint8_t current) { this->channel_current_[channel] = current; } void BP5758D::write_bit_(bool value) { - this->clock_pin_->digital_write(false); this->data_pin_->digital_write(value); + delayMicroseconds(BP5758D_DELAY); this->clock_pin_->digital_write(true); + delayMicroseconds(BP5758D_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); } void BP5758D::write_byte_(uint8_t data) { for (uint8_t mask = 0x80; mask; mask >>= 1) { this->write_bit_(data & mask); } - this->clock_pin_->digital_write(false); - this->data_pin_->digital_write(true); + + // ack bit + this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->clock_pin_->digital_write(true); + delayMicroseconds(BP5758D_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); + this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); } void BP5758D::write_buffer_(uint8_t *buffer, uint8_t size) { this->data_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); + for (uint32_t i = 0; i < size; i++) { this->write_byte_(buffer[i]); } - this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + delayMicroseconds(BP5758D_DELAY); this->data_pin_->digital_write(true); + delayMicroseconds(BP5758D_DELAY); } } // namespace bp5758d diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 55f2fe794a..366d0edf7d 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -1,29 +1,32 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, + CONF_MQTT_ID, CONF_ON_PRESS, CONF_TRIGGER_ID, - CONF_MQTT_ID, + CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_IDENTIFY, DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, + DEVICE_CLASS_IDENTIFY, DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ] @@ -41,16 +44,20 @@ ButtonPressTrigger = button_ns.class_( validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -BUTTON_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent), - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_ON_PRESS): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ButtonPressTrigger), - } - ), - } +BUTTON_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional(CONF_ON_PRESS): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ButtonPressTrigger), + } + ), + } + ) ) _UNDEF = object() @@ -83,13 +90,16 @@ async def setup_button_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) - if CONF_DEVICE_CLASS in config: - cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + if device_class := config.get(CONF_DEVICE_CLASS): + cg.add(var.set_device_class(device_class)) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if mqtt_id := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + async def register_button(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/bytebuffer/__init__.py b/esphome/components/bytebuffer/__init__.py new file mode 100644 index 0000000000..3c7c695118 --- /dev/null +++ b/esphome/components/bytebuffer/__init__.py @@ -0,0 +1,5 @@ +CODEOWNERS = ["@clydebarrow"] + +# Allows bytebuffer to be configured in yaml, to allow use of the C++ api. + +CONFIG_SCHEMA = {} diff --git a/esphome/components/bytebuffer/bytebuffer.h b/esphome/components/bytebuffer/bytebuffer.h new file mode 100644 index 0000000000..030484ce32 --- /dev/null +++ b/esphome/components/bytebuffer/bytebuffer.h @@ -0,0 +1,421 @@ +#pragma once + +#include +#include +#include +#include +#include "esphome/core/helpers.h" + +namespace esphome { +namespace bytebuffer { + +enum Endian { LITTLE, BIG }; + +/** + * A class modelled on the Java ByteBuffer class. It wraps a vector of bytes and permits putting and getting + * items of various sizes, with an automatically incremented position. + * + * There are three variables maintained pointing into the buffer: + * + * capacity: the maximum amount of data that can be stored - set on construction and cannot be changed + * limit: the limit of the data currently available to get or put + * position: the current insert or extract position + * + * 0 <= position <= limit <= capacity + * + * In addition a mark can be set to the current position with mark(). A subsequent call to reset() will restore + * the position to the mark. + * + * The buffer can be marked to be little-endian (default) or big-endian. All subsequent operations will use that order. + * + * The flip() operation will reset the position to 0 and limit to the current position. This is useful for reading + * data from a buffer after it has been written. + * + * The code is defined here in the header file rather than in a .cpp file, so that it does not get compiled if not used. + * The templated functions ensure that only those typed functions actually used are compiled. The functions + * are implicitly inline-able which will aid performance. + */ +class ByteBuffer { + public: + // Default constructor (compatibility with TEMPLATABLE_VALUE) + // Creates a zero-length ByteBuffer which is little use to anybody. + ByteBuffer() : ByteBuffer(std::vector()) {} + + /** + * Create a new Bytebuffer with the given capacity + */ + ByteBuffer(size_t capacity, Endian endianness = LITTLE) + : data_(std::vector(capacity)), endianness_(endianness), limit_(capacity){}; + + // templated functions to implement putting and getting data of various types. There are two flavours of all + // functions - one that uses the position as the offset, and updates the position accordingly, and one that + // takes an explicit offset and does not update the position. + // Separate temnplates are provided for types that fit into 32 bits and those that are bigger. These delegate + // the actual put/get to common code based around those sizes. + // This reduces the code size and execution time for smaller types. A similar structure for e.g. 16 bits is unlikely + // to provide any further benefit given that all target platforms are native 32 bit. + + template + T get(typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) { + // integral types that fit into 32 bit + return static_cast(this->get_uint32_(sizeof(T))); + } + + template + T get(size_t offset, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) { + return static_cast(this->get_uint32_(offset, sizeof(T))); + } + + template + void put(const T &value, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) { + this->put_uint32_(static_cast(value), sizeof(T)); + } + + template + void put(const T &value, size_t offset, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) { + this->put_uint32_(static_cast(value), offset, sizeof(T)); + } + + // integral types that do not fit into 32 bit (basically only 64 bit types) + template + T get(typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) { + return static_cast(this->get_uint64_(sizeof(T))); + } + + template + T get(size_t offset, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) { + return static_cast(this->get_uint64_(offset, sizeof(T))); + } + + template + void put(const T &value, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) { + this->put_uint64_(value, sizeof(T)); + } + + template + void put(const T &value, size_t offset, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) { + this->put_uint64_(static_cast(value), offset, sizeof(T)); + } + + // floating point types. Caters for 32 and 64 bit floating point. + template + T get(typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint32_t)), T>::type * = 0) { + return bit_cast(this->get_uint32_(sizeof(T))); + } + + template + T get(typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) { + return bit_cast(this->get_uint64_(sizeof(T))); + } + + template + T get(size_t offset, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint32_t)), T>::type * = 0) { + return bit_cast(this->get_uint32_(offset, sizeof(T))); + } + + template + T get(size_t offset, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) { + return bit_cast(this->get_uint64_(offset, sizeof(T))); + } + template + void put(const T &value, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) { + this->put_uint32_(bit_cast(value), sizeof(T)); + } + + template + void put(const T &value, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) { + this->put_uint64_(bit_cast(value), sizeof(T)); + } + + template + void put(const T &value, size_t offset, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) { + this->put_uint32_(bit_cast(value), offset, sizeof(T)); + } + + template + void put(const T &value, size_t offset, typename std::enable_if::value, T>::type * = 0, + typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) { + this->put_uint64_(bit_cast(value), offset, sizeof(T)); + } + + template static ByteBuffer wrap(T value, Endian endianness = LITTLE) { + ByteBuffer buffer = ByteBuffer(sizeof(T), endianness); + buffer.put(value); + buffer.flip(); + return buffer; + } + + static ByteBuffer wrap(std::vector const &data, Endian endianness = LITTLE) { + ByteBuffer buffer = {data}; + buffer.endianness_ = endianness; + return buffer; + } + + static ByteBuffer wrap(const uint8_t *ptr, size_t len, Endian endianness = LITTLE) { + return wrap(std::vector(ptr, ptr + len), endianness); + } + + // convenience functions with explicit types named.. + void put_float(float value) { this->put(value); } + void put_double(double value) { this->put(value); } + + uint8_t get_uint8() { return this->data_[this->position_++]; } + // Get a 16 bit unsigned value, increment by 2 + uint16_t get_uint16() { return this->get(); } + // Get a 24 bit unsigned value, increment by 3 + uint32_t get_uint24() { return this->get_uint32_(3); }; + // Get a 32 bit unsigned value, increment by 4 + uint32_t get_uint32() { return this->get(); }; + // Get a 64 bit unsigned value, increment by 8 + uint64_t get_uint64() { return this->get(); }; + // Signed versions of the get functions + uint8_t get_int8() { return static_cast(this->get_uint8()); }; + int16_t get_int16() { return this->get(); } + int32_t get_int32() { return this->get(); } + int64_t get_int64() { return this->get(); } + // Get a float value, increment by 4 + float get_float() { return this->get(); } + // Get a double value, increment by 8 + double get_double() { return this->get(); } + + // Get a bool value, increment by 1 + bool get_bool() { return static_cast(this->get_uint8()); } + + uint32_t get_int24(size_t offset) { + auto value = this->get_uint24(offset); + uint32_t mask = (~static_cast(0)) << 23; + if ((value & mask) != 0) + value |= mask; + return value; + } + + uint32_t get_int24() { + auto value = this->get_uint24(); + uint32_t mask = (~static_cast(0)) << 23; + if ((value & mask) != 0) + value |= mask; + return value; + } + std::vector get_vector(size_t length, size_t offset) { + auto start = this->data_.begin() + offset; + return {start, start + length}; + } + + std::vector get_vector(size_t length) { + auto result = this->get_vector(length, this->position_); + this->position_ += length; + return result; + } + + // Convenience named functions + void put_uint8(uint8_t value) { this->data_[this->position_++] = value; } + void put_uint16(uint16_t value) { this->put(value); } + void put_uint24(uint32_t value) { this->put_uint32_(value, 3); } + void put_uint32(uint32_t value) { this->put(value); } + void put_uint64(uint64_t value) { this->put(value); } + // Signed versions of the put functions + void put_int8(int8_t value) { this->put_uint8(static_cast(value)); } + void put_int16(int16_t value) { this->put(value); } + void put_int24(int32_t value) { this->put_uint32_(value, 3); } + void put_int32(int32_t value) { this->put(value); } + void put_int64(int64_t value) { this->put(value); } + // Extra put functions + void put_bool(bool value) { this->put_uint8(value); } + + // versions of the above with an offset, these do not update the position + + uint64_t get_uint64(size_t offset) { return this->get(offset); } + uint32_t get_uint24(size_t offset) { return this->get_uint32_(offset, 3); }; + double get_double(size_t offset) { return get(offset); } + + // Get one byte from the buffer, increment position by 1 + uint8_t get_uint8(size_t offset) { return this->data_[offset]; } + // Get a 16 bit unsigned value, increment by 2 + uint16_t get_uint16(size_t offset) { return get(offset); } + // Get a 24 bit unsigned value, increment by 3 + uint32_t get_uint32(size_t offset) { return this->get(offset); }; + // Get a 64 bit unsigned value, increment by 8 + uint8_t get_int8(size_t offset) { return get(offset); } + int16_t get_int16(size_t offset) { return get(offset); } + int32_t get_int32(size_t offset) { return get(offset); } + int64_t get_int64(size_t offset) { return get(offset); } + // Get a float value, increment by 4 + float get_float(size_t offset) { return get(offset); } + // Get a double value, increment by 8 + + // Get a bool value, increment by 1 + bool get_bool(size_t offset) { return this->get_uint8(offset); } + + void put_uint8(uint8_t value, size_t offset) { this->data_[offset] = value; } + void put_uint16(uint16_t value, size_t offset) { this->put(value, offset); } + void put_uint24(uint32_t value, size_t offset) { this->put(value, offset); } + void put_uint32(uint32_t value, size_t offset) { this->put(value, offset); } + void put_uint64(uint64_t value, size_t offset) { this->put(value, offset); } + // Signed versions of the put functions + void put_int8(int8_t value, size_t offset) { this->put_uint8(static_cast(value), offset); } + void put_int16(int16_t value, size_t offset) { this->put(value, offset); } + void put_int24(int32_t value, size_t offset) { this->put_uint32_(value, offset, 3); } + void put_int32(int32_t value, size_t offset) { this->put(value, offset); } + void put_int64(int64_t value, size_t offset) { this->put(value, offset); } + // Extra put functions + void put_float(float value, size_t offset) { this->put(value, offset); } + void put_double(double value, size_t offset) { this->put(value, offset); } + void put_bool(bool value, size_t offset) { this->put_uint8(value, offset); } + void put(const std::vector &value, size_t offset) { + std::copy(value.begin(), value.end(), this->data_.begin() + offset); + } + void put_vector(const std::vector &value, size_t offset) { this->put(value, offset); } + void put(const std::vector &value) { + this->put_vector(value, this->position_); + this->position_ += value.size(); + } + void put_vector(const std::vector &value) { this->put(value); } + + // Getters + + inline size_t get_capacity() const { return this->data_.size(); } + inline size_t get_position() const { return this->position_; } + inline size_t get_limit() const { return this->limit_; } + inline size_t get_remaining() const { return this->get_limit() - this->get_position(); } + inline Endian get_endianness() const { return this->endianness_; } + inline void mark() { this->mark_ = this->position_; } + inline void big_endian() { this->endianness_ = BIG; } + inline void little_endian() { this->endianness_ = LITTLE; } + // retrieve a pointer to the underlying data. + std::vector get_data() { return this->data_; }; + + void get_bytes(void *dest, size_t length) { + std::copy(this->data_.begin() + this->position_, this->data_.begin() + this->position_ + length, (uint8_t *) dest); + this->position_ += length; + } + + void get_bytes(void *dest, size_t length, size_t offset) { + std::copy(this->data_.begin() + offset, this->data_.begin() + offset + length, (uint8_t *) dest); + } + + void rewind() { this->position_ = 0; } + void reset() { this->position_ = this->mark_; } + + void set_limit(size_t limit) { this->limit_ = limit; } + void set_position(size_t position) { this->position_ = position; } + void clear() { + this->limit_ = this->get_capacity(); + this->position_ = 0; + } + void flip() { + this->limit_ = this->position_; + this->position_ = 0; + } + + protected: + uint64_t get_uint64_(size_t offset, size_t length) const { + uint64_t value = 0; + if (this->endianness_ == LITTLE) { + offset += length; + while (length-- != 0) { + value <<= 8; + value |= this->data_[--offset]; + } + } else { + while (length-- != 0) { + value <<= 8; + value |= this->data_[offset++]; + } + } + return value; + } + + uint64_t get_uint64_(size_t length) { + auto result = this->get_uint64_(this->position_, length); + this->position_ += length; + return result; + } + uint32_t get_uint32_(size_t offset, size_t length) const { + uint32_t value = 0; + if (this->endianness_ == LITTLE) { + offset += length; + while (length-- != 0) { + value <<= 8; + value |= this->data_[--offset]; + } + } else { + while (length-- != 0) { + value <<= 8; + value |= this->data_[offset++]; + } + } + return value; + } + + uint32_t get_uint32_(size_t length) { + auto result = this->get_uint32_(this->position_, length); + this->position_ += length; + return result; + } + + /// Putters + + void put_uint64_(uint64_t value, size_t length) { + this->put_uint64_(value, this->position_, length); + this->position_ += length; + } + void put_uint32_(uint32_t value, size_t length) { + this->put_uint32_(value, this->position_, length); + this->position_ += length; + } + + void put_uint64_(uint64_t value, size_t offset, size_t length) { + if (this->endianness_ == LITTLE) { + while (length-- != 0) { + this->data_[offset++] = static_cast(value); + value >>= 8; + } + } else { + offset += length; + while (length-- != 0) { + this->data_[--offset] = static_cast(value); + value >>= 8; + } + } + } + + void put_uint32_(uint32_t value, size_t offset, size_t length) { + if (this->endianness_ == LITTLE) { + while (length-- != 0) { + this->data_[offset++] = static_cast(value); + value >>= 8; + } + } else { + offset += length; + while (length-- != 0) { + this->data_[--offset] = static_cast(value); + value >>= 8; + } + } + } + ByteBuffer(std::vector const &data) : data_(data), limit_(data.size()) {} + + std::vector data_; + Endian endianness_{LITTLE}; + size_t position_{0}; + size_t mark_{0}; + size_t limit_{0}; +}; + +} // namespace bytebuffer +} // namespace esphome diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 1dbd743c75..76e77021ad 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -18,10 +18,10 @@ CONF_ON_FRAME = "on_frame" def validate_id(config): if CONF_CAN_ID in config: - id_value = config[CONF_CAN_ID] + can_id = config[CONF_CAN_ID] id_ext = config[CONF_USE_EXTENDED_ID] if not id_ext: - if id_value > 0x7FF: + if can_id > 0x7FF: raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") return config @@ -46,9 +46,13 @@ CanbusTrigger = canbus_ns.class_( CanSpeed = canbus_ns.enum("CAN_SPEED") CAN_SPEEDS = { + "1KBPS": CanSpeed.CAN_1KBPS, "5KBPS": CanSpeed.CAN_5KBPS, "10KBPS": CanSpeed.CAN_10KBPS, + "12K5BPS": CanSpeed.CAN_12K5BPS, + "16KBPS": CanSpeed.CAN_16KBPS, "20KBPS": CanSpeed.CAN_20KBPS, + "25KBPS": CanSpeed.CAN_25KBPS, "31K25BPS": CanSpeed.CAN_31K25BPS, "33KBPS": CanSpeed.CAN_33KBPS, "40KBPS": CanSpeed.CAN_40KBPS, @@ -61,9 +65,9 @@ CAN_SPEEDS = { "200KBPS": CanSpeed.CAN_200KBPS, "250KBPS": CanSpeed.CAN_250KBPS, "500KBPS": CanSpeed.CAN_500KBPS, + "800KBPS": CanSpeed.CAN_800KBPS, "1000KBPS": CanSpeed.CAN_1000KBPS, } - CANBUS_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(CanbusComponent), @@ -145,25 +149,21 @@ async def canbus_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_CANBUS_ID]) - if CONF_CAN_ID in config: - can_id = await cg.templatable(config[CONF_CAN_ID], args, cg.uint32) + if can_id := config.get(CONF_CAN_ID): + can_id = await cg.templatable(can_id, args, cg.uint32) cg.add(var.set_can_id(can_id)) - use_extended_id = await cg.templatable( - config[CONF_USE_EXTENDED_ID], args, cg.uint32 - ) - cg.add(var.set_use_extended_id(use_extended_id)) + cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID])) - remote_transmission_request = await cg.templatable( - config[CONF_REMOTE_TRANSMISSION_REQUEST], args, bool + cg.add( + var.set_remote_transmission_request(config[CONF_REMOTE_TRANSMISSION_REQUEST]) ) - cg.add(var.set_remote_transmission_request(remote_transmission_request)) data = config[CONF_DATA] - if isinstance(data, bytes): - data = [int(x) for x in 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: + if isinstance(data, bytes): + data = [int(x) for x in data] cg.add(var.set_data_static(data)) return var diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 3fe0d50f06..73b86cba87 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -16,9 +16,9 @@ void Canbus::setup() { void Canbus::dump_config() { if (this->use_extended_id_) { - ESP_LOGCONFIG(TAG, "config extended id=0x%08x", this->can_id_); + ESP_LOGCONFIG(TAG, "config extended id=0x%08" PRIx32, this->can_id_); } else { - ESP_LOGCONFIG(TAG, "config standard id=0x%03x", this->can_id_); + ESP_LOGCONFIG(TAG, "config standard id=0x%03" PRIx32, this->can_id_); } } @@ -28,9 +28,11 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm uint8_t size = static_cast(data.size()); if (use_extended_id) { - ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); + ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), + size); } else { - ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); + ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), + size); } if (size > CAN_MAX_DATA_LENGTH) size = CAN_MAX_DATA_LENGTH; @@ -49,9 +51,9 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm void Canbus::add_trigger(CanbusTrigger *trigger) { if (trigger->use_extended_id_) { - ESP_LOGVV(TAG, "add trigger for extended canid=0x%08x", trigger->can_id_); + ESP_LOGVV(TAG, "add trigger for extended canid=0x%08" PRIx32, trigger->can_id_); } else { - ESP_LOGVV(TAG, "add trigger for std canid=0x%03x", trigger->can_id_); + ESP_LOGVV(TAG, "add trigger for std canid=0x%03" PRIx32, trigger->can_id_); } this->triggers_.push_back(trigger); }; @@ -63,10 +65,10 @@ void Canbus::loop() { while (this->read_message(&can_message) == canbus::ERROR_OK) { message_counter++; if (can_message.use_extended_id) { - ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%x size=%d", message_counter, can_message.can_id, - can_message.can_data_length_code); + ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%" PRIx32 " size=%d", message_counter, + can_message.can_id, can_message.can_data_length_code); } else { - ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%x size=%d", message_counter, can_message.can_id, + ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%" PRIx32 " size=%d", message_counter, can_message.can_id, can_message.can_data_length_code); } diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 4a12742627..1e5214fef4 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -4,6 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/optional.h" +#include #include namespace esphome { @@ -19,9 +20,13 @@ enum Error : uint8_t { }; enum CanSpeed : uint8_t { + CAN_1KBPS, CAN_5KBPS, CAN_10KBPS, + CAN_12K5BPS, + CAN_16KBPS, CAN_20KBPS, + CAN_25KBPS, CAN_31K25BPS, CAN_33KBPS, CAN_40KBPS, @@ -34,6 +39,7 @@ enum CanSpeed : uint8_t { CAN_200KBPS, CAN_250KBPS, CAN_500KBPS, + CAN_800KBPS, CAN_1000KBPS }; diff --git a/esphome/components/cap1188/__init__.py b/esphome/components/cap1188/__init__.py index 80794c5146..f22e6d6c9e 100644 --- a/esphome/components/cap1188/__init__.py +++ b/esphome/components/cap1188/__init__.py @@ -9,7 +9,7 @@ CONF_ALLOW_MULTIPLE_TOUCHES = "allow_multiple_touches" DEPENDENCIES = ["i2c"] AUTO_LOAD = ["binary_sensor", "output"] -CODEOWNERS = ["@MrEditor97"] +CODEOWNERS = ["@mreditor97"] cap1188_ns = cg.esphome_ns.namespace("cap1188") CONF_CAP1188_ID = "cap1188_id" @@ -37,8 +37,8 @@ async def to_code(config): cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD])) cg.add(var.set_allow_multiple_touches(config[CONF_ALLOW_MULTIPLE_TOUCHES])) - if CONF_RESET_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + if reset_pin_config := config.get(CONF_RESET_PIN): + pin = await cg.gpio_pin_expression(reset_pin_config) cg.add(var.set_reset_pin(pin)) await cg.register_component(var, config) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index ff5266e84f..a90ea14c4e 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -2,7 +2,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID -from esphome.const import CONF_ID +from esphome.const import ( + CONF_ID, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_RTL87XX, +) from esphome.core import coroutine_with_priority, CORE AUTO_LOAD = ["web_server_base"] @@ -21,8 +27,7 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_arduino, - cv.only_on(["esp32", "esp8266"]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), ) @@ -34,8 +39,11 @@ async def to_code(config): await cg.register_component(var, config) cg.add_define("USE_CAPTIVE_PORTAL") - if CORE.is_esp32: - cg.add_library("DNSServer", None) - cg.add_library("WiFi", None) - if CORE.is_esp8266: - cg.add_library("DNSServer", None) + if CORE.using_arduino: + if CORE.is_esp32: + cg.add_library("DNSServer", None) + cg.add_library("WiFi", None) + if CORE.is_esp8266: + cg.add_library("DNSServer", None) + if CORE.is_libretiny: + cg.add_library("DNSServer", None) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index bf2e6e6e8b..8835762fb3 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -1,107 +1,109 @@ #pragma once // Generated from https://github.com/esphome/esphome-webserver -#include "esphome/core/hal.h" -namespace esphome { +#include "esphome/core/hal.h" + +namespace esphome { namespace captive_portal { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xdd, 0x58, 0x09, 0x6f, 0xdc, 0x36, 0x16, 0xfe, 0x2b, - 0xac, 0x92, 0x74, 0x34, 0x8d, 0xc5, 0xd1, 0x31, 0x97, 0x35, 0xd2, 0x14, 0x89, 0x37, 0x45, 0x0b, 0x24, 0x69, 0x00, - 0xbb, 0x5d, 0x14, 0x69, 0x00, 0x73, 0x24, 0x6a, 0xc4, 0x58, 0xa2, 0x54, 0x91, 0x9a, 0x23, 0x83, 0xd9, 0xdf, 0xde, - 0x47, 0x52, 0x73, 0x38, 0x6b, 0x2f, 0x90, 0x62, 0x8b, 0xa2, 0x4d, 0x6c, 0x9a, 0xc7, 0x3b, 0x3f, 0xf2, 0xf1, 0x3d, - 0x2a, 0xfa, 0x2a, 0xad, 0x12, 0xb9, 0xad, 0x29, 0xca, 0x65, 0x59, 0xcc, 0x23, 0xd5, 0xa2, 0x82, 0xf0, 0x65, 0x4c, - 0x39, 0x8c, 0x28, 0x49, 0xe7, 0x51, 0x49, 0x25, 0x41, 0x49, 0x4e, 0x1a, 0x41, 0x65, 0xfc, 0xd3, 0xcd, 0x77, 0xce, - 0x14, 0x0d, 0xe6, 0x51, 0xc1, 0xf8, 0x1d, 0x6a, 0x68, 0x11, 0xb3, 0xa4, 0xe2, 0x28, 0x6f, 0x68, 0x16, 0xa7, 0x44, - 0x92, 0x90, 0x95, 0x64, 0x49, 0x15, 0x81, 0x66, 0xe3, 0xa4, 0xa4, 0xf1, 0x8a, 0xd1, 0x75, 0x5d, 0x35, 0x12, 0x01, - 0xa5, 0xa4, 0x5c, 0xc6, 0xd6, 0x9a, 0xa5, 0x32, 0x8f, 0x53, 0xba, 0x62, 0x09, 0x75, 0xf4, 0xe0, 0x82, 0x71, 0x26, - 0x19, 0x29, 0x1c, 0x91, 0x90, 0x82, 0xc6, 0xde, 0x45, 0x2b, 0x68, 0xa3, 0x07, 0x64, 0x01, 0x63, 0x5e, 0x59, 0x20, - 0x52, 0x24, 0x0d, 0xab, 0x25, 0x52, 0xf6, 0xc6, 0x65, 0x95, 0xb6, 0x05, 0x9d, 0x67, 0x2d, 0x4f, 0x24, 0x03, 0x0b, - 0x84, 0xcd, 0xfb, 0xbb, 0x82, 0x4a, 0x44, 0xe3, 0x37, 0x44, 0xe6, 0xb8, 0x24, 0x1b, 0xdb, 0x74, 0x18, 0xb7, 0xfd, - 0x6f, 0x6c, 0xfe, 0xdc, 0x73, 0xdd, 0xfe, 0x85, 0x6e, 0xdc, 0xfe, 0x00, 0xfe, 0xce, 0x1a, 0x2a, 0xdb, 0x86, 0x23, - 0x62, 0xdf, 0x46, 0x35, 0x50, 0xa2, 0x34, 0xb6, 0x4a, 0xcf, 0xc7, 0xae, 0x3b, 0x45, 0xde, 0x25, 0xf6, 0x47, 0x8e, - 0xe7, 0xe1, 0xc0, 0xf1, 0x46, 0xc9, 0xc4, 0x19, 0x21, 0x6f, 0x08, 0x8d, 0xef, 0xe3, 0x11, 0x72, 0x3f, 0x59, 0x28, - 0x63, 0x45, 0x11, 0x5b, 0xbc, 0xe2, 0xd4, 0x42, 0x42, 0x36, 0xd5, 0x1d, 0x8d, 0xad, 0xa4, 0x6d, 0x1a, 0xf0, 0xee, - 0xaa, 0x2a, 0xaa, 0x06, 0xac, 0xfd, 0x95, 0xa3, 0x7b, 0xff, 0xbe, 0x58, 0x87, 0x6c, 0x08, 0x17, 0x59, 0xd5, 0x94, - 0xb1, 0xa5, 0x41, 0xb1, 0x9f, 0xee, 0xe8, 0x1e, 0xa9, 0xa6, 0x7f, 0xb6, 0xe8, 0x54, 0x0d, 0x5b, 0x32, 0x1e, 0x5b, - 0x9e, 0x8f, 0xbc, 0x29, 0xe8, 0xbd, 0xed, 0xef, 0x8f, 0xa0, 0x10, 0x05, 0x4a, 0xe7, 0x66, 0x65, 0xbf, 0xbf, 0x8d, - 0xc4, 0x6a, 0x89, 0x36, 0x65, 0xc1, 0x45, 0x6c, 0xe5, 0x52, 0xd6, 0xe1, 0x60, 0xb0, 0x5e, 0xaf, 0xf1, 0x3a, 0xc0, - 0x55, 0xb3, 0x1c, 0xf8, 0xae, 0xeb, 0x0e, 0x80, 0xc2, 0x42, 0x66, 0x7f, 0x2c, 0x7f, 0x68, 0xa1, 0x9c, 0xb2, 0x65, - 0x2e, 0x75, 0x7f, 0xfe, 0x74, 0xc7, 0xf7, 0x91, 0xa2, 0x98, 0xdf, 0x7e, 0x38, 0xd3, 0xd2, 0x9c, 0x69, 0xe1, 0xdf, - 0x12, 0xdb, 0x3a, 0xb8, 0xda, 0x7b, 0xa3, 0x8c, 0x9a, 0x10, 0x1f, 0xf9, 0xc8, 0xd5, 0xff, 0x7d, 0x47, 0xf5, 0xbb, - 0x91, 0xf3, 0xd9, 0x08, 0x9d, 0x8d, 0xe0, 0xaf, 0x02, 0xd0, 0x2f, 0xc7, 0xce, 0xe5, 0x91, 0xdf, 0x53, 0xeb, 0x2b, - 0xcf, 0x3d, 0x4d, 0x28, 0xa6, 0xef, 0xc7, 0xe7, 0x63, 0xc7, 0xff, 0x59, 0x11, 0x68, 0xf4, 0x8f, 0x5c, 0x8e, 0x9f, - 0x7b, 0x3f, 0x8f, 0xc9, 0x08, 0x8d, 0xba, 0x99, 0x91, 0xa3, 0xfa, 0xc7, 0x91, 0xd6, 0x85, 0x46, 0x2b, 0x20, 0x2b, - 0x9d, 0xb1, 0x33, 0x22, 0x01, 0x0a, 0x3a, 0xab, 0xa0, 0x07, 0xd3, 0x63, 0xe0, 0x3e, 0x9b, 0x73, 0x82, 0x4f, 0xbd, - 0xc1, 0xdc, 0xea, 0x87, 0x96, 0x75, 0x82, 0xa1, 0x3a, 0x87, 0x01, 0x7f, 0xac, 0xe0, 0xdc, 0x59, 0x56, 0x7f, 0x6f, - 0x7d, 0x2b, 0xc8, 0x8a, 0x5a, 0x71, 0x1c, 0x43, 0xa8, 0xb5, 0x25, 0x9c, 0x10, 0x5c, 0x54, 0x09, 0x51, 0x2c, 0x58, - 0x50, 0xd2, 0x24, 0xf9, 0xd7, 0x5f, 0xdb, 0xc7, 0xa5, 0x25, 0x95, 0xaf, 0x0a, 0xaa, 0xba, 0xe2, 0xe5, 0xf6, 0x86, - 0x2c, 0xdf, 0x42, 0x00, 0xd9, 0x16, 0x11, 0x2c, 0xa5, 0x56, 0xff, 0xbd, 0xfb, 0x01, 0x0b, 0xb9, 0x2d, 0x28, 0x4e, - 0x99, 0xa8, 0x0b, 0xb2, 0x8d, 0xad, 0x05, 0xc8, 0xba, 0xb3, 0xfa, 0x17, 0x19, 0x95, 0x49, 0x6e, 0x5b, 0x03, 0x08, - 0xb1, 0x8c, 0x2d, 0xf1, 0x47, 0x51, 0x71, 0xab, 0x8f, 0x65, 0x4e, 0xb9, 0x6d, 0x1f, 0x2c, 0x54, 0xf6, 0x71, 0xbd, - 0x64, 0x3f, 0xb4, 0x74, 0xb4, 0x41, 0x32, 0xa9, 0x42, 0x0e, 0xab, 0xe0, 0xbd, 0x38, 0xce, 0x2e, 0xaa, 0x74, 0xfb, - 0x88, 0x79, 0xb9, 0x67, 0x6c, 0x63, 0x9c, 0xd3, 0xe6, 0x86, 0x6e, 0xe0, 0xb8, 0xfc, 0x9b, 0x7d, 0xc7, 0xd0, 0x5b, - 0x2a, 0xd7, 0x55, 0x73, 0x27, 0x42, 0x64, 0x3d, 0x37, 0xe2, 0x66, 0x26, 0x42, 0x39, 0x26, 0xb5, 0xc0, 0xa2, 0x80, - 0xf0, 0xb7, 0xbd, 0x3e, 0xc4, 0x6a, 0x7d, 0xdf, 0x14, 0x83, 0xe2, 0x6d, 0x94, 0xb2, 0x15, 0x4a, 0x0a, 0x22, 0xe0, - 0xb8, 0x72, 0x23, 0xcb, 0x42, 0x87, 0xb8, 0xaa, 0x78, 0x02, 0xfc, 0x77, 0xb1, 0xf5, 0x00, 0x76, 0x2f, 0xb7, 0x3f, - 0xa4, 0x76, 0x4f, 0x00, 0x6a, 0xbd, 0x3e, 0x5e, 0x91, 0xa2, 0xa5, 0x28, 0x46, 0x32, 0x67, 0xe2, 0x64, 0xe2, 0xec, - 0x51, 0xb6, 0x5a, 0xdc, 0x01, 0x57, 0x06, 0xcb, 0xc2, 0xee, 0x5b, 0xc7, 0x38, 0x8e, 0x88, 0xb9, 0xe5, 0xac, 0x27, - 0xd6, 0x67, 0x36, 0x39, 0x05, 0xcd, 0xa4, 0x75, 0x16, 0xf0, 0x4f, 0x77, 0x70, 0x1b, 0xe1, 0x06, 0xf4, 0xf7, 0xf7, - 0xa7, 0xd9, 0x48, 0xd4, 0x84, 0x7f, 0xce, 0xaa, 0x6c, 0xd4, 0x81, 0x85, 0x55, 0x4f, 0x45, 0x17, 0x10, 0x9d, 0x74, - 0x0e, 0xc8, 0xb1, 0xff, 0x74, 0x07, 0x71, 0xa6, 0x8e, 0xce, 0xdd, 0x49, 0x68, 0x34, 0x00, 0x84, 0xe6, 0xb7, 0xfb, - 0x7e, 0xff, 0xe4, 0xce, 0x6f, 0x2d, 0x6d, 0xb6, 0xd7, 0xb4, 0xa0, 0x89, 0xac, 0x1a, 0xdb, 0x7a, 0x02, 0x9a, 0xe0, - 0x24, 0x68, 0xbf, 0xbf, 0xbf, 0x79, 0xf3, 0x3a, 0xae, 0x6c, 0xda, 0xbf, 0x78, 0x8c, 0x5a, 0xdd, 0xea, 0xef, 0xe1, - 0x56, 0xff, 0x4f, 0xdc, 0x53, 0xf7, 0x7a, 0xef, 0x03, 0xb0, 0x1a, 0xaf, 0x4f, 0x97, 0xbb, 0xba, 0x00, 0x9e, 0xc3, - 0x25, 0x72, 0x61, 0x3d, 0x17, 0xb6, 0x33, 0x1e, 0xf5, 0x41, 0x3d, 0xfc, 0x80, 0xe9, 0xfa, 0x7a, 0x86, 0x6b, 0x5a, - 0x1d, 0xd1, 0xf9, 0x37, 0xbb, 0x45, 0xb5, 0x71, 0x04, 0xfb, 0xc4, 0xf8, 0x32, 0x64, 0x3c, 0xa7, 0x0d, 0x93, 0x7b, - 0x30, 0x17, 0x6e, 0xfa, 0xba, 0x95, 0xbb, 0x9a, 0xa4, 0xa9, 0x5a, 0x19, 0xd5, 0x9b, 0x59, 0x06, 0x79, 0x41, 0x51, - 0xd2, 0xd0, 0xa3, 0xe5, 0xde, 0xac, 0xeb, 0x2b, 0x28, 0xbc, 0x1c, 0x3d, 0xdb, 0xab, 0x83, 0xb7, 0x93, 0xb0, 0x65, - 0x0e, 0x29, 0xd8, 0x92, 0x87, 0x09, 0xd8, 0x4d, 0x1b, 0xc3, 0x94, 0x91, 0x92, 0x15, 0xdb, 0x50, 0xc0, 0x65, 0xe8, - 0x40, 0xc2, 0x60, 0xd9, 0x7e, 0xd1, 0x4a, 0x59, 0x71, 0xd0, 0xdd, 0xa4, 0xb4, 0x09, 0xdd, 0x99, 0xe9, 0x38, 0x0d, - 0x49, 0x59, 0x2b, 0x42, 0x1c, 0x34, 0xb4, 0x9c, 0x2d, 0x48, 0x72, 0xb7, 0x6c, 0xaa, 0x96, 0xa7, 0x4e, 0xa2, 0x6e, - 0xeb, 0xf0, 0x89, 0x97, 0x91, 0x80, 0x26, 0xb3, 0x6e, 0x94, 0x65, 0xd9, 0x0c, 0x90, 0xa0, 0x8e, 0xb9, 0xfc, 0x42, - 0x1f, 0x0f, 0x15, 0xdb, 0x99, 0x99, 0xd8, 0x57, 0x13, 0xc6, 0x46, 0x48, 0x25, 0xcf, 0x66, 0x07, 0x77, 0xdc, 0x19, - 0xa4, 0x01, 0x01, 0x42, 0x6a, 0x88, 0x7f, 0x30, 0x73, 0x5f, 0x12, 0xc6, 0xcf, 0xad, 0x57, 0x67, 0x65, 0xd6, 0x85, - 0x2f, 0xc0, 0xa2, 0xd5, 0xe8, 0x20, 0x9e, 0x41, 0xa2, 0x32, 0xb9, 0x30, 0xf4, 0xc7, 0x6e, 0xbd, 0xd9, 0xe3, 0xee, - 0x8c, 0xec, 0x0e, 0xd4, 0x59, 0x41, 0x37, 0xb3, 0x8f, 0xad, 0x90, 0x2c, 0xdb, 0x3a, 0x5d, 0x2e, 0x0d, 0xe1, 0xbc, - 0x40, 0x0e, 0x5d, 0x00, 0x29, 0xa5, 0x7c, 0xa6, 0x75, 0x38, 0x4c, 0xd2, 0x52, 0x74, 0x38, 0x1d, 0xc5, 0xe8, 0x53, - 0x7a, 0x5f, 0xd6, 0xff, 0xa2, 0x56, 0xc7, 0x71, 0x57, 0x92, 0x06, 0x72, 0x8b, 0xb3, 0xa8, 0x00, 0xd3, 0x32, 0x74, - 0x26, 0xb0, 0x57, 0xdd, 0x94, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xfa, 0x6e, 0x3a, 0xe0, 0xed, 0xd5, 0x1b, 0x24, 0xaa, - 0x82, 0xa5, 0x1d, 0x9d, 0x26, 0x41, 0xee, 0x11, 0x1e, 0x0f, 0xb6, 0x1b, 0xa9, 0xb9, 0x03, 0xd4, 0xc3, 0x6c, 0x4a, - 0x3c, 0xf7, 0x81, 0x1d, 0x49, 0xb3, 0xcc, 0x5f, 0x64, 0x47, 0xa4, 0x54, 0xaa, 0xdd, 0xb3, 0xee, 0x54, 0xf8, 0x43, - 0x10, 0x70, 0xd8, 0x1b, 0xe8, 0xef, 0x99, 0x8e, 0x8b, 0xdd, 0x99, 0x14, 0x7d, 0x52, 0xc3, 0xb6, 0x29, 0xec, 0x87, - 0x4e, 0xee, 0xb3, 0xe0, 0xea, 0x94, 0x09, 0x7b, 0x8f, 0x67, 0xc2, 0x1e, 0x52, 0xb5, 0xcb, 0xcb, 0x6a, 0x13, 0xf7, - 0x74, 0x4e, 0x1a, 0xc2, 0x4f, 0xef, 0x59, 0xf0, 0x0a, 0xf8, 0xff, 0x2f, 0x29, 0xee, 0x0f, 0xa7, 0xb7, 0x2f, 0x48, - 0x6d, 0x5f, 0x98, 0xd5, 0x8c, 0x77, 0xca, 0x79, 0xe8, 0x41, 0xfa, 0x62, 0x58, 0xb0, 0xa5, 0xf7, 0x67, 0x40, 0xfb, - 0xdf, 0x38, 0x06, 0x2f, 0xbc, 0x29, 0xbe, 0x44, 0xba, 0x31, 0x10, 0xe1, 0x60, 0x8a, 0x26, 0x57, 0x43, 0x3c, 0xf4, - 0x90, 0xaa, 0x9a, 0xc6, 0x68, 0x82, 0xa7, 0x40, 0x30, 0xc6, 0xc1, 0x04, 0x26, 0x90, 0xef, 0xe1, 0xd1, 0x6b, 0x3f, - 0xc0, 0xe3, 0x11, 0x50, 0xf9, 0x2e, 0x0e, 0x7c, 0x64, 0x68, 0xc7, 0xd8, 0x07, 0x71, 0x8a, 0x24, 0x28, 0x01, 0xe8, - 0x24, 0xc0, 0xee, 0x04, 0xc4, 0x8d, 0xb1, 0x7b, 0x89, 0xa7, 0x63, 0x34, 0xc5, 0x13, 0x80, 0x0e, 0x0f, 0x47, 0x85, - 0x33, 0xc2, 0x1e, 0x4c, 0x07, 0x63, 0x32, 0xc5, 0xc3, 0x00, 0xe9, 0xc6, 0xc0, 0x31, 0x01, 0x11, 0x0e, 0x76, 0xbd, - 0xd7, 0x01, 0xf6, 0x27, 0xa0, 0x77, 0x38, 0x7c, 0x01, 0x62, 0x2f, 0x87, 0xc8, 0xb4, 0x06, 0x5e, 0x50, 0x30, 0x7a, - 0x0c, 0x34, 0xff, 0x9f, 0x0b, 0x1a, 0x40, 0xe2, 0xa1, 0x00, 0x5f, 0x42, 0xec, 0x7a, 0x8a, 0xdf, 0xb4, 0x06, 0x37, - 0xcf, 0x43, 0xee, 0x1f, 0xc6, 0x2c, 0xf8, 0xe7, 0x62, 0xe6, 0x29, 0x04, 0xa0, 0x0b, 0xba, 0x41, 0x0e, 0xd2, 0x8d, - 0xd1, 0x0d, 0xcc, 0xd3, 0xab, 0x4b, 0x34, 0x05, 0xae, 0xf1, 0x14, 0x5d, 0xa2, 0x91, 0x42, 0x17, 0xd8, 0x87, 0x86, - 0xc9, 0x01, 0xa6, 0x2f, 0x84, 0x71, 0xf8, 0x37, 0x86, 0xf1, 0x31, 0x9f, 0xfe, 0xc6, 0x2e, 0xfd, 0x15, 0x57, 0x10, - 0x94, 0x63, 0xba, 0x0c, 0x8b, 0x06, 0xe6, 0x15, 0xaf, 0xaa, 0x28, 0x78, 0x94, 0x43, 0x35, 0x02, 0xef, 0x7a, 0x0f, - 0xb1, 0x34, 0xce, 0xbd, 0xf9, 0xbd, 0x2a, 0x1d, 0x28, 0xbd, 0x79, 0xa4, 0xd3, 0xf9, 0xfc, 0x26, 0xa7, 0xe8, 0xd5, - 0xf5, 0x3b, 0x78, 0x08, 0x16, 0x05, 0xe2, 0xd5, 0x1a, 0xde, 0x9b, 0x5b, 0x24, 0x2b, 0xf5, 0x82, 0xe7, 0x50, 0x2a, - 0xaa, 0x2e, 0x3c, 0x20, 0x50, 0x57, 0x2c, 0x60, 0x8c, 0xa3, 0x45, 0x33, 0x7f, 0x57, 0x50, 0x22, 0x28, 0x5a, 0xb2, - 0x15, 0x45, 0x4c, 0x42, 0x1d, 0x50, 0x52, 0x24, 0x99, 0x6a, 0x8e, 0x8c, 0x9a, 0xee, 0x6d, 0x25, 0x69, 0x88, 0xae, - 0xaa, 0x7a, 0xab, 0x85, 0x24, 0x39, 0xe1, 0x4b, 0x9a, 0x1e, 0x84, 0x29, 0xea, 0x6d, 0xd5, 0x36, 0xe8, 0x97, 0x17, - 0x6f, 0x5e, 0xab, 0x87, 0x36, 0x45, 0x4e, 0xa7, 0x6c, 0x23, 0xd1, 0x8f, 0x37, 0x2f, 0x50, 0x5b, 0xc3, 0xa6, 0x53, - 0x63, 0x5b, 0xb5, 0xa2, 0xcd, 0x1a, 0x2a, 0x4b, 0xaa, 0x48, 0x40, 0xb9, 0xa0, 0x52, 0x42, 0xa1, 0x21, 0x30, 0x94, - 0xce, 0xda, 0x13, 0x53, 0x75, 0x83, 0xbb, 0x20, 0x7e, 0xde, 0x95, 0xd7, 0x51, 0x1e, 0x18, 0xd7, 0xaf, 0x3b, 0x6a, - 0x70, 0x3d, 0x98, 0x47, 0xea, 0x39, 0x8d, 0x88, 0x7e, 0x84, 0xc4, 0x83, 0x35, 0xcb, 0x98, 0x7a, 0xb8, 0xcd, 0x23, - 0x5d, 0x8f, 0x2a, 0x09, 0xaa, 0x24, 0x32, 0x5f, 0x34, 0x74, 0xaf, 0xa0, 0x7c, 0x09, 0xaf, 0x64, 0xd8, 0x70, 0xa8, - 0x50, 0x12, 0x9a, 0x57, 0x05, 0x54, 0x40, 0xf1, 0xf5, 0xf5, 0x0f, 0xff, 0x52, 0x9f, 0x3f, 0xc0, 0xcf, 0x13, 0x27, - 0x3c, 0x29, 0x0c, 0xa3, 0xea, 0x74, 0x7c, 0xe3, 0xa1, 0xf9, 0x90, 0x51, 0xc3, 0x7b, 0x00, 0xfc, 0x4e, 0xef, 0x49, - 0x79, 0x77, 0x98, 0xec, 0x24, 0xe9, 0x5f, 0x5d, 0xd9, 0x1a, 0x26, 0xd1, 0x2e, 0x4a, 0x26, 0xe7, 0xd7, 0x60, 0x60, - 0x34, 0x30, 0x0b, 0xe0, 0x9c, 0x72, 0xc0, 0xd0, 0xe6, 0x1d, 0x0f, 0xec, 0xa8, 0x42, 0xec, 0x27, 0x8d, 0x98, 0xd9, - 0x60, 0xed, 0x65, 0x49, 0x65, 0x5e, 0xa5, 0xf1, 0xbb, 0x1f, 0xaf, 0x6f, 0x8e, 0x1e, 0x77, 0xb0, 0x52, 0x9e, 0x98, - 0x0f, 0x2c, 0x6d, 0x21, 0x59, 0x4d, 0x1a, 0xa9, 0xc5, 0x3a, 0x2a, 0xce, 0x0e, 0x1e, 0xe9, 0x75, 0xbd, 0x33, 0xda, - 0xa9, 0x8e, 0x71, 0x30, 0x47, 0x0f, 0xd9, 0x78, 0xd0, 0xfd, 0x99, 0x95, 0x03, 0x73, 0x14, 0x07, 0xe6, 0x5c, 0x0e, - 0xf4, 0xe7, 0xa7, 0xdf, 0x01, 0xf1, 0x69, 0xfc, 0xac, 0x8e, 0x12, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x6d, 0x6f, 0xdb, 0x38, 0x12, 0xfe, 0xde, + 0x5f, 0x31, 0xa7, 0x36, 0x6b, 0x6b, 0x1b, 0x51, 0x22, 0xe5, 0xb7, 0xd8, 0x92, 0x16, 0x69, 0xae, 0x8b, 0x5d, 0xa0, + 0xdd, 0x2d, 0x90, 0x6c, 0xef, 0x43, 0x51, 0x20, 0xb4, 0x34, 0xb2, 0xd8, 0x48, 0xa4, 0x4e, 0xa4, 0x5f, 0x52, 0xc3, + 0xf7, 0xdb, 0x0f, 0x94, 0x6c, 0xc7, 0xe9, 0x35, 0x87, 0xeb, 0xe2, 0x0e, 0x87, 0xdd, 0x18, 0x21, 0x86, 0xe4, 0xcc, + 0x70, 0xe6, 0xf1, 0x0c, 0x67, 0xcc, 0xe8, 0x2f, 0x99, 0x4a, 0xcd, 0x7d, 0x8d, 0x50, 0x98, 0xaa, 0x4c, 0x22, 0x3b, + 0x42, 0xc9, 0xe5, 0x22, 0x46, 0x99, 0x44, 0x05, 0xf2, 0x2c, 0x89, 0x2a, 0x34, 0x1c, 0xd2, 0x82, 0x37, 0x1a, 0x4d, + 0xfc, 0xdb, 0xcd, 0x8f, 0xde, 0x04, 0xfc, 0x24, 0x2a, 0x85, 0xbc, 0x83, 0x06, 0xcb, 0x58, 0xa4, 0x4a, 0x42, 0xd1, + 0x60, 0x1e, 0x67, 0xdc, 0xf0, 0xa9, 0xa8, 0xf8, 0x02, 0x2d, 0x43, 0x2b, 0x26, 0x79, 0x85, 0xf1, 0x4a, 0xe0, 0xba, + 0x56, 0x8d, 0x81, 0x54, 0x49, 0x83, 0xd2, 0xc4, 0xce, 0x5a, 0x64, 0xa6, 0x88, 0x33, 0x5c, 0x89, 0x14, 0xbd, 0x76, + 0x72, 0x2e, 0xa4, 0x30, 0x82, 0x97, 0x9e, 0x4e, 0x79, 0x89, 0x31, 0x3d, 0x5f, 0x6a, 0x6c, 0xda, 0x09, 0x9f, 0x97, + 0x18, 0x4b, 0xe5, 0xf8, 0x49, 0xa4, 0xd3, 0x46, 0xd4, 0x06, 0xac, 0xbd, 0x71, 0xa5, 0xb2, 0x65, 0x89, 0x89, 0xef, + 0x73, 0xad, 0xd1, 0x68, 0x5f, 0xc8, 0x0c, 0x37, 0x64, 0x14, 0x86, 0x29, 0xe3, 0xe3, 0x9c, 0x7c, 0xd2, 0xcf, 0x32, + 0x95, 0x2e, 0x2b, 0x94, 0x86, 0x94, 0x2a, 0xe5, 0x46, 0x28, 0x49, 0x34, 0xf2, 0x26, 0x2d, 0xe2, 0x38, 0x76, 0x7e, + 0xd0, 0x7c, 0x85, 0xce, 0x77, 0xdf, 0xf5, 0x8f, 0x4c, 0x0b, 0x34, 0xaf, 0x4b, 0xb4, 0xa4, 0x7e, 0x75, 0x7f, 0xc3, + 0x17, 0xbf, 0xf0, 0x0a, 0xfb, 0x0e, 0xd7, 0x22, 0x43, 0xc7, 0xfd, 0x10, 0x7c, 0x24, 0xda, 0xdc, 0x97, 0x48, 0x32, + 0xa1, 0xeb, 0x92, 0xdf, 0xc7, 0xce, 0xbc, 0x54, 0xe9, 0x9d, 0xe3, 0xce, 0xf2, 0xa5, 0x4c, 0xad, 0x72, 0xd0, 0x7d, + 0x74, 0xb7, 0x25, 0x1a, 0x30, 0xf1, 0x5b, 0x6e, 0x0a, 0x52, 0xf1, 0x4d, 0xbf, 0x23, 0x84, 0xec, 0xb3, 0xef, 0xfb, + 0xf8, 0x92, 0x06, 0x81, 0x7b, 0xde, 0x0e, 0x81, 0xeb, 0xd3, 0x20, 0x98, 0x35, 0x68, 0x96, 0x8d, 0x04, 0xde, 0xbf, + 0x8d, 0x6a, 0x6e, 0x0a, 0xc8, 0x62, 0xa7, 0xa2, 0x8c, 0x04, 0xc1, 0x04, 0xe8, 0x05, 0x61, 0x43, 0x8f, 0x52, 0x12, + 0x7a, 0x74, 0x98, 0x8e, 0xbd, 0x21, 0xd0, 0x81, 0x37, 0x04, 0xc6, 0xc8, 0x10, 0x82, 0xcf, 0x0e, 0xe4, 0xa2, 0x2c, + 0x63, 0x47, 0x2a, 0x89, 0x0e, 0x68, 0xd3, 0xa8, 0x3b, 0x8c, 0x9d, 0x74, 0xd9, 0x34, 0x28, 0xcd, 0x95, 0x2a, 0x55, + 0xe3, 0xf8, 0xc9, 0x33, 0x78, 0xf4, 0xf7, 0xcd, 0x47, 0x98, 0x86, 0x4b, 0x9d, 0xab, 0xa6, 0x8a, 0x9d, 0xf6, 0x4b, + 0xe9, 0xbf, 0xd8, 0x9a, 0x1d, 0xd8, 0xc1, 0x3d, 0xd9, 0xf4, 0x54, 0x23, 0x16, 0x42, 0xc6, 0x0e, 0x65, 0x40, 0x27, + 0x8e, 0x9f, 0xdc, 0xba, 0xbb, 0x23, 0x26, 0xdc, 0x62, 0xb2, 0xf7, 0x52, 0xf5, 0x3f, 0xdc, 0x46, 0x7a, 0xb5, 0x80, + 0x4d, 0x55, 0x4a, 0x1d, 0x3b, 0x85, 0x31, 0xf5, 0xd4, 0xf7, 0xd7, 0xeb, 0x35, 0x59, 0x87, 0x44, 0x35, 0x0b, 0x9f, + 0x05, 0x41, 0xe0, 0xeb, 0xd5, 0xc2, 0x81, 0x2e, 0x3e, 0x1c, 0x36, 0x70, 0xa0, 0x40, 0xb1, 0x28, 0x4c, 0x4b, 0x27, + 0x2f, 0xb6, 0xb8, 0x8b, 0x2c, 0x47, 0x72, 0xfb, 0xf1, 0xe4, 0x14, 0x71, 0x72, 0x0a, 0xfe, 0x70, 0x82, 0x66, 0xef, + 0xad, 0x35, 0x6a, 0xcc, 0x19, 0x30, 0x08, 0xda, 0x0f, 0xf3, 0x2c, 0xbd, 0x9f, 0x79, 0x5f, 0xcc, 0xe0, 0x64, 0x06, + 0x0c, 0x9e, 0x01, 0xb0, 0x6a, 0xe4, 0x5d, 0x1c, 0xc5, 0xa9, 0xdd, 0x5e, 0xd1, 0xe0, 0x61, 0xc1, 0xca, 0xfc, 0x34, + 0x3a, 0x9d, 0x7b, 0xec, 0xbd, 0x65, 0xb0, 0xd8, 0x1f, 0x85, 0x3c, 0x56, 0xd0, 0xf7, 0x23, 0x3e, 0x84, 0xe1, 0x7e, + 0x65, 0xe8, 0x59, 0xfa, 0x38, 0xb3, 0x27, 0xc1, 0x70, 0xc5, 0x0a, 0x5a, 0x79, 0x23, 0x6f, 0xc8, 0x43, 0x08, 0xf7, + 0x26, 0x85, 0x10, 0xae, 0x58, 0x31, 0x7a, 0x3f, 0x3a, 0x5d, 0xf3, 0xc2, 0xcf, 0x3d, 0x0b, 0xf3, 0xd4, 0x71, 0x1e, + 0x30, 0x50, 0xa7, 0x18, 0x90, 0x4f, 0x4a, 0xc8, 0xbe, 0xe3, 0xb8, 0xbb, 0x1c, 0x4d, 0x5a, 0xf4, 0x1d, 0x3f, 0x55, + 0x32, 0x17, 0x0b, 0xf2, 0x49, 0x2b, 0xe9, 0xb8, 0xc4, 0x14, 0x28, 0xfb, 0x07, 0x51, 0x2b, 0x88, 0xed, 0x4e, 0xff, + 0xcb, 0x1d, 0xe3, 0x6e, 0x8f, 0xf9, 0x61, 0x84, 0x29, 0x31, 0x36, 0xc4, 0x66, 0xf4, 0xf9, 0x71, 0x75, 0xae, 0xb2, + 0xfb, 0x27, 0x52, 0xa7, 0xa0, 0x5d, 0xde, 0x08, 0x29, 0xb1, 0xb9, 0xc1, 0x8d, 0x89, 0x9d, 0xb7, 0x97, 0x57, 0x70, + 0x99, 0x65, 0x0d, 0x6a, 0x3d, 0x05, 0xe7, 0xa5, 0x21, 0x15, 0x4f, 0xff, 0x73, 0x5d, 0xf4, 0x91, 0xae, 0xbf, 0x89, + 0x1f, 0x05, 0xfc, 0x82, 0x66, 0xad, 0x9a, 0xbb, 0xbd, 0x36, 0x6b, 0xda, 0xcc, 0x66, 0x60, 0x13, 0x1b, 0xc2, 0x6b, + 0x4d, 0x74, 0x29, 0x52, 0xec, 0x53, 0x97, 0x54, 0xbc, 0x7e, 0xf0, 0x4a, 0x1e, 0x80, 0xba, 0x8d, 0x32, 0xb1, 0x82, + 0xb4, 0xe4, 0x5a, 0xc7, 0x8e, 0xec, 0x54, 0x39, 0xb0, 0x4f, 0x1b, 0x25, 0xd3, 0x52, 0xa4, 0x77, 0xb1, 0xf3, 0x95, + 0x1b, 0xe2, 0xd5, 0xfd, 0xcf, 0x59, 0xbf, 0xa7, 0xb5, 0xc8, 0x7a, 0x2e, 0x59, 0xf1, 0x72, 0x89, 0x10, 0x83, 0x29, + 0x84, 0x7e, 0x30, 0x70, 0xf6, 0xa4, 0x58, 0xad, 0xef, 0x7a, 0x2e, 0xc9, 0x55, 0xba, 0xd4, 0x7d, 0xd7, 0x39, 0x64, + 0x69, 0xc4, 0xbb, 0x3b, 0xd4, 0x79, 0xee, 0x7c, 0x61, 0x91, 0x57, 0x62, 0x6e, 0x9c, 0x87, 0x6c, 0x7e, 0xb1, 0xd5, + 0x7d, 0x49, 0x1a, 0xad, 0x85, 0xbb, 0x3b, 0x2e, 0x46, 0xba, 0xe6, 0xf2, 0x4b, 0x41, 0x6b, 0xa0, 0x4d, 0x1a, 0x49, + 0x2c, 0x65, 0x33, 0xa7, 0xe6, 0xf2, 0x78, 0xa0, 0xcf, 0x0f, 0xe4, 0x8b, 0xad, 0xe8, 0x4b, 0x7b, 0x4b, 0xde, 0x1d, + 0x35, 0x46, 0x7e, 0x26, 0x56, 0xc9, 0xed, 0xce, 0x7d, 0xf0, 0xe3, 0xef, 0x4b, 0x6c, 0xee, 0xaf, 0xb1, 0xc4, 0xd4, + 0xa8, 0xa6, 0xef, 0x3c, 0x97, 0x68, 0x1c, 0xb7, 0x73, 0xf8, 0xa7, 0x9b, 0xb7, 0x6f, 0x62, 0xd5, 0x6f, 0xdc, 0xf3, + 0xa7, 0xb8, 0x6d, 0xb5, 0xf8, 0xd0, 0x60, 0xf9, 0x8f, 0xb8, 0x67, 0xeb, 0x45, 0xef, 0xa3, 0xe3, 0x92, 0xd6, 0xdf, + 0xdb, 0x87, 0xa2, 0x61, 0x13, 0xfb, 0xe5, 0xa6, 0x2a, 0xcf, 0xad, 0x87, 0xde, 0x68, 0xe8, 0xee, 0x6e, 0x77, 0xee, + 0xce, 0x9d, 0x45, 0x7e, 0x77, 0xef, 0x27, 0x51, 0x7b, 0x05, 0x27, 0xdf, 0x6f, 0xe7, 0x6a, 0xe3, 0x69, 0xf1, 0x59, + 0xc8, 0xc5, 0x54, 0xc8, 0x02, 0x1b, 0x61, 0x76, 0x99, 0x58, 0x9d, 0x0b, 0x59, 0x2f, 0xcd, 0xb6, 0xe6, 0x59, 0x66, + 0x77, 0x86, 0xf5, 0x66, 0x96, 0x2b, 0x69, 0x2c, 0x27, 0x4e, 0x29, 0x56, 0xbb, 0x6e, 0xbf, 0xbd, 0x5b, 0xa6, 0x17, + 0xc3, 0xb3, 0x9d, 0x0d, 0xb8, 0xad, 0xc1, 0x8d, 0xf1, 0x78, 0x29, 0x16, 0x72, 0x9a, 0xa2, 0x34, 0xd8, 0x74, 0x42, + 0x39, 0xaf, 0x44, 0x79, 0x3f, 0xd5, 0x5c, 0x6a, 0x4f, 0x63, 0x23, 0xf2, 0xdd, 0x7c, 0x69, 0x8c, 0x92, 0xdb, 0xb9, + 0x6a, 0x32, 0x6c, 0xa6, 0xc1, 0xac, 0x23, 0xbc, 0x86, 0x67, 0x62, 0xa9, 0xa7, 0x24, 0x6c, 0xb0, 0x9a, 0xcd, 0x79, + 0x7a, 0xb7, 0x68, 0xd4, 0x52, 0x66, 0x5e, 0x6a, 0x6f, 0xe1, 0xe9, 0x73, 0x9a, 0xf3, 0x10, 0xd3, 0xd9, 0x7e, 0x96, + 0xe7, 0xf9, 0xac, 0x14, 0x12, 0xbd, 0xee, 0x56, 0x9b, 0x32, 0x32, 0xb0, 0x62, 0x27, 0x66, 0x12, 0x66, 0x17, 0x3a, + 0x1b, 0x69, 0x10, 0x9c, 0xcd, 0x0e, 0xee, 0x04, 0xb3, 0x74, 0xd9, 0x68, 0xd5, 0x4c, 0x6b, 0x25, 0xac, 0x99, 0xbb, + 0x8a, 0x0b, 0x79, 0x6a, 0xbd, 0x0d, 0x93, 0xd9, 0xbe, 0x3c, 0x4d, 0x85, 0x6c, 0x8f, 0x69, 0x8b, 0xd4, 0xac, 0x12, + 0xb2, 0x2b, 0xb2, 0x53, 0x36, 0x0a, 0xea, 0xcd, 0x8e, 0xec, 0x03, 0x64, 0x7b, 0xe0, 0xce, 0x4b, 0xdc, 0xcc, 0x3e, + 0x2d, 0xb5, 0x11, 0xf9, 0xbd, 0xb7, 0x2f, 0xd2, 0x53, 0x5d, 0xf3, 0x14, 0xbd, 0x39, 0x9a, 0x35, 0xa2, 0x9c, 0xb5, + 0x67, 0x78, 0xc2, 0x60, 0xa5, 0xf7, 0x38, 0x1d, 0xd5, 0xb4, 0x01, 0xfa, 0x58, 0xd7, 0xbf, 0xe3, 0xb6, 0xb1, 0xb8, + 0xad, 0x78, 0xb3, 0x10, 0xd2, 0x9b, 0x2b, 0x63, 0x54, 0x35, 0xf5, 0xc6, 0xf5, 0x66, 0xb6, 0x5f, 0xb2, 0xca, 0xa6, + 0xd4, 0x9a, 0xd9, 0xd6, 0xde, 0x03, 0xde, 0xb4, 0xde, 0x80, 0x56, 0xa5, 0xc8, 0xf6, 0x7c, 0x2d, 0x0b, 0x04, 0x47, + 0x78, 0xe8, 0xb0, 0xde, 0x80, 0x5d, 0x3b, 0x40, 0x3d, 0xc8, 0x27, 0x9c, 0x06, 0x5f, 0xf9, 0x46, 0xb2, 0x3c, 0x67, + 0xf3, 0xfc, 0x88, 0x94, 0x2d, 0xa1, 0x3b, 0xb1, 0x8f, 0x0a, 0x36, 0xa8, 0x37, 0xb3, 0xc3, 0x77, 0x33, 0xa8, 0x37, + 0x3b, 0xd1, 0xa6, 0xc5, 0xf6, 0x44, 0x4b, 0x1b, 0xaa, 0xd3, 0x65, 0x53, 0xf6, 0x9d, 0xaf, 0x84, 0xee, 0x59, 0x78, + 0xf5, 0x50, 0xe2, 0x7a, 0x4f, 0x97, 0xb8, 0x1e, 0xd8, 0xa6, 0xe8, 0x95, 0xda, 0xc4, 0xbd, 0xb6, 0xd8, 0x0c, 0x80, + 0x0d, 0x7a, 0x67, 0xe1, 0xeb, 0xb3, 0xf0, 0xea, 0xbf, 0x52, 0xbb, 0x7e, 0x77, 0xe1, 0xfa, 0x86, 0xaa, 0xf5, 0x8d, + 0x15, 0xab, 0xf3, 0xce, 0x3a, 0x7f, 0x16, 0xbe, 0x76, 0xdc, 0x9d, 0x20, 0x5a, 0x2c, 0xe8, 0xff, 0x02, 0xda, 0x7f, + 0xc5, 0x31, 0xbc, 0xa4, 0x13, 0x72, 0x01, 0xed, 0xd0, 0x41, 0x44, 0xc2, 0x09, 0x8c, 0xaf, 0x06, 0x64, 0x40, 0xc1, + 0xb6, 0x43, 0x23, 0x18, 0x93, 0xc9, 0x05, 0xd0, 0x11, 0x09, 0xc7, 0x40, 0x19, 0x30, 0x4a, 0x86, 0x6f, 0x58, 0x48, + 0x46, 0x43, 0x18, 0x5f, 0xb1, 0x80, 0x84, 0x0c, 0x3a, 0xde, 0x11, 0x61, 0x0c, 0x42, 0xcb, 0x12, 0x56, 0x01, 0xb0, + 0x34, 0x24, 0xc1, 0x18, 0x02, 0x18, 0x91, 0xe0, 0x82, 0x4c, 0x46, 0x30, 0x21, 0x63, 0x0a, 0x8c, 0x0c, 0x86, 0xa5, + 0x37, 0x24, 0x14, 0x46, 0x24, 0x1c, 0xf1, 0x09, 0x19, 0x84, 0xd0, 0x0e, 0x1d, 0x1c, 0x63, 0xc2, 0x98, 0x47, 0x02, + 0xfa, 0x26, 0x24, 0x6c, 0x0c, 0x63, 0x32, 0x18, 0x5c, 0xd2, 0x11, 0xb9, 0x18, 0x40, 0x37, 0x76, 0xf0, 0x52, 0x06, + 0xc3, 0xa7, 0x40, 0x63, 0x7f, 0x5e, 0xd0, 0x42, 0xc2, 0x28, 0x84, 0xe4, 0x62, 0xc2, 0x6d, 0x5f, 0xca, 0xa0, 0x1b, + 0x3b, 0xdc, 0x28, 0x85, 0xe0, 0x77, 0x63, 0x16, 0xfe, 0x79, 0x31, 0xa3, 0x16, 0x01, 0x46, 0x06, 0xe1, 0x25, 0x0d, + 0xc9, 0x08, 0xda, 0xa1, 0x3b, 0x9b, 0x32, 0x98, 0x5c, 0x5d, 0xc0, 0x04, 0x46, 0x64, 0x34, 0x81, 0x0b, 0x18, 0x5a, + 0x74, 0x2f, 0xc8, 0x64, 0xd0, 0x09, 0x79, 0x8c, 0x7c, 0x2b, 0x8c, 0x83, 0x3f, 0x30, 0x8c, 0x4f, 0xf9, 0xf4, 0x07, + 0x76, 0xe9, 0xff, 0x71, 0x05, 0x45, 0x7e, 0xd7, 0x86, 0x45, 0x7e, 0xf7, 0x3c, 0x60, 0xbb, 0xa8, 0x24, 0xb2, 0xdd, + 0x48, 0x12, 0x15, 0x14, 0x44, 0x16, 0x57, 0x3c, 0x4d, 0x4e, 0x5a, 0xfd, 0xc8, 0x2f, 0xe8, 0x61, 0xab, 0xa0, 0xc9, + 0xa3, 0xc6, 0xbd, 0xdb, 0x6b, 0x2b, 0x7d, 0x72, 0x53, 0x20, 0xbc, 0xbe, 0x7e, 0x07, 0x6b, 0x51, 0x96, 0x20, 0xd5, + 0x1a, 0x4c, 0x73, 0x0f, 0x46, 0xd9, 0x57, 0x03, 0x89, 0xa9, 0xb1, 0xa4, 0x29, 0x10, 0xf6, 0x7d, 0x04, 0x21, 0x24, + 0x9a, 0x37, 0xc9, 0xbb, 0x12, 0xb9, 0x46, 0x58, 0x88, 0x15, 0x82, 0x30, 0xa0, 0x55, 0x85, 0x60, 0x84, 0x1d, 0x8e, + 0x82, 0x2d, 0x5f, 0xe4, 0x77, 0x87, 0x74, 0x8d, 0xb2, 0xc8, 0x62, 0x89, 0x26, 0xd9, 0x77, 0xc4, 0x51, 0x11, 0x76, + 0x56, 0x5d, 0xa3, 0x31, 0x42, 0x2e, 0xac, 0x55, 0x61, 0x12, 0xd9, 0x5f, 0xb7, 0xc0, 0xdb, 0xdf, 0x0c, 0xb1, 0xbf, + 0x16, 0xb9, 0xb0, 0x6f, 0x06, 0x49, 0xd4, 0x76, 0x91, 0x56, 0x83, 0x6d, 0x64, 0xba, 0x07, 0x8e, 0x96, 0x2a, 0x51, + 0x2e, 0x4c, 0x11, 0x87, 0x0c, 0xea, 0x92, 0xa7, 0x58, 0xa8, 0x32, 0xc3, 0x26, 0xbe, 0xbe, 0xfe, 0xf9, 0xaf, 0xf6, + 0x35, 0xc4, 0x9a, 0x70, 0x94, 0xac, 0xf5, 0x5d, 0x27, 0x68, 0x89, 0xbd, 0xdc, 0x68, 0xd0, 0xbd, 0x6b, 0xd4, 0x5c, + 0xeb, 0xb5, 0x6a, 0xb2, 0x47, 0x5a, 0xde, 0x1d, 0x16, 0xf7, 0x9a, 0xda, 0xff, 0xb6, 0x1f, 0xed, 0x84, 0xf4, 0x72, + 0x5e, 0x09, 0x93, 0x5c, 0xf3, 0x15, 0x46, 0x7e, 0xb7, 0x91, 0x44, 0xbe, 0x75, 0xa0, 0xe3, 0x2d, 0xf6, 0x32, 0x05, + 0x4d, 0x7e, 0xbd, 0xb9, 0x84, 0xdf, 0xea, 0x8c, 0x1b, 0xec, 0xb0, 0x6f, 0xbd, 0xac, 0xd0, 0x14, 0x2a, 0x8b, 0xdf, + 0xfd, 0x7a, 0x7d, 0x73, 0xf4, 0x78, 0xd9, 0x32, 0x01, 0xca, 0xb4, 0x7b, 0x6f, 0x59, 0x96, 0x46, 0xd4, 0xbc, 0x31, + 0xad, 0x5a, 0xcf, 0x66, 0xc7, 0xc1, 0xa3, 0x76, 0x3f, 0x17, 0x25, 0x76, 0x4e, 0xed, 0x05, 0xfd, 0x04, 0xbe, 0x66, + 0xe3, 0xe1, 0xec, 0x2f, 0xac, 0xf4, 0xbb, 0x00, 0xf2, 0xbb, 0x68, 0xf2, 0xdb, 0xd7, 0xa8, 0x7f, 0x02, 0x14, 0xee, + 0xbc, 0x64, 0x9d, 0x12, 0x00, 0x00}; } // namespace captive_portal } // namespace esphome diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 3bfdea0ab5..d1960e9a93 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -1,6 +1,5 @@ -#ifdef USE_ARDUINO - #include "captive_portal.h" +#ifdef USE_CAPTIVE_PORTAL #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/components/wifi/wifi_component.h" @@ -14,7 +13,7 @@ static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("application/json"); stream->addHeader("cache-control", "public, max-age=0, must-revalidate"); - stream->printf(R"({"name":"%s","aps":[{})", App.get_name().c_str()); + stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); for (auto &scan : wifi::global_wifi_component->get_scan_result()) { if (scan.get_is_hidden()) @@ -46,10 +45,12 @@ void CaptivePortal::start() { this->base_->add_ota_handler(); } +#ifdef USE_ARDUINO this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); - this->dns_server_->start(53, "*", (uint32_t) ip); + this->dns_server_->start(53, "*", ip); +#endif this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { @@ -67,7 +68,7 @@ void CaptivePortal::start() { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { if (req->url() == "/") { - AsyncWebServerResponse *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); response->addHeader("Content-Encoding", "gzip"); req->send(response); return; @@ -91,5 +92,4 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo } // namespace captive_portal } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index c2aada171f..24d1295e6a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -1,9 +1,10 @@ #pragma once - -#ifdef USE_ARDUINO - +#include "esphome/core/defines.h" +#ifdef USE_CAPTIVE_PORTAL #include +#ifdef USE_ARDUINO #include +#endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" @@ -18,18 +19,22 @@ class CaptivePortal : public AsyncWebHandler, public Component { CaptivePortal(web_server_base::WebServerBase *base); void setup() override; void dump_config() override; +#ifdef USE_ARDUINO void loop() override { if (this->dns_server_ != nullptr) this->dns_server_->processNextRequest(); } +#endif float get_setup_priority() const override; void start(); bool is_active() const { return this->active_; } void end() { this->active_ = false; this->base_->deinit(); +#ifdef USE_ARDUINO this->dns_server_->stop(); this->dns_server_ = nullptr; +#endif } bool canHandle(AsyncWebServerRequest *request) override { @@ -58,12 +63,13 @@ class CaptivePortal : public AsyncWebHandler, public Component { web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; +#ifdef USE_ARDUINO std::unique_ptr dns_server_{nullptr}; +#endif }; extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace captive_portal } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index cb5c1108ba..66d9274fe8 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -6,7 +6,7 @@ from esphome.const import ( ICON_RADIATOR, ICON_RESTART, DEVICE_CLASS_CARBON_DIOXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, @@ -43,7 +43,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( @@ -69,16 +69,16 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_TVOC]) cg.add(var.set_tvoc(sens)) - if CONF_VERSION in config: - sens = await text_sensor.new_text_sensor(config[CONF_VERSION]) + if version_config := config.get(CONF_VERSION): + sens = await text_sensor.new_text_sensor(version_config) cg.add(var.set_version(sens)) - if CONF_BASELINE in config: - cg.add(var.set_baseline(config[CONF_BASELINE])) + if (baseline := config.get(CONF_BASELINE)) is not None: + cg.add(var.set_baseline(baseline)) - if CONF_TEMPERATURE in config: - sens = await cg.get_variable(config[CONF_TEMPERATURE]) + if temperature_id := config.get(CONF_TEMPERATURE): + sens = await cg.get_variable(temperature_id) cg.add(var.set_temperature(sens)) - if CONF_HUMIDITY in config: - sens = await cg.get_variable(config[CONF_HUMIDITY]) + if humidity_id := config.get(CONF_HUMIDITY): + sens = await cg.get_variable(humidity_id) cg.add(var.set_humidity(sens)) diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp index ea789c2d8c..f7629df3ca 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.cpp +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -1,5 +1,6 @@ #include "cd74hc4067.h" #include "esphome/core/log.h" +#include namespace esphome { namespace cd74hc4067 { @@ -27,7 +28,7 @@ void CD74HC4067Component::dump_config() { 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: %d", this->switch_delay_); + ESP_LOGCONFIG(TAG, "switch delay: %" PRIu32, this->switch_delay_); } void CD74HC4067Component::activate_pin(uint8_t pin) { diff --git a/esphome/components/ch422g/__init__.py b/esphome/components/ch422g/__init__.py new file mode 100644 index 0000000000..6a7bace0a2 --- /dev/null +++ b/esphome/components/ch422g/__init__.py @@ -0,0 +1,76 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.i2c import I2CBus +import esphome.config_validation as cv +from esphome.const import ( + CONF_I2C_ID, + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, +) + +CODEOWNERS = ["@jesterret", "@clydebarrow"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +ch422g_ns = cg.esphome_ns.namespace("ch422g") + +CH422GComponent = ch422g_ns.class_("CH422GComponent", cg.Component, i2c.I2CDevice) +CH422GGPIOPin = ch422g_ns.class_( + "CH422GGPIOPin", cg.GPIOPin, cg.Parented.template(CH422GComponent) +) + +CONF_CH422G = "ch422g" + +# Note that no address is configurable - each register in the CH422G has a dedicated i2c address +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(CH422GComponent), + cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + # Can't use register_i2c_device because there is no CONF_ADDRESS + parent = await cg.get_variable(config[CONF_I2C_ID]) + cg.add(var.set_i2c_bus(parent)) + + +# This is used as a final validation step so that modes have been fully transformed. +def pin_mode_check(pin_config, _): + if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8: + raise cv.Invalid("CH422G only supports input on pins 0-7") + if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8: + raise cv.Invalid("CH422G only supports open drain output on pins 8-11") + + +CH422G_PIN_SCHEMA = pins.gpio_base_schema( + CH422GGPIOPin, + cv.int_range(min=0, max=11), + modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN], +).extend( + { + cv.Required(CONF_CH422G): cv.use_id(CH422GComponent), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA, pin_mode_check) +async def ch422g_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_CH422G]) + + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp new file mode 100644 index 0000000000..0db179d99e --- /dev/null +++ b/esphome/components/ch422g/ch422g.cpp @@ -0,0 +1,139 @@ +#include "ch422g.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ch422g { + +static const uint8_t CH422G_REG_MODE = 0x24; +static const uint8_t CH422G_MODE_OUTPUT = 0x01; // enables output mode on 0-7 +static const uint8_t CH422G_MODE_OPEN_DRAIN = 0x04; // enables open drain mode on 8-11 +static const uint8_t CH422G_REG_IN = 0x26; // read reg for input bits +static const uint8_t CH422G_REG_OUT = 0x38; // write reg for output bits 0-7 +static const uint8_t CH422G_REG_OUT_UPPER = 0x23; // write reg for output bits 8-11 + +static const char *const TAG = "ch422g"; + +void CH422GComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up CH422G..."); + // set outputs before mode + this->write_outputs_(); + // Set mode and check for errors + if (!this->set_mode_(this->mode_value_) || !this->read_inputs_()) { + ESP_LOGE(TAG, "CH422G not detected at 0x%02X", this->address_); + this->mark_failed(); + return; + } + + ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); +} + +void CH422GComponent::loop() { + // Clear all the previously read flags. + this->pin_read_flags_ = 0x00; +} + +void CH422GComponent::dump_config() { + ESP_LOGCONFIG(TAG, "CH422G:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with CH422G failed!"); + } +} + +void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) { + if (pin < 8) { + if (flags & gpio::FLAG_OUTPUT) { + this->mode_value_ |= CH422G_MODE_OUTPUT; + } + } else { + if (flags & gpio::FLAG_OPEN_DRAIN) { + this->mode_value_ |= CH422G_MODE_OPEN_DRAIN; + } + } +} + +bool CH422GComponent::digital_read(uint8_t pin) { + if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) { + // Read values on first access or in case it's being read again in the same loop + this->read_inputs_(); + } + + this->pin_read_flags_ |= (1 << pin); + return (this->input_bits_ & (1 << pin)) != 0; +} + +void CH422GComponent::digital_write(uint8_t pin, bool value) { + if (value) { + this->output_bits_ |= (1 << pin); + } else { + this->output_bits_ &= ~(1 << pin); + } + this->write_outputs_(); +} + +bool CH422GComponent::read_inputs_() { + if (this->is_failed()) { + return false; + } + uint8_t result; + // reading inputs requires the chip to be in input mode, possibly temporarily. + if (this->mode_value_ & CH422G_MODE_OUTPUT) { + this->set_mode_(this->mode_value_ & ~CH422G_MODE_OUTPUT); + result = this->read_reg_(CH422G_REG_IN); + this->set_mode_(this->mode_value_); + } else { + result = this->read_reg_(CH422G_REG_IN); + } + this->input_bits_ = result; + this->status_clear_warning(); + return true; +} + +// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address. +bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { + auto err = this->bus_->write(reg, &value, 1); + if (err != i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str()); + return false; + } + this->status_clear_warning(); + return true; +} + +uint8_t CH422GComponent::read_reg_(uint8_t reg) { + uint8_t value; + auto err = this->bus_->read(reg, &value, 1); + if (err != i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str()); + return 0; + } + this->status_clear_warning(); + return value; +} + +bool CH422GComponent::set_mode_(uint8_t mode) { return this->write_reg_(CH422G_REG_MODE, mode); } + +bool CH422GComponent::write_outputs_() { + return this->write_reg_(CH422G_REG_OUT, static_cast(this->output_bits_)) && + this->write_reg_(CH422G_REG_OUT_UPPER, static_cast(this->output_bits_ >> 8)); +} + +float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } + +// Run our loop() method very early in the loop, so that we cache read values +// before other components call our digital_read() method. +float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI + +void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +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_); } +void CH422GGPIOPin::set_flags(gpio::Flags flags) { + flags_ = flags; + this->parent_->pin_mode(this->pin_, flags); +} + +} // namespace ch422g +} // namespace esphome diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h new file mode 100644 index 0000000000..30780e09ad --- /dev/null +++ b/esphome/components/ch422g/ch422g.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ch422g { + +class CH422GComponent : public Component, public i2c::I2CDevice { + public: + CH422GComponent() = default; + + /// Check i2c availability and setup masks + void setup() override; + /// Poll for input changes periodically + void loop() override; + /// Helper function to read the value of a pin. + bool digital_read(uint8_t pin); + /// Helper function to write the value of a pin. + void digital_write(uint8_t pin, bool value); + /// Helper function to set the pin mode of a pin. + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + float get_loop_priority() const override; + void dump_config() override; + + protected: + bool write_reg_(uint8_t reg, uint8_t value); + uint8_t read_reg_(uint8_t reg); + bool set_mode_(uint8_t mode); + bool read_inputs_(); + bool write_outputs_(); + + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint16_t output_bits_{0x00}; + /// Flags to check if read previously during this loop + uint8_t pin_read_flags_ = {0x00}; + /// Copy of last read values + uint8_t input_bits_ = {0x00}; + /// Copy of the mode value + uint8_t mode_value_{}; +}; + +/// Helper class to expose a CH422G pin as a GPIO pin. +class CH422GGPIOPin : public GPIOPin { + public: + void setup() override{}; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(CH422GComponent *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags); + + protected: + CH422GComponent *parent_{}; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; +}; + +} // namespace ch422g +} // namespace esphome diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index bf167fe837..ec68940726 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -1,13 +1,13 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.cpp_helpers import setup_entity from esphome import automation -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ACTION_STATE_TOPIC, CONF_AWAY, CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, + CONF_CURRENT_HUMIDITY_STATE_TOPIC, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -20,6 +20,7 @@ from esphome.const import ( CONF_MODE, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, + CONF_MQTT_ID, CONF_ON_CONTROL, CONF_ON_STATE, CONF_PRESET, @@ -28,21 +29,24 @@ from esphome.const import ( CONF_SWING_MODE, CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_STATE_TOPIC, + CONF_TARGET_HUMIDITY_COMMAND_TOPIC, + CONF_TARGET_HUMIDITY_STATE_TOPIC, CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, - CONF_TARGET_TEMPERATURE_STATE_TOPIC, CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC, CONF_TARGET_TEMPERATURE_LOW, CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC, + CONF_TARGET_TEMPERATURE_STATE_TOPIC, CONF_TEMPERATURE_STEP, CONF_TRIGGER_ID, CONF_VISUAL, - CONF_MQTT_ID, + CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -106,16 +110,30 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) CONF_CURRENT_TEMPERATURE = "current_temperature" +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)?" ) -def single_visual_temperature(value): - if isinstance(value, dict): - return value +VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema( + { + cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature, + cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature, + } +) + +def visual_temperature_step(value): + + # Allow defining target/current temperature steps separately + if isinstance(value, dict): + return VISUAL_TEMPERATURE_STEP_SCHEMA(value) + + # Otherwise, use the single value for both properties value = visual_temperature(value) return VISUAL_TEMPERATURE_STEP_SCHEMA( { @@ -127,95 +145,104 @@ def single_visual_temperature(value): # Actions ControlAction = climate_ns.class_("ControlAction", automation.Action) -StateTrigger = climate_ns.class_("StateTrigger", automation.Trigger.template()) -ControlTrigger = climate_ns.class_("ControlTrigger", automation.Trigger.template()) - -VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any( - single_visual_temperature, - cv.Schema( - { - cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature, - cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature, - } - ), +StateTrigger = climate_ns.class_( + "StateTrigger", automation.Trigger.template(Climate.operator("ref")) +) +ControlTrigger = climate_ns.class_( + "ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref")) ) -CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.GenerateID(): cv.declare_id(Climate), - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent), - cv.Optional(CONF_VISUAL, default={}): cv.Schema( - { - cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, - cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, - cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, - } - ), - cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_AWAY_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_AWAY_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_FAN_MODE_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_MODE_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_MODE_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_PRESET_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_PRESET_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_SWING_MODE_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_SWING_MODE_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_TARGET_TEMPERATURE_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_TARGET_TEMPERATURE_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_ON_CONTROL): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), - } - ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - } +CLIMATE_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(Climate), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent), + cv.Optional(CONF_VISUAL, default={}): cv.Schema( + { + cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, + cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, + cv.Optional(CONF_TEMPERATURE_STEP): visual_temperature_step, + cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int, + cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int, + } + ), + cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_AWAY_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_AWAY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_CURRENT_HUMIDITY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_FAN_MODE_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_MODE_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_MODE_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_PRESET_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_PRESET_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_SWING_MODE_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_SWING_MODE_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_TEMPERATURE_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_TEMPERATURE_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_ON_CONTROL): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), + } + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + } + ), + } + ) ) @@ -223,110 +250,167 @@ async def setup_climate_core_(var, config): await setup_entity(var, config) visual = config[CONF_VISUAL] - if CONF_MIN_TEMPERATURE in visual: - cg.add(var.set_visual_min_temperature_override(visual[CONF_MIN_TEMPERATURE])) - if CONF_MAX_TEMPERATURE in visual: - cg.add(var.set_visual_max_temperature_override(visual[CONF_MAX_TEMPERATURE])) - if CONF_TEMPERATURE_STEP in visual: + if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: + cg.add(var.set_visual_min_temperature_override(min_temp)) + if (max_temp := visual.get(CONF_MAX_TEMPERATURE)) is not None: + cg.add(var.set_visual_max_temperature_override(max_temp)) + if (temp_step := visual.get(CONF_TEMPERATURE_STEP)) is not None: cg.add( var.set_visual_temperature_step_override( - visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE], - visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], + temp_step[CONF_TARGET_TEMPERATURE], + temp_step[CONF_CURRENT_TEMPERATURE], ) ) + if (min_humidity := visual.get(CONF_MIN_HUMIDITY)) is not None: + cg.add(var.set_visual_min_humidity_override(min_humidity)) + if (max_humidity := visual.get(CONF_MAX_HUMIDITY)) is not None: + cg.add(var.set_visual_max_humidity_override(max_humidity)) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) - if CONF_ACTION_STATE_TOPIC in config: - cg.add(mqtt_.set_custom_action_state_topic(config[CONF_ACTION_STATE_TOPIC])) - if CONF_AWAY_COMMAND_TOPIC in config: - cg.add(mqtt_.set_custom_away_command_topic(config[CONF_AWAY_COMMAND_TOPIC])) - if CONF_AWAY_STATE_TOPIC in config: - cg.add(mqtt_.set_custom_away_state_topic(config[CONF_AWAY_STATE_TOPIC])) - if CONF_CURRENT_TEMPERATURE_STATE_TOPIC in config: + if (action_state_topic := config.get(CONF_ACTION_STATE_TOPIC)) is not None: + cg.add(mqtt_.set_custom_action_state_topic(action_state_topic)) + if (away_command_topic := config.get(CONF_AWAY_COMMAND_TOPIC)) is not None: + cg.add(mqtt_.set_custom_away_command_topic(away_command_topic)) + if (away_state_topic := config.get(CONF_AWAY_STATE_TOPIC)) is not None: + cg.add(mqtt_.set_custom_away_state_topic(away_state_topic)) + if ( + current_temperature_state_topic := config.get( + CONF_CURRENT_TEMPERATURE_STATE_TOPIC + ) + ) is not None: cg.add( mqtt_.set_custom_current_temperature_state_topic( - config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] + current_temperature_state_topic ) ) - if CONF_FAN_MODE_COMMAND_TOPIC in config: + if ( + current_humidity_state_topic := config.get( + CONF_CURRENT_HUMIDITY_STATE_TOPIC + ) + ) is not None: cg.add( - mqtt_.set_custom_fan_mode_command_topic( - config[CONF_FAN_MODE_COMMAND_TOPIC] + mqtt_.set_custom_current_humidity_state_topic( + current_humidity_state_topic ) ) - if CONF_FAN_MODE_STATE_TOPIC in config: - cg.add( - mqtt_.set_custom_fan_mode_state_topic(config[CONF_FAN_MODE_STATE_TOPIC]) + if ( + fan_mode_command_topic := config.get(CONF_FAN_MODE_COMMAND_TOPIC) + ) is not None: + cg.add(mqtt_.set_custom_fan_mode_command_topic(fan_mode_command_topic)) + if (fan_mode_state_topic := config.get(CONF_FAN_MODE_STATE_TOPIC)) is not None: + cg.add(mqtt_.set_custom_fan_mode_state_topic(fan_mode_state_topic)) + if (mode_command_topic := config.get(CONF_MODE_COMMAND_TOPIC)) is not None: + cg.add(mqtt_.set_custom_mode_command_topic(mode_command_topic)) + if (mode_state_topic := config.get(CONF_MODE_STATE_TOPIC)) is not None: + cg.add(mqtt_.set_custom_mode_state_topic(mode_state_topic)) + if (preset_command_topic := config.get(CONF_PRESET_COMMAND_TOPIC)) is not None: + cg.add(mqtt_.set_custom_preset_command_topic(preset_command_topic)) + if (preset_state_topic := config.get(CONF_PRESET_STATE_TOPIC)) is not None: + cg.add(mqtt_.set_custom_preset_state_topic(preset_state_topic)) + if ( + swing_mode_command_topic := config.get(CONF_SWING_MODE_COMMAND_TOPIC) + ) is not None: + cg.add(mqtt_.set_custom_swing_mode_command_topic(swing_mode_command_topic)) + if ( + swing_mode_state_topic := config.get(CONF_SWING_MODE_STATE_TOPIC) + ) is not None: + cg.add(mqtt_.set_custom_swing_mode_state_topic(swing_mode_state_topic)) + if ( + target_temperature_command_topic := config.get( + CONF_TARGET_TEMPERATURE_COMMAND_TOPIC ) - if CONF_MODE_COMMAND_TOPIC in config: - cg.add(mqtt_.set_custom_mode_command_topic(config[CONF_MODE_COMMAND_TOPIC])) - if CONF_MODE_STATE_TOPIC in config: - cg.add(mqtt_.set_custom_mode_state_topic(config[CONF_MODE_STATE_TOPIC])) - if CONF_PRESET_COMMAND_TOPIC in config: - cg.add( - mqtt_.set_custom_preset_command_topic(config[CONF_PRESET_COMMAND_TOPIC]) - ) - if CONF_PRESET_STATE_TOPIC in config: - cg.add(mqtt_.set_custom_preset_state_topic(config[CONF_PRESET_STATE_TOPIC])) - if CONF_SWING_MODE_COMMAND_TOPIC in config: - cg.add( - mqtt_.set_custom_swing_mode_command_topic( - config[CONF_SWING_MODE_COMMAND_TOPIC] - ) - ) - if CONF_SWING_MODE_STATE_TOPIC in config: - cg.add( - mqtt_.set_custom_swing_mode_state_topic( - config[CONF_SWING_MODE_STATE_TOPIC] - ) - ) - if CONF_TARGET_TEMPERATURE_COMMAND_TOPIC in config: + ) is not None: cg.add( mqtt_.set_custom_target_temperature_command_topic( - config[CONF_TARGET_TEMPERATURE_COMMAND_TOPIC] + target_temperature_command_topic ) ) - if CONF_TARGET_TEMPERATURE_STATE_TOPIC in config: + if ( + target_temperature_state_topic := config.get( + CONF_TARGET_TEMPERATURE_STATE_TOPIC + ) + ) is not None: cg.add( mqtt_.set_custom_target_temperature_state_topic( - config[CONF_TARGET_TEMPERATURE_STATE_TOPIC] + target_temperature_state_topic ) ) - if CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC in config: + if ( + target_temperature_high_command_topic := config.get( + CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC + ) + ) is not None: cg.add( mqtt_.set_custom_target_temperature_high_command_topic( - config[CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC] + target_temperature_high_command_topic ) ) - if CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC in config: + if ( + target_temperature_high_state_topic := config.get( + CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC + ) + ) is not None: cg.add( mqtt_.set_custom_target_temperature_high_state_topic( - config[CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC] + target_temperature_high_state_topic ) ) - if CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC in config: + if ( + target_temperature_low_command_topic := config.get( + CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC + ) + ) is not None: cg.add( mqtt_.set_custom_target_temperature_low_command_topic( - config[CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC] + target_temperature_low_command_topic ) ) - if CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC in config: + if ( + target_temperature_low_state_topic := config.get( + CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC + ) + ) is not None: cg.add( mqtt_.set_custom_target_temperature_state_topic( - config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] + target_temperature_low_state_topic + ) + ) + if ( + target_humidity_command_topic := config.get( + CONF_TARGET_HUMIDITY_COMMAND_TOPIC + ) + ) is not None: + cg.add( + mqtt_.set_custom_target_humidity_command_topic( + target_humidity_command_topic + ) + ) + if ( + target_humidity_state_topic := config.get(CONF_TARGET_HUMIDITY_STATE_TOPIC) + ) is not None: + cg.add( + mqtt_.set_custom_target_humidity_state_topic( + target_humidity_state_topic ) ) for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_automation( + trigger, [(Climate.operator("ref"), "x")], conf + ) for conf in config.get(CONF_ON_CONTROL, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_automation( + trigger, [(ClimateCall.operator("ref"), "x")], conf + ) + + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) async def register_climate(var, config): @@ -343,6 +427,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int), cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( validate_climate_fan_mode @@ -363,42 +448,35 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( async def climate_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_MODE in config: - template_ = await cg.templatable(config[CONF_MODE], args, ClimateMode) + if (mode := config.get(CONF_MODE)) is not None: + template_ = await cg.templatable(mode, args, ClimateMode) cg.add(var.set_mode(template_)) - if CONF_TARGET_TEMPERATURE in config: - template_ = await cg.templatable(config[CONF_TARGET_TEMPERATURE], args, float) + if (target_temp := config.get(CONF_TARGET_TEMPERATURE)) is not None: + template_ = await cg.templatable(target_temp, args, float) cg.add(var.set_target_temperature(template_)) - if CONF_TARGET_TEMPERATURE_LOW in config: - template_ = await cg.templatable( - config[CONF_TARGET_TEMPERATURE_LOW], args, float - ) + if (target_temp_low := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None: + template_ = await cg.templatable(target_temp_low, args, float) cg.add(var.set_target_temperature_low(template_)) - if CONF_TARGET_TEMPERATURE_HIGH in config: - template_ = await cg.templatable( - config[CONF_TARGET_TEMPERATURE_HIGH], args, float - ) + if (target_temp_high := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None: + template_ = await cg.templatable(target_temp_high, args, float) cg.add(var.set_target_temperature_high(template_)) - if CONF_FAN_MODE in config: - template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) + if (target_humidity := config.get(CONF_TARGET_HUMIDITY)) is not None: + template_ = await cg.templatable(target_humidity, args, float) + cg.add(var.set_target_humidity(template_)) + if (fan_mode := config.get(CONF_FAN_MODE)) is not None: + template_ = await cg.templatable(fan_mode, args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) - if CONF_CUSTOM_FAN_MODE in config: - template_ = await cg.templatable( - config[CONF_CUSTOM_FAN_MODE], args, cg.std_string - ) + if (custom_fan_mode := config.get(CONF_CUSTOM_FAN_MODE)) is not None: + template_ = await cg.templatable(custom_fan_mode, args, cg.std_string) cg.add(var.set_custom_fan_mode(template_)) - if CONF_PRESET in config: - template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset) + if (preset := config.get(CONF_PRESET)) is not None: + template_ = await cg.templatable(preset, args, ClimatePreset) cg.add(var.set_preset(template_)) - if CONF_CUSTOM_PRESET in config: - template_ = await cg.templatable( - config[CONF_CUSTOM_PRESET], args, cg.std_string - ) + if (custom_preset := config.get(CONF_CUSTOM_PRESET)) is not None: + template_ = await cg.templatable(custom_preset, args, cg.std_string) cg.add(var.set_custom_preset(template_)) - if CONF_SWING_MODE in config: - template_ = await cg.templatable( - config[CONF_SWING_MODE], args, ClimateSwingMode - ) + if (swing_mode := config.get(CONF_SWING_MODE)) is not None: + template_ = await cg.templatable(swing_mode, args, ClimateSwingMode) cg.add(var.set_swing_mode(template_)) return var diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 9b06563eb4..a4d13ade58 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -14,6 +14,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_high) + TEMPLATABLE_VALUE(float, target_humidity) TEMPLATABLE_VALUE(bool, away) TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) TEMPLATABLE_VALUE(std::string, custom_fan_mode) @@ -27,6 +28,7 @@ template class ControlAction : public Action { call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); + call.set_target_humidity(this->target_humidity_.optional_value(x...)); if (away_.has_value()) { call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); } @@ -42,17 +44,17 @@ template class ControlAction : public Action { Climate *climate_; }; -class ControlTrigger : public Trigger<> { +class ControlTrigger : public Trigger { public: ControlTrigger(Climate *climate) { - climate->add_on_control_callback([this]() { this->trigger(); }); + climate->add_on_control_callback([this](ClimateCall &x) { this->trigger(x); }); } }; -class StateTrigger : public Trigger<> { +class StateTrigger : public Trigger { public: StateTrigger(Climate *climate) { - climate->add_on_state_callback([this]() { this->trigger(); }); + climate->add_on_state_callback([this](Climate &x) { this->trigger(x); }); } }; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index a032596eb3..bc8d932089 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -7,6 +7,7 @@ namespace climate { static const char *const TAG = "climate"; void ClimateCall::perform() { + this->parent_->control_callback_.call(*this); ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { @@ -44,7 +45,9 @@ void ClimateCall::perform() { if (this->target_temperature_high_.has_value()) { ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } - this->parent_->control_callback_.call(); + if (this->target_humidity_.has_value()) { + ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_); + } this->parent_->control(*this); } void ClimateCall::validate_() { @@ -213,6 +216,8 @@ ClimateCall &ClimateCall::set_preset(const std::string &preset) { this->set_preset(CLIMATE_PRESET_SLEEP); } else if (str_equals_case_insensitive(preset, "ACTIVITY")) { this->set_preset(CLIMATE_PRESET_ACTIVITY); + } else if (str_equals_case_insensitive(preset, "NONE")) { + this->set_preset(CLIMATE_PRESET_NONE); } else { if (this->parent_->get_traits().supports_custom_preset(preset)) { this->custom_preset_ = preset; @@ -260,10 +265,16 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h this->target_temperature_high_ = target_temperature_high; return *this; } +ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { + this->target_humidity_ = target_humidity; + return *this; +} + const optional &ClimateCall::get_mode() const { return this->mode_; } const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } +const optional &ClimateCall::get_target_humidity() const { return this->target_humidity_; } const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } @@ -281,6 +292,10 @@ ClimateCall &ClimateCall::set_target_temperature(optional target_temperat this->target_temperature_ = target_temperature; return *this; } +ClimateCall &ClimateCall::set_target_humidity(optional target_humidity) { + this->target_humidity_ = target_humidity; + return *this; +} ClimateCall &ClimateCall::set_mode(optional mode) { this->mode_ = mode; return *this; @@ -300,11 +315,11 @@ ClimateCall &ClimateCall::set_swing_mode(optional swing_mode) return *this; } -void Climate::add_on_state_callback(std::function &&callback) { +void Climate::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -void Climate::add_on_control_callback(std::function &&callback) { +void Climate::add_on_control_callback(std::function &&callback) { this->control_callback_.add(std::move(callback)); } @@ -341,6 +356,9 @@ void Climate::save_state_() { } else { state.target_temperature = this->target_temperature; } + if (traits.get_supports_target_humidity()) { + state.target_humidity = this->target_humidity; + } if (traits.get_supports_fan_modes() && fan_mode.has_value()) { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); @@ -406,9 +424,15 @@ void Climate::publish_state() { } else { ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); } + if (traits.get_supports_current_humidity()) { + ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); + } + if (traits.get_supports_target_humidity()) { + ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); + } // Send state to frontend - this->state_callback_.call(); + this->state_callback_.call(*this); // Save state this->save_state_(); } @@ -425,6 +449,12 @@ ClimateTraits Climate::get_traits() { 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 (this->visual_max_humidity_override_.has_value()) { + traits.set_visual_max_humidity(*this->visual_max_humidity_override_); + } return traits; } @@ -439,6 +469,12 @@ void Climate::set_visual_temperature_step_override(float target, float current) this->visual_target_temperature_step_override_ = target; this->visual_current_temperature_step_override_ = current; } +void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { + this->visual_min_humidity_override_ = visual_min_humidity_override; +} +void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { + this->visual_max_humidity_override_ = visual_max_humidity_override; +} ClimateCall Climate::make_call() { return ClimateCall(this); } @@ -452,6 +488,9 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { } else { call.set_target_temperature(this->target_temperature); } + if (traits.get_supports_target_humidity()) { + call.set_target_humidity(this->target_humidity); + } if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { call.set_fan_mode(this->fan_mode); } @@ -472,6 +511,9 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { } else { climate->target_temperature = this->target_temperature; } + if (traits.get_supports_target_humidity()) { + climate->target_humidity = this->target_humidity; + } if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { climate->fan_mode = this->fan_mode; } @@ -528,17 +570,29 @@ void Climate::dump_traits_(const char *tag) { auto traits = this->get_traits(); ESP_LOGCONFIG(tag, "ClimateTraits:"); ESP_LOGCONFIG(tag, " [x] Visual settings:"); - ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); - ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); - ESP_LOGCONFIG(tag, " - Step:"); + ESP_LOGCONFIG(tag, " - Min temperature: %.1f", traits.get_visual_min_temperature()); + ESP_LOGCONFIG(tag, " - Max temperature: %.1f", traits.get_visual_max_temperature()); + ESP_LOGCONFIG(tag, " - Temperature step:"); ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); - ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); if (traits.get_supports_current_temperature()) { - ESP_LOGCONFIG(tag, " [x] Supports current temperature"); + ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); + } + if (traits.get_supports_target_humidity() || traits.get_supports_current_humidity()) { + ESP_LOGCONFIG(tag, " - Min humidity: %.0f", traits.get_visual_min_humidity()); + ESP_LOGCONFIG(tag, " - Max humidity: %.0f", traits.get_visual_max_humidity()); } if (traits.get_supports_two_point_target_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); } + if (traits.get_supports_current_temperature()) { + ESP_LOGCONFIG(tag, " [x] Supports current temperature"); + } + if (traits.get_supports_target_humidity()) { + ESP_LOGCONFIG(tag, " [x] Supports target humidity"); + } + if (traits.get_supports_current_humidity()) { + ESP_LOGCONFIG(tag, " [x] Supports current humidity"); + } if (traits.get_supports_action()) { ESP_LOGCONFIG(tag, " [x] Supports action"); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 656e1c4852..d81702fb0c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,6 +64,10 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional target_temperature_high); + /// Set the target humidity of the climate device. + ClimateCall &set_target_humidity(float target_humidity); + /// Set the target humidity of the climate device. + ClimateCall &set_target_humidity(optional target_humidity); /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(ClimateFanMode fan_mode); /// Set the fan mode of the climate device. @@ -93,6 +97,7 @@ class ClimateCall { const optional &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; + const optional &get_target_humidity() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_custom_fan_mode() const; @@ -107,6 +112,7 @@ class ClimateCall { optional target_temperature_; optional target_temperature_low_; optional target_temperature_high_; + optional target_humidity_; optional fan_mode_; optional swing_mode_; optional custom_fan_mode_; @@ -135,7 +141,8 @@ struct ClimateDeviceRestoreState { float target_temperature_low; float target_temperature_high; }; - }; + } __attribute__((packed)); + float target_humidity; /// Convert this struct to a climate call that can be performed. ClimateCall to_call(Climate *climate); @@ -160,24 +167,34 @@ struct ClimateDeviceRestoreState { */ class Climate : public EntityBase { public: + Climate() {} + /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; + /// The active state of the climate device. ClimateAction action{CLIMATE_ACTION_OFF}; + /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; + /// The current humidity of the climate device, as reported from the integration. + float current_humidity{NAN}; + union { /// The target temperature of the climate device. float target_temperature; struct { /// The minimum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_low; + float target_temperature_low{NAN}; /// The maximum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_high; + float target_temperature_high{NAN}; }; }; + /// The target humidity of the climate device. + float target_humidity; + /// The active fan mode of the climate device. optional fan_mode; @@ -198,7 +215,7 @@ class Climate : public EntityBase { * * @param callback The callback to call. */ - void add_on_state_callback(std::function &&callback); + void add_on_state_callback(std::function &&callback); /** * Add a callback for the climate device configuration; each time the configuration parameters of a climate device @@ -206,7 +223,7 @@ class Climate : public EntityBase { * * @param callback The callback to call. */ - void add_on_control_callback(std::function &&callback); + void add_on_control_callback(std::function &&callback); /** Make a climate device control call, this is used to control the climate device, see the ClimateCall description * for more info. @@ -231,6 +248,8 @@ class Climate : public EntityBase { 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); protected: friend ClimateCall; @@ -273,13 +292,15 @@ class Climate : public EntityBase { void dump_traits_(const char *tag); - CallbackManager state_callback_{}; - CallbackManager control_callback_{}; + CallbackManager state_callback_{}; + CallbackManager 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_{}; }; } // namespace climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index e8c2db6c06..58d7b586d7 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -44,10 +44,18 @@ class ClimateTraits { void set_supports_current_temperature(bool supports_current_temperature) { supports_current_temperature_ = supports_current_temperature; } + bool get_supports_current_humidity() const { return supports_current_humidity_; } + void set_supports_current_humidity(bool supports_current_humidity) { + supports_current_humidity_ = supports_current_humidity; + } bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { supports_two_point_target_temperature_ = supports_two_point_target_temperature; } + bool get_supports_target_humidity() const { return supports_target_humidity_; } + void set_supports_target_humidity(bool supports_target_humidity) { + supports_target_humidity_ = supports_target_humidity; + } void set_supported_modes(std::set modes) { supported_modes_ = std::move(modes); } void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") @@ -65,7 +73,7 @@ class ClimateTraits { ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); } bool supports_mode(ClimateMode mode) const { return supported_modes_.count(mode); } - std::set get_supported_modes() const { return supported_modes_; } + const std::set &get_supported_modes() const { return supported_modes_; } void set_supports_action(bool supports_action) { supports_action_ = supports_action; } bool get_supports_action() const { return supports_action_; } @@ -93,7 +101,7 @@ class ClimateTraits { void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { return !supported_fan_modes_.empty() || !supported_custom_fan_modes_.empty(); } - std::set get_supported_fan_modes() const { return supported_fan_modes_; } + const std::set &get_supported_fan_modes() const { return supported_fan_modes_; } void set_supported_custom_fan_modes(std::set supported_custom_fan_modes) { supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); @@ -132,7 +140,7 @@ class ClimateTraits { } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); } bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); } - std::set get_supported_swing_modes() const { return supported_swing_modes_; } + const std::set &get_supported_swing_modes() const { return supported_swing_modes_; } float get_visual_min_temperature() const { return visual_min_temperature_; } void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; } @@ -153,6 +161,11 @@ class ClimateTraits { int8_t get_target_temperature_accuracy_decimals() const; int8_t get_current_temperature_accuracy_decimals() const; + float get_visual_min_humidity() const { return visual_min_humidity_; } + void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; } + float get_visual_max_humidity() const { return visual_max_humidity_; } + void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; } + protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { @@ -177,7 +190,9 @@ class ClimateTraits { } bool supports_current_temperature_{false}; + bool supports_current_humidity_{false}; bool supports_two_point_target_temperature_{false}; + bool supports_target_humidity_{false}; std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; bool supports_action_{false}; std::set supported_fan_modes_; @@ -190,6 +205,8 @@ class ClimateTraits { float visual_max_temperature_{30}; float visual_target_temperature_step_{0.1}; float visual_current_temperature_step_{0.1}; + float visual_min_humidity_{30}; + float visual_max_humidity_{99}; }; } // namespace climate diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 1389ebfc6d..c7c286d679 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -1,38 +1,37 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import ( - climate, - remote_transmitter, - remote_receiver, - sensor, - remote_base, -) -from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID +from esphome.components import climate, sensor, remote_base from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR +DEPENDENCIES = ["remote_transmitter"] AUTO_LOAD = ["sensor", "remote_base"] CODEOWNERS = ["@glmnet"] climate_ir_ns = cg.esphome_ns.namespace("climate_ir") ClimateIR = climate_ir_ns.class_( - "ClimateIR", climate.Climate, cg.Component, remote_base.RemoteReceiverListener + "ClimateIR", + climate.Climate, + cg.Component, + remote_base.RemoteReceiverListener, + remote_base.RemoteTransmittable, ) -CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend( - { - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id( - remote_transmitter.RemoteTransmitterComponent - ), - 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), - } -).extend(cv.COMPONENT_SCHEMA) +CLIMATE_IR_SCHEMA = ( + climate.CLIMATE_SCHEMA.extend( + { + 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), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA) +) CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( { - cv.Optional(CONF_RECEIVER_ID): cv.use_id( - remote_receiver.RemoteReceiverComponent + cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( + remote_base.RemoteReceiverBase ), } ) @@ -41,15 +40,11 @@ CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( async def register_climate_ir(var, config): await cg.register_component(var, config) await climate.register_climate(var, config) - + await remote_base.register_transmittable(var, config) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) - if CONF_SENSOR in config: - sens = await cg.get_variable(config[CONF_SENSOR]) + if remote_base.CONF_RECEIVER_ID in config: + await remote_base.register_listener(var, config) + if sensor_id := config.get(CONF_SENSOR): + sens = await cg.get_variable(sensor_id) cg.add(var.set_sensor(sens)) - if CONF_RECEIVER_ID in config: - receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) - cg.add(receiver.register_listener(var)) - - transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID]) - cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 5be4fc06f5..ea0656121f 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -18,7 +18,10 @@ namespace climate_ir { Likewise to decode a IR into the AC state, implement bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true */ -class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { +class ClimateIR : public Component, + public climate::Climate, + public remote_base::RemoteReceiverListener, + public remote_base::RemoteTransmittable { public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, @@ -35,9 +38,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: void setup() override; void dump_config() override; - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } 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; } @@ -64,7 +64,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: std::set swing_modes_ = {}; std::set presets_ = {}; - remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; }; diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp index a41aad1bd0..c65f24ebc0 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -6,18 +6,24 @@ namespace climate_ir_lg { static const char *const TAG = "climate.climate_ir_lg"; -const uint32_t COMMAND_ON = 0x00000; -const uint32_t COMMAND_ON_AI = 0x03000; -const uint32_t COMMAND_COOL = 0x08000; -const uint32_t COMMAND_HEAT = 0x0C000; +// Commands +const uint32_t COMMAND_MASK = 0xFF000; const uint32_t COMMAND_OFF = 0xC0000; const uint32_t COMMAND_SWING = 0x10000; -// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. -const uint32_t COMMAND_AUTO = 0x0B000; -const uint32_t COMMAND_DRY_FAN = 0x09000; -const uint32_t COMMAND_MASK = 0xFF000; +const uint32_t COMMAND_ON_COOL = 0x00000; +const uint32_t COMMAND_ON_DRY = 0x01000; +const uint32_t COMMAND_ON_FAN_ONLY = 0x02000; +const uint32_t COMMAND_ON_AI = 0x03000; +const uint32_t COMMAND_ON_HEAT = 0x04000; +const uint32_t COMMAND_COOL = 0x08000; +const uint32_t COMMAND_DRY = 0x09000; +const uint32_t COMMAND_FAN_ONLY = 0x0A000; +const uint32_t COMMAND_AI = 0x0B000; +const uint32_t COMMAND_HEAT = 0x0C000; + +// Fan speed const uint32_t FAN_MASK = 0xF0; const uint32_t FAN_AUTO = 0x50; const uint32_t FAN_MIN = 0x00; @@ -35,69 +41,67 @@ void LgIrClimate::transmit_state() { uint32_t remote_state = 0x8800000; // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_); + + // Set command if (send_swing_cmd_) { send_swing_cmd_ = false; remote_state |= COMMAND_SWING; } else { - if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_HEAT_COOL) { - remote_state |= COMMAND_ON_AI; - } else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) { - remote_state |= COMMAND_ON; - this->mode = climate::CLIMATE_MODE_COOL; - } else { - switch (this->mode) { - case climate::CLIMATE_MODE_COOL: - remote_state |= COMMAND_COOL; - break; - case climate::CLIMATE_MODE_HEAT: - remote_state |= COMMAND_HEAT; - break; - case climate::CLIMATE_MODE_HEAT_COOL: - remote_state |= COMMAND_AUTO; - break; - case climate::CLIMATE_MODE_DRY: - remote_state |= COMMAND_DRY_FAN; - break; - case climate::CLIMATE_MODE_OFF: - default: - remote_state |= COMMAND_OFF; - break; - } - } - mode_before_ = this->mode; - - ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode); - - if (this->mode == climate::CLIMATE_MODE_OFF) { - remote_state |= FAN_AUTO; - } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY || - this->mode == climate::CLIMATE_MODE_HEAT) { - switch (this->fan_mode.value()) { - case climate::CLIMATE_FAN_HIGH: - remote_state |= FAN_MAX; - break; - case climate::CLIMATE_FAN_MEDIUM: - remote_state |= FAN_MED; - break; - case climate::CLIMATE_FAN_LOW: - remote_state |= FAN_MIN; - break; - case climate::CLIMATE_FAN_AUTO: - default: - remote_state |= FAN_AUTO; - break; - } - } - - if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) { - this->fan_mode = climate::CLIMATE_FAN_AUTO; - // remote_state |= FAN_MODE_AUTO_DRY; - } - if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) { - auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); - remote_state |= ((temp - 15) << TEMP_SHIFT); + bool climate_is_off = (mode_before_ == climate::CLIMATE_MODE_OFF); + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL; + break; + case climate::CLIMATE_MODE_DRY: + remote_state |= climate_is_off ? COMMAND_ON_DRY : COMMAND_DRY; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state |= climate_is_off ? COMMAND_ON_FAN_ONLY : COMMAND_FAN_ONLY; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + remote_state |= climate_is_off ? COMMAND_ON_AI : COMMAND_AI; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state |= climate_is_off ? COMMAND_ON_HEAT : COMMAND_HEAT; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state |= COMMAND_OFF; + break; } } + + mode_before_ = this->mode; + + ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode); + + // Set fan speed + if (this->mode == climate::CLIMATE_MODE_OFF) { + remote_state |= FAN_AUTO; + } else { + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_HIGH: + remote_state |= FAN_MAX; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state |= FAN_MIN; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state |= FAN_AUTO; + break; + } + } + + // Set temperature + if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) { + auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); + remote_state |= ((temp - 15) << TEMP_SHIFT); + } + transmit_(remote_state); this->publish_state(); } @@ -121,41 +125,46 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, "Decoded 0x%02X", remote_state); + ESP_LOGD(TAG, "Decoded 0x%02" PRIX32, remote_state); if ((remote_state & 0xFF00000) != 0x8800000) return false; - if ((remote_state & COMMAND_MASK) == COMMAND_ON) { - this->mode = climate::CLIMATE_MODE_COOL; - } else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) { - this->mode = climate::CLIMATE_MODE_HEAT_COOL; - } - + // Get command if ((remote_state & COMMAND_MASK) == COMMAND_OFF) { this->mode = climate::CLIMATE_MODE_OFF; } else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) { this->swing_mode = this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; } else { - if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) { - this->mode = climate::CLIMATE_MODE_HEAT_COOL; - } else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) { - this->mode = climate::CLIMATE_MODE_DRY; - } else if ((remote_state & COMMAND_MASK) == COMMAND_HEAT) { - this->mode = climate::CLIMATE_MODE_HEAT; - } else { - this->mode = climate::CLIMATE_MODE_COOL; + switch (remote_state & COMMAND_MASK) { + case COMMAND_DRY: + case COMMAND_ON_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case COMMAND_FAN_ONLY: + case COMMAND_ON_FAN_ONLY: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case COMMAND_AI: + case COMMAND_ON_AI: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case COMMAND_HEAT: + case COMMAND_ON_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case COMMAND_COOL: + case COMMAND_ON_COOL: + default: + this->mode = climate::CLIMATE_MODE_COOL; + break; } - // Temperature - if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) - this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15; - - // Fan Speed + // Get fan speed if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) { this->fan_mode = climate::CLIMATE_FAN_AUTO; - } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT || - this->mode == climate::CLIMATE_MODE_DRY) { + } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY || + this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_HEAT) { if ((remote_state & FAN_MASK) == FAN_AUTO) { this->fan_mode = climate::CLIMATE_FAN_AUTO; } else if ((remote_state & FAN_MASK) == FAN_MIN) { @@ -166,14 +175,20 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { this->fan_mode = climate::CLIMATE_FAN_HIGH; } } + + // Get temperature + if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) { + this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15; + } } this->publish_state(); return true; } + void LgIrClimate::transmit_(uint32_t value) { calc_checksum_(value); - ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02X", value); + ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, value); auto transmit = this->transmitter_->transmit(); auto *data = transmit.get_data(); diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h index 6b38b3247b..7ee041b86f 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.h +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -2,6 +2,8 @@ #include "esphome/components/climate_ir/climate_ir.h" +#include + namespace esphome { namespace climate_ir_lg { @@ -12,7 +14,7 @@ const uint8_t TEMP_MAX = 30; // Celsius class LgIrClimate : public climate_ir::ClimateIR { public: LgIrClimate() - : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, false, + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true, {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py index 9a85eace75..609d416a0b 100644 --- a/esphome/components/color/__init__.py +++ b/esphome/components/color/__init__.py @@ -14,15 +14,41 @@ CONF_HEX = "hex" def hex_color(value): + if isinstance(value, int): + value = str(value) + if not isinstance(value, str): + raise cv.Invalid("Invalid value for hex color") if len(value) != 6: - raise cv.Invalid("Color must have six digits") + raise cv.Invalid("Hex color must have six digits") try: - return (int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16)) + return int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16) except ValueError as exc: raise cv.Invalid("Color must be hexadecimal") from exc -CONFIG_SCHEMA = cv.Any( +components = { + CONF_RED, + CONF_RED_INT, + CONF_GREEN, + CONF_GREEN_INT, + CONF_BLUE, + CONF_BLUE_INT, + CONF_WHITE, + CONF_WHITE_INT, +} + + +def validate_color(config): + has_components = set(config) & components + has_hex = CONF_HEX in config + if has_hex and has_components: + raise cv.Invalid("Hex color value may not be combined with component values") + if not has_hex and not has_components: + raise cv.Invalid("Must provide at least one color option") + return config + + +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(ColorStruct), @@ -34,14 +60,10 @@ CONFIG_SCHEMA = cv.Any( cv.Exclusive(CONF_BLUE_INT, "blue"): cv.uint8_t, cv.Exclusive(CONF_WHITE, "white"): cv.percentage, cv.Exclusive(CONF_WHITE_INT, "white"): cv.uint8_t, + cv.Optional(CONF_HEX): hex_color, } ).extend(cv.COMPONENT_SCHEMA), - cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(ColorStruct), - cv.Required(CONF_HEX): hex_color, - } - ).extend(cv.COMPONENT_SCHEMA), + validate_color, ) @@ -82,5 +104,5 @@ async def to_code(config): cg.new_variable( config[CONF_ID], - cg.StructInitializer(ColorStruct, ("r", r), ("g", g), ("b", b), ("w", w)), + cg.ArrayInitializer(r, g, b, w), ) diff --git a/esphome/components/combination/__init__.py b/esphome/components/combination/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp new file mode 100644 index 0000000000..716d270390 --- /dev/null +++ b/esphome/components/combination/combination.cpp @@ -0,0 +1,262 @@ +#include "combination.h" + +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include +#include +#include + +namespace esphome { +namespace combination { + +static const char *const TAG = "combination"; + +void CombinationComponent::log_config_(const LogString *combo_type) { + LOG_SENSOR("", "Combination Sensor:", this); + ESP_LOGCONFIG(TAG, " Combination Type: %s", LOG_STR_ARG(combo_type)); + this->log_source_sensors(); +} + +void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); } + +void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &stddev) { + this->sensor_pairs_.emplace_back(sensor, stddev); +} + +void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) { + this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); +} + +void CombinationNoParameterComponent::log_source_sensors() { + ESP_LOGCONFIG(TAG, " Source Sensors:"); + for (const auto &sensor : this->sensors_) { + ESP_LOGCONFIG(TAG, " - %s", sensor->get_name().c_str()); + } +} + +void CombinationOneParameterComponent::log_source_sensors() { + ESP_LOGCONFIG(TAG, " Source Sensors:"); + for (const auto &sensor : this->sensor_pairs_) { + auto &entity = *sensor.first; + ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str()); + } +} + +void CombinationNoParameterComponent::setup() { + for (const auto &sensor : this->sensors_) { + // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result + // repeatedly in the same loop if multiple source senors update. + sensor->add_on_state_callback( + [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); + } +} + +void KalmanCombinationComponent::dump_config() { + this->log_config_(LOG_STR("kalman")); + ESP_LOGCONFIG(TAG, " Update variance: %f per ms", this->update_variance_value_); + + if (this->std_dev_sensor_ != nullptr) { + LOG_SENSOR(" ", "Standard Deviation Sensor:", this->std_dev_sensor_); + } +} + +void KalmanCombinationComponent::setup() { + for (const auto &sensor : this->sensor_pairs_) { + const auto stddev = sensor.second; + sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + } +} + +void KalmanCombinationComponent::update_variance_() { + uint32_t now = millis(); + + // Variance increases by update_variance_ each millisecond + auto dt = now - this->last_update_; + auto dv = this->update_variance_value_ * dt; + this->variance_ += dv; + this->last_update_ = now; +} + +void KalmanCombinationComponent::correct_(float value, float stddev) { + if (std::isnan(value) || std::isinf(stddev)) { + return; + } + + if (std::isnan(this->state_) || std::isinf(this->variance_)) { + this->state_ = value; + this->variance_ = stddev * stddev; + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(stddev); + } + return; + } + + this->update_variance_(); + + // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu + // Use the value with the smaller variance as mu1 to prevent precision errors + const bool this_first = this->variance_ < (stddev * stddev); + const float mu1 = this_first ? this->state_ : value; + const float mu2 = this_first ? value : this->state_; + + const float var1 = this_first ? this->variance_ : stddev * stddev; + const float var2 = this_first ? stddev * stddev : this->variance_; + + const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2); + const float var = var1 - (var1 * var1) / (var1 + var2); + + // Update and publish state + this->state_ = mu; + this->variance_ = var; + + this->publish_state(mu); + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(std::sqrt(var)); + } +} + +void LinearCombinationComponent::setup() { + for (const auto &sensor : this->sensor_pairs_) { + // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result + // repeatedly in the same loop if multiple source senors update. + sensor.first->add_on_state_callback( + [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); + } +} + +void LinearCombinationComponent::handle_new_value(float value) { + // Multiplies each sensor state by a configured coeffecient and then sums + + if (!std::isfinite(value)) + return; + + float sum = 0.0; + + for (const auto &sensor : this->sensor_pairs_) { + const float sensor_state = sensor.first->state; + if (std::isfinite(sensor_state)) { + sum += sensor_state * sensor.second(sensor_state); + } + } + + this->publish_state(sum); +}; + +void MaximumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float max_value = (-1) * std::numeric_limits::infinity(); // note x = max(x, -infinity) + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + max_value = std::max(max_value, sensor->state); + } + } + + this->publish_state(max_value); +} + +void MeanCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float sum = 0.0; + size_t count = 0.0; + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + ++count; + sum += sensor->state; + } + } + + float mean = sum / count; + + this->publish_state(mean); +} + +void MedianCombinationComponent::handle_new_value(float value) { + // Sorts sensor states in ascending order and determines the middle value + + if (!std::isfinite(value)) + return; + + std::vector sensor_states; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sensor_states.push_back(sensor->state); + } + } + + sort(sensor_states.begin(), sensor_states.end()); + size_t sensor_states_size = sensor_states.size(); + + float median = NAN; + + if (sensor_states_size) { + if (sensor_states_size % 2) { + // Odd number of measurements, use middle measurement + median = sensor_states[sensor_states_size / 2]; + } else { + // Even number of measurements, use the average of the two middle measurements + median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0; + } + } + + this->publish_state(median); +} + +void MinimumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float min_value = std::numeric_limits::infinity(); // note x = min(x, infinity) + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + min_value = std::min(min_value, sensor->state); + } + } + + this->publish_state(min_value); +} + +void MostRecentCombinationComponent::handle_new_value(float value) { this->publish_state(value); } + +void RangeCombinationComponent::handle_new_value(float value) { + // Sorts sensor states then takes difference between largest and smallest states + + if (!std::isfinite(value)) + return; + + std::vector sensor_states; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sensor_states.push_back(sensor->state); + } + } + + sort(sensor_states.begin(), sensor_states.end()); + + float range = sensor_states.back() - sensor_states.front(); + this->publish_state(range); +} + +void SumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float sum = 0.0; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sum += sensor->state; + } + } + + this->publish_state(sum); +} + +} // namespace combination +} // namespace esphome diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h new file mode 100644 index 0000000000..901aeaf259 --- /dev/null +++ b/esphome/components/combination/combination.h @@ -0,0 +1,141 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +#include + +namespace esphome { +namespace combination { + +class CombinationComponent : public Component, public sensor::Sensor { + public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + /// @brief Logs all source sensor's names + virtual void log_source_sensors() = 0; + + protected: + /// @brief Logs the sensor for use in dump_config + /// @param combo_type Name of the combination operation + void log_config_(const LogString *combo_type); +}; + +/// @brief Base class for operations that do not require an extra parameter to compute the combination +class CombinationNoParameterComponent : public CombinationComponent { + public: + /// @brief Adds a callback to each source sensor + void setup() override; + + void add_source(Sensor *sensor); + + /// @brief Computes the combination + /// @param value Newest sensor measurement + virtual void handle_new_value(float value) = 0; + + /// @brief Logs all source sensor's names in sensors_ + void log_source_sensors() override; + + protected: + std::vector sensors_; +}; + +// Base class for opertions that require one parameter to compute the combination +class CombinationOneParameterComponent : public CombinationComponent { + public: + void add_source(Sensor *sensor, std::function const &stddev); + void add_source(Sensor *sensor, float stddev); + + /// @brief Logs all source sensor's names in sensor_pairs_ + void log_source_sensors() override; + + protected: + std::vector>> sensor_pairs_; +}; + +class KalmanCombinationComponent : public CombinationOneParameterComponent { + public: + void dump_config() override; + void setup() override; + + void set_process_std_dev(float process_std_dev) { + this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f; + } + void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; } + + protected: + void update_variance_(); + void correct_(float value, float stddev); + + // Optional sensor for publishing the current error + sensor::Sensor *std_dev_sensor_{nullptr}; + + // Tick of the last update + uint32_t last_update_{0}; + // Change of the variance, per ms + float update_variance_value_{0.f}; + + // Best guess for the state and its variance + float state_{NAN}; + float variance_{INFINITY}; +}; + +class LinearCombinationComponent : public CombinationOneParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("linear")); } + void setup() override; + + void handle_new_value(float value); +}; + +class MaximumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("max")); } + + void handle_new_value(float value) override; +}; + +class MeanCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("mean")); } + + void handle_new_value(float value) override; +}; + +class MedianCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("median")); } + + void handle_new_value(float value) override; +}; + +class MinimumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("min")); } + + void handle_new_value(float value) override; +}; + +class MostRecentCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("most_recently_updated")); } + + void handle_new_value(float value) override; +}; + +class RangeCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("range")); } + + void handle_new_value(float value) override; +}; + +class SumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("sum")); } + + void handle_new_value(float value) override; +}; + +} // namespace combination +} // namespace esphome diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py new file mode 100644 index 0000000000..fad0277061 --- /dev/null +++ b/esphome/components/combination/sensor.py @@ -0,0 +1,176 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_RANGE, + CONF_SOURCE, + CONF_SUM, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, +) +from esphome.core.entity_helpers import inherit_property_from + +CODEOWNERS = ["@Cat-Ion", "@kahrendt"] + +combination_ns = cg.esphome_ns.namespace("combination") + +KalmanCombinationComponent = combination_ns.class_( + "KalmanCombinationComponent", cg.Component, sensor.Sensor +) +LinearCombinationComponent = combination_ns.class_( + "LinearCombinationComponent", cg.Component, sensor.Sensor +) +MaximumCombinationComponent = combination_ns.class_( + "MaximumCombinationComponent", cg.Component, sensor.Sensor +) +MeanCombinationComponent = combination_ns.class_( + "MeanCombinationComponent", cg.Component, sensor.Sensor +) +MedianCombinationComponent = combination_ns.class_( + "MedianCombinationComponent", cg.Component, sensor.Sensor +) +MinimumCombinationComponent = combination_ns.class_( + "MinimumCombinationComponent", cg.Component, sensor.Sensor +) +MostRecentCombinationComponent = combination_ns.class_( + "MostRecentCombinationComponent", cg.Component, sensor.Sensor +) +RangeCombinationComponent = combination_ns.class_( + "RangeCombinationComponent", cg.Component, sensor.Sensor +) +SumCombinationComponent = combination_ns.class_( + "SumCombinationComponent", cg.Component, sensor.Sensor +) + +CONF_COEFFECIENT = "coeffecient" +CONF_ERROR = "error" +CONF_KALMAN = "kalman" +CONF_LINEAR = "linear" +CONF_MAX = "max" +CONF_MEAN = "mean" +CONF_MEDIAN = "median" +CONF_MIN = "min" +CONF_MOST_RECENTLY_UPDATED = "most_recently_updated" +CONF_PROCESS_STD_DEV = "process_std_dev" +CONF_SOURCES = "sources" +CONF_STD_DEV = "std_dev" + + +KALMAN_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), + } +) + +LINEAR_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), + } +) + +SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + } +) + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_KALMAN: sensor.sensor_schema(KalmanCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend( + { + cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, + cv.Required(CONF_SOURCES): cv.ensure_list(KALMAN_SOURCE_SCHEMA), + cv.Optional(CONF_STD_DEV): sensor.sensor_schema(), + } + ), + CONF_LINEAR: sensor.sensor_schema(LinearCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(LINEAR_SOURCE_SCHEMA)}), + CONF_MAX: sensor.sensor_schema(MaximumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MEAN: sensor.sensor_schema(MeanCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MEDIAN: sensor.sensor_schema(MedianCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MIN: sensor.sensor_schema(MinimumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MOST_RECENTLY_UPDATED: sensor.sensor_schema(MostRecentCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_RANGE: sensor.sensor_schema(RangeCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_SUM: sensor.sensor_schema(SumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + } +) + + +# Inherit some sensor values from the first source, for both the state and the error value +# CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing" +properties_to_inherit = [ + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_UNIT_OF_MEASUREMENT, +] +inherit_schema_for_state = [ + inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] +inherit_schema_for_std_dev = [ + inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] + +FINAL_VALIDATE_SCHEMA = cv.All( + *inherit_schema_for_state, + *inherit_schema_for_std_dev, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + if proces_std_dev := config.get(CONF_PROCESS_STD_DEV): + cg.add(var.set_process_std_dev(proces_std_dev)) + + for source_conf in config[CONF_SOURCES]: + source = await cg.get_variable(source_conf[CONF_SOURCE]) + if config[CONF_TYPE] == CONF_KALMAN: + error = await cg.templatable( + source_conf[CONF_ERROR], + [(float, "x")], + cg.float_, + ) + cg.add(var.add_source(source, error)) + elif config[CONF_TYPE] == CONF_LINEAR: + coeffecient = await cg.templatable( + source_conf[CONF_COEFFECIENT], + [(float, "x")], + cg.float_, + ) + cg.add(var.add_source(source, coeffecient)) + else: + cg.add(var.add_source(source)) + + if CONF_STD_DEV in config: + sens = await sensor.new_sensor(config[CONF_STD_DEV]) + cg.add(var.set_std_dev_sensor(sens)) diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 738fd8d00d..22b3431c3e 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -101,12 +101,8 @@ void CoolixClimate::transmit_state() { } } } - ESP_LOGV(TAG, "Sending coolix code: 0x%06X", remote_state); - - auto transmit = this->transmitter_->transmit(); - auto *data = transmit.get_data(); - remote_base::CoolixProtocol().encode(data, remote_state); - transmit.perform(); + ESP_LOGV(TAG, "Sending coolix code: 0x%06" PRIX32, remote_state); + this->transmit_(remote_state); } bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) { @@ -114,8 +110,8 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei if (!decoded.has_value()) return false; // Decoded remote state y 3 bytes long code. - uint32_t remote_state = *decoded; - ESP_LOGV(TAG, "Decoded 0x%06X", remote_state); + uint32_t remote_state = (*decoded).second; + ESP_LOGV(TAG, "Decoded 0x%06" PRIX32, remote_state); if ((remote_state & 0xFF0000) != 0xB20000) return false; diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index 3419795875..f4b4ff8e0e 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -2,6 +2,8 @@ #include "esphome/components/climate_ir/climate_ir.h" +#include + namespace esphome { namespace coolix { diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 74d9da279f..15a7f5e025 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -12,6 +12,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; + this->preset_mode = source_->preset_mode; this->publish_state(); }); @@ -19,6 +20,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; + this->preset_mode = source_->preset_mode; this->publish_state(); } @@ -33,6 +35,7 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); + traits.set_supported_preset_modes(base.supported_preset_modes()); return traits; } @@ -46,6 +49,8 @@ void CopyFan::control(const fan::FanCall &call) { call2.set_speed(*call.get_speed()); if (call.get_direction().has_value()) call2.set_direction(*call.get_direction()); + if (!call.get_preset_mode().empty()) + call2.set_preset_mode(call.get_preset_mode()); call2.perform(); } diff --git a/esphome/components/copy/text/__init__.py b/esphome/components/copy/text/__init__.py new file mode 100644 index 0000000000..7593789250 --- /dev/null +++ b/esphome/components/copy/text/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_MODE, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyText = copy_ns.class_("CopyText", text.Text, cg.Component) + + +CONFIG_SCHEMA = text.TEXT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopyText), + cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_MODE, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await text.new_text(config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/text/copy_text.cpp b/esphome/components/copy/text/copy_text.cpp new file mode 100644 index 0000000000..cb74201383 --- /dev/null +++ b/esphome/components/copy/text/copy_text.cpp @@ -0,0 +1,25 @@ +#include "copy_text.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.text"; + +void CopyText::setup() { + source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopyText::dump_config() { LOG_TEXT("", "Copy Text", this); } + +void CopyText::control(const std::string &value) { + auto call2 = source_->make_call(); + call2.set_value(value); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/text/copy_text.h b/esphome/components/copy/text/copy_text.h new file mode 100644 index 0000000000..beb8610dfe --- /dev/null +++ b/esphome/components/copy/text/copy_text.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/text/text.h" + +namespace esphome { +namespace copy { + +class CopyText : public text::Text, public Component { + public: + void set_source(text::Text *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void control(const std::string &value) override; + + text::Text *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 90e5ee1f03..e7e3ac3bb0 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -1,22 +1,23 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.automation import maybe_simple_id, Condition -from esphome.components import mqtt +from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, CONF_DEVICE_CLASS, - CONF_STATE, + CONF_ID, + CONF_MQTT_ID, CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, CONF_POSITION_STATE_TOPIC, + CONF_STATE, + CONF_STOP, CONF_TILT, CONF_TILT_COMMAND_TOPIC, CONF_TILT_STATE_TOPIC, - CONF_STOP, - CONF_MQTT_ID, CONF_TRIGGER_ID, + CONF_WEB_SERVER, DEVICE_CLASS_AWNING, DEVICE_CLASS_BLIND, DEVICE_CLASS_CURTAIN, @@ -88,42 +89,46 @@ CoverClosedTrigger = cover_ns.class_( CONF_ON_CLOSED = "on_closed" -COVER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.GenerateID(): cv.declare_id(Cover), - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), - cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), - cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.subscribe_topic - ), - cv.Optional(CONF_POSITION_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.subscribe_topic - ), - cv.Optional(CONF_TILT_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.subscribe_topic - ), - cv.Optional(CONF_TILT_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.subscribe_topic - ), - cv.Optional(CONF_ON_OPEN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), - } - ), - cv.Optional(CONF_ON_CLOSED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger), - } - ), - } +COVER_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(Cover), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), + cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), + cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_POSITION_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_TILT_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_TILT_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_ON_OPEN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), + } + ), + cv.Optional(CONF_ON_CLOSED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger), + } + ), + } + ) ) async def setup_cover_core_(var, config): await setup_entity(var, config) - if CONF_DEVICE_CLASS in config: - cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) for conf in config.get(CONF_ON_OPEN, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -132,24 +137,23 @@ async def setup_cover_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) - if CONF_POSITION_STATE_TOPIC in config: - cg.add( - mqtt_.set_custom_position_state_topic(config[CONF_POSITION_STATE_TOPIC]) - ) - if CONF_POSITION_COMMAND_TOPIC in config: - cg.add( - mqtt_.set_custom_position_command_topic( - config[CONF_POSITION_COMMAND_TOPIC] - ) - ) - if CONF_TILT_STATE_TOPIC in config: - cg.add(mqtt_.set_custom_tilt_state_topic(config[CONF_TILT_STATE_TOPIC])) - if CONF_TILT_COMMAND_TOPIC in config: - cg.add(mqtt_.set_custom_tilt_command_topic(config[CONF_TILT_COMMAND_TOPIC])) + if (position_state_topic := config.get(CONF_POSITION_STATE_TOPIC)) is not None: + cg.add(mqtt_.set_custom_position_state_topic(position_state_topic)) + if ( + position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC) + ) is not None: + cg.add(mqtt_.set_custom_position_command_topic(position_command_topic)) + if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None: + cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic)) + if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None: + cg.add(mqtt_.set_custom_tilt_command_topic(tilt_command_topic)) + + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) async def register_cover(var, config): @@ -205,17 +209,17 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema( async def cover_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_STOP in config: - template_ = await cg.templatable(config[CONF_STOP], args, bool) + if (stop := config.get(CONF_STOP)) is not None: + template_ = await cg.templatable(stop, args, bool) cg.add(var.set_stop(template_)) - if CONF_STATE in config: - template_ = await cg.templatable(config[CONF_STATE], args, float) + if (state := config.get(CONF_STATE)) is not None: + template_ = await cg.templatable(state, args, float) cg.add(var.set_position(template_)) - if CONF_POSITION in config: - template_ = await cg.templatable(config[CONF_POSITION], args, float) + if (position := config.get(CONF_POSITION)) is not None: + template_ = await cg.templatable(position, args, float) cg.add(var.set_position(template_)) - if CONF_TILT in config: - template_ = await cg.templatable(config[CONF_TILT], args, float) + if (tilt := config.get(CONF_TILT)) is not None: + template_ = await cg.templatable(tilt, args, float) cg.add(var.set_tilt(template_)) return var diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 89598a9636..8b6f5b8a72 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -129,13 +129,13 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ - ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9") + ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9") void open(); /** Close the cover. * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ - ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9") + ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9") void close(); /** Stop the cover. * diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp index fb8e50b87a..0d347ae0c1 100644 --- a/esphome/components/cs5460a/cs5460a.cpp +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -86,7 +86,7 @@ void CS5460AComponent::hw_init_() { } uint32_t status = this->read_register_(REG_STATUS); - ESP_LOGCONFIG(TAG, " Version: %x", (status >> 6) & 7); + ESP_LOGCONFIG(TAG, " Version: %" PRIx32, (status >> 6) & 7); this->write_register_(REG_CYCLE_COUNT, samples_); this->write_register_(REG_PULSE_RATE, lroundf(pulse_freq_ * 32.0f)); @@ -323,7 +323,7 @@ void CS5460AComponent::dump_config() { ESP_LOGCONFIG(TAG, " Init status: %s", state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other")); LOG_PIN(" CS Pin: ", cs_); - ESP_LOGCONFIG(TAG, " Samples / cycle: %u", samples_); + ESP_LOGCONFIG(TAG, " Samples / cycle: %" PRIu32, samples_); ESP_LOGCONFIG(TAG, " Phase offset: %i", phase_offset_); ESP_LOGCONFIG(TAG, " PGA Gain: %s", pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x"); ESP_LOGCONFIG(TAG, " Current gain: %.5f", current_gain_); diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 699049757c..763ddc14fa 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -5,6 +5,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" +#include + namespace esphome { namespace cs5460a { diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index d6e3c2ba48..d8219e1df1 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_ID, CONF_POWER, CONF_VOLTAGE, + CONF_VOLTAGE_GAIN, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -33,7 +34,6 @@ CONF_SAMPLES = "samples" CONF_PHASE_OFFSET = "phase_offset" CONF_PGA_GAIN = "pga_gain" CONF_CURRENT_GAIN = "current_gain" -CONF_VOLTAGE_GAIN = "voltage_gain" CONF_CURRENT_HPF = "current_hpf" CONF_VOLTAGE_HPF = "voltage_hpf" CONF_PULSE_ENERGY = "pulse_energy" @@ -113,17 +113,14 @@ async def to_code(config): cg.add(var.set_hpf_enable(config[CONF_CURRENT_HPF], config[CONF_VOLTAGE_HPF])) cg.add(var.set_pulse_energy_wh(config[CONF_PULSE_ENERGY])) - if CONF_VOLTAGE in config: - conf = config[CONF_VOLTAGE] - sens = await sensor.new_sensor(conf) + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) cg.add(var.set_voltage_sensor(sens)) - if CONF_CURRENT in config: - conf = config[CONF_CURRENT] - sens = await sensor.new_sensor(conf) + if current_config := config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) cg.add(var.set_current_sensor(sens)) - if CONF_POWER in config: - conf = config[CONF_POWER] - sens = await sensor.new_sensor(conf) + if power_config := config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 3b8364f0bc..337996f6ef 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -217,7 +217,7 @@ void CSE7761Component::get_data_() { this->voltage_sensor_->publish_state(voltage); } - for (uint32_t channel = 0; channel < 2; channel++) { + for (uint8_t channel = 0; channel < 2; channel++) { // Active power = PowerPA * PowerPAC * 1000 / 0x80000000 float active_power = (float) this->data_.active_power[channel] / this->coefficient_by_unit_(POWER_PAC); // W float amps = (float) this->data_.current_rms[channel] / this->coefficient_by_unit_(RMS_IAC); // A diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index f232f35ea6..48240464b3 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -1,5 +1,8 @@ #include "cse7766.h" #include "esphome/core/log.h" +#include +#include +#include namespace esphome { namespace cse7766 { @@ -67,20 +70,26 @@ bool CSE7766Component::check_byte_() { return true; } void CSE7766Component::parse_data_() { - ESP_LOGVV(TAG, "CSE7766 Data: "); - for (uint8_t i = 0; i < 23; i++) { - ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]), - this->raw_data_[i]); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + { + std::stringstream ss; + ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0'); + for (uint8_t i = 0; i < 23; i++) { + ss << ' ' << std::setw(2) << static_cast(this->raw_data_[i]); + } + ESP_LOGVV(TAG, "%s", ss.str().c_str()); } +#endif + // Parse header uint8_t header1 = this->raw_data_[0]; + if (header1 == 0xAA) { ESP_LOGE(TAG, "CSE7766 not calibrated!"); return; } bool power_cycle_exceeds_range = false; - if ((header1 & 0xF0) == 0xF0) { if (header1 & 0xD) { ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1); @@ -93,98 +102,132 @@ void CSE7766Component::parse_data_() { if (header1 & (1 << 0)) { ESP_LOGE(TAG, " Coefficient storage area is abnormal."); } + + // Datasheet: voltage or current cycle exceeding range means invalid values return; } power_cycle_exceeds_range = header1 & (1 << 1); } - uint32_t voltage_calib = this->get_24_bit_uint_(2); + // Parse data frame + uint32_t voltage_coeff = this->get_24_bit_uint_(2); uint32_t voltage_cycle = this->get_24_bit_uint_(5); - uint32_t current_calib = this->get_24_bit_uint_(8); + uint32_t current_coeff = this->get_24_bit_uint_(8); uint32_t current_cycle = this->get_24_bit_uint_(11); - uint32_t power_calib = this->get_24_bit_uint_(14); + uint32_t power_coeff = this->get_24_bit_uint_(14); uint32_t power_cycle = this->get_24_bit_uint_(17); - uint8_t adj = this->raw_data_[20]; - uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; - - bool have_voltage = adj & 0x40; - if (have_voltage) { - // voltage cycle of serial port outputted is a complete cycle; - this->voltage_acc_ += voltage_calib / float(voltage_cycle); - this->voltage_counts_ += 1; - } + uint16_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; bool have_power = adj & 0x10; - float power = 0.0f; + bool have_current = adj & 0x20; + bool have_voltage = adj & 0x40; - if (have_power) { - // power cycle of serial port outputted is a complete cycle; - // According to the user manual, power cycle exceeding range means the measured power is 0 - if (!power_cycle_exceeds_range) { - power = power_calib / float(power_cycle); + float voltage = 0.0f; + if (have_voltage) { + voltage = voltage_coeff / float(voltage_cycle); + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(voltage); } - this->power_acc_ += power; - this->power_counts_ += 1; + } - uint32_t difference; - if (this->cf_pulses_last_ == 0) { + float energy = 0.0; + if (this->energy_sensor_ != nullptr) { + if (this->cf_pulses_last_ == 0 && !this->energy_sensor_->has_state()) { this->cf_pulses_last_ = cf_pulses; } - - if (cf_pulses < this->cf_pulses_last_) { - difference = cf_pulses + (0x10000 - this->cf_pulses_last_); - } else { - difference = cf_pulses - this->cf_pulses_last_; - } + uint16_t cf_diff = cf_pulses - this->cf_pulses_last_; + this->cf_pulses_total_ += cf_diff; this->cf_pulses_last_ = cf_pulses; - this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f; - this->energy_total_counts_ += 1; + energy = this->cf_pulses_total_ * float(power_coeff) / 1000000.0f / 3600.0f; + this->energy_sensor_->publish_state(energy); } - if (adj & 0x20) { - // indicates current cycle of serial port outputted is a complete cycle; - float current = 0.0f; - if (have_voltage && !have_power) { - // Testing has shown that when we have voltage and current but not power, that means the power is 0. - // We report a power of 0, which in turn means we should report a current of 0. - this->power_counts_ += 1; - } else if (power != 0.0f) { - current = current_calib / float(current_cycle); + float power = 0.0f; + if (power_cycle_exceeds_range) { + // Datasheet: power cycle exceeding range means active power is 0 + have_power = true; + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(0.0f); + } + } else if (have_power) { + power = power_coeff / float(power_cycle); + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(power); } - this->current_acc_ += current; - this->current_counts_ += 1; } -} -void CSE7766Component::update() { - const auto publish_state = [](const char *name, sensor::Sensor *sensor, float &acc, uint32_t &counts) { - if (counts != 0) { - const auto avg = acc / counts; - ESP_LOGV(TAG, "Got %s_acc=%.2f %s_counts=%d %s=%.1f", name, acc, name, counts, name, avg); + float current = 0.0f; + float calculated_current = 0.0f; + if (have_current) { + // Assumption: if we don't have power measurement, then current is likely below 50mA + if (have_power && voltage > 1.0f) { + calculated_current = power / voltage; + } + // Datasheet: minimum measured current is 50mA + if (calculated_current > 0.05f) { + current = current_coeff / float(current_cycle); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(current); + } + } - if (sensor != nullptr) { - sensor->publish_state(avg); + if (have_voltage && have_current) { + const float apparent_power = voltage * current; + if (this->apparent_power_sensor_ != nullptr) { + this->apparent_power_sensor_->publish_state(apparent_power); + } + if (have_power && this->reactive_power_sensor_ != nullptr) { + const float reactive_power = apparent_power - power; + if (reactive_power < 0.0f) { + ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power); + this->reactive_power_sensor_->publish_state(0.0f); + } else { + this->reactive_power_sensor_->publish_state(reactive_power); } - - acc = 0.0f; - counts = 0; } - }; - - publish_state("voltage", this->voltage_sensor_, this->voltage_acc_, this->voltage_counts_); - publish_state("current", this->current_sensor_, this->current_acc_, this->current_counts_); - publish_state("power", this->power_sensor_, this->power_acc_, this->power_counts_); - - if (this->energy_total_counts_ != 0) { - ESP_LOGV(TAG, "Got energy_total=%.2f energy_total_counts=%d", this->energy_total_, this->energy_total_counts_); - - if (this->energy_sensor_ != nullptr) { - this->energy_sensor_->publish_state(this->energy_total_); + if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) { + float pf = NAN; + 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; + } else if (current == 0 && calculated_current <= 0.05f) { + // Datasheet: minimum measured current is 50mA + ESP_LOGV(TAG, "Can't calculate power factor (current below minimum for CSE7766)"); + } else { + ESP_LOGW(TAG, "Can't calculate power factor from P = %.4f W, S = %.4f VA", power, apparent_power); + } + this->power_factor_sensor_->publish_state(pf); } - this->energy_total_counts_ = 0; } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + { + std::stringstream ss; + ss << "Parsed:"; + if (have_voltage) { + ss << " V=" << voltage << "V"; + } + if (have_current) { + ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)"; + } + if (have_power) { + ss << " P=" << power << "W"; + } + if (energy != 0.0f) { + ss << " E=" << energy << "kWh (" << cf_pulses << ")"; + } + ESP_LOGVV(TAG, "%s", ss.str().c_str()); + } +#endif } uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) { @@ -194,12 +237,14 @@ uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) { void CSE7766Component::dump_config() { ESP_LOGCONFIG(TAG, "CSE7766:"); - LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); LOG_SENSOR(" ", "Energy", this->energy_sensor_); - this->check_uart_settings(4800); + LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); + LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); + this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN); } } // namespace cse7766 diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 2f30eec09f..5d89b3b75b 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -7,16 +7,22 @@ namespace esphome { namespace cse7766 { -class CSE7766Component : public PollingComponent, public uart::UARTDevice { +class CSE7766Component : public Component, public uart::UARTDevice { public: void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) { + apparent_power_sensor_ = apparent_power_sensor; + } + void set_reactive_power_sensor(sensor::Sensor *reactive_power_sensor) { + reactive_power_sensor_ = reactive_power_sensor; + } + void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } void loop() override; float get_setup_priority() const override; - void update() override; void dump_config() override; protected: @@ -31,16 +37,11 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice { sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr}; - float voltage_acc_{0.0f}; - float current_acc_{0.0f}; - float power_acc_{0.0f}; - float energy_total_{0.0f}; - uint32_t cf_pulses_last_{0}; - uint32_t voltage_counts_{0}; - uint32_t current_counts_{0}; - uint32_t power_counts_{0}; - // Setting this to 1 means it will always publish 0 once at startup - uint32_t energy_total_counts_{1}; + sensor::Sensor *apparent_power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; + uint32_t cf_pulses_total_{0}; + uint16_t cf_pulses_last_{0}; }; } // namespace cse7766 diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index 2f48aff0aa..b5b11a661e 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -2,19 +2,27 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart from esphome.const import ( + CONF_APPARENT_POWER, CONF_CURRENT, CONF_ENERGY, CONF_ID, CONF_POWER, + CONF_POWER_FACTOR, + CONF_REACTIVE_POWER, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, - UNIT_VOLT, UNIT_AMPERE, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE, UNIT_WATT, UNIT_WATT_HOURS, ) @@ -22,45 +30,56 @@ from esphome.const import ( DEPENDENCIES = ["uart"] cse7766_ns = cg.esphome_ns.namespace("cse7766") -CSE7766Component = cse7766_ns.class_( - "CSE7766Component", cg.PollingComponent, uart.UARTDevice -) +CSE7766Component = cse7766_ns.class_("CSE7766Component", cg.Component, uart.UARTDevice) -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(CSE7766Component), - 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_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=3, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(uart.UART_DEVICE_SCHEMA) -) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CSE7766Component), + 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_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + 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_REACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_REACTIVE_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, + ), + } +).extend(uart.UART_DEVICE_SCHEMA) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "cse7766", baud_rate=4800, require_rx=True + "cse7766", baud_rate=4800, parity="EVEN", require_rx=True ) @@ -69,19 +88,24 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_VOLTAGE in config: - conf = config[CONF_VOLTAGE] - sens = await sensor.new_sensor(conf) + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) cg.add(var.set_voltage_sensor(sens)) - if CONF_CURRENT in config: - conf = config[CONF_CURRENT] - sens = await sensor.new_sensor(conf) + if current_config := config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) cg.add(var.set_current_sensor(sens)) - if CONF_POWER in config: - conf = config[CONF_POWER] - sens = await sensor.new_sensor(conf) + if power_config := config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) cg.add(var.set_power_sensor(sens)) - if CONF_ENERGY in config: - conf = config[CONF_ENERGY] - sens = await sensor.new_sensor(conf) + if energy_config := config.get(CONF_ENERGY): + sens = await sensor.new_sensor(energy_config) cg.add(var.set_energy_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 reactive_power_config := config.get(CONF_REACTIVE_POWER): + sens = await sensor.new_sensor(reactive_power_config) + cg.add(var.set_reactive_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)) diff --git a/esphome/components/cst226/__init__.py b/esphome/components/cst226/__init__.py new file mode 100644 index 0000000000..847e44dbda --- /dev/null +++ b/esphome/components/cst226/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +cst226_ns = cg.esphome_ns.namespace("cst226") diff --git a/esphome/components/cst226/touchscreen/__init__.py b/esphome/components/cst226/touchscreen/__init__.py new file mode 100644 index 0000000000..76975ffe78 --- /dev/null +++ b/esphome/components/cst226/touchscreen/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_INTERRUPT_PIN, CONF_ID, CONF_RESET_PIN +from .. import cst226_ns + + +CST226Touchscreen = cst226_ns.class_( + "CST226Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CST226ButtonListener = cst226_ns.class_("CST226ButtonListener") +CONFIG_SCHEMA = ( + touchscreen.touchscreen_schema("100ms") + .extend( + { + cv.GenerateID(): cv.declare_id(CST226Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x5A)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp new file mode 100644 index 0000000000..d4e43d30f5 --- /dev/null +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp @@ -0,0 +1,92 @@ +#include "cst226_touchscreen.h" + +namespace esphome { +namespace cst226 { + +void CST226Touchscreen::setup() { + esph_log_config(TAG, "Setting up CST226 Touchscreen..."); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + this->set_timeout(30, [this] { this->continue_setup_(); }); + } else { + this->continue_setup_(); + } +} + +void CST226Touchscreen::update_touches() { + uint8_t data[28]; + if (!this->read_bytes(CST226_REG_STATUS, data, sizeof data)) { + this->status_set_warning(); + this->skip_update_ = true; + return; + } + this->status_clear_warning(); + if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) { + this->skip_update_ = true; + return; + } + uint8_t num_of_touches = data[5] & 0x7F; + if (num_of_touches == 0 || num_of_touches > 5) { + this->write_byte(0, 0xAB); + return; + } + + size_t index = 0; + for (uint8_t i = 0; i != num_of_touches; i++) { + uint8_t id = data[index] >> 4; + int16_t x = (data[index + 1] << 4) | ((data[index + 3] >> 4) & 0x0F); + int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F); + int16_t z = data[index + 4]; + this->add_raw_touch_position_(id, x, y, z); + esph_log_v(TAG, "Read touch %d: %d/%d", id, x, y); + index += 5; + if (i == 0) + index += 2; + } +} + +void CST226Touchscreen::continue_setup_() { + uint8_t buffer[8]; + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + buffer[0] = 0xD1; + if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) { + esph_log_e(TAG, "Write byte to 0xD1 failed"); + this->mark_failed(); + return; + } + delay(10); + if (this->read16_(0xD204, buffer, 4)) { + uint16_t chip_id = buffer[2] + (buffer[3] << 8); + uint16_t project_id = buffer[0] + (buffer[1] << 8); + esph_log_config(TAG, "Chip ID %X, project ID %x", chip_id, project_id); + } + if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { + if (this->read16_(0xD1F8, buffer, 4)) { + this->x_raw_max_ = buffer[0] + (buffer[1] << 8); + this->y_raw_max_ = buffer[2] + (buffer[3] << 8); + } else { + this->x_raw_max_ = this->display_->get_native_width(); + this->y_raw_max_ = this->display_->get_native_height(); + } + } + this->setup_complete_ = true; + esph_log_config(TAG, "CST226 Touchscreen setup complete"); +} + +void CST226Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "CST226 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +} // namespace cst226 +} // namespace esphome diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.h b/esphome/components/cst226/touchscreen/cst226_touchscreen.h new file mode 100644 index 0000000000..9f518e5068 --- /dev/null +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace cst226 { + +static const char *const TAG = "cst226.touchscreen"; + +static const uint8_t CST226_REG_STATUS = 0x00; + +class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void update_touches() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + bool can_proceed() override { return this->setup_complete_ || this->is_failed(); } + + protected: + bool read16_(uint16_t addr, uint8_t *data, size_t len) { + if (this->read_register16(addr, data, len) != i2c::ERROR_OK) { + esph_log_e(TAG, "Read data from 0x%04X failed", addr); + this->mark_failed(); + return false; + } + return true; + } + void continue_setup_(); + + InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{}; + uint8_t chip_id_{}; + bool setup_complete_{}; +}; + +} // namespace cst226 +} // namespace esphome diff --git a/esphome/components/cst816/__init__.py b/esphome/components/cst816/__init__.py new file mode 100644 index 0000000000..674a80b7c1 --- /dev/null +++ b/esphome/components/cst816/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +cst816_ns = cg.esphome_ns.namespace("cst816") diff --git a/esphome/components/cst816/binary_sensor/__init__.py b/esphome/components/cst816/binary_sensor/__init__.py new file mode 100644 index 0000000000..b3fd5bb852 --- /dev/null +++ b/esphome/components/cst816/binary_sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor + +from .. import cst816_ns +from ..touchscreen import CST816Touchscreen, CST816ButtonListener + +CONF_CST816_ID = "cst816_id" + +CST816Button = cst816_ns.class_( + "CST816Button", + binary_sensor.BinarySensor, + cg.Component, + CST816ButtonListener, + cg.Parented.template(CST816Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST816Button).extend( + { + cv.GenerateID(CONF_CST816_ID): cv.use_id(CST816Touchscreen), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_CST816_ID]) diff --git a/esphome/components/cst816/binary_sensor/cst816_button.h b/esphome/components/cst816/binary_sensor/cst816_button.h new file mode 100644 index 0000000000..4ae856d506 --- /dev/null +++ b/esphome/components/cst816/binary_sensor/cst816_button.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/cst816/touchscreen/cst816_touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace cst816 { + +class CST816Button : public binary_sensor::BinarySensor, + public Component, + public CST816ButtonListener, + public Parented { + public: + void setup() override { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); + } + + void dump_config() override { LOG_BINARY_SENSOR("", "CST816 Button", this); } + + void update_button(bool state) override { this->publish_state(state); } +}; + +} // namespace cst816 +} // namespace esphome diff --git a/esphome/components/cst816/touchscreen/__init__.py b/esphome/components/cst816/touchscreen/__init__.py new file mode 100644 index 0000000000..288ca17593 --- /dev/null +++ b/esphome/components/cst816/touchscreen/__init__.py @@ -0,0 +1,37 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c, touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +from .. import cst816_ns + +CST816Touchscreen = cst816_ns.class_( + "CST816Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CST816ButtonListener = cst816_ns.class_("CST816ButtonListener") + +CONF_SKIP_PROBE = "skip_probe" +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CST816Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_SKIP_PROBE, default=False): cv.boolean, + } +).extend(i2c.i2c_device_schema(0x15)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_skip_probe(config[CONF_SKIP_PROBE])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp new file mode 100644 index 0000000000..7dcb130e20 --- /dev/null +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -0,0 +1,118 @@ +#include "cst816_touchscreen.h" + +namespace esphome { +namespace cst816 { + +void CST816Touchscreen::continue_setup_() { + if (this->interrupt_pin_ != nullptr) { + 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"); + this->mark_failed(); + return; + } + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } + ESP_LOGCONFIG(TAG, "CST816 Touchscreen setup complete"); +} + +void CST816Touchscreen::update_button_state_(bool state) { + if (this->button_touched_ == state) + return; + this->button_touched_ = state; + for (auto *listener : this->button_listeners_) + listener->update_button(state); +} + +void CST816Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up CST816 Touchscreen..."); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + this->set_timeout(30, [this] { this->continue_setup_(); }); + } else { + this->continue_setup_(); + } +} + +void CST816Touchscreen::update_touches() { + uint8_t data[13]; + if (!this->read_bytes(REG_STATUS, data, sizeof data)) { + this->status_set_warning(); + return; + } + uint8_t num_of_touches = data[REG_TOUCH_NUM] & 3; + if (num_of_touches == 0) { + this->update_button_state_(false); + return; + } + + uint16_t x = encode_uint16(data[REG_XPOS_HIGH] & 0xF, data[REG_XPOS_LOW]); + uint16_t y = encode_uint16(data[REG_YPOS_HIGH] & 0xF, data[REG_YPOS_LOW]); + ESP_LOGV(TAG, "Read touch %d/%d", x, y); + if (x >= this->x_raw_max_) { + this->update_button_state_(true); + } else { + this->add_raw_touch_position_(0, x, y); + } +} + +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_); + const char *name; + switch (this->chip_id_) { + case CST820_CHIP_ID: + name = "CST820"; + break; + case CST826_CHIP_ID: + name = "CST826"; + break; + case CST816S_CHIP_ID: + name = "CST816S"; + break; + case CST816D_CHIP_ID: + name = "CST816D"; + break; + case CST716_CHIP_ID: + name = "CST716"; + break; + case CST816T_CHIP_ID: + name = "CST816T"; + break; + default: + name = "Unknown"; + break; + } + ESP_LOGCONFIG(TAG, " Chip type: %s", name); +} + +} // namespace cst816 +} // namespace esphome diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.h b/esphome/components/cst816/touchscreen/cst816_touchscreen.h new file mode 100644 index 0000000000..dc00e675ba --- /dev/null +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace cst816 { + +static const char *const TAG = "cst816.touchscreen"; + +static const uint8_t REG_STATUS = 0x00; +static const uint8_t REG_TOUCH_NUM = 0x02; +static const uint8_t REG_XPOS_HIGH = 0x03; +static const uint8_t REG_XPOS_LOW = 0x04; +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_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 CST820_CHIP_ID = 0xB7; +static const uint8_t CST816S_CHIP_ID = 0xB4; +static const uint8_t CST816D_CHIP_ID = 0xB6; +static const uint8_t CST816T_CHIP_ID = 0xB5; +static const uint8_t CST716_CHIP_ID = 0x20; + +class CST816ButtonListener { + public: + virtual void update_button(bool state) = 0; +}; + +class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void update_touches() override; + void register_button_listener(CST816ButtonListener *listener) { this->button_listeners_.push_back(listener); } + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + void set_skip_probe(bool skip_probe) { this->skip_probe_ = skip_probe; } + + protected: + void continue_setup_(); + void update_button_state_(bool state); + + InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{}; + uint8_t chip_id_{}; + bool skip_probe_{}; // if set, do not expect to be able to probe the controller on the i2c bus. + std::vector button_listeners_; + bool button_touched_{}; +}; + +} // namespace cst816 +} // namespace esphome diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index d555befcde..0aa0258a9b 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -1,6 +1,7 @@ #include "ct_clamp_sensor.h" #include "esphome/core/log.h" +#include #include namespace esphome { @@ -37,8 +38,8 @@ void CTClampSensor::update() { float rms_ac = 0; if (rms_ac_squared > 0) rms_ac = std::sqrt(rms_ac_squared); - ESP_LOGD(TAG, "'%s' - Raw AC Value: %.3fA after %d different samples (%d SPS)", this->name_.c_str(), rms_ac, - this->num_samples_, 1000 * this->num_samples_ / this->sample_duration_); + ESP_LOGD(TAG, "'%s' - Raw AC Value: %.3fA after %" PRIu32 " different samples (%" PRIu32 " SPS)", + this->name_.c_str(), rms_ac, this->num_samples_, 1000 * this->num_samples_ / this->sample_duration_); this->publish_state(rms_ac); }); diff --git a/esphome/components/current_based/cover.py b/esphome/components/current_based/cover.py index eb77a90aff..1fa4eaa03f 100644 --- a/esphome/components/current_based/cover.py +++ b/esphome/components/current_based/cover.py @@ -66,59 +66,63 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield cover.register_cover(var, config) + await cg.register_component(var, config) + await cover.register_cover(var, config) - yield automation.build_automation( + await automation.build_automation( var.get_stop_trigger(), [], config[CONF_STOP_ACTION] ) # OPEN - bin = yield cg.get_variable(config[CONF_OPEN_SENSOR]) + bin = await cg.get_variable(config[CONF_OPEN_SENSOR]) cg.add(var.set_open_sensor(bin)) cg.add( var.set_open_moving_current_threshold( config[CONF_OPEN_MOVING_CURRENT_THRESHOLD] ) ) - if CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD in config: - cg.add( - var.set_open_obstacle_current_threshold( - config[CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD] - ) + if ( + open_obsticle_current_threshold := config.get( + CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD ) + ) is not None: + cg.add(var.set_open_obstacle_current_threshold(open_obsticle_current_threshold)) + cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) - yield automation.build_automation( + await automation.build_automation( var.get_open_trigger(), [], config[CONF_OPEN_ACTION] ) # CLOSE - bin = yield cg.get_variable(config[CONF_CLOSE_SENSOR]) + bin = await cg.get_variable(config[CONF_CLOSE_SENSOR]) cg.add(var.set_close_sensor(bin)) cg.add( var.set_close_moving_current_threshold( config[CONF_CLOSE_MOVING_CURRENT_THRESHOLD] ) ) - if CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD in config: - cg.add( - var.set_close_obstacle_current_threshold( - config[CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD] - ) + if ( + close_obsticle_current_threshold := config.get( + CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD ) + ) is not None: + cg.add( + var.set_close_obstacle_current_threshold(close_obsticle_current_threshold) + ) + cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) - yield automation.build_automation( + await automation.build_automation( var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] ) cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK])) - if CONF_MAX_DURATION in config: - cg.add(var.set_max_duration(config[CONF_MAX_DURATION])) + if (max_duration := config.get(CONF_MAX_DURATION)) is not None: + cg.add(var.set_max_duration(max_duration)) cg.add(var.set_malfunction_detection(config[CONF_MALFUNCTION_DETECTION])) - if CONF_MALFUNCTION_ACTION in config: - yield automation.build_automation( - var.get_malfunction_trigger(), [], config[CONF_MALFUNCTION_ACTION] + if malfunction_action := config.get(CONF_MALFUNCTION_ACTION): + await automation.build_automation( + var.get_malfunction_trigger(), [], malfunction_action ) cg.add(var.set_start_sensing_delay(config[CONF_START_SENSING_DELAY])) diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index ff5ad43997..8404e07894 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -38,7 +38,7 @@ void CurrentBasedCover::control(const CoverCall &call) { } if (call.get_position().has_value()) { auto pos = *call.get_position(); - if (pos == this->position) { + if (fabsf(this->position - pos) < 0.01) { // already at target } else { auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; @@ -104,7 +104,8 @@ void CurrentBasedCover::loop() { ESP_LOGD(TAG, "'%s' - Close position reached. Took %.1fs.", this->name_.c_str(), dur); this->direction_idle_(COVER_CLOSED); } - } else if (now - this->start_dir_time_ > this->max_duration_) { + } + if (now - this->start_dir_time_ > this->max_duration_) { ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str()); this->direction_idle_(); } diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index fc204b2f3b..c88a6b0054 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -37,16 +37,12 @@ async def to_code(config): cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: - cg.add( - var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) - ) + if cold_white_color_temperature := config.get(CONF_COLD_WHITE_COLOR_TEMPERATURE): + cg.add(var.set_cold_white_temperature(cold_white_color_temperature)) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: - cg.add( - var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) - ) + if warm_white_color_temperature := config.get(CONF_WARM_WHITE_COLOR_TEMPERATURE): + cg.add(var.set_warm_white_temperature(warm_white_color_temperature)) cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) diff --git a/esphome/components/daikin_arc/__init__.py b/esphome/components/daikin_arc/__init__.py new file mode 100644 index 0000000000..f9164fb02b --- /dev/null +++ b/esphome/components/daikin_arc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@MagicBear"] diff --git a/esphome/components/daikin_arc/climate.py b/esphome/components/daikin_arc/climate.py new file mode 100644 index 0000000000..51f253e9cb --- /dev/null +++ b/esphome/components/daikin_arc/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc") +DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(DaikinArcClimate)} +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp new file mode 100644 index 0000000000..f806463d00 --- /dev/null +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -0,0 +1,487 @@ +#include "daikin_arc.h" + +#include + +#include "esphome/components/remote_base/remote_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace daikin_arc { + +static const char *const TAG = "daikin.climate"; + +void DaikinArcClimate::setup() { + climate_ir::ClimateIR::setup(); + + // Never send nan to HA + if (std::isnan(this->target_humidity)) + this->target_humidity = 0; + if (std::isnan(this->current_temperature)) + this->current_temperature = 0; + if (std::isnan(this->current_humidity)) + this->current_humidity = 0; +} + +void DaikinArcClimate::transmit_query_() { + uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00}; + + // Calculate checksum + for (int i = 0; i < sizeof(remote_header) - 1; i++) { + remote_header[sizeof(remote_header) - 1] += remote_header[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DAIKIN_IR_FREQUENCY); + + data->mark(DAIKIN_ARC_PRE_MARK); + data->space(DAIKIN_ARC_PRE_SPACE); + + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + + for (uint8_t i : remote_header) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = i & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +void DaikinArcClimate::transmit_state() { + // 0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00, + // 0x42, 0x49, 0x05, 0xA2, + uint8_t remote_header[20] = {0x11, 0xDA, 0x27, 0x00, 0x02, 0xd0, 0x02, 0x03, 0x80, 0x03, 0x82, 0x30, 0x41, 0x1f, 0x82, + 0xf4, + /* とつど */ + /* 0x13 */ + 0x00, 0x24, 0x00, 0x00}; + + // 05 0 [1:3]MODE 1 [OFF TMR] [ON TMR] Power + // 06-07 TEMP + // 08 [0:3] SPEED [4:7] Swing + // 09 00 + // 10 00 + // 11, 12: timer + // 13 [0:6] 0000000 [7] POWERMODE + // 14 0a + // 15 c4 + // 16 [0:3] 8 00 [6:7] SENSOR WIND = 11 / NORMAL = 00 + // 17 24 + + uint8_t remote_state[19] = { + 0x11, 0xDA, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x0a, 0xC4, + /* MODE TEMP HUMD FANH FANL + パワフル音声応答 */ + /* ON + 0x01入 0x0a */ + /* OF + 0x00切 0x02 */ + 0x80, 0x24, 0x00 + /* センサー風 */ + /* ON 0x83 */ + /* OF 0x80 */ + }; + + remote_state[5] = this->operation_mode_() | 0x08; + remote_state[6] = this->temperature_(); + remote_state[7] = this->humidity_(); + static uint8_t last_humidity = 0x66; + if (remote_state[7] != last_humidity && this->mode != climate::CLIMATE_MODE_OFF) { + ESP_LOGD(TAG, "Set Humditiy: %d, %d\n", (int) this->target_humidity, (int) remote_state[7]); + remote_header[9] |= 0x10; + last_humidity = remote_state[7]; + } + uint16_t fan_speed = this->fan_speed_(); + remote_state[8] = fan_speed >> 8; + remote_state[9] = fan_speed & 0xff; + + // Calculate checksum + for (int i = 0; i < sizeof(remote_header) - 1; i++) { + remote_header[sizeof(remote_header) - 1] += remote_header[i]; + } + + // Calculate checksum + for (int i = 0; i < DAIKIN_STATE_FRAME_SIZE - 1; i++) { + remote_state[DAIKIN_STATE_FRAME_SIZE - 1] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DAIKIN_IR_FREQUENCY); + + data->mark(DAIKIN_ARC_PRE_MARK); + data->space(DAIKIN_ARC_PRE_SPACE); + + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + + for (uint8_t i : remote_header) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = i & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(DAIKIN_MESSAGE_SPACE); + + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + + for (uint8_t i : remote_state) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = i & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DaikinArcClimate::operation_mode_() { + uint8_t operating_mode = DAIKIN_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DAIKIN_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DAIKIN_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DAIKIN_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= DAIKIN_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DAIKIN_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DAIKIN_MODE_OFF; + break; + } + + return operating_mode; +} + +uint16_t DaikinArcClimate::fan_speed_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DAIKIN_FAN_1 << 8; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DAIKIN_FAN_3 << 8; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DAIKIN_FAN_5 << 8; + break; + case climate::CLIMATE_FAN_AUTO: + default: + fan_speed = DAIKIN_FAN_AUTO << 8; + } + + // If swing is enabled switch first 4 bits to 1111 + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + fan_speed |= 0x0F00; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + fan_speed |= 0x000F; + break; + case climate::CLIMATE_SWING_BOTH: + fan_speed |= 0x0F0F; + break; + default: + break; + } + return fan_speed; +} + +uint8_t DaikinArcClimate::temperature_() { + // Force special temperatures depending on the mode + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + return 0x32; + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_DRY: + return 0xc0; + default: + float new_temp = clamp(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX); + uint8_t temperature = (uint8_t) floor(new_temp); + return temperature << 1 | (new_temp - temperature > 0 ? 0x01 : 0); + } +} + +uint8_t DaikinArcClimate::humidity_() { + if (this->target_humidity == 39) { + return 0; + } else if (this->target_humidity <= 40 || this->target_humidity == 44) { + return 40; + } else if (this->target_humidity <= 45 || this->target_humidity == 49) // 41 - 45 + { + return 45; + } else if (this->target_humidity <= 50 || this->target_humidity == 52) // 45 - 50 + { + return 50; + } else { + return 0xff; + } +} + +climate::ClimateTraits DaikinArcClimate::traits() { + climate::ClimateTraits traits = climate_ir::ClimateIR::traits(); + traits.set_supports_current_temperature(true); + traits.set_supports_current_humidity(false); + traits.set_supports_target_humidity(true); + traits.set_visual_min_humidity(38); + traits.set_visual_max_humidity(52); + return traits; +} + +bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum) { + ESP_LOGI(TAG, "checksum error"); + return false; + } + + char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0}; + for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) { + sprintf(buf, "%s%02x ", buf, frame[i]); + } + ESP_LOGD(TAG, "FRAME %s", buf); + + uint8_t mode = frame[5]; + if (mode & DAIKIN_MODE_ON) { + switch (mode & 0xF0) { + case DAIKIN_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DAIKIN_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DAIKIN_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DAIKIN_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case DAIKIN_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + uint8_t temperature = frame[6]; + if (!(temperature & 0xC0)) { + this->target_temperature = temperature >> 1; + this->target_temperature += (temperature & 0x1) ? 0.5 : 0; + } + this->target_humidity = frame[7]; // 0, 40, 45, 50, 0xff + uint8_t fan_mode = frame[8]; + uint8_t swing_mode = frame[9]; + if (fan_mode & 0xF && swing_mode & 0xF) { + this->swing_mode = climate::CLIMATE_SWING_BOTH; + } else if (fan_mode & 0xF) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else if (swing_mode & 0xF) { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + switch (fan_mode & 0xF0) { + case DAIKIN_FAN_1: + case DAIKIN_FAN_2: + case DAIKIN_FAN_SILENT: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DAIKIN_FAN_3: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DAIKIN_FAN_4: + case DAIKIN_FAN_5: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case DAIKIN_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + /* + 05 0 [1:3]MODE 1 [OFF TMR] [ON TMR] Power + 06-07 TEMP + 08 [0:3] SPEED [4:7] Swing + 09 00 + 10 00 + 11, 12: timer + 13 [0:6] 0000000 [7] POWERMODE + 14 0a + 15 c4 + 16 [0:3] 8 00 [6:7] SENSOR WIND = 11 / NORMAL = 00 + 17 24 + 05 06 07 08 09 10 11 12 13 14 15 16 17 18 + None FRAME 11 da 27 00 00 49 2e 00 b0 00 00 06 60 00 0a c4 80 24 11 + 1H FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 c6 30 00 2a c4 80 24 c5 + 1H30 FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 a6 32 00 2a c4 80 24 a7 + 2H FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 86 34 00 2a c4 80 24 89 + + */ + this->publish_state(); + return true; +} + +bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {}; + + bool valid_daikin_frame = false; + if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { + valid_daikin_frame = true; + int bytes_count = data.size() / 2 / 8; + std::unique_ptr buf(new char[bytes_count * 3 + 1]); + buf[0] = '\0'; + for (size_t i = 0; i < bytes_count; i++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) { + valid_daikin_frame = false; + break; + } + } + sprintf(buf.get(), "%s%02x ", buf.get(), byte); + } + ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size()); + } + if (!valid_daikin_frame) { + char sbuf[16 * 10 + 1]; + sbuf[0] = '\0'; + for (size_t j = 0; j < data.size(); j++) { + if ((j - 2) % 16 == 0) { + if (j > 0) { + ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); + } + sbuf[0] = '\0'; + } + char type_ch = ' '; + // debug_tolerance = 25% + + if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)) + type_ch = 'P'; + if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)) + type_ch = 'a'; + if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)) + type_ch = 'H'; + if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)) + type_ch = 'h'; + if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)) + type_ch = 'B'; + if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)) + type_ch = '1'; + if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)) + type_ch = '0'; + + if (abs(data[j]) > 100000) { + sprintf(sbuf, "%s%-5d[%c] ", sbuf, data[j] > 0 ? 99999 : -99999, type_ch); + } else { + sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch); + } + if (j == data.size() - 1) { + ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); + } + } + } + + data.reset(); + + if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { + ESP_LOGI(TAG, "non daikin_arc expect item"); + return false; + } + + for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) { + ESP_LOGI(TAG, "non daikin_arc expect item pos: %d", pos); + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != 0x11) { + ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte); + return false; + } + } else if (pos == 1) { + // frame header + if (byte != 0xDA) { + ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte); + return false; + } + } else if (pos == 2) { + // frame header + if (byte != 0x27) { + ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte); + return false; + } + } else if (pos == 3) { // NOLINT(bugprone-branch-clone) + // frame header + if (byte != 0x00) { + ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte); + return false; + } + } else if (pos == 4) { + // frame type + if (byte != 0x00) { + ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte); + return false; + } + } else if (pos == 5) { + if (data.size() == 385) { + /* + 11 da 27 00 00 1a 0c 04 2c 21 61 07 00 07 0c 00 18 00 0e 3c 00 6c 1b 61 + Inside Temp + Outside Temp + Humdidity + + */ + this->current_temperature = state_frame[5]; // Inside temperature + // this->current_temperature = state_frame[6]; // Outside temperature + this->publish_state(); + return true; + } else if ((byte & 0x40) != 0x40) { + ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte); + return false; + } + } + } + return this->parse_state_frame_(state_frame); +} + +void DaikinArcClimate::control(const climate::ClimateCall &call) { + if (call.get_target_humidity().has_value()) { + this->target_humidity = *call.get_target_humidity(); + } + climate_ir::ClimateIR::control(call); +} + +} // namespace daikin_arc +} // namespace esphome diff --git a/esphome/components/daikin_arc/daikin_arc.h b/esphome/components/daikin_arc/daikin_arc.h new file mode 100644 index 0000000000..6cfffd4725 --- /dev/null +++ b/esphome/components/daikin_arc/daikin_arc.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace daikin_arc { + +// Values for Daikin ARC43XXX IR Controllers +// Temperature +const uint8_t DAIKIN_TEMP_MIN = 10; // Celsius +const uint8_t DAIKIN_TEMP_MAX = 30; // Celsius + +// Modes +const uint8_t DAIKIN_MODE_AUTO = 0x00; +const uint8_t DAIKIN_MODE_COOL = 0x30; +const uint8_t DAIKIN_MODE_HEAT = 0x40; +const uint8_t DAIKIN_MODE_DRY = 0x20; +const uint8_t DAIKIN_MODE_FAN = 0x60; +const uint8_t DAIKIN_MODE_OFF = 0x00; +const uint8_t DAIKIN_MODE_ON = 0x01; + +// Fan Speed +const uint8_t DAIKIN_FAN_AUTO = 0xA0; +const uint8_t DAIKIN_FAN_SILENT = 0xB0; +const uint8_t DAIKIN_FAN_1 = 0x30; +const uint8_t DAIKIN_FAN_2 = 0x40; +const uint8_t DAIKIN_FAN_3 = 0x50; +const uint8_t DAIKIN_FAN_4 = 0x60; +const uint8_t DAIKIN_FAN_5 = 0x70; + +// IR Transmission +const uint32_t DAIKIN_IR_FREQUENCY = 38000; +const uint32_t DAIKIN_ARC_PRE_MARK = 9950; +const uint32_t DAIKIN_ARC_PRE_SPACE = 25100; +const uint32_t DAIKIN_HEADER_MARK = 3450; +const uint32_t DAIKIN_HEADER_SPACE = 1760; +const uint32_t DAIKIN_BIT_MARK = 400; +const uint32_t DAIKIN_ONE_SPACE = 1300; +const uint32_t DAIKIN_ZERO_SPACE = 480; +const uint32_t DAIKIN_MESSAGE_SPACE = 35000; + +const uint8_t DAIKIN_DBG_TOLERANCE = 25; +#define DAIKIN_DBG_LOWER(x) ((100 - DAIKIN_DBG_TOLERANCE) * (x) / 100U) +#define DAIKIN_DBG_UPPER(x) ((100 + DAIKIN_DBG_TOLERANCE) * (x) / 100U) + +// State Frame size +const uint8_t DAIKIN_STATE_FRAME_SIZE = 19; + +class DaikinArcClimate : public climate_ir::ClimateIR { + public: + DaikinArcClimate() + : climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 0.5f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + + void setup() override; + + protected: + void control(const climate::ClimateCall &call) override; + // Transmit via IR the state of this climate controller. + void transmit_query_(); + void transmit_state() override; + climate::ClimateTraits traits() override; + uint8_t operation_mode_(); + uint16_t fan_speed_(); + uint8_t temperature_(); + uint8_t humidity_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); +}; + +} // namespace daikin_arc +} // namespace esphome diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py index 3468b6533c..7a5bd9b14d 100644 --- a/esphome/components/daikin_brc/climate.py +++ b/esphome/components/daikin_brc/climate.py @@ -1,14 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate_ir -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir"] daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc") DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) -CONF_USE_FAHRENHEIT = "use_fahrenheit" CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( { diff --git a/esphome/components/dallas/__init__.py b/esphome/components/dallas/__init__.py index 0f71399a7c..6c2a9d830e 100644 --- a/esphome/components/dallas/__init__.py +++ b/esphome/components/dallas/__init__.py @@ -1,25 +1,7 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins -from esphome.const import CONF_ID, CONF_PIN MULTI_CONF = True -AUTO_LOAD = ["sensor"] -dallas_ns = cg.esphome_ns.namespace("dallas") -DallasComponent = dallas_ns.class_("DallasComponent", cg.PollingComponent) - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(DallasComponent), - cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - } -).extend(cv.polling_component_schema("60s")) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - - pin = await cg.gpio_pin_expression(config[CONF_PIN]) - cg.add(var.set_pin(pin)) +CONFIG_SCHEMA = cv.invalid( + 'The "dallas" component has been replaced by the "one_wire" component.\nhttps://esphome.io/components/one_wire' +) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp deleted file mode 100644 index 302422d6c7..0000000000 --- a/esphome/components/dallas/dallas_component.cpp +++ /dev/null @@ -1,279 +0,0 @@ -#include "dallas_component.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace dallas { - -static const char *const TAG = "dallas.sensor"; - -static const uint8_t DALLAS_MODEL_DS18S20 = 0x10; -static const uint8_t DALLAS_MODEL_DS1822 = 0x22; -static const uint8_t DALLAS_MODEL_DS18B20 = 0x28; -static const uint8_t DALLAS_MODEL_DS1825 = 0x3B; -static const uint8_t DALLAS_MODEL_DS28EA00 = 0x42; -static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44; -static const uint8_t DALLAS_COMMAND_READ_SCRATCH_PAD = 0xBE; -static const uint8_t DALLAS_COMMAND_WRITE_SCRATCH_PAD = 0x4E; - -uint16_t DallasTemperatureSensor::millis_to_wait_for_conversion() const { - switch (this->resolution_) { - case 9: - return 94; - case 10: - return 188; - case 11: - return 375; - default: - return 750; - } -} - -void DallasComponent::setup() { - ESP_LOGCONFIG(TAG, "Setting up DallasComponent..."); - - pin_->setup(); - - // clear bus with 480µs high, otherwise initial reset in search_vec() fails - pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - delayMicroseconds(480); - - one_wire_ = new ESPOneWire(pin_); // NOLINT(cppcoreguidelines-owning-memory) - - std::vector raw_sensors; - raw_sensors = this->one_wire_->search_vec(); - - for (auto &address : raw_sensors) { - 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()); - continue; - } - if (address8[0] != DALLAS_MODEL_DS18S20 && address8[0] != DALLAS_MODEL_DS1822 && - address8[0] != DALLAS_MODEL_DS18B20 && address8[0] != DALLAS_MODEL_DS1825 && - address8[0] != DALLAS_MODEL_DS28EA00) { - ESP_LOGW(TAG, "Unknown device type 0x%02X.", address8[0]); - continue; - } - this->found_sensors_.push_back(address); - } - - for (auto *sensor : this->sensors_) { - if (sensor->get_index().has_value()) { - if (*sensor->get_index() >= this->found_sensors_.size()) { - this->status_set_error(); - continue; - } - sensor->set_address(this->found_sensors_[*sensor->get_index()]); - } - - if (!sensor->setup_sensor()) { - this->status_set_error(); - } - } -} -void DallasComponent::dump_config() { - ESP_LOGCONFIG(TAG, "DallasComponent:"); - LOG_PIN(" Pin: ", this->pin_); - LOG_UPDATE_INTERVAL(this); - - if (this->found_sensors_.empty()) { - ESP_LOGW(TAG, " Found no sensors!"); - } else { - ESP_LOGD(TAG, " Found sensors:"); - for (auto &address : this->found_sensors_) { - ESP_LOGD(TAG, " 0x%s", format_hex(address).c_str()); - } - } - - for (auto *sensor : this->sensors_) { - LOG_SENSOR(" ", "Device", sensor); - if (sensor->get_index().has_value()) { - ESP_LOGCONFIG(TAG, " Index %u", *sensor->get_index()); - if (*sensor->get_index() >= this->found_sensors_.size()) { - ESP_LOGE(TAG, "Couldn't find sensor by index - not connected. Proceeding without it."); - continue; - } - } - ESP_LOGCONFIG(TAG, " Address: %s", sensor->get_address_name().c_str()); - ESP_LOGCONFIG(TAG, " Resolution: %u", sensor->get_resolution()); - } -} - -void DallasComponent::register_sensor(DallasTemperatureSensor *sensor) { this->sensors_.push_back(sensor); } -void DallasComponent::update() { - this->status_clear_warning(); - - bool result; - { - InterruptLock lock; - result = this->one_wire_->reset(); - } - if (!result) { - ESP_LOGE(TAG, "Requesting conversion failed"); - this->status_set_warning(); - for (auto *sensor : this->sensors_) { - sensor->publish_state(NAN); - } - return; - } - - { - InterruptLock lock; - this->one_wire_->skip(); - this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); - } - - for (auto *sensor : this->sensors_) { - this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] { - bool res = sensor->read_scratch_pad(); - - if (!res) { - ESP_LOGW(TAG, "'%s' - Resetting bus for read failed!", sensor->get_name().c_str()); - sensor->publish_state(NAN); - this->status_set_warning(); - return; - } - if (!sensor->check_scratch_pad()) { - sensor->publish_state(NAN); - this->status_set_warning(); - return; - } - - float tempc = sensor->get_temp_c(); - ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", sensor->get_name().c_str(), tempc); - sensor->publish_state(tempc); - }); - } -} - -void DallasTemperatureSensor::set_address(uint64_t address) { this->address_ = address; } -uint8_t DallasTemperatureSensor::get_resolution() const { return this->resolution_; } -void DallasTemperatureSensor::set_resolution(uint8_t resolution) { this->resolution_ = resolution; } -optional DallasTemperatureSensor::get_index() const { return this->index_; } -void DallasTemperatureSensor::set_index(uint8_t index) { this->index_ = index; } -uint8_t *DallasTemperatureSensor::get_address8() { return reinterpret_cast(&this->address_); } -const std::string &DallasTemperatureSensor::get_address_name() { - if (this->address_name_.empty()) { - this->address_name_ = std::string("0x") + format_hex(this->address_); - } - - return this->address_name_; -} -bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { - auto *wire = this->parent_->one_wire_; - - { - InterruptLock lock; - - if (!wire->reset()) { - return false; - } - } - - { - InterruptLock lock; - - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD); - - for (unsigned char &i : this->scratch_pad_) { - i = wire->read8(); - } - } - - return true; -} -bool DallasTemperatureSensor::setup_sensor() { - bool r = this->read_scratch_pad(); - - if (!r) { - ESP_LOGE(TAG, "Reading scratchpad failed: reset"); - return false; - } - if (!this->check_scratch_pad()) - return false; - - if (this->scratch_pad_[4] == this->resolution_) - return false; - - if (this->get_address8()[0] == DALLAS_MODEL_DS18S20) { - // DS18S20 doesn't support resolution. - ESP_LOGW(TAG, "DS18S20 doesn't support setting resolution."); - return false; - } - - switch (this->resolution_) { - case 12: - this->scratch_pad_[4] = 0x7F; - break; - case 11: - this->scratch_pad_[4] = 0x5F; - break; - case 10: - this->scratch_pad_[4] = 0x3F; - break; - case 9: - default: - this->scratch_pad_[4] = 0x1F; - break; - } - - auto *wire = this->parent_->one_wire_; - { - InterruptLock lock; - if (wire->reset()) { - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); - wire->write8(this->scratch_pad_[2]); // high alarm temp - wire->write8(this->scratch_pad_[3]); // low alarm temp - wire->write8(this->scratch_pad_[4]); // resolution - wire->reset(); - - // write value to EEPROM - wire->select(this->address_); - wire->write8(0x48); - } - } - - delay(20); // allow it to finish operation - wire->reset(); - return true; -} -bool DallasTemperatureSensor::check_scratch_pad() { - bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]); - bool config_validity = false; - - switch (this->get_address8()[0]) { - case DALLAS_MODEL_DS18B20: - config_validity = ((this->scratch_pad_[4] & 0x9F) == 0x1F); - break; - default: - config_validity = ((this->scratch_pad_[4] & 0x10) == 0x10); - } - -#ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE - ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], - this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], - this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], - crc8(this->scratch_pad_, 8)); -#endif - if (!chksum_validity) { - ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str()); - } else if (!config_validity) { - ESP_LOGW(TAG, "'%s' - Scratch pad config register invalid!", this->get_name().c_str()); - } - return chksum_validity && config_validity; -} -float DallasTemperatureSensor::get_temp_c() { - int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3); - if (this->get_address8()[0] == DALLAS_MODEL_DS18S20) { - int diff = (this->scratch_pad_[7] - this->scratch_pad_[6]) << 7; - temp = ((temp & 0xFFF0) << 3) - 16 + (diff / this->scratch_pad_[7]); - } - - return temp / 128.0f; -} -std::string DallasTemperatureSensor::unique_id() { return "dallas-" + str_lower_case(format_hex(this->address_)); } - -} // namespace dallas -} // namespace esphome diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h deleted file mode 100644 index b21bc02e54..0000000000 --- a/esphome/components/dallas/dallas_component.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esp_one_wire.h" - -#include - -namespace esphome { -namespace dallas { - -class DallasTemperatureSensor; - -class DallasComponent : public PollingComponent { - public: - void set_pin(InternalGPIOPin *pin) { pin_ = pin; } - void register_sensor(DallasTemperatureSensor *sensor); - - void setup() override; - void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - - void update() override; - - protected: - friend DallasTemperatureSensor; - - InternalGPIOPin *pin_; - ESPOneWire *one_wire_; - std::vector sensors_; - std::vector found_sensors_; -}; - -/// Internal class that helps us create multiple sensors for one Dallas hub. -class DallasTemperatureSensor : public sensor::Sensor { - public: - void set_parent(DallasComponent *parent) { parent_ = parent; } - /// Helper to get a pointer to the address as uint8_t. - uint8_t *get_address8(); - /// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29". - const std::string &get_address_name(); - - /// Set the 64-bit unsigned address for this sensor. - void set_address(uint64_t address); - /// Get the index of this sensor. (0 if using address.) - optional get_index() const; - /// Set the index of this sensor. If using index, address will be set after setup. - void set_index(uint8_t index); - /// Get the set resolution for this sensor. - uint8_t get_resolution() const; - /// Set the resolution for this sensor. - void set_resolution(uint8_t resolution); - /// Get the number of milliseconds we have to wait for the conversion phase. - uint16_t millis_to_wait_for_conversion() const; - - bool setup_sensor(); - bool read_scratch_pad(); - - bool check_scratch_pad(); - - float get_temp_c(); - - std::string unique_id() override; - - protected: - DallasComponent *parent_; - uint64_t address_; - optional index_; - - uint8_t resolution_; - std::string address_name_; - uint8_t scratch_pad_[9] = { - 0, - }; -}; - -} // namespace dallas -} // namespace esphome diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp deleted file mode 100644 index 32ddf07fb6..0000000000 --- a/esphome/components/dallas/esp_one_wire.cpp +++ /dev/null @@ -1,252 +0,0 @@ -#include "esp_one_wire.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace dallas { - -static const char *const TAG = "dallas.one_wire"; - -const uint8_t ONE_WIRE_ROM_SELECT = 0x55; -const int ONE_WIRE_ROM_SEARCH = 0xF0; - -ESPOneWire::ESPOneWire(InternalGPIOPin *pin) { pin_ = pin->to_isr(); } - -bool HOT IRAM_ATTR ESPOneWire::reset() { - // See reset here: - // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html - // Wait for communication to clear (delay G) - pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - uint8_t retries = 125; - do { - if (--retries == 0) - return false; - delayMicroseconds(2); - } while (!pin_.digital_read()); - - // Send 480µs LOW TX reset pulse (drive bus low, delay H) - pin_.pin_mode(gpio::FLAG_OUTPUT); - pin_.digital_write(false); - delayMicroseconds(480); - - // Release the bus, delay I - pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - delayMicroseconds(70); - - // sample bus, 0=device(s) present, 1=no device present - bool r = !pin_.digital_read(); - // delay J - delayMicroseconds(410); - return r; -} - -void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { - // drive bus low - pin_.pin_mode(gpio::FLAG_OUTPUT); - pin_.digital_write(false); - - // from datasheet: - // write 0 low time: t_low0: min=60µs, max=120µs - // write 1 low time: t_low1: min=1µs, max=15µs - // time slot: t_slot: min=60µs, max=120µs - // recovery time: t_rec: min=1µs - // ds18b20 appears to read the bus after roughly 14µs - uint32_t delay0 = bit ? 6 : 60; - uint32_t delay1 = bit ? 54 : 5; - - // delay A/C - delayMicroseconds(delay0); - // release bus - pin_.digital_write(true); - // delay B/D - delayMicroseconds(delay1); -} - -bool HOT IRAM_ATTR ESPOneWire::read_bit() { - // drive bus low - pin_.pin_mode(gpio::FLAG_OUTPUT); - pin_.digital_write(false); - - // note: for reading we'll need very accurate timing, as the - // timing for the digital_read() is tight; according to the datasheet, - // we should read at the end of 16µs starting from the bus low - // typically, the ds18b20 pulls the line high after 11µs for a logical 1 - // and 29µs for a logical 0 - - uint32_t start = micros(); - // datasheet says >1µs - delayMicroseconds(3); - - // release bus, delay E - pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - - // Unfortunately some frameworks have different characteristics than others - // esp32 arduino appears to pull the bus low only after the digital_write(false), - // whereas on esp-idf it already happens during the pin_mode(OUTPUT) - // manually correct for this with these constants. - -#ifdef USE_ESP32 - uint32_t timing_constant = 12; -#else - uint32_t timing_constant = 14; -#endif - - // measure from start value directly, to get best accurate timing no matter - // how long pin_mode/delayMicroseconds took - while (micros() - start < timing_constant) - ; - - // sample bus to read bit from peer - bool r = pin_.digital_read(); - - // read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked - uint32_t now = micros(); - if (now - start < 60) - delayMicroseconds(60 - (now - start)); - - return r; -} - -void IRAM_ATTR ESPOneWire::write8(uint8_t val) { - for (uint8_t i = 0; i < 8; i++) { - this->write_bit(bool((1u << i) & val)); - } -} - -void IRAM_ATTR ESPOneWire::write64(uint64_t val) { - for (uint8_t i = 0; i < 64; i++) { - this->write_bit(bool((1ULL << i) & val)); - } -} - -uint8_t IRAM_ATTR ESPOneWire::read8() { - uint8_t ret = 0; - for (uint8_t i = 0; i < 8; i++) { - ret |= (uint8_t(this->read_bit()) << i); - } - return ret; -} -uint64_t IRAM_ATTR ESPOneWire::read64() { - uint64_t ret = 0; - for (uint8_t i = 0; i < 8; i++) { - ret |= (uint64_t(this->read_bit()) << i); - } - return ret; -} -void IRAM_ATTR ESPOneWire::select(uint64_t address) { - this->write8(ONE_WIRE_ROM_SELECT); - this->write64(address); -} -void IRAM_ATTR ESPOneWire::reset_search() { - this->last_discrepancy_ = 0; - this->last_device_flag_ = false; - this->rom_number_ = 0; -} -uint64_t IRAM_ATTR ESPOneWire::search() { - if (this->last_device_flag_) { - return 0u; - } - - { - InterruptLock lock; - if (!this->reset()) { - // Reset failed or no devices present - this->reset_search(); - return 0u; - } - } - - uint8_t id_bit_number = 1; - uint8_t last_zero = 0; - uint8_t rom_byte_number = 0; - bool search_result = false; - uint8_t rom_byte_mask = 1; - - { - InterruptLock lock; - // Initiate search - this->write8(ONE_WIRE_ROM_SEARCH); - do { - // read bit - bool id_bit = this->read_bit(); - // read its complement - bool cmp_id_bit = this->read_bit(); - - if (id_bit && cmp_id_bit) { - // No devices participating in search - break; - } - - bool branch; - - if (id_bit != cmp_id_bit) { - // only chose one branch, the other one doesn't have any devices. - branch = id_bit; - } else { - // there are devices with both 0s and 1s at this bit - if (id_bit_number < this->last_discrepancy_) { - branch = (this->rom_number8_()[rom_byte_number] & rom_byte_mask) > 0; - } else { - branch = id_bit_number == this->last_discrepancy_; - } - - if (!branch) { - last_zero = id_bit_number; - } - } - - if (branch) { - // set bit - this->rom_number8_()[rom_byte_number] |= rom_byte_mask; - } else { - // clear bit - this->rom_number8_()[rom_byte_number] &= ~rom_byte_mask; - } - - // choose/announce branch - this->write_bit(branch); - id_bit_number++; - rom_byte_mask <<= 1; - if (rom_byte_mask == 0u) { - // go to next byte - rom_byte_number++; - rom_byte_mask = 1; - } - } while (rom_byte_number < 8); // loop through all bytes - } - - if (id_bit_number >= 65) { - this->last_discrepancy_ = last_zero; - if (this->last_discrepancy_ == 0) { - // we're at root and have no choices left, so this was the last one. - this->last_device_flag_ = true; - } - search_result = true; - } - - search_result = search_result && (this->rom_number8_()[0] != 0); - if (!search_result) { - this->reset_search(); - return 0u; - } - - return this->rom_number_; -} -std::vector ESPOneWire::search_vec() { - std::vector res; - - this->reset_search(); - uint64_t address; - while ((address = this->search()) != 0u) - res.push_back(address); - - return res; -} -void IRAM_ATTR ESPOneWire::skip() { - this->write8(0xCC); // skip ROM -} - -uint8_t IRAM_ATTR *ESPOneWire::rom_number8_() { return reinterpret_cast(&this->rom_number_); } - -} // namespace dallas -} // namespace esphome diff --git a/esphome/components/dallas/esp_one_wire.h b/esphome/components/dallas/esp_one_wire.h deleted file mode 100644 index 7544a6fe98..0000000000 --- a/esphome/components/dallas/esp_one_wire.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include "esphome/core/hal.h" -#include - -namespace esphome { -namespace dallas { - -extern const uint8_t ONE_WIRE_ROM_SELECT; -extern const int ONE_WIRE_ROM_SEARCH; - -class ESPOneWire { - public: - explicit ESPOneWire(InternalGPIOPin *pin); - - /** Reset the bus, should be done before all write operations. - * - * Takes approximately 1ms. - * - * @return Whether the operation was successful. - */ - bool reset(); - - /// Write a single bit to the bus, takes about 70µs. - void write_bit(bool bit); - - /// Read a single bit from the bus, takes about 70µs - bool read_bit(); - - /// Write a word to the bus. LSB first. - void write8(uint8_t val); - - /// Write a 64 bit unsigned integer to the bus. LSB first. - void write64(uint64_t val); - - /// Write a command to the bus that addresses all devices by skipping the ROM. - void skip(); - - /// Read an 8 bit word from the bus. - uint8_t read8(); - - /// Read an 64-bit unsigned integer from the bus. - uint64_t read64(); - - /// Select a specific address on the bus for the following command. - void select(uint64_t address); - - /// Reset the device search. - void reset_search(); - - /// Search for a 1-Wire device on the bus. Returns 0 if all devices have been found. - uint64_t search(); - - /// Helper that wraps search in a std::vector. - std::vector search_vec(); - - protected: - /// Helper to get the internal 64-bit unsigned rom number as a 8-bit integer pointer. - inline uint8_t *rom_number8_(); - - ISRInternalGPIOPin pin_; - uint8_t last_discrepancy_{0}; - bool last_device_flag_{false}; - uint64_t rom_number_{0}; -}; - -} // namespace dallas -} // namespace esphome diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py index 9288f0a3a6..69f8fc3b9e 100644 --- a/esphome/components/dallas/sensor.py +++ b/esphome/components/dallas/sensor.py @@ -1,50 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor -from esphome.const import ( - CONF_ADDRESS, - CONF_DALLAS_ID, - CONF_INDEX, - CONF_RESOLUTION, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, + +CONFIG_SCHEMA = cv.invalid( + 'The "dallas" sensor is now "dallas_temp"\nhttps://esphome.io/components/sensor/dallas_temp' ) -from . import DallasComponent, dallas_ns - -DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sensor) - -CONFIG_SCHEMA = cv.All( - sensor.sensor_schema( - DallasTemperatureSensor, - unit_of_measurement=UNIT_CELSIUS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent), - cv.Optional(CONF_ADDRESS): cv.hex_int, - cv.Optional(CONF_INDEX): cv.positive_int, - cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12), - } - ), - cv.has_exactly_one_key(CONF_ADDRESS, CONF_INDEX), -) - - -async def to_code(config): - hub = await cg.get_variable(config[CONF_DALLAS_ID]) - var = await sensor.new_sensor(config) - - if CONF_ADDRESS in config: - cg.add(var.set_address(config[CONF_ADDRESS])) - else: - cg.add(var.set_index(config[CONF_INDEX])) - - if CONF_RESOLUTION in config: - cg.add(var.set_resolution(config[CONF_RESOLUTION])) - - cg.add(var.set_parent(hub)) - - cg.add(hub.register_sensor(var)) diff --git a/esphome/components/dallas_temp/__init__.py b/esphome/components/dallas_temp/__init__.py new file mode 100644 index 0000000000..3f73044ca8 --- /dev/null +++ b/esphome/components/dallas_temp/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ssieb"] diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp new file mode 100644 index 0000000000..ae567d6a76 --- /dev/null +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -0,0 +1,169 @@ +#include "dallas_temp.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace dallas_temp { + +static const char *const TAG = "dallas.temp.sensor"; + +static const uint8_t DALLAS_MODEL_DS18S20 = 0x10; +static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44; +static const uint8_t DALLAS_COMMAND_READ_SCRATCH_PAD = 0xBE; +static const uint8_t DALLAS_COMMAND_WRITE_SCRATCH_PAD = 0x4E; +static const uint8_t DALLAS_COMMAND_COPY_SCRATCH_PAD = 0x48; + +uint16_t DallasTemperatureSensor::millis_to_wait_for_conversion_() const { + switch (this->resolution_) { + case 9: + return 94; + case 10: + return 188; + case 11: + return 375; + default: + return 750; + } +} + +void DallasTemperatureSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Dallas Temperature Sensor:"); + if (this->address_ == 0) { + ESP_LOGW(TAG, " Unable to select an address"); + return; + } + LOG_ONE_WIRE_DEVICE(this); + ESP_LOGCONFIG(TAG, " Resolution: %u bits", this->resolution_); + LOG_UPDATE_INTERVAL(this); +} + +void DallasTemperatureSensor::update() { + if (this->address_ == 0) + return; + + this->status_clear_warning(); + + this->send_command_(DALLAS_COMMAND_START_CONVERSION); + + this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] { + if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) { + this->publish_state(NAN); + return; + } + + float tempc = this->get_temp_c_(); + ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", this->get_name().c_str(), tempc); + this->publish_state(tempc); + }); +} + +void IRAM_ATTR DallasTemperatureSensor::read_scratch_pad_int_() { + for (uint8_t &i : this->scratch_pad_) { + i = this->bus_->read8(); + } +} + +bool DallasTemperatureSensor::read_scratch_pad_() { + bool success; + { + InterruptLock lock; + success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD); + if (success) + this->read_scratch_pad_int_(); + } + if (!success) { + ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); + this->status_set_warning("bus reset failed"); + } + return success; +} + +void DallasTemperatureSensor::setup() { + ESP_LOGCONFIG(TAG, "setting up Dallas temperature sensor..."); + if (!this->check_address_()) + return; + if (!this->read_scratch_pad_()) + return; + if (!this->check_scratch_pad_()) + return; + + if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) { + // DS18S20 doesn't support resolution. + ESP_LOGW(TAG, "DS18S20 doesn't support setting resolution."); + return; + } + + uint8_t res; + switch (this->resolution_) { + case 12: + res = 0x7F; + break; + case 11: + res = 0x5F; + break; + case 10: + res = 0x3F; + break; + case 9: + default: + res = 0x1F; + break; + } + + if (this->scratch_pad_[4] == res) + return; + this->scratch_pad_[4] = res; + + { + InterruptLock lock; + if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) { + this->bus_->write8(this->scratch_pad_[2]); // high alarm temp + this->bus_->write8(this->scratch_pad_[3]); // low alarm temp + this->bus_->write8(this->scratch_pad_[4]); // resolution + } + + // write value to EEPROM + this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD); + } +} + +bool DallasTemperatureSensor::check_scratch_pad_() { + bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]); + +#ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE + ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], + this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], + this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], + crc8(this->scratch_pad_, 8)); +#endif + if (!chksum_validity) { + ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str()); + this->status_set_warning("scratch pad checksum invalid"); + } + return chksum_validity; +} + +float DallasTemperatureSensor::get_temp_c_() { + int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0]; + if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) { + return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25; + } + switch (this->resolution_) { + case 9: + temp &= 0xfff8; + break; + case 10: + temp &= 0xfffc; + break; + case 11: + temp &= 0xfffe; + break; + case 12: + default: + break; + } + + return temp / 16.0f; +} + +} // namespace dallas_temp +} // namespace esphome diff --git a/esphome/components/dallas_temp/dallas_temp.h b/esphome/components/dallas_temp/dallas_temp.h new file mode 100644 index 0000000000..604c9d0cd7 --- /dev/null +++ b/esphome/components/dallas_temp/dallas_temp.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/one_wire/one_wire.h" + +namespace esphome { +namespace dallas_temp { + +class DallasTemperatureSensor : public PollingComponent, public sensor::Sensor, public one_wire::OneWireDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + /// Set the resolution for this sensor. + void set_resolution(uint8_t resolution) { this->resolution_ = resolution; } + + protected: + uint8_t resolution_; + uint8_t scratch_pad_[9] = {0}; + + /// Get the number of milliseconds we have to wait for the conversion phase. + uint16_t millis_to_wait_for_conversion_() const; + bool read_scratch_pad_(); + void read_scratch_pad_int_(); + bool check_scratch_pad_(); + float get_temp_c_(); +}; + +} // namespace dallas_temp +} // namespace esphome diff --git a/esphome/components/dallas_temp/sensor.py b/esphome/components/dallas_temp/sensor.py new file mode 100644 index 0000000000..ab14a9afd5 --- /dev/null +++ b/esphome/components/dallas_temp/sensor.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import one_wire, sensor +from esphome.const import ( + CONF_RESOLUTION, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +dallas_temp_ns = cg.esphome_ns.namespace("dallas_temp") + +DallasTemperatureSensor = dallas_temp_ns.class_( + "DallasTemperatureSensor", + cg.PollingComponent, + sensor.Sensor, + one_wire.OneWireDevice, +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + DallasTemperatureSensor, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12), + } + ) + .extend(one_wire.one_wire_device_schema()) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await one_wire.register_one_wire_device(var, config) + + cg.add(var.set_resolution(config[CONF_RESOLUTION])) diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py index ce0cf5216a..669d40a68d 100644 --- a/esphome/components/daly_bms/__init__.py +++ b/esphome/components/daly_bms/__init__.py @@ -4,8 +4,8 @@ from esphome.components import uart from esphome.const import CONF_ID, CONF_ADDRESS CODEOWNERS = ["@s1lvi0"] +MULTI_CONF = True DEPENDENCIES = ["uart"] -AUTO_LOAD = ["sensor", "text_sensor", "binary_sensor"] CONF_BMS_DALY_ID = "bms_daly_id" diff --git a/esphome/components/daly_bms/binary_sensor.py b/esphome/components/daly_bms/binary_sensor.py index 7b252b5e89..724f19315b 100644 --- a/esphome/components/daly_bms/binary_sensor.py +++ b/esphome/components/daly_bms/binary_sensor.py @@ -27,9 +27,8 @@ CONFIG_SCHEMA = cv.All( async def setup_conf(config, key, hub): - if key in config: - conf = config[key] - var = await binary_sensor.new_binary_sensor(conf) + if sensor_config := config.get(key): + var = await binary_sensor.new_binary_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_binary_sensor")(var)) diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 3b41723327..8f6fc0fb57 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -1,6 +1,6 @@ #include "daly_bms.h" -#include "esphome/core/log.h" #include +#include "esphome/core/log.h" namespace esphome { namespace daly_bms { @@ -19,7 +19,7 @@ static const uint8_t DALY_REQUEST_STATUS = 0x94; static const uint8_t DALY_REQUEST_CELL_VOLTAGE = 0x95; static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96; -void DalyBmsComponent::setup() {} +void DalyBmsComponent::setup() { this->next_request_ = 1; } void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); @@ -27,20 +27,78 @@ void DalyBmsComponent::dump_config() { } void DalyBmsComponent::update() { - this->request_data_(DALY_REQUEST_BATTERY_LEVEL); - this->request_data_(DALY_REQUEST_MIN_MAX_VOLTAGE); - this->request_data_(DALY_REQUEST_MIN_MAX_TEMPERATURE); - this->request_data_(DALY_REQUEST_MOS); - this->request_data_(DALY_REQUEST_STATUS); - this->request_data_(DALY_REQUEST_CELL_VOLTAGE); - this->request_data_(DALY_REQUEST_TEMPERATURE); + this->trigger_next_ = true; + this->next_request_ = 0; +} - std::vector get_battery_level_data; - int available_data = this->available(); - if (available_data >= DALY_FRAME_SIZE) { - get_battery_level_data.resize(available_data); - this->read_array(get_battery_level_data.data(), available_data); - this->decode_data_(get_battery_level_data); +void DalyBmsComponent::loop() { + const uint32_t now = millis(); + if (this->receiving_ && (now - this->last_transmission_ >= 200)) { + // last transmission too long ago. Reset RX index. + ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index."); + this->data_.clear(); + this->receiving_ = false; + } + if ((now - this->last_transmission_ >= 250) && !this->trigger_next_) { + // last transmittion longer than 0.25s ago -> trigger next request + this->last_transmission_ = now; + this->trigger_next_ = true; + } + if (available()) + this->last_transmission_ = now; + while (available()) { + uint8_t c; + read_byte(&c); + if (!this->receiving_) { + if (c != 0xa5) + continue; + this->receiving_ = true; + } + this->data_.push_back(c); + if (this->data_.size() == 4) + this->data_count_ = c; + if ((this->data_.size() > 4) and (data_.size() == this->data_count_ + 5)) { + this->decode_data_(this->data_); + this->data_.clear(); + this->receiving_ = false; + } + } + + if (this->trigger_next_) { + this->trigger_next_ = false; + switch (this->next_request_) { + case 0: + this->request_data_(DALY_REQUEST_BATTERY_LEVEL); + this->next_request_ = 1; + break; + case 1: + this->request_data_(DALY_REQUEST_MIN_MAX_VOLTAGE); + this->next_request_ = 2; + break; + case 2: + this->request_data_(DALY_REQUEST_MIN_MAX_TEMPERATURE); + this->next_request_ = 3; + break; + case 3: + this->request_data_(DALY_REQUEST_MOS); + this->next_request_ = 4; + break; + case 4: + this->request_data_(DALY_REQUEST_STATUS); + this->next_request_ = 5; + break; + case 5: + this->request_data_(DALY_REQUEST_CELL_VOLTAGE); + this->next_request_ = 6; + break; + case 6: + this->request_data_(DALY_REQUEST_TEMPERATURE); + this->next_request_ = 7; + break; + case 7: + default: + break; + } } } @@ -49,21 +107,23 @@ float DalyBmsComponent::get_setup_priority() const { return setup_priority::DATA void DalyBmsComponent::request_data_(uint8_t data_id) { uint8_t request_message[DALY_FRAME_SIZE]; - request_message[0] = 0xA5; // Start Flag - request_message[1] = addr_; // Communication Module Address - request_message[2] = data_id; // Data ID - request_message[3] = 0x08; // Data Length (Fixed) - request_message[4] = 0x00; // Empty Data - request_message[5] = 0x00; // | - request_message[6] = 0x00; // | - request_message[7] = 0x00; // | - request_message[8] = 0x00; // | - request_message[9] = 0x00; // | - request_message[10] = 0x00; // | - request_message[11] = 0x00; // Empty Data + request_message[0] = 0xA5; // Start Flag + request_message[1] = this->addr_; // Communication Module Address + request_message[2] = data_id; // Data ID + request_message[3] = 0x08; // Data Length (Fixed) + request_message[4] = 0x00; // Empty Data + request_message[5] = 0x00; // | + request_message[6] = 0x00; // | + request_message[7] = 0x00; // | + request_message[8] = 0x00; // | + request_message[9] = 0x00; // | + request_message[10] = 0x00; // | + request_message[11] = 0x00; // Empty Data + request_message[12] = (uint8_t) (request_message[0] + request_message[1] + request_message[2] + request_message[3]); // Checksum (Lower byte of the other bytes sum) + ESP_LOGV(TAG, "Request datapacket Nr %x", data_id); this->write_array(request_message, sizeof(request_message)); this->flush(); } @@ -82,6 +142,7 @@ void DalyBmsComponent::decode_data_(std::vector data) { if (checksum == it[12]) { switch (it[2]) { +#ifdef USE_SENSOR case DALY_REQUEST_BATTERY_LEVEL: if (this->voltage_sensor_) { this->voltage_sensor_->publish_state((float) encode_uint16(it[4], it[5]) / 10); @@ -95,36 +156,37 @@ void DalyBmsComponent::decode_data_(std::vector data) { break; case DALY_REQUEST_MIN_MAX_VOLTAGE: - if (this->max_cell_voltage_) { - this->max_cell_voltage_->publish_state((float) encode_uint16(it[4], it[5]) / 1000); + if (this->max_cell_voltage_sensor_) { + this->max_cell_voltage_sensor_->publish_state((float) encode_uint16(it[4], it[5]) / 1000); } - if (this->max_cell_voltage_number_) { - this->max_cell_voltage_number_->publish_state(it[6]); + if (this->max_cell_voltage_number_sensor_) { + this->max_cell_voltage_number_sensor_->publish_state(it[6]); } - if (this->min_cell_voltage_) { - this->min_cell_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + if (this->min_cell_voltage_sensor_) { + this->min_cell_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); } - if (this->min_cell_voltage_number_) { - this->min_cell_voltage_number_->publish_state(it[9]); + if (this->min_cell_voltage_number_sensor_) { + this->min_cell_voltage_number_sensor_->publish_state(it[9]); } break; case DALY_REQUEST_MIN_MAX_TEMPERATURE: - if (this->max_temperature_) { - this->max_temperature_->publish_state(it[4] - DALY_TEMPERATURE_OFFSET); + if (this->max_temperature_sensor_) { + this->max_temperature_sensor_->publish_state(it[4] - DALY_TEMPERATURE_OFFSET); } - if (this->max_temperature_probe_number_) { - this->max_temperature_probe_number_->publish_state(it[5]); + if (this->max_temperature_probe_number_sensor_) { + this->max_temperature_probe_number_sensor_->publish_state(it[5]); } - if (this->min_temperature_) { - this->min_temperature_->publish_state(it[6] - DALY_TEMPERATURE_OFFSET); + if (this->min_temperature_sensor_) { + this->min_temperature_sensor_->publish_state(it[6] - DALY_TEMPERATURE_OFFSET); } - if (this->min_temperature_probe_number_) { - this->min_temperature_probe_number_->publish_state(it[7]); + if (this->min_temperature_probe_number_sensor_) { + this->min_temperature_probe_number_sensor_->publish_state(it[7]); } break; - +#endif case DALY_REQUEST_MOS: +#ifdef USE_TEXT_SENSOR if (this->status_text_sensor_ != nullptr) { switch (it[4]) { case 0: @@ -140,20 +202,27 @@ void DalyBmsComponent::decode_data_(std::vector data) { break; } } - if (this->charging_mos_enabled_) { - this->charging_mos_enabled_->publish_state(it[5]); +#endif +#ifdef USE_BINARY_SENSOR + if (this->charging_mos_enabled_binary_sensor_) { + this->charging_mos_enabled_binary_sensor_->publish_state(it[5]); } - if (this->discharging_mos_enabled_) { - this->discharging_mos_enabled_->publish_state(it[6]); + if (this->discharging_mos_enabled_binary_sensor_) { + this->discharging_mos_enabled_binary_sensor_->publish_state(it[6]); } - if (this->remaining_capacity_) { - this->remaining_capacity_->publish_state((float) encode_uint32(it[8], it[9], it[10], it[11]) / 1000); +#endif +#ifdef USE_SENSOR + if (this->remaining_capacity_sensor_) { + this->remaining_capacity_sensor_->publish_state((float) encode_uint32(it[8], it[9], it[10], it[11]) / + 1000); } +#endif break; +#ifdef USE_SENSOR case DALY_REQUEST_STATUS: - if (this->cells_number_) { - this->cells_number_->publish_state(it[4]); + if (this->cells_number_sensor_) { + this->cells_number_sensor_->publish_state(it[4]); } break; @@ -171,71 +240,73 @@ void DalyBmsComponent::decode_data_(std::vector data) { case DALY_REQUEST_CELL_VOLTAGE: switch (it[4]) { case 1: - if (this->cell_1_voltage_) { - this->cell_1_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + if (this->cell_1_voltage_sensor_) { + this->cell_1_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); } - if (this->cell_2_voltage_) { - this->cell_2_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + if (this->cell_2_voltage_sensor_) { + this->cell_2_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); } - if (this->cell_3_voltage_) { - this->cell_3_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + if (this->cell_3_voltage_sensor_) { + this->cell_3_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); } break; case 2: - if (this->cell_4_voltage_) { - this->cell_4_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + if (this->cell_4_voltage_sensor_) { + this->cell_4_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); } - if (this->cell_5_voltage_) { - this->cell_5_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + if (this->cell_5_voltage_sensor_) { + this->cell_5_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); } - if (this->cell_6_voltage_) { - this->cell_6_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + if (this->cell_6_voltage_sensor_) { + this->cell_6_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); } break; case 3: - if (this->cell_7_voltage_) { - this->cell_7_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + if (this->cell_7_voltage_sensor_) { + this->cell_7_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); } - if (this->cell_8_voltage_) { - this->cell_8_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + if (this->cell_8_voltage_sensor_) { + this->cell_8_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); } - if (this->cell_9_voltage_) { - this->cell_9_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + if (this->cell_9_voltage_sensor_) { + this->cell_9_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); } break; case 4: - if (this->cell_10_voltage_) { - this->cell_10_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + if (this->cell_10_voltage_sensor_) { + this->cell_10_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); } - if (this->cell_11_voltage_) { - this->cell_11_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + if (this->cell_11_voltage_sensor_) { + this->cell_11_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); } - if (this->cell_12_voltage_) { - this->cell_12_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + if (this->cell_12_voltage_sensor_) { + this->cell_12_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); } break; case 5: - if (this->cell_13_voltage_) { - this->cell_13_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + if (this->cell_13_voltage_sensor_) { + this->cell_13_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); } - if (this->cell_14_voltage_) { - this->cell_14_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + if (this->cell_14_voltage_sensor_) { + this->cell_14_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); } - if (this->cell_15_voltage_) { - this->cell_15_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + if (this->cell_15_voltage_sensor_) { + this->cell_15_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); } break; case 6: - if (this->cell_16_voltage_) { - this->cell_16_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + if (this->cell_16_voltage_sensor_) { + this->cell_16_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); } break; } break; - +#endif default: break; } + } else { + ESP_LOGW(TAG, "Checksum-Error on Packet %x", it[4]); } std::advance(it, DALY_FRAME_SIZE); } else { diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index d4fe84fe46..52ea30ecde 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -1,9 +1,16 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" +#ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_TEXT_SENSOR #include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" +#endif #include "esphome/components/uart/uart.h" #include @@ -15,60 +22,53 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { public: DalyBmsComponent() = default; - // SENSORS - void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } - void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } - void set_battery_level_sensor(sensor::Sensor *battery_level_sensor) { battery_level_sensor_ = battery_level_sensor; } - void set_max_cell_voltage_sensor(sensor::Sensor *max_cell_voltage) { max_cell_voltage_ = max_cell_voltage; } - void set_max_cell_voltage_number_sensor(sensor::Sensor *max_cell_voltage_number) { - max_cell_voltage_number_ = max_cell_voltage_number; - } - void set_min_cell_voltage_sensor(sensor::Sensor *min_cell_voltage) { min_cell_voltage_ = min_cell_voltage; } - void set_min_cell_voltage_number_sensor(sensor::Sensor *min_cell_voltage_number) { - min_cell_voltage_number_ = min_cell_voltage_number; - } - void set_max_temperature_sensor(sensor::Sensor *max_temperature) { max_temperature_ = max_temperature; } - void set_max_temperature_probe_number_sensor(sensor::Sensor *max_temperature_probe_number) { - max_temperature_probe_number_ = max_temperature_probe_number; - } - void set_min_temperature_sensor(sensor::Sensor *min_temperature) { min_temperature_ = min_temperature; } - void set_min_temperature_probe_number_sensor(sensor::Sensor *min_temperature_probe_number) { - min_temperature_probe_number_ = min_temperature_probe_number; - } - void set_remaining_capacity_sensor(sensor::Sensor *remaining_capacity) { remaining_capacity_ = remaining_capacity; } - void set_cells_number_sensor(sensor::Sensor *cells_number) { cells_number_ = cells_number; } - void set_temperature_1_sensor(sensor::Sensor *temperature_1_sensor) { temperature_1_sensor_ = temperature_1_sensor; } - void set_temperature_2_sensor(sensor::Sensor *temperature_2_sensor) { temperature_2_sensor_ = temperature_2_sensor; } - void set_cell_1_voltage_sensor(sensor::Sensor *cell_1_voltage) { cell_1_voltage_ = cell_1_voltage; } - void set_cell_2_voltage_sensor(sensor::Sensor *cell_2_voltage) { cell_2_voltage_ = cell_2_voltage; } - void set_cell_3_voltage_sensor(sensor::Sensor *cell_3_voltage) { cell_3_voltage_ = cell_3_voltage; } - void set_cell_4_voltage_sensor(sensor::Sensor *cell_4_voltage) { cell_4_voltage_ = cell_4_voltage; } - void set_cell_5_voltage_sensor(sensor::Sensor *cell_5_voltage) { cell_5_voltage_ = cell_5_voltage; } - void set_cell_6_voltage_sensor(sensor::Sensor *cell_6_voltage) { cell_6_voltage_ = cell_6_voltage; } - void set_cell_7_voltage_sensor(sensor::Sensor *cell_7_voltage) { cell_7_voltage_ = cell_7_voltage; } - void set_cell_8_voltage_sensor(sensor::Sensor *cell_8_voltage) { cell_8_voltage_ = cell_8_voltage; } - void set_cell_9_voltage_sensor(sensor::Sensor *cell_9_voltage) { cell_9_voltage_ = cell_9_voltage; } - void set_cell_10_voltage_sensor(sensor::Sensor *cell_10_voltage) { cell_10_voltage_ = cell_10_voltage; } - void set_cell_11_voltage_sensor(sensor::Sensor *cell_11_voltage) { cell_11_voltage_ = cell_11_voltage; } - void set_cell_12_voltage_sensor(sensor::Sensor *cell_12_voltage) { cell_12_voltage_ = cell_12_voltage; } - void set_cell_13_voltage_sensor(sensor::Sensor *cell_13_voltage) { cell_13_voltage_ = cell_13_voltage; } - void set_cell_14_voltage_sensor(sensor::Sensor *cell_14_voltage) { cell_14_voltage_ = cell_14_voltage; } - void set_cell_15_voltage_sensor(sensor::Sensor *cell_15_voltage) { cell_15_voltage_ = cell_15_voltage; } - void set_cell_16_voltage_sensor(sensor::Sensor *cell_16_voltage) { cell_16_voltage_ = cell_16_voltage; } +#ifdef USE_SENSOR + SUB_SENSOR(voltage) + SUB_SENSOR(current) + SUB_SENSOR(battery_level) + SUB_SENSOR(max_cell_voltage) + SUB_SENSOR(max_cell_voltage_number) + SUB_SENSOR(min_cell_voltage) + SUB_SENSOR(min_cell_voltage_number) + SUB_SENSOR(max_temperature) + SUB_SENSOR(max_temperature_probe_number) + SUB_SENSOR(min_temperature) + SUB_SENSOR(min_temperature_probe_number) + SUB_SENSOR(remaining_capacity) + SUB_SENSOR(cells_number) + SUB_SENSOR(temperature_1) + SUB_SENSOR(temperature_2) + SUB_SENSOR(cell_1_voltage) + SUB_SENSOR(cell_2_voltage) + SUB_SENSOR(cell_3_voltage) + SUB_SENSOR(cell_4_voltage) + SUB_SENSOR(cell_5_voltage) + SUB_SENSOR(cell_6_voltage) + SUB_SENSOR(cell_7_voltage) + SUB_SENSOR(cell_8_voltage) + SUB_SENSOR(cell_9_voltage) + SUB_SENSOR(cell_10_voltage) + SUB_SENSOR(cell_11_voltage) + SUB_SENSOR(cell_12_voltage) + SUB_SENSOR(cell_13_voltage) + SUB_SENSOR(cell_14_voltage) + SUB_SENSOR(cell_15_voltage) + SUB_SENSOR(cell_16_voltage) +#endif - // TEXT_SENSORS - void set_status_text_sensor(text_sensor::TextSensor *status_text_sensor) { status_text_sensor_ = status_text_sensor; } - // BINARY_SENSORS - void set_charging_mos_enabled_binary_sensor(binary_sensor::BinarySensor *charging_mos_enabled) { - charging_mos_enabled_ = charging_mos_enabled; - } - void set_discharging_mos_enabled_binary_sensor(binary_sensor::BinarySensor *discharging_mos_enabled) { - discharging_mos_enabled_ = discharging_mos_enabled; - } +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(status) +#endif + +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(charging_mos_enabled) + SUB_BINARY_SENSOR(discharging_mos_enabled) +#endif void setup() override; void dump_config() override; void update() override; + void loop() override; float get_setup_priority() const override; void set_address(uint8_t address) { this->addr_ = address; } @@ -79,42 +79,12 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { uint8_t addr_; - sensor::Sensor *voltage_sensor_{nullptr}; - sensor::Sensor *current_sensor_{nullptr}; - sensor::Sensor *battery_level_sensor_{nullptr}; - sensor::Sensor *max_cell_voltage_{nullptr}; - sensor::Sensor *max_cell_voltage_number_{nullptr}; - sensor::Sensor *min_cell_voltage_{nullptr}; - sensor::Sensor *min_cell_voltage_number_{nullptr}; - sensor::Sensor *max_temperature_{nullptr}; - sensor::Sensor *max_temperature_probe_number_{nullptr}; - sensor::Sensor *min_temperature_{nullptr}; - sensor::Sensor *min_temperature_probe_number_{nullptr}; - sensor::Sensor *remaining_capacity_{nullptr}; - sensor::Sensor *cells_number_{nullptr}; - sensor::Sensor *temperature_1_sensor_{nullptr}; - sensor::Sensor *temperature_2_sensor_{nullptr}; - sensor::Sensor *cell_1_voltage_{nullptr}; - sensor::Sensor *cell_2_voltage_{nullptr}; - sensor::Sensor *cell_3_voltage_{nullptr}; - sensor::Sensor *cell_4_voltage_{nullptr}; - sensor::Sensor *cell_5_voltage_{nullptr}; - sensor::Sensor *cell_6_voltage_{nullptr}; - sensor::Sensor *cell_7_voltage_{nullptr}; - sensor::Sensor *cell_8_voltage_{nullptr}; - sensor::Sensor *cell_9_voltage_{nullptr}; - sensor::Sensor *cell_10_voltage_{nullptr}; - sensor::Sensor *cell_11_voltage_{nullptr}; - sensor::Sensor *cell_12_voltage_{nullptr}; - sensor::Sensor *cell_13_voltage_{nullptr}; - sensor::Sensor *cell_14_voltage_{nullptr}; - sensor::Sensor *cell_15_voltage_{nullptr}; - sensor::Sensor *cell_16_voltage_{nullptr}; - - text_sensor::TextSensor *status_text_sensor_{nullptr}; - - binary_sensor::BinarySensor *charging_mos_enabled_{nullptr}; - binary_sensor::BinarySensor *discharging_mos_enabled_{nullptr}; + std::vector data_; + bool receiving_{false}; + uint8_t data_count_; + uint32_t last_transmission_{0}; + bool trigger_next_; + uint8_t next_request_; }; } // namespace daly_bms diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index 2274a2153a..c447fbd8a2 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -218,9 +218,8 @@ CONFIG_SCHEMA = cv.All( async def setup_conf(config, key, hub): - if key in config: - conf = config[key] - sens = await sensor.new_sensor(conf) + if sensor_config := config.get(key): + sens = await sensor.new_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/daly_bms/text_sensor.py b/esphome/components/daly_bms/text_sensor.py index 9f23e5f373..fcd5ee531b 100644 --- a/esphome/components/daly_bms/text_sensor.py +++ b/esphome/components/daly_bms/text_sensor.py @@ -23,9 +23,8 @@ CONFIG_SCHEMA = cv.All( async def setup_conf(config, key, hub): - if key in config: - conf = config[key] - sens = await text_sensor.new_text_sensor(conf) + if sensor_config := config.get(key): + sens = await text_sensor.new_text_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index e0994be6a0..b1b22b816b 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -2,8 +2,10 @@ import base64 import secrets from pathlib import Path from typing import Optional +import re import requests +from ruamel.yaml import YAML import esphome.codegen as cg import esphome.config_validation as cv @@ -11,7 +13,6 @@ import esphome.final_validate as fv from esphome import git from esphome.components.packages import validate_source_shorthand from esphome.const import CONF_REF, CONF_WIFI, CONF_ESPHOME, CONF_PROJECT -from esphome.wizard import wizard_file from esphome.yaml_util import dump dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") @@ -94,75 +95,74 @@ def import_config( if p.exists(): raise FileExistsError - if project_name == "esphome.web": - if "esp32c3" in import_url: - board = "esp32-c3-devkitm-1" - platform = "ESP32" - elif "esp32s2" in import_url: - board = "esp32-s2-saola-1" - platform = "ESP32" - elif "esp32s3" in import_url: - board = "esp32-s3-devkitc-1" - platform = "ESP32" - elif "esp32" in import_url: - board = "esp32dev" - platform = "ESP32" - elif "esp8266" in import_url: - board = "esp01_1m" - platform = "ESP8266" - elif "pico-w" in import_url: - board = "pico-w" - platform = "RP2040" + git_file = git.GitFile.from_shorthand(import_url) - kwargs = { - "name": name, - "friendly_name": friendly_name, - "platform": platform, - "board": board, - "ssid": "!secret wifi_ssid", - "psk": "!secret wifi_password", + if git_file.query and "full_config" in git_file.query: + url = git_file.raw_url + try: + req = requests.get(url, timeout=30) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise ValueError(f"Error while fetching {url}: {e}") from e + + contents = req.text + yaml = YAML() + loaded_yaml = yaml.load(contents) + if ( + "name_add_mac_suffix" in loaded_yaml["esphome"] + and loaded_yaml["esphome"]["name_add_mac_suffix"] + ): + loaded_yaml["esphome"]["name_add_mac_suffix"] = False + name_val = loaded_yaml["esphome"]["name"] + sub_pattern = re.compile(r"\$\{?([a-zA-Z-_]+)\}?") + if match := sub_pattern.match(name_val): + name_sub = match.group(1) + if name_sub in loaded_yaml["substitutions"]: + loaded_yaml["substitutions"][name_sub] = name + else: + raise ValueError( + f"Name substitution {name_sub} not found in substitutions" + ) + else: + loaded_yaml["esphome"]["name"] = name + if friendly_name is not None: + friendly_name_val = loaded_yaml["esphome"]["friendly_name"] + if match := sub_pattern.match(friendly_name_val): + friendly_name_sub = match.group(1) + if friendly_name_sub in loaded_yaml["substitutions"]: + loaded_yaml["substitutions"][friendly_name_sub] = friendly_name + else: + raise ValueError( + f"Friendly name substitution {friendly_name_sub} not found in substitutions" + ) + else: + loaded_yaml["esphome"]["friendly_name"] = friendly_name + + with p.open("w", encoding="utf8") as f: + yaml.dump(loaded_yaml, f) + else: + with p.open("w", encoding="utf8") as f: + f.write(contents) + + else: + substitutions = {"name": name} + esphome_core = {"name": "${name}", "name_add_mac_suffix": False} + if friendly_name: + substitutions["friendly_name"] = friendly_name + esphome_core["friendly_name"] = "${friendly_name}" + config = { + "substitutions": substitutions, + "packages": {project_name: import_url}, + "esphome": esphome_core, } if encryption: noise_psk = secrets.token_bytes(32) key = base64.b64encode(noise_psk).decode() - kwargs["api_encryption_key"] = key + config["api"] = {"encryption": {"key": key}} - p.write_text( - wizard_file(**kwargs), - encoding="utf8", - ) - else: - git_file = git.GitFile.from_shorthand(import_url) + output = dump(config) - if git_file.query and "full_config" in git_file.query: - url = git_file.raw_url - try: - req = requests.get(url, timeout=30) - req.raise_for_status() - except requests.exceptions.RequestException as e: - raise ValueError(f"Error while fetching {url}: {e}") from e + if network == CONF_WIFI: + output += WIFI_CONFIG - p.write_text(req.text, encoding="utf8") - - else: - substitutions = {"name": name} - esphome_core = {"name": "${name}", "name_add_mac_suffix": False} - if friendly_name: - substitutions["friendly_name"] = friendly_name - esphome_core["friendly_name"] = "${friendly_name}" - config = { - "substitutions": substitutions, - "packages": {project_name: import_url}, - "esphome": esphome_core, - } - if encryption: - noise_psk = secrets.token_bytes(32) - key = base64.b64encode(noise_psk).decode() - config["api"] = {"encryption": {"key": key}} - - output = dump(config) - - if network == CONF_WIFI: - output += WIFI_CONFIG - - p.write_text(output, encoding="utf8") + p.write_text(output, encoding="utf8") diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py new file mode 100644 index 0000000000..630bf6962c --- /dev/null +++ b/esphome/components/datetime/__init__.py @@ -0,0 +1,271 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import mqtt, time, web_server +import esphome.config_validation as cv +from esphome.const import ( + CONF_DATE, + CONF_DATETIME, + CONF_DAY, + CONF_HOUR, + CONF_ID, + CONF_MINUTE, + CONF_MONTH, + CONF_MQTT_ID, + CONF_ON_TIME, + CONF_ON_VALUE, + CONF_SECOND, + CONF_TIME, + CONF_TIME_ID, + CONF_TRIGGER_ID, + CONF_TYPE, + CONF_WEB_SERVER, + CONF_YEAR, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@rfdarter", "@jesserockz"] + +IS_PLATFORM_COMPONENT = True + +datetime_ns = cg.esphome_ns.namespace("datetime") +DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase) +DateEntity = datetime_ns.class_("DateEntity", DateTimeBase) +TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase) +DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase) + +# Actions +DateSetAction = datetime_ns.class_("DateSetAction", automation.Action) +TimeSetAction = datetime_ns.class_("TimeSetAction", automation.Action) +DateTimeSetAction = datetime_ns.class_("DateTimeSetAction", automation.Action) + +DateTimeStateTrigger = datetime_ns.class_( + "DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime) +) + +OnTimeTrigger = datetime_ns.class_( + "OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity) +) +OnDateTimeTrigger = datetime_ns.class_( + "OnDateTimeTrigger", + automation.Trigger, + cg.Component, + cg.Parented.template(DateTimeEntity), +) + +DATETIME_MODES = [ + "DATE", + "TIME", + "DATETIME", +] + + +def _validate_time_present(config): + config = config.copy() + if CONF_ON_TIME in config and CONF_TIME_ID not in config: + time_id = cv.use_id(time.RealTimeClock)(None) + config[CONF_TIME_ID] = time_id + return config + + +_DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger), + } + ), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + } + ) + .extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) +).add_extra(_validate_time_present) + + +def date_schema(class_: MockObjClass) -> cv.Schema: + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(class_), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent), + cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True), + } + ) + return _DATETIME_SCHEMA.extend(schema) + + +def time_schema(class_: MockObjClass) -> cv.Schema: + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(class_), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent), + cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True), + cv.Optional(CONF_ON_TIME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTimeTrigger), + } + ), + } + ) + return _DATETIME_SCHEMA.extend(schema) + + +def datetime_schema(class_: MockObjClass) -> cv.Schema: + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(class_), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( + mqtt.MQTTDateTimeComponent + ), + cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of( + "DATETIME", upper=True + ), + cv.Optional(CONF_ON_TIME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnDateTimeTrigger), + } + ), + } + ) + return _DATETIME_SCHEMA.extend(schema) + + +async def setup_datetime_core_(var, config): + await setup_entity(var, config) + + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) + await mqtt.register_mqtt_component(mqtt_, config) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf) + + if CONF_TIME_ID in config: + rtc = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_rtc(rtc)) + + for conf in config.get(CONF_ON_TIME, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + await automation.build_automation(trigger, [], conf) + await cg.register_component(trigger, conf) + await cg.register_parented(trigger, var) + + +async def register_datetime(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(getattr(cg.App, f"register_{config[CONF_TYPE].lower()}")(var)) + await setup_datetime_core_(var, config) + cg.add_define(f"USE_DATETIME_{config[CONF_TYPE]}") + + +async def new_datetime(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_datetime(var, config) + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_DATETIME") + cg.add_global(datetime_ns.using) + + +@automation.register_action( + "datetime.date.set", + DateSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(DateEntity), + cv.Required(CONF_DATE): cv.Any( + cv.returning_lambda, cv.date_time(date=True, time=False) + ), + } + ), +) +async def datetime_date_set_to_code(config, action_id, template_arg, args): + action_var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(action_var, config[CONF_ID]) + + date_config = config[CONF_DATE] + if cg.is_template(date_config): + template_ = await cg.templatable(date_config, args, cg.ESPTime) + cg.add(action_var.set_date(template_)) + else: + date_struct = cg.StructInitializer( + cg.ESPTime, + ("day_of_month", date_config[CONF_DAY]), + ("month", date_config[CONF_MONTH]), + ("year", date_config[CONF_YEAR]), + ) + cg.add(action_var.set_date(date_struct)) + return action_var + + +@automation.register_action( + "datetime.time.set", + TimeSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(TimeEntity), + cv.Required(CONF_TIME): cv.Any( + cv.returning_lambda, cv.date_time(date=False, time=True) + ), + } + ), +) +async def datetime_time_set_to_code(config, action_id, template_arg, args): + action_var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(action_var, config[CONF_ID]) + + time_config = config[CONF_TIME] + if cg.is_template(time_config): + template_ = await cg.templatable(time_config, args, cg.ESPTime) + cg.add(action_var.set_time(template_)) + else: + time_struct = cg.StructInitializer( + cg.ESPTime, + ("second", time_config[CONF_SECOND]), + ("minute", time_config[CONF_MINUTE]), + ("hour", time_config[CONF_HOUR]), + ) + cg.add(action_var.set_time(time_struct)) + return action_var + + +@automation.register_action( + "datetime.datetime.set", + DateTimeSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(DateTimeEntity), + cv.Required(CONF_DATETIME): cv.Any( + cv.returning_lambda, cv.date_time(date=True, time=True) + ), + }, + ), +) +async def datetime_datetime_set_to_code(config, action_id, template_arg, args): + action_var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(action_var, config[CONF_ID]) + + datetime_config = config[CONF_DATETIME] + if cg.is_template(datetime_config): + template_ = await cg.templatable(datetime_config, args, cg.ESPTime) + cg.add(action_var.set_datetime(template_)) + else: + datetime_struct = cg.StructInitializer( + cg.ESPTime, + ("second", datetime_config[CONF_SECOND]), + ("minute", datetime_config[CONF_MINUTE]), + ("hour", datetime_config[CONF_HOUR]), + ("day_of_month", datetime_config[CONF_DAY]), + ("month", datetime_config[CONF_MONTH]), + ("year", datetime_config[CONF_YEAR]), + ) + cg.add(action_var.set_datetime(datetime_struct)) + return action_var diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp new file mode 100644 index 0000000000..b5bcef43af --- /dev/null +++ b/esphome/components/datetime/date_entity.cpp @@ -0,0 +1,131 @@ +#include "date_entity.h" + +#ifdef USE_DATETIME_DATE + +#include "esphome/core/log.h" + +namespace esphome { +namespace datetime { + +static const char *const TAG = "datetime.date_entity"; + +void DateEntity::publish_state() { + if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { + this->has_state_ = false; + return; + } + if (this->year_ < 1970 || this->year_ > 3000) { + this->has_state_ = false; + ESP_LOGE(TAG, "Year must be between 1970 and 3000"); + return; + } + if (this->month_ < 1 || this->month_ > 12) { + this->has_state_ = false; + ESP_LOGE(TAG, "Month must be between 1 and 12"); + return; + } + if (this->day_ > days_in_month(this->month_, this->year_)) { + this->has_state_ = false; + ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); + return; + } + this->has_state_ = true; + ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); + this->state_callback_.call(); +} + +DateCall DateEntity::make_call() { return DateCall(this); } + +void DateCall::validate_() { + if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) { + ESP_LOGE(TAG, "Year must be between 1970 and 3000"); + this->year_.reset(); + this->month_.reset(); + this->day_.reset(); + } + if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) { + ESP_LOGE(TAG, "Month must be between 1 and 12"); + this->month_.reset(); + this->day_.reset(); + } + if (this->day_.has_value()) { + uint16_t year = 0; + uint8_t month = 0; + if (this->month_.has_value()) { + month = *this->month_; + } else { + if (this->parent_->month != 0) { + month = this->parent_->month; + } else { + ESP_LOGE(TAG, "Month must be set to validate day"); + this->day_.reset(); + } + } + if (this->year_.has_value()) { + year = *this->year_; + } else { + if (this->parent_->year != 0) { + year = this->parent_->year; + } else { + ESP_LOGE(TAG, "Year must be set to validate day"); + this->day_.reset(); + } + } + if (this->day_.has_value() && *this->day_ > days_in_month(month, year)) { + ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(month, year), month); + this->day_.reset(); + } + } +} + +void DateCall::perform() { + this->validate_(); + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + + if (this->year_.has_value()) { + ESP_LOGD(TAG, " Year: %d", *this->year_); + } + if (this->month_.has_value()) { + ESP_LOGD(TAG, " Month: %d", *this->month_); + } + if (this->day_.has_value()) { + ESP_LOGD(TAG, " Day: %d", *this->day_); + } + this->parent_->control(*this); +} + +DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) { + this->year_ = year; + this->month_ = month; + this->day_ = day; + return *this; +}; + +DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); }; + +DateCall &DateCall::set_date(const std::string &date) { + ESPTime val{}; + if (!ESPTime::strptime(date, val)) { + ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object"); + return *this; + } + return this->set_date(val); +} + +DateCall DateEntityRestoreState::to_call(DateEntity *date) { + DateCall call = date->make_call(); + call.set_date(this->year, this->month, this->day); + return call; +} + +void DateEntityRestoreState::apply(DateEntity *date) { + date->year_ = this->year; + date->month_ = this->month; + date->day_ = this->day; + date->publish_state(); +} + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_DATE diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h new file mode 100644 index 0000000000..ce43c5639d --- /dev/null +++ b/esphome/components/datetime/date_entity.h @@ -0,0 +1,117 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_DATE + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "esphome/core/time.h" + +#include "datetime_base.h" + +namespace esphome { +namespace datetime { + +#define LOG_DATETIME_DATE(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +class DateCall; +class DateEntity; + +struct DateEntityRestoreState { + uint16_t year; + uint8_t month; + uint8_t day; + + DateCall to_call(DateEntity *date); + void apply(DateEntity *date); +} __attribute__((packed)); + +class DateEntity : public DateTimeBase { + protected: + uint16_t year_; + uint8_t month_; + uint8_t day_; + + public: + void publish_state(); + DateCall make_call(); + + ESPTime state_as_esptime() const override { + ESPTime obj; + obj.year = this->year_; + obj.month = this->month_; + obj.day_of_month = this->day_; + return obj; + } + + const uint16_t &year = year_; + const uint8_t &month = month_; + const uint8_t &day = day_; + + protected: + friend class DateCall; + friend struct DateEntityRestoreState; + + virtual void control(const DateCall &call) = 0; +}; + +class DateCall { + public: + explicit DateCall(DateEntity *parent) : parent_(parent) {} + void perform(); + DateCall &set_date(uint16_t year, uint8_t month, uint8_t day); + DateCall &set_date(ESPTime time); + DateCall &set_date(const std::string &date); + + DateCall &set_year(uint16_t year) { + this->year_ = year; + return *this; + } + DateCall &set_month(uint8_t month) { + this->month_ = month; + return *this; + } + DateCall &set_day(uint8_t day) { + this->day_ = day; + return *this; + } + + optional get_year() const { return this->year_; } + optional get_month() const { return this->month_; } + optional get_day() const { return this->day_; } + + protected: + void validate_(); + + DateEntity *parent_; + + optional year_; + optional month_; + optional day_; +}; + +template class DateSetAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(ESPTime, date) + + void play(Ts... x) override { + auto call = this->parent_->make_call(); + + if (this->date_.has_value()) { + call.set_date(this->date_.value(x...)); + } + call.perform(); + } +}; + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_DATE diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h new file mode 100644 index 0000000000..dea34e6110 --- /dev/null +++ b/esphome/components/datetime/datetime_base.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/time.h" +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace datetime { + +class DateTimeBase : public EntityBase { + public: + /// Return whether this Datetime has gotten a full state yet. + bool has_state() const { return this->has_state_; } + + virtual ESPTime state_as_esptime() const = 0; + + void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + +#ifdef USE_TIME + void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; } + time::RealTimeClock *get_rtc() const { return this->rtc_; } +#endif + + protected: + CallbackManager state_callback_; + +#ifdef USE_TIME + time::RealTimeClock *rtc_; +#endif + + bool has_state_{false}; +}; + +#ifdef USE_TIME +class DateTimeStateTrigger : public Trigger { + public: + explicit DateTimeStateTrigger(DateTimeBase *parent) { + parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); }); + } +}; +#endif + +} // namespace datetime +} // namespace esphome diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp new file mode 100644 index 0000000000..f215b7acb5 --- /dev/null +++ b/esphome/components/datetime/datetime_entity.cpp @@ -0,0 +1,254 @@ +#include "datetime_entity.h" + +#ifdef USE_DATETIME_DATETIME + +#include "esphome/core/log.h" + +namespace esphome { +namespace datetime { + +static const char *const TAG = "datetime.datetime_entity"; + +void DateTimeEntity::publish_state() { + if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { + this->has_state_ = false; + return; + } + if (this->year_ < 1970 || this->year_ > 3000) { + this->has_state_ = false; + ESP_LOGE(TAG, "Year must be between 1970 and 3000"); + return; + } + if (this->month_ < 1 || this->month_ > 12) { + this->has_state_ = false; + ESP_LOGE(TAG, "Month must be between 1 and 12"); + return; + } + if (this->day_ > days_in_month(this->month_, this->year_)) { + this->has_state_ = false; + ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); + return; + } + if (this->hour_ > 23) { + this->has_state_ = false; + ESP_LOGE(TAG, "Hour must be between 0 and 23"); + return; + } + if (this->minute_ > 59) { + this->has_state_ = false; + ESP_LOGE(TAG, "Minute must be between 0 and 59"); + return; + } + if (this->second_ > 59) { + this->has_state_ = false; + ESP_LOGE(TAG, "Second must be between 0 and 59"); + return; + } + this->has_state_ = true; + ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, + this->month_, this->day_, this->hour_, this->minute_, this->second_); + this->state_callback_.call(); +} + +DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } + +ESPTime DateTimeEntity::state_as_esptime() const { + ESPTime obj; + obj.year = this->year_; + obj.month = this->month_; + obj.day_of_month = this->day_; + obj.hour = this->hour_; + obj.minute = this->minute_; + obj.second = this->second_; + obj.day_of_week = 1; // Required to be valid for recalc_timestamp_local but not used. + obj.day_of_year = 1; // Required to be valid for recalc_timestamp_local but not used. + obj.recalc_timestamp_local(false); + return obj; +} + +void DateTimeCall::validate_() { + if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) { + ESP_LOGE(TAG, "Year must be between 1970 and 3000"); + this->year_.reset(); + this->month_.reset(); + this->day_.reset(); + } + if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) { + ESP_LOGE(TAG, "Month must be between 1 and 12"); + this->month_.reset(); + this->day_.reset(); + } + if (this->day_.has_value()) { + uint16_t year = 0; + uint8_t month = 0; + if (this->month_.has_value()) { + month = *this->month_; + } else { + if (this->parent_->month != 0) { + month = this->parent_->month; + } else { + ESP_LOGE(TAG, "Month must be set to validate day"); + this->day_.reset(); + } + } + if (this->year_.has_value()) { + year = *this->year_; + } else { + if (this->parent_->year != 0) { + year = this->parent_->year; + } else { + ESP_LOGE(TAG, "Year must be set to validate day"); + this->day_.reset(); + } + } + if (this->day_.has_value() && *this->day_ > days_in_month(month, year)) { + ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(month, year), month); + this->day_.reset(); + } + } + + if (this->hour_.has_value() && this->hour_ > 23) { + ESP_LOGE(TAG, "Hour must be between 0 and 23"); + this->hour_.reset(); + } + if (this->minute_.has_value() && this->minute_ > 59) { + ESP_LOGE(TAG, "Minute must be between 0 and 59"); + this->minute_.reset(); + } + if (this->second_.has_value() && this->second_ > 59) { + ESP_LOGE(TAG, "Second must be between 0 and 59"); + this->second_.reset(); + } +} + +void DateTimeCall::perform() { + this->validate_(); + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + + if (this->year_.has_value()) { + ESP_LOGD(TAG, " Year: %d", *this->year_); + } + if (this->month_.has_value()) { + ESP_LOGD(TAG, " Month: %d", *this->month_); + } + if (this->day_.has_value()) { + ESP_LOGD(TAG, " Day: %d", *this->day_); + } + if (this->hour_.has_value()) { + ESP_LOGD(TAG, " Hour: %d", *this->hour_); + } + if (this->minute_.has_value()) { + ESP_LOGD(TAG, " Minute: %d", *this->minute_); + } + if (this->second_.has_value()) { + ESP_LOGD(TAG, " Second: %d", *this->second_); + } + this->parent_->control(*this); +} + +DateTimeCall &DateTimeCall::set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, + uint8_t second) { + this->year_ = year; + this->month_ = month; + this->day_ = day; + this->hour_ = hour; + this->minute_ = minute; + this->second_ = second; + return *this; +}; + +DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) { + return this->set_datetime(datetime.year, datetime.month, datetime.day_of_month, datetime.hour, datetime.minute, + datetime.second); +}; + +DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) { + ESPTime val{}; + if (!ESPTime::strptime(datetime, val)) { + ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object"); + return *this; + } + return this->set_datetime(val); +} + +DateTimeCall &DateTimeCall::set_datetime(time_t epoch_seconds) { + ESPTime val = ESPTime::from_epoch_local(epoch_seconds); + return this->set_datetime(val); +} + +DateTimeCall DateTimeEntityRestoreState::to_call(DateTimeEntity *datetime) { + DateTimeCall call = datetime->make_call(); + call.set_datetime(this->year, this->month, this->day, this->hour, this->minute, this->second); + return call; +} + +void DateTimeEntityRestoreState::apply(DateTimeEntity *time) { + time->year_ = this->year; + time->month_ = this->month; + time->day_ = this->day; + time->hour_ = this->hour; + time->minute_ = this->minute; + time->second_ = this->second; + time->publish_state(); +} + +#ifdef USE_TIME +static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider + // there has been a drastic time synchronization + +void OnDateTimeTrigger::loop() { + if (!this->parent_->has_state()) { + return; + } + ESPTime time = this->parent_->rtc_->now(); + if (!time.is_valid()) { + return; + } + if (this->last_check_.has_value()) { + if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { + // We went back in time (a lot), probably caused by time synchronization + ESP_LOGW(TAG, "Time has jumped back!"); + } else if (*this->last_check_ >= time) { + // already handled this one + return; + } else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) { + // We went ahead in time (a lot), probably caused by time synchronization + ESP_LOGW(TAG, "Time has jumped ahead!"); + this->last_check_ = time; + return; + } + + while (true) { + this->last_check_->increment_second(); + if (*this->last_check_ >= time) + break; + + if (this->matches_(*this->last_check_)) { + this->trigger(); + break; + } + } + } + + this->last_check_ = time; + if (!time.fields_in_range()) { + ESP_LOGW(TAG, "Time is out of range!"); + ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u Day=%02u Month=%02u Year=%04u", time.second, time.minute, + time.hour, time.day_of_month, time.month, time.year); + } + + if (this->matches_(time)) + this->trigger(); +} + +bool OnDateTimeTrigger::matches_(const ESPTime &time) const { + return time.is_valid() && time.year == this->parent_->year && time.month == this->parent_->month && + time.day_of_month == this->parent_->day && time.hour == this->parent_->hour && + time.minute == this->parent_->minute && time.second == this->parent_->second; +} +#endif + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_TIME diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h new file mode 100644 index 0000000000..27db84cf7e --- /dev/null +++ b/esphome/components/datetime/datetime_entity.h @@ -0,0 +1,152 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_DATETIME + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "esphome/core/time.h" + +#include "datetime_base.h" + +namespace esphome { +namespace datetime { + +#define LOG_DATETIME_DATETIME(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +class DateTimeCall; +class DateTimeEntity; + +struct DateTimeEntityRestoreState { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + + DateTimeCall to_call(DateTimeEntity *datetime); + void apply(DateTimeEntity *datetime); +} __attribute__((packed)); + +class DateTimeEntity : public DateTimeBase { + protected: + uint16_t year_; + uint8_t month_; + uint8_t day_; + uint8_t hour_; + uint8_t minute_; + uint8_t second_; + + public: + void publish_state(); + DateTimeCall make_call(); + + ESPTime state_as_esptime() const override; + + const uint16_t &year = year_; + const uint8_t &month = month_; + const uint8_t &day = day_; + const uint8_t &hour = hour_; + const uint8_t &minute = minute_; + const uint8_t &second = second_; + + protected: + friend class DateTimeCall; + friend struct DateTimeEntityRestoreState; + friend class OnDateTimeTrigger; + + virtual void control(const DateTimeCall &call) = 0; +}; + +class DateTimeCall { + public: + explicit DateTimeCall(DateTimeEntity *parent) : parent_(parent) {} + void perform(); + DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second); + DateTimeCall &set_datetime(ESPTime datetime); + DateTimeCall &set_datetime(const std::string &datetime); + DateTimeCall &set_datetime(time_t epoch_seconds); + + DateTimeCall &set_year(uint16_t year) { + this->year_ = year; + return *this; + } + DateTimeCall &set_month(uint8_t month) { + this->month_ = month; + return *this; + } + DateTimeCall &set_day(uint8_t day) { + this->day_ = day; + return *this; + } + DateTimeCall &set_hour(uint8_t hour) { + this->hour_ = hour; + return *this; + } + DateTimeCall &set_minute(uint8_t minute) { + this->minute_ = minute; + return *this; + } + DateTimeCall &set_second(uint8_t second) { + this->second_ = second; + return *this; + } + + optional get_year() const { return this->year_; } + optional get_month() const { return this->month_; } + optional get_day() const { return this->day_; } + optional get_hour() const { return this->hour_; } + optional get_minute() const { return this->minute_; } + optional get_second() const { return this->second_; } + + protected: + void validate_(); + + DateTimeEntity *parent_; + + optional year_; + optional month_; + optional day_; + optional hour_; + optional minute_; + optional second_; +}; + +template class DateTimeSetAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(ESPTime, datetime) + + void play(Ts... x) override { + auto call = this->parent_->make_call(); + + if (this->datetime_.has_value()) { + call.set_datetime(this->datetime_.value(x...)); + } + call.perform(); + } +}; + +#ifdef USE_TIME +class OnDateTimeTrigger : public Trigger<>, public Component, public Parented { + public: + void loop() override; + + protected: + bool matches_(const ESPTime &time) const; + + optional last_check_; +}; +#endif + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_DATETIME diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp new file mode 100644 index 0000000000..db0094ae01 --- /dev/null +++ b/esphome/components/datetime/time_entity.cpp @@ -0,0 +1,154 @@ +#include "time_entity.h" + +#ifdef USE_DATETIME_TIME + +#include "esphome/core/log.h" + +namespace esphome { +namespace datetime { + +static const char *const TAG = "datetime.time_entity"; + +void TimeEntity::publish_state() { + if (this->hour_ > 23) { + this->has_state_ = false; + ESP_LOGE(TAG, "Hour must be between 0 and 23"); + return; + } + if (this->minute_ > 59) { + this->has_state_ = false; + ESP_LOGE(TAG, "Minute must be between 0 and 59"); + return; + } + if (this->second_ > 59) { + this->has_state_ = false; + ESP_LOGE(TAG, "Second must be between 0 and 59"); + return; + } + this->has_state_ = true; + ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, + this->second_); + this->state_callback_.call(); +} + +TimeCall TimeEntity::make_call() { return TimeCall(this); } + +void TimeCall::validate_() { + if (this->hour_.has_value() && this->hour_ > 23) { + ESP_LOGE(TAG, "Hour must be between 0 and 23"); + this->hour_.reset(); + } + if (this->minute_.has_value() && this->minute_ > 59) { + ESP_LOGE(TAG, "Minute must be between 0 and 59"); + this->minute_.reset(); + } + if (this->second_.has_value() && this->second_ > 59) { + ESP_LOGE(TAG, "Second must be between 0 and 59"); + this->second_.reset(); + } +} + +void TimeCall::perform() { + this->validate_(); + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + if (this->hour_.has_value()) { + ESP_LOGD(TAG, " Hour: %d", *this->hour_); + } + if (this->minute_.has_value()) { + ESP_LOGD(TAG, " Minute: %d", *this->minute_); + } + if (this->second_.has_value()) { + ESP_LOGD(TAG, " Second: %d", *this->second_); + } + this->parent_->control(*this); +} + +TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) { + this->hour_ = hour; + this->minute_ = minute; + this->second_ = second; + return *this; +}; + +TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); }; + +TimeCall &TimeCall::set_time(const std::string &time) { + ESPTime val{}; + if (!ESPTime::strptime(time, val)) { + ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object"); + return *this; + } + return this->set_time(val); +} + +TimeCall TimeEntityRestoreState::to_call(TimeEntity *time) { + TimeCall call = time->make_call(); + call.set_time(this->hour, this->minute, this->second); + return call; +} + +void TimeEntityRestoreState::apply(TimeEntity *time) { + time->hour_ = this->hour; + time->minute_ = this->minute; + time->second_ = this->second; + time->publish_state(); +} + +#ifdef USE_TIME +static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider + // there has been a drastic time synchronization + +void OnTimeTrigger::loop() { + if (!this->parent_->has_state()) { + return; + } + ESPTime time = this->parent_->rtc_->now(); + if (!time.is_valid()) { + return; + } + if (this->last_check_.has_value()) { + if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { + // We went back in time (a lot), probably caused by time synchronization + ESP_LOGW(TAG, "Time has jumped back!"); + } else if (*this->last_check_ >= time) { + // already handled this one + return; + } else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) { + // We went ahead in time (a lot), probably caused by time synchronization + ESP_LOGW(TAG, "Time has jumped ahead!"); + this->last_check_ = time; + return; + } + + while (true) { + this->last_check_->increment_second(); + if (*this->last_check_ >= time) + break; + + if (this->matches_(*this->last_check_)) { + this->trigger(); + break; + } + } + } + + this->last_check_ = time; + if (!time.fields_in_range()) { + ESP_LOGW(TAG, "Time is out of range!"); + ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u", time.second, time.minute, time.hour); + } + + if (this->matches_(time)) + this->trigger(); +} + +bool OnTimeTrigger::matches_(const ESPTime &time) const { + return time.is_valid() && time.hour == this->parent_->hour && time.minute == this->parent_->minute && + time.second == this->parent_->second; +} +#endif + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_TIME diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h new file mode 100644 index 0000000000..f7e0a7ddd9 --- /dev/null +++ b/esphome/components/datetime/time_entity.h @@ -0,0 +1,131 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_TIME + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "esphome/core/time.h" + +#include "datetime_base.h" + +namespace esphome { +namespace datetime { + +#define LOG_DATETIME_TIME(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +class TimeCall; +class TimeEntity; +class OnTimeTrigger; + +struct TimeEntityRestoreState { + uint8_t hour; + uint8_t minute; + uint8_t second; + + TimeCall to_call(TimeEntity *time); + void apply(TimeEntity *time); +} __attribute__((packed)); + +class TimeEntity : public DateTimeBase { + protected: + uint8_t hour_; + uint8_t minute_; + uint8_t second_; + + public: + void publish_state(); + TimeCall make_call(); + + ESPTime state_as_esptime() const override { + ESPTime obj; + obj.hour = this->hour_; + obj.minute = this->minute_; + obj.second = this->second_; + return obj; + } + + const uint8_t &hour = hour_; + const uint8_t &minute = minute_; + const uint8_t &second = second_; + + protected: + friend class TimeCall; + friend struct TimeEntityRestoreState; + friend class OnTimeTrigger; + + virtual void control(const TimeCall &call) = 0; +}; + +class TimeCall { + public: + explicit TimeCall(TimeEntity *parent) : parent_(parent) {} + void perform(); + TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second); + TimeCall &set_time(ESPTime time); + TimeCall &set_time(const std::string &time); + + TimeCall &set_hour(uint8_t hour) { + this->hour_ = hour; + return *this; + } + TimeCall &set_minute(uint8_t minute) { + this->minute_ = minute; + return *this; + } + TimeCall &set_second(uint8_t second) { + this->second_ = second; + return *this; + } + + optional get_hour() const { return this->hour_; } + optional get_minute() const { return this->minute_; } + optional get_second() const { return this->second_; } + + protected: + void validate_(); + + TimeEntity *parent_; + + optional hour_; + optional minute_; + optional second_; +}; + +template class TimeSetAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(ESPTime, time) + + void play(Ts... x) override { + auto call = this->parent_->make_call(); + + if (this->time_.has_value()) { + call.set_time(this->time_.value(x...)); + } + call.perform(); + } +}; + +#ifdef USE_TIME +class OnTimeTrigger : public Trigger<>, public Component, public Parented { + public: + void loop() override; + + protected: + bool matches_(const ESPTime &time) const; + + optional last_check_; +}; +#endif + +} // namespace datetime +} // namespace esphome + +#endif // USE_DATETIME_TIME diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 9742b3b19e..1955b5d22c 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -38,7 +38,6 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.polling_component_schema("60s")), - cv.only_on(["esp32", "esp8266"]), ) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 9843fa1c99..cbd4249d92 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -5,46 +5,19 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/version.h" - -#ifdef USE_ESP32 - -#include -#include - -#if ESP_IDF_VERSION_MAJOR >= 4 -#include -#else -#include -#endif - -#endif // USE_ESP32 - -#ifdef USE_ARDUINO -#include -#endif +#include +#include namespace esphome { namespace debug { static const char *const TAG = "debug"; -static uint32_t get_free_heap() { -#if defined(USE_ESP8266) - return ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance) -#elif defined(USE_ESP32) - return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); -#endif -} - void DebugComponent::dump_config() { #ifndef ESPHOME_LOG_HAS_DEBUG return; // Can't log below if debug logging is disabled #endif - std::string device_info; - std::string reset_reason; - device_info.reserve(256); - ESP_LOGCONFIG(TAG, "Debug component:"); #ifdef USE_TEXT_SENSOR LOG_TEXT_SENSOR(" ", "Device info", this->device_info_); @@ -57,220 +30,15 @@ void DebugComponent::dump_config() { #endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #endif // USE_SENSOR + std::string device_info; + device_info.reserve(256); ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); device_info += ESPHOME_VERSION; - this->free_heap_ = get_free_heap(); - ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_); + this->free_heap_ = get_free_heap_(); + ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); -#ifdef USE_ARDUINO - const char *flash_mode; - switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) - case FM_QIO: - flash_mode = "QIO"; - break; - case FM_QOUT: - flash_mode = "QOUT"; - break; - case FM_DIO: - flash_mode = "DIO"; - break; - case FM_DOUT: - flash_mode = "DOUT"; - break; -#ifdef USE_ESP32 - case FM_FAST_READ: - flash_mode = "FAST_READ"; - break; - case FM_SLOW_READ: - flash_mode = "SLOW_READ"; - break; -#endif - default: - flash_mode = "UNKNOWN"; - } - ESP_LOGD(TAG, "Flash Chip: Size=%ukB Speed=%uMHz Mode=%s", - ESP.getFlashChipSize() / 1024, // NOLINT - ESP.getFlashChipSpeed() / 1000000, flash_mode); // NOLINT - device_info += "|Flash: " + to_string(ESP.getFlashChipSize() / 1024) + // NOLINT - "kB Speed:" + to_string(ESP.getFlashChipSpeed() / 1000000) + "MHz Mode:"; // NOLINT - device_info += flash_mode; -#endif // USE_ARDUINO - -#ifdef USE_ESP32 - esp_chip_info_t info; - esp_chip_info(&info); - const char *model; - switch (info.model) { - case CHIP_ESP32: - model = "ESP32"; - break; - default: - model = "UNKNOWN"; - } - std::string features; - if (info.features & CHIP_FEATURE_EMB_FLASH) { - features += "EMB_FLASH,"; - info.features &= ~CHIP_FEATURE_EMB_FLASH; - } - if (info.features & CHIP_FEATURE_WIFI_BGN) { - features += "WIFI_BGN,"; - info.features &= ~CHIP_FEATURE_WIFI_BGN; - } - if (info.features & CHIP_FEATURE_BLE) { - features += "BLE,"; - info.features &= ~CHIP_FEATURE_BLE; - } - if (info.features & CHIP_FEATURE_BT) { - features += "BT,"; - info.features &= ~CHIP_FEATURE_BT; - } - if (info.features) - features += "Other:" + format_hex(info.features); - ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores, - info.revision); - device_info += "|Chip: "; - device_info += model; - device_info += " Features:"; - device_info += features; - device_info += " Cores:" + to_string(info.cores); - device_info += " Revision:" + to_string(info.revision); - - ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); - device_info += "|ESP-IDF: "; - device_info += esp_get_idf_version(); - - std::string mac = get_mac_address_pretty(); - ESP_LOGD(TAG, "EFuse MAC: %s", mac.c_str()); - device_info += "|EFuse MAC: "; - device_info += mac; - - switch (rtc_get_reset_reason(0)) { - case POWERON_RESET: - reset_reason = "Power On Reset"; - break; - case SW_RESET: - reset_reason = "Software Reset Digital Core"; - break; - case OWDT_RESET: - reset_reason = "Watch Dog Reset Digital Core"; - break; - case DEEPSLEEP_RESET: - reset_reason = "Deep Sleep Reset Digital Core"; - break; - case SDIO_RESET: - reset_reason = "SLC Module Reset Digital Core"; - break; - case TG0WDT_SYS_RESET: - reset_reason = "Timer Group 0 Watch Dog Reset Digital Core"; - break; - case TG1WDT_SYS_RESET: - reset_reason = "Timer Group 1 Watch Dog Reset Digital Core"; - break; - case RTCWDT_SYS_RESET: - reset_reason = "RTC Watch Dog Reset Digital Core"; - break; - case INTRUSION_RESET: - reset_reason = "Intrusion Reset CPU"; - break; - case TGWDT_CPU_RESET: - reset_reason = "Timer Group Reset CPU"; - break; - case SW_CPU_RESET: - reset_reason = "Software Reset CPU"; - break; - case RTCWDT_CPU_RESET: - reset_reason = "RTC Watch Dog Reset CPU"; - break; - case EXT_CPU_RESET: - reset_reason = "External CPU Reset"; - break; - case RTCWDT_BROWN_OUT_RESET: - reset_reason = "Voltage Unstable Reset"; - break; - case RTCWDT_RTC_RESET: - reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module"; - break; - default: - reset_reason = "Unknown Reset Reason"; - } - ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); - device_info += "|Reset: "; - device_info += reset_reason; - - const char *wakeup_reason; - switch (rtc_get_wakeup_cause()) { - case NO_SLEEP: - wakeup_reason = "No Sleep"; - break; - case EXT_EVENT0_TRIG: - wakeup_reason = "External Event 0"; - break; - case EXT_EVENT1_TRIG: - wakeup_reason = "External Event 1"; - break; - case GPIO_TRIG: - wakeup_reason = "GPIO"; - break; - case TIMER_EXPIRE: - wakeup_reason = "Wakeup Timer"; - break; - case SDIO_TRIG: - wakeup_reason = "SDIO"; - break; - case MAC_TRIG: - wakeup_reason = "MAC"; - break; - case UART0_TRIG: - wakeup_reason = "UART0"; - break; - case UART1_TRIG: - wakeup_reason = "UART1"; - break; - case TOUCH_TRIG: - wakeup_reason = "Touch"; - break; - case SAR_TRIG: - wakeup_reason = "SAR"; - break; - case BT_TRIG: - wakeup_reason = "BT"; - break; - default: - wakeup_reason = "Unknown"; - } - ESP_LOGD(TAG, "Wakeup Reason: %s", wakeup_reason); - device_info += "|Wakeup: "; - device_info += wakeup_reason; -#endif - -#if defined(USE_ESP8266) && !defined(CLANG_TIDY) - ESP_LOGD(TAG, "Chip ID: 0x%08X", ESP.getChipId()); - ESP_LOGD(TAG, "SDK Version: %s", ESP.getSdkVersion()); - ESP_LOGD(TAG, "Core Version: %s", ESP.getCoreVersion().c_str()); - ESP_LOGD(TAG, "Boot Version=%u Mode=%u", ESP.getBootVersion(), ESP.getBootMode()); - ESP_LOGD(TAG, "CPU Frequency: %u", ESP.getCpuFreqMHz()); - ESP_LOGD(TAG, "Flash Chip ID=0x%08X", ESP.getFlashChipId()); - ESP_LOGD(TAG, "Reset Reason: %s", ESP.getResetReason().c_str()); - ESP_LOGD(TAG, "Reset Info: %s", ESP.getResetInfo().c_str()); - - device_info += "|Chip: 0x" + format_hex(ESP.getChipId()); - device_info += "|SDK: "; - device_info += ESP.getSdkVersion(); - device_info += "|Core: "; - device_info += ESP.getCoreVersion().c_str(); - device_info += "|Boot: "; - device_info += to_string(ESP.getBootVersion()); - device_info += "|Mode: " + to_string(ESP.getBootMode()); - device_info += "|CPU: " + to_string(ESP.getCpuFreqMHz()); - device_info += "|Flash: 0x" + format_hex(ESP.getFlashChipId()); - device_info += "|Reset: "; - device_info += ESP.getResetReason().c_str(); - device_info += "|"; - device_info += ESP.getResetInfo().c_str(); - - reset_reason = ESP.getResetReason().c_str(); -#endif + get_device_info_(device_info); #ifdef USE_TEXT_SENSOR if (this->device_info_ != nullptr) { @@ -279,17 +47,17 @@ void DebugComponent::dump_config() { this->device_info_->publish_state(device_info); } if (this->reset_reason_ != nullptr) { - this->reset_reason_->publish_state(reset_reason); + this->reset_reason_->publish_state(get_reset_reason_()); } #endif // USE_TEXT_SENSOR } void DebugComponent::loop() { // log when free heap space has halved - uint32_t new_free_heap = get_free_heap(); + uint32_t new_free_heap = get_free_heap_(); if (new_free_heap < this->free_heap_ / 2) { this->free_heap_ = new_free_heap; - ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_); + ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); this->status_momentary_warning("heap", 1000); } @@ -307,30 +75,16 @@ void DebugComponent::loop() { void DebugComponent::update() { #ifdef USE_SENSOR if (this->free_sensor_ != nullptr) { - this->free_sensor_->publish_state(get_free_heap()); + this->free_sensor_->publish_state(get_free_heap_()); } - if (this->block_sensor_ != nullptr) { -#if defined(USE_ESP8266) - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize()); -#elif defined(USE_ESP32) - this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); -#endif - } - -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) - if (this->fragmentation_sensor_ != nullptr) { - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation()); - } -#endif - if (this->loop_time_sensor_ != nullptr) { this->loop_time_sensor_->publish_state(this->max_loop_time_); this->max_loop_time_ = 0; } + #endif // USE_SENSOR + update_platform_(); } float DebugComponent::get_setup_priority() const { return setup_priority::LATE; } diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index b80fda55eb..2b54406603 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -33,6 +33,9 @@ class DebugComponent : public PollingComponent { void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } #endif void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } +#ifdef USE_ESP32 + void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; } +#endif // USE_ESP32 #endif // USE_SENSOR protected: uint32_t free_heap_{}; @@ -47,12 +50,20 @@ class DebugComponent : public PollingComponent { sensor::Sensor *fragmentation_sensor_{nullptr}; #endif sensor::Sensor *loop_time_sensor_{nullptr}; +#ifdef USE_ESP32 + sensor::Sensor *psram_sensor_{nullptr}; +#endif // USE_ESP32 #endif // USE_SENSOR #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *device_info_{nullptr}; text_sensor::TextSensor *reset_reason_{nullptr}; #endif // USE_TEXT_SENSOR + + std::string get_reset_reason_(); + uint32_t get_free_heap_(); + void get_device_info_(std::string &device_info); + void update_platform_(); }; } // namespace debug diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp new file mode 100644 index 0000000000..cb4330f422 --- /dev/null +++ b/esphome/components/debug/debug_esp32.cpp @@ -0,0 +1,297 @@ +#include "debug_component.h" +#ifdef USE_ESP32 +#include "esphome/core/log.h" + +#include +#include +#include + +#if defined(USE_ESP32_VARIANT_ESP32) +#include +#elif defined(USE_ESP32_VARIANT_ESP32C3) +#include +#elif defined(USE_ESP32_VARIANT_ESP32C6) +#include +#elif defined(USE_ESP32_VARIANT_ESP32S2) +#include +#elif defined(USE_ESP32_VARIANT_ESP32S3) +#include +#elif defined(USE_ESP32_VARIANT_ESP32H2) +#include +#endif +#ifdef USE_ARDUINO +#include +#endif + +namespace esphome { +namespace debug { + +static const char *const TAG = "debug"; + +std::string DebugComponent::get_reset_reason_() { + std::string reset_reason; + switch (rtc_get_reset_reason(0)) { + case POWERON_RESET: + reset_reason = "Power On Reset"; + break; +#if defined(USE_ESP32_VARIANT_ESP32) + case SW_RESET: +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) + case RTC_SW_SYS_RESET: +#endif + reset_reason = "Software Reset Digital Core"; + break; +#if defined(USE_ESP32_VARIANT_ESP32) + case OWDT_RESET: + reset_reason = "Watch Dog Reset Digital Core"; + break; +#endif + case DEEPSLEEP_RESET: + reset_reason = "Deep Sleep Reset Digital Core"; + break; +#if defined(USE_ESP32_VARIANT_ESP32) + case SDIO_RESET: + reset_reason = "SLC Module Reset Digital Core"; + break; +#endif + case TG0WDT_SYS_RESET: + reset_reason = "Timer Group 0 Watch Dog Reset Digital Core"; + break; + case TG1WDT_SYS_RESET: + reset_reason = "Timer Group 1 Watch Dog Reset Digital Core"; + break; + case RTCWDT_SYS_RESET: + reset_reason = "RTC Watch Dog Reset Digital Core"; + break; +#if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) + case INTRUSION_RESET: + reset_reason = "Intrusion Reset CPU"; + break; +#endif +#if defined(USE_ESP32_VARIANT_ESP32) + case TGWDT_CPU_RESET: + reset_reason = "Timer Group Reset CPU"; + break; +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) + case TG0WDT_CPU_RESET: + reset_reason = "Timer Group 0 Reset CPU"; + break; +#endif +#if defined(USE_ESP32_VARIANT_ESP32) + case SW_CPU_RESET: +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) + case RTC_SW_CPU_RESET: +#endif + reset_reason = "Software Reset CPU"; + break; + case RTCWDT_CPU_RESET: + reset_reason = "RTC Watch Dog Reset CPU"; + break; +#if defined(USE_ESP32_VARIANT_ESP32) + case EXT_CPU_RESET: + reset_reason = "External CPU Reset"; + break; +#endif + case RTCWDT_BROWN_OUT_RESET: + reset_reason = "Voltage Unstable Reset"; + break; + case RTCWDT_RTC_RESET: + reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module"; + break; +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + defined(USE_ESP32_VARIANT_ESP32C6) + case TG1WDT_CPU_RESET: + reset_reason = "Timer Group 1 Reset CPU"; + break; + case SUPER_WDT_RESET: + reset_reason = "Super Watchdog Reset Digital Core And RTC Module"; + break; + case EFUSE_RESET: + reset_reason = "eFuse Reset Digital Core"; + break; +#endif +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + case GLITCH_RTC_RESET: + reset_reason = "Glitch Reset Digital Core And RTC Module"; + break; +#endif +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) + case USB_UART_CHIP_RESET: + reset_reason = "USB UART Reset Digital Core"; + break; + case USB_JTAG_CHIP_RESET: + reset_reason = "USB JTAG Reset Digital Core"; + break; +#endif +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) + case POWER_GLITCH_RESET: + reset_reason = "Power Glitch Reset Digital Core And RTC Module"; + break; +#endif + default: + reset_reason = "Unknown Reset Reason"; + } + ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); + return reset_reason; +} + +uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); } + +void DebugComponent::get_device_info_(std::string &device_info) { +#if defined(USE_ARDUINO) + const char *flash_mode; + switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) + case FM_QIO: + flash_mode = "QIO"; + break; + case FM_QOUT: + flash_mode = "QOUT"; + break; + case FM_DIO: + flash_mode = "DIO"; + break; + case FM_DOUT: + flash_mode = "DOUT"; + break; + case FM_FAST_READ: + flash_mode = "FAST_READ"; + break; + case FM_SLOW_READ: + flash_mode = "SLOW_READ"; + break; + default: + flash_mode = "UNKNOWN"; + } + ESP_LOGD(TAG, "Flash Chip: Size=%ukB Speed=%uMHz Mode=%s", + ESP.getFlashChipSize() / 1024, // NOLINT + ESP.getFlashChipSpeed() / 1000000, flash_mode); // NOLINT + device_info += "|Flash: " + to_string(ESP.getFlashChipSize() / 1024) + // NOLINT + "kB Speed:" + to_string(ESP.getFlashChipSpeed() / 1000000) + "MHz Mode:"; // NOLINT + device_info += flash_mode; +#endif + + esp_chip_info_t info; + esp_chip_info(&info); + const char *model; +#if defined(USE_ESP32_VARIANT_ESP32) + model = "ESP32"; +#elif defined(USE_ESP32_VARIANT_ESP32C3) + model = "ESP32-C3"; +#elif defined(USE_ESP32_VARIANT_ESP32C6) + model = "ESP32-C6"; +#elif defined(USE_ESP32_VARIANT_ESP32S2) + model = "ESP32-S2"; +#elif defined(USE_ESP32_VARIANT_ESP32S3) + model = "ESP32-S3"; +#elif defined(USE_ESP32_VARIANT_ESP32H2) + model = "ESP32-H2"; +#else + model = "UNKNOWN"; +#endif + std::string features; + if (info.features & CHIP_FEATURE_EMB_FLASH) { + features += "EMB_FLASH,"; + info.features &= ~CHIP_FEATURE_EMB_FLASH; + } + if (info.features & CHIP_FEATURE_WIFI_BGN) { + features += "WIFI_BGN,"; + info.features &= ~CHIP_FEATURE_WIFI_BGN; + } + if (info.features & CHIP_FEATURE_BLE) { + features += "BLE,"; + info.features &= ~CHIP_FEATURE_BLE; + } + if (info.features & CHIP_FEATURE_BT) { + features += "BT,"; + info.features &= ~CHIP_FEATURE_BT; + } + if (info.features & CHIP_FEATURE_EMB_PSRAM) { + features += "EMB_PSRAM,"; + info.features &= ~CHIP_FEATURE_EMB_PSRAM; + } + if (info.features) + features += "Other:" + format_hex(info.features); + ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores, + info.revision); + device_info += "|Chip: "; + device_info += model; + device_info += " Features:"; + device_info += features; + device_info += " Cores:" + to_string(info.cores); + device_info += " Revision:" + to_string(info.revision); + + ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); + device_info += "|ESP-IDF: "; + device_info += esp_get_idf_version(); + + std::string mac = get_mac_address_pretty(); + ESP_LOGD(TAG, "EFuse MAC: %s", mac.c_str()); + device_info += "|EFuse MAC: "; + device_info += mac; + + device_info += "|Reset: "; + device_info += get_reset_reason_(); + + const char *wakeup_reason; + switch (rtc_get_wakeup_cause()) { + case NO_SLEEP: + wakeup_reason = "No Sleep"; + break; + case EXT_EVENT0_TRIG: + wakeup_reason = "External Event 0"; + break; + case EXT_EVENT1_TRIG: + wakeup_reason = "External Event 1"; + break; + case GPIO_TRIG: + wakeup_reason = "GPIO"; + break; + case TIMER_EXPIRE: + wakeup_reason = "Wakeup Timer"; + break; + case SDIO_TRIG: + wakeup_reason = "SDIO"; + break; + case MAC_TRIG: + wakeup_reason = "MAC"; + break; + case UART0_TRIG: + wakeup_reason = "UART0"; + break; + case UART1_TRIG: + wakeup_reason = "UART1"; + break; + case TOUCH_TRIG: + wakeup_reason = "Touch"; + break; + case SAR_TRIG: + wakeup_reason = "SAR"; + break; + case BT_TRIG: + wakeup_reason = "BT"; + break; + default: + wakeup_reason = "Unknown"; + } + ESP_LOGD(TAG, "Wakeup Reason: %s", wakeup_reason); + device_info += "|Wakeup: "; + device_info += wakeup_reason; +} + +void DebugComponent::update_platform_() { +#ifdef USE_SENSOR + if (this->block_sensor_ != nullptr) { + this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); + } + if (this->psram_sensor_ != nullptr) { + this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + } +#endif +} + +} // namespace debug +} // namespace esphome +#endif diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp new file mode 100644 index 0000000000..3395d9db12 --- /dev/null +++ b/esphome/components/debug/debug_esp8266.cpp @@ -0,0 +1,94 @@ +#include "debug_component.h" +#ifdef USE_ESP8266 +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace debug { + +static const char *const TAG = "debug"; + +std::string DebugComponent::get_reset_reason_() { +#if !defined(CLANG_TIDY) + return ESP.getResetReason().c_str(); +#else + return ""; +#endif +} + +uint32_t DebugComponent::get_free_heap_() { + return ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance) +} + +void DebugComponent::get_device_info_(std::string &device_info) { + const char *flash_mode; + switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) + case FM_QIO: + flash_mode = "QIO"; + break; + case FM_QOUT: + flash_mode = "QOUT"; + break; + case FM_DIO: + flash_mode = "DIO"; + break; + case FM_DOUT: + flash_mode = "DOUT"; + break; + default: + flash_mode = "UNKNOWN"; + } + ESP_LOGD(TAG, "Flash Chip: Size=%ukB Speed=%uMHz Mode=%s", + ESP.getFlashChipSize() / 1024, // NOLINT + ESP.getFlashChipSpeed() / 1000000, flash_mode); // NOLINT + device_info += "|Flash: " + to_string(ESP.getFlashChipSize() / 1024) + // NOLINT + "kB Speed:" + to_string(ESP.getFlashChipSpeed() / 1000000) + "MHz Mode:"; // NOLINT + device_info += flash_mode; + +#if !defined(CLANG_TIDY) + auto reset_reason = get_reset_reason_(); + ESP_LOGD(TAG, "Chip ID: 0x%08X", ESP.getChipId()); + ESP_LOGD(TAG, "SDK Version: %s", ESP.getSdkVersion()); + ESP_LOGD(TAG, "Core Version: %s", ESP.getCoreVersion().c_str()); + ESP_LOGD(TAG, "Boot Version=%u Mode=%u", ESP.getBootVersion(), ESP.getBootMode()); + ESP_LOGD(TAG, "CPU Frequency: %u", ESP.getCpuFreqMHz()); + ESP_LOGD(TAG, "Flash Chip ID=0x%08X", ESP.getFlashChipId()); + ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); + ESP_LOGD(TAG, "Reset Info: %s", ESP.getResetInfo().c_str()); + + device_info += "|Chip: 0x" + format_hex(ESP.getChipId()); + device_info += "|SDK: "; + device_info += ESP.getSdkVersion(); + device_info += "|Core: "; + device_info += ESP.getCoreVersion().c_str(); + device_info += "|Boot: "; + device_info += to_string(ESP.getBootVersion()); + device_info += "|Mode: " + to_string(ESP.getBootMode()); + device_info += "|CPU: " + to_string(ESP.getCpuFreqMHz()); + device_info += "|Flash: 0x" + format_hex(ESP.getFlashChipId()); + device_info += "|Reset: "; + device_info += reset_reason; + device_info += "|"; + device_info += ESP.getResetInfo().c_str(); +#endif +} + +void DebugComponent::update_platform_() { +#ifdef USE_SENSOR + if (this->block_sensor_ != nullptr) { + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize()); + } +#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) + if (this->fragmentation_sensor_ != nullptr) { + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation()); + } +#endif + +#endif +} + +} // namespace debug +} // namespace esphome +#endif diff --git a/esphome/components/debug/debug_host.cpp b/esphome/components/debug/debug_host.cpp new file mode 100644 index 0000000000..09ad34ef88 --- /dev/null +++ b/esphome/components/debug/debug_host.cpp @@ -0,0 +1,18 @@ +#include "debug_component.h" +#ifdef USE_HOST +#include + +namespace esphome { +namespace debug { + +std::string DebugComponent::get_reset_reason_() { return ""; } + +uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } + +void DebugComponent::get_device_info_(std::string &device_info) {} + +void DebugComponent::update_platform_() {} + +} // namespace debug +} // namespace esphome +#endif diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp new file mode 100644 index 0000000000..b5e2a5b310 --- /dev/null +++ b/esphome/components/debug/debug_libretiny.cpp @@ -0,0 +1,44 @@ +#include "debug_component.h" +#ifdef USE_LIBRETINY +#include "esphome/core/log.h" + +namespace esphome { +namespace debug { + +static const char *const TAG = "debug"; + +std::string DebugComponent::get_reset_reason_() { return lt_get_reboot_reason_name(lt_get_reboot_reason()); } + +uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); } + +void DebugComponent::get_device_info_(std::string &device_info) { + std::string reset_reason = get_reset_reason_(); + ESP_LOGD(TAG, "LibreTiny Version: %s", lt_get_version()); + ESP_LOGD(TAG, "Chip: %s (%04x) @ %u MHz", lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz()); + ESP_LOGD(TAG, "Chip ID: 0x%06X", lt_cpu_get_mac_id()); + ESP_LOGD(TAG, "Board: %s", lt_get_board_code()); + ESP_LOGD(TAG, "Flash: %u KiB / RAM: %u KiB", lt_flash_get_size() / 1024, lt_ram_get_size() / 1024); + ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); + + device_info += "|Version: "; + device_info += LT_BANNER_STR + 10; + device_info += "|Reset Reason: "; + device_info += reset_reason; + device_info += "|Chip Name: "; + device_info += lt_cpu_get_model_name(); + device_info += "|Chip ID: 0x" + format_hex(lt_cpu_get_mac_id()); + device_info += "|Flash: " + to_string(lt_flash_get_size() / 1024) + " KiB"; + device_info += "|RAM: " + to_string(lt_ram_get_size() / 1024) + " KiB"; +} + +void DebugComponent::update_platform_() { +#ifdef USE_SENSOR + if (this->block_sensor_ != nullptr) { + this->block_sensor_->publish_state(lt_heap_get_max_alloc()); + } +#endif +} + +} // namespace debug +} // namespace esphome +#endif diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp new file mode 100644 index 0000000000..497547e30d --- /dev/null +++ b/esphome/components/debug/debug_rp2040.cpp @@ -0,0 +1,23 @@ +#include "debug_component.h" +#ifdef USE_RP2040 +#include "esphome/core/log.h" +#include +namespace esphome { +namespace debug { + +static const char *const TAG = "debug"; + +std::string DebugComponent::get_reset_reason_() { return ""; } + +uint32_t DebugComponent::get_free_heap_() { return rp2040.getFreeHeap(); } + +void DebugComponent::get_device_info_(std::string &device_info) { + ESP_LOGD(TAG, "CPU Frequency: %u", rp2040.f_cpu()); + device_info += "CPU Frequency: " + to_string(rp2040.f_cpu()); +} + +void DebugComponent::update_platform_() {} + +} // namespace debug +} // namespace esphome +#endif diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index f7ea07d138..061c2750e4 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -17,6 +17,8 @@ from . import CONF_DEBUG_ID, DebugComponent DEPENDENCIES = ["debug"] +CONF_PSRAM = "psram" + CONFIG_SCHEMA = { cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), cv.Optional(CONF_FREE): sensor.sensor_schema( @@ -47,24 +49,38 @@ CONFIG_SCHEMA = { accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), + cv.Optional(CONF_PSRAM): cv.All( + cv.only_on_esp32, + cv.requires_component("psram"), + sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), } async def to_code(config): debug_component = await cg.get_variable(config[CONF_DEBUG_ID]) - if CONF_FREE in config: - sens = await sensor.new_sensor(config[CONF_FREE]) + if free_conf := config.get(CONF_FREE): + sens = await sensor.new_sensor(free_conf) cg.add(debug_component.set_free_sensor(sens)) - if CONF_BLOCK in config: - sens = await sensor.new_sensor(config[CONF_BLOCK]) + if block_conf := config.get(CONF_BLOCK): + sens = await sensor.new_sensor(block_conf) cg.add(debug_component.set_block_sensor(sens)) - if CONF_FRAGMENTATION in config: - sens = await sensor.new_sensor(config[CONF_FRAGMENTATION]) + if fragmentation_conf := config.get(CONF_FRAGMENTATION): + sens = await sensor.new_sensor(fragmentation_conf) cg.add(debug_component.set_fragmentation_sensor(sens)) - if CONF_LOOP_TIME in config: - sens = await sensor.new_sensor(config[CONF_LOOP_TIME]) + if loop_time_conf := config.get(CONF_LOOP_TIME): + sens = await sensor.new_sensor(loop_time_conf) cg.add(debug_component.set_loop_time_sensor(sens)) + + if psram_conf := config.get(CONF_PSRAM): + sens = await sensor.new_sensor(psram_conf) + cg.add(debug_component.set_psram_sensor(sens)) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index bbd10d58c5..fd7ef6fcce 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -14,6 +14,8 @@ from esphome.const import ( CONF_SLEEP_DURATION, CONF_TIME_ID, CONF_WAKEUP_PIN, + PLATFORM_ESP32, + PLATFORM_ESP8266, ) from esphome.components.esp32 import get_esp32_variant @@ -22,6 +24,9 @@ from esphome.components.esp32.const import ( VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3, + VARIANT_ESP32C2, + VARIANT_ESP32C6, + VARIANT_ESP32H2, ) WAKEUP_PINS = { @@ -94,6 +99,9 @@ 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], } @@ -159,34 +167,39 @@ WAKEUP_CAUSES_SCHEMA = cv.Schema( } ) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(DeepSleepComponent), - cv.Optional(CONF_RUN_DURATION): cv.Any( - 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_WAKEUP_PIN_MODE): cv.All( - cv.only_on_esp32, cv.enum(WAKEUP_PIN_MODES), upper=True - ), - cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( - cv.only_on_esp32, - cv.Schema( - { - cv.Required(CONF_PINS): cv.ensure_list( - pins.internal_gpio_input_pin_schema, validate_pin_number - ), - cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True), - } +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DeepSleepComponent), + cv.Optional(CONF_RUN_DURATION): cv.Any( + cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA), + cv.positive_time_period_milliseconds, ), - ), - cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean), - } -).extend(cv.COMPONENT_SCHEMA) + 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_WAKEUP_PIN_MODE): cv.All( + cv.only_on_esp32, cv.enum(WAKEUP_PIN_MODES), upper=True + ), + cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( + cv.only_on_esp32, + cv.Schema( + { + cv.Required(CONF_PINS): cv.ensure_list( + pins.internal_gpio_input_pin_schema, validate_pin_number + ), + cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True), + } + ), + ), + cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), +) async def to_code(config): diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index f6472a123c..1e7637f3e5 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -1,12 +1,7 @@ #include "deep_sleep_component.h" -#include #include "esphome/core/application.h" #include "esphome/core/log.h" -#ifdef USE_ESP8266 -#include -#endif - namespace esphome { namespace deep_sleep { @@ -14,104 +9,53 @@ static const char *const TAG = "deep_sleep"; bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -optional DeepSleepComponent::get_run_duration_() const { -#ifdef USE_ESP32 - if (this->wakeup_cause_to_run_duration_.has_value()) { - esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause(); - switch (wakeup_cause) { - case ESP_SLEEP_WAKEUP_EXT0: - case ESP_SLEEP_WAKEUP_EXT1: - case ESP_SLEEP_WAKEUP_GPIO: - return this->wakeup_cause_to_run_duration_->gpio_cause; - case ESP_SLEEP_WAKEUP_TOUCHPAD: - return this->wakeup_cause_to_run_duration_->touch_cause; - default: - return this->wakeup_cause_to_run_duration_->default_cause; - } - } -#endif - return this->run_duration_; -} - void DeepSleepComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Deep Sleep..."); global_has_deep_sleep = true; const optional run_duration = get_run_duration_(); if (run_duration.has_value()) { - ESP_LOGI(TAG, "Scheduling Deep Sleep to start in %u ms", *run_duration); + ESP_LOGI(TAG, "Scheduling Deep Sleep to start in %" PRIu32 " ms", *run_duration); this->set_timeout(*run_duration, [this]() { this->begin_sleep(); }); } else { ESP_LOGD(TAG, "Not scheduling Deep Sleep, as no run duration is configured."); } } + void DeepSleepComponent::dump_config() { ESP_LOGCONFIG(TAG, "Setting up Deep Sleep..."); if (this->sleep_duration_.has_value()) { uint32_t duration = *this->sleep_duration_ / 1000; - ESP_LOGCONFIG(TAG, " Sleep Duration: %u ms", duration); + ESP_LOGCONFIG(TAG, " Sleep Duration: %" PRIu32 " ms", duration); } if (this->run_duration_.has_value()) { - ESP_LOGCONFIG(TAG, " Run Duration: %u ms", *this->run_duration_); + ESP_LOGCONFIG(TAG, " Run Duration: %" PRIu32 " ms", *this->run_duration_); } -#ifdef USE_ESP32 - if (wakeup_pin_ != nullptr) { - LOG_PIN(" Wakeup Pin: ", this->wakeup_pin_); - } - if (this->wakeup_cause_to_run_duration_.has_value()) { - ESP_LOGCONFIG(TAG, " Default Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->default_cause); - ESP_LOGCONFIG(TAG, " Touch Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->touch_cause); - ESP_LOGCONFIG(TAG, " GPIO Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->gpio_cause); - } -#endif + this->dump_config_platform_(); } + void DeepSleepComponent::loop() { if (this->next_enter_deep_sleep_) this->begin_sleep(); } + float DeepSleepComponent::get_loop_priority() const { return -100.0f; // run after everything else is ready } + void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } -#if defined(USE_ESP32) -void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { - this->wakeup_pin_mode_ = wakeup_pin_mode; -} -#endif - -#if defined(USE_ESP32) -#if !defined(USE_ESP32_VARIANT_ESP32C3) - -void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } - -void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } - -#endif - -void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { - wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; -} - -#endif void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } + void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { this->next_enter_deep_sleep_ = true; return; } -#ifdef USE_ESP32 - if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_KEEP_AWAKE && this->wakeup_pin_ != nullptr && - !this->sleep_duration_.has_value() && this->wakeup_pin_->digital_read()) { - // Defer deep sleep until inactive - if (!this->next_enter_deep_sleep_) { - this->status_set_warning(); - ESP_LOGW(TAG, "Waiting for pin_ to switch state to enter deep sleep..."); - } - this->next_enter_deep_sleep_ = true; + + if (!this->prepare_to_sleep_()) { return; } -#endif ESP_LOGI(TAG, "Beginning Deep Sleep"); if (this->sleep_duration_.has_value()) { @@ -119,47 +63,13 @@ void DeepSleepComponent::begin_sleep(bool manual) { } App.run_safe_shutdown_hooks(); -#if defined(USE_ESP32) -#if !defined(USE_ESP32_VARIANT_ESP32C3) - if (this->sleep_duration_.has_value()) - esp_sleep_enable_timer_wakeup(*this->sleep_duration_); - if (this->wakeup_pin_ != nullptr) { - bool level = !this->wakeup_pin_->is_inverted(); - if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { - level = !level; - } - esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); - } - if (this->ext1_wakeup_.has_value()) { - esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode); - } - - 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); - } -#endif -#ifdef USE_ESP32_VARIANT_ESP32C3 - if (this->sleep_duration_.has_value()) - esp_sleep_enable_timer_wakeup(*this->sleep_duration_); - if (this->wakeup_pin_ != nullptr) { - bool level = !this->wakeup_pin_->is_inverted(); - if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { - level = !level; - } - esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(), - static_cast(level)); - } -#endif - esp_deep_sleep_start(); -#endif - -#ifdef USE_ESP8266 - ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) -#endif + this->deep_sleep_(); } + float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } + void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } + void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; } } // namespace deep_sleep diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 8dc87cece8..7a640b9ea5 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -1,9 +1,9 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #ifdef USE_ESP32 #include @@ -11,8 +11,11 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif +#include + namespace esphome { namespace deep_sleep { @@ -31,10 +34,12 @@ enum WakeupPinMode { WAKEUP_PIN_MODE_INVERT_WAKEUP, }; +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) struct Ext1Wakeup { uint64_t mask; esp_sleep_ext1_wakeup_mode_t wakeup_mode; }; +#endif struct WakeupCauseToRunDuration { // Run duration if woken up by timer or any other reason besides those below. @@ -103,11 +108,19 @@ class DeepSleepComponent : public Component { // duration before entering deep sleep. optional get_run_duration_() const; + void dump_config_platform_(); + bool prepare_to_sleep_(); + void deep_sleep_(); + optional sleep_duration_; #ifdef USE_ESP32 InternalGPIOPin *wakeup_pin_; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; + +#if !defined(USE_ESP32_VARIANT_ESP32C3) optional ext1_wakeup_; +#endif + optional touch_wakeup_; optional wakeup_cause_to_run_duration_; #endif @@ -170,7 +183,7 @@ template class EnterDeepSleepAction : public Action { if (after_time) timestamp += 60 * 60 * 24; - int32_t offset = time::ESPTime::timezone_offset(); + int32_t offset = ESPTime::timezone_offset(); timestamp -= offset; // Change timestamp to utc const uint32_t ms_left = (timestamp - timestamp_now) * 1000; this->deep_sleep_->set_sleep_duration(ms_left); diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp new file mode 100644 index 0000000000..d54046bc11 --- /dev/null +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -0,0 +1,104 @@ +#ifdef USE_ESP32 +#include "deep_sleep_component.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace deep_sleep { + +static const char *const TAG = "deep_sleep"; + +optional DeepSleepComponent::get_run_duration_() const { + if (this->wakeup_cause_to_run_duration_.has_value()) { + esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause(); + switch (wakeup_cause) { + case ESP_SLEEP_WAKEUP_EXT0: + case ESP_SLEEP_WAKEUP_EXT1: + case ESP_SLEEP_WAKEUP_GPIO: + return this->wakeup_cause_to_run_duration_->gpio_cause; + case ESP_SLEEP_WAKEUP_TOUCHPAD: + return this->wakeup_cause_to_run_duration_->touch_cause; + default: + return this->wakeup_cause_to_run_duration_->default_cause; + } + } + return this->run_duration_; +} + +void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { + this->wakeup_pin_mode_ = wakeup_pin_mode; +} + +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) +void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } + +void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } +#endif + +void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { + wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; +} + +void DeepSleepComponent::dump_config_platform_() { + if (wakeup_pin_ != nullptr) { + LOG_PIN(" Wakeup Pin: ", this->wakeup_pin_); + } + if (this->wakeup_cause_to_run_duration_.has_value()) { + ESP_LOGCONFIG(TAG, " Default Wakeup Run Duration: %" PRIu32 " ms", + this->wakeup_cause_to_run_duration_->default_cause); + ESP_LOGCONFIG(TAG, " Touch Wakeup Run Duration: %" PRIu32 " ms", this->wakeup_cause_to_run_duration_->touch_cause); + ESP_LOGCONFIG(TAG, " GPIO Wakeup Run Duration: %" PRIu32 " ms", this->wakeup_cause_to_run_duration_->gpio_cause); + } +} + +bool DeepSleepComponent::prepare_to_sleep_() { + if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_KEEP_AWAKE && this->wakeup_pin_ != nullptr && + !this->sleep_duration_.has_value() && this->wakeup_pin_->digital_read()) { + // Defer deep sleep until inactive + if (!this->next_enter_deep_sleep_) { + this->status_set_warning(); + ESP_LOGW(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_() { +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) + if (this->sleep_duration_.has_value()) + esp_sleep_enable_timer_wakeup(*this->sleep_duration_); + if (this->wakeup_pin_ != nullptr) { + bool level = !this->wakeup_pin_->is_inverted(); + if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { + level = !level; + } + esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); + } + if (this->ext1_wakeup_.has_value()) { + esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode); + } + + 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); + } +#endif +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) + if (this->sleep_duration_.has_value()) + esp_sleep_enable_timer_wakeup(*this->sleep_duration_); + if (this->wakeup_pin_ != nullptr) { + bool level = !this->wakeup_pin_->is_inverted(); + if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { + level = !level; + } + esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(), + static_cast(level)); + } +#endif + esp_deep_sleep_start(); +} + +} // namespace deep_sleep +} // namespace esphome +#endif diff --git a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp new file mode 100644 index 0000000000..54d2aa993d --- /dev/null +++ b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp @@ -0,0 +1,23 @@ +#ifdef USE_ESP8266 +#include "deep_sleep_component.h" + +#include + +namespace esphome { +namespace deep_sleep { + +static const char *const TAG = "deep_sleep"; + +optional DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } + +void DeepSleepComponent::dump_config_platform_() {} + +bool DeepSleepComponent::prepare_to_sleep_() { return true; } + +void DeepSleepComponent::deep_sleep_() { + ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) +} + +} // namespace deep_sleep +} // namespace esphome +#endif diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h index b4afa03e11..d965d987de 100644 --- a/esphome/components/demo/demo_sensor.h +++ b/esphome/components/demo/demo_sensor.h @@ -16,10 +16,11 @@ class DemoSensor : public sensor::Sensor, public PollingComponent { float base = std::isnan(this->state) ? 0.0f : this->state; this->publish_state(base + val * 10); } else { - if (val < 0.1) + if (val < 0.1) { this->publish_state(NAN); - else + } else { this->publish_state(val * 100); + } } } }; diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 5ea04b4804..c37c9999aa 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE +from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE, CONF_VOLUME from esphome.components import uart DEPENDENCIES = ["uart"] @@ -19,7 +19,6 @@ DFPlayerIsPlayingCondition = dfplayer_ns.class_( MULTI_CONF = True CONF_FOLDER = "folder" CONF_LOOP = "loop" -CONF_VOLUME = "volume" CONF_EQ_PRESET = "eq_preset" CONF_ON_FINISHED_PLAYBACK = "on_finished_playback" diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index a6339dc988..aa2dc260e0 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -7,10 +7,10 @@ namespace dfplayer { static const char *const TAG = "dfplayer"; void DFPlayer::play_folder(uint16_t folder, uint16_t file) { - if (folder <= 10 && file <= 1000) { + if (folder < 100 && file < 256) { this->ack_set_is_playing_ = true; this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); - } else if (folder < 100 && file < 256) { + } else if (folder <= 15 && file <= 3000) { this->ack_set_is_playing_ = true; this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); } else { @@ -101,6 +101,11 @@ void DFPlayer::loop() { ESP_LOGV(TAG, "Nack"); this->ack_set_is_playing_ = false; this->ack_reset_is_playing_ = false; + if (argument == 6) { + ESP_LOGV(TAG, "File not found"); + this->is_playing_ = false; + } + break; case 0x41: ESP_LOGV(TAG, "Ack ok"); this->is_playing_ |= this->ack_set_is_playing_; diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py new file mode 100644 index 0000000000..39787ca66b --- /dev/null +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -0,0 +1,205 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_SENSITIVITY +from esphome.components import uart + +CODEOWNERS = ["@niklasweber"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395") +DfrobotSen0395Component = dfrobot_sen0395_ns.class_( + "DfrobotSen0395Component", cg.Component +) + +# Actions +DfrobotSen0395ResetAction = dfrobot_sen0395_ns.class_( + "DfrobotSen0395ResetAction", automation.Action +) +DfrobotSen0395SettingsAction = dfrobot_sen0395_ns.class_( + "DfrobotSen0395SettingsAction", automation.Action +) + +CONF_DFROBOT_SEN0395_ID = "dfrobot_sen0395_id" + +CONF_DELAY_AFTER_DETECT = "delay_after_detect" +CONF_DELAY_AFTER_DISAPPEAR = "delay_after_disappear" +CONF_DETECTION_SEGMENTS = "detection_segments" +CONF_OUTPUT_LATENCY = "output_latency" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DfrobotSen0395Component), + } + ).extend(uart.UART_DEVICE_SCHEMA) +) + + +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) + + +@automation.register_action( + "dfrobot_sen0395.reset", + DfrobotSen0395ResetAction, + maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DfrobotSen0395Component), + } + ), +) +async def dfrobot_sen0395_reset_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 range_segment_list(input): + """Validate input is a list of ranges which can be used to configure the dfrobot mmwave radar + + A list of segments should be provided. A minimum of one segment is required and a maximum of + four segments is allowed. A segment describes a range of distances. E.g. from 0mm to 1m. + The distances need to be defined in an ascending order and they cannot contain / intersect + each other. + """ + + # Flatten input to one dimensional list + flat_list = [] + if isinstance(input, list): + for list_item in input: + if isinstance(list_item, list): + for item in list_item: + flat_list.append(item) + else: + flat_list.append(list_item) + else: + flat_list.append(input) + + input = flat_list + + if len(input) < 2: + raise cv.Invalid( + "At least two values need to be specified (start + stop distances)" + ) + if len(input) % 2 != 0: + raise cv.Invalid( + "An even number of arguments must be specified (pairs of min + max)" + ) + if len(input) > 8: + raise cv.Invalid( + "Maximum four segments can be specified (8 values: 4 * min + max)" + ) + + largest_distance = -1 + for distance in input: + if isinstance(distance, cv.Lambda): + continue + m = cv.distance(distance) + if m > 9: + raise cv.Invalid("Maximum distance is 9m") + if m < 0: + raise cv.Invalid("Minimum distance is 0m") + if m <= largest_distance: + raise cv.Invalid( + "Distances must be delared from small to large " + "and they cannot contain each other" + ) + largest_distance = m + # Replace distance object with meters float + input[input.index(distance)] = m + + return input + + +MMWAVE_SETTINGS_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(DfrobotSen0395Component), + cv.Optional(CONF_FACTORY_RESET): cv.templatable(cv.boolean), + cv.Optional(CONF_DETECTION_SEGMENTS): range_segment_list, + cv.Optional(CONF_OUTPUT_LATENCY): { + cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable( + cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(seconds=1638.375)), + ) + ), + cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable( + cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(seconds=1638.375)), + ) + ), + }, + cv.Optional(CONF_SENSITIVITY): cv.templatable(cv.int_range(min=0, max=9)), + } +).add_extra( + cv.has_at_least_one_key( + CONF_FACTORY_RESET, + CONF_DETECTION_SEGMENTS, + CONF_OUTPUT_LATENCY, + CONF_SENSITIVITY, + ) +) + + +@automation.register_action( + "dfrobot_sen0395.settings", + DfrobotSen0395SettingsAction, + MMWAVE_SETTINGS_SCHEMA, +) +async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + if factory_reset_config := config.get(CONF_FACTORY_RESET): + template_ = await cg.templatable(factory_reset_config, args, int) + cg.add(var.set_factory_reset(template_)) + + if CONF_DETECTION_SEGMENTS in config: + segments = config[CONF_DETECTION_SEGMENTS] + + if len(segments) >= 2: + template_ = await cg.templatable(segments[0], args, float) + cg.add(var.set_det_min1(template_)) + template_ = await cg.templatable(segments[1], args, float) + cg.add(var.set_det_max1(template_)) + if len(segments) >= 4: + template_ = await cg.templatable(segments[2], args, float) + cg.add(var.set_det_min2(template_)) + template_ = await cg.templatable(segments[3], args, float) + cg.add(var.set_det_max2(template_)) + if len(segments) >= 6: + template_ = await cg.templatable(segments[4], args, float) + cg.add(var.set_det_min3(template_)) + template_ = await cg.templatable(segments[5], args, float) + cg.add(var.set_det_max3(template_)) + if len(segments) >= 8: + template_ = await cg.templatable(segments[6], args, float) + cg.add(var.set_det_min4(template_)) + template_ = await cg.templatable(segments[7], args, float) + cg.add(var.set_det_max4(template_)) + if CONF_OUTPUT_LATENCY in config: + template_ = await cg.templatable( + config[CONF_OUTPUT_LATENCY][CONF_DELAY_AFTER_DETECT], args, float + ) + if isinstance(template_, cv.TimePeriod): + template_ = template_.total_milliseconds / 1000 + cg.add(var.set_delay_after_detect(template_)) + + template_ = await cg.templatable( + config[CONF_OUTPUT_LATENCY][CONF_DELAY_AFTER_DISAPPEAR], args, float + ) + if isinstance(template_, cv.TimePeriod): + template_ = template_.total_milliseconds / 1000 + cg.add(var.set_delay_after_disappear(template_)) + if CONF_SENSITIVITY in config: + template_ = await cg.templatable(config[CONF_SENSITIVITY], args, int) + cg.add(var.set_sensitivity(template_)) + + return var diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h new file mode 100644 index 0000000000..3f69e482b7 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -0,0 +1,89 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include "dfrobot_sen0395.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +template +class DfrobotSen0395ResetAction : public Action, public Parented { + public: + void play(Ts... x) { this->parent_->enqueue(make_unique()); } +}; + +template +class DfrobotSen0395SettingsAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(int8_t, factory_reset) + TEMPLATABLE_VALUE(int8_t, start_after_power_on) + TEMPLATABLE_VALUE(int8_t, turn_on_led) + TEMPLATABLE_VALUE(int8_t, presence_via_uart) + TEMPLATABLE_VALUE(int8_t, sensitivity) + TEMPLATABLE_VALUE(float, delay_after_detect) + TEMPLATABLE_VALUE(float, delay_after_disappear) + TEMPLATABLE_VALUE(float, det_min1) + TEMPLATABLE_VALUE(float, det_max1) + TEMPLATABLE_VALUE(float, det_min2) + TEMPLATABLE_VALUE(float, det_max2) + TEMPLATABLE_VALUE(float, det_min3) + TEMPLATABLE_VALUE(float, det_max3) + TEMPLATABLE_VALUE(float, det_min4) + TEMPLATABLE_VALUE(float, det_max4) + + void play(Ts... x) { + this->parent_->enqueue(make_unique(0)); + if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { + this->parent_->enqueue(make_unique()); + } + if (this->det_min1_.has_value() && this->det_max1_.has_value()) { + if (this->det_min1_.value() >= 0 && this->det_max1_.value() >= 0) { + this->parent_->enqueue(make_unique( + this->det_min1_.value_or(-1), this->det_max1_.value_or(-1), this->det_min2_.value_or(-1), + this->det_max2_.value_or(-1), this->det_min3_.value_or(-1), this->det_max3_.value_or(-1), + this->det_min4_.value_or(-1), this->det_max4_.value_or(-1))); + } + } + if (this->delay_after_detect_.has_value() && this->delay_after_disappear_.has_value()) { + float detect = this->delay_after_detect_.value(x...); + float disappear = this->delay_after_disappear_.value(x...); + if (detect >= 0 && disappear >= 0) { + this->parent_->enqueue(make_unique(detect, disappear)); + } + } + if (this->start_after_power_on_.has_value()) { + int8_t val = this->start_after_power_on_.value(x...); + if (val >= 0) { + this->parent_->enqueue(make_unique(val)); + } + } + if (this->turn_on_led_.has_value()) { + int8_t val = this->turn_on_led_.value(x...); + if (val >= 0) { + this->parent_->enqueue(make_unique(val)); + } + } + if (this->presence_via_uart_.has_value()) { + int8_t val = this->presence_via_uart_.value(x...); + if (val >= 0) { + this->parent_->enqueue(make_unique(val)); + } + } + if (this->sensitivity_.has_value()) { + int8_t val = this->sensitivity_.value(x...); + if (val >= 0) { + if (val > 9) { + val = 9; + } + this->parent_->enqueue(make_unique(val)); + } + } + this->parent_->enqueue(make_unique()); + this->parent_->enqueue(make_unique(1)); + } +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/binary_sensor.py b/esphome/components/dfrobot_sen0395/binary_sensor.py new file mode 100644 index 0000000000..2fd0510476 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/binary_sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import DEVICE_CLASS_MOTION +from . import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component + +DEPENDENCIES = ["dfrobot_sen0395"] + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION +).extend( + { + cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID]) + binary_sens = await binary_sensor.new_binary_sensor(config) + + cg.add(parent.set_detected_binary_sensor(binary_sens)) diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp new file mode 100644 index 0000000000..2c60fb3449 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -0,0 +1,321 @@ +#include "commands.h" + +#include + +#include "esphome/core/log.h" + +#include "dfrobot_sen0395.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +static const char *const TAG = "dfrobot_sen0395.commands"; + +uint8_t Command::execute(DfrobotSen0395Component *parent) { + this->parent_ = parent; + if (this->cmd_sent_) { + if (this->parent_->read_message_()) { + std::string message(this->parent_->read_buffer_); + if (message.rfind("is not recognized as a CLI command") != std::string::npos) { + ESP_LOGD(TAG, "Command not recognized properly by sensor"); + if (this->retries_left_ > 0) { + this->retries_left_ -= 1; + this->cmd_sent_ = false; + ESP_LOGD(TAG, "Retrying..."); + return 0; + } else { + this->parent_->find_prompt_(); + return 1; // Command done + } + } + uint8_t rc = on_message(message); + if (rc == 2) { + if (this->retries_left_ > 0) { + this->retries_left_ -= 1; + this->cmd_sent_ = false; + ESP_LOGD(TAG, "Retrying..."); + return 0; + } else { + this->parent_->find_prompt_(); + return 1; // Command done + } + } else if (rc == 0) { + return 0; + } else { + this->parent_->find_prompt_(); + return 1; + } + } + if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) { + ESP_LOGD(TAG, "Command timeout"); + if (this->retries_left_ > 0) { + this->retries_left_ -= 1; + this->cmd_sent_ = false; + ESP_LOGD(TAG, "Retrying..."); + } else { + return 1; // Command done + } + } + } else if (this->parent_->send_cmd_(this->cmd_.c_str(), this->cmd_duration_ms_)) { + this->cmd_sent_ = true; + } + return 0; // Command not done yet +} + +uint8_t ReadStateCommand::execute(DfrobotSen0395Component *parent) { + this->parent_ = parent; + if (this->parent_->read_message_()) { + std::string message(this->parent_->read_buffer_); + if (message.rfind("$JYBSS,0, , , *") != std::string::npos) { + this->parent_->set_detected_(false); + this->parent_->set_active(true); + return 1; // Command done + } else if (message.rfind("$JYBSS,1, , , *") != std::string::npos) { + this->parent_->set_detected_(true); + this->parent_->set_active(true); + return 1; // Command done + } + } + if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) { + return 1; // Command done, timeout + } + return 0; // Command not done yet. +} + +uint8_t ReadStateCommand::on_message(std::string &message) { return 1; } + +uint8_t PowerCommand::on_message(std::string &message) { + if (message == "sensor stopped already") { + this->parent_->set_active(false); + ESP_LOGI(TAG, "Stopped sensor (already stopped)"); + return 1; // Command done + } else if (message == "sensor started already") { + this->parent_->set_active(true); + ESP_LOGI(TAG, "Started sensor (already started)"); + return 1; // Command done + } else if (message == "new parameter isn't save, can't startSensor") { + this->parent_->set_active(false); + ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)"); + return 1; // Command done + } else if (message == "Done") { + this->parent_->set_active(this->power_on_); + if (this->power_on_) { + ESP_LOGI(TAG, "Started sensor"); + } else { + ESP_LOGI(TAG, "Stopped sensor"); + } + return 1; // Command done + } + return 0; // Command not done yet. +} + +DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, + float min4, float max4) { + // TODO: Print warning when values are rounded + if (min1 < 0 || max1 < 0) { + this->min1_ = min1 = 0; + this->max1_ = max1 = 0; + this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = + this->max4_ = max4 = -1; + + ESP_LOGW(TAG, "DetRangeCfgCommand invalid input parameters. Using range config 0 0."); + + this->cmd_ = "detRangeCfg -1 0 0"; + } else if (min2 < 0 || max2 < 0) { + this->min1_ = min1 = round(min1 / 0.15) * 0.15; + this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = + this->max4_ = max4 = -1; + + this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15); + } else if (min3 < 0 || max3 < 0) { + this->min1_ = min1 = round(min1 / 0.15) * 0.15; + this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min2_ = min2 = round(min2 / 0.15) * 0.15; + this->max2_ = max2 = round(max2 / 0.15) * 0.15; + this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1; + + this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15); + } else if (min4 < 0 || max4 < 0) { + this->min1_ = min1 = round(min1 / 0.15) * 0.15; + this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min2_ = min2 = round(min2 / 0.15) * 0.15; + this->max2_ = max2 = round(max2 / 0.15) * 0.15; + this->min3_ = min3 = round(min3 / 0.15) * 0.15; + this->max3_ = max3 = round(max3 / 0.15) * 0.15; + this->min4_ = min4 = this->max4_ = max4 = -1; + + this->cmd_ = str_sprintf("detRangeCfg -1 " + "%.0f %.0f %.0f %.0f %.0f %.0f", + min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15); + } else { + this->min1_ = min1 = round(min1 / 0.15) * 0.15; + this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min2_ = min2 = round(min2 / 0.15) * 0.15; + this->max2_ = max2 = round(max2 / 0.15) * 0.15; + this->min3_ = min3 = round(min3 / 0.15) * 0.15; + this->max3_ = max3 = round(max3 / 0.15) * 0.15; + this->min4_ = min4 = round(min4 / 0.15) * 0.15; + this->max4_ = max4 = round(max4 / 0.15) * 0.15; + + this->cmd_ = str_sprintf("detRangeCfg -1 " + "%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", + min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, + max4 / 0.15); + } + + this->min1_ = min1; + this->max1_ = max1; + this->min2_ = min2; + this->max2_ = max2; + this->min3_ = min3; + this->max3_ = max3; + this->min4_ = min4; + this->max4_ = max4; +}; + +uint8_t DetRangeCfgCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + 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_); + if (this->min2_ >= 0 && this->max2_ >= 0) { + ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); + } + if (this->min3_ >= 0 && this->max3_ >= 0) { + ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); + } + if (this->min4_ >= 0 && this->max4_ >= 0) { + ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); + } + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet. +} + +SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) { + delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f; + delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f; + this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f); + this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f); + this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_); +}; + +uint8_t SetLatencyCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + 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_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t SensorCfgStartCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot configure sensor startup behavior. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Updated sensor startup behavior:"); + if (startup_mode_) { + this->parent_->set_start_after_boot(true); + ESP_LOGI(TAG, "Sensor will start automatically after power-on."); + } else { + this->parent_->set_start_after_boot(false); + ESP_LOGI(TAG, "Sensor needs to be started manually after power-on."); + } + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t FactoryResetCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot factory reset. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Sensor factory reset done."); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t ResetSystemCommand::on_message(std::string &message) { + if (message == "leapMMW:/>") { + ESP_LOGI(TAG, "Restarted sensor."); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t SaveCfgCommand::on_message(std::string &message) { + if (message == "no parameter has changed") { + ESP_LOGI(TAG, "Not saving config (no parameter changed)."); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor."); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t LedModeCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot set led mode. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Set led mode done."); + if (this->active_) { + this->parent_->set_led_active(true); + ESP_LOGI(TAG, "Sensor LED will blink."); + } else { + this->parent_->set_led_active(false); + ESP_LOGI(TAG, "Turned off LED."); + } + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t UartOutputCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot set uart output mode. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Set uart mode done."); + if (this->active_) { + this->parent_->set_uart_presence_active(true); + ESP_LOGI(TAG, "Presence information is sent via UART and GPIO."); + } else { + this->parent_->set_uart_presence_active(false); + ESP_LOGI(TAG, "Presence information is only sent via GPIO."); + } + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t SensitivityCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot set sensitivity. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", this->sensitivity_); + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h new file mode 100644 index 0000000000..cf3ba50be0 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -0,0 +1,156 @@ +#pragma once + +#include +#include + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +class DfrobotSen0395Component; + +// Use command queue and time stamps to avoid blocking. +// When component has run time, check if minimum time (1s) between +// commands has passed. After that run a command from the queue. +class Command { + public: + virtual ~Command() = default; + virtual uint8_t execute(DfrobotSen0395Component *parent); + virtual uint8_t on_message(std::string &message) = 0; + + protected: + DfrobotSen0395Component *parent_{nullptr}; + std::string cmd_; + bool cmd_sent_{false}; + int8_t retries_left_{2}; + uint32_t cmd_duration_ms_{1000}; + uint32_t timeout_ms_{1500}; +}; + +class ReadStateCommand : public Command { + public: + uint8_t execute(DfrobotSen0395Component *parent) override; + uint8_t on_message(std::string &message) override; + + protected: + uint32_t timeout_ms_{500}; +}; + +class PowerCommand : public Command { + public: + PowerCommand(bool power_on) : power_on_(power_on) { + if (power_on) { + cmd_ = "sensorStart"; + } else { + cmd_ = "sensorStop"; + } + }; + uint8_t on_message(std::string &message) override; + + protected: + bool power_on_; +}; + +class DetRangeCfgCommand : public Command { + public: + DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, float min4, float max4); + uint8_t on_message(std::string &message) override; + + protected: + float min1_, max1_, min2_, max2_, min3_, max3_, min4_, max4_; + // TODO: Set min max values in component, so they can be published as sensor. +}; + +class SetLatencyCommand : public Command { + public: + SetLatencyCommand(float delay_after_detection, float delay_after_disappear); + uint8_t on_message(std::string &message) override; + + protected: + float delay_after_detection_; + float delay_after_disappear_; +}; + +class SensorCfgStartCommand : public Command { + public: + SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) { + char tmp_cmd[20] = {0}; + sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode); + cmd_ = std::string(tmp_cmd); + } + uint8_t on_message(std::string &message) override; + + protected: + bool startup_mode_; +}; + +class FactoryResetCommand : public Command { + public: + FactoryResetCommand() { cmd_ = "factoryReset 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; }; + uint8_t on_message(std::string &message) override; +}; + +class ResetSystemCommand : public Command { + public: + ResetSystemCommand() { cmd_ = "resetSystem"; } + uint8_t on_message(std::string &message) override; +}; + +class SaveCfgCommand : public Command { + public: + SaveCfgCommand() { cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; } + uint8_t on_message(std::string &message) override; + + protected: + uint32_t cmd_duration_ms_{3000}; + uint32_t timeout_ms_{3500}; +}; + +class LedModeCommand : public Command { + public: + LedModeCommand(bool active) : active_(active) { + if (active) { + cmd_ = "setLedMode 1 0"; + } else { + cmd_ = "setLedMode 1 1"; + } + }; + uint8_t on_message(std::string &message) override; + + protected: + bool active_; +}; + +class UartOutputCommand : public Command { + public: + UartOutputCommand(bool active) : active_(active) { + if (active) { + cmd_ = "setUartOutput 1 1"; + } else { + cmd_ = "setUartOutput 1 0"; + } + }; + uint8_t on_message(std::string &message) override; + + protected: + bool active_; +}; + +class SensitivityCommand : public Command { + public: + SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) { + if (sensitivity > 9) + sensitivity_ = sensitivity = 9; + char tmp_cmd[20] = {0}; + sprintf(tmp_cmd, "setSensitivity %d", sensitivity); + cmd_ = std::string(tmp_cmd); + }; + uint8_t on_message(std::string &message) override; + + protected: + uint8_t sensitivity_; +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp new file mode 100644 index 0000000000..f8ef6c7138 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp @@ -0,0 +1,142 @@ +#include "dfrobot_sen0395.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +static const char *const TAG = "dfrobot_sen0395"; +const char ASCII_CR = 0x0D; +const char ASCII_LF = 0x0A; + +void DfrobotSen0395Component::dump_config() { + ESP_LOGCONFIG(TAG, "Dfrobot Mmwave Radar:"); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Registered", this->detected_binary_sensor_); +#endif +#ifdef USE_SWITCH + LOG_SWITCH(" ", "Sensor Active Switch", this->sensor_active_switch_); + LOG_SWITCH(" ", "Turn on LED Switch", this->turn_on_led_switch_); + LOG_SWITCH(" ", "Presence via UART Switch", this->presence_via_uart_switch_); + LOG_SWITCH(" ", "Start after Boot Switch", this->start_after_boot_switch_); +#endif +} + +void DfrobotSen0395Component::loop() { + if (cmd_queue_.is_empty()) { + // Command queue empty. Read sensor state. + cmd_queue_.enqueue(make_unique()); + } + + // Commands are non-blocking and need to be called repeatedly. + if (cmd_queue_.process(this)) { + // Dequeue if command is done + cmd_queue_.dequeue(); + } +} + +int8_t DfrobotSen0395Component::enqueue(std::unique_ptr cmd) { + return cmd_queue_.enqueue(std::move(cmd)); // Transfer ownership using std::move +} + +uint8_t DfrobotSen0395Component::read_message_() { + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte); + + if (byte == ASCII_CR) + continue; + if (byte >= 0x7F) + byte = '?'; // needs to be valid utf8 string for log functions. + this->read_buffer_[this->read_pos_] = byte; + + if (this->read_pos_ == 9 && byte == '>') + this->read_buffer_[++this->read_pos_] = ASCII_LF; + + if (this->read_buffer_[this->read_pos_] == ASCII_LF) { + this->read_buffer_[this->read_pos_] = 0; + this->read_pos_ = 0; + ESP_LOGV(TAG, "Message: %s", this->read_buffer_); + return 1; // Full message in buffer + } else { + this->read_pos_++; + } + } + return 0; // No full message yet +} + +uint8_t DfrobotSen0395Component::find_prompt_() { + if (this->read_message_()) { + std::string message(this->read_buffer_); + if (message.rfind("leapMMW:/>") != std::string::npos) { + return 1; // Prompt found + } + } + return 0; // Not found yet +} + +uint8_t DfrobotSen0395Component::send_cmd_(const char *cmd, uint32_t duration) { + // The interval between two commands must be larger than the specified duration (in ms). + if (millis() - ts_last_cmd_sent_ > duration) { + this->write_str(cmd); + ts_last_cmd_sent_ = millis(); + return 1; // Command sent + } + // Could not send command yet as command duration did not fully pass yet. + return 0; +} + +void DfrobotSen0395Component::set_detected_(bool detected) { + this->detected_ = detected; +#ifdef USE_BINARY_SENSOR + if (this->detected_binary_sensor_ != nullptr) + this->detected_binary_sensor_->publish_state(detected); +#endif +} + +int8_t CircularCommandQueue::enqueue(std::unique_ptr cmd) { + if (this->is_full()) { + ESP_LOGE(TAG, "Command queue is full"); + return -1; + } else if (this->is_empty()) + front_++; + rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE; + commands_[rear_] = std::move(cmd); // Transfer ownership using std::move + return 1; +} + +std::unique_ptr CircularCommandQueue::dequeue() { + if (this->is_empty()) + return nullptr; + std::unique_ptr dequeued_cmd = std::move(commands_[front_]); + if (front_ == rear_) { + front_ = -1; + rear_ = -1; + } else + front_ = (front_ + 1) % COMMAND_QUEUE_SIZE; + + return dequeued_cmd; +} + +bool CircularCommandQueue::is_empty() { return front_ == -1; } + +bool CircularCommandQueue::is_full() { return (rear_ + 1) % COMMAND_QUEUE_SIZE == front_; } + +// Run execute method of first in line command. +// Execute is non-blocking and has to be called until it returns 1. +uint8_t CircularCommandQueue::process(DfrobotSen0395Component *parent) { + if (!is_empty()) { + return commands_[front_]->execute(parent); + } else { + return 1; + } +} + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h new file mode 100644 index 0000000000..d3b2ecedc3 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h @@ -0,0 +1,125 @@ +#pragma once + +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif + +#include "commands.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255; + +// forward declaration due to circular dependency +class DfrobotSen0395Component; + +static const uint8_t COMMAND_QUEUE_SIZE = 20; + +class CircularCommandQueue { + public: + int8_t enqueue(std::unique_ptr cmd); + std::unique_ptr dequeue(); + bool is_empty(); + bool is_full(); + uint8_t process(DfrobotSen0395Component *parent); + + protected: + int front_{-1}; + int rear_{-1}; + std::unique_ptr commands_[COMMAND_QUEUE_SIZE]; +}; + +class DfrobotSen0395Component : public uart::UARTDevice, public Component { +#ifdef USE_SWITCH + SUB_SWITCH(sensor_active) + SUB_SWITCH(turn_on_led) + SUB_SWITCH(presence_via_uart) + SUB_SWITCH(start_after_boot) +#endif + + public: + void dump_config() override; + void loop() override; + void set_active(bool active) { + if (active != active_) { +#ifdef USE_SWITCH + if (this->sensor_active_switch_ != nullptr) + this->sensor_active_switch_->publish_state(active); +#endif + active_ = active; + } + } + bool is_active() { return active_; } + + void set_led_active(bool active) { + if (led_active_ != active) { +#ifdef USE_SWITCH + if (this->turn_on_led_switch_ != nullptr) + this->turn_on_led_switch_->publish_state(active); +#endif + led_active_ = active; + } + } + bool is_led_active() { return led_active_; } + + void set_uart_presence_active(bool active) { + uart_presence_active_ = active; +#ifdef USE_SWITCH + if (this->presence_via_uart_switch_ != nullptr) + this->presence_via_uart_switch_->publish_state(active); +#endif + } + bool is_uart_presence_active() { return uart_presence_active_; } + + void set_start_after_boot(bool start) { + start_after_boot_ = start; +#ifdef USE_SWITCH + if (this->start_after_boot_switch_ != nullptr) + this->start_after_boot_switch_->publish_state(start); +#endif + } + bool does_start_after_boot() { return start_after_boot_; } + +#ifdef USE_BINARY_SENSOR + void set_detected_binary_sensor(binary_sensor::BinarySensor *detected_binary_sensor) { + detected_binary_sensor_ = detected_binary_sensor; + } +#endif + + int8_t enqueue(std::unique_ptr cmd); + + protected: +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *detected_binary_sensor_{nullptr}; +#endif + + bool detected_{false}; + bool active_{false}; + bool led_active_{false}; + bool uart_presence_active_{false}; + bool start_after_boot_{false}; + char read_buffer_[MMWAVE_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + CircularCommandQueue cmd_queue_; + uint32_t ts_last_cmd_sent_{0}; + + uint8_t read_message_(); + uint8_t find_prompt_(); + uint8_t send_cmd_(const char *cmd, uint32_t duration); + + void set_detected_(bool detected); + + friend class Command; + friend class ReadStateCommand; +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/switch/__init__.py b/esphome/components/dfrobot_sen0395/switch/__init__.py new file mode 100644 index 0000000000..b1c35d27ac --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/__init__.py @@ -0,0 +1,65 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ENTITY_CATEGORY_CONFIG, CONF_TYPE + +from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component + + +DEPENDENCIES = ["dfrobot_sen0395"] + +dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395") +DfrobotSen0395Switch = dfrobot_sen0395_ns.class_( + "DfrobotSen0395Switch", + switch.Switch, + cg.Component, + cg.Parented.template(DfrobotSen0395Component), +) + +Sen0395PowerSwitch = dfrobot_sen0395_ns.class_( + "Sen0395PowerSwitch", DfrobotSen0395Switch +) +Sen0395LedSwitch = dfrobot_sen0395_ns.class_("Sen0395LedSwitch", DfrobotSen0395Switch) +Sen0395UartPresenceSwitch = dfrobot_sen0395_ns.class_( + "Sen0395UartPresenceSwitch", DfrobotSen0395Switch +) +Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_( + "Sen0395StartAfterBootSwitch", DfrobotSen0395Switch +) + +_SWITCH_SCHEMA = ( + switch.switch_schema( + entity_category=ENTITY_CATEGORY_CONFIG, + ) + .extend( + { + cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.typed_schema( + { + "sensor_active": _SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(Sen0395PowerSwitch)} + ), + "turn_on_led": _SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(Sen0395LedSwitch)} + ), + "presence_via_uart": _SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(Sen0395UartPresenceSwitch)} + ), + "start_after_boot": _SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(Sen0395StartAfterBootSwitch)} + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID]) + var = await switch.new_switch(config) + await cg.register_component(var, config) + await cg.register_parented(var, parent) + cg.add(getattr(parent, f"set_{config[CONF_TYPE]}_switch")(var)) diff --git a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp new file mode 100644 index 0000000000..ca72d94531 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp @@ -0,0 +1,48 @@ +#include "dfrobot_sen0395_switch.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique(state)); } + +void Sen0395LedSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +void Sen0395UartPresenceSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +void Sen0395StartAfterBootSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h new file mode 100644 index 0000000000..ab32d81dd8 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +#include "../dfrobot_sen0395.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +class DfrobotSen0395Switch : public switch_::Switch, public Component, public Parented {}; + +class Sen0395PowerSwitch : public DfrobotSen0395Switch { + public: + void write_state(bool state) override; +}; + +class Sen0395LedSwitch : public DfrobotSen0395Switch { + public: + void write_state(bool state) override; +}; + +class Sen0395UartPresenceSwitch : public DfrobotSen0395Switch { + public: + void write_state(bool state) override; +}; + +class Sen0395StartAfterBootSwitch : public DfrobotSen0395Switch { + public: + void write_state(bool state) override; +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index c70b227330..db1c851d5f 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -86,12 +86,17 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r 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_AM2302) { + } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) { delayMicroseconds(1000); } else { delayMicroseconds(800); @@ -217,8 +222,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF); - if (this->model_ != DHT_MODEL_DHT22_TYPE2 && (raw_temperature & 0x8000) != 0) - raw_temperature = ~(raw_temperature & 0x7FFF); + if (raw_temperature & 0x8000) { + if (!(raw_temperature & 0x4000)) + raw_temperature = ~(raw_temperature & 0x7FFF); + } else if (raw_temperature & 0x800) { + raw_temperature |= 0xf000; + } if (raw_temperature == 1 && raw_humidity == 10) { if (report_errors) { diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index f3a29f9ce9..327e8a4f5c 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -11,6 +11,7 @@ enum DHTModel { DHT_MODEL_AUTO_DETECT = 0, DHT_MODEL_DHT11, DHT_MODEL_DHT22, + DHT_MODEL_AM2120, DHT_MODEL_AM2302, DHT_MODEL_RHT03, DHT_MODEL_SI7021, @@ -27,6 +28,7 @@ class DHT : public PollingComponent { * - DHT_MODEL_AUTO_DETECT (default) * - DHT_MODEL_DHT11 * - DHT_MODEL_DHT22 + * - DHT_MODEL_AM2120 * - DHT_MODEL_AM2302 * - DHT_MODEL_RHT03 * - DHT_MODEL_SI7021 diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index cd1886728e..da92a97e1f 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -23,6 +23,7 @@ DHT_MODELS = { "AUTO_DETECT": DHTModel.DHT_MODEL_AUTO_DETECT, "DHT11": DHTModel.DHT_MODEL_DHT11, "DHT22": DHTModel.DHT_MODEL_DHT22, + "AM2120": DHTModel.DHT_MODEL_AM2120, "AM2302": DHTModel.DHT_MODEL_AM2302, "RHT03": DHTModel.DHT_MODEL_RHT03, "SI7021": DHTModel.DHT_MODEL_SI7021, diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 0d403f99f0..32a8b3b090 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -1,15 +1,15 @@ +from esphome import automation, core +from esphome.automation import maybe_simple_id import esphome.codegen as cg import esphome.config_validation as cv -from esphome import core, automation -from esphome.automation import maybe_simple_id from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, + CONF_FROM, CONF_ID, CONF_LAMBDA, - CONF_PAGES, CONF_PAGE_ID, + CONF_PAGES, CONF_ROTATION, - CONF_FROM, CONF_TO, CONF_TRIGGER_ID, ) @@ -18,10 +18,11 @@ from esphome.core import coroutine_with_priority IS_PLATFORM_COMPONENT = True display_ns = cg.esphome_ns.namespace("display") -DisplayBuffer = display_ns.class_("DisplayBuffer") +Display = display_ns.class_("Display", cg.PollingComponent) +DisplayBuffer = display_ns.class_("DisplayBuffer", Display) DisplayPage = display_ns.class_("DisplayPage") DisplayPagePtr = DisplayPage.operator("ptr") -DisplayBufferRef = DisplayBuffer.operator("ref") +DisplayRef = Display.operator("ref") DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action) DisplayPageShowNextAction = display_ns.class_( "DisplayPageShowNextAction", automation.Action @@ -37,6 +38,7 @@ DisplayOnPageChangeTrigger = display_ns.class_( ) CONF_ON_PAGE_CHANGE = "on_page_change" +CONF_SHOW_TEST_CARD = "show_test_card" DISPLAY_ROTATIONS = { 0: display_ns.DISPLAY_ROTATION_0_DEGREES, @@ -57,7 +59,7 @@ BASIC_DISPLAY_SCHEMA = cv.Schema( { cv.Optional(CONF_LAMBDA): cv.lambda_, } -) +).extend(cv.polling_component_schema("1s")) FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( { @@ -81,6 +83,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( } ), cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, + cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, } ) @@ -96,7 +99,7 @@ async def setup_display_core_(var, config): pages = [] for conf in config[CONF_PAGES]: lambda_ = await cg.process_lambda( - conf[CONF_LAMBDA], [(DisplayBufferRef, "it")], return_type=cg.void + conf[CONF_LAMBDA], [(DisplayRef, "it")], return_type=cg.void ) page = cg.new_Pvariable(conf[CONF_ID], lambda_) pages.append(page) @@ -112,9 +115,12 @@ async def setup_display_core_(var, config): await automation.build_automation( trigger, [(DisplayPagePtr, "from"), (DisplayPagePtr, "to")], conf ) + if config.get(CONF_SHOW_TEST_CARD): + cg.add(var.show_test_card()) async def register_display(var, config): + await cg.register_component(var, config) await setup_display_core_(var, config) @@ -143,7 +149,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args): DisplayPageShowNextAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), + cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) @@ -157,7 +163,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args): DisplayPageShowPrevAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), + cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) @@ -171,7 +177,7 @@ async def display_page_show_previous_to_code(config, action_id, template_arg, ar DisplayIsDisplayingPageCondition, cv.maybe_simple_value( { - cv.GenerateID(CONF_ID): cv.use_id(DisplayBuffer), + cv.GenerateID(CONF_ID): cv.use_id(Display), cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage), }, key=CONF_PAGE_ID, @@ -189,3 +195,4 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(display_ns.using) + cg.add_define("USE_DISPLAY") diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp new file mode 100644 index 0000000000..145a4f5278 --- /dev/null +++ b/esphome/components/display/display.cpp @@ -0,0 +1,852 @@ +#include "display.h" +#include "display_color_utils.h" +#include +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace display { + +static const char *const TAG = "display"; + +const Color COLOR_OFF(0, 0, 0, 0); +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; + int32_t err = dx + dy; + + while (true) { + this->draw_pixel_at(x1, y1, color); + if (x1 == x2 && y1 == y2) + break; + int32_t e2 = 2 * err; + if (e2 >= dy) { + err += dy; + x1 += sx; + } + if (e2 <= dx) { + err += dx; + y1 += sy; + } + } +} + +void Display::line_at_angle(int x, int y, int angle, int length, Color color) { + this->line_at_angle(x, y, angle, 0, length, color); +} + +void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color) { + // Calculate start and end points + int x1 = (start_radius * cos(angle * M_PI / 180)) + x; + int y1 = (start_radius * sin(angle * M_PI / 180)) + y; + int x2 = (stop_radius * cos(angle * M_PI / 180)) + x; + int y2 = (stop_radius * sin(angle * M_PI / 180)) + y; + + // Draw line + this->line(x1, y1, x2, y2, color); +} + +void Display::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) { + size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels + uint32_t color_value; + for (int y = 0; y != h; y++) { + size_t source_idx = (y_offset + y) * line_stride + x_offset; + size_t source_idx_mod; + for (int x = 0; x != w; x++, source_idx++) { + switch (bitness) { + default: + color_value = ptr[source_idx]; + break; + case COLOR_BITNESS_565: + source_idx_mod = source_idx * 2; + if (big_endian) { + color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1]; + } else { + color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8); + } + break; + case COLOR_BITNESS_888: + source_idx_mod = source_idx * 3; + if (big_endian) { + color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2]; + } else { + color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16); + } + break; + } + this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness)); + } + } +} + +void HOT Display::horizontal_line(int x, int y, int width, Color color) { + // Future: Could be made more efficient by manipulating buffer directly in certain rotations. + 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; + int err = 2 - 2 * radius; + int e2; + + do { + this->draw_pixel_at(center_x - dx, center_xy + dy, color); + this->draw_pixel_at(center_x + dx, center_xy + dy, color); + this->draw_pixel_at(center_x + dx, center_xy - dy, color); + this->draw_pixel_at(center_x - dx, center_xy - dy, color); + e2 = err; + if (e2 < dy) { + err += ++dy * 2 + 1; + if (-dx == dy && e2 <= dx) { + e2 = 0; + } + } + if (e2 > dx) { + err += ++dx * 2 + 1; + } + } 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; + int err = 2 - 2 * radius; + int e2; + + do { + this->draw_pixel_at(center_x - dx, center_y + dy, color); + this->draw_pixel_at(center_x + dx, center_y + dy, color); + this->draw_pixel_at(center_x + dx, center_y - dy, color); + this->draw_pixel_at(center_x - dx, center_y - dy, color); + int hline_width = 2 * (-dx) + 1; + this->horizontal_line(center_x + dx, center_y + dy, hline_width, color); + this->horizontal_line(center_x + dx, center_y - dy, hline_width, color); + e2 = err; + if (e2 < dy) { + err += ++dy * 2 + 1; + if (-dx == dy && e2 <= dx) { + e2 = 0; + } + } + if (e2 > dx) { + err += ++dx * 2 + 1; + } + } 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; + int dxmax = -int32_t(rmax), dxmin = -int32_t(rmin); + int dymax = 0, dymin = 0; + int errmax = 2 - 2 * rmax, errmin = 2 - 2 * rmin; + int e2max, e2min; + do { + // 8 dots for borders + this->draw_pixel_at(center_x - dxmax, center_y + dymax, color); + this->draw_pixel_at(center_x + dxmax, center_y + dymax, color); + this->draw_pixel_at(center_x - dxmin, center_y + dymin, color); + this->draw_pixel_at(center_x + dxmin, center_y + dymin, color); + this->draw_pixel_at(center_x + dxmax, center_y - dymax, color); + this->draw_pixel_at(center_x - dxmax, center_y - dymax, color); + this->draw_pixel_at(center_x + dxmin, center_y - dymin, color); + this->draw_pixel_at(center_x - dxmin, center_y - dymin, color); + if (dymin < rmin) { + // two parts - four lines + int hline_width = -(dxmax - dxmin) + 1; + this->horizontal_line(center_x + dxmax, center_y + dymax, hline_width, color); + this->horizontal_line(center_x - dxmin, center_y + dymax, hline_width, color); + this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color); + this->horizontal_line(center_x - dxmin, center_y - dymax, hline_width, color); + } else { + // one part - top and bottom + int hline_width = 2 * (-dxmax) + 1; + this->horizontal_line(center_x + dxmax, center_y + dymax, hline_width, color); + this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color); + } + e2max = errmax; + // tune external + if (e2max < dymax) { + errmax += ++dymax * 2 + 1; + if (-dxmax == dymax && e2max <= dxmax) { + e2max = 0; + } + } + if (e2max > dxmax) { + errmax += ++dxmax * 2 + 1; + } + // tune internal + while (dymin < dymax && dymin < rmin) { + e2min = errmin; + if (e2min < dymin) { + errmin += ++dymin * 2 + 1; + if (-dxmin == dymin && e2min <= dxmin) { + e2min = 0; + } + } + if (e2min > dxmin) { + errmin += ++dxmin * 2 + 1; + } + } + } 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; + int dxmax = -int32_t(rmax), dxmin = -int32_t(rmin), upd_dxmax, upd_dxmin; + int dymax = 0, dymin = 0; + int errmax = 2 - 2 * rmax, errmin = 2 - 2 * rmin; + int e2max, e2min; + progress = std::max(0, std::min(progress, 100)); // 0..100 + int draw_progress = progress > 50 ? (100 - progress) : progress; + float tan_a = (progress == 50) ? 65535 : tan(float(draw_progress) * M_PI / 100); // slope + + do { + // 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 + int lhline_width = -(dxmax - dxmin) + 1; + if (progress >= 50) { + if (float(dymax) < float(-dxmax) * tan_a) { + upd_dxmax = ceil(float(dymax) / tan_a); + } else { + upd_dxmax = -dxmax; + } + 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 + 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); + } + } else { + if (float(dymin) > float(-dxmin) * tan_a) { + upd_dxmin = ceil(float(dymin) / tan_a); + } else { + upd_dxmin = -dxmin; + } + lhline_width = -(dxmax + upd_dxmin) + 1; + if (!dymax) + this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border + if (lhline_width > 0) + this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); + } + } else { // top part + int hline_width = 2 * (-dxmax) + 1; + if (progress >= 50) { + if (dymax < float(-dxmax) * tan_a) { + upd_dxmax = ceil(float(dymax) / tan_a); + hline_width = -dxmax + upd_dxmax + 1; + } + } else { + if (dymax < float(-dxmax) * tan_a) { + upd_dxmax = ceil(float(dymax) / tan_a); + hline_width = -dxmax - upd_dxmax + 1; + } else + hline_width = 0; + } + if (hline_width > 0) + this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color); + } + e2max = errmax; + if (e2max < dymax) { + errmax += ++dymax * 2 + 1; + if (-dxmax == dymax && e2max <= dxmax) { + e2max = 0; + } + } + if (e2max > dxmax) { + errmax += ++dxmax * 2 + 1; + } + while (dymin <= dymax && dymin <= rmin && dxmin <= 0) { + this->draw_pixel_at(center_x + dxmin, center_y - dymin, color); + this->draw_pixel_at(center_x - dxmin, center_y - dymin, color); + e2min = errmin; + if (e2min < dymin) { + errmin += ++dymin * 2 + 1; + if (-dxmin == dymin && e2min <= dxmin) { + e2min = 0; + } + } + if (e2min > dxmin) { + errmin += ++dxmin * 2 + 1; + } + } + } 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; + *x1 = *x2, *y1 = *y2; + *x2 = x_temp, *y2 = y_temp; + } + if (*y1 > *y3) { + int x_temp = *x1, y_temp = *y1; + *x1 = *x3, *y1 = *y3; + *x3 = x_temp, *y3 = y_temp; + } + if (*y2 > *y3) { + int x_temp = *x2, y_temp = *y2; + *x2 = *x3, *y2 = *y3; + *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) + + // Initialize Bresenham's algorithm for side 1 + int s1_current_x = x1; + int s1_current_y = y1; + bool s1_axis_swap = false; + int s1_dx = abs(x2 - x1); + 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 + int tmp = s1_dx; + s1_dx = s1_dy; + s1_dy = tmp; + s1_axis_swap = true; + } + int s1_error = 2 * s1_dy - s1_dx; + + // Initialize Bresenham's algorithm for side 2 + int s2_current_x = x1; + int s2_current_y = y1; + bool s2_axis_swap = false; + int s2_dx = abs(x3 - x1); + 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 + int tmp = s2_dx; + s2_dx = s2_dy; + s2_dy = tmp; + s2_axis_swap = true; + } + int s2_error = 2 * s2_dy - s2_dx; + + // Iterate on side 1 and allow side 2 to be processed to match the advance of the y-axis. + for (int i = 0; i <= s1_dx; i++) { + if (s1_current_x <= s2_current_x) { + this->horizontal_line(s1_current_x, s1_current_y, s2_current_x - s1_current_x + 1, color); + } else { + this->horizontal_line(s2_current_x, s2_current_y, s1_current_x - s2_current_x + 1, color); + } + + // Bresenham's #1 + // Side 1 s1_current_x and s1_current_y calculation + while (s1_error >= 0) { + if (s1_axis_swap) { + s1_current_x += s1_sign_x; + } else { + s1_current_y += s1_sign_y; + } + s1_error = s1_error - 2 * s1_dx; + } + if (s1_axis_swap) { + s1_current_y += s1_sign_y; + } else { + s1_current_x += s1_sign_x; + } + s1_error = s1_error + 2 * s1_dy; + + // Bresenham's #2 + // Side 2 s2_current_x and s2_current_y calculation + while (s2_current_y != s1_current_y) { + while (s2_error >= 0) { + if (s2_axis_swap) { + s2_current_x += s2_sign_x; + } else { + s2_current_y += s2_sign_y; + } + s2_error = s2_error - 2 * s2_dx; + } + if (s2_axis_swap) { + s2_current_y += s2_sign_y; + } else { + s2_current_x += s2_sign_x; + } + s2_error = s2_error + 2 * s2_dy; + } + } +} +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 + 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 + 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 + 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) { + if (edges >= 2) { + // Given the orientation of the display component, an angle is measured clockwise from the x axis. + // For a regular polygon, the human reference would be the top of the polygon, + // hence we rotate the shape by 270° to orient the polygon up. + rotation_degrees += ROTATION_270_DEGREES; + // Convert the rotation to radians, easier to use in trigonometrical calculations + float rotation_radians = rotation_degrees * PI / 180; + // A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no + // additional rotation of the shape. + // A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal, + // this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the + // left side of the first horizontal edge. + rotation_radians -= (variation == VARIATION_FLAT_TOP) ? PI / edges : 0.0; + + float vertex_angle = ((float) vertex_id) / edges * 2 * PI + rotation_radians; + *vertex_x = (int) round(cos(vertex_angle) * radius) + center_x; + *vertex_y = (int) round(sin(vertex_angle) * radius) + center_y; + } +} + +void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, + float rotation_degrees, Color color, RegularPolygonDrawing drawing) { + if (edges >= 2) { + int previous_vertex_x, previous_vertex_y; + for (int current_vertex_id = 0; current_vertex_id <= edges; current_vertex_id++) { + 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 (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) { + this->line(previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color); + } + } + previous_vertex_x = current_vertex_x; + previous_vertex_y = current_vertex_y; + } + } +} +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); +} + +void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text, Color background) { + int x_start, y_start; + int width, height; + this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); + font->print(x_start, y_start, this, color, text, background); +} + +void Display::vprintf_(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, + va_list arg) { + char buffer[256]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + if (ret > 0) + this->print(x, y, font, color, align, buffer, background); +} + +void Display::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { + this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off); +} + +void Display::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) { + auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT))); + auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT))); + + switch (x_align) { + case ImageAlign::RIGHT: + x -= image->get_width(); + break; + case ImageAlign::CENTER_HORIZONTAL: + x -= image->get_width() / 2; + break; + case ImageAlign::LEFT: + default: + break; + } + + switch (y_align) { + case ImageAlign::BOTTOM: + y -= image->get_height(); + break; + case ImageAlign::CENTER_VERTICAL: + y -= image->get_height() / 2; + break; + case ImageAlign::TOP: + default: + break; + } + + image->draw(x, y, this, color_on, color_off); +} + +#ifdef USE_GRAPH +void Display::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); } +void Display::legend(int x, int y, graph::Graph *graph, Color color_on) { graph->draw_legend(this, x, y, color_on); } +#endif // USE_GRAPH + +#ifdef USE_QR_CODE +void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) { + qr_code->draw(this, x, y, color_on, scale); +} +#endif // USE_QR_CODE + +#ifdef USE_GRAPHICAL_DISPLAY_MENU +void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) { + Rect rect(x, y, width, height); + menu->draw(this, &rect); +} +#endif // USE_GRAPHICAL_DISPLAY_MENU + +void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, + int *width, int *height) { + int x_offset, baseline; + font->measure(text, width, &x_offset, &baseline, height); + + auto x_align = TextAlign(int(align) & 0x18); + auto y_align = TextAlign(int(align) & 0x07); + + switch (x_align) { + case TextAlign::RIGHT: + *x1 = x - *width; + break; + case TextAlign::CENTER_HORIZONTAL: + *x1 = x - (*width) / 2; + break; + case TextAlign::LEFT: + default: + // LEFT + *x1 = x; + break; + } + + switch (y_align) { + case TextAlign::BOTTOM: + *y1 = y - *height; + break; + case TextAlign::BASELINE: + *y1 = y - baseline; + break; + case TextAlign::CENTER_VERTICAL: + *y1 = y - (*height) / 2; + break; + case TextAlign::TOP: + default: + *y1 = y; + 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; + va_start(arg, format); + 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); + + for (uint32_t i = 0; i < pages.size() - 1; i++) { + pages[i]->set_next(pages[i + 1]); + pages[i + 1]->set_prev(pages[i]); + } + pages[0]->set_prev(pages[pages.size() - 1]); + 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; + if (this->previous_page_ != this->page_) { + for (auto *t : on_page_change_triggers_) + 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(); + } + if (this->show_test_card_) { + this->test_card(); + } else if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } + 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, TextAlign align, const char *format, ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + this->print(x, y, font, color, align, buffer); +} +void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) { + this->strftime(x, y, font, color, 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, align, format, time); +} +void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) { + this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); +} + +void Display::start_clipping(Rect rect) { + if (!this->clipping_rectangle_.empty()) { + Rect r = this->clipping_rectangle_.back(); + rect.shrink(r); + } + this->clipping_rectangle_.push_back(rect); +} +void Display::end_clipping() { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "clear: Clipping is not set."); + } else { + 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."); + } else { + 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."); + } else { + this->clipping_rectangle_.back().shrink(add_rect); + } +} +Rect Display::get_clipping() const { + if (this->clipping_rectangle_.empty()) { + return Rect(); + } else { + 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; + if (!this->get_clipping().inside(x, 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()); + + if (!this->clipping_rectangle_.empty()) { + const auto &rect = this->clipping_rectangle_.back(); + if (!rect.is_set()) + return false; + + min_x = std::max(min_x, (int) rect.x); + max_x = std::min(max_x, (int) rect.x2()); + } + + 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()); + + if (!this->clipping_rectangle_.empty()) { + const auto &rect = this->clipping_rectangle_.back(); + if (!rect.is_set()) + return false; + + min_y = std::max(min_y, (int) rect.y); + max_y = std::min(max_y, (int) rect.y2()); + } + + return min_y < max_y; +} + +const uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' + +void Display::test_card() { + int w = get_width(), h = get_height(), image_w, image_h; + this->clear(); + this->show_test_card_ = false; + 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)); + this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); // + + this->horizontal_line(shift_x + image_c - line_w, shift_y + i, line_w, g.fade_to_white(c)); + this->horizontal_line(shift_x + image_c, shift_y + i, line_w, g.fade_to_black(c)); + + 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)); + + 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); + } + } + } + } + this->rectangle(0, 0, w, h, Color(127, 0, 127)); + this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255)); + this->stop_poller(); +} + +DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} +void DisplayPage::show() { this->parent_->show_page(this); } +void DisplayPage::show_next() { this->next_->show(); } +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; } +const display_writer_t &DisplayPage::get_writer() const { return this->writer_; } + +const LogString *text_align_to_string(TextAlign textalign) { + switch (textalign) { + case TextAlign::TOP_LEFT: + return LOG_STR("TOP_LEFT"); + case TextAlign::TOP_CENTER: + return LOG_STR("TOP_CENTER"); + case TextAlign::TOP_RIGHT: + return LOG_STR("TOP_RIGHT"); + case TextAlign::CENTER_LEFT: + return LOG_STR("CENTER_LEFT"); + case TextAlign::CENTER: + return LOG_STR("CENTER"); + case TextAlign::CENTER_RIGHT: + return LOG_STR("CENTER_RIGHT"); + case TextAlign::BASELINE_LEFT: + return LOG_STR("BASELINE_LEFT"); + case TextAlign::BASELINE_CENTER: + return LOG_STR("BASELINE_CENTER"); + case TextAlign::BASELINE_RIGHT: + return LOG_STR("BASELINE_RIGHT"); + case TextAlign::BOTTOM_LEFT: + return LOG_STR("BOTTOM_LEFT"); + case TextAlign::BOTTOM_CENTER: + return LOG_STR("BOTTOM_CENTER"); + case TextAlign::BOTTOM_RIGHT: + return LOG_STR("BOTTOM_RIGHT"); + default: + return LOG_STR("UNKNOWN"); + } +} + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h new file mode 100644 index 0000000000..54e897cdec --- /dev/null +++ b/esphome/components/display/display.h @@ -0,0 +1,751 @@ +#pragma once + +#include +#include + +#include "rect.h" + +#include "esphome/core/color.h" +#include "esphome/core/automation.h" +#include "esphome/core/time.h" +#include "esphome/core/log.h" +#include "display_color_utils.h" + +#ifdef USE_GRAPH +#include "esphome/components/graph/graph.h" +#endif + +#ifdef USE_QR_CODE +#include "esphome/components/qr_code/qr_code.h" +#endif + +#ifdef USE_GRAPHICAL_DISPLAY_MENU +#include "esphome/components/graphical_display_menu/graphical_display_menu.h" +#endif + +namespace esphome { +namespace display { + +/** TextAlign is used to tell the display class how to position a piece of text. By default + * the coordinates you enter for the print*() functions take the upper left corner of the text + * as the "anchor" point. You can customize this behavior to, for example, make the coordinates + * refer to the *center* of the text. + * + * All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis + * these options are allowed: + * + * - LEFT (x-coordinate of anchor point is on left) + * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text) + * - RIGHT (x-coordinate of anchor point is on right) + * + * For the Y-Axis alignment these options are allowed: + * + * - TOP (y-coordinate of anchor is on the top of the text) + * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text) + * - BASELINE (y-coordinate of anchor is on the baseline of the text) + * - BOTTOM (y-coordinate of anchor is on the bottom of the text) + * + * These options are then combined to create combined TextAlignment options like: + * - TOP_LEFT (default) + * - CENTER (anchor point is in the middle of the text bounds) + * - ... + */ +enum class TextAlign { + TOP = 0x00, + CENTER_VERTICAL = 0x01, + BASELINE = 0x02, + BOTTOM = 0x04, + + LEFT = 0x00, + CENTER_HORIZONTAL = 0x08, + RIGHT = 0x10, + + TOP_LEFT = TOP | LEFT, + TOP_CENTER = TOP | CENTER_HORIZONTAL, + TOP_RIGHT = TOP | RIGHT, + + CENTER_LEFT = CENTER_VERTICAL | LEFT, + CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, + CENTER_RIGHT = CENTER_VERTICAL | RIGHT, + + BASELINE_LEFT = BASELINE | LEFT, + BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL, + BASELINE_RIGHT = BASELINE | RIGHT, + + BOTTOM_LEFT = BOTTOM | LEFT, + BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, + BOTTOM_RIGHT = BOTTOM | RIGHT, +}; + +/** ImageAlign is used to tell the display class how to position a image. By default + * the coordinates you enter for the image() functions take the upper left corner of the image + * as the "anchor" point. You can customize this behavior to, for example, make the coordinates + * refer to the *center* of the image. + * + * All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis + * these options are allowed: + * + * - LEFT (x-coordinate of anchor point is on left) + * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image) + * - RIGHT (x-coordinate of anchor point is on right) + * + * For the Y-Axis alignment these options are allowed: + * + * - TOP (y-coordinate of anchor is on the top of the image) + * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image) + * - BOTTOM (y-coordinate of anchor is on the bottom of the image) + * + * These options are then combined to create combined TextAlignment options like: + * - TOP_LEFT (default) + * - CENTER (anchor point is in the middle of the image bounds) + * - ... + */ +enum class ImageAlign { + TOP = 0x00, + CENTER_VERTICAL = 0x01, + BOTTOM = 0x02, + + LEFT = 0x00, + CENTER_HORIZONTAL = 0x04, + RIGHT = 0x08, + + TOP_LEFT = TOP | LEFT, + TOP_CENTER = TOP | CENTER_HORIZONTAL, + TOP_RIGHT = TOP | RIGHT, + + CENTER_LEFT = CENTER_VERTICAL | LEFT, + CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, + CENTER_RIGHT = CENTER_VERTICAL | RIGHT, + + BOTTOM_LEFT = BOTTOM | LEFT, + BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, + BOTTOM_RIGHT = BOTTOM | RIGHT, + + HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT, + VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM +}; + +enum DisplayType { + DISPLAY_TYPE_BINARY = 1, + DISPLAY_TYPE_GRAYSCALE = 2, + DISPLAY_TYPE_COLOR = 3, +}; + +enum DisplayRotation { + DISPLAY_ROTATION_0_DEGREES = 0, + DISPLAY_ROTATION_90_DEGREES = 90, + DISPLAY_ROTATION_180_DEGREES = 180, + DISPLAY_ROTATION_270_DEGREES = 270, +}; + +#define PI 3.1415926535897932384626433832795 + +const int EDGES_TRIGON = 3; +const int EDGES_TRIANGLE = 3; +const int EDGES_TETRAGON = 4; +const int EDGES_QUADRILATERAL = 4; +const int EDGES_PENTAGON = 5; +const int EDGES_HEXAGON = 6; +const int EDGES_HEPTAGON = 7; +const int EDGES_OCTAGON = 8; +const int EDGES_NONAGON = 9; +const int EDGES_ENNEAGON = 9; +const int EDGES_DECAGON = 10; +const int EDGES_HENDECAGON = 11; +const int EDGES_DODECAGON = 12; +const int EDGES_TRIDECAGON = 13; +const int EDGES_TETRADECAGON = 14; +const int EDGES_PENTADECAGON = 15; +const int EDGES_HEXADECAGON = 16; + +const float ROTATION_0_DEGREES = 0.0; +const float ROTATION_45_DEGREES = 45.0; +const float ROTATION_90_DEGREES = 90.0; +const float ROTATION_180_DEGREES = 180.0; +const float ROTATION_270_DEGREES = 270.0; + +enum RegularPolygonVariation { + VARIATION_POINTY_TOP = 0, + VARIATION_FLAT_TOP = 1, +}; + +enum RegularPolygonDrawing { + DRAWING_OUTLINE = 0, + DRAWING_FILLED = 1, +}; + +class Display; +class DisplayPage; +class DisplayOnPageChangeTrigger; + +using display_writer_t = std::function; + +#define LOG_DISPLAY(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, prefix type); \ + ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, (obj)->rotation_); \ + ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \ + } + +/// Turn the pixel OFF. +extern const Color COLOR_OFF; +/// Turn the pixel ON. +extern const Color COLOR_ON; + +class BaseImage { + public: + virtual void draw(int x, int y, Display *display, Color color_on, Color color_off) = 0; + virtual int get_width() const = 0; + virtual int get_height() const = 0; +}; + +class BaseFont { + public: + virtual void print(int x, int y, Display *display, Color color, const char *text, Color background) = 0; + virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; +}; + +class Display : public PollingComponent { + public: + /// Fill the entire screen with the given color. + virtual void fill(Color color); + /// Clear the entire screen by filling it with OFF pixels. + void clear(); + + /// Get the calculated width of the display in pixels with rotation applied. + virtual int get_width() { return this->get_width_internal(); } + /// Get the calculated height of the display in pixels with rotation applied. + virtual int get_height() { return this->get_height_internal(); } + + /// Get the native (original) width of the display in pixels. + int get_native_width() { return this->get_width_internal(); } + /// Get the native (original) height of the display in pixels. + int get_native_height() { return this->get_height_internal(); } + + /// Set a single pixel at the specified coordinates to default color. + inline void draw_pixel_at(int x, int y) { this->draw_pixel_at(x, y, COLOR_ON); } + + /// Set a single pixel at the specified coordinates to the given color. + virtual void draw_pixel_at(int x, int y, Color color) = 0; + + /** Given an array of pixels encoded in the nominated format, draw these into the display's buffer. + * The naive implementation here will work in all cases, but can be overridden by sub-classes + * in order to optimise the procedure. + * The parameters describe a rectangular block of pixels, potentially within a larger buffer. + * + * \param x_start The starting destination x position + * \param y_start The starting destination y position + * \param w the width of the pixel block + * \param h the height of the pixel block + * \param ptr A pointer to the start of the data to be copied + * \param order The ordering of the colors + * \param bitness Defines the number of bits and their format for each pixel + * \param big_endian True if 16 bit values are stored big-endian + * \param x_offset The initial x-offset into the source buffer. + * \param y_offset The initial y-offset into the source buffer. + * \param x_pad How many pixels are in each line after the end of the pixels to be copied. + * + * The length of each source buffer line (stride) will be x_offset + w + x_pad. + */ + virtual void 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); + + /// Convenience overload for base case where the pixels are packed into the buffer with no gaps (e.g. suits LVGL.) + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order, + ColorBitness bitness, bool big_endian) { + this->draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, 0, 0, 0); + } + + /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. + void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); + + /// Draw a straight line at the given angle based on the origin [x, y] for a specified length with the given color. + void line_at_angle(int x, int y, int angle, int length, Color color = COLOR_ON); + + /// Draw a straight line at the given angle based on the origin [x, y] from a specified start and stop radius with the + /// given color. + void line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color = COLOR_ON); + + /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color. + void horizontal_line(int x, int y, int width, Color color = COLOR_ON); + + /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color. + void vertical_line(int x, int y, int height, Color color = COLOR_ON); + + /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at + /// [x1+width,y1+height]. + void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); + + /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height]. + void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); + + /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color. + void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON); + + /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. + void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); + + /// Fill a ring centered around [center_x,center_y] between two circles with the radius1 and radius2 with the given + /// color. + void filled_ring(int center_x, int center_y, int radius1, int radius2, Color color = COLOR_ON); + /// Fill a half-ring "gauge" centered around [center_x,center_y] between two circles with the radius1 and radius2 + /// with he given color and filled up to 'progress' percent + void filled_gauge(int center_x, int center_y, int radius1, int radius2, int progress, Color color = COLOR_ON); + + /// Draw the outline of a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color. + void triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON); + + /// Fill a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color. + void filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON); + + /// Get the specified vertex (x,y) coordinates for the regular polygon inscribed in the circle centered on + /// [center_x,center_y] with the given radius. Vertex id are 0-indexed and rotate clockwise. In a pointy-topped + /// variation of a polygon with a 0° rotation, the vertex #0 is located at the top of the polygon. In a flat-topped + /// variation of a polygon with a 0° rotation, the vertex #0 is located on the left-side of the horizontal top + /// edge, and the vertex #1 is located on the right-side of the horizontal top edge. + /// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon. + /// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon. + /// Use the rotation in degrees to rotate the shape clockwise. + void 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 = VARIATION_POINTY_TOP, + float rotation_degrees = ROTATION_0_DEGREES); + + /// Draw the outline of a regular polygon inscribed in the circle centered on [x,y] with the given + /// radius and color. + /// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon. + /// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon. + /// Use the rotation in degrees to rotate the shape clockwise. + /// Use the drawing to switch between outlining or filling the polygon. + void regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation = VARIATION_POINTY_TOP, + float rotation_degrees = ROTATION_0_DEGREES, Color color = COLOR_ON, + RegularPolygonDrawing drawing = DRAWING_OUTLINE); + void regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color, + RegularPolygonDrawing drawing = DRAWING_OUTLINE); + void regular_polygon(int x, int y, int radius, int edges, Color color, + RegularPolygonDrawing drawing = DRAWING_OUTLINE); + + /// Fill a regular polygon inscribed in the circle centered on [x,y] with the given radius and color. + /// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon. + /// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon. + /// Use the rotation in degrees to rotate the shape clockwise. + void filled_regular_polygon(int x, int y, int radius, int edges, + RegularPolygonVariation variation = VARIATION_POINTY_TOP, + float rotation_degrees = ROTATION_0_DEGREES, Color color = COLOR_ON); + void filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color); + void filled_regular_polygon(int x, int y, int radius, int edges, Color color); + + /** Print `text` with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param align The alignment of the text. + * @param text The text to draw. + * @param background When using multi-bit (anti-aliased) fonts, blend this background color into pixels + */ + void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text, + Color background = COLOR_OFF); + + /** Print `text` with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param text The text to draw. + * @param background When using multi-bit (anti-aliased) fonts, blend this background color into pixels + */ + void print(int x, int y, BaseFont *font, Color color, const char *text, Color background = COLOR_OFF); + + /** Print `text` with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param align The alignment of the text. + * @param text The text to draw. + */ + void print(int x, int y, BaseFont *font, TextAlign align, const char *text); + + /** Print `text` with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param text The text to draw. + */ + void print(int x, int y, BaseFont *font, const char *text); + + /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param background The background color to use for anti-aliasing + * @param align The alignment of the text. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ...) + __attribute__((format(printf, 8, 9))); + + /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param align The alignment of the text. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) + __attribute__((format(printf, 7, 8))); + + /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); + + /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param align The alignment of the text. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) + __attribute__((format(printf, 6, 7))); + + /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6))); + + /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param align The alignment of the text. + * @param format The strftime format to use. + * @param time The time to format. + */ + void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) + __attribute__((format(strftime, 7, 0))); + + /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param format The strftime format to use. + * @param time The time to format. + */ + void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) + __attribute__((format(strftime, 6, 0))); + + /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param align The alignment of the text. + * @param format The strftime format to use. + * @param time The time to format. + */ + void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) + __attribute__((format(strftime, 6, 0))); + + /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param format The strftime format to use. + * @param time The time to format. + */ + void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0))); + + /** Draw the `image` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param image The image to draw. + * @param color_on The color to replace in binary images for the on bits. + * @param color_off The color to replace in binary images for the off bits. + */ + void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + + /** Draw the `image` at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param image The image to draw. + * @param align The alignment of the image. + * @param color_on The color to replace in binary images for the on bits. + * @param color_off The color to replace in binary images for the off bits. + */ + void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + +#ifdef USE_GRAPH + /** Draw the `graph` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param graph The graph id to draw + * @param color_on The color to replace in binary images for the on bits. + */ + void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); + + /** Draw the `legend` for graph with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param graph The graph id for which the legend applies to + * @param graph The graph id for which the legend applies to + * @param graph The graph id for which the legend applies to + * @param name_font The font used for the trace name + * @param value_font The font used for the trace value and units + * @param color_on The color of the border + */ + void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); +#endif // USE_GRAPH + +#ifdef USE_QR_CODE + /** Draw the `qr_code` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param qr_code The qr_code to draw + * @param color_on The color to replace in binary images for the on bits. + */ + void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); +#endif + +#ifdef USE_GRAPHICAL_DISPLAY_MENU + /** + * @param x The x coordinate of the upper left corner + * @param y The y coordinate of the upper left corner + * @param menu The GraphicalDisplayMenu to draw + * @param width Width of the menu + * @param height Height of the menu + */ + void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height); +#endif // USE_GRAPHICAL_DISPLAY_MENU + + /** Get the text bounds of the given string. + * + * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. + * @param y The y coordinate to place the string at, can be 0 if only interested in dimensions. + * @param text The text to measure. + * @param font The font to measure the text bounds with. + * @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions. + * @param x1 A pointer to store the returned x coordinate of the upper left corner in. + * @param y1 A pointer to store the returned y coordinate of the upper left corner in. + * @param width A pointer to store the returned text width in. + * @param height A pointer to store the returned text height in. + */ + void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width, + int *height); + + /// Internal method to set the display writer lambda. + void set_writer(display_writer_t &&writer); + + void show_page(DisplayPage *page); + void show_next_page(); + void show_prev_page(); + + void set_pages(std::vector pages); + + const DisplayPage *get_active_page() const { return this->page_; } + + void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); } + + /// Internal method to set the display rotation with. + void set_rotation(DisplayRotation rotation); + + // Internal method to set display auto clearing. + void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } + + DisplayRotation get_rotation() const { return this->rotation_; } + + /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays, + * returns the type the display is currently configured to. + */ + virtual DisplayType get_display_type() = 0; + + /** Set the clipping rectangle for further drawing + * + * @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) + * + * return true if success, false if error + */ + void start_clipping(Rect rect); + void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + start_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** Add a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ + void extend_clipping(Rect rect); + void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + this->extend_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** substract a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ + void shrink_clipping(Rect rect); + void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { + this->shrink_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** Reset the invalidation region + */ + void end_clipping(); + + /** Get the current the clipping rectangle + * + * return rect for active clipping region + */ + Rect get_clipping() const; + + bool is_clipping() const { return !this->clipping_rectangle_.empty(); } + + /** Check if pixel is within region of display. + */ + bool clip(int x, int y); + + void test_card(); + void show_test_card() { this->show_test_card_ = true; } + + protected: + bool clamp_x_(int x, int w, int &min_x, int &max_x); + bool clamp_y_(int y, int h, int &min_y, int &max_y); + void vprintf_(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, + va_list arg); + + void do_update_(); + void clear_clipping_(); + + virtual int get_height_internal() = 0; + virtual int get_width_internal() = 0; + + /** + * This method fills a triangle using only integer variables by using a + * modified bresenham algorithm. + * It is mandatory that [x2,y2] and [x3,y3] lie on the same horizontal line, + * so y2 must be equal to y3. + */ + void filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color); + void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3); + + DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; + optional writer_{}; + DisplayPage *page_{nullptr}; + DisplayPage *previous_page_{nullptr}; + std::vector on_page_change_triggers_; + bool auto_clear_enabled_{true}; + std::vector clipping_rectangle_; + bool show_test_card_{false}; +}; + +class DisplayPage { + public: + DisplayPage(display_writer_t writer); + void show(); + void show_next(); + void show_prev(); + void set_parent(Display *parent); + void set_prev(DisplayPage *prev); + void set_next(DisplayPage *next); + const display_writer_t &get_writer() const; + + protected: + Display *parent_; + display_writer_t writer_; + DisplayPage *prev_{nullptr}; + DisplayPage *next_{nullptr}; +}; + +template class DisplayPageShowAction : public Action { + public: + TEMPLATABLE_VALUE(DisplayPage *, page) + + void play(Ts... x) override { + auto *page = this->page_.value(x...); + if (page != nullptr) { + page->show(); + } + } +}; + +template class DisplayPageShowNextAction : public Action { + public: + DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {} + + void play(Ts... x) override { this->buffer_->show_next_page(); } + + Display *buffer_; +}; + +template class DisplayPageShowPrevAction : public Action { + public: + DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {} + + void play(Ts... x) override { this->buffer_->show_prev_page(); } + + Display *buffer_; +}; + +template class DisplayIsDisplayingPageCondition : public Condition { + public: + DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {} + + void set_page(DisplayPage *page) { this->page_ = page; } + bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } + + protected: + Display *parent_; + DisplayPage *page_; +}; + +class DisplayOnPageChangeTrigger : public Trigger { + public: + explicit DisplayOnPageChangeTrigger(Display *parent) { parent->add_on_page_change_trigger(this); } + void process(DisplayPage *from, DisplayPage *to); + void set_from(DisplayPage *p) { this->from_ = p; } + void set_to(DisplayPage *p) { this->to_ = p; } + + protected: + DisplayPage *from_{nullptr}; + DisplayPage *to_{nullptr}; +}; + +const LogString *text_align_to_string(TextAlign textalign); + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 0d76fa09ec..3af1b63e01 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -1,107 +1,15 @@ #include "display_buffer.h" #include + #include "esphome/core/application.h" -#include "esphome/core/color.h" #include "esphome/core/log.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" namespace esphome { namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 255); -const Color COLOR_ON(255, 255, 255, 255); - -void Rect::expand(int16_t horizontal, int16_t vertical) { - if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { - this->x = this->x - horizontal; - this->y = this->y - vertical; - this->w = this->w + (2 * horizontal); - this->h = this->h + (2 * vertical); - } -} - -void Rect::extend(Rect rect) { - if (!this->is_set()) { - this->x = rect.x; - this->y = rect.y; - this->w = rect.w; - this->h = rect.h; - } else { - if (this->x > rect.x) { - this->w = this->w + (this->x - rect.x); - this->x = rect.x; - } - if (this->y > rect.y) { - this->h = this->h + (this->y - rect.y); - this->y = rect.y; - } - if (this->x2() < rect.x2()) { - this->w = rect.x2() - this->x; - } - if (this->y2() < rect.y2()) { - this->h = rect.y2() - this->y; - } - } -} -void Rect::shrink(Rect rect) { - if (!this->inside(rect)) { - (*this) = Rect(); - } else { - if (this->x2() > rect.x2()) { - this->w = rect.x2() - this->x; - } - if (this->x < rect.x) { - this->w = this->w + (this->x - rect.x); - this->x = rect.x; - } - if (this->y2() > rect.y2()) { - this->h = rect.y2() - this->y; - } - if (this->y < rect.y) { - this->h = this->h + (this->y - rect.y); - this->y = rect.y; - } - } -} - -bool Rect::equal(Rect rect) { - return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h); -} - -bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT - if (!this->is_set()) { - return true; - } - if (absolute) { - return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2())); - } else { - return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h)); - } -} - -bool Rect::inside(Rect rect, bool absolute) { - if (!this->is_set() || !rect.is_set()) { - return true; - } - if (absolute) { - return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); - } else { - return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); - } -} - -void Rect::info(const std::string &prefix) { - if (this->is_set()) { - ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), - this->y2()); - } else - ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); -} - void DisplayBuffer::init_internal_(uint32_t buffer_length) { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->buffer_ = allocator.allocate(buffer_length); @@ -112,8 +20,6 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { this->clear(); } -void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } -void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { switch (this->rotation_) { case DISPLAY_ROTATION_90_DEGREES: @@ -125,6 +31,7 @@ int DisplayBuffer::get_width() { return this->get_width_internal(); } } + int DisplayBuffer::get_height() { switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: @@ -136,7 +43,7 @@ int DisplayBuffer::get_height() { return this->get_width_internal(); } } -void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } + void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { if (!this->get_clipping().inside(x, y)) return; // NOLINT @@ -160,674 +67,6 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { this->draw_absolute_pixel_internal(x, y, color); App.feed_wdt(); } -void HOT DisplayBuffer::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; - int32_t err = dx + dy; - - while (true) { - this->draw_pixel_at(x1, y1, color); - if (x1 == x2 && y1 == y2) - break; - int32_t e2 = 2 * err; - if (e2 >= dy) { - err += dy; - x1 += sx; - } - if (e2 <= dx) { - err += dx; - y1 += sy; - } - } -} -void HOT DisplayBuffer::horizontal_line(int x, int y, int width, Color color) { - // Future: Could be made more efficient by manipulating buffer directly in certain rotations. - for (int i = x; i < x + width; i++) - this->draw_pixel_at(i, y, color); -} -void HOT DisplayBuffer::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 DisplayBuffer::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 DisplayBuffer::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 DisplayBuffer::circle(int center_x, int center_xy, int radius, Color color) { - int dx = -radius; - int dy = 0; - int err = 2 - 2 * radius; - int e2; - - do { - this->draw_pixel_at(center_x - dx, center_xy + dy, color); - this->draw_pixel_at(center_x + dx, center_xy + dy, color); - this->draw_pixel_at(center_x + dx, center_xy - dy, color); - this->draw_pixel_at(center_x - dx, center_xy - dy, color); - e2 = err; - if (e2 < dy) { - err += ++dy * 2 + 1; - if (-dx == dy && e2 <= dx) { - e2 = 0; - } - } - if (e2 > dx) { - err += ++dx * 2 + 1; - } - } while (dx <= 0); -} -void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color color) { - int dx = -int32_t(radius); - int dy = 0; - int err = 2 - 2 * radius; - int e2; - - do { - this->draw_pixel_at(center_x - dx, center_y + dy, color); - this->draw_pixel_at(center_x + dx, center_y + dy, color); - this->draw_pixel_at(center_x + dx, center_y - dy, color); - this->draw_pixel_at(center_x - dx, center_y - dy, color); - int hline_width = 2 * (-dx) + 1; - this->horizontal_line(center_x + dx, center_y + dy, hline_width, color); - this->horizontal_line(center_x + dx, center_y - dy, hline_width, color); - e2 = err; - if (e2 < dy) { - err += ++dy * 2 + 1; - if (-dx == dy && e2 <= dx) { - e2 = 0; - } - } - if (e2 > dx) { - err += ++dx * 2 + 1; - } - } while (dx <= 0); -} - -void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) { - int x_start, y_start; - int width, height; - this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); - - int i = 0; - int x_at = x_start; - while (text[i] != '\0') { - int match_length; - int glyph_n = font->match_next_glyph(text + i, &match_length); - if (glyph_n < 0) { - // Unknown char, skip - ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); - if (!font->get_glyphs().empty()) { - uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width; - for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) { - for (int glyph_y = 0; glyph_y < height; glyph_y++) - this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); - } - x_at += glyph_width; - } - - i++; - continue; - } - - const Glyph &glyph = font->get_glyphs()[glyph_n]; - int scan_x1, scan_y1, scan_width, scan_height; - glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - - { - const int glyph_x_max = scan_x1 + scan_width; - const int glyph_y_max = scan_y1 + scan_height; - for (int glyph_x = scan_x1; glyph_x < glyph_x_max; glyph_x++) { - for (int glyph_y = scan_y1; glyph_y < glyph_y_max; glyph_y++) { - if (glyph.get_pixel(glyph_x, glyph_y)) { - this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); - } - } - } - } - - x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; - - i += match_length; - } -} -void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) { - char buffer[256]; - int ret = vsnprintf(buffer, sizeof(buffer), format, arg); - if (ret > 0) - this->print(x, y, font, color, align, buffer); -} - -void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { - bool transparent = image->has_transparency(); - - switch (image->get_type()) { - case IMAGE_TYPE_BINARY: { - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - if (image->get_pixel(img_x, img_y)) { - this->draw_pixel_at(x + img_x, y + img_y, color_on); - } else if (!transparent) { - this->draw_pixel_at(x + img_x, y + img_y, color_off); - } - } - } - break; - } - case IMAGE_TYPE_GRAYSCALE: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - auto color = image->get_grayscale_pixel(img_x, img_y); - if (color.w >= 0x80) { - this->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - case IMAGE_TYPE_RGB565: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - auto color = image->get_rgb565_pixel(img_x, img_y); - if (color.w >= 0x80) { - this->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - case IMAGE_TYPE_RGB24: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - auto color = image->get_color_pixel(img_x, img_y); - if (color.w >= 0x80) { - this->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - case IMAGE_TYPE_RGBA: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - auto color = image->get_rgba_pixel(img_x, img_y); - if (color.w >= 0x80) { - this->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - } -} - -#ifdef USE_GRAPH -void DisplayBuffer::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); } -void DisplayBuffer::legend(int x, int y, graph::Graph *graph, Color color_on) { - graph->draw_legend(this, x, y, color_on); -} -#endif // USE_GRAPH - -#ifdef USE_QR_CODE -void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) { - qr_code->draw(this, x, y, color_on, scale); -} -#endif // USE_QR_CODE - -void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, - int *width, int *height) { - int x_offset, baseline; - font->measure(text, width, &x_offset, &baseline, height); - - auto x_align = TextAlign(int(align) & 0x18); - auto y_align = TextAlign(int(align) & 0x07); - - switch (x_align) { - case TextAlign::RIGHT: - *x1 = x - *width; - break; - case TextAlign::CENTER_HORIZONTAL: - *x1 = x - (*width) / 2; - break; - case TextAlign::LEFT: - default: - // LEFT - *x1 = x; - break; - } - - switch (y_align) { - case TextAlign::BOTTOM: - *y1 = y - *height; - break; - case TextAlign::BASELINE: - *y1 = y - baseline; - break; - case TextAlign::CENTER_VERTICAL: - *y1 = y - (*height) / 2; - break; - case TextAlign::TOP: - default: - *y1 = y; - break; - } -} -void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) { - this->print(x, y, font, color, TextAlign::TOP_LEFT, text); -} -void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) { - this->print(x, y, font, COLOR_ON, align, text); -} -void DisplayBuffer::print(int x, int y, Font *font, const char *text) { - this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); -} -void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->vprintf_(x, y, font, color, align, format, arg); - va_end(arg); -} -void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); - va_end(arg); -} -void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->vprintf_(x, y, font, COLOR_ON, align, format, arg); - va_end(arg); -} -void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); - va_end(arg); -} -void DisplayBuffer::set_writer(display_writer_t &&writer) { this->writer_ = writer; } -void DisplayBuffer::set_pages(std::vector pages) { - for (auto *page : pages) - page->set_parent(this); - - for (uint32_t i = 0; i < pages.size() - 1; i++) { - pages[i]->set_next(pages[i + 1]); - pages[i + 1]->set_prev(pages[i]); - } - pages[0]->set_prev(pages[pages.size() - 1]); - pages[pages.size() - 1]->set_next(pages[0]); - this->show_page(pages[0]); -} -void DisplayBuffer::show_page(DisplayPage *page) { - this->previous_page_ = this->page_; - this->page_ = page; - if (this->previous_page_ != this->page_) { - for (auto *t : on_page_change_triggers_) - t->process(this->previous_page_, this->page_); - } -} -void DisplayBuffer::show_next_page() { this->page_->show_next(); } -void DisplayBuffer::show_prev_page() { this->page_->show_prev(); } -void DisplayBuffer::do_update_() { - if (this->auto_clear_enabled_) { - this->clear(); - } - if (this->page_ != nullptr) { - this->page_->get_writer()(*this); - } else if (this->writer_.has_value()) { - (*this->writer_)(*this); - } - // remove all not ended clipping regions - while (is_clipping()) { - end_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); -} -#ifdef USE_TIME -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, - time::ESPTime time) { - char buffer[64]; - size_t ret = time.strftime(buffer, sizeof(buffer), format); - if (ret > 0) - this->print(x, y, font, color, align, buffer); -} -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { - this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); -} -void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { - this->strftime(x, y, font, COLOR_ON, align, format, time); -} -void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time::ESPTime time) { - this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); -} -#endif - -void DisplayBuffer::start_clipping(Rect rect) { - if (!this->clipping_rectangle_.empty()) { - Rect r = this->clipping_rectangle_.back(); - rect.shrink(r); - } - this->clipping_rectangle_.push_back(rect); -} -void DisplayBuffer::end_clipping() { - if (this->clipping_rectangle_.empty()) { - ESP_LOGE(TAG, "clear: Clipping is not set."); - } else { - this->clipping_rectangle_.pop_back(); - } -} -void DisplayBuffer::extend_clipping(Rect add_rect) { - if (this->clipping_rectangle_.empty()) { - ESP_LOGE(TAG, "add: Clipping is not set."); - } else { - this->clipping_rectangle_.back().extend(add_rect); - } -} -void DisplayBuffer::shrink_clipping(Rect add_rect) { - if (this->clipping_rectangle_.empty()) { - ESP_LOGE(TAG, "add: Clipping is not set."); - } else { - this->clipping_rectangle_.back().shrink(add_rect); - } -} -Rect DisplayBuffer::get_clipping() { - if (this->clipping_rectangle_.empty()) { - return Rect(); - } else { - return this->clipping_rectangle_.back(); - } -} -bool Glyph::get_pixel(int x, int y) const { - const int x_data = x - this->glyph_data_->offset_x; - const int y_data = y - this->glyph_data_->offset_y; - if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height) - return false; - const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u; - const uint32_t pos = x_data + y_data * width_8; - return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u)); -} -const char *Glyph::get_char() const { return this->glyph_data_->a_char; } -bool Glyph::compare_to(const char *str) const { - // 1 -> this->char_ - // 2 -> str - for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') - return true; - if (str[i] == '\0') - return false; - if (this->glyph_data_->a_char[i] > str[i]) - return false; - if (this->glyph_data_->a_char[i] < str[i]) - return true; - } - // this should not happen - return false; -} -int Glyph::match_length(const char *str) const { - for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') - return i; - if (str[i] != this->glyph_data_->a_char[i]) - return 0; - } - // this should not happen - return 0; -} -void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { - *x1 = this->glyph_data_->offset_x; - *y1 = this->glyph_data_->offset_y; - *width = this->glyph_data_->width; - *height = this->glyph_data_->height; -} -int Font::match_next_glyph(const char *str, int *match_length) { - int lo = 0; - int hi = this->glyphs_.size() - 1; - while (lo != hi) { - int mid = (lo + hi + 1) / 2; - if (this->glyphs_[mid].compare_to(str)) { - lo = mid; - } else { - hi = mid - 1; - } - } - *match_length = this->glyphs_[lo].match_length(str); - if (*match_length <= 0) - return -1; - return lo; -} -void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { - *baseline = this->baseline_; - *height = this->height_; - int i = 0; - int min_x = 0; - bool has_char = false; - int x = 0; - while (str[i] != '\0') { - int match_length; - int glyph_n = this->match_next_glyph(str + i, &match_length); - if (glyph_n < 0) { - // Unknown char, skip - if (!this->get_glyphs().empty()) - x += this->get_glyphs()[0].glyph_data_->width; - i++; - continue; - } - - const Glyph &glyph = this->glyphs_[glyph_n]; - if (!has_char) { - min_x = glyph.glyph_data_->offset_x; - } else { - min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); - } - x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; - - i += match_length; - has_char = true; - } - *x_offset = min_x; - *width = x - min_x; -} -Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { - glyphs_.reserve(data_nr); - for (int i = 0; i < data_nr; ++i) - glyphs_.emplace_back(&data[i]); -} - -bool Image::get_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return false; - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t pos = x + y * width_8; - return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); -} -Color Image::get_rgba_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_) * 4; - return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); -} -Color Image::get_color_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_) * 3; - Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2)); - if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { - // (0, 0, 1) has been defined as transparent color for non-alpha images. - // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) - color.w = 0; - } else { - color.w = 0xFF; - } - return color; -} -Color Image::get_rgb565_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_) * 2; - uint16_t rgb565 = - progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); - auto r = (rgb565 & 0xF800) >> 11; - auto g = (rgb565 & 0x07E0) >> 5; - auto b = rgb565 & 0x001F; - Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); - if (rgb565 == 0x0020 && transparent_) { - // darkest green has been defined as transparent color for transparent RGB565 images. - color.w = 0; - } else { - color.w = 0xFF; - } - return color; -} -Color Image::get_grayscale_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_); - const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; - return Color(gray, gray, gray, alpha); -} -int Image::get_width() const { return this->width_; } -int Image::get_height() const { return this->height_; } -ImageType Image::get_type() const { return this->type_; } -Image::Image(const uint8_t *data_start, int width, int height, ImageType type) - : width_(width), height_(height), type_(type), data_start_(data_start) {} -int Image::get_current_frame() const { return 0; } - -bool Animation::get_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return false; - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t frame_index = this->height_ * width_8 * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return false; - const uint32_t pos = x + y * width_8 + frame_index; - return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); -} -Color Animation::get_rgba_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 4; - return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); -} -Color Animation::get_color_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 3; - Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2)); - if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { - // (0, 0, 1) has been defined as transparent color for non-alpha images. - // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) - color.w = 0; - } else { - color.w = 0xFF; - } - return color; -} -Color Animation::get_rgb565_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 2; - uint16_t rgb565 = - progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); - auto r = (rgb565 & 0xF800) >> 11; - auto g = (rgb565 & 0x07E0) >> 5; - auto b = rgb565 & 0x001F; - Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); - if (rgb565 == 0x0020 && transparent_) { - // darkest green has been defined as transparent color for transparent RGB565 images. - color.w = 0; - } else { - color.w = 0xFF; - } - return color; -} -Color Animation::get_grayscale_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index); - const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; - return Color(gray, gray, gray, alpha); -} -Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) - : Image(data_start, width, height, type), - current_frame_(0), - animation_frame_count_(animation_frame_count), - loop_start_frame_(0), - loop_end_frame_(animation_frame_count_), - loop_count_(0), - loop_current_iteration_(1) {} -void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) { - loop_start_frame_ = std::min(start_frame, animation_frame_count_); - loop_end_frame_ = std::min(end_frame, animation_frame_count_); - loop_count_ = count; - loop_current_iteration_ = 1; -} - -uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; } -int Animation::get_current_frame() const { return this->current_frame_; } -void Animation::next_frame() { - this->current_frame_++; - if (loop_count_ && this->current_frame_ == loop_end_frame_ && - (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { - this->current_frame_ = loop_start_frame_; - this->loop_current_iteration_++; - } - if (this->current_frame_ >= animation_frame_count_) { - this->loop_current_iteration_ = 1; - this->current_frame_ = 0; - } -} -void Animation::prev_frame() { - this->current_frame_--; - if (this->current_frame_ < 0) { - this->current_frame_ = this->animation_frame_count_ - 1; - } -} - -void Animation::set_frame(int frame) { - unsigned abs_frame = abs(frame); - - if (abs_frame < this->animation_frame_count_) { - if (frame >= 0) { - this->current_frame_ = frame; - } else { - this->current_frame_ = this->animation_frame_count_ - abs_frame; - } - } -} - -DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} -void DisplayPage::show() { this->parent_->show_page(this); } -void DisplayPage::show_next() { this->next_->show(); } -void DisplayPage::show_prev() { this->prev_->show(); } -void DisplayPage::set_parent(DisplayBuffer *parent) { this->parent_ = parent; } -void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } -void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } -const display_writer_t &DisplayPage::get_writer() const { return this->writer_; } } // namespace display } // namespace esphome diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 2474d6f5a0..b7c4db56be 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -1,648 +1,33 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/defines.h" -#include "esphome/core/automation.h" -#include "display_color_utils.h" #include #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "display.h" +#include "display_color_utils.h" -#ifdef USE_GRAPH -#include "esphome/components/graph/graph.h" -#endif - -#ifdef USE_QR_CODE -#include "esphome/components/qr_code/qr_code.h" -#endif +#include "esphome/core/component.h" +#include "esphome/core/defines.h" namespace esphome { namespace display { -/** TextAlign is used to tell the display class how to position a piece of text. By default - * the coordinates you enter for the print*() functions take the upper left corner of the text - * as the "anchor" point. You can customize this behavior to, for example, make the coordinates - * refer to the *center* of the text. - * - * All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis - * these options are allowed: - * - * - LEFT (x-coordinate of anchor point is on left) - * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text) - * - RIGHT (x-coordinate of anchor point is on right) - * - * For the Y-Axis alignment these options are allowed: - * - * - TOP (y-coordinate of anchor is on the top of the text) - * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text) - * - BASELINE (y-coordinate of anchor is on the baseline of the text) - * - BOTTOM (y-coordinate of anchor is on the bottom of the text) - * - * These options are then combined to create combined TextAlignment options like: - * - TOP_LEFT (default) - * - CENTER (anchor point is in the middle of the text bounds) - * - ... - */ -enum class TextAlign { - TOP = 0x00, - CENTER_VERTICAL = 0x01, - BASELINE = 0x02, - BOTTOM = 0x04, - - LEFT = 0x00, - CENTER_HORIZONTAL = 0x08, - RIGHT = 0x10, - - TOP_LEFT = TOP | LEFT, - TOP_CENTER = TOP | CENTER_HORIZONTAL, - TOP_RIGHT = TOP | RIGHT, - - CENTER_LEFT = CENTER_VERTICAL | LEFT, - CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, - CENTER_RIGHT = CENTER_VERTICAL | RIGHT, - - BASELINE_LEFT = BASELINE | LEFT, - BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL, - BASELINE_RIGHT = BASELINE | RIGHT, - - BOTTOM_LEFT = BOTTOM | LEFT, - BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, - BOTTOM_RIGHT = BOTTOM | RIGHT, -}; - -/// Turn the pixel OFF. -extern const Color COLOR_OFF; -/// Turn the pixel ON. -extern const Color COLOR_ON; - -enum ImageType { - IMAGE_TYPE_BINARY = 0, - IMAGE_TYPE_GRAYSCALE = 1, - IMAGE_TYPE_RGB24 = 2, - IMAGE_TYPE_RGB565 = 3, - IMAGE_TYPE_RGBA = 4, -}; - -enum DisplayType { - DISPLAY_TYPE_BINARY = 1, - DISPLAY_TYPE_GRAYSCALE = 2, - DISPLAY_TYPE_COLOR = 3, -}; - -enum DisplayRotation { - DISPLAY_ROTATION_0_DEGREES = 0, - DISPLAY_ROTATION_90_DEGREES = 90, - DISPLAY_ROTATION_180_DEGREES = 180, - DISPLAY_ROTATION_270_DEGREES = 270, -}; - -static const int16_t VALUE_NO_SET = 32766; - -class Rect { +class DisplayBuffer : public Display { public: - int16_t x; ///< X coordinate of corner - int16_t y; ///< Y coordinate of corner - int16_t w; ///< Width of region - int16_t h; ///< Height of region - - Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT - inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {} - inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner - inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner - - inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); } - - void expand(int16_t horizontal, int16_t vertical); - - void extend(Rect rect); - void shrink(Rect rect); - - bool inside(Rect rect, bool absolute = true); - bool inside(int16_t test_x, int16_t test_y, bool absolute = true); - bool equal(Rect rect); - void info(const std::string &prefix = "rect info:"); -}; - -class Font; -class Image; -class DisplayBuffer; -class DisplayPage; -class DisplayOnPageChangeTrigger; - -using display_writer_t = std::function; - -#define LOG_DISPLAY(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type); \ - ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, (obj)->rotation_); \ - ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \ - } - -class DisplayBuffer { - public: - /// Fill the entire screen with the given color. - virtual void fill(Color color); - /// Clear the entire screen by filling it with OFF pixels. - void clear(); - /// Get the width of the image in pixels with rotation applied. - int get_width(); + int get_width() override; /// Get the height of the image in pixels with rotation applied. - int get_height(); + int get_height() override; /// Set a single pixel at the specified coordinates to the given color. - void draw_pixel_at(int x, int y, Color color = COLOR_ON); - - /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. - void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); - - /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color. - void horizontal_line(int x, int y, int width, Color color = COLOR_ON); - - /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color. - void vertical_line(int x, int y, int height, Color color = COLOR_ON); - - /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at - /// [x1+width,y1+height]. - void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); - - /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height]. - void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); - - /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color. - void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON); - - /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. - void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); - - /** Print `text` with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param align The alignment of the text. - * @param text The text to draw. - */ - void print(int x, int y, Font *font, Color color, TextAlign align, const char *text); - - /** Print `text` with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param text The text to draw. - */ - void print(int x, int y, Font *font, Color color, const char *text); - - /** Print `text` with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param align The alignment of the text. - * @param text The text to draw. - */ - void print(int x, int y, Font *font, TextAlign align, const char *text); - - /** Print `text` with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param text The text to draw. - */ - void print(int x, int y, Font *font, const char *text); - - /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param align The alignment of the text. - * @param format The format to use. - * @param ... The arguments to use for the text formatting. - */ - void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) - __attribute__((format(printf, 7, 8))); - - /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param format The format to use. - * @param ... The arguments to use for the text formatting. - */ - void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); - - /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param align The alignment of the text. - * @param format The format to use. - * @param ... The arguments to use for the text formatting. - */ - void printf(int x, int y, Font *font, TextAlign align, const char *format, ...) __attribute__((format(printf, 6, 7))); - - /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param format The format to use. - * @param ... The arguments to use for the text formatting. - */ - void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6))); - -#ifdef USE_TIME - /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param align The alignment of the text. - * @param format The strftime format to use. - * @param time The time to format. - */ - void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) - __attribute__((format(strftime, 7, 0))); - - /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param format The strftime format to use. - * @param time The time to format. - */ - void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) - __attribute__((format(strftime, 6, 0))); - - /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param align The alignment of the text. - * @param format The strftime format to use. - * @param time The time to format. - */ - void strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) - __attribute__((format(strftime, 6, 0))); - - /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param format The strftime format to use. - * @param time The time to format. - */ - void strftime(int x, int y, Font *font, const char *format, time::ESPTime time) - __attribute__((format(strftime, 5, 0))); -#endif - - /** Draw the `image` with the top-left corner at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param image The image to draw - * @param color_on The color to replace in binary images for the on bits. - * @param color_off The color to replace in binary images for the off bits. - */ - void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); - -#ifdef USE_GRAPH - /** Draw the `graph` with the top-left corner at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param graph The graph id to draw - * @param color_on The color to replace in binary images for the on bits. - */ - void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); - - /** Draw the `legend` for graph with the top-left corner at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param graph The graph id for which the legend applies to - * @param graph The graph id for which the legend applies to - * @param graph The graph id for which the legend applies to - * @param name_font The font used for the trace name - * @param value_font The font used for the trace value and units - * @param color_on The color of the border - */ - void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); -#endif // USE_GRAPH - -#ifdef USE_QR_CODE - /** Draw the `qr_code` with the top-left corner at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param qr_code The qr_code to draw - * @param color_on The color to replace in binary images for the on bits. - */ - void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); -#endif - - /** Get the text bounds of the given string. - * - * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. - * @param y The y coordinate to place the string at, can be 0 if only interested in dimensions. - * @param text The text to measure. - * @param font The font to measure the text bounds with. - * @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions. - * @param x1 A pointer to store the returned x coordinate of the upper left corner in. - * @param y1 A pointer to store the returned y coordinate of the upper left corner in. - * @param width A pointer to store the returned text width in. - * @param height A pointer to store the returned text height in. - */ - void get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width, - int *height); - - /// Internal method to set the display writer lambda. - void set_writer(display_writer_t &&writer); - - void show_page(DisplayPage *page); - void show_next_page(); - void show_prev_page(); - - void set_pages(std::vector pages); - - const DisplayPage *get_active_page() const { return this->page_; } - - void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); } - - /// Internal method to set the display rotation with. - void set_rotation(DisplayRotation rotation); - - // Internal method to set display auto clearing. - void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } - - virtual int get_height_internal() = 0; - virtual int get_width_internal() = 0; - DisplayRotation get_rotation() const { return this->rotation_; } - - /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays, - * returns the type the display is currently configured to. - */ - virtual DisplayType get_display_type() = 0; - - /** Set the clipping rectangle for further drawing - * - * @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) - * - * return true if success, false if error - */ - void start_clipping(Rect rect); - void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { - start_clipping(Rect(left, top, right - left, bottom - top)); - }; - - /** Add a rectangular region to the invalidation region - * - This is usually called when an element has been modified - * - * @param[in] rect: Rectangle to add to the invalidation region - */ - void extend_clipping(Rect rect); - void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { - this->extend_clipping(Rect(left, top, right - left, bottom - top)); - }; - - /** substract a rectangular region to the invalidation region - * - This is usually called when an element has been modified - * - * @param[in] rect: Rectangle to add to the invalidation region - */ - void shrink_clipping(Rect rect); - void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { - this->shrink_clipping(Rect(left, top, right - left, bottom - top)); - }; - - /** Reset the invalidation region - */ - void end_clipping(); - - /** Get the current the clipping rectangle - * - * return rect for active clipping region - */ - Rect get_clipping(); - - bool is_clipping() const { return !this->clipping_rectangle_.empty(); } + void draw_pixel_at(int x, int y, Color color) override; protected: - void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); - virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; void init_internal_(uint32_t buffer_length); - void do_update_(); - uint8_t *buffer_{nullptr}; - DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; - optional writer_{}; - DisplayPage *page_{nullptr}; - DisplayPage *previous_page_{nullptr}; - std::vector on_page_change_triggers_; - bool auto_clear_enabled_{true}; - std::vector clipping_rectangle_; -}; - -class DisplayPage { - public: - DisplayPage(display_writer_t writer); - void show(); - void show_next(); - void show_prev(); - void set_parent(DisplayBuffer *parent); - void set_prev(DisplayPage *prev); - void set_next(DisplayPage *next); - const display_writer_t &get_writer() const; - - protected: - DisplayBuffer *parent_; - display_writer_t writer_; - DisplayPage *prev_{nullptr}; - DisplayPage *next_{nullptr}; -}; - -struct GlyphData { - const char *a_char; - const uint8_t *data; - int offset_x; - int offset_y; - int width; - int height; -}; - -class Glyph { - public: - Glyph(const GlyphData *data) : glyph_data_(data) {} - - bool get_pixel(int x, int y) const; - - const char *get_char() const; - - bool compare_to(const char *str) const; - - int match_length(const char *str) const; - - void scan_area(int *x1, int *y1, int *width, int *height) const; - - protected: - friend Font; - friend DisplayBuffer; - - const GlyphData *glyph_data_; -}; - -class Font { - public: - /** Construct the font with the given glyphs. - * - * @param glyphs A vector of glyphs, must be sorted lexicographically. - * @param baseline The y-offset from the top of the text to the baseline. - * @param bottom The y-offset from the top of the text to the bottom (i.e. height). - */ - Font(const GlyphData *data, int data_nr, int baseline, int height); - - int match_next_glyph(const char *str, int *match_length); - - void measure(const char *str, int *width, int *x_offset, int *baseline, int *height); - inline int get_baseline() { return this->baseline_; } - inline int get_height() { return this->height_; } - - const std::vector> &get_glyphs() const { return glyphs_; } - - protected: - std::vector> glyphs_; - int baseline_; - int height_; -}; - -class Image { - public: - Image(const uint8_t *data_start, int width, int height, ImageType type); - virtual bool get_pixel(int x, int y) const; - virtual Color get_color_pixel(int x, int y) const; - virtual Color get_rgba_pixel(int x, int y) const; - virtual Color get_rgb565_pixel(int x, int y) const; - virtual Color get_grayscale_pixel(int x, int y) const; - int get_width() const; - int get_height() const; - ImageType get_type() const; - - virtual int get_current_frame() const; - - void set_transparency(bool transparent) { transparent_ = transparent; } - bool has_transparency() const { return transparent_; } - - protected: - int width_; - int height_; - ImageType type_; - const uint8_t *data_start_; - bool transparent_; -}; - -class Animation : public Image { - public: - Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); - bool get_pixel(int x, int y) const override; - Color get_color_pixel(int x, int y) const override; - Color get_rgba_pixel(int x, int y) const override; - Color get_rgb565_pixel(int x, int y) const override; - Color get_grayscale_pixel(int x, int y) const override; - - uint32_t get_animation_frame_count() const; - int get_current_frame() const override; - void next_frame(); - void prev_frame(); - - /** Selects a specific frame within the animation. - * - * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame. - */ - void set_frame(int frame); - - void set_loop(uint32_t start_frame, uint32_t end_frame, int count); - - protected: - int current_frame_; - uint32_t animation_frame_count_; - uint32_t loop_start_frame_; - uint32_t loop_end_frame_; - int loop_count_; - int loop_current_iteration_; -}; - -template class DisplayPageShowAction : public Action { - public: - TEMPLATABLE_VALUE(DisplayPage *, page) - - void play(Ts... x) override { - auto *page = this->page_.value(x...); - if (page != nullptr) { - page->show(); - } - } -}; - -template class DisplayPageShowNextAction : public Action { - public: - DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {} - - void play(Ts... x) override { this->buffer_->show_next_page(); } - - DisplayBuffer *buffer_; -}; - -template class DisplayPageShowPrevAction : public Action { - public: - DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {} - - void play(Ts... x) override { this->buffer_->show_prev_page(); } - - DisplayBuffer *buffer_; -}; - -template class DisplayIsDisplayingPageCondition : public Condition { - public: - DisplayIsDisplayingPageCondition(DisplayBuffer *parent) : parent_(parent) {} - - void set_page(DisplayPage *page) { this->page_ = page; } - bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } - - protected: - DisplayBuffer *parent_; - DisplayPage *page_; -}; - -class DisplayOnPageChangeTrigger : public Trigger { - public: - explicit DisplayOnPageChangeTrigger(DisplayBuffer *parent) { parent->add_on_page_change_trigger(this); } - void process(DisplayPage *from, DisplayPage *to); - void set_from(DisplayPage *p) { this->from_ = p; } - void set_to(DisplayPage *p) { this->to_ = p; } - - protected: - DisplayPage *from_{nullptr}; - DisplayPage *to_{nullptr}; }; } // namespace display diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp new file mode 100644 index 0000000000..34b611191f --- /dev/null +++ b/esphome/components/display/rect.cpp @@ -0,0 +1,98 @@ +#include "rect.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace display { + +static const char *const TAG = "display"; + +void Rect::expand(int16_t horizontal, int16_t vertical) { + if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { + this->x = this->x - horizontal; + this->y = this->y - vertical; + this->w = this->w + (2 * horizontal); + this->h = this->h + (2 * vertical); + } +} + +void Rect::extend(Rect rect) { + if (!this->is_set()) { + this->x = rect.x; + this->y = rect.y; + this->w = rect.w; + this->h = rect.h; + } else { + if (this->x > rect.x) { + this->w = this->w + (this->x - rect.x); + this->x = rect.x; + } + if (this->y > rect.y) { + this->h = this->h + (this->y - rect.y); + this->y = rect.y; + } + if (this->x2() < rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() < rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} +void Rect::shrink(Rect rect) { + if (!this->inside(rect)) { + (*this) = Rect(); + } else { + if (this->x2() > rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->x < rect.x) { + this->w = this->w + (this->x - rect.x); + this->x = rect.x; + } + if (this->y2() > rect.y2()) { + this->h = rect.y2() - this->y; + } + if (this->y < rect.y) { + this->h = this->h + (this->y - rect.y); + this->y = rect.y; + } + } +} + +bool Rect::equal(Rect rect) const { + return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h); +} + +bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOLINT + if (!this->is_set()) { + return true; + } + if (absolute) { + return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2())); + } else { + return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h)); + } +} + +bool Rect::inside(Rect rect, bool absolute) const { + if (!this->is_set() || !rect.is_set()) { + return true; + } + if (absolute) { + return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); + } else { + return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); + } +} + +void Rect::info(const std::string &prefix) { + if (this->is_set()) { + ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), + this->y2()); + } else + ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); +} + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display/rect.h b/esphome/components/display/rect.h new file mode 100644 index 0000000000..f55c2fe201 --- /dev/null +++ b/esphome/components/display/rect.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace display { + +static const int16_t VALUE_NO_SET = 32766; + +class Rect { + public: + int16_t x; ///< X coordinate of corner + int16_t y; ///< Y coordinate of corner + int16_t w; ///< Width of region + int16_t h; ///< Height of region + + Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT + inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ESPHOME_ALWAYS_INLINE : x(x), y(y), w(w), h(h) {} + inline int16_t x2() const { return this->x + this->w; }; ///< X coordinate of corner + inline int16_t y2() const { return this->y + this->h; }; ///< Y coordinate of corner + + inline bool is_set() const ESPHOME_ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); } + + void expand(int16_t horizontal, int16_t vertical); + + void extend(Rect rect); + void shrink(Rect rect); + + bool inside(Rect rect, bool absolute = true) const; + bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const; + bool equal(Rect rect) const; + void info(const std::string &prefix = "rect info:"); +}; + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index d7326cdc65..8ae9cbc2a4 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -1,39 +1,39 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv + from esphome import automation, core +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components.number import Number +from esphome.components.select import Select +from esphome.components.switch import Switch +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_TYPE, - CONF_TRIGGER_ID, - CONF_ON_VALUE, + CONF_ACTIVE, CONF_COMMAND, CONF_CUSTOM, - CONF_NUMBER, CONF_FORMAT, + CONF_ID, + CONF_ITEMS, CONF_MODE, - CONF_ACTIVE, + CONF_NUMBER, + CONF_ON_VALUE, + CONF_TEXT, + CONF_TRIGGER_ID, + CONF_TYPE, ) -from esphome.automation import maybe_simple_id -from esphome.components.select import Select -from esphome.components.number import Number -from esphome.components.switch import Switch CODEOWNERS = ["@numo68"] display_menu_base_ns = cg.esphome_ns.namespace("display_menu_base") -CONF_DISPLAY_ID = "display_id" CONF_ROTARY = "rotary" CONF_JOYSTICK = "joystick" CONF_LABEL = "label" CONF_MENU = "menu" CONF_BACK = "back" -CONF_TEXT = "text" CONF_SELECT = "select" CONF_SWITCH = "switch" -CONF_ITEMS = "items" CONF_ON_TEXT = "on_text" CONF_OFF_TEXT = "off_text" CONF_VALUE_LAMBDA = "value_lambda" diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp index 57da3cec35..5502623607 100644 --- a/esphome/components/display_menu_base/display_menu_base.cpp +++ b/esphome/components/display_menu_base/display_menu_base.cpp @@ -60,6 +60,8 @@ void DisplayMenuComponent::left() { if (this->editing_) { this->finish_editing_(); changed = true; + } else { + changed = this->leave_menu_(); } break; case MENU_MODE_JOYSTICK: @@ -172,6 +174,8 @@ void DisplayMenuComponent::show_main() { this->process_initial_(); + this->on_before_show(); + if (this->active_ && this->editing_) this->finish_editing_(); @@ -188,6 +192,8 @@ void DisplayMenuComponent::show_main() { } this->draw_and_update(); + + this->on_after_show(); } void DisplayMenuComponent::show() { @@ -196,18 +202,26 @@ void DisplayMenuComponent::show() { this->process_initial_(); + this->on_before_show(); + if (!this->active_) { this->active_ = true; this->draw_and_update(); } + + this->on_after_show(); } void DisplayMenuComponent::hide() { if (this->check_healthy_and_active_()) { + this->on_before_hide(); + if (this->editing_) this->finish_editing_(); this->active_ = false; this->update(); + + this->on_after_hide(); } } diff --git a/esphome/components/display_menu_base/display_menu_base.h b/esphome/components/display_menu_base/display_menu_base.h index 46bb0a8192..6208fcd3b4 100644 --- a/esphome/components/display_menu_base/display_menu_base.h +++ b/esphome/components/display_menu_base/display_menu_base.h @@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component { update(); } + virtual void on_before_show(){}; + virtual void on_after_show(){}; + virtual void on_before_hide(){}; + virtual void on_after_hide(){}; + uint8_t rows_; bool active_; MenuMode mode_; diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index bbe6ec0e89..2c7f34c493 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -5,6 +5,29 @@ namespace esphome { namespace display_menu_base { +const LogString *menu_item_type_to_string(MenuItemType type) { + switch (type) { + case MenuItemType::MENU_ITEM_LABEL: + return LOG_STR("MENU_ITEM_LABEL"); + case MenuItemType::MENU_ITEM_MENU: + return LOG_STR("MENU_ITEM_MENU"); + case MenuItemType::MENU_ITEM_BACK: + return LOG_STR("MENU_ITEM_BACK"); + case MenuItemType::MENU_ITEM_SELECT: + return LOG_STR("MENU_ITEM_SELECT"); + case MenuItemType::MENU_ITEM_NUMBER: + return LOG_STR("MENU_ITEM_NUMBER"); + case MenuItemType::MENU_ITEM_SWITCH: + return LOG_STR("MENU_ITEM_SWITCH"); + case MenuItemType::MENU_ITEM_COMMAND: + return LOG_STR("MENU_ITEM_COMMAND"); + case MenuItemType::MENU_ITEM_CUSTOM: + return LOG_STR("MENU_ITEM_CUSTOM"); + default: + return LOG_STR("UNKNOWN"); + } +} + void MenuItem::on_enter() { this->on_enter_callbacks_.call(); } void MenuItem::on_leave() { this->on_leave_callbacks_.call(); } diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h index a30f31e88f..36de146031 100644 --- a/esphome/components/display_menu_base/menu_item.h +++ b/esphome/components/display_menu_base/menu_item.h @@ -14,6 +14,7 @@ #endif #include +#include "esphome/core/log.h" namespace esphome { namespace display_menu_base { @@ -29,6 +30,9 @@ enum MenuItemType { MENU_ITEM_CUSTOM, }; +/// @brief Returns a string representation of a menu item type suitable for logging +const LogString *menu_item_type_to_string(MenuItemType type); + class MenuItem; class MenuItemMenu; using value_getter_t = std::function; diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index d249e9743a..9df8a1d373 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -37,14 +37,18 @@ void DS1307Component::read_time() { ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); return; } - time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), - .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), - .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), - .day_of_week = uint8_t(ds1307_.reg.weekday), - .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) - .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), - .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; + ESPTime rtc_time{ + .second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), + .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), + .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), + .day_of_week = uint8_t(ds1307_.reg.weekday), + .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), + .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000), + .is_dst = false, // not used + .timestamp = 0 // overwritten by recalc_timestamp_utc(false) + }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index f4f8305ba6..9f56dc3465 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -19,6 +19,7 @@ CONF_CRC_CHECK = "crc_check" CONF_DECRYPTION_KEY = "decryption_key" CONF_DSMR_ID = "dsmr_id" CONF_GAS_MBUS_ID = "gas_mbus_id" +CONF_WATER_MBUS_ID = "water_mbus_id" CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" CONF_REQUEST_INTERVAL = "request_interval" CONF_REQUEST_PIN = "request_pin" @@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DECRYPTION_KEY): _validate_key, cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, + cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, cv.Optional( @@ -82,9 +84,10 @@ async def to_code(config): cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) + cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) # DSMR Parser - cg.add_library("glmnet/Dsmr", "0.5") + cg.add_library("glmnet/Dsmr", "0.8") # Crypto cg.add_library("rweather/Crypto", "0.4.0") diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index f382730912..193ea1d4e5 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -256,6 +256,7 @@ bool Dsmr::parse_telegram() { MyData data; 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. @@ -267,6 +268,11 @@ bool Dsmr::parse_telegram() { } else { this->status_clear_warning(); this->publish_sensors(data); + + // 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_)); + } return true; } } diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 6621d02cae..7304737b50 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -85,6 +85,9 @@ class Dsmr : public Component, public uart::UARTDevice { void set_##s(text_sensor::TextSensor *sensor) { s_##s##_ = sensor; } DSMR_TEXT_SENSOR_LIST(DSMR_SET_TEXT_SENSOR, ) + // handled outside dsmr + void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; } + protected: void receive_telegram_(); void receive_encrypted_telegram_(); @@ -124,6 +127,9 @@ class Dsmr : public Component, public uart::UARTDevice { bool header_found_{false}; bool footer_found_{false}; + // handled outside dsmr + text_sensor::TextSensor *s_telegram_{nullptr}; + // Sensor member pointers #define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr}; DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 0b0439baa4..f2398d1908 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_WATER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, @@ -236,6 +237,36 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional("water_delivered"): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_WATER, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional( + "active_energy_import_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( + "active_energy_import_maximum_demand_running_month" + ): 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_maximum_demand_last_13_months" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + 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 202cc07020..7c13fe7d58 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor - +from esphome.const import CONF_INTERNAL from . import Dsmr, CONF_DSMR_ID AUTO_LOAD = ["dsmr"] @@ -22,6 +22,9 @@ CONFIG_SCHEMA = cv.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("telegram"): text_sensor.text_sensor_schema().extend( + {cv.Optional(CONF_INTERNAL, default=True): cv.boolean} + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -37,7 +40,9 @@ async def to_code(config): if id and id.type == text_sensor.TextSensor: var = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}")(var)) - text_sensors.append(f"F({key})") + if key != "telegram": + # telegram is not handled by dsmr + text_sensors.append(f"F({key})") if text_sensors: cg.add_define( diff --git a/esphome/components/duty_time/__init__.py b/esphome/components/duty_time/__init__.py new file mode 100644 index 0000000000..b708cee80b --- /dev/null +++ b/esphome/components/duty_time/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@dudanov"] diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp new file mode 100644 index 0000000000..d4369c89c0 --- /dev/null +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -0,0 +1,105 @@ +#include "duty_time_sensor.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace duty_time_sensor { + +static const char *const TAG = "duty_time_sensor"; + +#ifdef USE_BINARY_SENSOR +void DutyTimeSensor::set_sensor(binary_sensor::BinarySensor *const sensor) { + sensor->add_on_state_callback([this](bool state) { this->process_state_(state); }); +} +#endif + +void DutyTimeSensor::start() { + if (!this->last_state_) + this->process_state_(true); +} + +void DutyTimeSensor::stop() { + if (this->last_state_) + this->process_state_(false); +} + +void DutyTimeSensor::update() { + if (this->last_state_) + this->process_state_(true); +} + +void DutyTimeSensor::loop() { + if (this->func_ == nullptr) + return; + + const bool state = this->func_(); + + if (state != this->last_state_) + this->process_state_(state); +} + +void DutyTimeSensor::setup() { + uint32_t seconds = 0; + + if (this->restore_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_.load(&seconds); + } + + this->set_value_(seconds); +} + +void DutyTimeSensor::set_value_(const uint32_t sec) { + this->last_time_ = 0; + if (this->last_state_) + this->last_time_ = millis(); // last time with 0 ms correction + this->publish_and_save_(sec, 0); +} + +void DutyTimeSensor::process_state_(const bool state) { + const uint32_t now = millis(); + + if (this->last_state_) { + // update or falling edge + const uint32_t tm = now - this->last_time_; + const uint32_t ms = tm % 1000; + + this->publish_and_save_(this->total_sec_ + tm / 1000, ms); + this->last_time_ = now - ms; // store time with ms correction + + if (!state) { + // falling edge + this->last_time_ = ms; // temporary store ms correction only + this->last_state_ = false; + + if (this->last_duty_time_sensor_ != nullptr) { + const uint32_t turn_on_ms = now - this->edge_time_; + this->last_duty_time_sensor_->publish_state(turn_on_ms * 1e-3f); + } + } + + } else if (state) { + // rising edge + this->last_time_ = now - this->last_time_; // store time with ms correction + this->edge_time_ = now; // store turn-on start time + this->last_state_ = true; + } +} + +void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) { + this->total_sec_ = sec; + this->publish_state(sec + ms * 1e-3f); + + if (this->restore_) + this->pref_.save(&sec); +} + +void DutyTimeSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Duty Time:"); + ESP_LOGCONFIG(TAG, " Update Interval: %" PRId32 "ms", this->get_update_interval()); + ESP_LOGCONFIG(TAG, " Restore: %s", ONOFF(this->restore_)); + LOG_SENSOR(" ", "Duty Time Sensor:", this); + LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_); +} + +} // namespace duty_time_sensor +} // namespace esphome diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h new file mode 100644 index 0000000000..38655f104a --- /dev/null +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -0,0 +1,76 @@ +#pragma once + +#include + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/components/sensor/sensor.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +namespace esphome { +namespace duty_time_sensor { + +class DutyTimeSensor : public sensor::Sensor, public PollingComponent { + public: + void setup() override; + void update() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void start(); + void stop(); + bool is_running() const { return this->last_state_; } + void reset() { this->set_value_(0); } + +#ifdef USE_BINARY_SENSOR + void set_sensor(binary_sensor::BinarySensor *sensor); +#endif + void set_lambda(std::function &&func) { this->func_ = func; } + void set_last_duty_time_sensor(sensor::Sensor *sensor) { this->last_duty_time_sensor_ = sensor; } + void set_restore(bool restore) { this->restore_ = restore; } + + protected: + void set_value_(uint32_t sec); + void process_state_(bool state); + void publish_and_save_(uint32_t sec, uint32_t ms); + + std::function func_{nullptr}; + sensor::Sensor *last_duty_time_sensor_{nullptr}; + ESPPreferenceObject pref_; + + uint32_t total_sec_; + uint32_t last_time_; + uint32_t edge_time_; + bool last_state_{false}; + bool restore_; +}; + +template class BaseAction : public Action, public Parented {}; + +template class StartAction : public BaseAction { + void play(Ts... x) override { this->parent_->start(); } +}; + +template class StopAction : public BaseAction { + void play(Ts... x) override { this->parent_->stop(); } +}; + +template class ResetAction : public BaseAction { + void play(Ts... x) override { this->parent_->reset(); } +}; + +template class RunningCondition : public Condition, public Parented { + public: + explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {} + + protected: + bool check(Ts... x) override { return this->parent_->is_running() == this->state_; } + bool state_; +}; + +} // namespace duty_time_sensor +} // namespace esphome diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py new file mode 100644 index 0000000000..556cd459a5 --- /dev/null +++ b/esphome/components/duty_time/sensor.py @@ -0,0 +1,127 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.automation import ( + Action, + Condition, + maybe_simple_id, + register_action, + register_condition, +) +from esphome.components import binary_sensor, sensor +from esphome.const import ( + CONF_ID, + CONF_SENSOR, + CONF_RESTORE, + CONF_LAMBDA, + UNIT_SECOND, + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, + DEVICE_CLASS_DURATION, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +CONF_LAST_TIME = "last_time" + +duty_time_sensor_ns = cg.esphome_ns.namespace("duty_time_sensor") +DutyTimeSensor = duty_time_sensor_ns.class_( + "DutyTimeSensor", sensor.Sensor, cg.PollingComponent +) +BaseAction = duty_time_sensor_ns.class_("BaseAction", Action, cg.Parented) +StartAction = duty_time_sensor_ns.class_("StartAction", BaseAction) +StopAction = duty_time_sensor_ns.class_("StopAction", BaseAction) +ResetAction = duty_time_sensor_ns.class_("ResetAction", BaseAction) +SetAction = duty_time_sensor_ns.class_("SetAction", BaseAction) +RunningCondition = duty_time_sensor_ns.class_( + "RunningCondition", Condition, cg.Parented +) + + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + DutyTimeSensor, + unit_of_measurement=UNIT_SECOND, + icon="mdi:timer-play-outline", + accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=DEVICE_CLASS_DURATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + .extend( + { + cv.Optional(CONF_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_RESTORE, default=False): cv.boolean, + cv.Optional(CONF_LAST_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + icon="mdi:timer-marker-outline", + accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL, + device_class=DEVICE_CLASS_DURATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")), + cv.has_at_most_one_key(CONF_SENSOR, CONF_LAMBDA), +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + cg.add(var.set_restore(config[CONF_RESTORE])) + if CONF_SENSOR in config: + sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.bool_) + cg.add(var.set_lambda(lambda_)) + if CONF_LAST_TIME in config: + sens = await sensor.new_sensor(config[CONF_LAST_TIME]) + cg.add(var.set_last_duty_time_sensor(sens)) + + +# AUTOMATIONS + +DUTY_TIME_ID_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(DutyTimeSensor), + } +) + + +@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_start_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 + + +@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_stop_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 + + +@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_reset_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 + + +@register_condition( + "sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA +) +async def duty_time_is_running_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, True) + + +@register_condition( + "sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA +) +async def duty_time_is_not_running_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, False) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 818006ced7..a74fc9be4a 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -1,4 +1,5 @@ #include "e131.h" +#ifdef USE_NETWORK #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" @@ -40,7 +41,6 @@ void E131Component::setup() { this->mark_failed(); return; } - server.ss_family = AF_INET; err = this->socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { @@ -119,3 +119,4 @@ bool E131Component::process_(int universe, const E131Packet &packet) { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 364a05af75..d0e38fa98c 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,8 +1,10 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include "esphome/components/socket/socket.h" #include "esphome/core/component.h" +#include #include #include #include @@ -52,3 +54,4 @@ class E131Component : public esphome::Component { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 42eb0fc56b..4d1f98ab6c 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -1,5 +1,6 @@ #include "e131_addressable_light_effect.h" #include "e131.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" namespace esphome { @@ -51,14 +52,14 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet if (universe < first_universe_ || universe > get_last_universe()) return false; - int output_offset = (universe - first_universe_) * get_lights_per_universe(); + int32_t output_offset = (universe - first_universe_) * get_lights_per_universe(); // limit amount of lights per universe and received int output_end = std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); auto *input_data = packet.values + 1; - ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %d-%d.", get_name().c_str(), universe, output_offset, - output_end); + ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe, + output_offset, output_end); switch (channels_) { case E131_MONO: @@ -90,3 +91,4 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index 56df9cd80f..17d7bd2829 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/light/addressable_light_effect.h" - +#ifdef USE_NETWORK namespace esphome { namespace e131 { @@ -42,3 +42,4 @@ class E131AddressableLightEffect : public light::AddressableLightEffect { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index ac8b72f6e7..b8fa73b707 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,5 +1,6 @@ #include #include "e131.h" +#ifdef USE_NETWORK #include "esphome/components/network/ip_address.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -67,8 +68,8 @@ bool E131Component::join_igmp_groups_() { if (!universe.second) continue; - ip4_addr_t multicast_addr = {static_cast( - network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)))}; + ip4_addr_t multicast_addr = + network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)); auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); @@ -101,8 +102,7 @@ void E131Component::leave_(int universe) { } if (listen_method_ == E131_MULTICAST) { - ip4_addr_t multicast_addr = { - static_cast(network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))}; + ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); } @@ -138,3 +138,4 @@ bool E131Component::packet_(const std::vector &data, int &universe, E13 } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/ektf2232/ektf2232.cpp b/esphome/components/ektf2232/ektf2232.cpp deleted file mode 100644 index 80f5f8a8e2..0000000000 --- a/esphome/components/ektf2232/ektf2232.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "ektf2232.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -#include - -namespace esphome { -namespace ektf2232 { - -static const char *const TAG = "ektf2232"; - -static const uint8_t SOFT_RESET_CMD[4] = {0x77, 0x77, 0x77, 0x77}; -static const uint8_t HELLO[4] = {0x55, 0x55, 0x55, 0x55}; -static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00}; -static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; -static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; - -void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; } - -void EKTF2232Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); - this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - this->interrupt_pin_->setup(); - - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_, - gpio::INTERRUPT_FALLING_EDGE); - - this->rts_pin_->setup(); - - this->hard_reset_(); - if (!this->soft_reset_()) { - ESP_LOGE(TAG, "Failed to soft reset EKT2232!"); - this->interrupt_pin_->detach_interrupt(); - this->mark_failed(); - return; - } - - // Get touch resolution - uint8_t received[4]; - this->write(GET_X_RES, 4); - if (this->read(received, 4)) { - ESP_LOGE(TAG, "Failed to read X resolution!"); - this->interrupt_pin_->detach_interrupt(); - this->mark_failed(); - return; - } - this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); - - this->write(GET_Y_RES, 4); - if (this->read(received, 4)) { - ESP_LOGE(TAG, "Failed to read Y resolution!"); - this->interrupt_pin_->detach_interrupt(); - this->mark_failed(); - return; - } - this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); - this->store_.touch = false; - - this->set_power_state(true); -} - -void EKTF2232Touchscreen::loop() { - if (!this->store_.touch) - return; - this->store_.touch = false; - - uint8_t touch_count = 0; - std::vector touches; - - uint8_t raw[8]; - this->read(raw, 8); - for (int i = 0; i < 8; i++) { - if (raw[7] & (1 << i)) - touch_count++; - } - - if (touch_count == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - - touch_count = std::min(touch_count, 2); - - ESP_LOGV(TAG, "Touch count: %d", touch_count); - - for (int i = 0; i < touch_count; i++) { - uint8_t *d = raw + 1 + (i * 3); - uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1]; - uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2]; - - raw_x = raw_x * this->display_height_ - 1; - raw_y = raw_y * this->display_width_ - 1; - - TouchPoint tp; - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = raw_x / this->x_resolution_; - tp.x = this->display_width_ - 1 - (raw_y / this->y_resolution_); - break; - case ROTATE_90_DEGREES: - tp.x = raw_x / this->x_resolution_; - tp.y = raw_y / this->y_resolution_; - break; - case ROTATE_180_DEGREES: - tp.y = this->display_height_ - 1 - (raw_x / this->x_resolution_); - tp.x = raw_y / this->y_resolution_; - break; - case ROTATE_270_DEGREES: - tp.x = this->display_height_ - 1 - (raw_x / this->x_resolution_); - tp.y = this->display_width_ - 1 - (raw_y / this->y_resolution_); - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); - } -} - -void EKTF2232Touchscreen::set_power_state(bool enable) { - uint8_t data[] = {0x54, 0x50, 0x00, 0x01}; - data[1] |= (enable << 3); - this->write(data, 4); -} - -bool EKTF2232Touchscreen::get_power_state() { - uint8_t received[4]; - this->write(GET_POWER_STATE_CMD, 4); - this->store_.touch = false; - this->read(received, 4); - return (received[1] >> 3) & 1; -} - -void EKTF2232Touchscreen::hard_reset_() { - this->rts_pin_->digital_write(false); - delay(15); - this->rts_pin_->digital_write(true); - delay(15); -} - -bool EKTF2232Touchscreen::soft_reset_() { - auto err = this->write(SOFT_RESET_CMD, 4); - if (err != i2c::ERROR_OK) - return false; - - uint8_t received[4]; - uint16_t timeout = 1000; - while (!this->store_.touch && timeout > 0) { - delay(1); - timeout--; - } - if (timeout > 0) - this->store_.touch = true; - this->read(received, 4); - this->store_.touch = false; - - return !memcmp(received, HELLO, 4); -} - -void EKTF2232Touchscreen::dump_config() { - ESP_LOGCONFIG(TAG, "EKT2232 Touchscreen:"); - LOG_I2C_DEVICE(this); - LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); - LOG_PIN(" RTS Pin: ", this->rts_pin_); -} - -} // namespace ektf2232 -} // namespace esphome diff --git a/esphome/components/ektf2232/ektf2232.h b/esphome/components/ektf2232/ektf2232.h deleted file mode 100644 index e880b77f99..0000000000 --- a/esphome/components/ektf2232/ektf2232.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include "esphome/components/i2c/i2c.h" -#include "esphome/components/touchscreen/touchscreen.h" -#include "esphome/core/automation.h" -#include "esphome/core/component.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace ektf2232 { - -struct EKTF2232TouchscreenStore { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(EKTF2232TouchscreenStore *store); -}; - -using namespace touchscreen; - -class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { - public: - void setup() override; - void loop() override; - void dump_config() override; - - void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } - void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } - - void set_power_state(bool enable); - bool get_power_state(); - - protected: - void hard_reset_(); - bool soft_reset_(); - - InternalGPIOPin *interrupt_pin_; - GPIOPin *rts_pin_; - EKTF2232TouchscreenStore store_; - uint16_t x_resolution_; - uint16_t y_resolution_; -}; - -} // namespace ektf2232 -} // namespace esphome diff --git a/esphome/components/ektf2232/touchscreen.py b/esphome/components/ektf2232/touchscreen.py deleted file mode 100644 index d937265e7a..0000000000 --- a/esphome/components/ektf2232/touchscreen.py +++ /dev/null @@ -1,46 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv - -from esphome import pins -from esphome.components import i2c, touchscreen -from esphome.const import CONF_ID, CONF_INTERRUPT_PIN - -CODEOWNERS = ["@jesserockz"] -DEPENDENCIES = ["i2c"] - -ektf2232_ns = cg.esphome_ns.namespace("ektf2232") -EKTF2232Touchscreen = ektf2232_ns.class_( - "EKTF2232Touchscreen", - touchscreen.Touchscreen, - cg.Component, - i2c.I2CDevice, -) - -CONF_EKTF2232_ID = "ektf2232_id" -CONF_RTS_PIN = "rts_pin" - -CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(EKTF2232Touchscreen), - cv.Required(CONF_INTERRUPT_PIN): cv.All( - pins.internal_gpio_input_pin_schema - ), - cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, - } - ) - .extend(i2c.i2c_device_schema(0x15)) - .extend(cv.COMPONENT_SCHEMA) -) - - -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 touchscreen.register_touchscreen(var, config) - - interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_interrupt_pin(interrupt_pin)) - rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN]) - cg.add(var.set_rts_pin(rts_pin)) diff --git a/esphome/components/ektf2232/touchscreen/__init__.py b/esphome/components/ektf2232/touchscreen/__init__.py new file mode 100644 index 0000000000..c1fefb7f09 --- /dev/null +++ b/esphome/components/ektf2232/touchscreen/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] + +ektf2232_ns = cg.esphome_ns.namespace("ektf2232") +EKTF2232Touchscreen = ektf2232_ns.class_( + "EKTF2232Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONF_EKTF2232_ID = "ektf2232_id" +CONF_RTS_PIN = "rts_pin" + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EKTF2232Touchscreen), + cv.Required(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, + } + ).extend(i2c.i2c_device_schema(0x15)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) + rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN]) + cg.add(var.set_rts_pin(rts_pin)) diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp new file mode 100644 index 0000000000..ef8f1c6802 --- /dev/null +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -0,0 +1,132 @@ +#include "ektf2232.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace ektf2232 { + +static const char *const TAG = "ektf2232"; + +static const uint8_t SOFT_RESET_CMD[4] = {0x77, 0x77, 0x77, 0x77}; +static const uint8_t HELLO[4] = {0x55, 0x55, 0x55, 0x55}; +static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00}; +static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; +static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; + +void EKTF2232Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + + this->rts_pin_->setup(); + + this->hard_reset_(); + if (!this->soft_reset_()) { + ESP_LOGE(TAG, "Failed to soft reset EKT2232!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + + // Get touch resolution + uint8_t received[4]; + if (this->x_raw_max_ == this->x_raw_min_) { + this->write(GET_X_RES, 4); + if (this->read(received, 4)) { + ESP_LOGE(TAG, "Failed to read X resolution!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); + } + + if (this->y_raw_max_ == this->y_raw_min_) { + this->write(GET_Y_RES, 4); + if (this->read(received, 4)) { + ESP_LOGE(TAG, "Failed to read Y resolution!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); + } + this->set_power_state(true); +} + +void EKTF2232Touchscreen::update_touches() { + uint8_t touch_count = 0; + int16_t x_raw, y_raw; + + uint8_t raw[8]; + this->read(raw, 8); + for (int i = 0; i < 8; i++) { + if (raw[7] & (1 << i)) + touch_count++; + } + + touch_count = std::min(touch_count, 2); + + ESP_LOGV(TAG, "Touch count: %d", touch_count); + + for (int i = 0; i < touch_count; i++) { + uint8_t *d = raw + 1 + (i * 3); + x_raw = (d[0] & 0xF0) << 4 | d[1]; + y_raw = (d[0] & 0x0F) << 8 | d[2]; + this->add_raw_touch_position_(i, x_raw, y_raw); + } +} + +void EKTF2232Touchscreen::set_power_state(bool enable) { + uint8_t data[] = {0x54, 0x50, 0x00, 0x01}; + data[1] |= (enable << 3); + this->write(data, 4); +} + +bool EKTF2232Touchscreen::get_power_state() { + uint8_t received[4]; + this->write(GET_POWER_STATE_CMD, 4); + this->store_.touched = false; + this->read(received, 4); + return (received[1] >> 3) & 1; +} + +void EKTF2232Touchscreen::hard_reset_() { + this->rts_pin_->digital_write(false); + delay(15); + this->rts_pin_->digital_write(true); + delay(15); +} + +bool EKTF2232Touchscreen::soft_reset_() { + auto err = this->write(SOFT_RESET_CMD, 4); + if (err != i2c::ERROR_OK) + return false; + + uint8_t received[4]; + uint16_t timeout = 1000; + while (!this->store_.touched && timeout > 0) { + delay(1); + timeout--; + } + if (timeout > 0) + this->store_.touched = true; + this->read(received, 4); + this->store_.touched = false; + + return !memcmp(received, HELLO, 4); +} + +void EKTF2232Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "EKT2232 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" RTS Pin: ", this->rts_pin_); +} + +} // namespace ektf2232 +} // namespace esphome diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.h b/esphome/components/ektf2232/touchscreen/ektf2232.h new file mode 100644 index 0000000000..e9288d0a27 --- /dev/null +++ b/esphome/components/ektf2232/touchscreen/ektf2232.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ektf2232 { + +using namespace touchscreen; + +class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } + + void set_power_state(bool enable); + bool get_power_state(); + + protected: + void hard_reset_(); + bool soft_reset_(); + void update_touches() override; + + InternalGPIOPin *interrupt_pin_; + GPIOPin *rts_pin_; +}; + +} // namespace ektf2232 +} // namespace esphome diff --git a/esphome/components/emc2101/__init__.py b/esphome/components/emc2101/__init__.py new file mode 100644 index 0000000000..8012d3e897 --- /dev/null +++ b/esphome/components/emc2101/__init__.py @@ -0,0 +1,83 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_INVERTED, CONF_RESOLUTION + +CODEOWNERS = ["@ellull"] + +DEPENDENCIES = ["i2c"] + +MULTI_CONF = True + +CONF_PWM = "pwm" +CONF_DIVIDER = "divider" +CONF_DAC = "dac" +CONF_CONVERSION_RATE = "conversion_rate" + +CONF_EMC2101_ID = "emc2101_id" + +emc2101_ns = cg.esphome_ns.namespace("emc2101") + +Emc2101DACConversionRate = emc2101_ns.enum("Emc2101DACConversionRate") +CONVERSIONS_PER_SECOND = { + "1/16": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_16S, + "1/8": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_8S, + "1/4": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_4S, + "1/2": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_2S, + "1": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_SECOND, + "2": Emc2101DACConversionRate.Emc2101_DAC_2_EVERY_SECOND, + "4": Emc2101DACConversionRate.Emc2101_DAC_4_EVERY_SECOND, + "8": Emc2101DACConversionRate.Emc2101_DAC_8_EVERY_SECOND, + "16": Emc2101DACConversionRate.Emc2101_DAC_16_EVERY_SECOND, + "32": Emc2101DACConversionRate.Emc2101_DAC_32_EVERY_SECOND, +} + +Emc2101Component = emc2101_ns.class_("Emc2101Component", cg.Component, i2c.I2CDevice) + +EMC2101_COMPONENT_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_EMC2101_ID): cv.use_id(Emc2101Component), + } +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Emc2101Component), + cv.Optional(CONF_PWM): cv.Schema( + { + cv.Optional(CONF_RESOLUTION, default=23): cv.int_range( + min=0, max=31 + ), + cv.Optional(CONF_DIVIDER, default=1): cv.uint8_t, + } + ), + cv.Optional(CONF_DAC): cv.Schema( + { + cv.Optional(CONF_CONVERSION_RATE, default="16"): cv.enum( + CONVERSIONS_PER_SECOND + ), + } + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x4C)), + cv.has_exactly_one_key(CONF_PWM, CONF_DAC), +) + + +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) + + if pwm_config := config.get(CONF_PWM): + cg.add(var.set_dac_mode(False)) + cg.add(var.set_pwm_resolution(pwm_config[CONF_RESOLUTION])) + cg.add(var.set_pwm_divider(pwm_config[CONF_DIVIDER])) + if dac_config := config.get(CONF_DAC): + cg.add(var.set_dac_mode(True)) + cg.add(var.set_dac_conversion_rate(dac_config[CONF_CONVERSION_RATE])) + cg.add(var.set_inverted(config[CONF_INVERTED])) diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp new file mode 100644 index 0000000000..2f45b2e27a --- /dev/null +++ b/esphome/components/emc2101/emc2101.cpp @@ -0,0 +1,169 @@ +// Implementation based on: +// - Adafruit_EMC2101: https://github.com/adafruit/Adafruit_EMC2101 +// - Official Datasheet: https://ww1.microchip.com/downloads/en/DeviceDoc/2101.pdf + +#include "esphome/core/log.h" +#include "emc2101.h" + +namespace esphome { +namespace emc2101 { + +static const char *const TAG = "EMC2101"; + +static const uint8_t EMC2101_CHIP_ID = 0x16; // EMC2101 default device id from part id +static const uint8_t EMC2101_ALT_CHIP_ID = 0x28; // EMC2101 alternate device id from part id + +// EMC2101 registers from the datasheet. We only define what we use. +static const uint8_t EMC2101_REGISTER_INTERNAL_TEMP = 0x00; // The internal temperature register +static const uint8_t EMC2101_REGISTER_EXTERNAL_TEMP_MSB = 0x01; // high byte for the external temperature reading +static const uint8_t EMC2101_REGISTER_DAC_CONV_RATE = 0x04; // DAC convesion rate config +static const uint8_t EMC2101_REGISTER_EXTERNAL_TEMP_LSB = 0x10; // low byte for the external temperature reading +static const uint8_t EMC2101_REGISTER_CONFIG = 0x03; // configuration register +static const uint8_t EMC2101_REGISTER_TACH_LSB = 0x46; // Tach RPM data low byte +static const uint8_t EMC2101_REGISTER_TACH_MSB = 0x47; // Tach RPM data high byte +static const uint8_t EMC2101_REGISTER_FAN_CONFIG = 0x4A; // General fan config register +static const uint8_t EMC2101_REGISTER_FAN_SETTING = 0x4C; // Fan speed for non-LUT settings +static const uint8_t EMC2101_REGISTER_PWM_FREQ = 0x4D; // PWM frequency setting +static const uint8_t EMC2101_REGISTER_PWM_DIV = 0x4E; // PWM frequency divisor +static const uint8_t EMC2101_REGISTER_WHOAMI = 0xFD; // Chip ID register + +// EMC2101 configuration bits from the datasheet. We only define what we use. + +// Determines the funcionallity of the ALERT/TACH pin. +// 0 (default): The ALERT/TECH pin will function as an open drain, active low interrupt. +// 1: The ALERT/TECH pin will function as a high impedance TACH input. This may require an +// external pull-up resistor to set the proper signaling levels. +static const uint8_t EMC2101_ALT_TCH_BIT = 1 << 2; + +// Determines the FAN output mode. +// 0 (default): PWM output enabled at FAN pin. +// 1: DAC output enabled at FAN ping. +static const uint8_t EMC2101_DAC_BIT = 1 << 4; + +// Overrides the CLK_SEL bit and uses the Frequency Divide Register to determine +// the base PWM frequency. It is recommended that this bit be set for maximum PWM resolution. +// 0 (default): The base clock frequency for the PWM is determined by the CLK_SEL bit. +// 1 (recommended): The base clock that is used to determine the PWM frequency is set by the +// Frequency Divide Register +static const uint8_t EMC2101_CLK_OVR_BIT = 1 << 2; + +// Sets the polarity of the Fan output driver. +// 0 (default): The polarity of the Fan output driver is non-inverted. A '00h' setting will +// correspond to a 0% duty cycle or a minimum DAC output voltage. +// 1: The polarity of the Fan output driver is inverted. A '00h' setting will correspond to a +// 100% duty cycle or a maximum DAC output voltage. +static const uint8_t EMC2101_POLARITY_BIT = 1 << 4; + +float Emc2101Component::get_setup_priority() const { return setup_priority::HARDWARE; } + +void Emc2101Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up Emc2101 sensor..."); + + // make sure we're talking to the right chip + uint8_t chip_id = reg(EMC2101_REGISTER_WHOAMI).get(); + if ((chip_id != EMC2101_CHIP_ID) && (chip_id != EMC2101_ALT_CHIP_ID)) { + ESP_LOGE(TAG, "Wrong chip ID %02X", chip_id); + this->mark_failed(); + return; + } + + // Configure EMC2101 + i2c::I2CRegister config = reg(EMC2101_REGISTER_CONFIG); + config |= EMC2101_ALT_TCH_BIT; + if (this->dac_mode_) { + config |= EMC2101_DAC_BIT; + } + if (this->inverted_) { + config |= EMC2101_POLARITY_BIT; + } + + if (this->dac_mode_) { // DAC mode configurations + // set DAC conversion rate + reg(EMC2101_REGISTER_DAC_CONV_RATE) = this->dac_conversion_rate_; + } else { // PWM mode configurations + // set PWM divider + reg(EMC2101_REGISTER_FAN_CONFIG) |= EMC2101_CLK_OVR_BIT; + reg(EMC2101_REGISTER_PWM_DIV) = this->pwm_divider_; + + // set PWM resolution + reg(EMC2101_REGISTER_PWM_FREQ) = this->pwm_resolution_; + } +} + +void Emc2101Component::dump_config() { + ESP_LOGCONFIG(TAG, "Emc2101 component:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + } + ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? "DAC" : "PWM"); + if (this->dac_mode_) { + ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_); + } else { + ESP_LOGCONFIG(TAG, " PWM Resolution: %02X", this->pwm_resolution_); + ESP_LOGCONFIG(TAG, " PWM Divider: %02X", this->pwm_divider_); + } + ESP_LOGCONFIG(TAG, " Inverted: %s", YESNO(this->inverted_)); +} + +void Emc2101Component::set_duty_cycle(float value) { + uint8_t duty_cycle = remap(value, 0.0f, 1.0f, (uint8_t) 0, this->max_output_value_); + ESP_LOGD(TAG, "Setting duty fan setting to %02X", duty_cycle); + if (!this->write_byte(EMC2101_REGISTER_FAN_SETTING, duty_cycle)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return; + } +} + +float Emc2101Component::get_duty_cycle() { + uint8_t duty_cycle; + if (!this->read_byte(EMC2101_REGISTER_FAN_SETTING, &duty_cycle)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return NAN; + } + return remap(duty_cycle, (uint8_t) 0, this->max_output_value_, 0.0f, 1.0f); +} + +float Emc2101Component::get_internal_temperature() { + uint8_t temperature; + if (!this->read_byte(EMC2101_REGISTER_INTERNAL_TEMP, &temperature)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return NAN; + } + return temperature; +} + +float Emc2101Component::get_external_temperature() { + // Read **MSB** first to match 'Data Read Interlock' behavior from 6.1 of datasheet + uint8_t lsb, msb; + if (!this->read_byte(EMC2101_REGISTER_EXTERNAL_TEMP_MSB, &msb) || + !this->read_byte(EMC2101_REGISTER_EXTERNAL_TEMP_LSB, &lsb)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return NAN; + } + + // join msb and lsb (5 least significant bits are not used) + uint16_t raw = (msb << 8 | lsb) >> 5; + return raw * 0.125; +} + +float Emc2101Component::get_speed() { + // Read **LSB** first to match 'Data Read Interlock' behavior from 6.1 of datasheet + uint8_t lsb, msb; + if (!this->read_byte(EMC2101_REGISTER_TACH_LSB, &lsb) || !this->read_byte(EMC2101_REGISTER_TACH_MSB, &msb)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return NAN; + } + + // calculate RPMs + uint16_t tach = msb << 8 | lsb; + return tach == 0xFFFF ? 0.0f : 5400000.0f / tach; +} + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/emc2101.h b/esphome/components/emc2101/emc2101.h new file mode 100644 index 0000000000..0f4bc560dd --- /dev/null +++ b/esphome/components/emc2101/emc2101.h @@ -0,0 +1,115 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace emc2101 { + +/** Enum listing all DAC conversion rates for the EMC2101. + * + * Specific values of the enum constants are register values taken from the EMC2101 datasheet. + */ +enum Emc2101DACConversionRate { + EMC2101_DAC_1_EVERY_16_S, + EMC2101_DAC_1_EVERY_8_S, + EMC2101_DAC_1_EVERY_4_S, + EMC2101_DAC_1_EVERY_2_S, + EMC2101_DAC_1_EVERY_SECOND, + EMC2101_DAC_2_EVERY_SECOND, + EMC2101_DAC_4_EVERY_SECOND, + EMC2101_DAC_8_EVERY_SECOND, + EMC2101_DAC_16_EVERY_SECOND, + EMC2101_DAC_32_EVERY_SECOND, +}; + +/// This class includes support for the EMC2101 i2c fan controller. +/// The device has an output (PWM or DAC) and several sensors and this +/// class is for the EMC2101 configuration. +class Emc2101Component : public Component, public i2c::I2CDevice { + public: + /** Sets the mode of the output. + * + * @param dac_mode false for PWM output and true for DAC mode. + */ + void set_dac_mode(bool dac_mode) { + this->dac_mode_ = dac_mode; + this->max_output_value_ = 63; + } + + /** Sets the PWM resolution. + * + * @param resolution the PWM resolution. + */ + void set_pwm_resolution(uint8_t resolution) { + this->pwm_resolution_ = resolution; + this->max_output_value_ = 2 * resolution; + } + + /** Sets the PWM divider used to derive the PWM frequency. + * + * @param divider The PWM divider. + */ + void set_pwm_divider(uint8_t divider) { this->pwm_divider_ = divider; } + + /** Sets the DAC conversion rate (how many conversions per second). + * + * @param conversion_rate The DAC conversion rate. + */ + void set_dac_conversion_rate(Emc2101DACConversionRate conversion_rate) { + this->dac_conversion_rate_ = conversion_rate; + } + + /** Inverts the polarity of the Fan output. + * + * @param inverted Invert or not the Fan output. + */ + void set_inverted(bool inverted) { this->inverted_ = inverted; } + + /** Sets the Fan output duty cycle + * + * @param value The duty cycle value, from 0.0f to 1.0f. + */ + void set_duty_cycle(float value); + + /** Gets the Fan output duty cycle + * + * @return The duty cycle percentage from 0.0f to 1.0f. + */ + float get_duty_cycle(); + + /** Gets the internal temperature sensor reading. + * + * @return The temperature in degrees celsius. + */ + float get_internal_temperature(); + + /** Gets the external temperature sensor reading. + * + * @return The temperature in degrees celsius. + */ + float get_external_temperature(); + + /** Gets the tachometer speed sensor reading. + * + * @return The fan speed in RPMs. + */ + float get_speed(); + + /** Used by ESPHome framework. */ + void setup() override; + /** Used by ESPHome framework. */ + void dump_config() override; + /** Used by ESPHome framework. */ + float get_setup_priority() const override; + + bool dac_mode_{false}; + bool inverted_{false}; + uint8_t max_output_value_; + uint8_t pwm_resolution_; + uint8_t pwm_divider_; + Emc2101DACConversionRate dac_conversion_rate_; +}; + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/output/__init__.py b/esphome/components/emc2101/output/__init__.py new file mode 100644 index 0000000000..461ef73de1 --- /dev/null +++ b/esphome/components/emc2101/output/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_ID +from .. import EMC2101_COMPONENT_SCHEMA, CONF_EMC2101_ID, emc2101_ns + +DEPENDENCIES = ["emc2101"] + +EMC2101Output = emc2101_ns.class_("EMC2101Output", output.FloatOutput) + +CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(EMC2101Output), + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_EMC2101_ID]) + var = cg.new_Pvariable(config[CONF_ID], paren) + await output.register_output(var, config) diff --git a/esphome/components/emc2101/output/emc2101_output.cpp b/esphome/components/emc2101/output/emc2101_output.cpp new file mode 100644 index 0000000000..2ed506cd99 --- /dev/null +++ b/esphome/components/emc2101/output/emc2101_output.cpp @@ -0,0 +1,9 @@ +#include "emc2101_output.h" + +namespace esphome { +namespace emc2101 { + +void EMC2101Output::write_state(float state) { this->parent_->set_duty_cycle(state); } + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/output/emc2101_output.h b/esphome/components/emc2101/output/emc2101_output.h new file mode 100644 index 0000000000..232df6ff5f --- /dev/null +++ b/esphome/components/emc2101/output/emc2101_output.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../emc2101.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace emc2101 { + +/// This class allows to control the EMC2101 output. +class EMC2101Output : public output::FloatOutput { + public: + EMC2101Output(Emc2101Component *parent) : parent_(parent) {} + + protected: + /** Used by ESPHome framework. */ + void write_state(float state) override; + + Emc2101Component *parent_; +}; + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/sensor/__init__.py b/esphome/components/emc2101/sensor/__init__.py new file mode 100644 index 0000000000..10ea3dfae6 --- /dev/null +++ b/esphome/components/emc2101/sensor/__init__.py @@ -0,0 +1,74 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_EXTERNAL_TEMPERATURE, + CONF_ID, + CONF_INTERNAL_TEMPERATURE, + CONF_SPEED, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_REVOLUTIONS_PER_MINUTE, + ICON_PERCENT, +) +from .. import EMC2101_COMPONENT_SCHEMA, CONF_EMC2101_ID, emc2101_ns + +DEPENDENCIES = ["emc2101"] + +CONF_DUTY_CYCLE = "duty_cycle" + +EMC2101Sensor = emc2101_ns.class_("EMC2101Sensor", cg.PollingComponent) + +CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EMC2101Sensor), + cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + 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=3, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:fan", + ), + cv.Optional(CONF_DUTY_CYCLE): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_PERCENT, + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_EMC2101_ID]) + var = cg.new_Pvariable(config[CONF_ID], paren) + await cg.register_component(var, config) + + if CONF_INTERNAL_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_INTERNAL_TEMPERATURE]) + cg.add(var.set_internal_temperature_sensor(sens)) + + if CONF_EXTERNAL_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_EXTERNAL_TEMPERATURE]) + cg.add(var.set_external_temperature_sensor(sens)) + + if CONF_SPEED in config: + sens = await sensor.new_sensor(config[CONF_SPEED]) + cg.add(var.set_speed_sensor(sens)) + + if CONF_DUTY_CYCLE in config: + sens = await sensor.new_sensor(config[CONF_DUTY_CYCLE]) + cg.add(var.set_duty_cycle_sensor(sens)) diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.cpp b/esphome/components/emc2101/sensor/emc2101_sensor.cpp new file mode 100644 index 0000000000..2a199f48e9 --- /dev/null +++ b/esphome/components/emc2101/sensor/emc2101_sensor.cpp @@ -0,0 +1,43 @@ +#include "emc2101_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace emc2101 { + +static const char *const TAG = "EMC2101.sensor"; + +float EMC2101Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void EMC2101Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "Emc2101 sensor:"); + LOG_SENSOR(" ", "Internal temperature", this->internal_temperature_sensor_); + LOG_SENSOR(" ", "External temperature", this->external_temperature_sensor_); + LOG_SENSOR(" ", "Speed", this->speed_sensor_); + LOG_SENSOR(" ", "Duty cycle", this->duty_cycle_sensor_); +} + +void EMC2101Sensor::update() { + if (this->internal_temperature_sensor_ != nullptr) { + float internal_temperature = this->parent_->get_internal_temperature(); + this->internal_temperature_sensor_->publish_state(internal_temperature); + } + + if (this->external_temperature_sensor_ != nullptr) { + float external_temperature = this->parent_->get_external_temperature(); + this->external_temperature_sensor_->publish_state(external_temperature); + } + + if (this->speed_sensor_ != nullptr) { + float speed = this->parent_->get_speed(); + this->speed_sensor_->publish_state(speed); + } + + if (this->duty_cycle_sensor_ != nullptr) { + float duty_cycle = this->parent_->get_duty_cycle(); + this->duty_cycle_sensor_->publish_state(duty_cycle * 100.0f); + } +} + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.h b/esphome/components/emc2101/sensor/emc2101_sensor.h new file mode 100644 index 0000000000..3e8dcebc8e --- /dev/null +++ b/esphome/components/emc2101/sensor/emc2101_sensor.h @@ -0,0 +1,39 @@ +#pragma once + +#include "../emc2101.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace emc2101 { + +/// This class exposes the EMC2101 sensors. +class EMC2101Sensor : public PollingComponent { + public: + EMC2101Sensor(Emc2101Component *parent) : parent_(parent) {} + /** Used by ESPHome framework. */ + void dump_config() override; + /** Used by ESPHome framework. */ + void update() override; + /** Used by ESPHome framework. */ + float get_setup_priority() const override; + + /** Used by ESPHome framework. */ + void set_internal_temperature_sensor(sensor::Sensor *sensor) { this->internal_temperature_sensor_ = sensor; } + /** Used by ESPHome framework. */ + void set_external_temperature_sensor(sensor::Sensor *sensor) { this->external_temperature_sensor_ = sensor; } + /** Used by ESPHome framework. */ + void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; } + /** Used by ESPHome framework. */ + void set_duty_cycle_sensor(sensor::Sensor *sensor) { this->duty_cycle_sensor_ = sensor; } + + protected: + Emc2101Component *parent_; + sensor::Sensor *internal_temperature_sensor_{nullptr}; + sensor::Sensor *external_temperature_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *duty_cycle_sensor_{nullptr}; +}; + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emmeti/__init__.py b/esphome/components/emmeti/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/emmeti/climate.py b/esphome/components/emmeti/climate.py new file mode 100644 index 0000000000..36585061e6 --- /dev/null +++ b/esphome/components/emmeti/climate.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +CODEOWNERS = ["@E440QF"] +AUTO_LOAD = ["climate_ir"] + +emmeti_ns = cg.esphome_ns.namespace("emmeti") +EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EmmetiClimate), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp new file mode 100644 index 0000000000..3cb184f868 --- /dev/null +++ b/esphome/components/emmeti/emmeti.cpp @@ -0,0 +1,316 @@ +#include "emmeti.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace emmeti { + +static const char *const TAG = "emmeti.climate"; + +// setters +uint8_t EmmetiClimate::set_temp_() { + return (uint8_t) roundf(clamp(this->target_temperature, EMMETI_TEMP_MIN, EMMETI_TEMP_MAX) - EMMETI_TEMP_MIN); +} + +uint8_t EmmetiClimate::set_mode_() { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + return EMMETI_MODE_COOL; + case climate::CLIMATE_MODE_DRY: + return EMMETI_MODE_DRY; + case climate::CLIMATE_MODE_HEAT: + return EMMETI_MODE_HEAT; + case climate::CLIMATE_MODE_FAN_ONLY: + return EMMETI_MODE_FAN; + case climate::CLIMATE_MODE_HEAT_COOL: + default: + return EMMETI_MODE_HEAT_COOL; + } +} + +uint8_t EmmetiClimate::set_fan_speed_() { + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + return EMMETI_FAN_1; + case climate::CLIMATE_FAN_MEDIUM: + return EMMETI_FAN_2; + case climate::CLIMATE_FAN_HIGH: + return EMMETI_FAN_3; + case climate::CLIMATE_FAN_AUTO: + default: + return EMMETI_FAN_AUTO; + } +} + +uint8_t EmmetiClimate::set_blades_() { + if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) { + switch (this->blades_) { + case EMMETI_BLADES_1: + case EMMETI_BLADES_2: + case EMMETI_BLADES_HIGH: + this->blades_ = EMMETI_BLADES_HIGH; + break; + case EMMETI_BLADES_3: + case EMMETI_BLADES_MID: + this->blades_ = EMMETI_BLADES_MID; + break; + case EMMETI_BLADES_4: + case EMMETI_BLADES_5: + case EMMETI_BLADES_LOW: + this->blades_ = EMMETI_BLADES_LOW; + break; + default: + this->blades_ = EMMETI_BLADES_FULL; + break; + } + } else { + switch (this->blades_) { + case EMMETI_BLADES_1: + case EMMETI_BLADES_2: + case EMMETI_BLADES_HIGH: + this->blades_ = EMMETI_BLADES_1; + break; + case EMMETI_BLADES_3: + case EMMETI_BLADES_MID: + this->blades_ = EMMETI_BLADES_3; + break; + case EMMETI_BLADES_4: + case EMMETI_BLADES_5: + case EMMETI_BLADES_LOW: + this->blades_ = EMMETI_BLADES_5; + break; + default: + this->blades_ = EMMETI_BLADES_STOP; + break; + } + } + return this->blades_; +} + +uint8_t EmmetiClimate::gen_checksum_() { return (this->set_temp_() + this->set_mode_() + 2) % 16; } + +// getters +float EmmetiClimate::get_temp_(uint8_t temp) { return (float) (temp + EMMETI_TEMP_MIN); } + +climate::ClimateMode EmmetiClimate::get_mode_(uint8_t mode) { + switch (mode) { + case EMMETI_MODE_COOL: + return climate::CLIMATE_MODE_COOL; + case EMMETI_MODE_DRY: + return climate::CLIMATE_MODE_DRY; + case EMMETI_MODE_HEAT: + return climate::CLIMATE_MODE_HEAT; + case EMMETI_MODE_HEAT_COOL: + return climate::CLIMATE_MODE_HEAT_COOL; + case EMMETI_MODE_FAN: + return climate::CLIMATE_MODE_FAN_ONLY; + default: + return climate::CLIMATE_MODE_HEAT_COOL; + } +} + +climate::ClimateFanMode EmmetiClimate::get_fan_speed_(uint8_t fan_speed) { + switch (fan_speed) { + case EMMETI_FAN_1: + return climate::CLIMATE_FAN_LOW; + case EMMETI_FAN_2: + return climate::CLIMATE_FAN_MEDIUM; + case EMMETI_FAN_3: + return climate::CLIMATE_FAN_HIGH; + case EMMETI_FAN_AUTO: + default: + return climate::CLIMATE_FAN_AUTO; + } +} + +climate::ClimateSwingMode EmmetiClimate::get_swing_(uint8_t bitmap) { + return (bitmap >> 1) & 0x01 ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; +} + +template T EmmetiClimate::reverse_(T val, size_t len) { + T result = 0; + for (size_t i = 0; i < len; i++) { + result |= ((val & 1 << i) != 0) << (len - 1 - i); + } + return result; +} + +template void EmmetiClimate::add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data) { + for (size_t i = len; i > 0; i--) { + data->mark(EMMETI_BIT_MARK); + data->space((val & (1 << (i - 1))) ? EMMETI_ONE_SPACE : EMMETI_ZERO_SPACE); + } +} + +template void EmmetiClimate::add_(T val, esphome::remote_base::RemoteTransmitData *data) { + data->mark(EMMETI_BIT_MARK); + data->space((val & 1) ? EMMETI_ONE_SPACE : EMMETI_ZERO_SPACE); +} + +template +void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data) { + this->add_(this->reverse_(val, len), len, data); +} + +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); + + return checksum == expected; +} + +void EmmetiClimate::transmit_state() { + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(EMMETI_IR_FREQUENCY); + + data->mark(EMMETI_HEADER_MARK); + data->space(EMMETI_HEADER_SPACE); + + if (this->mode != climate::CLIMATE_MODE_OFF) { + this->reverse_add_(this->set_mode_(), 3, data); + this->add_(1, data); + this->reverse_add_(this->set_fan_speed_(), 2, data); + this->add_(this->swing_mode != climate::CLIMATE_SWING_OFF, data); + this->add_(0, data); // sleep mode + this->reverse_add_(this->set_temp_(), 4, data); + this->add_(0, 8, data); // zeros + this->add_(0, data); // turbo mode + this->add_(1, data); // light + this->add_(1, data); // tree icon thingy + this->add_(0, data); // blow mode + this->add_(0x52, 11, data); // idk + + data->mark(EMMETI_BIT_MARK); + data->space(EMMETI_MESSAGE_SPACE); + + this->reverse_add_(this->set_blades_(), 4, data); + this->add_(0, 4, data); // zeros + this->reverse_add_(2, 2, data); // thermometer + this->add_(0, 18, data); // zeros + this->reverse_add_(this->gen_checksum_(), 4, data); + } else { + this->add_(9, 12, data); + this->add_(0, 8, data); + this->add_(0x2052, 15, data); + data->mark(EMMETI_BIT_MARK); + data->space(EMMETI_MESSAGE_SPACE); + this->add_(0, 8, data); + this->add_(1, 2, data); + this->add_(0, 18, data); + this->add_(0x0C, 4, data); + } + data->mark(EMMETI_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +bool EmmetiClimate::parse_state_frame_(EmmetiState curr_state) { + this->mode = this->get_mode_(curr_state.mode); + this->fan_mode = this->get_fan_speed_(curr_state.fan_speed); + this->target_temperature = this->get_temp_(curr_state.temp); + this->swing_mode = this->get_swing_(curr_state.bitmap); + // this->blades_ = curr_state.fan_pos; + if (!(curr_state.bitmap & 0x01)) { + this->mode = climate::CLIMATE_MODE_OFF; + } + + this->publish_state(); + return true; +} + +bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { + if (!data.expect_item(EMMETI_HEADER_MARK, EMMETI_HEADER_SPACE)) { + return false; + } + ESP_LOGD(TAG, "Received emmeti frame"); + + EmmetiState curr_state; + + for (size_t pos = 0; pos < 3; pos++) { + if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { + curr_state.mode |= 1 << pos; + } else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) { + return false; + } + } + + ESP_LOGD(TAG, "Mode: %d", curr_state.mode); + + if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { + curr_state.bitmap |= 1 << 0; + } else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) { + return false; + } + + ESP_LOGD(TAG, "On: %d", curr_state.bitmap & 0x01); + + for (size_t pos = 0; pos < 2; pos++) { + if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { + curr_state.fan_speed |= 1 << pos; + } else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) { + return false; + } + } + + ESP_LOGD(TAG, "Fan speed: %d", curr_state.fan_speed); + + for (size_t pos = 0; pos < 2; pos++) { + if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { + curr_state.bitmap |= 1 << (pos + 1); + } else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) { + return false; + } + } + + ESP_LOGD(TAG, "Swing: %d", (curr_state.bitmap >> 1) & 0x01); + ESP_LOGD(TAG, "Sleep: %d", (curr_state.bitmap >> 2) & 0x01); + + for (size_t pos = 0; pos < 4; pos++) { + if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { + curr_state.temp |= 1 << pos; + } else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) { + return false; + } + } + + ESP_LOGD(TAG, "Temp: %d", curr_state.temp); + + for (size_t pos = 0; pos < 8; pos++) { + if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) { + return false; + } + } + + for (size_t pos = 0; pos < 4; pos++) { + if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { + curr_state.bitmap |= 1 << (pos + 3); + } else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) { + return false; + } + } + + 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); + + uint16_t control_data = 0; + for (size_t pos = 0; pos < 11; pos++) { + if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { + control_data |= 1 << pos; + } else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) { + return false; + } + } + + if (control_data != 0x250) { + return false; + } + + return this->parse_state_frame_(curr_state); +} + +} // namespace emmeti +} // namespace esphome diff --git a/esphome/components/emmeti/emmeti.h b/esphome/components/emmeti/emmeti.h new file mode 100644 index 0000000000..9bfb7a7a98 --- /dev/null +++ b/esphome/components/emmeti/emmeti.h @@ -0,0 +1,109 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace emmeti { + +const uint8_t EMMETI_TEMP_MIN = 16; // Celsius +const uint8_t EMMETI_TEMP_MAX = 30; // Celsius + +// Modes + +enum EmmetiMode : uint8_t { + EMMETI_MODE_HEAT_COOL = 0x00, + EMMETI_MODE_COOL = 0x01, + EMMETI_MODE_DRY = 0x02, + EMMETI_MODE_FAN = 0x03, + EMMETI_MODE_HEAT = 0x04, +}; + +// Fan Speed + +enum EmmetiFanMode : uint8_t { + EMMETI_FAN_AUTO = 0x00, + EMMETI_FAN_1 = 0x01, + EMMETI_FAN_2 = 0x02, + EMMETI_FAN_3 = 0x03, +}; + +// Fan Position + +enum EmmetiBlades : uint8_t { + EMMETI_BLADES_STOP = 0x00, + EMMETI_BLADES_FULL = 0x01, + EMMETI_BLADES_1 = 0x02, + EMMETI_BLADES_2 = 0x03, + EMMETI_BLADES_3 = 0x04, + EMMETI_BLADES_4 = 0x05, + EMMETI_BLADES_5 = 0x06, + EMMETI_BLADES_LOW = 0x07, + EMMETI_BLADES_MID = 0x09, + EMMETI_BLADES_HIGH = 0x11, +}; + +// IR Transmission +const uint32_t EMMETI_IR_FREQUENCY = 38000; +const uint32_t EMMETI_HEADER_MARK = 9076; +const uint32_t EMMETI_HEADER_SPACE = 4408; +const uint32_t EMMETI_BIT_MARK = 660; +const uint32_t EMMETI_ONE_SPACE = 1630; +const uint32_t EMMETI_ZERO_SPACE = 530; +const uint32_t EMMETI_MESSAGE_SPACE = 20000; + +struct EmmetiState { + uint8_t mode = 0; + uint8_t bitmap = 0; + uint8_t fan_speed = 0; + uint8_t temp = 0; + uint8_t fan_pos = 0; + uint8_t th = 0; + uint8_t checksum = 0; +}; + +class EmmetiClimate : public climate_ir::ClimateIR { + public: + EmmetiClimate() + : climate_ir::ClimateIR(EMMETI_TEMP_MIN, EMMETI_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + protected: + // Transmit via IR the state of this climate controller + void transmit_state() override; + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(EmmetiState curr_state); + + // setters + uint8_t set_mode_(); + uint8_t set_temp_(); + uint8_t set_fan_speed_(); + uint8_t gen_checksum_(); + uint8_t set_blades_(); + + // getters + climate::ClimateMode get_mode_(uint8_t mode); + climate::ClimateFanMode get_fan_speed_(uint8_t fan); + void get_blades_(uint8_t fanpos); + // get swing + climate::ClimateSwingMode get_swing_(uint8_t bitmap); + float get_temp_(uint8_t temp); + + // check if the received frame is valid + bool check_checksum_(uint8_t checksum); + + template T reverse_(T val, size_t len); + + template void add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *ata); + + template void add_(T val, esphome::remote_base::RemoteTransmitData *data); + + template void reverse_add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data); + + uint8_t blades_ = EMMETI_BLADES_STOP; +}; + +} // namespace emmeti +} // namespace esphome diff --git a/esphome/components/ens160/__init__.py b/esphome/components/ens160/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ens160/sensor.py b/esphome/components/ens160/sensor.py new file mode 100644 index 0000000000..441671fb7b --- /dev/null +++ b/esphome/components/ens160/sensor.py @@ -0,0 +1,7 @@ +import esphome.config_validation as cv + +CODEOWNERS = ["@latonita"] + +CONFIG_SCHEMA = cv.invalid( + "The ens160 sensor component has been renamed to ens160_i2c." +) diff --git a/esphome/components/ens160_base/__init__.py b/esphome/components/ens160_base/__init__.py new file mode 100644 index 0000000000..eb6d0880af --- /dev/null +++ b/esphome/components/ens160_base/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_COMPENSATION, + CONF_ECO2, + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + CONF_TVOC, + DEVICE_CLASS_AQI, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ICON_CHEMICAL_WEAPON, + ICON_MOLECULE_CO2, + ICON_RADIATOR, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_BILLION, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@vincentscode", "@latonita"] + +ens160_ns = cg.esphome_ns.namespace("ens160_base") + +CONF_AQI = "aqi" +UNIT_INDEX = "index" + +CONFIG_SCHEMA_BASE = cv.Schema( + { + cv.Required(CONF_ECO2): 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.Required(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_AQI): sensor.sensor_schema( + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_COMPENSATION): cv.Schema( + { + cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), + cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), + } + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + sens = await sensor.new_sensor(config[CONF_ECO2]) + cg.add(var.set_co2(sens)) + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) + sens = await sensor.new_sensor(config[CONF_AQI]) + cg.add(var.set_aqi(sens)) + + if compensation_config := config.get(CONF_COMPENSATION): + sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + sens = await cg.get_variable(compensation_config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + + return var diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp new file mode 100644 index 0000000000..71082c58c2 --- /dev/null +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -0,0 +1,320 @@ +// ENS160 sensor with I2C interface from ScioSense +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/documents/SC-001224-DS-7-ENS160-Datasheet.pdf +// +// Implementation based on: +// https://github.com/sciosense/ENS160_driver + +#include "ens160_base.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ens160_base { + +static const char *const TAG = "ens160"; + +static const uint8_t ENS160_BOOTING = 10; + +static const uint16_t ENS160_PART_ID = 0x0160; + +static const uint8_t ENS160_REG_PART_ID = 0x00; +static const uint8_t ENS160_REG_OPMODE = 0x10; +static const uint8_t ENS160_REG_CONFIG = 0x11; +static const uint8_t ENS160_REG_COMMAND = 0x12; +static const uint8_t ENS160_REG_TEMP_IN = 0x13; +static const uint8_t ENS160_REG_DATA_STATUS = 0x20; +static const uint8_t ENS160_REG_DATA_AQI = 0x21; +static const uint8_t ENS160_REG_DATA_TVOC = 0x22; +static const uint8_t ENS160_REG_DATA_ECO2 = 0x24; + +static const uint8_t ENS160_REG_GPR_READ_0 = 0x48; +static const uint8_t ENS160_REG_GPR_READ_4 = ENS160_REG_GPR_READ_0 + 4; + +static const uint8_t ENS160_COMMAND_NOP = 0x00; +static const uint8_t ENS160_COMMAND_CLRGPR = 0xCC; +static const uint8_t ENS160_COMMAND_GET_APPVER = 0x0E; + +static const uint8_t ENS160_OPMODE_RESET = 0xF0; +static const uint8_t ENS160_OPMODE_IDLE = 0x01; +static const uint8_t ENS160_OPMODE_STD = 0x02; + +static const uint8_t ENS160_DATA_STATUS_STATAS = 0x80; +static const uint8_t ENS160_DATA_STATUS_STATER = 0x40; +static const uint8_t ENS160_DATA_STATUS_VALIDITY = 0x0C; +static const uint8_t ENS160_DATA_STATUS_NEWDAT = 0x02; +static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01; + +// helps remove reserved bits in aqi data register +static const uint8_t ENS160_DATA_AQI = 0x07; + +void ENS160Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ENS160..."); + + // check part_id + uint16_t part_id; + if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast(&part_id), 2)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + if (part_id != ENS160_PART_ID) { + this->error_code_ = INVALID_ID; + this->mark_failed(); + return; + } + + // set mode to reset + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + delay(ENS160_BOOTING); + + // check status + uint8_t status_value; + if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + this->validity_flag_ = static_cast((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); + + if (this->validity_flag_ == INVALID_OUTPUT) { + this->error_code_ = VALIDITY_INVALID; + this->mark_failed(); + return; + } + + // set mode to idle + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + // clear command + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + + // read firmware version + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + uint8_t version_data[3]; + if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + this->firmware_ver_major_ = version_data[0]; + this->firmware_ver_minor_ = version_data[1]; + this->firmware_ver_build_ = version_data[2]; + + // set mode to standard + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + + // read opmode and check standard mode is achieved before finishing Setup + uint8_t op_mode; + if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + + if (op_mode != ENS160_OPMODE_STD) { + this->error_code_ = STD_OPMODE_FAILED; + this->mark_failed(); + return; + } +} + +void ENS160Component::update() { + uint8_t status_value, data_ready; + + if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) { + ESP_LOGW(TAG, "Error reading status register"); + this->status_set_warning(); + return; + } + + // 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", + (ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR); + + data_ready = ENS160_DATA_STATUS_NEWDAT & status_value; + this->validity_flag_ = static_cast((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); + + switch (validity_flag_) { + case NORMAL_OPERATION: + if (data_ready != ENS160_DATA_STATUS_NEWDAT) { + ESP_LOGD(TAG, "ENS160 readings unavailable - Normal Operation but readings not ready"); + return; + } + break; + case INITIAL_STARTUP: + if (!this->initial_startup_) { + this->initial_startup_ = true; + ESP_LOGI(TAG, "ENS160 readings unavailable - 1 hour startup required after first power on"); + } + return; + case WARMING_UP: + if (!this->warming_up_) { + this->warming_up_ = true; + ESP_LOGI(TAG, "ENS160 readings not available yet - Warming up requires 3 minutes"); + this->send_env_data_(); + } + return; + case INVALID_OUTPUT: + ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output"); + this->status_set_warning(); + return; + } + + // read new data + uint16_t data_eco2; + if (!this->read_bytes(ENS160_REG_DATA_ECO2, reinterpret_cast(&data_eco2), 2)) { + ESP_LOGW(TAG, "Error reading eCO2 data register"); + this->status_set_warning(); + return; + } + if (this->co2_ != nullptr) { + this->co2_->publish_state(data_eco2); + } + + uint16_t data_tvoc; + if (!this->read_bytes(ENS160_REG_DATA_TVOC, reinterpret_cast(&data_tvoc), 2)) { + ESP_LOGW(TAG, "Error reading TVOC data register"); + this->status_set_warning(); + return; + } + if (this->tvoc_ != nullptr) { + this->tvoc_->publish_state(data_tvoc); + } + + uint8_t data_aqi; + if (!this->read_byte(ENS160_REG_DATA_AQI, &data_aqi)) { + ESP_LOGW(TAG, "Error reading AQI data register"); + this->status_set_warning(); + return; + } + if (this->aqi_ != nullptr) { + // remove reserved bits, just in case they are used in future + data_aqi = ENS160_DATA_AQI & data_aqi; + + this->aqi_->publish_state(data_aqi); + } + + this->status_clear_warning(); + + // set temperature and humidity compensation data + this->send_env_data_(); +} + +void ENS160Component::send_env_data_() { + if (this->temperature_ == nullptr && this->humidity_ == nullptr) + return; + + float temperature = NAN; + if (this->temperature_ != nullptr) + temperature = this->temperature_->state; + + if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + ESP_LOGW(TAG, "Invalid external temperature - compensation values not updated"); + return; + } else { + ESP_LOGV(TAG, "External temperature compensation: %.1f°C", temperature); + } + + float humidity = NAN; + if (this->humidity_ != nullptr) + humidity = this->humidity_->state; + + if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + ESP_LOGW(TAG, "Invalid external humidity - compensation values not updated"); + return; + } else { + ESP_LOGV(TAG, "External humidity compensation: %.1f%%", humidity); + } + + uint16_t t = (uint16_t) ((temperature + 273.15f) * 64.0f); + uint16_t h = (uint16_t) (humidity * 512.0f); + + uint8_t data[4]; + data[0] = t & 0xff; + data[1] = (t >> 8) & 0xff; + data[2] = h & 0xff; + data[3] = (h >> 8) & 0xff; + + if (!this->write_bytes(ENS160_REG_TEMP_IN, data, 4)) { + ESP_LOGE(TAG, "Error writing compensation values"); + this->status_set_warning(); + return; + } +} + +void ENS160Component::dump_config() { + ESP_LOGCONFIG(TAG, "ENS160:"); + + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication failed! Is the sensor connected?"); + break; + case READ_FAILED: + ESP_LOGE(TAG, "Error reading from register"); + break; + case WRITE_FAILED: + ESP_LOGE(TAG, "Error writing to register"); + break; + case INVALID_ID: + ESP_LOGE(TAG, "Sensor reported an invalid ID. Is this a ENS160?"); + break; + case VALIDITY_INVALID: + ESP_LOGE(TAG, "Invalid Device Status - No valid output"); + break; + case STD_OPMODE_FAILED: + ESP_LOGE(TAG, "Device failed to achieve Standard Operating Mode"); + break; + case NONE: + ESP_LOGD(TAG, "Setup successful"); + break; + } + ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_, + this->firmware_ver_build_); + + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "CO2 Sensor:", this->co2_); + LOG_SENSOR(" ", "TVOC Sensor:", this->tvoc_); + LOG_SENSOR(" ", "AQI Sensor:", this->aqi_); + + if (this->temperature_ != nullptr && this->humidity_ != nullptr) { + LOG_SENSOR(" ", " Temperature Compensation:", this->temperature_); + LOG_SENSOR(" ", " Humidity Compensation:", this->humidity_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: Not configured"); + } +} + +} // namespace ens160_base +} // namespace esphome diff --git a/esphome/components/ens160_base/ens160_base.h b/esphome/components/ens160_base/ens160_base.h new file mode 100644 index 0000000000..729225a5ae --- /dev/null +++ b/esphome/components/ens160_base/ens160_base.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace ens160_base { + +class ENS160Component : public PollingComponent, public sensor::Sensor { + public: + void set_co2(sensor::Sensor *co2) { co2_ = co2; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + void set_aqi(sensor::Sensor *aqi) { aqi_ = aqi; } + + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void send_env_data_(); + + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + INVALID_ID, + VALIDITY_INVALID, + READ_FAILED, + WRITE_FAILED, + STD_OPMODE_FAILED, + } error_code_{NONE}; + + enum ValidityFlag { + NORMAL_OPERATION = 0, + WARMING_UP, + INITIAL_STARTUP, + INVALID_OUTPUT, + } validity_flag_; + + bool warming_up_{false}; + bool initial_startup_{false}; + + virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + + uint8_t firmware_ver_major_{0}; + uint8_t firmware_ver_minor_{0}; + uint8_t firmware_ver_build_{0}; + + sensor::Sensor *co2_{nullptr}; + sensor::Sensor *tvoc_{nullptr}; + sensor::Sensor *aqi_{nullptr}; + + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *temperature_{nullptr}; +}; + +} // namespace ens160_base +} // namespace esphome diff --git a/esphome/components/ens160_i2c/__init__.py b/esphome/components/ens160_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ens160_i2c/ens160_i2c.cpp b/esphome/components/ens160_i2c/ens160_i2c.cpp new file mode 100644 index 0000000000..7163a5ad6e --- /dev/null +++ b/esphome/components/ens160_i2c/ens160_i2c.cpp @@ -0,0 +1,32 @@ +#include +#include + +#include "ens160_i2c.h" +#include "esphome/components/i2c/i2c.h" +#include "../ens160_base/ens160_base.h" + +namespace esphome { +namespace ens160_i2c { + +static const char *const TAG = "ens160_i2c.sensor"; + +bool ENS160I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { + return I2CDevice::read_byte(a_register, data); +}; +bool ENS160I2CComponent::write_byte(uint8_t a_register, uint8_t data) { + return I2CDevice::write_byte(a_register, data); +}; +bool ENS160I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::read_bytes(a_register, data, len); +}; +bool ENS160I2CComponent::write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::write_bytes(a_register, data, len); +}; + +void ENS160I2CComponent::dump_config() { + ENS160Component::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace ens160_i2c +} // namespace esphome diff --git a/esphome/components/ens160_i2c/ens160_i2c.h b/esphome/components/ens160_i2c/ens160_i2c.h new file mode 100644 index 0000000000..2df32f27bf --- /dev/null +++ b/esphome/components/ens160_i2c/ens160_i2c.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/ens160_base/ens160_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ens160_i2c { + +class ENS160I2CComponent : public esphome::ens160_base::ENS160Component, public i2c::I2CDevice { + void dump_config() override; + + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; +}; + +} // namespace ens160_i2c +} // namespace esphome diff --git a/esphome/components/ens160_i2c/sensor.py b/esphome/components/ens160_i2c/sensor.py new file mode 100644 index 0000000000..96cbbaa7e9 --- /dev/null +++ b/esphome/components/ens160_i2c/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +from esphome.components import i2c +from ..ens160_base import to_code_base, cv, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["ens160_base"] +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +ens160_ns = cg.esphome_ns.namespace("ens160_i2c") + +ENS160I2CComponent = ens160_ns.class_( + "ENS160I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x52) +).extend({cv.GenerateID(): cv.declare_id(ENS160I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ens160_spi/__init__.py b/esphome/components/ens160_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ens160_spi/ens160_spi.cpp b/esphome/components/ens160_spi/ens160_spi.cpp new file mode 100644 index 0000000000..fba2fdf0e4 --- /dev/null +++ b/esphome/components/ens160_spi/ens160_spi.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include "ens160_spi.h" +#include + +namespace esphome { +namespace ens160_spi { + +static const char *const TAG = "ens160_spi.sensor"; + +inline uint8_t reg_read(uint8_t reg) { return (reg << 1) | 0x01; } + +inline uint8_t reg_write(uint8_t reg) { return (reg << 1) & 0xFE; } + +void ENS160SPIComponent::setup() { + this->spi_setup(); + ENS160Component::setup(); +}; + +void ENS160SPIComponent::dump_config() { + ENS160Component::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +bool ENS160SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(reg_read(a_register)); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool ENS160SPIComponent::write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(reg_write(a_register)); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool ENS160SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(reg_read(a_register)); + this->read_array(data, len); + this->disable(); + return true; +} + +bool ENS160SPIComponent::write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(reg_write(a_register)); + this->transfer_array(data, len); + this->disable(); + return true; +} + +} // namespace ens160_spi +} // namespace esphome diff --git a/esphome/components/ens160_spi/ens160_spi.h b/esphome/components/ens160_spi/ens160_spi.h new file mode 100644 index 0000000000..3371f37ffd --- /dev/null +++ b/esphome/components/ens160_spi/ens160_spi.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/ens160_base/ens160_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ens160_spi { + +class ENS160SPIComponent : public esphome::ens160_base::ENS160Component, + public spi::SPIDevice { + void setup() override; + void dump_config() override; + + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; +}; + +} // namespace ens160_spi +} // namespace esphome diff --git a/esphome/components/ens160_spi/sensor.py b/esphome/components/ens160_spi/sensor.py new file mode 100644 index 0000000000..552697fe1b --- /dev/null +++ b/esphome/components/ens160_spi/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +from esphome.components import spi +from ..ens160_base import to_code_base, cv, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["ens160_base"] +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["spi"] + +ens160_spi_ns = cg.esphome_ns.namespace("ens160_spi") + +ENS160SPIComponent = ens160_spi_ns.class_( + "ENS160SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend( + {cv.GenerateID(): cv.declare_id(ENS160SPIComponent)} +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/es8311/__init__.py b/esphome/components/es8311/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/es8311/audio_dac.py b/esphome/components/es8311/audio_dac.py new file mode 100644 index 0000000000..1b450c3c11 --- /dev/null +++ b/esphome/components/es8311/audio_dac.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.audio_dac import AudioDac +import esphome.config_validation as cv +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE + +CODEOWNERS = ["@kroimon", "@kahrendt"] +DEPENDENCIES = ["i2c"] + +es8311_ns = cg.esphome_ns.namespace("es8311") +ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice) + +CONF_MIC_GAIN = "mic_gain" +CONF_USE_MCLK = "use_mclk" +CONF_USE_MICROPHONE = "use_microphone" + +es8311_resolution = es8311_ns.enum("ES8311Resolution") +ES8311_BITS_PER_SAMPLE_ENUM = { + 16: es8311_resolution.ES8311_RESOLUTION_16, + 24: es8311_resolution.ES8311_RESOLUTION_24, + 32: es8311_resolution.ES8311_RESOLUTION_32, +} + +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, + "18DB": es8311_mic_gain.ES8311_MIC_GAIN_18DB, + "24DB": es8311_mic_gain.ES8311_MIC_GAIN_24DB, + "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, +} + + +_validate_bits = cv.float_with_unit("bits", "bit") + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ES8311), + cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( + _validate_bits, cv.enum(ES8311_BITS_PER_SAMPLE_ENUM) + ), + cv.Optional(CONF_MIC_GAIN, default="42DB"): cv.enum( + ES8311_MIC_GAIN_ENUM, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), + cv.Optional(CONF_USE_MCLK, default=True): cv.boolean, + cv.Optional(CONF_USE_MICROPHONE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x18)) +) + + +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) + + cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) + cg.add(var.set_mic_gain(config[CONF_MIC_GAIN])) + cg.add(var.set_sample_frequency(config[CONF_SAMPLE_RATE])) + cg.add(var.set_use_mclk(config[CONF_USE_MCLK])) + cg.add(var.set_use_mic(config[CONF_USE_MICROPHONE])) diff --git a/esphome/components/es8311/es8311.cpp b/esphome/components/es8311/es8311.cpp new file mode 100644 index 0000000000..1cb1fbbe08 --- /dev/null +++ b/esphome/components/es8311/es8311.cpp @@ -0,0 +1,227 @@ +#include "es8311.h" +#include "es8311_const.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace es8311 { + +static const char *const TAG = "es8311"; + +// Mark the component as failed; use only in setup +#define ES8311_ERROR_FAILED(func) \ + if (!(func)) { \ + this->mark_failed(); \ + return; \ + } +// Return false; use outside of setup +#define ES8311_ERROR_CHECK(func) \ + if (!(func)) { \ + return false; \ + } + +void ES8311::setup() { + ESP_LOGCONFIG(TAG, "Setting up ES8311..."); + + // Reset + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x1F)); + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x00)); + + ES8311_ERROR_FAILED(this->configure_clock_()); + ES8311_ERROR_FAILED(this->configure_format_()); + ES8311_ERROR_FAILED(this->configure_mic_()); + + // Set initial volume + this->set_volume(0.75); // 0.75 = 0xBF = 0dB + + // Power up analog circuitry + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0D_SYSTEM, 0x01)); + // Enable analog PGA, enable ADC modulator + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0E_SYSTEM, 0x02)); + // Power up DAC + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG12_SYSTEM, 0x00)); + // Enable output to HP drive + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG13_SYSTEM, 0x10)); + // ADC Equalizer bypass, cancel DC offset in digital domain + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG1C_ADC, 0x6A)); + // Bypass DAC equalizer + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG37_DAC, 0x08)); + // Power On + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x80)); +} + +void ES8311::dump_config() { + ESP_LOGCONFIG(TAG, "ES8311 Audio Codec:"); + ESP_LOGCONFIG(TAG, " Use MCLK: %s", YESNO(this->use_mclk_)); + ESP_LOGCONFIG(TAG, " Use Microphone: %s", YESNO(this->use_mic_)); + ESP_LOGCONFIG(TAG, " DAC Bits per Sample: %" PRIu8, this->resolution_out_); + ESP_LOGCONFIG(TAG, " Sample Rate: %" PRIu32, this->sample_frequency_); + + if (this->is_failed()) { + ESP_LOGCONFIG(TAG, " Failed to initialize!"); + return; + } +} + +bool ES8311::set_volume(float volume) { + volume = clamp(volume, 0.0f, 1.0f); + uint8_t reg32 = remap(volume, 0.0f, 1.0f, 0, 255); + return this->write_byte(ES8311_REG32_DAC, reg32); +} + +float ES8311::volume() { + uint8_t reg32; + this->read_byte(ES8311_REG32_DAC, ®32); + return remap(reg32, 0, 255, 0.0f, 1.0f); +} + +uint8_t ES8311::calculate_resolution_value(ES8311Resolution resolution) { + switch (resolution) { + case ES8311_RESOLUTION_16: + return (3 << 2); + case ES8311_RESOLUTION_18: + return (2 << 2); + case ES8311_RESOLUTION_20: + return (1 << 2); + case ES8311_RESOLUTION_24: + return (0 << 2); + case ES8311_RESOLUTION_32: + return (4 << 2); + default: + return 0; + } +} + +const ES8311Coefficient *ES8311::get_coefficient(uint32_t mclk, uint32_t rate) { + for (const auto &coefficient : ES8311_COEFFICIENTS) { + if (coefficient.mclk == mclk && coefficient.rate == rate) + return &coefficient; + } + return nullptr; +} + +bool ES8311::configure_clock_() { + // Register 0x01: select clock source for internal MCLK and determine its frequency + uint8_t reg01 = 0x3F; // Enable all clocks + + uint32_t mclk_frequency = this->sample_frequency_ * this->mclk_multiple_; + if (!this->use_mclk_) { + reg01 |= BIT(7); // Use SCLK + mclk_frequency = this->sample_frequency_ * (int) this->resolution_out_ * 2; + } + if (this->mclk_inverted_) { + reg01 |= BIT(6); // Invert MCLK pin + } + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG01_CLK_MANAGER, reg01)); + + // Get clock coefficients from coefficient table + auto *coefficient = get_coefficient(mclk_frequency, this->sample_frequency_); + if (coefficient == nullptr) { + ESP_LOGE(TAG, "Unable to configure sample rate %" PRIu32 "Hz with %" PRIu32 "Hz MCLK", this->sample_frequency_, + mclk_frequency); + return false; + } + + // Register 0x02 + uint8_t reg02; + ES8311_ERROR_CHECK(this->read_byte(ES8311_REG02_CLK_MANAGER, ®02)); + reg02 &= 0x07; + reg02 |= (coefficient->pre_div - 1) << 5; + reg02 |= coefficient->pre_mult << 3; + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG02_CLK_MANAGER, reg02)); + + // Register 0x03 + const uint8_t reg03 = (coefficient->fs_mode << 6) | coefficient->adc_osr; + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG03_CLK_MANAGER, reg03)); + + // Register 0x04 + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG04_CLK_MANAGER, coefficient->dac_osr)); + + // Register 0x05 + const uint8_t reg05 = ((coefficient->adc_div - 1) << 4) | (coefficient->dac_div - 1); + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG05_CLK_MANAGER, reg05)); + + // Register 0x06 + uint8_t reg06; + ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, ®06)); + if (this->sclk_inverted_) { + reg06 |= BIT(5); + } else { + reg06 &= ~BIT(5); + } + reg06 &= 0xE0; + if (coefficient->bclk_div < 19) { + reg06 |= (coefficient->bclk_div - 1) << 0; + } else { + reg06 |= (coefficient->bclk_div) << 0; + } + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG06_CLK_MANAGER, reg06)); + + // Register 0x07 + uint8_t reg07; + ES8311_ERROR_CHECK(this->read_byte(ES8311_REG07_CLK_MANAGER, ®07)); + reg07 &= 0xC0; + reg07 |= coefficient->lrck_h << 0; + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG07_CLK_MANAGER, reg07)); + + // Register 0x08 + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG08_CLK_MANAGER, coefficient->lrck_l)); + + // Successfully configured the clock + return true; +} + +bool ES8311::configure_format_() { + // Configure I2S mode and format + uint8_t reg00; + ES8311_ERROR_CHECK(this->read_byte(ES8311_REG00_RESET, ®00)); + reg00 &= 0xBF; + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG00_RESET, reg00)); + + // Configure SDP in resolution + uint8_t reg09 = calculate_resolution_value(this->resolution_in_); + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG09_SDPIN, reg09)); + + // Configure SDP out resolution + uint8_t reg0a = calculate_resolution_value(this->resolution_out_); + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG0A_SDPOUT, reg0a)); + + // Successfully configured the format + return true; +} + +bool ES8311::configure_mic_() { + uint8_t reg14 = 0x1A; // Enable analog MIC and max PGA gain + if (this->use_mic_) { + reg14 |= BIT(6); // Enable PDM digital microphone + } + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG14_SYSTEM, reg14)); + + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG16_ADC, this->mic_gain_)); // ADC gain scale up + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG17_ADC, 0xC8)); // Set ADC gain + + // Successfully configured the microphones + return true; +} + +bool ES8311::set_mute_state_(bool mute_state) { + uint8_t reg31; + + this->is_muted_ = mute_state; + + if (!this->read_byte(ES8311_REG31_DAC, ®31)) { + return false; + } + + if (mute_state) { + reg31 |= BIT(6) | BIT(5); + } else { + reg31 &= ~(BIT(6) | BIT(5)); + } + + return this->write_byte(ES8311_REG31_DAC, reg31); +} + +} // namespace es8311 +} // namespace esphome diff --git a/esphome/components/es8311/es8311.h b/esphome/components/es8311/es8311.h new file mode 100644 index 0000000000..840a07204c --- /dev/null +++ b/esphome/components/es8311/es8311.h @@ -0,0 +1,135 @@ +#pragma once + +#include "esphome/components/audio_dac/audio_dac.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace es8311 { + +enum ES8311MicGain { + ES8311_MIC_GAIN_MIN = -1, + ES8311_MIC_GAIN_0DB, + ES8311_MIC_GAIN_6DB, + ES8311_MIC_GAIN_12DB, + ES8311_MIC_GAIN_18DB, + ES8311_MIC_GAIN_24DB, + ES8311_MIC_GAIN_30DB, + ES8311_MIC_GAIN_36DB, + ES8311_MIC_GAIN_42DB, + ES8311_MIC_GAIN_MAX +}; + +enum ES8311Resolution : uint8_t { + ES8311_RESOLUTION_16 = 16, + ES8311_RESOLUTION_18 = 18, + ES8311_RESOLUTION_20 = 20, + ES8311_RESOLUTION_24 = 24, + ES8311_RESOLUTION_32 = 32 +}; + +struct ES8311Coefficient { + uint32_t mclk; // mclk frequency + uint32_t rate; // sample rate + uint8_t pre_div; // the pre divider with range from 1 to 8 + uint8_t pre_mult; // the pre multiplier with x1, x2, x4 and x8 selection + uint8_t adc_div; // adcclk divider + uint8_t dac_div; // dacclk divider + uint8_t fs_mode; // single speed (0) or double speed (1) + uint8_t lrck_h; // adc lrck divider and dac lrck divider + uint8_t lrck_l; // + uint8_t bclk_div; // sclk divider + uint8_t adc_osr; // adc osr + uint8_t dac_osr; // dac osr +}; + +class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { + public: + ///////////////////////// + // Component overrides // + ///////////////////////// + + void setup() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void dump_config() override; + + //////////////////////// + // AudioDac overrides // + //////////////////////// + + /// @brief Writes the volume out to the DAC + /// @param volume floating point between 0.0 and 1.0 + /// @return True if successful and false otherwise + bool set_volume(float volume) override; + + /// @brief Gets the current volume out from the DAC + /// @return floating point between 0.0 and 1.0 + float volume() override; + + /// @brief Disables mute for audio out + /// @return True if successful and false otherwise + bool set_mute_off() override { return this->set_mute_state_(false); } + + /// @brief Enables mute for audio out + /// @return True if successful and false otherwise + bool set_mute_on() override { return this->set_mute_state_(true); } + + bool is_muted() override { return this->is_muted_; } + + ////////////////////////////////// + // ES8311 configuration setters // + ////////////////////////////////// + + void set_use_mclk(bool use_mclk) { this->use_mclk_ = use_mclk; } + void set_bits_per_sample(ES8311Resolution resolution) { + this->resolution_in_ = resolution; + this->resolution_out_ = resolution; + } + void set_sample_frequency(uint32_t sample_frequency) { this->sample_frequency_ = sample_frequency; } + void set_use_mic(bool use_mic) { this->use_mic_ = use_mic; } + void set_mic_gain(ES8311MicGain mic_gain) { this->mic_gain_ = mic_gain; } + + protected: + /// @brief Computes the register value for the configured resolution (bits per sample) + /// @param resolution bits per sample enum for both audio in and audio out + /// @return register value + static uint8_t calculate_resolution_value(ES8311Resolution resolution); + + /// @brief Retrieves the appropriate registers values for the configured mclk and rate + /// @param mclk mlck frequency in Hz + /// @param rate sample rate frequency in Hz + /// @return ES8311Coeffecient containing appropriate register values to configure the ES8311 or nullptr if impossible + static const ES8311Coefficient *get_coefficient(uint32_t mclk, uint32_t rate); + + /// @brief Configures the ES8311 registers for the chosen sample rate + /// @return True if successful and false otherwise + bool configure_clock_(); + + /// @brief Configures the ES8311 registers for the chosen bits per sample + /// @return True if successful and false otherwise + bool configure_format_(); + + /// @brief Configures the ES8311 microphone registers + /// @return True if successful and false otherwise + bool configure_mic_(); + + /// @brief Mutes or unmute the DAC audio out + /// @param mute_state True to mute, false to unmute + /// @return + bool set_mute_state_(bool mute_state); + + bool use_mic_; + ES8311MicGain mic_gain_; + + bool use_mclk_; // true = use dedicated MCLK pin, false = use SCLK + bool sclk_inverted_{false}; // SCLK is inverted + bool mclk_inverted_{false}; // MCLK is inverted (ignored if use_mclk_ == false) + uint32_t mclk_multiple_{256}; // MCLK frequency is sample rate * mclk_multiple_ (ignored if use_mclk_ == false) + + uint32_t sample_frequency_; // in Hz + ES8311Resolution resolution_in_; + ES8311Resolution resolution_out_; +}; + +} // namespace es8311 +} // namespace esphome diff --git a/esphome/components/es8311/es8311_const.h b/esphome/components/es8311/es8311_const.h new file mode 100644 index 0000000000..7463a92ef1 --- /dev/null +++ b/esphome/components/es8311/es8311_const.h @@ -0,0 +1,195 @@ +#pragma once + +#include "es8311.h" + +namespace esphome { +namespace es8311 { + +// ES8311 register addresses +static const uint8_t ES8311_REG00_RESET = 0x00; // Reset +static const uint8_t ES8311_REG01_CLK_MANAGER = 0x01; // Clock Manager: select clk src for mclk, enable clock for codec +static const uint8_t ES8311_REG02_CLK_MANAGER = 0x02; // Clock Manager: clk divider and clk multiplier +static const uint8_t ES8311_REG03_CLK_MANAGER = 0x03; // Clock Manager: adc fsmode and osr +static const uint8_t ES8311_REG04_CLK_MANAGER = 0x04; // Clock Manager: dac osr +static const uint8_t ES8311_REG05_CLK_MANAGER = 0x05; // Clock Manager: clk divider for adc and dac +static const uint8_t ES8311_REG06_CLK_MANAGER = 0x06; // Clock Manager: bclk inverter BIT(5) and divider +static const uint8_t ES8311_REG07_CLK_MANAGER = 0x07; // Clock Manager: tri-state, lrck divider +static const uint8_t ES8311_REG08_CLK_MANAGER = 0x08; // Clock Manager: lrck divider +static const uint8_t ES8311_REG09_SDPIN = 0x09; // Serial Digital Port: DAC +static const uint8_t ES8311_REG0A_SDPOUT = 0x0A; // Serial Digital Port: ADC +static const uint8_t ES8311_REG0B_SYSTEM = 0x0B; // System +static const uint8_t ES8311_REG0C_SYSTEM = 0x0C; // System +static const uint8_t ES8311_REG0D_SYSTEM = 0x0D; // System: power up/down +static const uint8_t ES8311_REG0E_SYSTEM = 0x0E; // System: power up/down +static const uint8_t ES8311_REG0F_SYSTEM = 0x0F; // System: low power +static const uint8_t ES8311_REG10_SYSTEM = 0x10; // System +static const uint8_t ES8311_REG11_SYSTEM = 0x11; // System +static const uint8_t ES8311_REG12_SYSTEM = 0x12; // System: Enable DAC +static const uint8_t ES8311_REG13_SYSTEM = 0x13; // System +static const uint8_t ES8311_REG14_SYSTEM = 0x14; // System: select DMIC, select analog pga gain +static const uint8_t ES8311_REG15_ADC = 0x15; // ADC: adc ramp rate, dmic sense +static const uint8_t ES8311_REG16_ADC = 0x16; // ADC +static const uint8_t ES8311_REG17_ADC = 0x17; // ADC: volume +static const uint8_t ES8311_REG18_ADC = 0x18; // ADC: alc enable and winsize +static const uint8_t ES8311_REG19_ADC = 0x19; // ADC: alc maxlevel +static const uint8_t ES8311_REG1A_ADC = 0x1A; // ADC: alc automute +static const uint8_t ES8311_REG1B_ADC = 0x1B; // ADC: alc automute, adc hpf s1 +static const uint8_t ES8311_REG1C_ADC = 0x1C; // ADC: equalizer, hpf s2 +static const uint8_t ES8311_REG1D_ADCEQ = 0x1D; // ADCEQ: equalizer B0 +static const uint8_t ES8311_REG1E_ADCEQ = 0x1E; // ADCEQ: equalizer B0 +static const uint8_t ES8311_REG1F_ADCEQ = 0x1F; // ADCEQ: equalizer B0 +static const uint8_t ES8311_REG20_ADCEQ = 0x20; // ADCEQ: equalizer B0 +static const uint8_t ES8311_REG21_ADCEQ = 0x21; // ADCEQ: equalizer A1 +static const uint8_t ES8311_REG22_ADCEQ = 0x22; // ADCEQ: equalizer A1 +static const uint8_t ES8311_REG23_ADCEQ = 0x23; // ADCEQ: equalizer A1 +static const uint8_t ES8311_REG24_ADCEQ = 0x24; // ADCEQ: equalizer A1 +static const uint8_t ES8311_REG25_ADCEQ = 0x25; // ADCEQ: equalizer A2 +static const uint8_t ES8311_REG26_ADCEQ = 0x26; // ADCEQ: equalizer A2 +static const uint8_t ES8311_REG27_ADCEQ = 0x27; // ADCEQ: equalizer A2 +static const uint8_t ES8311_REG28_ADCEQ = 0x28; // ADCEQ: equalizer A2 +static const uint8_t ES8311_REG29_ADCEQ = 0x29; // ADCEQ: equalizer B1 +static const uint8_t ES8311_REG2A_ADCEQ = 0x2A; // ADCEQ: equalizer B1 +static const uint8_t ES8311_REG2B_ADCEQ = 0x2B; // ADCEQ: equalizer B1 +static const uint8_t ES8311_REG2C_ADCEQ = 0x2C; // ADCEQ: equalizer B1 +static const uint8_t ES8311_REG2D_ADCEQ = 0x2D; // ADCEQ: equalizer B2 +static const uint8_t ES8311_REG2E_ADCEQ = 0x2E; // ADCEQ: equalizer B2 +static const uint8_t ES8311_REG2F_ADCEQ = 0x2F; // ADCEQ: equalizer B2 +static const uint8_t ES8311_REG30_ADCEQ = 0x30; // ADCEQ: equalizer B2 +static const uint8_t ES8311_REG31_DAC = 0x31; // DAC: mute +static const uint8_t ES8311_REG32_DAC = 0x32; // DAC: volume +static const uint8_t ES8311_REG33_DAC = 0x33; // DAC: offset +static const uint8_t ES8311_REG34_DAC = 0x34; // DAC: drc enable, drc winsize +static const uint8_t ES8311_REG35_DAC = 0x35; // DAC: drc maxlevel, minilevel +static const uint8_t ES8311_REG36_DAC = 0x36; // DAC +static const uint8_t ES8311_REG37_DAC = 0x37; // DAC: ramprate +static const uint8_t ES8311_REG38_DACEQ = 0x38; // DACEQ: equalizer B0 +static const uint8_t ES8311_REG39_DACEQ = 0x39; // DACEQ: equalizer B0 +static const uint8_t ES8311_REG3A_DACEQ = 0x3A; // DACEQ: equalizer B0 +static const uint8_t ES8311_REG3B_DACEQ = 0x3B; // DACEQ: equalizer B0 +static const uint8_t ES8311_REG3C_DACEQ = 0x3C; // DACEQ: equalizer B1 +static const uint8_t ES8311_REG3D_DACEQ = 0x3D; // DACEQ: equalizer B1 +static const uint8_t ES8311_REG3E_DACEQ = 0x3E; // DACEQ: equalizer B1 +static const uint8_t ES8311_REG3F_DACEQ = 0x3F; // DACEQ: equalizer B1 +static const uint8_t ES8311_REG40_DACEQ = 0x40; // DACEQ: equalizer A1 +static const uint8_t ES8311_REG41_DACEQ = 0x41; // DACEQ: equalizer A1 +static const uint8_t ES8311_REG42_DACEQ = 0x42; // DACEQ: equalizer A1 +static const uint8_t ES8311_REG43_DACEQ = 0x43; // DACEQ: equalizer A1 +static const uint8_t ES8311_REG44_GPIO = 0x44; // GPIO: dac2adc for test +static const uint8_t ES8311_REG45_GP = 0x45; // GPIO: GP control +static const uint8_t ES8311_REGFA_I2C = 0xFA; // I2C: reset registers +static const uint8_t ES8311_REGFC_FLAG = 0xFC; // Flag +static const uint8_t ES8311_REGFD_CHD1 = 0xFD; // Chip: ID1 +static const uint8_t ES8311_REGFE_CHD2 = 0xFE; // Chip: ID2 +static const uint8_t ES8311_REGFF_CHVER = 0xFF; // Chip: Version + +// ES8311 clock divider coefficients +static const ES8311Coefficient ES8311_COEFFICIENTS[] = { + // clang-format off + + // mclk, rate, pre_ pre_ adc_ dac_ fs_ lrck lrck bclk_ adc_ dac_ + // div, mult, div, div, mode, _h, _l, div, osr, osr + + // 8k + {12288000, 8000, 0x06, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + {18432000, 8000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x05, 0xff, 0x18, 0x10, 0x20}, + {16384000, 8000, 0x08, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 8192000, 8000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 6144000, 8000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 4096000, 8000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 3072000, 8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 2048000, 8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1536000, 8000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1024000, 8000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + + // 11.025k + {11289600, 11025, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 5644800, 11025, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 2822400, 11025, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1411200, 11025, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + + // 12k + {12288000, 12000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 6144000, 12000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 3072000, 12000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1536000, 12000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + + // 16k + {12288000, 16000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + {18432000, 16000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x20}, + {16384000, 16000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 8192000, 16000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 6144000, 16000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 4096000, 16000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 3072000, 16000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 2048000, 16000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1536000, 16000, 0x03, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1024000, 16000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + + // 22.05k + {11289600, 22050, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 5644800, 22050, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 2822400, 22050, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1411200, 22050, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 24k + {12288000, 24000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 24000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 24000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 24000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 24000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 32k + {12288000, 32000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 32000, 0x03, 0x04, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x10}, + {16384000, 32000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 8192000, 32000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 32000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 4096000, 32000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 32000, 0x03, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 2048000, 32000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 32000, 0x03, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, + { 1024000, 32000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 44.1k + {11289600, 44100, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 5644800, 44100, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 2822400, 44100, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1411200, 44100, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 48k + {12288000, 48000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 48000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 48000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 48000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 48000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 64k + {12288000, 64000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 64000, 0x03, 0x04, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, + {16384000, 64000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 8192000, 64000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 64000, 0x01, 0x04, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, + { 4096000, 64000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 64000, 0x01, 0x08, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, + { 2048000, 64000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 64000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0xbf, 0x03, 0x18, 0x18}, + { 1024000, 64000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, + + // 88.2k + {11289600, 88200, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 5644800, 88200, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 2822400, 88200, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1411200, 88200, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, + + // 96k + {12288000, 96000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 96000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 96000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 96000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 96000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, + + // clang-format on +}; + +} // namespace es8311 +} // namespace esphome diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 3ca140f0d4..61fbb53e3a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1,58 +1,66 @@ from dataclasses import dataclass -from typing import Union -from pathlib import Path import logging import os +from pathlib import Path +from typing import Optional, Union -from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p +from esphome import git +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_ADVANCED, CONF_BOARD, CONF_COMPONENTS, + CONF_ESPHOME, CONF_FRAMEWORK, + CONF_IGNORE_EFUSE_CUSTOM_MAC, + CONF_IGNORE_EFUSE_MAC_CRC, CONF_NAME, + CONF_PATH, + CONF_PLATFORM_VERSION, + CONF_PLATFORMIO_OPTIONS, + CONF_REF, + CONF_REFRESH, CONF_SOURCE, CONF_TYPE, + CONF_URL, CONF_VARIANT, CONF_VERSION, - CONF_ADVANCED, - CONF_REFRESH, - CONF_PATH, - CONF_URL, - CONF_REF, - CONF_IGNORE_EFUSE_MAC_CRC, KEY_CORE, KEY_FRAMEWORK_VERSION, + KEY_NAME, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + PLATFORM_ESP32, TYPE_GIT, TYPE_LOCAL, __version__, ) from esphome.core import CORE, HexInt, TimePeriod -import esphome.config_validation as cv -import esphome.codegen as cg -from esphome import git +import esphome.final_validate as fv +from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed +from .boards import BOARDS from .const import ( # noqa KEY_BOARD, KEY_COMPONENTS, KEY_ESP32, + KEY_EXTRA_BUILD_FILES, KEY_PATH, KEY_REF, KEY_REFRESH, KEY_REPO, KEY_SDKCONFIG_OPTIONS, + KEY_SUBMODULES, KEY_VARIANT, - VARIANT_ESP32C3, + VARIANT_ESP32, VARIANT_FRIENDLY, VARIANTS, ) -from .boards import BOARDS # force import gpio to register pin schema from .gpio import esp32_pin_to_code # noqa - _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["preferences"] @@ -60,7 +68,7 @@ AUTO_LOAD = ["preferences"] def set_core_data(config): CORE.data[KEY_ESP32] = {} - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "esp32" + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32 conf = config[CONF_FRAMEWORK] if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "esp-idf" @@ -73,6 +81,8 @@ def set_core_data(config): ) CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] + CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} + return config @@ -80,6 +90,27 @@ def get_esp32_variant(core_obj=None): return (core_obj or CORE).data[KEY_ESP32][KEY_VARIANT] +def get_board(core_obj=None): + return (core_obj or CORE).data[KEY_ESP32][KEY_BOARD] + + +def get_download_types(storage_json): + return [ + { + "title": "Factory format (Previously Modern)", + "description": "For use with ESPHome Web and other tools.", + "file": "firmware.factory.bin", + "download": f"{storage_json.name}.factory.bin", + }, + { + "title": "OTA format (Previously Legacy)", + "description": "For OTA updating a device.", + "file": "firmware.ota.bin", + "download": f"{storage_json.name}.ota.bin", + }, + ] + + def only_on_variant(*, supported=None, unsupported=None): """Config validator for features only available on some ESP32 variants.""" if supported is not None and not isinstance(supported, list): @@ -120,18 +151,60 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_component( - name: str, repo: str, ref: str = None, path: str = None, refresh: TimePeriod = None + *, + name: str, + repo: str, + ref: str = None, + path: str = None, + refresh: TimePeriod = None, + components: Optional[list[str]] = None, + submodules: Optional[list[str]] = None, ): """Add an esp-idf component to the project.""" if not CORE.using_esp_idf: raise ValueError("Not an esp-idf project") + if components is None: + components = [] if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: path, KEY_REFRESH: refresh, + KEY_COMPONENTS: components, + KEY_SUBMODULES: submodules, } + else: + component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] + if components is not None: + component_config[KEY_COMPONENTS] = list( + set(component_config[KEY_COMPONENTS] + components) + ) + if submodules is not None: + if component_config[KEY_SUBMODULES] is None: + component_config[KEY_SUBMODULES] = submodules + else: + component_config[KEY_SUBMODULES] = list( + set(component_config[KEY_SUBMODULES] + submodules) + ) + + +def add_extra_script(stage: str, filename: str, path: str): + """Add an extra script to the project.""" + key = f"{stage}:{filename}" + if add_extra_build_file(filename, path): + cg.add_platformio_option("extra_scripts", [key]) + + +def add_extra_build_file(filename: str, path: str) -> bool: + """Add an extra build file to the project.""" + if filename not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES][filename] = { + KEY_NAME: filename, + KEY_PATH: path, + } + return True + return False def _format_framework_arduino_version(ver: cv.Version) -> str: @@ -163,23 +236,23 @@ RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5) # The platformio/espressif32 version to use for arduino frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ARDUINO_PLATFORM_VERSION = cv.Version(5, 3, 0) +ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 4) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 8) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(5, 3, 0) +ESP_IDF_PLATFORM_VERSION = cv.Version(5, 4, 0) def _arduino_check_versions(value): value = value.copy() lookups = { "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(2, 0, 7), None), + "latest": (cv.Version(2, 0, 9), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -213,8 +286,8 @@ def _arduino_check_versions(value): def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 0, 1), None), + "dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(5, 1, 2), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } @@ -258,21 +331,63 @@ def _parse_platform_version(value): def _detect_variant(value): - if CONF_VARIANT not in value: - board = value[CONF_BOARD] - if board not in BOARDS: + board = value[CONF_BOARD] + if board in BOARDS: + variant = BOARDS[board][KEY_VARIANT] + if CONF_VARIANT in value and variant != value[CONF_VARIANT]: raise cv.Invalid( - "This board is unknown, please set the variant manually", + f"Option '{CONF_VARIANT}' does not match selected board.", + path=[CONF_VARIANT], + ) + value = value.copy() + value[CONF_VARIANT] = variant + else: + if CONF_VARIANT not in value: + raise cv.Invalid( + "This board is unknown, if you are sure you want to compile with this board selection, " + f"override with option '{CONF_VARIANT}'", path=[CONF_BOARD], ) - - value = value.copy() - value[CONF_VARIANT] = BOARDS[board][KEY_VARIANT] - + _LOGGER.warning( + "This board is unknown. Make sure the chosen chip component is correct.", + ) return value -CONF_PLATFORM_VERSION = "platform_version" +def final_validate(config): + if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]: + return config + + pio_flash_size_key = "board_upload.flash_size" + pio_partitions_key = "board_build.partitions" + if ( + CONF_PARTITIONS in config + and pio_partitions_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" + ) + + if ( + pio_flash_size_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" + ) + + if ( + config[CONF_VARIANT] != VARIANT_ESP32 + and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) + and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] + ): + raise cv.Invalid( + f"{CONF_IGNORE_EFUSE_MAC_CRC} is not supported on {config[CONF_VARIANT]}" + ) + + return config + ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( @@ -280,6 +395,13 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, cv.Optional(CONF_SOURCE): cv.string_strict, cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional( + CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False + ): cv.boolean, + } + ), } ), _arduino_check_versions, @@ -297,7 +419,10 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( }, cv.Optional(CONF_ADVANCED, default={}): cv.Schema( { - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, + cv.Optional( + CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False + ): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( @@ -331,10 +456,24 @@ FRAMEWORK_SCHEMA = cv.typed_schema( ) +FLASH_SIZES = [ + "2MB", + "4MB", + "8MB", + "16MB", + "32MB", +] + +CONF_FLASH_SIZE = "flash_size" +CONF_PARTITIONS = "partitions" CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( + *FLASH_SIZES, upper=True + ), + cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } @@ -344,8 +483,12 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) + + 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_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") @@ -358,7 +501,14 @@ async def to_code(config): conf = config[CONF_FRAMEWORK] cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) - cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) + if CONF_ADVANCED in conf and conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: + cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") + + add_extra_script( + "post", + "post_build.py", + os.path.join(os.path.dirname(__file__), "post_build.py.script"), + ) if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_platformio_option("framework", "espidf") @@ -392,12 +542,16 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) - if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_MAC_CRC]: - cg.add_define("USE_ESP32_IGNORE_EFUSE_MAC_CRC") + if conf[CONF_ADVANCED].get(CONF_IGNORE_EFUSE_MAC_CRC): + add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) if (framework_ver.major, framework_ver.minor) >= (4, 4): add_idf_sdkconfig_option( "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False @@ -436,7 +590,10 @@ async def to_code(config): [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], ) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_define( "USE_ARDUINO_VERSION_CODE", @@ -446,24 +603,47 @@ async def to_code(config): ) -ARDUINO_PARTITIONS_CSV = """\ -nvs, data, nvs, 0x009000, 0x005000, -otadata, data, ota, 0x00e000, 0x002000, -app0, app, ota_0, 0x010000, 0x1C0000, -app1, app, ota_1, 0x1D0000, 0x1C0000, -eeprom, data, 0x99, 0x390000, 0x001000, -spiffs, data, spiffs, 0x391000, 0x00F000 +APP_PARTITION_SIZES = { + "2MB": 0x0C0000, # 768 KB + "4MB": 0x1C0000, # 1792 KB + "8MB": 0x3C0000, # 3840 KB + "16MB": 0x7C0000, # 7936 KB + "32MB": 0xFC0000, # 16128 KB +} + + +def get_arduino_partition_csv(flash_size): + app_partition_size = APP_PARTITION_SIZES[flash_size] + eeprom_partition_size = 0x1000 # 4 KB + spiffs_partition_size = 0xF000 # 60 KB + + app0_partition_start = 0x010000 # 64 KB + app1_partition_start = app0_partition_start + app_partition_size + eeprom_partition_start = app1_partition_start + app_partition_size + spiffs_partition_start = eeprom_partition_start + eeprom_partition_size + + partition_csv = f"""\ +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xE000, 0x2000, +app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, +app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, +eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, +spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} """ + return partition_csv -IDF_PARTITIONS_CSV = """\ -# Name, Type, SubType, Offset, Size, Flags +def get_idf_partition_csv(flash_size): + app_partition_size = APP_PARTITION_SIZES[flash_size] + + partition_csv = f"""\ otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, -app0, app, ota_0, , 0x1C0000, -app1, app, ota_1, , 0x1C0000, -nvs, data, nvs, , 0x6d000, +app0, app, ota_0, , 0x{app_partition_size:X}, +app1, app, ota_1, , 0x{app_partition_size:X}, +nvs, data, nvs, , 0x6D000, """ + return partition_csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: @@ -504,16 +684,22 @@ def _write_sdkconfig(): # Called by writer.py def copy_files(): if CORE.using_arduino: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - ARDUINO_PARTITIONS_CSV, - ) + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) if CORE.using_esp_idf: _write_sdkconfig() - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - IDF_PARTITIONS_CSV, - ) + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_idf_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, # and no version.txt file exists, the CMake script fails for some setups. @@ -536,22 +722,51 @@ def copy_files(): ref=component[KEY_REF], refresh=component[KEY_REFRESH], domain="idf_components", + submodules=component[KEY_SUBMODULES], ) mkdir_p(CORE.relative_build_path("components")) component_dir = repo_dir if component[KEY_PATH] is not None: component_dir = component_dir / component[KEY_PATH] - shutil.copytree( - component_dir, - CORE.relative_build_path(f"components/{name}"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git", ".github"), - ) + if component[KEY_COMPONENTS] == ["*"]: + shutil.copytree( + component_dir, + CORE.relative_build_path("components"), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".git*"), + symlinks=True, + ignore_dangling_symlinks=True, + ) + elif len(component[KEY_COMPONENTS]) > 0: + for comp in component[KEY_COMPONENTS]: + shutil.copytree( + component_dir / comp, + CORE.relative_build_path(f"components/{comp}"), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".git*"), + symlinks=True, + ignore_dangling_symlinks=True, + ) + else: + shutil.copytree( + component_dir, + CORE.relative_build_path(f"components/{name}"), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".git*"), + symlinks=True, + ignore_dangling_symlinks=True, + ) - dir = os.path.dirname(__file__) - post_build_file = os.path.join(dir, "post_build.py.script") - copy_file_if_changed( - post_build_file, - CORE.relative_build_path("post_build.py"), - ) + for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): + if file[KEY_PATH].startswith("http"): + import requests + + mkdir_p(CORE.relative_build_path(os.path.dirname(file[KEY_NAME]))) + with open(CORE.relative_build_path(file[KEY_NAME]), "wb") as f: + f.write(requests.get(file[KEY_PATH], timeout=30).content) + else: + copy_file_if_changed( + file[KEY_PATH], + CORE.relative_build_path(file[KEY_NAME]), + ) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index e4fdaec0aa..02744ecb6f 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1,4 +1,4 @@ -from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3, VARIANT_ESP32S3 +from .const import VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3 ESP32_BASE_PINS = { "TX": 1, @@ -42,6 +42,234 @@ ESP32_BASE_PINS = { } ESP32_BOARD_PINS = { + "adafruit_feather_esp32_v2": { + "A0": 26, + "A1": 25, + "A2": 34, + "A3": 39, + "A4": 36, + "A5": 4, + "SCK": 5, + "MOSI": 19, + "MISO": 21, + "RX": 7, + "TX": 8, + "D37": 37, + "LED": 13, + "LED_BUILTIN": 13, + "D12": 12, + "D27": 27, + "D33": 33, + "D15": 15, + "D32": 32, + "D14": 14, + "SCL": 20, + "SDA": 22, + "BUTTON": 38, + "NEOPIXEL": 0, + "PIN_NEOPIXEL": 0, + "NEOPIXEL_POWER": 2, + }, + "adafruit_feather_esp32s2_tft": { + "BUTTON": 0, + "A0": 18, + "A1": 17, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "SCK": 36, + "MOSI": 35, + "MISO": 37, + "RX": 2, + "TX": 1, + "D13": 13, + "D12": 12, + "D11": 11, + "D10": 10, + "D9": 9, + "D6": 6, + "D5": 5, + "NEOPIXEL": 33, + "PIN_NEOPIXEL": 33, + "NEOPIXEL_POWER": 34, + "SCL": 41, + "SDA": 42, + "TFT_I2C_POWER": 21, + "TFT_CS": 7, + "TFT_DC": 39, + "TFT_RESET": 40, + "TFT_BACKLIGHT": 45, + "LED": 13, + "LED_BUILTIN": 13, + }, + "adafruit_feather_esp32s3": { + "BUTTON": 0, + "A0": 18, + "A1": 17, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "SCK": 36, + "MOSI": 35, + "MISO": 37, + "RX": 38, + "TX": 39, + "SCL": 4, + "SDA": 3, + "NEOPIXEL": 33, + "PIN_NEOPIXEL": 33, + "NEOPIXEL_POWER": 21, + "I2C_POWER": 7, + "LED": 13, + "LED_BUILTIN": 13, + }, + "adafruit_feather_esp32s3_nopsram": { + "BUTTON": 0, + "A0": 18, + "A1": 17, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "SCK": 36, + "MOSI": 35, + "MISO": 37, + "RX": 38, + "TX": 39, + "SCL": 4, + "SDA": 3, + "NEOPIXEL": 33, + "PIN_NEOPIXEL": 33, + "NEOPIXEL_POWER": 21, + "I2C_POWER": 7, + "LED": 13, + "LED_BUILTIN": 13, + }, + "adafruit_feather_esp32s3_tft": { + "BUTTON": 0, + "A0": 18, + "A1": 17, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "SCK": 36, + "MOSI": 35, + "MISO": 37, + "RX": 2, + "TX": 1, + "SCL": 41, + "SDA": 42, + "NEOPIXEL": 33, + "PIN_NEOPIXEL": 33, + "NEOPIXEL_POWER": 34, + "TFT_I2C_POWER": 21, + "TFT_CS": 7, + "TFT_DC": 39, + "TFT_RESET": 40, + "TFT_BACKLIGHT": 45, + "LED": 13, + "LED_BUILTIN": 13, + }, + "adafruit_funhouse_esp32s2": { + "BUTTON_UP": 5, + "BUTTON_DOWN": 3, + "BUTTON_SELECT": 4, + "DOTSTAR_DATA": 14, + "DOTSTAR_CLOCK": 15, + "PIR_SENSE": 16, + "A0": 17, + "A1": 2, + "A2": 1, + "CAP6": 6, + "CAP7": 7, + "CAP8": 8, + "CAP9": 9, + "CAP10": 10, + "CAP11": 11, + "CAP12": 12, + "CAP13": 13, + "SPEAKER": 42, + "LED": 37, + "LIGHT": 18, + "TFT_MOSI": 35, + "TFT_SCK": 36, + "TFT_CS": 40, + "TFT_DC": 39, + "TFT_RESET": 41, + "TFT_BACKLIGHT": 21, + "RED_LED": 31, + "BUTTON": 0, + }, + "adafruit_itsybitsy_esp32": { + "A0": 25, + "A1": 26, + "A2": 4, + "A3": 38, + "A4": 37, + "A5": 36, + "SCK": 19, + "MOSI": 21, + "MISO": 22, + "SCL": 27, + "SDA": 15, + "TX": 20, + "RX": 8, + "NEOPIXEL": 0, + "PIN_NEOPIXEL": 0, + "NEOPIXEL_POWER": 2, + "BUTTON": 35, + }, + "adafruit_magtag29_esp32s2": { + "A1": 18, + "BUTTON_A": 15, + "BUTTON_B": 14, + "BUTTON_C": 12, + "BUTTON_D": 11, + "SDA": 33, + "SCL": 34, + "SPEAKER": 17, + "SPEAKER_ENABLE": 16, + "VOLTAGE_MONITOR": 4, + "ACCELEROMETER_INT": 9, + "ACCELEROMETER_INTERRUPT": 9, + "LIGHT": 3, + "NEOPIXEL": 1, + "PIN_NEOPIXEL": 1, + "NEOPIXEL_POWER": 21, + "EPD_BUSY": 5, + "EPD_RESET": 6, + "EPD_DC": 7, + "EPD_CS": 8, + "EPD_MOSI": 35, + "EPD_SCK": 36, + "EPD_MISO": 37, + "BUTTON": 0, + "LED": 13, + "LED_BUILTIN": 13, + }, + "adafruit_metro_esp32s2": { + "A0": 17, + "A1": 18, + "A2": 1, + "A3": 2, + "A4": 3, + "A5": 4, + "RX": 38, + "TX": 37, + "SCL": 34, + "SDA": 33, + "MISO": 37, + "SCK": 36, + "MOSI": 35, + "NEOPIXEL": 45, + "PIN_NEOPIXEL": 45, + "LED": 42, + "LED_BUILTIN": 42, + "BUTTON": 0, + }, "adafruit_qtpy_esp32c3": { "A0": 4, "A1": 3, @@ -80,6 +308,26 @@ ESP32_BOARD_PINS = { "BUTTON": 0, "SWITCH": 0, }, + "adafruit_qtpy_esp32s3_nopsram": { + "A0": 18, + "A1": 17, + "A2": 9, + "A3": 8, + "SDA": 7, + "SCL": 6, + "MOSI": 35, + "MISO": 37, + "SCK": 36, + "RX": 16, + "TX": 5, + "SDA1": 41, + "SCL1": 40, + "NEOPIXEL": 39, + "PIN_NEOPIXEL": 39, + "NEOPIXEL_POWER": 38, + "BUTTON": 0, + "SWITCH": 0, + }, "adafruit_qtpy_esp32": { "A0": 26, "A1": 25, @@ -100,6 +348,10 @@ ESP32_BOARD_PINS = { "BUTTON": 0, "SWITCH": 0, }, + "airm2m_core_esp32c3": { + "LED1_BUILTIN": 12, + "LED2_BUILTIN": 13, + }, "alksesp32": { "A0": 32, "A1": 33, @@ -202,6 +454,7 @@ ESP32_BOARD_PINS = { "SDA": 5, "SS": 15, }, + "denky_d4": {"RX": 8, "LED": 14}, "esp-wrover-kit": {}, "esp32-devkitlipo": {}, "esp32-evb": { @@ -1002,7 +1255,18 @@ ESP32_BOARD_PINS = { "_VBAT": 35, }, "wemosbat": {"LED": 16}, - "wesp32": {"MISO": 32, "SCL": 4, "SDA": 15}, + "wesp32": { + "MISO": 32, + "MOSI": 23, + "SCK": 18, + "SCL": 4, + "SDA": 15, + "MISO1": 12, + "MOSI1": 13, + "SCK1": 14, + "SCL1": 5, + "SDA1": 33, + }, "widora-air": { "A1": 39, "A2": 35, @@ -1168,6 +1432,10 @@ BOARDS = { "name": "BPI-Bit", "variant": VARIANT_ESP32, }, + "bpi_leaf_s3": { + "name": "BPI-Leaf-S3", + "variant": VARIANT_ESP32S3, + }, "briki_abc_esp32": { "name": "Briki ABC (MBC-WB) - ESP32", "variant": VARIANT_ESP32, @@ -1184,6 +1452,10 @@ BOARDS = { "name": "Connaxio's Espoir", "variant": VARIANT_ESP32, }, + "cytron_maker_feather_aiot_s3": { + "name": "Cytron Maker Feather AIoT S3", + "variant": VARIANT_ESP32S3, + }, "d-duino-32": { "name": "D-duino-32", "variant": VARIANT_ESP32, @@ -1192,6 +1464,10 @@ BOARDS = { "name": "Deneyap Kart 1A", "variant": VARIANT_ESP32, }, + "deneyapkart1Av2": { + "name": "Deneyap Kart 1A v2", + "variant": VARIANT_ESP32S3, + }, "deneyapkartg": { "name": "Deneyap Kart G", "variant": VARIANT_ESP32C3, @@ -1204,6 +1480,10 @@ BOARDS = { "name": "Deneyap Mini", "variant": VARIANT_ESP32S2, }, + "deneyapminiv2": { + "name": "Deneyap Mini v2", + "variant": VARIANT_ESP32S2, + }, "denky32": { "name": "Denky32 (WROOM32)", "variant": VARIANT_ESP32, @@ -1232,6 +1512,10 @@ BOARDS = { "name": "Espressif ESP32-C3-DevKitM-1", "variant": VARIANT_ESP32C3, }, + "esp32-c3-m1i-kit": { + "name": "Ai-Thinker ESP-C3-M1-I-Kit", + "variant": VARIANT_ESP32C3, + }, "esp32cam": { "name": "AI Thinker ESP32-CAM", "variant": VARIANT_ESP32, @@ -1296,6 +1580,10 @@ BOARDS = { "name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)", "variant": VARIANT_ESP32S3, }, + "esp32-s3-korvo-2": { + "name": "Espressif ESP32-S3-Korvo-2", + "variant": VARIANT_ESP32S3, + }, "esp32thing": { "name": "SparkFun ESP32 Thing", "variant": VARIANT_ESP32, @@ -1604,6 +1892,10 @@ BOARDS = { "name": "Noduino Quantum", "variant": VARIANT_ESP32, }, + "redpill_esp32s3": { + "name": "Munich Labs RedPill ESP32-S3", + "variant": VARIANT_ESP32S3, + }, "seeed_xiao_esp32c3": { "name": "Seeed Studio XIAO ESP32C3", "variant": VARIANT_ESP32C3, diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index d13df01d3a..a86713e857 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -9,17 +9,23 @@ KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" KEY_PATH = "path" +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_ESP32C6 = "ESP32C6" VARIANT_ESP32H2 = "ESP32H2" VARIANTS = [ VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32S3, + VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C6, VARIANT_ESP32H2, ] @@ -27,7 +33,9 @@ VARIANT_FRIENDLY = { VARIANT_ESP32: "ESP32", VARIANT_ESP32S2: "ESP32-S2", VARIANT_ESP32S3: "ESP32-S3", + VARIANT_ESP32C2: "ESP32-C2", VARIANT_ESP32C3: "ESP32-C3", + VARIANT_ESP32C6: "ESP32-C6", VARIANT_ESP32H2: "ESP32-H2", } diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 512a8857b6..48c8b2b04d 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -10,9 +10,7 @@ #include #include -#if ESP_IDF_VERSION_MAJOR >= 4 #include -#endif #ifdef USE_ARDUINO #include @@ -55,15 +53,11 @@ void arch_init() { void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } -uint32_t arch_get_cpu_cycle_count() { -#if ESP_IDF_VERSION_MAJOR >= 4 - return cpu_hal_get_cycle_count(); +#if ESP_IDF_VERSION_MAJOR >= 5 +uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } #else - uint32_t ccount; - __asm__ __volatile__("esync; rsr %0,ccount" : "=a"(ccount)); - return ccount; +uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } #endif -} uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); } #ifdef USE_ESP_IDF diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 7848d1d552..df01769a66 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -1,21 +1,22 @@ from dataclasses import dataclass +import logging from typing import Any +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( CONF_ID, - CONF_INPUT, + CONF_IGNORE_PIN_VALIDATION_ERROR, + CONF_IGNORE_STRAPPING_WARNING, CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OPEN_DRAIN, CONF_OUTPUT, - CONF_PULLDOWN, - CONF_PULLUP, + PLATFORM_ESP32, ) -from esphome import pins from esphome.core import CORE -import esphome.config_validation as cv -import esphome.codegen as cg from . import boards from .const import ( @@ -23,24 +24,28 @@ from .const import ( KEY_ESP32, KEY_VARIANT, VARIANT_ESP32, + VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32H2, esp32_ns, ) - - from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports -from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports +from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports -from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports +from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports - +from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports +from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin) +_LOGGER = logging.getLogger(__name__) + + def _lookup_pin(value): board = CORE.data[KEY_ESP32][KEY_BOARD] board_pins = boards.ESP32_BOARD_PINS.get(board, {}) @@ -62,8 +67,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: @@ -95,6 +102,14 @@ _esp32_validations = { pin_validation=esp32_s3_validate_gpio_pin, usage_validation=esp32_s3_validate_supports, ), + VARIANT_ESP32C2: ESP32ValidationFunctions( + pin_validation=esp32_c2_validate_gpio_pin, + usage_validation=esp32_c2_validate_supports, + ), + VARIANT_ESP32C6: ESP32ValidationFunctions( + pin_validation=esp32_c6_validate_gpio_pin, + usage_validation=esp32_c6_validate_supports, + ), VARIANT_ESP32H2: ESP32ValidationFunctions( pin_validation=esp32_h2_validate_gpio_pin, usage_validation=esp32_h2_validate_supports, @@ -102,7 +117,7 @@ _esp32_validations = { } -def validate_gpio_pin(value): +def gpio_pin_number_validator(value): value = _translate_pin(value) board = CORE.data[KEY_ESP32][KEY_BOARD] board_pins = boards.ESP32_BOARD_PINS.get(board, {}) @@ -118,7 +133,33 @@ def validate_gpio_pin(value): if variant not in _esp32_validations: raise cv.Invalid(f"Unsupported ESP32 variant {variant}") - return _esp32_validations[variant].pin_validation(value) + return value + + +def validate_gpio_pin(pin): + variant = CORE.data[KEY_ESP32][KEY_VARIANT] + if variant not in _esp32_validations: + raise cv.Invalid(f"Unsupported ESP32 variant {variant}") + + ignore_pin_validation_warning = pin[CONF_IGNORE_PIN_VALIDATION_ERROR] + try: + pin[CONF_NUMBER] = _esp32_validations[variant].pin_validation(pin[CONF_NUMBER]) + except cv.Invalid as exc: + if not ignore_pin_validation_warning: + raise + + _LOGGER.warning( + "Ignoring validation error on pin %d; error: %s", + pin[CONF_NUMBER], + exc, + ) + else: + # Throw an exception if used for a pin that would not have resulted + # in a validation error anyway! + if ignore_pin_validation_warning: + raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin") + + return pin def validate_supports(value): @@ -148,32 +189,25 @@ DRIVE_STRENGTHS = { } gpio_num_t = cg.global_ns.enum("gpio_num_t") - CONF_DRIVE_STRENGTH = "drive_strength" + ESP32_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( - cv.float_with_unit("current", "mA", optional_unit=True), - cv.enum(DRIVE_STRENGTHS), - ), - }, + pins.gpio_base_schema(ESP32InternalGPIOPin, gpio_pin_number_validator).extend( + { + cv.Optional(CONF_IGNORE_PIN_VALIDATION_ERROR, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean, + cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( + cv.float_with_unit("current", "mA", optional_unit=True), + cv.enum(DRIVE_STRENGTHS), + ), + } + ), + validate_gpio_pin, validate_supports, ) -@pins.PIN_SCHEMA_REGISTRY.register("esp32", ESP32_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA) async def esp32_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index 66ba2ffa62..e4d3b6aaf3 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -1,5 +1,6 @@ import logging +import esphome.config_validation as cv from esphome.const import ( CONF_INPUT, CONF_MODE, @@ -8,8 +9,7 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) -import esphome.config_validation as cv - +from esphome.pins import check_strapping_pin _ESP_SDIO_PINS = { 6: "Flash Clock", @@ -18,7 +18,7 @@ _ESP_SDIO_PINS = { 11: "Flash Command", } -_ESP32_STRAPPING_PINS = {0, 2, 4, 12, 15} +_ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15} _LOGGER = logging.getLogger(__name__) @@ -35,13 +35,6 @@ def esp32_validate_gpio_pin(value): "flash interface in QUAD IO flash mode.", value, ) - if value in _ESP32_STRAPPING_PINS: - _LOGGER.warning( - "GPIO%d is a Strapping PIN and should be avoided.\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", - value, - ) if value in (24, 28, 29, 30, 31): # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet @@ -74,4 +67,5 @@ def esp32_validate_supports(value): f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] ) + check_strapping_pin(value, _ESP32_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c2.py b/esphome/components/esp32/gpio_esp32_c2.py new file mode 100644 index 0000000000..abdcb1b655 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_c2.py @@ -0,0 +1,32 @@ +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 + +_ESP32C2_STRAPPING_PINS = {8, 9} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_c2_validate_gpio_pin(value): + if value < 0 or value > 20: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)") + + return value + + +def esp32_c2_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + + if num < 0 or num > 20: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)") + + if is_input: + # All ESP32 pins support input mode + pass + + check_strapping_pin(value, _ESP32C2_STRAPPING_PINS, _LOGGER) + return value diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index fc1cef29e5..5b9ec0ebd9 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -1,11 +1,8 @@ import logging -from esphome.const import ( - CONF_INPUT, - CONF_MODE, - CONF_NUMBER, -) import esphome.config_validation as cv +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER +from esphome.pins import check_strapping_pin _ESP32C3_SPI_PSRAM_PINS = { 12: "SPIHD", @@ -28,13 +25,6 @@ def esp32_c3_validate_gpio_pin(value): raise cv.Invalid( f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" ) - if value in _ESP32C3_STRAPPING_PINS: - _LOGGER.warning( - "GPIO%d is a Strapping PIN and should be avoided.\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", - value, - ) return value @@ -50,4 +40,6 @@ def esp32_c3_validate_supports(value): if is_input: # All ESP32 pins support input mode pass + + check_strapping_pin(value, _ESP32C3_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py new file mode 100644 index 0000000000..bc735f85c4 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -0,0 +1,45 @@ +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 + +_ESP32C6_SPI_PSRAM_PINS = { + 24: "SPICS0", + 25: "SPIQ", + 26: "SPIWP", + 27: "VDD_SPI", + 28: "SPIHD", + 29: "SPICLK", + 30: "SPID", +} + +_ESP32C6_STRAPPING_PINS = {8, 9, 15} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_c6_validate_gpio_pin(value): + if value < 0 or value > 23: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)") + if value in _ESP32C6_SPI_PSRAM_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})" + ) + + return value + + +def esp32_c6_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + + if num < 0 or num > 23: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)") + if is_input: + # All ESP32 pins support input mode + pass + + check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER) + return value diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 5196ef0c09..7413bf4db5 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -1,11 +1,52 @@ +import logging + import esphome.config_validation as cv +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER + +_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} + +_ESP32H2_USB_JTAG_PINS = {26, 27} + +_ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25} + +_LOGGER = logging.getLogger(__name__) def esp32_h2_validate_gpio_pin(value): - # ESP32-H2 not yet supported - raise cv.Invalid("ESP32-H2 isn't supported yet") + if value < 0 or value > 27: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") + if value in _ESP32H2_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\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", + value, + ) + if value in _ESP32H2_SPI_FLASH_PINS: + _LOGGER.warning( + "GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n" + "Utilizing SPI-reserved pins could cause unexpected failures.\n" + "See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html", + value, + ) + if value in _ESP32H2_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" + "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + value, + ) + + return value def esp32_h2_validate_supports(value): - # ESP32-H2 not yet supported - raise cv.Invalid("ESP32-H2 isn't supported yet") + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + + if num < 0 or num > 27: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") + if is_input: + # All ESP32 pins support input mode + pass + return value diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py index db244b6259..331aeb9d94 100644 --- a/esphome/components/esp32/gpio_esp32_s2.py +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -1,5 +1,6 @@ import logging +import esphome.config_validation as cv from esphome.const import ( CONF_INPUT, CONF_MODE, @@ -8,8 +9,7 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) - -import esphome.config_validation as cv +from esphome.pins import check_strapping_pin _ESP32S2_SPI_PSRAM_PINS = { 26: "SPICS1", @@ -34,13 +34,6 @@ def esp32_s2_validate_gpio_pin(value): raise cv.Invalid( f"This pin cannot be used on ESP32-S2s and is already used by the SPI/PSRAM interface (function: {_ESP32S2_SPI_PSRAM_PINS[value]})" ) - if value in _ESP32S2_STRAPPING_PINS: - _LOGGER.warning( - "GPIO%d is a Strapping PIN and should be avoided.\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", - value, - ) if value in (22, 23, 24, 25): # These pins are not exposed in GPIO mux (reason unknown) @@ -77,4 +70,5 @@ def esp32_s2_validate_supports(value): f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] ) + check_strapping_pin(value, _ESP32S2_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index f729a757c2..7120504693 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -1,12 +1,8 @@ import logging -from esphome.const import ( - CONF_INPUT, - CONF_MODE, - CONF_NUMBER, -) - import esphome.config_validation as cv +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER +from esphome.pins import check_strapping_pin _ESP_32S3_SPI_PSRAM_PINS = { 26: "SPICS1", @@ -45,14 +41,6 @@ def esp32_s3_validate_gpio_pin(value): value, ) - if value in _ESP_32S3_STRAPPING_PINS: - _LOGGER.warning( - "GPIO%d is a Strapping PIN and should be avoided.\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", - value, - ) - if value in (22, 23, 24, 25): # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet @@ -71,4 +59,6 @@ def esp32_s3_validate_supports(value): if is_input: # All ESP32 pins support input mode pass + + check_strapping_pin(value, _ESP_32S3_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index c941bdb386..c181cf30b1 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -17,17 +17,19 @@ from SCons.Script import ARGUMENTS # Copy over the default sdkconfig. from os import path + if path.exists("./sdkconfig.defaults"): os.makedirs(".temp", exist_ok=True) shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf") + def esp32_create_combined_bin(source, target, env): verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) if verbose: print("Generating combined binary for serial flashing") app_offset = 0x10000 - new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") chip = env.get("BOARD_MCU") @@ -62,5 +64,14 @@ def esp32_create_combined_bin(source, target, env): else: subprocess.run(["esptool.py", *cmd]) + +def esp32_copy_ota_bin(source, target, env): + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin") + + shutil.copyfile(firmware_name, new_file_name) + + # pylint: disable=E0602 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index f508cecb87..75cf9d707d 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,15 +1,16 @@ +from esphome import automation import esphome.codegen as cg +from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant, const DEPENDENCIES = ["esp32"] -CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_beacon"] +CODEOWNERS = ["@jesserockz", "@Rapsssito"] CONF_BLE_ID = "ble_id" CONF_IO_CAPABILITY = "io_capability" +CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] @@ -20,6 +21,10 @@ GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") +BLEEnabledCondition = esp32_ble_ns.class_("BLEEnabledCondition", automation.Condition) +BLEEnableAction = esp32_ble_ns.class_("BLEEnableAction", automation.Action) +BLEDisableAction = esp32_ble_ns.class_("BLEDisableAction", automation.Action) + IoCapability = esp32_ble_ns.enum("IoCapability") IO_CAPABILITY = { "none": IoCapability.IO_CAP_NONE, @@ -29,12 +34,29 @@ IO_CAPABILITY = { "display_yes_no": IoCapability.IO_CAP_IO, } +esp_power_level_t = cg.global_ns.enum("esp_power_level_t") + +TX_POWER_LEVELS = { + -12: esp_power_level_t.ESP_PWR_LVL_N12, + -9: esp_power_level_t.ESP_PWR_LVL_N9, + -6: esp_power_level_t.ESP_PWR_LVL_N6, + -3: esp_power_level_t.ESP_PWR_LVL_N3, + 0: esp_power_level_t.ESP_PWR_LVL_N0, + 3: esp_power_level_t.ESP_PWR_LVL_P3, + 6: esp_power_level_t.ESP_PWR_LVL_P6, + 9: esp_power_level_t.ESP_PWR_LVL_P9, +} + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLE), cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( IO_CAPABILITY, lower=True ), + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + cv.Optional( + CONF_ADVERTISING_CYCLE_TIME, default="10s" + ): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA) @@ -50,8 +72,28 @@ FINAL_VALIDATE_SCHEMA = validate_variant async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) + cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME])) + await cg.register_component(var, config) if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) + + cg.add_define("USE_ESP32_BLE") + + +@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) +async def ble_enabled_to_code(config, condition_id, template_arg, args): + return cg.new_Pvariable(condition_id, template_arg) + + +@automation.register_action("ble.enable", BLEEnableAction, cv.Schema({})) +async def ble_enable_to_code(config, action_id, template_arg, args): + return cg.new_Pvariable(action_id, template_arg) + + +@automation.register_action("ble.disable", BLEDisableAction, cv.Schema({})) +async def ble_disable_to_code(config, action_id, template_arg, args): + return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 21ec005e07..5d08b6e973 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,12 +1,17 @@ #ifdef USE_ESP32 #include "ble.h" + +#ifdef USE_ESP32_VARIANT_ESP32C6 +#include "const_esp32c6.h" +#endif // USE_ESP32_VARIANT_ESP32C6 + #include "esphome/core/application.h" #include "esphome/core/log.h" #include -#include #include +#include #include #include #include @@ -26,30 +31,90 @@ void ESP32BLE::setup() { global_ble = this; ESP_LOGCONFIG(TAG, "Setting up BLE..."); - if (!ble_setup_()) { - ESP_LOGE(TAG, "BLE could not be set up"); + if (!ble_pre_setup_()) { + ESP_LOGE(TAG, "BLE could not be prepared for configuration"); this->mark_failed(); return; } -#ifdef USE_ESP32_BLE_SERVER - this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory) - - this->advertising_->set_scan_response(true); - this->advertising_->set_min_preferred_interval(0x06); - this->advertising_->start(); -#endif // USE_ESP32_BLE_SERVER - - ESP_LOGD(TAG, "BLE setup complete"); + this->state_ = BLE_COMPONENT_STATE_DISABLED; + if (this->enable_on_boot_) { + this->enable(); + } } -bool ESP32BLE::ble_setup_() { +void ESP32BLE::enable() { + if (this->state_ != BLE_COMPONENT_STATE_DISABLED) + return; + + this->state_ = BLE_COMPONENT_STATE_ENABLE; +} + +void ESP32BLE::disable() { + if (this->state_ == BLE_COMPONENT_STATE_DISABLED) + return; + + this->state_ = BLE_COMPONENT_STATE_DISABLE; +} + +bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } + +void ESP32BLE::advertising_start() { + this->advertising_init_(); + if (!this->is_active()) + return; + this->advertising_->start(); +} + +void ESP32BLE::advertising_set_service_data(const std::vector &data) { + this->advertising_init_(); + this->advertising_->set_service_data(data); + this->advertising_start(); +} + +void ESP32BLE::advertising_set_manufacturer_data(const std::vector &data) { + this->advertising_init_(); + this->advertising_->set_manufacturer_data(data); + this->advertising_start(); +} + +void ESP32BLE::advertising_register_raw_advertisement_callback(std::function &&callback) { + this->advertising_init_(); + this->advertising_->register_raw_advertisement_callback(std::move(callback)); +} + +void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) { + this->advertising_init_(); + this->advertising_->add_service_uuid(uuid); + this->advertising_start(); +} + +void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) { + this->advertising_init_(); + this->advertising_->remove_service_uuid(uuid); + this->advertising_start(); +} + +bool ESP32BLE::ble_pre_setup_() { esp_err_t err = nvs_flash_init(); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); return false; } + return true; +} +void ESP32BLE::advertising_init_() { + if (this->advertising_ != nullptr) + return; + this->advertising_ = new BLEAdvertising(this->advertising_cycle_time_); // NOLINT(cppcoreguidelines-owning-memory) + + this->advertising_->set_scan_response(true); + this->advertising_->set_min_preferred_interval(0x06); +} + +bool ESP32BLE::ble_setup_() { + esp_err_t err; #ifdef USE_ARDUINO if (!btStart()) { ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); @@ -59,7 +124,11 @@ bool ESP32BLE::ble_setup_() { 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) { +#ifdef USE_ESP32_VARIANT_ESP32C6 + esp_bt_controller_config_t cfg = BT_CONTROLLER_CONFIG; +#else esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); +#endif err = esp_bt_controller_init(&cfg); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); @@ -146,7 +215,88 @@ bool ESP32BLE::ble_setup_() { return true; } +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; + } + err = esp_bluedroid_deinit(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err); + return false; + } + +#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) { + err = esp_bt_controller_disable(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bt_controller_disable failed: %s", esp_err_to_name(err)); + return false; + } + while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) + ; + } + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) { + err = esp_bt_controller_deinit(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bt_controller_deinit failed: %s", esp_err_to_name(err)); + return false; + } + } + if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_IDLE) { + ESP_LOGE(TAG, "esp bt controller disable failed"); + return false; + } + } +#endif + return true; +} + void ESP32BLE::loop() { + switch (this->state_) { + case BLE_COMPONENT_STATE_OFF: + case BLE_COMPONENT_STATE_DISABLED: + return; + case BLE_COMPONENT_STATE_DISABLE: { + ESP_LOGD(TAG, "Disabling BLE..."); + + for (auto *ble_event_handler : this->ble_status_event_handlers_) { + ble_event_handler->ble_before_disabled_event_handler(); + } + + if (!ble_dismantle_()) { + ESP_LOGE(TAG, "BLE could not be dismantled"); + this->mark_failed(); + return; + } + this->state_ = BLE_COMPONENT_STATE_DISABLED; + return; + } + case BLE_COMPONENT_STATE_ENABLE: { + ESP_LOGD(TAG, "Enabling BLE..."); + this->state_ = BLE_COMPONENT_STATE_OFF; + + if (!ble_setup_()) { + ESP_LOGE(TAG, "BLE could not be set up"); + this->mark_failed(); + return; + } + + this->state_ = BLE_COMPONENT_STATE_ACTIVE; + return; + } + case BLE_COMPONENT_STATE_ACTIVE: + break; + } + BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { @@ -167,6 +317,9 @@ void ESP32BLE::loop() { delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) ble_event = this->ble_events_.pop(); } + if (this->advertising_ != nullptr) { + this->advertising_->loop(); + } } void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { @@ -244,6 +397,17 @@ void ESP32BLE::dump_config() { } } +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} + ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 11ae826544..7c55583852 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -1,23 +1,29 @@ #pragma once #include "ble_advertising.h" +#include "ble_uuid.h" +#include + +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "queue.h" #include "ble_event.h" +#include "queue.h" #ifdef USE_ESP32 #include -#include #include +#include namespace esphome { namespace esp32_ble { +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); + // NOLINTNEXTLINE(modernize-use-using) typedef struct { void *peer_device; @@ -33,6 +39,19 @@ enum IoCapability { IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, }; +enum BLEComponentState { + /** Nothing has been initialized yet. */ + BLE_COMPONENT_STATE_OFF = 0, + /** BLE should be disabled on next loop. */ + BLE_COMPONENT_STATE_DISABLE, + /** BLE is disabled. */ + BLE_COMPONENT_STATE_DISABLED, + /** BLE should be enabled on next loop. */ + BLE_COMPONENT_STATE_ENABLE, + /** BLE is active. */ + BLE_COMPONENT_STATE_ACTIVE, +}; + class GAPEventHandler { public: virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; @@ -50,20 +69,42 @@ class GATTsEventHandler { esp_ble_gatts_cb_param_t *param) = 0; }; +class BLEStatusEventHandler { + public: + virtual void ble_before_disabled_event_handler() = 0; +}; + class ESP32BLE : public Component { public: void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } + void set_advertising_cycle_time(uint32_t advertising_cycle_time) { + this->advertising_cycle_time_ = advertising_cycle_time; + } + uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; } + + void enable(); + void disable(); + bool is_active(); void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override; - BLEAdvertising *get_advertising() { return this->advertising_; } + void advertising_start(); + void advertising_set_service_data(const std::vector &data); + void advertising_set_manufacturer_data(const std::vector &data); + void advertising_add_service_uuid(ESPBTUUID uuid); + void advertising_remove_service_uuid(ESPBTUUID uuid); + void advertising_register_raw_advertisement_callback(std::function &&callback); void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } + void register_ble_status_event_handler(BLEStatusEventHandler *handler) { + this->ble_status_event_handlers_.push_back(handler); + } + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } protected: static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); @@ -75,19 +116,41 @@ class ESP32BLE : public Component { void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); bool ble_setup_(); + bool ble_dismantle_(); + bool ble_pre_setup_(); + void advertising_init_(); std::vector gap_event_handlers_; std::vector gattc_event_handlers_; std::vector gatts_event_handlers_; + std::vector ble_status_event_handlers_; + BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; Queue ble_events_; BLEAdvertising *advertising_; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; + uint32_t advertising_cycle_time_; + bool enable_on_boot_; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLE *global_ble; +template class BLEEnabledCondition : public Condition { + public: + bool check(Ts... x) override { return global_ble->is_active(); } +}; + +template class BLEEnableAction : public Action { + public: + void play(Ts... x) override { global_ble->enable(); } +}; + +template class BLEDisableAction : public Action { + public: + void play(Ts... x) override { global_ble->disable(); } +}; + } // namespace esp32_ble } // namespace esphome diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 2083bf5f08..1d340c76d9 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -2,22 +2,22 @@ #ifdef USE_ESP32 -#include "ble_uuid.h" -#include #include +#include +#include "ble_uuid.h" #include "esphome/core/log.h" namespace esphome { namespace esp32_ble { -static const char *const TAG = "esp32_ble"; +static const char *const TAG = "esp32_ble.advertising"; -BLEAdvertising::BLEAdvertising() { +BLEAdvertising::BLEAdvertising(uint32_t advertising_cycle_time) : advertising_cycle_time_(advertising_cycle_time) { this->advertising_data_.set_scan_rsp = false; this->advertising_data_.include_name = true; this->advertising_data_.include_txpower = true; - this->advertising_data_.min_interval = 0x20; - this->advertising_data_.max_interval = 0x40; + this->advertising_data_.min_interval = 0; + this->advertising_data_.max_interval = 0; this->advertising_data_.appearance = 0x00; this->advertising_data_.manufacturer_len = 0; this->advertising_data_.p_manufacturer_data = nullptr; @@ -42,12 +42,29 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.end()); } -void BLEAdvertising::set_manufacturer_data(uint8_t *data, uint16_t size) { - this->advertising_data_.p_manufacturer_data = data; - this->advertising_data_.manufacturer_len = size; +void BLEAdvertising::set_service_data(const std::vector &data) { + delete[] this->advertising_data_.p_service_data; + this->advertising_data_.p_service_data = nullptr; + this->advertising_data_.service_data_len = data.size(); + if (!data.empty()) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->advertising_data_.p_service_data = new uint8_t[data.size()]; + memcpy(this->advertising_data_.p_service_data, data.data(), data.size()); + } } -void BLEAdvertising::start() { +void BLEAdvertising::set_manufacturer_data(const std::vector &data) { + delete[] this->advertising_data_.p_manufacturer_data; + this->advertising_data_.p_manufacturer_data = nullptr; + this->advertising_data_.manufacturer_len = data.size(); + if (!data.empty()) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->advertising_data_.p_manufacturer_data = new uint8_t[data.size()]; + memcpy(this->advertising_data_.p_manufacturer_data, data.data(), data.size()); + } +} + +esp_err_t BLEAdvertising::services_advertisement_() { int num_services = this->advertising_uuids_.size(); if (num_services == 0) { this->advertising_data_.service_uuid_len = 0; @@ -70,20 +87,23 @@ void BLEAdvertising::start() { this->advertising_data_.include_txpower = !this->scan_response_; err = esp_ble_gap_config_adv_data(&this->advertising_data_); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %d", err); - return; + ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %s", esp_err_to_name(err)); + return err; } - memcpy(&this->scan_response_data_, &this->advertising_data_, sizeof(esp_ble_adv_data_t)); - this->scan_response_data_.set_scan_rsp = true; - this->scan_response_data_.include_name = true; - this->scan_response_data_.include_txpower = true; - this->scan_response_data_.appearance = 0; - this->scan_response_data_.flag = 0; - err = esp_ble_gap_config_adv_data(&this->scan_response_data_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %d", err); - return; + if (this->scan_response_) { + memcpy(&this->scan_response_data_, &this->advertising_data_, sizeof(esp_ble_adv_data_t)); + this->scan_response_data_.set_scan_rsp = true; + this->scan_response_data_.include_name = true; + this->scan_response_data_.include_txpower = true; + this->scan_response_data_.manufacturer_len = 0; + this->scan_response_data_.appearance = 0; + this->scan_response_data_.flag = 0; + err = esp_ble_gap_config_adv_data(&this->scan_response_data_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %s", esp_err_to_name(err)); + return err; + } } if (this->advertising_data_.service_uuid_len > 0) { @@ -93,8 +113,18 @@ void BLEAdvertising::start() { err = esp_ble_gap_start_advertising(&this->advertising_params_); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err); - return; + ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err)); + return err; + } + + return ESP_OK; +} + +void BLEAdvertising::start() { + if (this->current_adv_index_ == -1) { + this->services_advertisement_(); + } else { + this->raw_advertisements_callbacks_[this->current_adv_index_](true); } } @@ -104,6 +134,29 @@ void BLEAdvertising::stop() { ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err); return; } + if (this->current_adv_index_ != -1) { + this->raw_advertisements_callbacks_[this->current_adv_index_](false); + } +} + +void BLEAdvertising::loop() { + if (this->raw_advertisements_callbacks_.empty()) { + return; + } + const uint32_t now = millis(); + if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { + this->stop(); + this->current_adv_index_ += 1; + if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) { + this->current_adv_index_ = -1; + } + this->start(); + this->last_advertisement_time_ = now; + } +} + +void BLEAdvertising::register_raw_advertisement_callback(std::function &&callback) { + this->raw_advertisements_callbacks_.push_back(std::move(callback)); } } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 079bd6c14c..946e414c1d 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -1,36 +1,57 @@ #pragma once +#include +#include #include #ifdef USE_ESP32 +#include #include #include namespace esphome { namespace esp32_ble { +using raw_adv_data_t = struct { + uint8_t *data; + size_t length; + esp_power_level_t power_level; +}; + class ESPBTUUID; class BLEAdvertising { public: - BLEAdvertising(); + BLEAdvertising(uint32_t advertising_cycle_time); + + void loop(); void add_service_uuid(ESPBTUUID uuid); void remove_service_uuid(ESPBTUUID uuid); void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } - void set_manufacturer_data(uint8_t *data, uint16_t size); + void set_manufacturer_data(const std::vector &data); + void set_service_data(const std::vector &data); + void register_raw_advertisement_callback(std::function &&callback); void start(); void stop(); protected: + esp_err_t services_advertisement_(); + bool scan_response_; esp_ble_adv_data_t advertising_data_; esp_ble_adv_data_t scan_response_data_; esp_ble_adv_params_t advertising_params_; std::vector advertising_uuids_; + + std::vector> raw_advertisements_callbacks_; + + const uint32_t advertising_cycle_time_; + uint32_t last_advertisement_time_{0}; + int8_t current_adv_index_{-1}; // -1 means standard scan response }; } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index a50d3dbd42..07ac719434 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "esphome/core/log.h" namespace esphome { @@ -30,6 +31,13 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128); return ret; } +ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_128; + for (int i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[ESP_UUID_LEN_128 - 1 - i] = data[i]; + return ret; +} ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ret; if (data.length() == 4) { @@ -166,7 +174,7 @@ std::string ESPBTUUID::to_string() const { case ESP_UUID_LEN_16: return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); case ESP_UUID_LEN_32: - return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24, + return str_snprintf("0x%02" PRIX32 "%02" PRIX32 "%02" PRIX32 "%02" PRIX32, 10, (this->uuid_.uuid.uuid32 >> 24), (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); default: diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 790a57c59d..d90db3a599 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -20,6 +20,7 @@ class ESPBTUUID { static ESPBTUUID from_uint32(uint32_t uuid); 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); diff --git a/esphome/components/esp32_ble/const_esp32c6.h b/esphome/components/esp32_ble/const_esp32c6.h new file mode 100644 index 0000000000..89179d8dd9 --- /dev/null +++ b/esphome/components/esp32_ble/const_esp32c6.h @@ -0,0 +1,74 @@ +#pragma once + +#ifdef USE_ESP32_VARIANT_ESP32C6 + +#include + +namespace esphome { +namespace esp32_ble { + +static const esp_bt_controller_config_t BT_CONTROLLER_CONFIG = { + .config_version = CONFIG_VERSION, + .ble_ll_resolv_list_size = CONFIG_BT_LE_LL_RESOLV_LIST_SIZE, + .ble_hci_evt_hi_buf_count = DEFAULT_BT_LE_HCI_EVT_HI_BUF_COUNT, + .ble_hci_evt_lo_buf_count = DEFAULT_BT_LE_HCI_EVT_LO_BUF_COUNT, + .ble_ll_sync_list_cnt = DEFAULT_BT_LE_MAX_PERIODIC_ADVERTISER_LIST, + .ble_ll_sync_cnt = DEFAULT_BT_LE_MAX_PERIODIC_SYNCS, + .ble_ll_rsp_dup_list_count = CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT, + .ble_ll_adv_dup_list_count = CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT, + .ble_ll_tx_pwr_dbm = BLE_LL_TX_PWR_DBM_N, + .rtc_freq = RTC_FREQ_N, + .ble_ll_sca = CONFIG_BT_LE_LL_SCA, + .ble_ll_scan_phy_number = BLE_LL_SCAN_PHY_NUMBER_N, + .ble_ll_conn_def_auth_pyld_tmo = BLE_LL_CONN_DEF_AUTH_PYLD_TMO_N, + .ble_ll_jitter_usecs = BLE_LL_JITTER_USECS_N, + .ble_ll_sched_max_adv_pdu_usecs = BLE_LL_SCHED_MAX_ADV_PDU_USECS_N, + .ble_ll_sched_direct_adv_max_usecs = BLE_LL_SCHED_DIRECT_ADV_MAX_USECS_N, + .ble_ll_sched_adv_max_usecs = BLE_LL_SCHED_ADV_MAX_USECS_N, + .ble_scan_rsp_data_max_len = DEFAULT_BT_LE_SCAN_RSP_DATA_MAX_LEN_N, + .ble_ll_cfg_num_hci_cmd_pkts = BLE_LL_CFG_NUM_HCI_CMD_PKTS_N, + .ble_ll_ctrl_proc_timeout_ms = BLE_LL_CTRL_PROC_TIMEOUT_MS_N, + .nimble_max_connections = DEFAULT_BT_LE_MAX_CONNECTIONS, + .ble_whitelist_size = DEFAULT_BT_NIMBLE_WHITELIST_SIZE, // NOLINT + .ble_acl_buf_size = DEFAULT_BT_LE_ACL_BUF_SIZE, + .ble_acl_buf_count = DEFAULT_BT_LE_ACL_BUF_COUNT, + .ble_hci_evt_buf_size = DEFAULT_BT_LE_HCI_EVT_BUF_SIZE, + .ble_multi_adv_instances = DEFAULT_BT_LE_MAX_EXT_ADV_INSTANCES, + .ble_ext_adv_max_size = DEFAULT_BT_LE_EXT_ADV_MAX_SIZE, + .controller_task_stack_size = NIMBLE_LL_STACK_SIZE, + .controller_task_prio = ESP_TASK_BT_CONTROLLER_PRIO, + .controller_run_cpu = 0, + .enable_qa_test = RUN_QA_TEST, + .enable_bqb_test = RUN_BQB_TEST, +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 1) + // The following fields have been removed since ESP IDF version 5.3.1, see commit: + // https://github.com/espressif/esp-idf/commit/e761c1de8f9c0777829d597b4d5a33bb070a30a8 + .enable_uart_hci = HCI_UART_EN, + .ble_hci_uart_port = DEFAULT_BT_LE_HCI_UART_PORT, + .ble_hci_uart_baud = DEFAULT_BT_LE_HCI_UART_BAUD, + .ble_hci_uart_data_bits = DEFAULT_BT_LE_HCI_UART_DATA_BITS, + .ble_hci_uart_stop_bits = DEFAULT_BT_LE_HCI_UART_STOP_BITS, + .ble_hci_uart_flow_ctrl = DEFAULT_BT_LE_HCI_UART_FLOW_CTRL, + .ble_hci_uart_uart_parity = DEFAULT_BT_LE_HCI_UART_PARITY, +#endif + .enable_tx_cca = DEFAULT_BT_LE_TX_CCA_ENABLED, + .cca_rssi_thresh = 256 - DEFAULT_BT_LE_CCA_RSSI_THRESH, + .sleep_en = NIMBLE_SLEEP_ENABLE, + .coex_phy_coded_tx_rx_time_limit = DEFAULT_BT_LE_COEX_PHY_CODED_TX_RX_TLIM_EFF, + .dis_scan_backoff = NIMBLE_DISABLE_SCAN_BACKOFF, + .ble_scan_classify_filter_enable = 1, + .main_xtal_freq = CONFIG_XTAL_FREQ, + .version_num = (uint8_t) efuse_hal_chip_revision(), + .cpu_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ, + .ignore_wl_for_direct_adv = 0, + .enable_pcl = DEFAULT_BT_LE_POWER_CONTROL_ENABLED, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 3) + .csa2_select = DEFAULT_BT_LE_50_FEATURE_SUPPORT, +#endif + .config_magic = CONFIG_MAGIC, +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32C6 diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index 311919dcd4..f97f289a0a 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -1,16 +1,21 @@ import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER -from esphome.core import CORE, TimePeriod -from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components import esp32_ble +from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32_ble import CONF_BLE_ID +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TX_POWER, CONF_TYPE, CONF_UUID +from esphome.core import CORE, TimePeriod +AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] -CONFLICTS_WITH = ["esp32_ble_tracker"] esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") -ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) - +ESP32BLEBeacon = esp32_ble_beacon_ns.class_( + "ESP32BLEBeacon", + cg.Component, + esp32_ble.GAPEventHandler, + cg.Parented.template(esp32_ble.ESP32BLE), +) CONF_MAJOR = "major" CONF_MINOR = "minor" CONF_MIN_INTERVAL = "min_interval" @@ -28,6 +33,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), + cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True), cv.Required(CONF_UUID): cv.uuid, cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, @@ -48,7 +54,7 @@ CONFIG_SCHEMA = cv.All( min=-128, max=0 ), cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( - cv.decibel, cv.one_of(-12, -9, -6, -3, 0, 3, 6, 9, int=True) + cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True) ), } ).extend(cv.COMPONENT_SCHEMA), @@ -62,6 +68,10 @@ async def to_code(config): uuid = config[CONF_UUID].hex uuid_arr = [cg.RawExpression(f"0x{uuid[i:i + 2]}") for i in range(0, len(uuid), 2)] var = cg.new_Pvariable(config[CONF_ID], uuid_arr) + + parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) + cg.add(parent.register_gap_event_handler(var)) + await cg.register_component(var, config) cg.add(var.set_major(config[CONF_MAJOR])) cg.add(var.set_minor(config[CONF_MINOR])) @@ -72,3 +82,4 @@ async def to_code(config): if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 589fcc1e82..423fe61592 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -3,14 +3,16 @@ #ifdef USE_ESP32 -#include -#include -#include #include -#include +#include #include +#include +#include +#include #include + #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #ifdef USE_ARDUINO #include @@ -21,20 +23,6 @@ namespace esp32_ble_beacon { static const char *const TAG = "esp32_ble_beacon"; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static esp_ble_adv_params_t ble_adv_params = { - .adv_int_min = 0x20, - .adv_int_max = 0x40, - .adv_type = ADV_TYPE_NONCONN_IND, - .own_addr_type = BLE_ADDR_TYPE_PUBLIC, - .peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, - .channel_map = ADV_CHNL_ALL, - .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, -}; - -#define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8)) - static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = { .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}}; @@ -53,117 +41,62 @@ void ESP32BLEBeacon::dump_config() { " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" ", TX Power: %ddBm", uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, - this->tx_power_); + (this->tx_power_ * 3) - 12); } +float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } + void ESP32BLEBeacon::setup() { - ESP_LOGCONFIG(TAG, "Setting up ESP32 BLE beacon..."); - global_esp32_ble_beacon = this; + this->ble_adv_params_ = { + .adv_int_min = static_cast(this->min_interval_ / 0.625f), + .adv_int_max = static_cast(this->max_interval_ / 0.625f), + .adv_type = ADV_TYPE_NONCONN_IND, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + }; - xTaskCreatePinnedToCore(ESP32BLEBeacon::ble_core_task, - "ble_task", // name - 10000, // stack size (in words) - nullptr, // input params - 1, // priority - nullptr, // Handle, not needed - 0 // core - ); + global_ble->advertising_register_raw_advertisement_callback([this](bool advertise) { + this->advertising_ = advertise; + if (advertise) { + this->on_advertise_(); + } + }); } -float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::BLUETOOTH; } -void ESP32BLEBeacon::ble_core_task(void *params) { - ble_setup(); - - while (true) { - delay(1000); // NOLINT - } -} - -void ESP32BLEBeacon::ble_setup() { - ble_adv_params.adv_int_min = static_cast(global_esp32_ble_beacon->min_interval_ / 0.625f); - ble_adv_params.adv_int_max = static_cast(global_esp32_ble_beacon->max_interval_ / 0.625f); - - // Initialize non-volatile storage for the bluetooth controller - esp_err_t err = nvs_flash_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); - return; - } - -#ifdef USE_ARDUINO - if (!btStart()) { - ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); - return; - } -#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) { - esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); - err = esp_bt_controller_init(&cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); - return; - } - while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) - ; - } - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) { - err = esp_bt_controller_enable(ESP_BT_MODE_BLE); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err)); - return; - } - } - if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { - ESP_LOGE(TAG, "esp bt controller enable failed"); - return; - } - } -#endif - - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - - err = esp_bluedroid_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); - return; - } - err = esp_bluedroid_enable(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); - return; - } - err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, - static_cast((global_esp32_ble_beacon->tx_power_ + 12) / 3)); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); - return; - } - err = esp_ble_gap_register_callback(ESP32BLEBeacon::gap_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); - return; - } - +void ESP32BLEBeacon::on_advertise_() { esp_ble_ibeacon_t ibeacon_adv_data; memcpy(&ibeacon_adv_data.ibeacon_head, &IBEACON_COMMON_HEAD, sizeof(esp_ble_ibeacon_head_t)); - memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, global_esp32_ble_beacon->uuid_.data(), + memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, this->uuid_.data(), sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid)); - ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_); - ibeacon_adv_data.ibeacon_vendor.major = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->major_); - ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast(global_esp32_ble_beacon->measured_power_); + ibeacon_adv_data.ibeacon_vendor.minor = byteswap(this->minor_); + ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_); + ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast(this->measured_power_); - esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); + ESP_LOGD(TAG, "Setting BLE TX power"); + esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); + } + err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err)); + return; + } } void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + if (!this->advertising_) + return; + esp_err_t err; switch (event) { case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: { - err = esp_ble_gap_start_advertising(&ble_adv_params); + err = esp_ble_gap_start_advertising(&this->ble_adv_params_); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err); + ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err)); } break; } @@ -181,6 +114,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap } else { ESP_LOGD(TAG, "BLE stopped advertising successfully"); } + // this->advertising_ = false; break; } default: @@ -188,8 +122,6 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap } } -ESP32BLEBeacon *global_esp32_ble_beacon = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esp32_ble_beacon } // namespace esphome diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 5208b67ea3..e37edf6cde 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -1,39 +1,39 @@ #pragma once +#include "esphome/components/esp32_ble/ble.h" #include "esphome/core/component.h" #ifdef USE_ESP32 -#include #include +#include namespace esphome { namespace esp32_ble_beacon { -// NOLINTNEXTLINE(modernize-use-using) -typedef struct { +using esp_ble_ibeacon_head_t = struct { uint8_t flags[3]; uint8_t length; uint8_t type; uint8_t company_id[2]; uint8_t beacon_type[2]; -} __attribute__((packed)) esp_ble_ibeacon_head_t; +} __attribute__((packed)); -// NOLINTNEXTLINE(modernize-use-using) -typedef struct { +using esp_ble_ibeacon_vendor_t = struct { uint8_t proximity_uuid[16]; uint16_t major; uint16_t minor; uint8_t measured_power; -} __attribute__((packed)) esp_ble_ibeacon_vendor_t; +} __attribute__((packed)); -// NOLINTNEXTLINE(modernize-use-using) -typedef struct { +using esp_ble_ibeacon_t = struct { esp_ble_ibeacon_head_t ibeacon_head; esp_ble_ibeacon_vendor_t ibeacon_vendor; -} __attribute__((packed)) esp_ble_ibeacon_t; +} __attribute__((packed)); -class ESP32BLEBeacon : public Component { +using namespace esp32_ble; + +class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented { public: explicit ESP32BLEBeacon(const std::array &uuid) : uuid_(uuid) {} @@ -46,12 +46,11 @@ class ESP32BLEBeacon : public Component { void set_min_interval(uint16_t val) { this->min_interval_ = val; } void set_max_interval(uint16_t val) { this->max_interval_ = val; } void set_measured_power(int8_t val) { this->measured_power_ = val; } - void set_tx_power(int8_t val) { this->tx_power_ = val; } + void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; protected: - static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - static void ble_core_task(void *params); - static void ble_setup(); + void on_advertise_(); std::array uuid_; uint16_t major_{}; @@ -59,12 +58,11 @@ class ESP32BLEBeacon : public Component { uint16_t min_interval_{}; uint16_t max_interval_{}; int8_t measured_power_{}; - int8_t tx_power_{}; + esp_power_level_t tx_power_{}; + esp_ble_adv_params_t ble_adv_params_; + bool advertising_{false}; }; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -extern ESP32BLEBeacon *global_esp32_ble_beacon; - } // namespace esp32_ble_beacon } // namespace esphome diff --git a/esphome/components/esp32_ble_client/__init__.py b/esphome/components/esp32_ble_client/__init__.py index 94a5576d0b..25957ed0da 100644 --- a/esphome/components/esp32_ble_client/__init__.py +++ b/esphome/components/esp32_ble_client/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg - from esphome.components import esp32_ble_tracker AUTO_LOAD = ["esp32_ble_tracker"] diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 40eff49266..98e7792792 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -20,16 +20,21 @@ static const esp_bt_uuid_t NOTIFY_DESC_UUID = { void BLEClientBase::setup() { static uint8_t connection_index = 0; this->connection_index_ = connection_index++; - - auto ret = esp_ble_gattc_app_register(this->app_id); - if (ret) { - ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); - this->mark_failed(); - } - this->set_state(espbt::ClientState::IDLE); } void BLEClientBase::loop() { + if (!esp32_ble::global_ble->is_active()) { + this->set_state(espbt::ClientState::INIT); + return; + } + if (this->state_ == espbt::ClientState::INIT) { + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_state(espbt::ClientState::IDLE); + } // READY_TO_CONNECT means we have discovered the device // and the scanner has been stopped by the tracker. if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { @@ -40,21 +45,19 @@ void BLEClientBase::loop() { float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { + if (!this->auto_connect_) + return false; if (this->address_ == 0 || device.address_uint64() != this->address_) return false; if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) return false; - ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str()); - this->set_state(espbt::ClientState::DISCOVERED); + this->log_event_("Found device"); + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) + esp32_ble_tracker::global_esp32_ble_tracker->print_bt_device_info(device); - auto addr = device.address_uint64(); - this->remote_bda_[0] = (addr >> 40) & 0xFF; - this->remote_bda_[1] = (addr >> 32) & 0xFF; - this->remote_bda_[2] = (addr >> 24) & 0xFF; - this->remote_bda_[3] = (addr >> 16) & 0xFF; - this->remote_bda_[4] = (addr >> 8) & 0xFF; - this->remote_bda_[5] = (addr >> 0) & 0xFF; + this->set_state(espbt::ClientState::DISCOVERED); + this->set_address(device.address_uint64()); this->remote_addr_type_ = device.get_address_type(); return true; } @@ -103,6 +106,10 @@ void BLEClientBase::release_services() { #endif } +void BLEClientBase::log_event_(const char *name) { + ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); +} + bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, esp_ble_gattc_cb_param_t *param) { if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) @@ -126,7 +133,9 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_OPEN_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str()); + if (!this->check_addr(param->open.remote_bda)) + return false; + this->log_event_("ESP_GATTC_OPEN_EVT"); this->conn_id_ = param->open.conn_id; this->service_count_ = 0; if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { @@ -140,37 +149,57 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_, this->address_str_.c_str(), ret); } + this->set_state(espbt::ClientState::CONNECTED); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); - this->set_state(espbt::ClientState::CONNECTED); + // only set our state, subclients might have more stuff to do yet. this->state_ = espbt::ClientState::ESTABLISHED; break; } esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; } - case ESP_GATTC_CFG_MTU_EVT: { - 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); - this->set_state(espbt::ClientState::IDLE); - break; - } - ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), - param->cfg_mtu.status, param->cfg_mtu.mtu); - this->mtu_ = param->cfg_mtu.mtu; + case ESP_GATTC_CONNECT_EVT: { + if (!this->check_addr(param->connect.remote_bda)) + return false; + this->log_event_("ESP_GATTC_CONNECT_EVT"); break; } case ESP_GATTC_DISCONNECT_EVT: { - if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0) + if (!this->check_addr(param->disconnect.remote_bda)) return false; - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, this->address_str_.c_str(), param->disconnect.reason); this->release_services(); this->set_state(espbt::ClientState::IDLE); break; } + + case ESP_GATTC_CFG_MTU_EVT: { + 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); + // 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(), + param->cfg_mtu.status, param->cfg_mtu.mtu); + this->mtu_ = param->cfg_mtu.mtu; + break; + } + case ESP_GATTC_CLOSE_EVT: { + if (this->conn_id_ != param->close.conn_id) + return false; + this->log_event_("ESP_GATTC_CLOSE_EVT"); + this->release_services(); + this->set_state(espbt::ClientState::IDLE); + break; + } case ESP_GATTC_SEARCH_RES_EVT: { + if (this->conn_id_ != param->search_res.conn_id) + return false; this->service_count_++; if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // V3 clients don't need services initialized since @@ -186,7 +215,9 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTC_SEARCH_CMPL_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str()); + if (this->conn_id_ != param->search_cmpl.conn_id) + return false; + this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT"); 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()); @@ -194,11 +225,41 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->address_str_.c_str(), svc->start_handle, svc->end_handle); } ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); - this->set_state(espbt::ClientState::CONNECTED); this->state_ = espbt::ClientState::ESTABLISHED; break; } + case ESP_GATTC_READ_DESCR_EVT: { + if (this->conn_id_ != param->write.conn_id) + return false; + this->log_event_("ESP_GATTC_READ_DESCR_EVT"); + break; + } + case ESP_GATTC_WRITE_DESCR_EVT: { + if (this->conn_id_ != param->write.conn_id) + return false; + this->log_event_("ESP_GATTC_WRITE_DESCR_EVT"); + break; + } + case ESP_GATTC_WRITE_CHAR_EVT: { + if (this->conn_id_ != param->write.conn_id) + return false; + this->log_event_("ESP_GATTC_WRITE_CHAR_EVT"); + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (this->conn_id_ != param->read.conn_id) + return false; + this->log_event_("ESP_GATTC_READ_CHAR_EVT"); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (this->conn_id_ != param->notify.conn_id) + return false; + this->log_event_("ESP_GATTC_NOTIFY_EVT"); + break; + } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT"); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // Client is responsible for flipping the descriptor value @@ -207,9 +268,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } esp_gattc_descr_elem_t desc_result; uint16_t count = 1; - esp_gatt_status_t descr_status = - esp_ble_gattc_get_descr_by_char_handle(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, - NOTIFY_DESC_UUID, &desc_result, &count); + esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle( + this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count); if (descr_status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_, this->address_str_.c_str(), descr_status); @@ -217,7 +277,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } esp_gattc_char_elem_t char_result; esp_gatt_status_t char_status = - esp_ble_gattc_get_all_char(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, + esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, param->reg_for_notify.handle, &char_result, &count, 0); if (char_status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, @@ -233,6 +293,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_err_t status = esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); if (status) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_, this->address_str_.c_str(), status); @@ -241,24 +302,31 @@ 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); break; } return true; } +// clients can't call defer() directly since it's protected. +void BLEClientBase::run_later(std::function &&f) { // NOLINT + this->defer(std::move(f)); +} + void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { // This event is sent by the server when it requests security case ESP_GAP_BLE_SEC_REQ_EVT: - if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) - break; + 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_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 (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) - break; + 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. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), @@ -268,11 +336,12 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ param->ble_security.auth_cmpl.fail_reason); } else { this->paired_ = true; - ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, + ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode); } break; + // There are other events we'll want to implement at some point to support things like pass key // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md default: diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 97886d0b19..fca66c0b3c 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -27,6 +27,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void loop() override; float get_setup_priority() const override; + void run_later(std::function &&f); // NOLINT bool parse_device(const espbt::ESPBTDevice &device) override; void on_scan_end() override {} bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, @@ -34,15 +35,22 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; void connect() override; esp_err_t pair(); - void disconnect(); + void disconnect() override; void release_services(); bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; } + void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; } + void set_address(uint64_t address) { this->address_ = address; + this->remote_bda_[0] = (address >> 40) & 0xFF; + this->remote_bda_[1] = (address >> 32) & 0xFF; + this->remote_bda_[2] = (address >> 24) & 0xFF; + this->remote_bda_[3] = (address >> 16) & 0xFF; + this->remote_bda_[4] = (address >> 8) & 0xFF; + this->remote_bda_[5] = (address >> 0) & 0xFF; if (address == 0) { - memset(this->remote_bda_, 0, sizeof(this->remote_bda_)); this->address_str_ = ""; } else { this->address_str_ = @@ -79,20 +87,24 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; } + bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; } + protected: int gattc_if_; esp_bd_addr_t remote_bda_; - esp_ble_addr_type_t remote_addr_type_; + esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; uint16_t conn_id_{0xFFFF}; uint64_t address_{0}; + bool auto_connect_{false}; std::string address_str_{}; uint8_t connection_index_; int16_t service_count_{0}; uint16_t mtu_{23}; bool paired_{false}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; - std::vector services_; + + void log_event_(const char *name); }; } // namespace esp32_ble_client diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 0ddfa62c1b..9da7d13999 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -1,16 +1,16 @@ import esphome.codegen as cg +from esphome.components import esp32_ble +from esphome.components.esp32 import add_idf_sdkconfig_option import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODEL -from esphome.components import esp32_ble from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option AUTO_LOAD = ["esp32_ble"] -CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_beacon"] +CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] DEPENDENCIES = ["esp32"] CONF_MANUFACTURER = "manufacturer" +CONF_MANUFACTURER_DATA = "manufacturer_data" esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") BLEServer = esp32_ble_server_ns.class_( @@ -27,6 +27,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(BLEServer), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, + cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.hex_uint8_t]), cv.Optional(CONF_MODEL): cv.string, } ).extend(cv.COMPONENT_SCHEMA) @@ -39,9 +40,12 @@ async def to_code(config): parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) cg.add(parent.register_gatts_event_handler(var)) + cg.add(parent.register_ble_status_event_handler(var)) cg.add(var.set_parent(parent)) cg.add(var.set_manufacturer(config[CONF_MANUFACTURER])) + if CONF_MANUFACTURER_DATA in config: + cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) if CONF_MODEL in config: cg.add(var.set_model(config[CONF_MODEL])) cg.add_define("USE_ESP32_BLE_SERVER") diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 15a51f6ede..6ff7d615f9 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -11,6 +11,13 @@ namespace esp32_ble_server { static const char *const TAG = "esp32_ble_server.characteristic"; +BLECharacteristic::~BLECharacteristic() { + for (auto *descriptor : this->descriptors_) { + delete descriptor; // NOLINT(cppcoreguidelines-owning-memory) + } + vSemaphoreDelete(this->set_value_lock_); +} + BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) { this->set_value_lock_ = xSemaphoreCreateBinary(); xSemaphoreGive(this->set_value_lock_); @@ -98,6 +105,11 @@ void BLECharacteristic::notify(bool notification) { void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { this->descriptors_.push_back(descriptor); } +void BLECharacteristic::remove_descriptor(BLEDescriptor *descriptor) { + this->descriptors_.erase(std::remove(this->descriptors_.begin(), this->descriptors_.end(), descriptor), + this->descriptors_.end()); +} + void BLECharacteristic::do_create(BLEService *service) { this->service_ = service; esp_attr_control_t control; diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index d7af3a934a..8837c796a5 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -25,6 +25,7 @@ class BLEService; class BLECharacteristic { public: BLECharacteristic(ESPBTUUID uuid, uint32_t properties); + ~BLECharacteristic(); void set_value(const uint8_t *data, size_t length); void set_value(std::vector value); @@ -52,6 +53,7 @@ class BLECharacteristic { void on_write(const std::function &)> &&func) { this->on_write_ = func; } void add_descriptor(BLEDescriptor *descriptor); + void remove_descriptor(BLEDescriptor *descriptor); BLEService *get_service() { return this->service_; } ESPBTUUID get_uuid() { return this->uuid_; } diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 7cbf40c076..338413f64e 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -30,13 +30,13 @@ void BLEServer::setup() { ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE"); return; } - - ESP_LOGD(TAG, "Setting up BLE Server..."); - global_ble_server = this; } void BLEServer::loop() { + if (!this->parent_->is_active()) { + return; + } switch (this->state_) { case RUNNING: return; @@ -53,10 +53,16 @@ void BLEServer::loop() { } case REGISTERING: { if (this->registered_) { - this->device_information_service_ = this->create_service(DEVICE_INFORMATION_SERVICE_UUID); - - this->create_device_characteristics_(); - + // Create all services previously created + for (auto &pair : this->services_) { + pair.second->do_create(this); + } + if (this->device_information_service_ == nullptr) { + this->create_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID)); + this->device_information_service_ = + this->get_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID)); + this->create_device_characteristics_(); + } this->state_ = STARTING_SERVICE; } break; @@ -67,7 +73,7 @@ void BLEServer::loop() { } if (this->device_information_service_->is_running()) { this->state_ = RUNNING; - this->can_proceed_ = true; + this->restart_advertising_(); ESP_LOGD(TAG, "BLE server setup successfully"); } else if (!this->device_information_service_->is_starting()) { this->device_information_service_->start(); @@ -77,6 +83,16 @@ void BLEServer::loop() { } } +bool BLEServer::is_running() { return this->parent_->is_active() && this->state_ == RUNNING; } + +bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_active(); } + +void BLEServer::restart_advertising_() { + if (this->is_running()) { + this->parent_->advertising_set_manufacturer_data(this->manufacturer_data_); + } +} + bool BLEServer::create_device_characteristics_() { if (this->model_.has_value()) { BLECharacteristic *model = @@ -99,24 +115,36 @@ bool BLEServer::create_device_characteristics_() { return true; } -std::shared_ptr BLEServer::create_service(const uint8_t *uuid, bool advertise) { - return this->create_service(ESPBTUUID::from_raw(uuid), advertise); -} -std::shared_ptr BLEServer::create_service(uint16_t uuid, bool advertise) { - return this->create_service(ESPBTUUID::from_uint16(uuid), advertise); -} -std::shared_ptr BLEServer::create_service(const std::string &uuid, bool advertise) { - return this->create_service(ESPBTUUID::from_raw(uuid), advertise); -} -std::shared_ptr BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, - uint8_t inst_id) { - ESP_LOGV(TAG, "Creating service - %s", uuid.to_string().c_str()); - std::shared_ptr service = std::make_shared(uuid, num_handles, inst_id); - this->services_.emplace_back(service); - if (advertise) { - esp32_ble::global_ble->get_advertising()->add_service_uuid(uuid); +void BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) { + ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str()); + // If the service already exists, do nothing + BLEService *service = this->get_service(uuid); + if (service != nullptr) { + ESP_LOGW(TAG, "BLE service %s already exists", uuid.to_string().c_str()); + return; } + service = new BLEService(uuid, num_handles, inst_id, advertise); // NOLINT(cppcoreguidelines-owning-memory) + this->services_.emplace(uuid.to_string(), service); service->do_create(this); +} + +void BLEServer::remove_service(ESPBTUUID uuid) { + ESP_LOGV(TAG, "Removing BLE service - %s", uuid.to_string().c_str()); + BLEService *service = this->get_service(uuid); + if (service == nullptr) { + ESP_LOGW(TAG, "BLE service %s not found", uuid.to_string().c_str()); + return; + } + service->do_delete(); + delete service; // NOLINT(cppcoreguidelines-owning-memory) + this->services_.erase(uuid.to_string()); +} + +BLEService *BLEServer::get_service(ESPBTUUID uuid) { + BLEService *service = nullptr; + if (this->services_.count(uuid.to_string()) > 0) { + service = this->services_.at(uuid.to_string()); + } return service; } @@ -136,7 +164,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga ESP_LOGD(TAG, "BLE Client disconnected"); if (this->remove_client_(param->disconnect.conn_id)) this->connected_clients_--; - esp32_ble::global_ble->get_advertising()->start(); + this->parent_->advertising_start(); for (auto *component : this->service_components_) { component->on_client_disconnect(); } @@ -151,11 +179,22 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga break; } - for (const auto &service : this->services_) { - service->gatts_event_handler(event, gatts_if, param); + for (const auto &pair : this->services_) { + pair.second->gatts_event_handler(event, gatts_if, param); } } +void BLEServer::ble_before_disabled_event_handler() { + // Delete all clients + this->clients_.clear(); + // Delete all services + for (auto &pair : this->services_) { + pair.second->do_delete(); + } + this->registered_ = false; + this->state_ = INIT; +} + float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; } void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index ac759f2dcd..e379e67296 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -11,9 +11,9 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" -#include #include #include +#include #ifdef USE_ESP32 @@ -33,54 +33,56 @@ class BLEServiceComponent { virtual void stop(); }; -class BLEServer : public Component, public GATTsEventHandler, public Parented { +class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented { public: void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override; - bool can_proceed() override { return this->can_proceed_; } + bool can_proceed() override; void teardown(); + bool is_running(); void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; } void set_model(const std::string &model) { this->model_ = model; } + void set_manufacturer_data(const std::vector &data) { + this->manufacturer_data_ = data; + this->restart_advertising_(); + } - std::shared_ptr create_service(const uint8_t *uuid, bool advertise = false); - std::shared_ptr create_service(uint16_t uuid, bool advertise = false); - std::shared_ptr create_service(const std::string &uuid, bool advertise = false); - std::shared_ptr create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, - uint8_t inst_id = 0); + void create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0); + void remove_service(ESPBTUUID uuid); + BLEService *get_service(ESPBTUUID uuid); esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } uint32_t get_connected_client_count() { return this->connected_clients_; } - const std::map &get_clients() { return this->clients_; } + const std::unordered_map &get_clients() { return this->clients_; } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) override; + void ble_before_disabled_event_handler() override; + void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } protected: bool create_device_characteristics_(); + void restart_advertising_(); - void add_client_(uint16_t conn_id, void *client) { - this->clients_.insert(std::pair(conn_id, client)); - } + void add_client_(uint16_t conn_id, void *client) { this->clients_.emplace(conn_id, client); } bool remove_client_(uint16_t conn_id) { return this->clients_.erase(conn_id) > 0; } - bool can_proceed_{false}; - std::string manufacturer_; optional model_; + std::vector manufacturer_data_; esp_gatt_if_t gatts_if_{0}; bool registered_{false}; uint32_t connected_clients_{0}; - std::map clients_; - - std::vector> services_; - std::shared_ptr device_information_service_; + std::unordered_map clients_; + std::unordered_map services_; + BLEService *device_information_service_; std::vector service_components_; diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 4fcd2e3e79..368f03fb52 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -9,8 +9,8 @@ namespace esp32_ble_server { static const char *const TAG = "esp32_ble_server.service"; -BLEService::BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id) - : uuid_(uuid), num_handles_(num_handles), inst_id_(inst_id) {} +BLEService::BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id, bool advertise) + : uuid_(uuid), num_handles_(num_handles), inst_id_(inst_id), advertise_(advertise) {} BLEService::~BLEService() { for (auto &chr : this->characteristics_) @@ -58,6 +58,20 @@ void BLEService::do_create(BLEServer *server) { this->init_state_ = CREATING; } +void BLEService::do_delete() { + if (this->init_state_ == DELETING || this->init_state_ == DELETED) + return; + this->init_state_ = DELETING; + this->created_characteristic_count_ = 0; + this->last_created_characteristic_ = nullptr; + this->stop_(); + esp_err_t err = esp_ble_gatts_delete_service(this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_delete_service failed: %d", err); + return; + } +} + bool BLEService::do_create_characteristics_() { if (this->created_characteristic_count_ >= this->characteristics_.size() && (this->last_created_characteristic_ == nullptr || this->last_created_characteristic_->is_created())) @@ -75,22 +89,34 @@ bool BLEService::do_create_characteristics_() { void BLEService::start() { if (this->do_create_characteristics_()) return; + should_start_ = true; esp_err_t err = esp_ble_gatts_start_service(this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err); return; } + if (this->advertise_) + esp32_ble::global_ble->advertising_add_service_uuid(this->uuid_); this->running_state_ = STARTING; } void BLEService::stop() { + should_start_ = false; + this->stop_(); +} + +void BLEService::stop_() { + if (this->running_state_ == STOPPING || this->running_state_ == STOPPED) + return; + this->running_state_ = STOPPING; esp_err_t err = esp_ble_gatts_stop_service(this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err); return; } - this->running_state_ = STOPPING; + if (this->advertise_) + esp32_ble::global_ble->advertising_remove_service_uuid(this->uuid_); } bool BLEService::is_created() { return this->init_state_ == CREATED; } @@ -114,9 +140,16 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g this->inst_id_ == param->create.service_id.id.inst_id) { this->handle_ = param->create.service_handle; this->init_state_ = CREATED; + if (this->should_start_) + this->start(); } break; } + case ESP_GATTS_DELETE_EVT: + if (param->del.service_handle == this->handle_) { + this->init_state_ = DELETED; + } + break; case ESP_GATTS_START_EVT: { if (param->start.service_handle == this->handle_) { this->running_state_ = RUNNING; diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h index 2766c931a7..5e5883b6bf 100644 --- a/esphome/components/esp32_ble_server/ble_service.h +++ b/esphome/components/esp32_ble_server/ble_service.h @@ -7,11 +7,11 @@ #ifdef USE_ESP32 +#include #include #include #include #include -#include namespace esphome { namespace esp32_ble_server { @@ -22,7 +22,7 @@ using namespace esp32_ble; class BLEService { public: - BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id); + BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id, bool advertise); ~BLEService(); BLECharacteristic *get_characteristic(ESPBTUUID uuid); BLECharacteristic *get_characteristic(uint16_t uuid); @@ -38,6 +38,7 @@ class BLEService { BLEServer *get_server() { return this->server_; } void do_create(BLEServer *server); + void do_delete(); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void start(); @@ -48,6 +49,7 @@ class BLEService { bool is_running() { return this->running_state_ == RUNNING; } bool is_starting() { return this->running_state_ == STARTING; } + bool is_deleted() { return this->init_state_ == DELETED; } protected: std::vector characteristics_; @@ -58,8 +60,11 @@ class BLEService { uint16_t num_handles_; uint16_t handle_{0xFFFF}; uint8_t inst_id_; + bool advertise_{false}; + bool should_start_{false}; bool do_create_characteristics_(); + void stop_(); enum InitState : uint8_t { FAILED = 0x00, @@ -67,6 +72,8 @@ class BLEService { CREATING, CREATING_DEPENDENTS, CREATED, + DELETING, + DELETED, } init_state_{INIT}; enum RunningState : uint8_t { diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 23f109b5ac..0aa8eadd0a 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,23 +1,26 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv + from esphome import automation +import esphome.codegen as cg +from esphome.components import esp32_ble +from esphome.components.esp32 import add_idf_sdkconfig_option +import esphome.config_validation as cv from esphome.const import ( CONF_ACTIVE, + CONF_DURATION, CONF_ID, CONF_INTERVAL, - CONF_DURATION, - CONF_TRIGGER_ID, CONF_MAC_ADDRESS, - CONF_SERVICE_UUID, CONF_MANUFACTURER_ID, CONF_ON_BLE_ADVERTISE, - CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, + CONF_ON_BLE_SERVICE_DATA_ADVERTISE, + CONF_SERVICE_UUID, + CONF_TRIGGER_ID, + KEY_CORE, + KEY_FRAMEWORK_VERSION, ) -from esphome.components import esp32_ble from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] @@ -167,7 +170,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), } ), cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( @@ -212,6 +215,7 @@ async def to_code(config): parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) cg.add(parent.register_gap_event_handler(var)) cg.add(parent.register_gattc_event_handler(var)) + cg.add(parent.register_ble_status_event_handler(var)) cg.add(var.set_parent(parent)) params = config[CONF_SCAN_PARAMETERS] @@ -223,7 +227,10 @@ async def to_code(config): for conf in config.get(CONF_ON_BLE_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: - cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + addr_list = [] + for it in conf[CONF_MAC_ADDRESS]: + addr_list.append(it.as_hex) + cg.add(trigger.set_addresses(addr_list)) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -259,7 +266,11 @@ async def to_code(config): # https://github.com/espressif/esp-idf/issues/2503 # Match arduino CONFIG_BTU_TASK_STACK_SIZE # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 - add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6): + add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) + else: + add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 6131d6ddf7..6bef9edcb3 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,18 +10,22 @@ namespace esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_address(uint64_t address) { this->address_ = address; } + void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { - if (this->address_ && device.address_uint64() != this->address_) { - return false; + uint64_t u64_addr = device.address_uint64(); + if (!address_vec_.empty()) { + if (std::find(address_vec_.begin(), address_vec_.end(), u64_addr) == address_vec_.end()) { + return false; + } } + this->trigger(device); return true; } protected: - uint64_t address_ = 0; + std::vector address_vec_; }; class BLEServiceDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 26687ba9cc..74b4b9aa89 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -15,15 +15,19 @@ #include #include #include +#include #ifdef USE_OTA -#include "esphome/components/ota/ota_component.h" +#include "esphome/components/ota/ota_backend.h" #endif #ifdef USE_ARDUINO #include #endif +#define MBEDTLS_AES_ALT +#include + // bt_trace.h #undef TAG @@ -34,17 +38,6 @@ static const char *const TAG = "esp32_ble_tracker"; ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { - uint64_t u = 0; - u |= uint64_t(address[0] & 0xFF) << 40; - u |= uint64_t(address[1] & 0xFF) << 32; - u |= uint64_t(address[2] & 0xFF) << 24; - u |= uint64_t(address[3] & 0xFF) << 16; - u |= uint64_t(address[4] & 0xFF) << 8; - u |= uint64_t(address[5] & 0xFF) << 0; - return u; -} - float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void ESP32BLETracker::setup() { @@ -68,23 +61,26 @@ void ESP32BLETracker::setup() { this->scanner_idle_ = true; #ifdef USE_OTA - ota::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) { - if (state == ota::OTA_STARTED) { - this->stop_scan(); - } - }); + 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(); + } + }); #endif - - if (this->scan_continuous_) { - if (xSemaphoreTake(this->scan_end_lock_, 0L)) { - this->start_scan_(true); - } else { - ESP_LOGW(TAG, "Cannot start scan!"); - } - } } void ESP32BLETracker::loop() { + if (!this->parent_->is_active()) { + this->ble_was_disabled_ = true; + return; + } else if (this->ble_was_disabled_) { + this->ble_was_disabled_ = false; + // If the BLE stack was disabled, we need to start the scan again. + if (this->scan_continuous_) { + this->start_scan(); + } + } int connecting = 0; int discovered = 0; int searching = 0; @@ -114,10 +110,20 @@ void ESP32BLETracker::loop() { if (this->scan_result_index_ && // if it looks like we have a scan result we will take the lock xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { uint32_t index = this->scan_result_index_; - if (index) { - if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { - ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); + if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { + ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); + } + + if (this->raw_advertisements_) { + for (auto *listener : this->listeners_) { + listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); } + for (auto *client : this->clients_) { + client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } + } + + if (this->parse_advertisements_) { for (size_t i = 0; i < index; i++) { ESPBTDevice device; device.parse_scan_rst(this->scan_result_buffer_[i]); @@ -141,8 +147,8 @@ void ESP32BLETracker::loop() { this->print_bt_device_info(device); } } - this->scan_result_index_ = 0; } + this->scan_result_index_ = 0; xSemaphoreGive(this->scan_result_lock_); } @@ -182,8 +188,7 @@ void ESP32BLETracker::loop() { xSemaphoreGive(this->scan_end_lock_); } else { ESP_LOGD(TAG, "Stopping scan after failure..."); - esp_ble_gap_stop_scanning(); - this->cancel_timeout("scan"); + this->stop_scan_(); } if (this->scan_start_failed_) { ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_); @@ -212,8 +217,7 @@ void ESP32BLETracker::loop() { client->set_state(ClientState::READY_TO_CONNECT); } else { ESP_LOGD(TAG, "Pausing scan to make connection..."); - esp_ble_gap_stop_scanning(); - this->cancel_timeout("scan"); + this->stop_scan_(); } break; } @@ -232,11 +236,31 @@ void ESP32BLETracker::start_scan() { void ESP32BLETracker::stop_scan() { ESP_LOGD(TAG, "Stopping scan."); this->scan_continuous_ = false; - esp_ble_gap_stop_scanning(); + this->stop_scan_(); +} + +void ESP32BLETracker::ble_before_disabled_event_handler() { + this->stop_scan_(); + xSemaphoreGive(this->scan_end_lock_); +} + +void ESP32BLETracker::stop_scan_() { this->cancel_timeout("scan"); + if (this->scanner_idle_) { + return; + } + esp_err_t err = esp_ble_gap_stop_scanning(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_stop_scanning failed: %d", err); + return; + } } void ESP32BLETracker::start_scan_(bool first) { + if (!this->parent_->is_active()) { + ESP_LOGW(TAG, "Cannot start scan while ESP32BLE is disabled."); + return; + } // The lock must be held when calling this function. if (xSemaphoreTake(this->scan_end_lock_, 0L)) { ESP_LOGE(TAG, "start_scan called without holding scan_end_lock_"); @@ -249,15 +273,23 @@ void ESP32BLETracker::start_scan_(bool first) { listener->on_scan_end(); } this->already_discovered_.clear(); - this->scanner_idle_ = false; this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; this->scan_params_.scan_interval = this->scan_interval_; this->scan_params_.scan_window = this->scan_window_; - esp_ble_gap_set_scan_params(&this->scan_params_); - esp_ble_gap_start_scanning(this->scan_duration_); + esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_scan_params failed: %d", err); + return; + } + err = esp_ble_gap_start_scanning(this->scan_duration_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_start_scanning failed: %d", err); + return; + } + this->scanner_idle_ = false; this->set_timeout("scan", this->scan_duration_ * 2000, []() { ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); @@ -285,6 +317,32 @@ void ESP32BLETracker::end_of_scan_() { void ESP32BLETracker::register_client(ESPBTClient *client) { client->app_id = ++this->app_id_; this->clients_.push_back(client); + this->recalculate_advertisement_parser_types(); +} + +void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { + listener->set_parent(this); + this->listeners_.push_back(listener); + this->recalculate_advertisement_parser_types(); +} + +void ESP32BLETracker::recalculate_advertisement_parser_types() { + this->raw_advertisements_ = false; + this->parse_advertisements_ = false; + for (auto *listener : this->listeners_) { + if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { + this->parse_advertisements_ = true; + } else { + this->raw_advertisements_ = true; + } + } + for (auto *client : this->clients_) { + if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { + this->parse_advertisements_ = true; + } else { + this->raw_advertisements_ = true; + } + } } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { @@ -310,7 +368,11 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga } void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { - this->scan_set_param_failed_ = param.status; + if (param.status == ESP_BT_STATUS_DONE) { + this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; + } else { + this->scan_set_param_failed_ = param.status; + } } void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) { @@ -400,14 +462,16 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } for (auto &data : this->manufacturer_datas_) { - ESP_LOGVV(TAG, " Manufacturer data: %s", format_hex_pretty(data.data).c_str()); - if (this->get_ibeacon().has_value()) { - auto ibeacon = this->get_ibeacon().value(); - ESP_LOGVV(TAG, " iBeacon data:"); - ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str()); - ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major()); - ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor()); - ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power()); + 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()); + 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()); } } for (auto &data : this->service_datas_) { @@ -416,7 +480,7 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); } - ESP_LOGVV(TAG, "Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); + ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { @@ -585,11 +649,11 @@ std::string ESPBTDevice::address_str() const { this->address_[3], this->address_[4], this->address_[5]); return mac; } -uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); } +uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); - ESP_LOGCONFIG(TAG, " Scan Duration: %u s", this->scan_duration_); + ESP_LOGCONFIG(TAG, " Scan Duration: %" PRIu32 " s", this->scan_duration_); ESP_LOGCONFIG(TAG, " Scan Interval: %.1f ms", this->scan_interval_ * 0.625f); ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f); ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); @@ -634,6 +698,39 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { } } +bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { + uint8_t ecb_key[16]; + uint8_t ecb_plaintext[16]; + uint8_t ecb_ciphertext[16]; + + uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_); + + memcpy(&ecb_key, irk, 16); + memset(&ecb_plaintext, 0, 16); + + ecb_plaintext[13] = (addr64 >> 40) & 0xff; + ecb_plaintext[14] = (addr64 >> 32) & 0xff; + ecb_plaintext[15] = (addr64 >> 24) & 0xff; + + mbedtls_aes_context ctx = {0, 0, {0}}; + mbedtls_aes_init(&ctx); + + if (mbedtls_aes_setkey_enc(&ctx, ecb_key, 128) != 0) { + mbedtls_aes_free(&ctx); + return false; + } + + if (mbedtls_aes_crypt_ecb(&ctx, ESP_AES_ENCRYPT, ecb_plaintext, ecb_ciphertext) != 0) { + mbedtls_aes_free(&ctx); + return false; + } + + mbedtls_aes_free(&ctx); + + return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && + ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); +} + } // namespace esp32_ble_tracker } // namespace esphome diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 798f68f2bd..2fc5da829d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -2,6 +2,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include @@ -10,9 +11,9 @@ #ifdef USE_ESP32 +#include #include #include -#include #include #include @@ -27,6 +28,11 @@ using namespace esp32_ble; using adv_data_t = std::vector; +enum AdvertisementParserType { + PARSED_ADVERTISEMENTS, + RAW_ADVERTISEMENTS, +}; + struct ServiceData { ESPBTUUID uuid; adv_data_t data; @@ -38,10 +44,10 @@ class ESPBLEiBeacon { ESPBLEiBeacon(const uint8_t *data); static optional from_manufacturer_data(const ServiceData &data); - uint16_t get_major() { return ((this->beacon_data_.major & 0xFF) << 8) | (this->beacon_data_.major >> 8); } - uint16_t get_minor() { return ((this->beacon_data_.minor & 0xFF) << 8) | (this->beacon_data_.minor >> 8); } + uint16_t get_major() { return byteswap(this->beacon_data_.major); } + uint16_t get_minor() { return byteswap(this->beacon_data_.minor); } int8_t get_signal_power() { return this->beacon_data_.signal_power; } - ESPBTUUID get_uuid() { return ESPBTUUID::from_raw(this->beacon_data_.proximity_uuid); } + ESPBTUUID get_uuid() { return ESPBTUUID::from_raw_reversed(this->beacon_data_.proximity_uuid); } protected: struct { @@ -80,6 +86,8 @@ class ESPBTDevice { const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; } + bool resolve_irk(const uint8_t *irk) const; + optional get_ibeacon() const { for (auto &it : this->manufacturer_datas_) { auto res = ESPBLEiBeacon::from_manufacturer_data(it); @@ -113,6 +121,12 @@ class ESPBTDeviceListener { public: virtual void on_scan_end() {} virtual bool parse_device(const ESPBTDevice &device) = 0; + virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { + return false; + }; + virtual AdvertisementParserType get_advertisement_parser_type() { + return AdvertisementParserType::PARSED_ADVERTISEMENTS; + }; void set_parent(ESP32BLETracker *parent) { parent_ = parent; } protected: @@ -158,6 +172,7 @@ class ESPBTClient : public ESPBTDeviceListener { esp_ble_gattc_cb_param_t *param) = 0; virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void connect() = 0; + virtual void disconnect() = 0; virtual void set_state(ClientState st) { this->state_ = st; } ClientState state() const { return state_; } int app_id; @@ -166,7 +181,11 @@ class ESPBTClient : public ESPBTDeviceListener { ClientState state_; }; -class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEventHandler, public Parented { +class ESP32BLETracker : public Component, + public GAPEventHandler, + public GATTcEventHandler, + public BLEStatusEventHandler, + public Parented { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } @@ -181,12 +200,9 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv void loop() override; - void register_listener(ESPBTDeviceListener *listener) { - listener->set_parent(this); - this->listeners_.push_back(listener); - } - + void register_listener(ESPBTDeviceListener *listener); void register_client(ESPBTClient *client); + void recalculate_advertisement_parser_types(); void print_bt_device_info(const ESPBTDevice &device); @@ -196,8 +212,10 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void ble_before_disabled_event_handler() override; protected: + void stop_scan_(); /// Start a single scan by setting up the parameters and doing some esp-idf calls. void start_scan_(bool first); /// Called when a scan ends @@ -228,14 +246,17 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv bool scan_continuous_; bool scan_active_; bool scanner_idle_; + bool ble_was_disabled_{true}; + bool raw_advertisements_{false}; + bool parse_advertisements_{false}; SemaphoreHandle_t scan_result_lock_; SemaphoreHandle_t scan_end_lock_; size_t scan_result_index_{0}; -#if CONFIG_SPIRAM +#ifdef USE_PSRAM const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; #else const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 16; -#endif // CONFIG_SPIRAM +#endif // USE_PSRAM esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 4cbdf7ca5c..2f1f9b90bb 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -14,9 +14,10 @@ from esphome.const import ( CONF_BRIGHTNESS, CONF_CONTRAST, CONF_TRIGGER_ID, + CONF_VSYNC_PIN, ) from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import add_idf_component from esphome.cpp_helpers import setup_entity DEPENDENCIES = ["esp32"] @@ -25,6 +26,11 @@ AUTO_LOAD = ["psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) +ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") +# Triggers +ESP32CameraImageTrigger = esp32_camera_ns.class_( + "ESP32CameraImageTrigger", automation.Trigger.template() +) ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( "ESP32CameraStreamStartTrigger", automation.Trigger.template(), @@ -107,7 +113,6 @@ ENUM_SPECIAL_EFFECT = { } # pin assignment -CONF_VSYNC_PIN = "vsync_pin" CONF_HREF_PIN = "href_pin" CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin" CONF_EXTERNAL_CLOCK = "external_clock" @@ -135,10 +140,13 @@ CONF_TEST_PATTERN = "test_pattern" # framerates CONF_MAX_FRAMERATE = "max_framerate" CONF_IDLE_FRAMERATE = "idle_framerate" +# frame buffer +CONF_FRAME_BUFFER_COUNT = "frame_buffer_count" # stream trigger CONF_ON_STREAM_START = "on_stream_start" CONF_ON_STREAM_STOP = "on_stream_stop" +CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) @@ -207,6 +215,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( cv.framerate, cv.Range(min=0, max=1) ), + cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -221,6 +230,11 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ), } ), + cv.Optional(CONF_ON_IMAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -274,13 +288,17 @@ async def to_code(config): cg.add(var.set_idle_update_interval(0)) else: cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE])) + cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT])) cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_ESP32_CAMERA") if CORE.using_esp_idf: - cg.add_library("espressif/esp32-camera", "1.0.0") - add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) + add_idf_component( + name="esp32-camera", + repo="https://github.com/espressif/esp32-camera.git", + ref="v2.0.9", + ) for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -289,3 +307,9 @@ async def to_code(config): for conf in config.get(CONF_ON_STREAM_STOP, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_IMAGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(ESP32CameraImageData, "image")], conf + ) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index e4020a902e..e9e9d3cffb 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -37,7 +37,7 @@ void ESP32Camera::setup() { "framebuffer_task", // name 1024, // stack size nullptr, // task pv params - 0, // priority + 1, // priority nullptr, // handle 1 // core ); @@ -127,7 +127,7 @@ void ESP32Camera::dump_config() { sensor_t *s = esp_camera_sensor_get(); auto st = s->status; ESP_LOGCONFIG(TAG, " JPEG Quality: %u", st.quality); - // ESP_LOGCONFIG(TAG, " Framebuffer Count: %u", conf.fb_count); + ESP_LOGCONFIG(TAG, " Framebuffer Count: %u", conf.fb_count); ESP_LOGCONFIG(TAG, " Contrast: %d", st.contrast); ESP_LOGCONFIG(TAG, " Brightness: %d", st.brightness); ESP_LOGCONFIG(TAG, " Saturation: %d", st.saturation); @@ -212,6 +212,8 @@ ESP32Camera::ESP32Camera() { this->config_.frame_size = FRAMESIZE_VGA; // 640x480 this->config_.jpeg_quality = 10; this->config_.fb_count = 1; + this->config_.grab_mode = CAMERA_GRAB_WHEN_EMPTY; + this->config_.fb_location = CAMERA_FB_IN_PSRAM; global_esp32_camera = this; } @@ -333,10 +335,16 @@ void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) { void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { this->idle_update_interval_ = idle_update_interval; } +/* set frame buffer parameters */ +void ESP32Camera::set_frame_buffer_mode(camera_grab_mode_t mode) { this->config_.grab_mode = mode; } +void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) { + this->config_.fb_count = fb_count; + this->set_frame_buffer_mode(fb_count > 1 ? CAMERA_GRAB_LATEST : CAMERA_GRAB_WHEN_EMPTY); +} /* ---------------- public API (specific) ---------------- */ -void ESP32Camera::add_image_callback(std::function)> &&f) { - this->new_image_callback_.add(std::move(f)); +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)); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 5f88c6fda8..71f47d3c06 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -86,6 +86,11 @@ class CameraImage { uint8_t requesters_; }; +struct CameraImageData { + uint8_t *data; + size_t length; +}; + /* ---------------- CameraImageReader class ---------------- */ class CameraImageReader { public: @@ -140,6 +145,9 @@ class ESP32Camera : public Component, public EntityBase { /* -- framerates */ void set_max_update_interval(uint32_t max_update_interval); void set_idle_update_interval(uint32_t idle_update_interval); + /* -- frame buffer */ + void set_frame_buffer_mode(camera_grab_mode_t mode); + void set_frame_buffer_count(uint8_t fb_count); /* public API (derivated) */ void setup() override; @@ -147,12 +155,12 @@ class ESP32Camera : public Component, public EntityBase { void dump_config() override; float get_setup_priority() const override; /* public API (specific) */ - void add_image_callback(std::function)> &&f); void start_stream(CameraRequester requester); void stop_stream(CameraRequester requester); void request_image(CameraRequester requester); void update_camera_parameters(); + void add_image_callback(std::function)> &&callback); void add_stream_start_callback(std::function &&callback); void add_stream_stop_callback(std::function &&callback); @@ -196,7 +204,7 @@ class ESP32Camera : public Component, public EntityBase { uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; - CallbackManager)> new_image_callback_; + CallbackManager)> new_image_callback_{}; CallbackManager stream_start_callback_{}; CallbackManager stream_stop_callback_{}; @@ -207,6 +215,18 @@ class ESP32Camera : public Component, public EntityBase { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32Camera *global_esp32_camera; +class ESP32CameraImageTrigger : public Trigger { + 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); + }); + } +}; + class ESP32CameraStreamStartTrigger : public Trigger<> { public: explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { 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 3210989ff5..7ca0c56d23 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -194,8 +194,8 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { int64_t frame_time = millis() - last_frame; last_frame = millis(); - ESP_LOGD(TAG, "MJPG: %uB %ums (%.1ffps)", (uint32_t) image->get_data_length(), (uint32_t) frame_time, - 1000.0 / (uint32_t) frame_time); + ESP_LOGD(TAG, "MJPG: %" PRIu32 "B %" PRIu32 "ms (%.1ffps)", (uint32_t) image->get_data_length(), + (uint32_t) frame_time, 1000.0 / (uint32_t) frame_time); } } @@ -205,7 +205,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::WEB_REQUESTER); - ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames); + ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames); return res; } 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 509ca81592..f65625554c 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.h +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include #include #include diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index 7761418c6a..37bdfa3962 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -1,9 +1,24 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import canbus -from esphome.const import CONF_ID, CONF_RX_PIN, CONF_TX_PIN -from esphome.components.canbus import CanbusComponent, CanSpeed, CONF_BIT_RATE +from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_RX_PIN, + CONF_RX_QUEUE_LEN, + CONF_TX_PIN, + CONF_TX_QUEUE_LEN, +) CODEOWNERS = ["@Sympatron"] DEPENDENCIES = ["esp32"] @@ -12,21 +27,63 @@ esp32_can_ns = cg.esphome_ns.namespace("esp32_can") esp32_can = esp32_can_ns.class_("ESP32Can", CanbusComponent) # Currently the driver only supports a subset of the bit rates defined in canbus -CAN_SPEEDS = { +# The supported bit rates differ between ESP32 variants. +# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI) + +CAN_SPEEDS_ESP32 = { + "25KBPS": CanSpeed.CAN_25KBPS, "50KBPS": CanSpeed.CAN_50KBPS, "100KBPS": CanSpeed.CAN_100KBPS, "125KBPS": CanSpeed.CAN_125KBPS, "250KBPS": CanSpeed.CAN_250KBPS, "500KBPS": CanSpeed.CAN_500KBPS, + "800KBPS": CanSpeed.CAN_800KBPS, "1000KBPS": CanSpeed.CAN_1000KBPS, } +CAN_SPEEDS_ESP32_S2 = { + "1KBPS": CanSpeed.CAN_1KBPS, + "5KBPS": CanSpeed.CAN_5KBPS, + "10KBPS": CanSpeed.CAN_10KBPS, + "12K5BPS": CanSpeed.CAN_12K5BPS, + "16KBPS": CanSpeed.CAN_16KBPS, + "20KBPS": CanSpeed.CAN_20KBPS, + **CAN_SPEEDS_ESP32, +} + +CAN_SPEEDS_ESP32_S3 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_C3 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_C6 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_H2 = {**CAN_SPEEDS_ESP32_S2} + +CAN_SPEEDS = { + VARIANT_ESP32: CAN_SPEEDS_ESP32, + 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, +} + + +def validate_bit_rate(value): + variant = get_esp32_variant() + if variant not in CAN_SPEEDS: + raise cv.Invalid(f"{variant} is not supported by component {esp32_can_ns}") + value = value.upper() + if value not in CAN_SPEEDS[variant]: + raise cv.Invalid(f"Bit rate {value} is not supported on {variant}") + return cv.enum(CAN_SPEEDS[variant])(value) + + CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(esp32_can), - cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True), + cv.Optional(CONF_BIT_RATE, default="125KBPS"): validate_bit_rate, cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_RX_QUEUE_LEN): cv.uint32_t, + cv.Optional(CONF_TX_QUEUE_LEN): cv.uint32_t, } ) @@ -37,3 +94,7 @@ async def to_code(config): cg.add(var.set_rx(config[CONF_RX_PIN])) cg.add(var.set_tx(config[CONF_TX_PIN])) + if (rx_queue_len := config.get(CONF_RX_QUEUE_LEN)) is not None: + cg.add(var.set_rx_queue_len(rx_queue_len)) + if (tx_queue_len := config.get(CONF_TX_QUEUE_LEN)) is not None: + cg.add(var.set_tx_queue_len(tx_queue_len)) diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index 3eb2d1f035..5a45859b1f 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -16,6 +16,30 @@ 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_ESP32H6) + case canbus::CAN_1KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1KBITS(); + return true; + case canbus::CAN_5KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_5KBITS(); + return true; + case canbus::CAN_10KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_10KBITS(); + return true; + case canbus::CAN_12K5BPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_12_5KBITS(); + return true; + case canbus::CAN_16KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS(); + return true; + case canbus::CAN_20KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS(); + return true; +#endif + case canbus::CAN_25KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_25KBITS(); + return true; case canbus::CAN_50KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_50KBITS(); return true; @@ -31,6 +55,9 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config case canbus::CAN_500KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_500KBITS(); return true; + case canbus::CAN_800KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_800KBITS(); + return true; case canbus::CAN_1000KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1MBITS(); return true; @@ -42,6 +69,13 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config bool ESP32Can::setup_internal() { twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); + if (this->tx_queue_len_.has_value()) { + g_config.tx_queue_len = this->tx_queue_len_.value(); + } + if (this->rx_queue_len_.has_value()) { + g_config.rx_queue_len = this->rx_queue_len_.value(); + } + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); twai_timing_config_t t_config; @@ -84,6 +118,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { .flags = flags, .identifier = frame->can_id, .data_length_code = frame->can_data_length_code, + .data = {}, // to suppress warning, data is initialized properly below }; if (!frame->remote_transmission_request) { memcpy(message.data, frame->data, frame->can_data_length_code); diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h index a428834f65..b3086f9a48 100644 --- a/esphome/components/esp32_can/esp32_can.h +++ b/esphome/components/esp32_can/esp32_can.h @@ -12,6 +12,8 @@ class ESP32Can : public canbus::Canbus { public: void set_rx(int rx) { rx_ = rx; } void set_tx(int tx) { tx_ = tx; } + void set_tx_queue_len(uint32_t tx_queue_len) { this->tx_queue_len_ = tx_queue_len; } + void set_rx_queue_len(uint32_t rx_queue_len) { this->rx_queue_len_ = rx_queue_len; } ESP32Can(){}; protected: @@ -21,6 +23,8 @@ class ESP32Can : public canbus::Canbus { int rx_{-1}; int tx_{-1}; + optional tx_queue_len_{}; + optional rx_queue_len_{}; }; } // namespace esp32_can diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index ae7f0b6427..ecc07d4c91 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,25 +1,47 @@ +from esphome import automation import esphome.codegen as cg +from esphome.components import binary_sensor, esp32_ble_server, output import esphome.config_validation as cv -from esphome.components import binary_sensor, output, esp32_ble_server -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID - -AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"] +AUTO_LOAD = ["esp32_ble_server"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["wifi", "esp32"] CONF_AUTHORIZED_DURATION = "authorized_duration" CONF_AUTHORIZER = "authorizer" 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" +improv_ns = cg.esphome_ns.namespace("improv") +Error = improv_ns.enum("Error") +State = improv_ns.enum("State") + esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") ESP32ImprovComponent = esp32_improv_ns.class_( "ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent ) +ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_( + "ESP32ImprovProvisionedTrigger", automation.Trigger.template() +) +ESP32ImprovProvisioningTrigger = esp32_improv_ns.class_( + "ESP32ImprovProvisioningTrigger", automation.Trigger.template() +) +ESP32ImprovStartTrigger = esp32_improv_ns.class_( + "ESP32ImprovStartTrigger", automation.Trigger.template() +) +ESP32ImprovStateTrigger = esp32_improv_ns.class_( + "ESP32ImprovStateTrigger", automation.Trigger.template() +) +ESP32ImprovStoppedTrigger = esp32_improv_ns.class_( + "ESP32ImprovStoppedTrigger", automation.Trigger.template() +) CONFIG_SCHEMA = cv.Schema( @@ -36,6 +58,40 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_WIFI_TIMEOUT, default="1min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovProvisionedTrigger + ), + } + ), + cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovProvisioningTrigger + ), + } + ), + cv.Optional(CONF_ON_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger), + } + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger), + } + ), + cv.Optional(CONF_ON_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovStoppedTrigger + ), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -48,11 +104,13 @@ async def to_code(config): cg.add(ble_server.register_service_component(var)) cg.add_define("USE_IMPROV") - cg.add_library("esphome/Improv", "1.2.3") + cg.add_library("improv/Improv", "1.2.4") cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) + cg.add(var.set_wifi_timeout(config[CONF_WIFI_TIMEOUT])) + if CONF_AUTHORIZER in config and config[CONF_AUTHORIZER] is not None: activator = await cg.get_variable(config[CONF_AUTHORIZER]) cg.add(var.set_authorizer(activator)) @@ -60,3 +118,29 @@ async def to_code(config): if CONF_STATUS_INDICATOR in config: status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR]) cg.add(var.set_status_indicator(status_indicator)) + + use_state_callback = False + for conf in config.get(CONF_ON_PROVISIONED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + for conf in config.get(CONF_ON_PROVISIONING, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + for conf in config.get(CONF_ON_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(State, "state"), (Error, "error")], conf + ) + use_state_callback = True + for conf in config.get(CONF_ON_STOP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + if use_state_callback: + cg.add_define("USE_ESP32_IMPROV_STATE_CALLBACK") diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/esp32_improv/automation.h new file mode 100644 index 0000000000..52c5da125b --- /dev/null +++ b/esphome/components/esp32_improv/automation.h @@ -0,0 +1,72 @@ +#pragma once +#ifdef USE_ESP32 +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#include "esp32_improv_component.h" + +#include "esphome/core/automation.h" + +#include + +namespace esphome { +namespace esp32_improv { + +class ESP32ImprovProvisionedTrigger : public Trigger<> { + public: + explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONED && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class ESP32ImprovProvisioningTrigger : public Trigger<> { + public: + explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONING && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class ESP32ImprovStartTrigger : public Trigger<> { + public: + explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && + !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class ESP32ImprovStateTrigger : public Trigger { + public: + explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if (!parent->is_failed()) { + trigger(state, error); + } + }); + } +}; + +class ESP32ImprovStoppedTrigger : public Trigger<> { + public: + explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if (state == improv::STATE_STOPPED && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +} // namespace esp32_improv +} // namespace esphome +#endif +#endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 85013c006b..d36b50feb0 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -16,8 +16,16 @@ static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirec ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } void ESP32ImprovComponent::setup() { - this->service_ = global_ble_server->create_service(improv::SERVICE_UUID, true); - this->setup_characteristics(); +#ifdef USE_BINARY_SENSOR + if (this->authorizer_ != nullptr) { + this->authorizer_->add_on_state_callback([this](bool state) { + if (state) { + this->authorized_start_ = millis(); + this->identify_start_ = 0; + } + }); + } +#endif } void ESP32ImprovComponent::setup_characteristics() { @@ -50,29 +58,47 @@ void ESP32ImprovComponent::setup_characteristics() { BLEDescriptor *capabilities_descriptor = new BLE2902(); this->capabilities_->add_descriptor(capabilities_descriptor); uint8_t capabilities = 0x00; +#ifdef USE_OUTPUT if (this->status_indicator_ != nullptr) capabilities |= improv::CAPABILITY_IDENTIFY; +#endif this->capabilities_->set_value(capabilities); this->setup_complete_ = true; } void ESP32ImprovComponent::loop() { + if (!global_ble_server->is_running()) { + if (this->state_ != improv::STATE_STOPPED) { + this->state_ = improv::STATE_STOPPED; +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK + this->state_callback_.call(this->state_, this->error_state_); +#endif + } + this->incoming_data_.clear(); + return; + } + if (this->service_ == nullptr) { + // Setup the service + ESP_LOGD(TAG, "Creating Improv service"); + global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true); + this->service_ = global_ble_server->get_service(ESPBTUUID::from_raw(improv::SERVICE_UUID)); + this->setup_characteristics(); + } + if (!this->incoming_data_.empty()) this->process_incoming_data_(); uint32_t now = millis(); switch (this->state_) { case improv::STATE_STOPPED: - if (this->status_indicator_ != nullptr) - this->status_indicator_->turn_off(); + this->set_status_indicator_state_(false); if (this->service_->is_created() && this->should_start_ && this->setup_complete_) { if (this->service_->is_running()) { - esp32_ble::global_ble->get_advertising()->start(); + esp32_ble::global_ble->advertising_start(); this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); this->set_error_(improv::ERROR_NONE); - this->should_start_ = false; ESP_LOGD(TAG, "Service started!"); } else { this->service_->start(); @@ -80,18 +106,22 @@ void ESP32ImprovComponent::loop() { } break; case improv::STATE_AWAITING_AUTHORIZATION: { - if (this->authorizer_ == nullptr || this->authorizer_->state) { +#ifdef USE_BINARY_SENSOR + if (this->authorizer_ == nullptr || + (this->authorized_start_ != 0 && ((now - this->authorized_start_) < this->authorized_duration_))) { this->set_state_(improv::STATE_AUTHORIZED); - this->authorized_start_ = now; - } else { - if (this->status_indicator_ != nullptr) { - if (!this->check_identify_()) - this->status_indicator_->turn_on(); - } + } else +#else + this->set_state_(improv::STATE_AUTHORIZED); +#endif + { + if (!this->check_identify_()) + this->set_status_indicator_state_(true); } break; } case improv::STATE_AUTHORIZED: { +#ifdef USE_BINARY_SENSOR if (this->authorizer_ != nullptr) { if (now - this->authorized_start_ > this->authorized_duration_) { ESP_LOGD(TAG, "Authorization timeout"); @@ -99,25 +129,14 @@ void ESP32ImprovComponent::loop() { return; } } - if (this->status_indicator_ != nullptr) { - if (!this->check_identify_()) { - if ((now % 1000) < 500) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } - } +#endif + if (!this->check_identify_()) { + this->set_status_indicator_state_((now % 1000) < 500); } break; } case improv::STATE_PROVISIONING: { - if (this->status_indicator_ != nullptr) { - if ((now % 200) < 100) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } - } + this->set_status_indicator_state_((now % 200) < 100); if (wifi::global_wifi_component->is_connected()) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); @@ -127,28 +146,43 @@ void ESP32ImprovComponent::loop() { std::vector urls = {ESPHOME_MY_LINK}; #ifdef USE_WEBSERVER - auto ip = wifi::global_wifi_component->wifi_sta_ip(); - std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); - urls.push_back(webserver_url); + for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { + if (ip.is_ip4()) { + std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); + urls.push_back(webserver_url); + break; + } + } #endif std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); this->send_response_(data); - this->set_timeout("end-service", 1000, [this] { - this->service_->stop(); - this->set_state_(improv::STATE_STOPPED); - }); + this->stop(); } break; } case improv::STATE_PROVISIONED: { this->incoming_data_.clear(); - if (this->status_indicator_ != nullptr) - this->status_indicator_->turn_off(); + this->set_status_indicator_state_(false); break; } } } +void ESP32ImprovComponent::set_status_indicator_state_(bool state) { +#ifdef USE_OUTPUT + if (this->status_indicator_ == nullptr) + return; + if (this->status_indicator_state_ == state) + return; + this->status_indicator_state_ = state; + if (state) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } +#endif +} + bool ESP32ImprovComponent::check_identify_() { uint32_t now = millis(); @@ -156,11 +190,7 @@ bool ESP32ImprovComponent::check_identify_() { if (identify) { uint32_t time = now % 1000; - if (time < 600 && time % 200 < 100) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } + this->set_status_indicator_state_(time < 600 && time % 200 < 100); } return identify; } @@ -174,6 +204,27 @@ void ESP32ImprovComponent::set_state_(improv::State state) { if (state != improv::STATE_STOPPED) this->status_->notify(); } + std::vector service_data(8, 0); + service_data[0] = 0x77; // PR + service_data[1] = 0x46; // IM + service_data[2] = static_cast(state); + + uint8_t capabilities = 0x00; +#ifdef USE_OUTPUT + if (this->status_indicator_ != nullptr) + capabilities |= improv::CAPABILITY_IDENTIFY; +#endif + + service_data[3] = capabilities; + service_data[4] = 0x00; // Reserved + service_data[5] = 0x00; // Reserved + service_data[6] = 0x00; // Reserved + service_data[7] = 0x00; // Reserved + + esp32_ble::global_ble->advertising_set_service_data(service_data); +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK + this->state_callback_.call(this->state_, this->error_state_); +#endif } void ESP32ImprovComponent::set_error_(improv::Error error) { @@ -203,7 +254,10 @@ void ESP32ImprovComponent::start() { } void ESP32ImprovComponent::stop() { + this->should_start_ = false; this->set_timeout("end-service", 1000, [this] { + if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr) + return; this->service_->stop(); this->set_state_(improv::STATE_STOPPED); }); @@ -213,14 +267,18 @@ float ESP32ImprovComponent::get_setup_priority() const { return setup_priority:: void ESP32ImprovComponent::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 Improv:"); +#ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_); +#endif +#ifdef USE_OUTPUT ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr)); +#endif } void ESP32ImprovComponent::process_incoming_data_() { uint8_t length = this->incoming_data_[1]; - ESP_LOGD(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str()); + ESP_LOGV(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str()); if (this->incoming_data_.size() - 3 == length) { this->set_error_(improv::ERROR_NONE); improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_); @@ -243,9 +301,9 @@ void ESP32ImprovComponent::process_incoming_data_() { this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_scanning(); + wifi::global_wifi_component->start_connecting(sta, false); this->set_state_(improv::STATE_PROVISIONING); - ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this); @@ -263,7 +321,7 @@ void ESP32ImprovComponent::process_incoming_data_() { this->incoming_data_.clear(); } } else if (this->incoming_data_.size() - 2 > length) { - ESP_LOGV(TAG, "Too much data came in, or malformed resetting buffer..."); + ESP_LOGV(TAG, "Too much data received or data malformed; resetting buffer..."); this->incoming_data_.clear(); } else { ESP_LOGV(TAG, "Waiting for split data packets..."); @@ -273,9 +331,11 @@ void ESP32ImprovComponent::process_incoming_data_() { void ESP32ImprovComponent::on_wifi_connect_timeout_() { this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_state_(improv::STATE_AUTHORIZED); +#ifdef USE_BINARY_SENSOR if (this->authorizer_ != nullptr) this->authorized_start_ = millis(); - ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); +#endif + ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); wifi::global_wifi_component->clear_sta(); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 1a142c94b6..062b3f585b 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -1,14 +1,26 @@ #pragma once -#include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/esp32_ble_server/ble_characteristic.h" -#include "esphome/components/esp32_ble_server/ble_server.h" -#include "esphome/components/output/binary_output.h" -#include "esphome/components/wifi/wifi_component.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/components/esp32_ble_server/ble_characteristic.h" +#include "esphome/components/esp32_ble_server/ble_server.h" +#include "esphome/components/wifi/wifi_component.h" + +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#include "esphome/core/automation.h" +#endif + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_OUTPUT +#include "esphome/components/output/binary_output.h" +#endif + #include #ifdef USE_ESP32 @@ -34,11 +46,26 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { void stop() override; bool is_active() const { return this->state_ != improv::STATE_STOPPED; } +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK + void add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); + } +#endif +#ifdef USE_BINARY_SENSOR void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; } +#endif +#ifdef USE_OUTPUT void set_status_indicator(output::BinaryOutput *status_indicator) { this->status_indicator_ = status_indicator; } +#endif void set_identify_duration(uint32_t identify_duration) { this->identify_duration_ = identify_duration; } void set_authorized_duration(uint32_t authorized_duration) { this->authorized_duration_ = authorized_duration; } + void set_wifi_timeout(uint32_t wifi_timeout) { this->wifi_timeout_ = wifi_timeout; } + uint32_t get_wifi_timeout() const { return this->wifi_timeout_; } + + improv::State get_improv_state() const { return this->state_; } + improv::Error get_improv_error_state() const { return this->error_state_; } + protected: bool should_start_{false}; bool setup_complete_{false}; @@ -48,21 +75,33 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { uint32_t authorized_start_{0}; uint32_t authorized_duration_; + uint32_t wifi_timeout_{}; + std::vector incoming_data_; wifi::WiFiAP connecting_sta_; - std::shared_ptr service_; + BLEService *service_ = nullptr; BLECharacteristic *status_; BLECharacteristic *error_; BLECharacteristic *rpc_; BLECharacteristic *rpc_response_; BLECharacteristic *capabilities_; +#ifdef USE_BINARY_SENSOR binary_sensor::BinarySensor *authorizer_{nullptr}; +#endif +#ifdef USE_OUTPUT output::BinaryOutput *status_indicator_{nullptr}; +#endif improv::State state_{improv::STATE_STOPPED}; improv::Error error_state_{improv::ERROR_NONE}; +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK + CallbackManager state_callback_{}; +#endif + + bool status_indicator_state_{false}; + void set_status_indicator_state_(bool state); void set_state_(improv::State state); void set_error_(improv::Error error); diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py new file mode 100644 index 0000000000..bda240680b --- /dev/null +++ b/esphome/components/esp32_rmt/__init__.py @@ -0,0 +1,55 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.components import esp32 + +CODEOWNERS = ["@jesserockz"] + +RMT_TX_CHANNELS = { + esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], + esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32C3: [0, 1], + esp32.const.VARIANT_ESP32C6: [0, 1], + esp32.const.VARIANT_ESP32H2: [0, 1], +} + +RMT_RX_CHANNELS = { + esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], + esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32S3: [4, 5, 6, 7], + esp32.const.VARIANT_ESP32C3: [2, 3], + esp32.const.VARIANT_ESP32C6: [2, 3], + esp32.const.VARIANT_ESP32H2: [2, 3], +} + +rmt_channel_t = cg.global_ns.enum("rmt_channel_t") +RMT_CHANNEL_ENUMS = { + 0: rmt_channel_t.RMT_CHANNEL_0, + 1: rmt_channel_t.RMT_CHANNEL_1, + 2: rmt_channel_t.RMT_CHANNEL_2, + 3: rmt_channel_t.RMT_CHANNEL_3, + 4: rmt_channel_t.RMT_CHANNEL_4, + 5: rmt_channel_t.RMT_CHANNEL_5, + 6: rmt_channel_t.RMT_CHANNEL_6, + 7: rmt_channel_t.RMT_CHANNEL_7, +} + + +def validate_rmt_channel(*, tx: bool): + + rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS + + def _validator(value): + cv.only_on_esp32(value) + value = cv.int_(value) + variant = esp32.get_esp32_variant() + if variant not in rmt_channels: + raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") + if value not in rmt_channels[variant]: + raise cv.Invalid( + f"RMT channel {value} does not support {'transmitting' if tx else 'receiving'} for ESP32 variant {variant}." + ) + return cv.enum(RMT_CHANNEL_ENUMS)(value) + + return _validator diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index eec1bdc992..c2209f7a6c 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -1,3 +1,4 @@ +#include #include "led_strip.h" #ifdef USE_ESP32 @@ -12,6 +13,8 @@ namespace esp32_rmt_led_strip { static const char *const TAG = "esp32_rmt_led_strip"; +static const uint32_t RMT_CLK_FREQ = 80000000; + static const uint8_t RMT_CLK_DIV = 2; void ESP32RMTLEDStripLightOutput::setup() { @@ -19,7 +22,7 @@ void ESP32RMTLEDStripLightOutput::setup() { size_t buffer_size = this->get_buffer_size_(); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); this->buf_ = allocator.allocate(buffer_size); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Cannot allocate LED buffer!"); @@ -34,8 +37,9 @@ void ESP32RMTLEDStripLightOutput::setup() { return; } - ExternalRAMAllocator rmt_allocator(ExternalRAMAllocator::ALLOW_FAILURE); - this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8); // 8 bits per byte, 1 rmt_item32_t per bit + RAMAllocator rmt_allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); + this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + + 1); // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset rmt_config_t config; memset(&config, 0, sizeof(config)); @@ -63,8 +67,8 @@ void ESP32RMTLEDStripLightOutput::setup() { } void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, - uint32_t bit1_low) { - float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f; + uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) { + float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; // 0-bit this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); @@ -76,6 +80,11 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi this->bit1_.level0 = 1; this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); this->bit1_.level1 = 0; + // reset + this->reset_.duration0 = (uint32_t) (ratio * reset_time_high); + this->reset_.level0 = 1; + this->reset_.duration1 = (uint32_t) (ratio * reset_time_low); + this->reset_.level1 = 0; } void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { @@ -115,6 +124,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { psrc++; } + if (this->reset_.duration0 > 0 || this->reset_.duration1 > 0) { + pdest->val = this->reset_.val; + pdest++; + len++; + } + if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) { ESP_LOGE(TAG, "RMT TX error"); this->status_set_warning(); @@ -157,11 +172,13 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index b = 0; break; } - uint8_t multiplier = this->is_rgbw_ ? 4 : 3; - return {this->buf_ + (index * multiplier) + r, - this->buf_ + (index * multiplier) + g, - this->buf_ + (index * multiplier) + b, - this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, + uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3; + uint8_t white = this->is_wrgb_ ? 0 : 3; + + return {this->buf_ + (index * multiplier) + r + this->is_wrgb_, + this->buf_ + (index * multiplier) + g + this->is_wrgb_, + this->buf_ + (index * multiplier) + b + this->is_wrgb_, + this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr, &this->effect_data_[index], &this->correction_}; } @@ -195,7 +212,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { break; } ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order); - ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_); + ESP_LOGCONFIG(TAG, " Max refresh rate: %" PRIu32, *this->max_refresh_rate_); ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_); } diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index 11d61b07e1..d21bd86e75 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -33,7 +33,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { int32_t size() const override { return this->num_leds_; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->is_rgbw_) { + if (this->is_rgbw_ || this->is_wrgb_) { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}); } else { traits.set_supported_color_modes({light::ColorMode::RGB}); @@ -44,11 +44,14 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { void set_pin(uint8_t pin) { this->pin_ = pin; } 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; } + void set_use_psram(bool use_psram) { this->use_psram_ = use_psram; } /// Set a maximum refresh rate in µs as some lights do not like being updated too often. void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } - void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low); + void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low, + uint32_t reset_time_high, uint32_t reset_time_low); void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } @@ -63,7 +66,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { protected: light::ESPColorView get_view_internal(int32_t index) const override; - size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + size_t get_buffer_size_() const { return this->num_leds_ * (this->is_rgbw_ || this->is_wrgb_ ? 4 : 3); } uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -72,8 +75,10 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint8_t pin_; uint16_t num_leds_; bool is_rgbw_; + bool is_wrgb_; + bool use_psram_; - rmt_item32_t bit0_, bit1_; + rmt_item32_t bit0_, bit1_, reset_; RGBOrder rgb_order_; rmt_channel_t channel_; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 3ca758c1e1..79f339e248 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,14 +3,16 @@ from dataclasses import dataclass import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.components import esp32, light +from esphome.components import esp32_rmt, light from esphome.const import ( CONF_CHIPSET, + CONF_IS_RGBW, CONF_MAX_REFRESH_RATE, CONF_NUM_LEDS, CONF_OUTPUT_ID, CONF_PIN, CONF_RGB_ORDER, + CONF_RMT_CHANNEL, ) CODEOWNERS = ["@jesserockz"] @@ -41,40 +43,26 @@ class LEDStripTimings: bit0_low: int bit1_high: int bit1_low: int + reset_high: int + reset_low: int CHIPSETS = { - "WS2812": LEDStripTimings(400, 1000, 1000, 400), - "SK6812": LEDStripTimings(300, 900, 600, 600), - "APA106": LEDStripTimings(350, 1360, 1360, 350), - "SM16703": LEDStripTimings(300, 900, 1360, 350), + "WS2811": LEDStripTimings(300, 1090, 1090, 320, 0, 300000), + "WS2812": LEDStripTimings(400, 1000, 1000, 400, 0, 0), + "SK6812": LEDStripTimings(300, 900, 600, 600, 0, 0), + "APA106": LEDStripTimings(350, 1360, 1360, 350, 0, 0), + "SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0), } - -CONF_IS_RGBW = "is_rgbw" +CONF_USE_PSRAM = "use_psram" +CONF_IS_WRGB = "is_wrgb" CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_LOW = "bit0_low" CONF_BIT1_HIGH = "bit1_high" CONF_BIT1_LOW = "bit1_low" -CONF_RMT_CHANNEL = "rmt_channel" - -RMT_CHANNELS = { - esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], - esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], - esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], - esp32.const.VARIANT_ESP32C3: [0, 1], -} - - -def _validate_rmt_channel(value): - variant = esp32.get_esp32_variant() - if variant not in RMT_CHANNELS: - raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") - if value not in RMT_CHANNELS[variant]: - raise cv.Invalid( - f"RMT channel {value} is not supported for ESP32 variant {variant}." - ) - return value +CONF_RESET_HIGH = "reset_high" +CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( @@ -84,26 +72,36 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), - cv.Required(CONF_RMT_CHANNEL): _validate_rmt_channel, + cv.Required(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=True), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, + cv.Optional(CONF_IS_WRGB, default=False): cv.boolean, + cv.Optional(CONF_USE_PSRAM, default=True): cv.boolean, cv.Inclusive( CONF_BIT0_HIGH, "custom", - ): cv.positive_time_period_microseconds, + ): cv.positive_time_period_nanoseconds, cv.Inclusive( CONF_BIT0_LOW, "custom", - ): cv.positive_time_period_microseconds, + ): cv.positive_time_period_nanoseconds, cv.Inclusive( CONF_BIT1_HIGH, "custom", - ): cv.positive_time_period_microseconds, + ): cv.positive_time_period_nanoseconds, cv.Inclusive( CONF_BIT1_LOW, "custom", - ): cv.positive_time_period_microseconds, + ): cv.positive_time_period_nanoseconds, + cv.Optional( + CONF_RESET_HIGH, + default="0 us", + ): cv.positive_time_period_nanoseconds, + cv.Optional( + CONF_RESET_LOW, + default="0 us", + ): cv.positive_time_period_nanoseconds, } ), cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), @@ -129,6 +127,8 @@ async def to_code(config): chipset.bit0_low, chipset.bit1_high, chipset.bit1_low, + chipset.reset_high, + chipset.reset_low, ) ) else: @@ -138,11 +138,15 @@ async def to_code(config): config[CONF_BIT0_LOW], config[CONF_BIT1_HIGH], config[CONF_BIT1_LOW], + config[CONF_RESET_HIGH], + config[CONF_RESET_LOW], ) ) cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) + cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) + cg.add(var.set_use_psram(config[CONF_USE_PSRAM])) cg.add( var.set_rmt_channel( diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index 3c9bef9665..fc7bf200e4 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -12,25 +12,112 @@ from esphome.const import ( ) from esphome.core import TimePeriod from esphome.components import esp32 +from esphome.components.esp32 import get_esp32_variant, gpio +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) AUTO_LOAD = ["binary_sensor"] DEPENDENCIES = ["esp32"] +CONF_DEBOUNCE_COUNT = "debounce_count" +CONF_DENOISE_GRADE = "denoise_grade" +CONF_DENOISE_CAP_LEVEL = "denoise_cap_level" +CONF_FILTER_MODE = "filter_mode" +CONF_NOISE_THRESHOLD = "noise_threshold" +CONF_JITTER_STEP = "jitter_step" +CONF_SMOOTH_MODE = "smooth_mode" +CONF_WATERPROOF_GUARD_RING = "waterproof_guard_ring" +CONF_WATERPROOF_SHIELD_DRIVER = "waterproof_shield_driver" + esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch") ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component) +TOUCH_PADS = { + VARIANT_ESP32: { + 4: cg.global_ns.TOUCH_PAD_NUM0, + 0: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 15: cg.global_ns.TOUCH_PAD_NUM3, + 13: cg.global_ns.TOUCH_PAD_NUM4, + 12: cg.global_ns.TOUCH_PAD_NUM5, + 14: cg.global_ns.TOUCH_PAD_NUM6, + 27: cg.global_ns.TOUCH_PAD_NUM7, + 33: cg.global_ns.TOUCH_PAD_NUM8, + 32: cg.global_ns.TOUCH_PAD_NUM9, + }, + VARIANT_ESP32S2: { + 1: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 3: cg.global_ns.TOUCH_PAD_NUM3, + 4: cg.global_ns.TOUCH_PAD_NUM4, + 5: cg.global_ns.TOUCH_PAD_NUM5, + 6: cg.global_ns.TOUCH_PAD_NUM6, + 7: cg.global_ns.TOUCH_PAD_NUM7, + 8: cg.global_ns.TOUCH_PAD_NUM8, + 9: cg.global_ns.TOUCH_PAD_NUM9, + 10: cg.global_ns.TOUCH_PAD_NUM10, + 11: cg.global_ns.TOUCH_PAD_NUM11, + 12: cg.global_ns.TOUCH_PAD_NUM12, + 13: cg.global_ns.TOUCH_PAD_NUM13, + 14: cg.global_ns.TOUCH_PAD_NUM14, + }, + VARIANT_ESP32S3: { + 1: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 3: cg.global_ns.TOUCH_PAD_NUM3, + 4: cg.global_ns.TOUCH_PAD_NUM4, + 5: cg.global_ns.TOUCH_PAD_NUM5, + 6: cg.global_ns.TOUCH_PAD_NUM6, + 7: cg.global_ns.TOUCH_PAD_NUM7, + 8: cg.global_ns.TOUCH_PAD_NUM8, + 9: cg.global_ns.TOUCH_PAD_NUM9, + 10: cg.global_ns.TOUCH_PAD_NUM10, + 11: cg.global_ns.TOUCH_PAD_NUM11, + 12: cg.global_ns.TOUCH_PAD_NUM12, + 13: cg.global_ns.TOUCH_PAD_NUM13, + 14: cg.global_ns.TOUCH_PAD_NUM14, + }, +} -def validate_voltage(values): - def validator(value): - if isinstance(value, float) and value.is_integer(): - value = int(value) - value = cv.string(value) - if not value.endswith("V"): - value += "V" - return cv.one_of(*values)(value) - return validator +TOUCH_PAD_DENOISE_GRADE = { + "BIT12": cg.global_ns.TOUCH_PAD_DENOISE_BIT12, + "BIT10": cg.global_ns.TOUCH_PAD_DENOISE_BIT10, + "BIT8": cg.global_ns.TOUCH_PAD_DENOISE_BIT8, + "BIT4": cg.global_ns.TOUCH_PAD_DENOISE_BIT4, +} +TOUCH_PAD_DENOISE_CAP_LEVEL = { + "L0": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L0, + "L1": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L1, + "L2": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L2, + "L3": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L3, + "L4": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L4, + "L5": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L5, + "L6": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L6, + "L7": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L7, +} + +TOUCH_PAD_FILTER_MODE = { + "IIR_4": cg.global_ns.TOUCH_PAD_FILTER_IIR_4, + "IIR_8": cg.global_ns.TOUCH_PAD_FILTER_IIR_8, + "IIR_16": cg.global_ns.TOUCH_PAD_FILTER_IIR_16, + "IIR_32": cg.global_ns.TOUCH_PAD_FILTER_IIR_32, + "IIR_64": cg.global_ns.TOUCH_PAD_FILTER_IIR_64, + "IIR_128": cg.global_ns.TOUCH_PAD_FILTER_IIR_128, + "IIR_256": cg.global_ns.TOUCH_PAD_FILTER_IIR_256, + "JITTER": cg.global_ns.TOUCH_PAD_FILTER_JITTER, +} + +TOUCH_PAD_SMOOTH_MODE = { + "OFF": cg.global_ns.TOUCH_PAD_SMOOTH_OFF, + "IIR_2": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_2, + "IIR_4": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_4, + "IIR_8": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_8, +} LOW_VOLTAGE_REFERENCE = { "0.5V": cg.global_ns.TOUCH_LVOLT_0V5, @@ -50,15 +137,74 @@ VOLTAGE_ATTENUATION = { "0.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V5, "0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V, } +TOUCH_PAD_WATERPROOF_SHIELD_DRIVER = { + "L0": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L0, + "L1": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L1, + "L2": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L2, + "L3": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L3, + "L4": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L4, + "L5": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L5, + "L6": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L6, + "L7": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L7, +} + + +def validate_touch_pad(value): + value = gpio.gpio_pin_number_validator(value) + variant = get_esp32_variant() + if variant not in TOUCH_PADS: + raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.") + + pads = TOUCH_PADS[variant] + if value not in pads: + raise cv.Invalid(f"Pin {value} does not support touch pads.") + return cv.enum(pads)(value) + + +def validate_variant_vars(config): + if get_esp32_variant() == VARIANT_ESP32: + variant_vars = { + CONF_DEBOUNCE_COUNT, + CONF_DENOISE_GRADE, + CONF_DENOISE_CAP_LEVEL, + CONF_FILTER_MODE, + CONF_NOISE_THRESHOLD, + CONF_JITTER_STEP, + CONF_SMOOTH_MODE, + CONF_WATERPROOF_GUARD_RING, + CONF_WATERPROOF_SHIELD_DRIVER, + } + for vvar in variant_vars: + if vvar in config: + raise cv.Invalid(f"{vvar} is not valid on {VARIANT_ESP32}") + elif ( + get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3 + ) and CONF_IIR_FILTER in config: + raise cv.Invalid( + f"{CONF_IIR_FILTER} is not valid on {VARIANT_ESP32S2} or {VARIANT_ESP32S3}" + ) + + return config + + +def validate_voltage(values): + def validator(value): + if isinstance(value, float) and value.is_integer(): + value = int(value) + value = cv.string(value) + if not value.endswith("V"): + value += "V" + return cv.one_of(*values)(value) + + return validator + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32TouchComponent), cv.Optional(CONF_SETUP_MODE, default=False): cv.boolean, - cv.Optional( - CONF_IIR_FILTER, default="0ms" - ): cv.positive_time_period_milliseconds, + # common options cv.Optional(CONF_SLEEP_DURATION, default="27306us"): cv.All( cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=436906)) ), @@ -74,13 +220,47 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( VOLTAGE_ATTENUATION ), + # ESP32 only + cv.Optional(CONF_IIR_FILTER): cv.positive_time_period_milliseconds, + # ESP32-S2/S3 only + cv.Optional(CONF_DEBOUNCE_COUNT): cv.int_range(min=0, max=7), + cv.Optional(CONF_FILTER_MODE): cv.enum( + TOUCH_PAD_FILTER_MODE, upper=True, space="_" + ), + cv.Optional(CONF_NOISE_THRESHOLD): cv.int_range(min=0, max=3), + cv.Optional(CONF_JITTER_STEP): cv.int_range(min=0, max=15), + cv.Optional(CONF_SMOOTH_MODE): cv.enum( + TOUCH_PAD_SMOOTH_MODE, upper=True, space="_" + ), + cv.Optional(CONF_DENOISE_GRADE): cv.enum( + TOUCH_PAD_DENOISE_GRADE, upper=True, space="_" + ), + cv.Optional(CONF_DENOISE_CAP_LEVEL): cv.enum( + TOUCH_PAD_DENOISE_CAP_LEVEL, upper=True, space="_" + ), + cv.Optional(CONF_WATERPROOF_GUARD_RING): validate_touch_pad, + cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.enum( + TOUCH_PAD_WATERPROOF_SHIELD_DRIVER, upper=True, space="_" + ), } ).extend(cv.COMPONENT_SCHEMA), + cv.has_none_or_all_keys(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL), + cv.has_none_or_all_keys( + CONF_DEBOUNCE_COUNT, + CONF_FILTER_MODE, + CONF_NOISE_THRESHOLD, + CONF_JITTER_STEP, + CONF_SMOOTH_MODE, + ), + 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, ] ), + validate_variant_vars, ) @@ -89,7 +269,6 @@ async def to_code(config): await cg.register_component(touch, config) cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE])) - cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) sleep_duration = int(round(config[CONF_SLEEP_DURATION].total_microseconds * 0.15)) cg.add(touch.set_sleep_duration(sleep_duration)) @@ -114,3 +293,33 @@ async def to_code(config): VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]] ) ) + + if get_esp32_variant() == VARIANT_ESP32: + if CONF_IIR_FILTER in config: + cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) + + if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3: + if CONF_FILTER_MODE in config: + cg.add(touch.set_filter_mode(config[CONF_FILTER_MODE])) + if CONF_DEBOUNCE_COUNT in config: + cg.add(touch.set_debounce_count(config[CONF_DEBOUNCE_COUNT])) + if CONF_NOISE_THRESHOLD in config: + cg.add(touch.set_noise_threshold(config[CONF_NOISE_THRESHOLD])) + if CONF_JITTER_STEP in config: + cg.add(touch.set_jitter_step(config[CONF_JITTER_STEP])) + if CONF_SMOOTH_MODE in config: + cg.add(touch.set_smooth_level(config[CONF_SMOOTH_MODE])) + if CONF_DENOISE_GRADE in config: + cg.add(touch.set_denoise_grade(config[CONF_DENOISE_GRADE])) + if CONF_DENOISE_CAP_LEVEL in config: + cg.add(touch.set_denoise_cap(config[CONF_DENOISE_CAP_LEVEL])) + if CONF_WATERPROOF_GUARD_RING in config: + cg.add( + touch.set_waterproof_guard_ring_pad(config[CONF_WATERPROOF_GUARD_RING]) + ) + if CONF_WATERPROOF_SHIELD_DRIVER in config: + cg.add( + touch.set_waterproof_shield_driver( + config[CONF_WATERPROOF_SHIELD_DRIVER] + ) + ) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index 2cdf1343c3..e9322b3080 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -1,87 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.core import CORE from esphome.components import binary_sensor from esphome.const import ( CONF_PIN, CONF_THRESHOLD, CONF_ID, ) -from esphome.components.esp32 import gpio -from esphome.components.esp32.const import ( - KEY_ESP32, - KEY_VARIANT, - VARIANT_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32S3, -) -from . import esp32_touch_ns, ESP32TouchComponent +from . import esp32_touch_ns, ESP32TouchComponent, validate_touch_pad DEPENDENCIES = ["esp32_touch", "esp32"] CONF_ESP32_TOUCH_ID = "esp32_touch_id" CONF_WAKEUP_THRESHOLD = "wakeup_threshold" -TOUCH_PADS = { - VARIANT_ESP32: { - 4: cg.global_ns.TOUCH_PAD_NUM0, - 0: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 15: cg.global_ns.TOUCH_PAD_NUM3, - 13: cg.global_ns.TOUCH_PAD_NUM4, - 12: cg.global_ns.TOUCH_PAD_NUM5, - 14: cg.global_ns.TOUCH_PAD_NUM6, - 27: cg.global_ns.TOUCH_PAD_NUM7, - 33: cg.global_ns.TOUCH_PAD_NUM8, - 32: cg.global_ns.TOUCH_PAD_NUM9, - }, - VARIANT_ESP32S2: { - 1: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 3: cg.global_ns.TOUCH_PAD_NUM3, - 4: cg.global_ns.TOUCH_PAD_NUM4, - 5: cg.global_ns.TOUCH_PAD_NUM5, - 6: cg.global_ns.TOUCH_PAD_NUM6, - 7: cg.global_ns.TOUCH_PAD_NUM7, - 8: cg.global_ns.TOUCH_PAD_NUM8, - 9: cg.global_ns.TOUCH_PAD_NUM9, - 10: cg.global_ns.TOUCH_PAD_NUM10, - 11: cg.global_ns.TOUCH_PAD_NUM11, - 12: cg.global_ns.TOUCH_PAD_NUM12, - 13: cg.global_ns.TOUCH_PAD_NUM13, - 14: cg.global_ns.TOUCH_PAD_NUM14, - }, - VARIANT_ESP32S3: { - 1: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 3: cg.global_ns.TOUCH_PAD_NUM3, - 4: cg.global_ns.TOUCH_PAD_NUM4, - 5: cg.global_ns.TOUCH_PAD_NUM5, - 6: cg.global_ns.TOUCH_PAD_NUM6, - 7: cg.global_ns.TOUCH_PAD_NUM7, - 8: cg.global_ns.TOUCH_PAD_NUM8, - 9: cg.global_ns.TOUCH_PAD_NUM9, - 10: cg.global_ns.TOUCH_PAD_NUM10, - 11: cg.global_ns.TOUCH_PAD_NUM11, - 12: cg.global_ns.TOUCH_PAD_NUM12, - 13: cg.global_ns.TOUCH_PAD_NUM13, - 14: cg.global_ns.TOUCH_PAD_NUM14, - }, -} - - -def validate_touch_pad(value): - value = gpio.validate_gpio_pin(value) - variant = CORE.data[KEY_ESP32][KEY_VARIANT] - if variant not in TOUCH_PADS: - raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.") - - pads = TOUCH_PADS[variant] - if value not in pads: - raise cv.Invalid(f"Pin {value} does not support touch pads.") - return cv.enum(pads)(value) - - ESP32TouchBinarySensor = esp32_touch_ns.class_( "ESP32TouchBinarySensor", binary_sensor.BinarySensor ) @@ -90,8 +21,8 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(ESP32TouchBinarySensor).exten { cv.GenerateID(CONF_ESP32_TOUCH_ID): cv.use_id(ESP32TouchComponent), cv.Required(CONF_PIN): validate_touch_pad, - cv.Required(CONF_THRESHOLD): cv.uint16_t, - cv.Optional(CONF_WAKEUP_THRESHOLD, default=0): cv.uint16_t, + cv.Required(CONF_THRESHOLD): cv.uint32_t, + cv.Optional(CONF_WAKEUP_THRESHOLD, default=0): cv.uint32_t, } ) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index 0e3d3d9fd5..e43c3b844c 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -5,6 +5,8 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace esp32_touch { @@ -13,18 +15,58 @@ static const char *const TAG = "esp32_touch"; void ESP32TouchComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP32 Touch Hub..."); touch_pad_init(); +// set up and enable/start filtering based on ESP32 variant +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + if (this->filter_configured_()) { + touch_filter_config_t filter_info = { + .mode = this->filter_mode_, + .debounce_cnt = this->debounce_count_, + .noise_thr = this->noise_threshold_, + .jitter_step = this->jitter_step_, + .smh_lvl = this->smooth_level_, + }; + touch_pad_filter_set_config(&filter_info); + touch_pad_filter_enable(); + } + if (this->denoise_configured_()) { + touch_pad_denoise_t denoise = { + .grade = this->grade_, + .cap_level = this->cap_level_, + }; + touch_pad_denoise_set_config(&denoise); + touch_pad_denoise_enable(); + } + + if (this->waterproof_configured_()) { + touch_pad_waterproof_t waterproof = { + .guard_ring_pad = this->waterproof_guard_ring_pad_, + .shield_driver = this->waterproof_shield_driver_, + }; + touch_pad_waterproof_set_config(&waterproof); + touch_pad_waterproof_enable(); + } +#else if (this->iir_filter_enabled_()) { touch_pad_filter_start(this->iir_filter_); } +#endif touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); for (auto *child : this->children_) { +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + touch_pad_config(child->get_touch_pad()); +#else // Disable interrupt threshold touch_pad_config(child->get_touch_pad(), 0); +#endif } +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); + touch_pad_fsm_start(); +#endif } void ESP32TouchComponent::dump_config() { @@ -92,38 +134,168 @@ void ESP32TouchComponent::dump_config() { } ESP_LOGCONFIG(TAG, " Voltage Attenuation: %s", atten_s); +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + if (this->filter_configured_()) { + const char *filter_mode_s; + switch (this->filter_mode_) { + case TOUCH_PAD_FILTER_IIR_4: + filter_mode_s = "IIR_4"; + break; + case TOUCH_PAD_FILTER_IIR_8: + filter_mode_s = "IIR_8"; + break; + case TOUCH_PAD_FILTER_IIR_16: + filter_mode_s = "IIR_16"; + break; + case TOUCH_PAD_FILTER_IIR_32: + filter_mode_s = "IIR_32"; + break; + case TOUCH_PAD_FILTER_IIR_64: + filter_mode_s = "IIR_64"; + break; + case TOUCH_PAD_FILTER_IIR_128: + filter_mode_s = "IIR_128"; + break; + case TOUCH_PAD_FILTER_IIR_256: + filter_mode_s = "IIR_256"; + break; + case TOUCH_PAD_FILTER_JITTER: + filter_mode_s = "JITTER"; + break; + default: + filter_mode_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Filter mode: %s", filter_mode_s); + ESP_LOGCONFIG(TAG, " Debounce count: %" PRIu32, this->debounce_count_); + ESP_LOGCONFIG(TAG, " Noise threshold coefficient: %" PRIu32, this->noise_threshold_); + ESP_LOGCONFIG(TAG, " Jitter filter step size: %" PRIu32, this->jitter_step_); + const char *smooth_level_s; + switch (this->smooth_level_) { + case TOUCH_PAD_SMOOTH_OFF: + smooth_level_s = "OFF"; + break; + case TOUCH_PAD_SMOOTH_IIR_2: + smooth_level_s = "IIR_2"; + break; + case TOUCH_PAD_SMOOTH_IIR_4: + smooth_level_s = "IIR_4"; + break; + case TOUCH_PAD_SMOOTH_IIR_8: + smooth_level_s = "IIR_8"; + break; + default: + smooth_level_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); + } + + if (this->denoise_configured_()) { + const char *grade_s; + switch (this->grade_) { + case TOUCH_PAD_DENOISE_BIT12: + grade_s = "BIT12"; + break; + case TOUCH_PAD_DENOISE_BIT10: + grade_s = "BIT10"; + break; + case TOUCH_PAD_DENOISE_BIT8: + grade_s = "BIT8"; + break; + case TOUCH_PAD_DENOISE_BIT4: + grade_s = "BIT4"; + break; + default: + grade_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); + + const char *cap_level_s; + switch (this->cap_level_) { + case TOUCH_PAD_DENOISE_CAP_L0: + cap_level_s = "L0"; + break; + case TOUCH_PAD_DENOISE_CAP_L1: + cap_level_s = "L1"; + break; + case TOUCH_PAD_DENOISE_CAP_L2: + cap_level_s = "L2"; + break; + case TOUCH_PAD_DENOISE_CAP_L3: + cap_level_s = "L3"; + break; + case TOUCH_PAD_DENOISE_CAP_L4: + cap_level_s = "L4"; + break; + case TOUCH_PAD_DENOISE_CAP_L5: + cap_level_s = "L5"; + break; + case TOUCH_PAD_DENOISE_CAP_L6: + cap_level_s = "L6"; + break; + case TOUCH_PAD_DENOISE_CAP_L7: + cap_level_s = "L7"; + break; + default: + cap_level_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); + } +#else if (this->iir_filter_enabled_()) { - ESP_LOGCONFIG(TAG, " IIR Filter: %ums", this->iir_filter_); + ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); } else { ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); } +#endif + if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED!"); + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); } for (auto *child : this->children_) { LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, " Pad: T%d", child->get_touch_pad()); - ESP_LOGCONFIG(TAG, " Threshold: %u", child->get_threshold()); + ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad()); + ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold()); } } +uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) { +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + uint32_t value = 0; + if (this->filter_configured_()) { + touch_pad_filter_read_smooth(tp, &value); + } else { + touch_pad_read_raw_data(tp, &value); + } +#else + uint16_t value = 0; + if (this->iir_filter_enabled_()) { + touch_pad_read_filtered(tp, &value); + } else { + touch_pad_read(tp, &value); + } +#endif + return value; +} + void ESP32TouchComponent::loop() { const uint32_t now = millis(); bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; for (auto *child : this->children_) { - uint16_t value; - if (this->iir_filter_enabled_()) { - touch_pad_read_filtered(child->get_touch_pad(), &value); - } else { - touch_pad_read(child->get_touch_pad(), &value); - } - - child->value_ = value; - child->publish_state(value < child->get_threshold()); + child->value_ = this->component_touch_pad_read(child->get_touch_pad()); +#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) + child->publish_state(child->value_ < child->get_threshold()); +#else + child->publish_state(child->value_ > child->get_threshold()); +#endif if (should_print) { - ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); + ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), + (uint32_t) child->get_touch_pad(), child->value_); } App.feed_wdt(); @@ -138,10 +310,12 @@ void ESP32TouchComponent::loop() { void ESP32TouchComponent::on_shutdown() { bool is_wakeup_source = false; +#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) if (this->iir_filter_enabled_()) { touch_pad_filter_stop(); touch_pad_filter_delete(); } +#endif for (auto *child : this->children_) { if (child->get_wakeup_threshold() != 0) { @@ -151,8 +325,10 @@ void ESP32TouchComponent::on_shutdown() { touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); } +#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) // No filter available when using as wake-up source. touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); +#endif } } @@ -161,7 +337,7 @@ void ESP32TouchComponent::on_shutdown() { } } -ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint16_t threshold, uint16_t wakeup_threshold) +ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} } // namespace esp32_touch diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index c954a14654..0eac590ce7 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -8,11 +8,7 @@ #include -#if ESP_IDF_VERSION_MAJOR >= 4 #include -#else -#include -#endif namespace esphome { namespace esp32_touch { @@ -21,25 +17,37 @@ class ESP32TouchBinarySensor; class ESP32TouchComponent : public Component { public: - void register_touch_pad(ESP32TouchBinarySensor *pad) { children_.push_back(pad); } - - void set_setup_mode(bool setup_mode) { setup_mode_ = setup_mode; } - - void set_iir_filter(uint32_t iir_filter) { iir_filter_ = iir_filter; } - - void set_sleep_duration(uint16_t sleep_duration) { sleep_cycle_ = sleep_duration; } - - void set_measurement_duration(uint16_t meas_cycle) { meas_cycle_ = meas_cycle; } + void register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); } + void set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; } + void set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; } + void set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; } void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { - low_voltage_reference_ = low_voltage_reference; + this->low_voltage_reference_ = low_voltage_reference; } - void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { - high_voltage_reference_ = high_voltage_reference; + this->high_voltage_reference_ = high_voltage_reference; } + void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { + this->voltage_attenuation_ = voltage_attenuation; + } +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; } + void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; } + void set_noise_threshold(uint32_t noise_threshold) { this->noise_threshold_ = noise_threshold; } + void set_jitter_step(uint32_t jitter_step) { this->jitter_step_ = jitter_step; } + void set_smooth_level(touch_smooth_mode_t smooth_level) { this->smooth_level_ = smooth_level; } + void set_denoise_grade(touch_pad_denoise_grade_t denoise_grade) { this->grade_ = denoise_grade; } + void set_denoise_cap(touch_pad_denoise_cap_t cap_level) { this->cap_level_ = cap_level; } + void set_waterproof_guard_ring_pad(touch_pad_t pad) { this->waterproof_guard_ring_pad_ = pad; } + void set_waterproof_shield_driver(touch_pad_shield_driver_t drive_capability) { + this->waterproof_shield_driver_ = drive_capability; + } +#else + void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } +#endif - void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { voltage_attenuation_ = voltage_attenuation; } + uint32_t component_touch_pad_read(touch_pad_t tp); void setup() override; void dump_config() override; @@ -49,38 +57,63 @@ class ESP32TouchComponent : public Component { void on_shutdown() override; protected: - /// Is the IIR filter enabled? - bool iir_filter_enabled_() const { return iir_filter_ > 0; } +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + bool filter_configured_() const { + return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX); + } + bool denoise_configured_() const { + return (this->grade_ != TOUCH_PAD_DENOISE_MAX) && (this->cap_level_ != TOUCH_PAD_DENOISE_CAP_MAX); + } + bool waterproof_configured_() const { + return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) && + (this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX); + } +#else + bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } +#endif - uint16_t sleep_cycle_{}; - uint16_t meas_cycle_{65535}; - touch_low_volt_t low_voltage_reference_{}; - touch_high_volt_t high_voltage_reference_{}; - touch_volt_atten_t voltage_attenuation_{}; std::vector children_; bool setup_mode_{false}; - uint32_t setup_mode_last_log_print_{}; + uint32_t setup_mode_last_log_print_{0}; + // common parameters + uint16_t sleep_cycle_{4095}; + uint16_t meas_cycle_{65535}; + touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; + touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; + touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; + uint32_t debounce_count_{0}; + uint32_t noise_threshold_{0}; + uint32_t jitter_step_{0}; + touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; + touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; + touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; + touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; + touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; +#else uint32_t iir_filter_{0}; +#endif }; /// Simple helper class to expose a touch pad value as a binary sensor. class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: - ESP32TouchBinarySensor(touch_pad_t touch_pad, uint16_t threshold, uint16_t wakeup_threshold); + ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold); - touch_pad_t get_touch_pad() const { return touch_pad_; } - uint16_t get_threshold() const { return threshold_; } - void set_threshold(uint16_t threshold) { threshold_ = threshold; } - uint16_t get_value() const { return value_; } - uint16_t get_wakeup_threshold() const { return wakeup_threshold_; } + touch_pad_t get_touch_pad() const { return this->touch_pad_; } + uint32_t get_threshold() const { return this->threshold_; } + void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } + uint32_t get_value() const { return this->value_; } + uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } protected: friend ESP32TouchComponent; - touch_pad_t touch_pad_; - uint16_t threshold_; - uint16_t value_; - const uint16_t wakeup_threshold_; + touch_pad_t touch_pad_{TOUCH_PAD_MAX}; + uint32_t threshold_{0}; + uint32_t value_{0}; + const uint32_t wakeup_threshold_{0}; }; } // namespace esp32_touch diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 674f433d52..64b127bda3 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -11,6 +11,8 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + PLATFORM_ESP8266, + CONF_PLATFORM_VERSION, ) from esphome.core import CORE, coroutine_with_priority import esphome.config_validation as cv @@ -38,7 +40,7 @@ AUTO_LOAD = ["preferences"] def set_core_data(config): CORE.data[KEY_ESP8266] = {} - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "esp8266" + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266 CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] @@ -50,6 +52,17 @@ def set_core_data(config): return config +def get_download_types(storage_json): + return [ + { + "title": "Standard format", + "description": "For flashing ESP8266.", + "file": "firmware.bin", + "download": f"{storage_json.name}.bin", + }, + ] + + def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/esp8266/Arduino/releases) version to # a PIO platformio/framework-arduinoespressif8266 value @@ -71,20 +84,22 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # The default/recommended arduino framework version # - https://github.com/esp8266/Arduino/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266 -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 0, 2) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2) # The platformio/espressif8266 version to use for arduino 2 framework versions # - https://github.com/platformio/platform-espressif8266/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266 ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3) # for arduino 3 framework versions ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) +# for arduino 4 framework versions +ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(3, 0, 2), "https://github.com/esp8266/Arduino.git"), - "latest": (cv.Version(3, 0, 2), None), + "dev": (cv.Version(3, 1, 2), "https://github.com/esp8266/Arduino.git"), + "latest": (cv.Version(3, 1, 2), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -104,7 +119,9 @@ def _arduino_check_versions(value): platform_version = value.get(CONF_PLATFORM_VERSION) if platform_version is None: - if version >= cv.Version(3, 0, 0): + if version >= cv.Version(3, 1, 0): + platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION)) + elif version >= cv.Version(3, 0, 0): platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION)) elif version >= cv.Version(2, 5, 0): platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION)) @@ -130,7 +147,6 @@ def _parse_platform_version(value): return value -CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index d4b2078524..53016d2130 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -1,7 +1,11 @@ -import logging from dataclasses import dataclass +import logging +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_ANALOG, CONF_ID, CONF_INPUT, CONF_INVERTED, @@ -11,19 +15,15 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, + PLATFORM_ESP8266, ) -from esphome import pins from esphome.core import CORE, coroutine_with_priority -import esphome.config_validation as cv -import esphome.codegen as cg from . import boards from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns - _LOGGER = logging.getLogger(__name__) - ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin) @@ -48,8 +48,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: @@ -123,6 +125,8 @@ def validate_supports(value): (True, False, False, False, False), # OUTPUT (False, True, False, False, False), + # INPUT and OUTPUT, e.g. for i2c + (True, True, False, False, False), # INPUT_PULLUP (True, False, False, True, False), # INPUT_PULLDOWN_16 @@ -140,23 +144,12 @@ def validate_supports(value): return value -CONF_ANALOG = "analog" ESP8266_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(ESP8266GPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_ANALOG, default=False): cv.boolean, - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, + pins.gpio_base_schema( + ESP8266GPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), + ), validate_supports, ) @@ -167,7 +160,7 @@ class PinInitialState: level: int = 255 -@pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA) async def esp8266_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] diff --git a/esphome/components/esp8266/post_build.py.script b/esphome/components/esp8266/post_build.py.script index 4dab1cbd27..0a854d7599 100644 --- a/esphome/components/esp8266/post_build.py.script +++ b/esphome/components/esp8266/post_build.py.script @@ -6,10 +6,18 @@ Import("env") # noqa def esp8266_copy_factory_bin(source, target, env): firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") - new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") + + shutil.copyfile(firmware_name, new_file_name) + + +def esp8266_copy_ota_bin(source, target, env): + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin") shutil.copyfile(firmware_name, new_file_name) # pylint: disable=E0602 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_factory_bin) # noqa +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_ota_bin) # noqa diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py new file mode 100644 index 0000000000..a852d8d001 --- /dev/null +++ b/esphome/components/esphome/ota/__init__.py @@ -0,0 +1,134 @@ +import logging + +import esphome.codegen as cg +import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent +from esphome.config_helpers import merge_config +from esphome.const import ( + CONF_ESPHOME, + CONF_ID, + CONF_NUM_ATTEMPTS, + CONF_OTA, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_REBOOT_TIMEOUT, + CONF_SAFE_MODE, + CONF_VERSION, +) +from esphome.core import coroutine_with_priority + +_LOGGER = logging.getLogger(__name__) + + +CODEOWNERS = ["@esphome/core"] +AUTO_LOAD = ["md5", "socket"] +DEPENDENCIES = ["network"] + +esphome = cg.esphome_ns.namespace("esphome") +ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", OTAComponent) + + +def ota_esphome_final_validate(config): + full_conf = fv.full_config.get() + full_ota_conf = full_conf[CONF_OTA] + new_ota_conf = [] + merged_ota_esphome_configs_by_port = {} + ports_with_merged_configs = [] + for ota_conf in full_ota_conf: + if ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME: + if ( + conf_port := ota_conf.get(CONF_PORT) + ) not in merged_ota_esphome_configs_by_port: + merged_ota_esphome_configs_by_port[conf_port] = ota_conf + else: + if merged_ota_esphome_configs_by_port[conf_port][ + CONF_VERSION + ] != ota_conf.get(CONF_VERSION): + raise cv.Invalid( + f"Found multiple configurations but {CONF_VERSION} is inconsistent" + ) + if ( + merged_ota_esphome_configs_by_port[conf_port][CONF_ID].is_manual + and ota_conf.get(CONF_ID).is_manual + ): + raise cv.Invalid( + f"Found multiple configurations but {CONF_ID} is inconsistent" + ) + if ( + CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port] + and CONF_PASSWORD in ota_conf + and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD] + != ota_conf.get(CONF_PASSWORD) + ): + raise cv.Invalid( + f"Found multiple configurations but {CONF_PASSWORD} is inconsistent" + ) + + ports_with_merged_configs.append(conf_port) + merged_ota_esphome_configs_by_port[conf_port] = merge_config( + merged_ota_esphome_configs_by_port[conf_port], ota_conf + ) + else: + new_ota_conf.append(ota_conf) + + for port_conf in merged_ota_esphome_configs_by_port.values(): + new_ota_conf.append(port_conf) + + full_conf[CONF_OTA] = new_ota_conf + fv.full_config.set(full_conf) + + if len(ports_with_merged_configs) > 0: + _LOGGER.warning( + "Found and merged multiple configurations for %s %s %s port(s) %s", + CONF_OTA, + CONF_PLATFORM, + CONF_ESPHOME, + ports_with_merged_configs, + ) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent), + cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True), + cv.SplitDefault( + CONF_PORT, + esp8266=8266, + esp32=3232, + rp2040=2040, + bk72xx=8892, + rtl87xx=8892, + ): cv.port, + cv.Optional(CONF_PASSWORD): cv.string, + cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid( + f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" + ), + cv.Optional(CONF_REBOOT_TIMEOUT): cv.invalid( + f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" + ), + cv.Optional(CONF_SAFE_MODE): cv.invalid( + f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" + ), + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate + + +@coroutine_with_priority(52.0) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ota_to_code(var, config) + cg.add(var.set_port(config[CONF_PORT])) + if CONF_PASSWORD in config: + cg.add(var.set_auth_password(config[CONF_PASSWORD])) + cg.add_define("USE_OTA_PASSWORD") + 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 new file mode 100644 index 0000000000..7e2ef42a97 --- /dev/null +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -0,0 +1,413 @@ +#include "ota_esphome.h" +#ifdef USE_OTA +#include "esphome/components/md5/md5.h" +#include "esphome/components/network/util.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_arduino_libretiny.h" +#include "esphome/components/ota/ota_backend_arduino_rp2040.h" +#include "esphome/components/ota/ota_backend_esp_idf.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include + +namespace esphome { + +static const char *const TAG = "esphome.ota"; +static constexpr u_int16_t OTA_BLOCK_SIZE = 8192; + +void ESPHomeOTAComponent::setup() { +#ifdef USE_OTA_STATE_CALLBACK + ota::register_ota_platform(this); +#endif + + server_ = socket::socket_ip(SOCK_STREAM, 0); + if (server_ == nullptr) { + ESP_LOGW(TAG, "Could not create socket"); + this->mark_failed(); + return; + } + int enable = 1; + int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + // we can still continue + } + err = server_->setblocking(false); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->mark_failed(); + return; + } + + struct sockaddr_storage server; + + socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); + if (sl == 0) { + ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); + this->mark_failed(); + return; + } + + err = server_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); + return; + } + + err = server_->listen(4); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); + this->mark_failed(); + return; + } +} + +void ESPHomeOTAComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Over-The-Air updates:"); + ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->port_); + ESP_LOGCONFIG(TAG, " Version: %d", USE_OTA_VERSION); +#ifdef USE_OTA_PASSWORD + if (!this->password_.empty()) { + ESP_LOGCONFIG(TAG, " Password configured"); + } +#endif +} + +void ESPHomeOTAComponent::loop() { this->handle_(); } + +static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; + +void ESPHomeOTAComponent::handle_() { + ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; + bool update_started = false; + size_t total = 0; + uint32_t last_progress = 0; + uint8_t buf[1024]; + char *sbuf = reinterpret_cast(buf); + size_t ota_size; + uint8_t ota_features; + std::unique_ptr backend; + (void) ota_features; +#if USE_OTA_VERSION == 2 + size_t size_acknowledged = 0; +#endif + + if (client_ == nullptr) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len); + } + if (client_ == nullptr) + return; + + int enable = 1; + int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); + return; + } + + ESP_LOGD(TAG, "Starting update from %s...", this->client_->getpeername().c_str()); + this->status_set_warning(); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#endif + + if (!this->readall_(buf, 5)) { + ESP_LOGW(TAG, "Reading magic bytes failed"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + // 0x6C, 0x26, 0xF7, 0x5C, 0x45 + if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) { + ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3], + buf[4]); + error_code = ota::OTA_RESPONSE_ERROR_MAGIC; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + + // Send OK and version - 2 bytes + buf[0] = ota::OTA_RESPONSE_OK; + buf[1] = USE_OTA_VERSION; + this->writeall_(buf, 2); + + backend = ota::make_ota_backend(); + + // Read features - 1 byte + if (!this->readall_(buf, 1)) { + ESP_LOGW(TAG, "Reading features failed"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + ota_features = buf[0]; // NOLINT + ESP_LOGV(TAG, "Features: 0x%02X", ota_features); + + // Acknowledge header - 1 byte + buf[0] = ota::OTA_RESPONSE_HEADER_OK; + if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) { + buf[0] = ota::OTA_RESPONSE_SUPPORTS_COMPRESSION; + } + + this->writeall_(buf, 1); + +#ifdef USE_OTA_PASSWORD + if (!this->password_.empty()) { + buf[0] = ota::OTA_RESPONSE_REQUEST_AUTH; + this->writeall_(buf, 1); + md5::MD5Digest md5{}; + md5.init(); + sprintf(sbuf, "%08" PRIx32, random_uint32()); + md5.add(sbuf, 8); + md5.calculate(); + md5.get_hex(sbuf); + ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf); + + // Send nonce, 32 bytes hex MD5 + if (!this->writeall_(reinterpret_cast(sbuf), 32)) { + ESP_LOGW(TAG, "Auth: Writing nonce failed"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + + // prepare challenge + md5.init(); + md5.add(this->password_.c_str(), this->password_.length()); + // add nonce + md5.add(sbuf, 32); + + // Receive cnonce, 32 bytes hex MD5 + if (!this->readall_(buf, 32)) { + ESP_LOGW(TAG, "Auth: Reading cnonce failed"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + sbuf[32] = '\0'; + ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf); + // add cnonce + md5.add(sbuf, 32); + + // calculate result + md5.calculate(); + md5.get_hex(sbuf); + ESP_LOGV(TAG, "Auth: Result is %s", sbuf); + + // Receive result, 32 bytes hex MD5 + if (!this->readall_(buf + 64, 32)) { + ESP_LOGW(TAG, "Auth: Reading response failed"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + sbuf[64 + 32] = '\0'; + ESP_LOGV(TAG, "Auth: Response is %s", sbuf + 64); + + bool matches = true; + for (uint8_t i = 0; i < 32; i++) + matches = matches && buf[i] == buf[64 + i]; + + if (!matches) { + ESP_LOGW(TAG, "Auth failed! Passwords do not match"); + error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + } +#endif // USE_OTA_PASSWORD + + // Acknowledge auth OK - 1 byte + buf[0] = ota::OTA_RESPONSE_AUTH_OK; + this->writeall_(buf, 1); + + // Read size, 4 bytes MSB first + if (!this->readall_(buf, 4)) { + ESP_LOGW(TAG, "Reading size failed"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + ota_size = 0; + for (uint8_t i = 0; i < 4; i++) { + ota_size <<= 8; + ota_size |= buf[i]; + } + ESP_LOGV(TAG, "Size is %u bytes", ota_size); + + error_code = backend->begin(ota_size); + if (error_code != ota::OTA_RESPONSE_OK) + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + update_started = true; + + // Acknowledge prepare OK - 1 byte + buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK; + this->writeall_(buf, 1); + + // Read binary MD5, 32 bytes + if (!this->readall_(buf, 32)) { + ESP_LOGW(TAG, "Reading binary MD5 checksum failed"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + sbuf[32] = '\0'; + ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf); + backend->set_update_md5(sbuf); + + // Acknowledge MD5 OK - 1 byte + buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; + this->writeall_(buf, 1); + + while (total < ota_size) { + // TODO: timeout check + size_t requested = std::min(sizeof(buf), ota_size - total); + ssize_t read = this->client_->read(buf, requested); + if (read == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); + delay(1); + continue; + } + ESP_LOGW(TAG, "Error receiving data for update, errno %d", errno); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } else if (read == 0) { + // $ man recv + // "When a stream socket peer has performed an orderly shutdown, the return value will + // be 0 (the traditional "end-of-file" return)." + ESP_LOGW(TAG, "Remote end closed connection"); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + + error_code = backend->write(buf, read); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Error writing binary data to flash!, error_code: %d", error_code); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + total += read; +#if USE_OTA_VERSION == 2 + while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { + buf[0] = ota::OTA_RESPONSE_CHUNK_OK; + this->writeall_(buf, 1); + size_acknowledged += OTA_BLOCK_SIZE; + } +#endif + + uint32_t now = millis(); + if (now - last_progress > 1000) { + 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); +#endif + // feed watchdog and give other tasks a chance to run + App.feed_wdt(); + yield(); + } + } + + // Acknowledge receive OK - 1 byte + buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; + this->writeall_(buf, 1); + + error_code = backend->end(); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + + // Acknowledge Update end OK - 1 byte + buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK; + this->writeall_(buf, 1); + + // Read ACK + if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Reading back acknowledgement failed"); + // do not go to error, this is not fatal + } + + this->client_->close(); + this->client_ = nullptr; + 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); +#endif + delay(100); // NOLINT + App.safe_reboot(); + +error: + buf[0] = static_cast(error_code); + this->writeall_(buf, 1); + this->client_->close(); + this->client_ = nullptr; + + if (backend != nullptr && update_started) { + 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)); +#endif +} + +bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { + uint32_t start = millis(); + uint32_t at = 0; + while (len - at > 0) { + uint32_t now = millis(); + if (now - start > 1000) { + ESP_LOGW(TAG, "Timed out reading %d bytes of data", len); + return false; + } + + ssize_t read = this->client_->read(buf + at, len - at); + if (read == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); + delay(1); + continue; + } + ESP_LOGW(TAG, "Failed to read %d bytes of data, errno %d", len, errno); + return false; + } else if (read == 0) { + ESP_LOGW(TAG, "Remote closed connection"); + return false; + } else { + at += read; + } + App.feed_wdt(); + delay(1); + } + + return true; +} +bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { + uint32_t start = millis(); + uint32_t at = 0; + while (len - at > 0) { + uint32_t now = millis(); + if (now - start > 1000) { + ESP_LOGW(TAG, "Timed out writing %d bytes of data", len); + return false; + } + + ssize_t written = this->client_->write(buf + at, len - at); + if (written == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); + delay(1); + continue; + } + ESP_LOGW(TAG, "Failed to write %d bytes of data, errno %d", len, errno); + return false; + } else { + at += written; + } + App.feed_wdt(); + delay(1); + } + return true; +} + +float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } +uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } +void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } +} // namespace esphome +#endif diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h new file mode 100644 index 0000000000..e0d09ff37e --- /dev/null +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_OTA +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" +#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/socket/socket.h" + +namespace esphome { + +/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. +class ESPHomeOTAComponent : public ota::OTAComponent { + public: +#ifdef USE_OTA_PASSWORD + void set_auth_password(const std::string &password) { password_ = password; } +#endif // USE_OTA_PASSWORD + + /// Manually set the port OTA should listen on + void set_port(uint16_t port); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + uint16_t get_port() const; + + protected: + void handle_(); + bool readall_(uint8_t *buf, size_t len); + bool writeall_(const uint8_t *buf, size_t len); + +#ifdef USE_OTA_PASSWORD + std::string password_; +#endif // USE_OTA_PASSWORD + + uint16_t port_; + + std::unique_ptr server_; + std::unique_ptr client_; +}; + +} // namespace esphome +#endif diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index bedc0a4c30..dca37b8dc2 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,31 +1,58 @@ +import logging from esphome import pins -import esphome.config_validation as cv import esphome.codegen as cg +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +from esphome.components.network import IPAddress +from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface +import esphome.config_validation as cv from esphome.const import ( - CONF_DOMAIN, - CONF_ID, - CONF_MANUAL_IP, - CONF_STATIC_IP, - CONF_TYPE, - CONF_USE_ADDRESS, - CONF_GATEWAY, - CONF_SUBNET, + CONF_ADDRESS, + CONF_CLK_PIN, + CONF_CS_PIN, CONF_DNS1, CONF_DNS2, + CONF_DOMAIN, + CONF_GATEWAY, + CONF_ID, + CONF_INTERRUPT_PIN, + CONF_MANUAL_IP, + CONF_MISO_PIN, + CONF_MOSI_PIN, + CONF_PAGE_ID, + CONF_POLLING_INTERVAL, + CONF_RESET_PIN, + CONF_SPI, + CONF_STATIC_IP, + CONF_SUBNET, + CONF_TYPE, + CONF_USE_ADDRESS, + CONF_VALUE, + KEY_CORE, + KEY_FRAMEWORK_VERSION, ) -from esphome.core import CORE, coroutine_with_priority -from esphome.components.network import IPAddress +from esphome.core import CORE, TimePeriodMilliseconds, coroutine_with_priority +import esphome.final_validate as fv CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] +LOGGER = logging.getLogger(__name__) ethernet_ns = cg.esphome_ns.namespace("ethernet") +PHYRegister = ethernet_ns.struct("PHYRegister") CONF_PHY_ADDR = "phy_addr" CONF_MDC_PIN = "mdc_pin" CONF_MDIO_PIN = "mdio_pin" CONF_CLK_MODE = "clk_mode" CONF_POWER_PIN = "power_pin" +CONF_PHY_REGISTERS = "phy_registers" + +CONF_CLOCK_SPEED = "clock_speed" EthernetType = ethernet_ns.enum("EthernetType") ETHERNET_TYPES = { @@ -35,8 +62,14 @@ ETHERNET_TYPES = { "IP101": EthernetType.ETHERNET_TYPE_IP101, "JL1101": EthernetType.ETHERNET_TYPE_JL1101, "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, + "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, + "W5500": EthernetType.ETHERNET_TYPE_W5500, + "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, } +SPI_ETHERNET_TYPES = ["W5500"] +SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) + emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") CLK_MODES = { @@ -73,6 +106,24 @@ EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component) ManualIP = ethernet_ns.struct("ManualIP") +def _is_framework_spi_polling_mode_supported(): + # SPI Ethernet without IRQ feature is added in + # esp-idf >= (5.3+ ,5.2.1+, 5.1.4) and arduino-esp32 >= 3.0.0 + framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if CORE.using_esp_idf: + if framework_version >= cv.Version(5, 3, 0): + return True + if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1): + return True + if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): + return True + return False + if CORE.using_arduino: + return framework_version >= cv.Version(3, 0, 0) + # fail safe: Unknown framework + return False + + def _validate(config): if CONF_USE_ADDRESS not in config: if CONF_MANUAL_IP in config: @@ -80,14 +131,53 @@ def _validate(config): else: use_address = CORE.name + config[CONF_DOMAIN] config[CONF_USE_ADDRESS] = use_address + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: + if _is_framework_spi_polling_mode_supported(): + if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: + raise cv.Invalid( + f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}" + ) + if CONF_POLLING_INTERVAL not in config and CONF_INTERRUPT_PIN not in config: + config[CONF_POLLING_INTERVAL] = SPI_ETHERNET_DEFAULT_POLLING_INTERVAL + else: + if CONF_POLLING_INTERVAL in config: + raise cv.Invalid( + "In this version of the framework " + f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " + f"'{CONF_POLLING_INTERVAL}' is not supported." + ) + if CONF_INTERRUPT_PIN not in config: + raise cv.Invalid( + "In this version of the framework " + f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " + f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." + ) return config -CONFIG_SCHEMA = cv.All( +BASE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(EthernetComponent), + 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." + ), + } +).extend(cv.COMPONENT_SCHEMA) + +PHY_REGISTER_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.hex_int, + cv.Required(CONF_VALUE): cv.hex_int, + cv.Optional(CONF_PAGE_ID): cv.hex_int, + } +) +RMII_SCHEMA = BASE_SCHEMA.extend( cv.Schema( { - cv.GenerateID(): cv.declare_id(EthernetComponent), - cv.Required(CONF_TYPE): cv.enum(ETHERNET_TYPES, upper=True), cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_CLK_MODE, default="GPIO0_IN"): cv.enum( @@ -95,19 +185,73 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, - 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_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), + } + ) +) + +SPI_SCHEMA = BASE_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All( + cv.frequency, cv.int_range(int(8e6), int(80e6)) + ), + # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() + cv.Optional(CONF_POLLING_INTERVAL): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(min=TimePeriodMilliseconds(milliseconds=1)), ), } - ).extend(cv.COMPONENT_SCHEMA), + ), +) + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "LAN8720": RMII_SCHEMA, + "RTL8201": RMII_SCHEMA, + "DP83848": RMII_SCHEMA, + "IP101": RMII_SCHEMA, + "JL1101": RMII_SCHEMA, + "KSZ8081": RMII_SCHEMA, + "KSZ8081RNA": RMII_SCHEMA, + "W5500": SPI_SCHEMA, + "OPENETH": BASE_SCHEMA, + }, + upper=True, + ), _validate, ) +def _final_validate(config): + if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: + return + if spi_configs := fv.full_config.get().get(CONF_SPI): + variant = get_esp32_variant() + if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3): + spi_host = "SPI2_HOST" + else: + spi_host = "SPI3_HOST" + for spi_conf in spi_configs: + if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: + interface = get_spi_interface(index) + if interface == spi_host: + raise cv.Invalid( + f"`spi` component is using interface '{interface}'. " + f"To use {config[CONF_TYPE]}, you must change the `interface` on the `spi` component.", + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + def manual_ip(config): return cg.StructInitializer( ManualIP, @@ -119,20 +263,59 @@ def manual_ip(config): ) +def phy_register(address: int, value: int, page: int): + return cg.StructInitializer( + PHYRegister, + ("address", address), + ("value", value), + ("page", page), + ) + + @coroutine_with_priority(60.0) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) - cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) - cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) - cg.add(var.set_type(config[CONF_TYPE])) - cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) - cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) + if config[CONF_TYPE] == "W5500": + cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) + cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) + cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN])) + cg.add(var.set_cs_pin(config[CONF_CS_PIN])) + if CONF_INTERRUPT_PIN in config: + cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN])) + else: + cg.add(var.set_polling_interval(config[CONF_POLLING_INTERVAL])) + if _is_framework_spi_polling_mode_supported(): + cg.add_define("USE_ETHERNET_SPI_POLLING_SUPPORT") + if CONF_RESET_PIN in config: + cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) + cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED])) - if CONF_POWER_PIN in config: - cg.add(var.set_power_pin(config[CONF_POWER_PIN])) + cg.add_define("USE_ETHERNET_SPI") + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) + add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True) + elif config[CONF_TYPE] == "OPENETH": + cg.add_define("USE_ETHERNET_OPENETH") + add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) + else: + cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) + cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) + cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) + cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) + if CONF_POWER_PIN in config: + cg.add(var.set_power_pin(config[CONF_POWER_PIN])) + for register_value in config.get(CONF_PHY_REGISTERS, []): + reg = phy_register( + register_value.get(CONF_ADDRESS), + register_value.get(CONF_VALUE), + register_value.get(CONF_PAGE_ID), + ) + cg.add(var.add_phy_register(reg)) + + cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) + cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_MANUAL_IP in config: cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 6011795033..de2a6f4f35 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -19,7 +19,11 @@ #include #include "esp_log.h" #include "esp_eth.h" +#if ESP_IDF_VERSION_MAJOR >= 5 +#include "esp_eth_phy_802_3.h" +#else #include "eth_phy_regs_struct.h" +#endif #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" @@ -170,7 +174,11 @@ static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) { return ESP_OK; } +#if ESP_IDF_VERSION_MAJOR >= 5 +static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy, eth_phy_autoneg_cmd_t cmd, bool *nego_state) { +#else static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) { +#endif phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); esp_eth_mediator_t *eth = jl1101->eth; /* in case any link status has changed, let's assume we're in link down status */ @@ -285,7 +293,11 @@ static esp_err_t jl1101_init(esp_eth_phy_t *phy) { esp_eth_mediator_t *eth = jl1101->eth; // Detect PHY address if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) { +#if ESP_IDF_VERSION_MAJOR >= 5 + PHY_CHECK(esp_eth_phy_802_3_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); +#else PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); +#endif } /* Power on Ethernet PHY */ PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err); @@ -324,7 +336,11 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { jl1101->parent.init = jl1101_init; jl1101->parent.deinit = jl1101_deinit; jl1101->parent.set_mediator = jl1101_set_mediator; +#if ESP_IDF_VERSION_MAJOR >= 5 + jl1101->parent.autonego_ctrl = jl1101_negotiate; +#else jl1101->parent.negotiate = jl1101_negotiate; +#endif jl1101->parent.get_link = jl1101_get_link; jl1101->parent.pwrctl = jl1101_pwrctl; jl1101->parent.get_addr = jl1101_get_addr; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 0487ea5498..08f5fa6642 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -1,13 +1,19 @@ #include "ethernet_component.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -#include "esphome/core/application.h" #ifdef USE_ESP32 #include +#include #include "esp_event.h" +#ifdef USE_ETHERNET_SPI +#include +#include +#endif + namespace esphome { namespace ethernet { @@ -22,6 +28,13 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non- return; \ } +#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ + if ((err) != ESP_OK) { \ + ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ + this->mark_failed(); \ + return ret; \ + } + EthernetComponent::EthernetComponent() { global_eth_component = this; } void EthernetComponent::setup() { @@ -32,6 +45,37 @@ void EthernetComponent::setup() { } esp_err_t err; + +#ifdef USE_ETHERNET_SPI + // Install GPIO ISR handler to be able to service SPI Eth modules interrupts + gpio_install_isr_service(0); + + spi_bus_config_t buscfg = { + .mosi_io_num = this->mosi_pin_, + .miso_io_num = this->miso_pin_, + .sclk_io_num = this->clk_pin_, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .data4_io_num = -1, + .data5_io_num = -1, + .data6_io_num = -1, + .data7_io_num = -1, + .max_transfer_sz = 0, + .flags = 0, + .intr_flags = 0, + }; + +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + defined(USE_ESP32_VARIANT_ESP32C6) + auto host = SPI2_HOST; +#else + auto host = SPI3_HOST; +#endif + + err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); + ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); +#endif + err = esp_netif_init(); ESPHL_ERROR_CHECK(err, "ETH netif init error"); err = esp_event_loop_create_default(); @@ -41,20 +85,77 @@ void EthernetComponent::setup() { this->eth_netif_ = esp_netif_new(&cfg); // Init MAC and PHY configs to default - eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); +#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module + spi_device_interface_config_t devcfg = { + .command_bits = 16, // Actually it's the address phase in W5500 SPI frame + .address_bits = 8, // Actually it's the control phase in W5500 SPI frame + .dummy_bits = 0, + .mode = 0, + .duty_cycle_pos = 0, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .clock_speed_hz = this->clock_speed_, + .input_delay_ns = 0, + .spics_io_num = this->cs_pin_, + .flags = 0, + .queue_size = 20, + .pre_cb = nullptr, + .post_cb = nullptr, + }; + +#if USE_ESP_IDF && (ESP_IDF_VERSION_MAJOR >= 5) + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); +#else + spi_device_handle_t spi_handle = nullptr; + err = spi_bus_add_device(host, &devcfg, &spi_handle); + ESPHL_ERROR_CHECK(err, "SPI bus add device error"); + + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle); +#endif + w5500_config.int_gpio_num = this->interrupt_pin_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + w5500_config.poll_period_ms = this->polling_interval_; +#endif + phy_config.phy_addr = this->phy_addr_spi_; + phy_config.reset_gpio_num = this->reset_pin_; + + esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); +#elif defined(USE_ETHERNET_OPENETH) + esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); +#else phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; +#if ESP_IDF_VERSION_MAJOR >= 5 + eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; + esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; + esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; + esp32_emac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); +#else mac_config.smi_mdc_gpio_num = this->mdc_pin_; mac_config.smi_mdio_gpio_num = this->mdio_pin_; mac_config.clock_config.rmii.clock_mode = this->clk_mode_; mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); +#endif +#endif switch (this->type_) { +#ifdef USE_ETHERNET_OPENETH + case ETHERNET_TYPE_OPENETH: { + phy_config.autonego_timeout_ms = 1000; + this->phy_ = esp_eth_phy_new_dp83848(&phy_config); + break; + } +#endif +#if CONFIG_ETH_USE_ESP32_EMAC case ETHERNET_TYPE_LAN8720: { this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); break; @@ -75,10 +176,22 @@ void EthernetComponent::setup() { this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; } - case ETHERNET_TYPE_KSZ8081: { + case ETHERNET_TYPE_KSZ8081: + case ETHERNET_TYPE_KSZ8081RNA: { +#if ESP_IDF_VERSION_MAJOR >= 5 + this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); +#else this->phy_ = esp_eth_phy_new_ksz8081(&phy_config); +#endif break; } +#endif +#ifdef USE_ETHERNET_SPI + case ETHERNET_TYPE_W5500: { + this->phy_ = esp_eth_phy_new_w5500(&phy_config); + break; + } +#endif default: { this->mark_failed(); return; @@ -89,6 +202,24 @@ void EthernetComponent::setup() { this->eth_handle_ = nullptr; err = esp_eth_driver_install(ð_config, &this->eth_handle_); ESPHL_ERROR_CHECK(err, "ETH driver install error"); + +#ifndef USE_ETHERNET_SPI + if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { + // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. + this->ksz8081_set_clock_reference_(mac); + } + + for (const auto &phy_register : this->phy_registers_) { + this->write_phy_register_(mac, phy_register); + } +#endif + + // use ESP internal eth mac + uint8_t mac_addr[6]; + esp_read_mac(mac_addr, ESP_MAC_ETH); + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr); + ESPHL_ERROR_CHECK(err, "set mac address error"); + /* attach Ethernet driver to TCP/IP stack */ err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); ESPHL_ERROR_CHECK(err, "ETH netif attach error"); @@ -98,6 +229,10 @@ void EthernetComponent::setup() { ESPHL_ERROR_CHECK(err, "ETH event handler register error"); err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr); ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); +#if USE_NETWORK_IPV6 + err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error"); +#endif /* USE_NETWORK_IPV6 */ /* start Ethernet driver state machine */ err = esp_eth_start(this->eth_handle_); @@ -171,6 +306,18 @@ void EthernetComponent::dump_config() { eth_type = "KSZ8081"; break; + case ETHERNET_TYPE_KSZ8081RNA: + eth_type = "KSZ8081RNA"; + break; + + case ETHERNET_TYPE_W5500: + eth_type = "W5500"; + break; + + case ETHERNET_TYPE_OPENETH: + eth_type = "OPENETH"; + break; + default: eth_type = "Unknown"; break; @@ -178,23 +325,63 @@ void EthernetComponent::dump_config() { ESP_LOGCONFIG(TAG, "Ethernet:"); this->dump_connect_params_(); +#ifdef USE_ETHERNET_SPI + ESP_LOGCONFIG(TAG, " CLK Pin: %u", this->clk_pin_); + ESP_LOGCONFIG(TAG, " MISO Pin: %u", this->miso_pin_); + ESP_LOGCONFIG(TAG, " MOSI Pin: %u", this->mosi_pin_); + ESP_LOGCONFIG(TAG, " CS Pin: %u", this->cs_pin_); +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + if (this->polling_interval_ != 0) { + ESP_LOGCONFIG(TAG, " Polling Interval: %lu ms", this->polling_interval_); + } else +#endif + { + ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_); + } + ESP_LOGCONFIG(TAG, " Reset Pin: %d", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Clock Speed: %d MHz", this->clock_speed_ / 1000000); +#else if (this->power_pin_ != -1) { ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); } ESP_LOGCONFIG(TAG, " MDC Pin: %u", this->mdc_pin_); ESP_LOGCONFIG(TAG, " MDIO Pin: %u", this->mdio_pin_); - ESP_LOGCONFIG(TAG, " Type: %s", eth_type); ESP_LOGCONFIG(TAG, " PHY addr: %u", this->phy_addr_); +#endif + ESP_LOGCONFIG(TAG, " Type: %s", eth_type); } float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } bool EthernetComponent::can_proceed() { return this->is_connected(); } -network::IPAddress EthernetComponent::get_ip_address() { +network::IPAddresses EthernetComponent::get_ip_addresses() { + network::IPAddresses addresses; esp_netif_ip_info_t ip; - esp_netif_get_ip_info(this->eth_netif_, &ip); - return {ip.ip.addr}; + esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); + // TODO: do something smarter + // return false; + } else { + addresses[0] = network::IPAddress(&ip.ip); + } +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + addresses[i + 1] = network::IPAddress(&if_ip6s[i]); + } +#endif /* USE_NETWORK_IPV6 */ + + return addresses; +} + +network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { + const ip_addr_t *dns_ip = dns_getserver(num); + return dns_ip; } void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) { @@ -221,18 +408,44 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base return; } - ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event); + ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event); } void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; + const esp_netif_ip_info_t *ip_info = &event->ip_info; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip)); + global_eth_component->got_ipv4_address_ = true; +#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) + global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; +#else global_eth_component->connected_ = true; - ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%d)", event_id); +#endif /* USE_NETWORK_IPV6 */ } +#if USE_NETWORK_IPV6 +void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data) { + ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip)); + global_eth_component->ipv6_count_ += 1; +#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) + global_eth_component->connected_ = + global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); +#else + global_eth_component->connected_ = global_eth_component->got_ipv4_address_; +#endif +} +#endif /* USE_NETWORK_IPV6 */ + void EthernetComponent::start_connect_() { + global_eth_component->got_ipv4_address_ = false; +#if USE_NETWORK_IPV6 + global_eth_component->ipv6_count_ = 0; +#endif /* USE_NETWORK_IPV6 */ this->connect_begin_ = millis(); - this->status_set_warning(); + this->status_set_warning("waiting for IP configuration"); esp_err_t err; err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); @@ -242,9 +455,9 @@ void EthernetComponent::start_connect_() { esp_netif_ip_info_t info; if (this->manual_ip_.has_value()) { - info.ip.addr = static_cast(this->manual_ip_->static_ip); - info.gw.addr = static_cast(this->manual_ip_->gateway); - info.netmask.addr = static_cast(this->manual_ip_->subnet); + info.ip = this->manual_ip_->static_ip; + info.gw = this->manual_ip_->gateway; + info.netmask = this->manual_ip_->subnet; } else { info.ip.addr = 0; info.gw.addr = 0; @@ -267,24 +480,14 @@ void EthernetComponent::start_connect_() { ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); if (this->manual_ip_.has_value()) { - if (uint32_t(this->manual_ip_->dns1) != 0) { + if (this->manual_ip_->dns1.is_set()) { ip_addr_t d; -#if LWIP_IPV6 - d.type = IPADDR_TYPE_V4; - d.u_addr.ip4.addr = static_cast(this->manual_ip_->dns1); -#else - d.addr = static_cast(this->manual_ip_->dns1); -#endif + d = this->manual_ip_->dns1; dns_setserver(0, &d); } - if (uint32_t(this->manual_ip_->dns2) != 0) { + if (this->manual_ip_->dns2.is_set()) { ip_addr_t d; -#if LWIP_IPV6 - d.type = IPADDR_TYPE_V4; - d.u_addr.ip4.addr = static_cast(this->manual_ip_->dns2); -#else - d.addr = static_cast(this->manual_ip_->dns2); -#endif + d = this->manual_ip_->dns2; dns_setserver(1, &d); } } else { @@ -293,6 +496,12 @@ void EthernetComponent::start_connect_() { ESPHL_ERROR_CHECK(err, "DHCPC start error"); } } +#if USE_NETWORK_IPV6 + err = esp_netif_create_ip6_linklocal(this->eth_netif_); + if (err != ESP_OK) { + ESPHL_ERROR_CHECK(err, "Enable IPv6 link local failed"); + } +#endif /* USE_NETWORK_IPV6 */ this->connect_begin_ = millis(); this->status_set_warning(); @@ -303,49 +512,55 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen void EthernetComponent::dump_connect_params_() { esp_netif_ip_info_t ip; esp_netif_get_ip_info(this->eth_netif_, &ip); - ESP_LOGCONFIG(TAG, " IP Address: %s", network::IPAddress(ip.ip.addr).str().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", network::IPAddress(&ip.ip).str().c_str()); ESP_LOGCONFIG(TAG, " Hostname: '%s'", App.get_name().c_str()); - ESP_LOGCONFIG(TAG, " Subnet: %s", network::IPAddress(ip.netmask.addr).str().c_str()); - ESP_LOGCONFIG(TAG, " Gateway: %s", network::IPAddress(ip.gw.addr).str().c_str()); + ESP_LOGCONFIG(TAG, " Subnet: %s", network::IPAddress(&ip.netmask).str().c_str()); + ESP_LOGCONFIG(TAG, " Gateway: %s", network::IPAddress(&ip.gw).str().c_str()); const ip_addr_t *dns_ip1 = dns_getserver(0); const ip_addr_t *dns_ip2 = dns_getserver(1); -#if LWIP_IPV6 - ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1->u_addr.ip4.addr).str().c_str()); - ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2->u_addr.ip4.addr).str().c_str()); -#else - ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1->addr).str().c_str()); - ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2->addr).str().c_str()); -#endif + ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1).str().c_str()); + ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2).str().c_str()); - esp_err_t err; +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i])); + } +#endif /* USE_NETWORK_IPV6 */ - uint8_t mac[6]; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, &mac); - ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); - ESP_LOGCONFIG(TAG, " MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - eth_duplex_t duplex_mode; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); - ESPHL_ERROR_CHECK(err, "ETH_CMD_G_DUPLEX_MODE error"); - ESP_LOGCONFIG(TAG, " Is Full Duplex: %s", YESNO(duplex_mode == ETH_DUPLEX_FULL)); - - eth_speed_t speed; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); - ESPHL_ERROR_CHECK(err, "ETH_CMD_G_SPEED error"); - ESP_LOGCONFIG(TAG, " Link Speed: %u", speed == ETH_SPEED_100M ? 100 : 10); + ESP_LOGCONFIG(TAG, " MAC Address: %s", this->get_eth_mac_address_pretty().c_str()); + ESP_LOGCONFIG(TAG, " Is Full Duplex: %s", YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL)); + ESP_LOGCONFIG(TAG, " Link Speed: %u", this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); } +#ifdef USE_ETHERNET_SPI +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } +void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } +void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } +void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } +void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } +void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT +void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } +#endif +#else void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } -void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio) { this->clk_mode_ = clk_mode; this->clk_gpio_ = clk_gpio; } +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; } void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } std::string EthernetComponent::get_use_address() const { @@ -357,6 +572,34 @@ std::string EthernetComponent::get_use_address() const { void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { + esp_err_t err; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac); + ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); +} + +std::string EthernetComponent::get_eth_mac_address_pretty() { + uint8_t mac[6]; + get_mac_address_raw(mac); + return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + +eth_duplex_t EthernetComponent::get_duplex_mode() { + esp_err_t err; + eth_duplex_t duplex_mode; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); + ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF); + return duplex_mode; +} + +eth_speed_t EthernetComponent::get_link_speed() { + esp_err_t err; + eth_speed_t speed; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); + ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M); + return speed; +} + bool EthernetComponent::powerdown() { ESP_LOGI(TAG, "Powering down ethernet PHY"); if (this->phy_ == nullptr) { @@ -372,6 +615,61 @@ bool EthernetComponent::powerdown() { return true; } +#ifndef USE_ETHERNET_SPI + +constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F; + +void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { + esp_err_t err; + + 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()); + + /* + * Bit 7 is `RMII Reference Clock Select`. Default is `0`. + * KSZ8081RNA: + * 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. + * KSZ8081RND: + * 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode. + */ + if ((phy_control_2 & (1 << 7)) != (1 << 7)) { + phy_control_2 |= 1 << 7; + err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2); + 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()); + } +} + +void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { + esp_err_t err; + constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; + + if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { + ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page); + err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page); + 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); + err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); + ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); + + if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { + ESP_LOGD(TAG, "Select PHY Register Page 0x00"); + err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0); + ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed"); + } +} + +#endif + } // namespace ethernet } // namespace esphome diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 918e47212f..fb178431d5 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/components/network/ip_address.h" @@ -9,6 +10,7 @@ #include "esp_eth.h" #include "esp_eth_mac.h" #include "esp_netif.h" +#include "esp_mac.h" namespace esphome { namespace ethernet { @@ -21,6 +23,9 @@ enum EthernetType { ETHERNET_TYPE_IP101, ETHERNET_TYPE_JL1101, ETHERNET_TYPE_KSZ8081, + ETHERNET_TYPE_KSZ8081RNA, + ETHERNET_TYPE_W5500, + ETHERNET_TYPE_OPENETH, }; struct ManualIP { @@ -31,6 +36,12 @@ struct ManualIP { network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; +struct PHYRegister { + uint32_t address; + uint32_t value; + uint32_t page; +}; + enum class EthernetComponentState { STOPPED, CONNECTING, @@ -48,38 +59,83 @@ class EthernetComponent : public Component { void on_shutdown() override { powerdown(); } bool is_connected(); +#ifdef USE_ETHERNET_SPI + void set_clk_pin(uint8_t clk_pin); + void set_miso_pin(uint8_t miso_pin); + void set_mosi_pin(uint8_t mosi_pin); + void set_cs_pin(uint8_t cs_pin); + void set_interrupt_pin(uint8_t interrupt_pin); + void set_reset_pin(uint8_t reset_pin); + void set_clock_speed(int clock_speed); +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + void set_polling_interval(uint32_t polling_interval); +#endif +#else void set_phy_addr(uint8_t phy_addr); void set_power_pin(int power_pin); void set_mdc_pin(uint8_t mdc_pin); void set_mdio_pin(uint8_t mdio_pin); - void set_type(EthernetType type); void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); + void add_phy_register(PHYRegister register_value); +#endif + void set_type(EthernetType type); void set_manual_ip(const ManualIP &manual_ip); - network::IPAddress get_ip_address(); + network::IPAddresses get_ip_addresses(); + network::IPAddress get_dns_address(uint8_t num); std::string get_use_address() const; void set_use_address(const std::string &use_address); + void get_eth_mac_address_raw(uint8_t *mac); + std::string get_eth_mac_address_pretty(); + eth_duplex_t get_duplex_mode(); + eth_speed_t get_link_speed(); bool powerdown(); protected: static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); +#if LWIP_IPV6 + static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); +#endif /* LWIP_IPV6 */ void start_connect_(); void dump_connect_params_(); + /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. + void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); + /// @brief Set arbitratry PHY registers from config. + void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); std::string use_address_; +#ifdef USE_ETHERNET_SPI + uint8_t clk_pin_; + uint8_t miso_pin_; + uint8_t mosi_pin_; + uint8_t cs_pin_; + int interrupt_pin_{-1}; + int reset_pin_{-1}; + int phy_addr_spi_{-1}; + int clock_speed_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + uint32_t polling_interval_{0}; +#endif +#else uint8_t phy_addr_{0}; int power_pin_{-1}; uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; - EthernetType type_{ETHERNET_TYPE_UNKNOWN}; emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; + std::vector phy_registers_{}; +#endif + EthernetType type_{ETHERNET_TYPE_UNKNOWN}; optional manual_ip_{}; bool started_{false}; bool connected_{false}; + bool got_ipv4_address_{false}; +#if LWIP_IPV6 + uint8_t ipv6_count_{0}; +#endif /* LWIP_IPV6 */ EthernetComponentState state_{EthernetComponentState::STOPPED}; uint32_t connect_begin_; esp_netif_t *eth_netif_{nullptr}; diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp index e69872c290..329fb9113a 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -1,7 +1,7 @@ #include "ethernet_info_text_sensor.h" #include "esphome/core/log.h" -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_ESP32 namespace esphome { namespace ethernet_info { @@ -9,8 +9,10 @@ namespace ethernet_info { static const char *const TAG = "ethernet_info"; void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IPAddress", this); } +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 -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#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 aad8f362b5..94eed886e5 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -4,7 +4,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/ethernet/ethernet_component.h" -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_ESP32 namespace esphome { namespace ethernet_info { @@ -12,22 +12,61 @@ namespace ethernet_info { class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { - auto ip = ethernet::global_eth_component->get_ip_address(); - if (ip != this->last_ip_) { - this->last_ip_ = ip; - this->publish_state(network::IPAddress(ip).str()); + auto ips = ethernet::global_eth_component->get_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::ETHERNET; } std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; } void dump_config() override; + void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } protected: - network::IPAddress last_ip_; + network::IPAddresses last_ips_; + std::array ip_sensors_; +}; + +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); + + 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); + } + } + float get_setup_priority() const override { return setup_priority::ETHERNET; } + std::string unique_id() override { return get_mac_address() + "-ethernetinfo-dns"; } + void dump_config() override; + + protected: + std::string last_results_; +}; + +class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor { + public: + void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); } + std::string unique_id() override { return get_mac_address() + "-ethernetinfo-mac"; } + void dump_config() override; }; } // namespace ethernet_info } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/text_sensor.py b/esphome/components/ethernet_info/text_sensor.py index 7cb9944c92..31da516e44 100644 --- a/esphome/components/ethernet_info/text_sensor.py +++ b/esphome/components/ethernet_info/text_sensor.py @@ -1,8 +1,10 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import text_sensor +import esphome.config_validation as cv from esphome.const import ( + CONF_DNS_ADDRESS, CONF_IP_ADDRESS, + CONF_MAC_ADDRESS, ENTITY_CATEGORY_DIAGNOSTIC, ) @@ -10,25 +12,53 @@ DEPENDENCIES = ["ethernet"] ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info") -IPAddressEsthernetInfo = ethernet_info_ns.class_( +IPAddressEthernetInfo = ethernet_info_ns.class_( "IPAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent ) +DNSAddressEthernetInfo = ethernet_info_ns.class_( + "DNSAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent +) + +MACAddressEthernetInfo = ethernet_info_ns.class_( + "MACAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent +) + CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( - IPAddressEsthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")) + IPAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ) + .extend(cv.polling_component_schema("1s")) + .extend( + { + cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + for x in range(5) + } + ), + cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema( + DNSAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ).extend(cv.polling_component_schema("1s")), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + MACAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), } ) -async def setup_conf(config, key): - if key in config: - conf = config[key] - var = await text_sensor.new_text_sensor(conf) - await cg.register_component(var, conf) - - async def to_code(config): - await setup_conf(config, CONF_IP_ADDRESS) + if conf := config.get(CONF_IP_ADDRESS): + ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS]) + await cg.register_component(ip_info, config[CONF_IP_ADDRESS]) + for x in range(5): + if sensor_conf := conf.get(f"address_{x}"): + sens = await text_sensor.new_text_sensor(sensor_conf) + cg.add(ip_info.add_ip_sensors(x, sens)) + if conf := config.get(CONF_DNS_ADDRESS): + dns_info = await text_sensor.new_text_sensor(config[CONF_DNS_ADDRESS]) + await cg.register_component(dns_info, config[CONF_DNS_ADDRESS]) + if conf := config.get(CONF_MAC_ADDRESS): + mac_info = await text_sensor.new_text_sensor(config[CONF_MAC_ADDRESS]) + await cg.register_component(mac_info, config[CONF_MAC_ADDRESS]) diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py new file mode 100644 index 0000000000..a7732dfcaf --- /dev/null +++ b/esphome/components/event/__init__.py @@ -0,0 +1,142 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_EVENT_TYPE, + CONF_ICON, + CONF_ID, + CONF_MQTT_ID, + CONF_ON_EVENT, + CONF_TRIGGER_ID, + CONF_WEB_SERVER, + DEVICE_CLASS_BUTTON, + DEVICE_CLASS_DOORBELL, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_MOTION, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@nohat"] +IS_PLATFORM_COMPONENT = True + +DEVICE_CLASSES = [ + DEVICE_CLASS_BUTTON, + DEVICE_CLASS_DOORBELL, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_MOTION, +] + +event_ns = cg.esphome_ns.namespace("event") +Event = event_ns.class_("Event", cg.EntityBase) +EventPtr = Event.operator("ptr") + +TriggerEventAction = event_ns.class_("TriggerEventAction", automation.Action) + +EventTrigger = event_ns.class_("EventTrigger", automation.Trigger.template()) + +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") + +EVENT_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent), + cv.GenerateID(): cv.declare_id(Event), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional(CONF_ON_EVENT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(EventTrigger), + } + ), + } + ) +) + +_UNDEF = object() + + +def event_schema( + class_: MockObjClass = _UNDEF, + *, + icon: str = _UNDEF, + entity_category: str = _UNDEF, + device_class: str = _UNDEF, +) -> cv.Schema: + schema = {} + + if class_ is not _UNDEF: + 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), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + ]: + if default is not _UNDEF: + schema[cv.Optional(key, default=default)] = validator + + return EVENT_SCHEMA.extend(schema) + + +async def setup_event_core_(var, config, *, event_types: list[str]): + await setup_entity(var, config) + + for conf in config.get(CONF_ON_EVENT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.std_string, "event_type")], conf + ) + + cg.add(var.set_event_types(event_types)) + + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + + if mqtt_id := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id, var) + await mqtt.register_mqtt_component(mqtt_, config) + + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + + +async def register_event(var, config, *, event_types: list[str]): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_event(var)) + await setup_event_core_(var, config, event_types=event_types) + + +async def new_event(config, *, event_types: list[str]): + var = cg.new_Pvariable(config[CONF_ID]) + await register_event(var, config, event_types=event_types) + return var + + +TRIGGER_EVENT_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Event), + cv.Required(CONF_EVENT_TYPE): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action("event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA) +async def event_fire_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + templ = await cg.templatable(config[CONF_EVENT_TYPE], args, cg.std_string) + cg.add(var.set_event_type(templ)) + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_EVENT") + cg.add_global(event_ns.using) diff --git a/esphome/components/event/automation.h b/esphome/components/event/automation.h new file mode 100644 index 0000000000..9ebcb654a0 --- /dev/null +++ b/esphome/components/event/automation.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/event/event.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace event { + +template class TriggerEventAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(std::string, event_type) + + void play(Ts... x) override { this->parent_->trigger(this->event_type_.value(x...)); } +}; + +class EventTrigger : public Trigger { + public: + EventTrigger(Event *event) { + event->add_on_event_callback([this](const std::string &event_type) { this->trigger(event_type); }); + } +}; + +} // namespace event +} // namespace esphome diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp new file mode 100644 index 0000000000..061afcb026 --- /dev/null +++ b/esphome/components/event/event.cpp @@ -0,0 +1,24 @@ +#include "event.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace event { + +static const char *const TAG = "event"; + +void Event::trigger(const std::string &event_type) { + if (types_.find(event_type) == types_.end()) { + ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); + return; + } + ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), event_type.c_str()); + this->event_callback_.call(event_type); +} + +void Event::add_on_event_callback(std::function &&callback) { + this->event_callback_.add(std::move(callback)); +} + +} // namespace event +} // namespace esphome diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h new file mode 100644 index 0000000000..067a867360 --- /dev/null +++ b/esphome/components/event/event.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace event { + +#define LOG_EVENT(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + } \ + } + +class Event : public EntityBase, public EntityBase_DeviceClass { + public: + void trigger(const std::string &event_type); + void set_event_types(const std::set &event_types) { this->types_ = event_types; } + std::set get_event_types() const { return this->types_; } + void add_on_event_callback(std::function &&callback); + + protected: + CallbackManager event_callback_; + std::set types_; +}; + +} // namespace event +} // namespace esphome diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index bbb703dc5c..f4432a0362 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -49,7 +49,16 @@ def _process_git_config(config: dict, refresh) -> str: password=config.get(CONF_PASSWORD), ) - if (repo_dir / "esphome" / "components").is_dir(): + if path := config.get(CONF_PATH): + if (repo_dir / path).is_dir(): + components_dir = repo_dir / path + else: + raise cv.Invalid( + "Could not find components folder for source. Please check the source contains a '" + + path + + "' folder" + ) + elif (repo_dir / "esphome" / "components").is_dir(): components_dir = repo_dir / "esphome" / "components" elif (repo_dir / "components").is_dir(): components_dir = repo_dir / "components" diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py index e65fcf74ca..87cda41f89 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -1,7 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c -from esphome.const import CONF_ADDRESS, CONF_COMMAND, CONF_ID, CONF_DURATION +from esphome.const import ( + CONF_ADDRESS, + CONF_COMMAND, + CONF_ID, + CONF_DURATION, + CONF_VOLUME, +) from esphome import automation from esphome.automation import maybe_simple_id @@ -9,7 +15,6 @@ CODEOWNERS = ["@carlos-sarmiento"] DEPENDENCIES = ["i2c"] MULTI_CONF = True -CONF_VOLUME = "volume" CONF_VOLUME_PER_MINUTE = "volume_per_minute" ezo_pmp_ns = cg.esphome_ns.namespace("ezo_pmp") diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 9a05bff3a0..4e0e52cd65 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -1,25 +1,31 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_DIRECTION, CONF_ID, CONF_MQTT_ID, + CONF_OFF_SPEED_CYCLE, + CONF_ON_DIRECTION_SET, + CONF_ON_OSCILLATING_SET, + CONF_ON_PRESET_SET, + CONF_ON_SPEED_SET, + CONF_ON_STATE, + CONF_ON_TURN_OFF, + CONF_ON_TURN_ON, CONF_OSCILLATING, CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, + CONF_RESTORE_MODE, CONF_SPEED, + CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LEVEL_COMMAND_TOPIC, CONF_SPEED_LEVEL_STATE_TOPIC, - CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, - CONF_ON_SPEED_SET, - CONF_ON_TURN_OFF, - CONF_ON_TURN_ON, CONF_TRIGGER_ID, - CONF_DIRECTION, - CONF_RESTORE_MODE, + CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -53,106 +59,189 @@ TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) ToggleAction = fan_ns.class_("ToggleAction", automation.Action) CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action) +FanStateTrigger = fan_ns.class_( + "FanStateTrigger", automation.Trigger.template(Fan.operator("ptr")) +) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) -FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) +FanDirectionSetTrigger = fan_ns.class_( + "FanDirectionSetTrigger", automation.Trigger.template(FanDirection) +) +FanOscillatingSetTrigger = fan_ns.class_( + "FanOscillatingSetTrigger", automation.Trigger.template(cg.bool_) +) +FanSpeedSetTrigger = fan_ns.class_( + "FanSpeedSetTrigger", automation.Trigger.template(cg.int_) +) +FanPresetSetTrigger = fan_ns.class_( + "FanPresetSetTrigger", automation.Trigger.template(cg.std_string) +) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) -FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.GenerateID(): cv.declare_id(Fan), - cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( - RESTORE_MODES, upper=True, space="_" - ), - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent), - cv.Optional(CONF_OSCILLATION_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.subscribe_topic - ), - cv.Optional(CONF_SPEED_LEVEL_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_SPEED_LEVEL_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.subscribe_topic - ), - cv.Optional(CONF_SPEED_STATE_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.publish_topic - ), - cv.Optional(CONF_SPEED_COMMAND_TOPIC): cv.All( - cv.requires_component("mqtt"), cv.subscribe_topic - ), - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger), - } - ), - cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), - } - ), - } +FAN_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(Fan), + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent), + cv.Optional(CONF_OSCILLATION_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_SPEED_LEVEL_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_SPEED_LEVEL_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_SPEED_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_SPEED_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanStateTrigger), + } + ), + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOnTrigger), + } + ), + cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger), + } + ), + cv.Optional(CONF_ON_DIRECTION_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FanDirectionSetTrigger + ), + } + ), + cv.Optional(CONF_ON_OSCILLATING_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FanOscillatingSetTrigger + ), + } + ), + cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), + } + ), + cv.Optional(CONF_ON_PRESET_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanPresetSetTrigger), + } + ), + } + ) ) +_PRESET_MODES_SCHEMA = cv.All( + cv.ensure_list(cv.string_strict), + cv.Length(min=1), +) + + +def validate_preset_modes(value): + # Check against defined schema + value = _PRESET_MODES_SCHEMA(value) + + # Ensure preset names are unique + errors = [] + presets = set() + for i, preset in enumerate(value): + # If name does not exist yet add it + if preset not in presets: + presets.add(preset) + continue + + # Otherwise it's an error + errors.append( + cv.Invalid( + f"Found duplicate preset name '{preset}'. Presets must have unique names.", + [i], + ) + ) + + if errors: + raise cv.MultipleInvalid(errors) + + return value + async def setup_fan_core_(var, config): await setup_entity(var, config) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) - if CONF_OSCILLATION_STATE_TOPIC in config: + if ( + oscillation_state_topic := config.get(CONF_OSCILLATION_STATE_TOPIC) + ) is not None: + cg.add(mqtt_.set_custom_oscillation_state_topic(oscillation_state_topic)) + if ( + oscillation_command_topic := config.get(CONF_OSCILLATION_COMMAND_TOPIC) + ) is not None: cg.add( - mqtt_.set_custom_oscillation_state_topic( - config[CONF_OSCILLATION_STATE_TOPIC] - ) + mqtt_.set_custom_oscillation_command_topic(oscillation_command_topic) ) - if CONF_OSCILLATION_COMMAND_TOPIC in config: + if ( + speed_level_state_topic := config.get(CONF_SPEED_LEVEL_STATE_TOPIC) + ) is not None: + cg.add(mqtt_.set_custom_speed_level_state_topic(speed_level_state_topic)) + if ( + speed_level_command_topic := config.get(CONF_SPEED_LEVEL_COMMAND_TOPIC) + ) is not None: cg.add( - mqtt_.set_custom_oscillation_command_topic( - config[CONF_OSCILLATION_COMMAND_TOPIC] - ) - ) - if CONF_SPEED_LEVEL_STATE_TOPIC in config: - cg.add( - mqtt_.set_custom_speed_level_state_topic( - config[CONF_SPEED_LEVEL_STATE_TOPIC] - ) - ) - if CONF_SPEED_LEVEL_COMMAND_TOPIC in config: - cg.add( - mqtt_.set_custom_speed_level_command_topic( - config[CONF_SPEED_LEVEL_COMMAND_TOPIC] - ) - ) - if CONF_SPEED_STATE_TOPIC in config: - cg.add(mqtt_.set_custom_speed_state_topic(config[CONF_SPEED_STATE_TOPIC])) - if CONF_SPEED_COMMAND_TOPIC in config: - cg.add( - mqtt_.set_custom_speed_command_topic(config[CONF_SPEED_COMMAND_TOPIC]) + mqtt_.set_custom_speed_level_command_topic(speed_level_command_topic) ) + if (speed_state_topic := config.get(CONF_SPEED_STATE_TOPIC)) is not None: + cg.add(mqtt_.set_custom_speed_state_topic(speed_state_topic)) + if (speed_command_topic := config.get(CONF_SPEED_COMMAND_TOPIC)) is not None: + cg.add(mqtt_.set_custom_speed_command_topic(speed_command_topic)) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(Fan.operator("ptr"), "x")], conf) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_TURN_OFF, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_DIRECTION_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(FanDirection, "x")], conf) + for conf in config.get(CONF_ON_OSCILLATING_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.bool_, "x")], conf) for conf in config.get(CONF_ON_SPEED_SET, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_automation(trigger, [(cg.int_, "x")], conf) + for conf in config.get(CONF_ON_PRESET_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) async def register_fan(var, config): @@ -205,22 +294,34 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): async def fan_turn_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_OSCILLATING in config: - template_ = await cg.templatable(config[CONF_OSCILLATING], args, bool) + if (oscillating := config.get(CONF_OSCILLATING)) is not None: + template_ = await cg.templatable(oscillating, args, bool) cg.add(var.set_oscillating(template_)) - if CONF_SPEED in config: - template_ = await cg.templatable(config[CONF_SPEED], args, int) + if (speed := config.get(CONF_SPEED)) is not None: + template_ = await cg.templatable(speed, args, int) cg.add(var.set_speed(template_)) - if CONF_DIRECTION in config: - template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection) + if (direction := config.get(CONF_DIRECTION)) is not None: + template_ = await cg.templatable(direction, args, FanDirection) cg.add(var.set_direction(template_)) return var -@automation.register_action("fan.cycle_speed", CycleSpeedAction, FAN_ACTION_SCHEMA) +@automation.register_action( + "fan.cycle_speed", + CycleSpeedAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Fan), + cv.Optional(CONF_OFF_SPEED_CYCLE, default=True): cv.boolean, + } + ), +) async def fan_cycle_speed_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) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_OFF_SPEED_CYCLE], args, bool) + cg.add(var.set_no_off_cycle(template_)) + return var @automation.register_condition( diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 23fb70a95b..d480a2ef44 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -54,18 +54,26 @@ template class CycleSpeedAction : public Action { public: explicit CycleSpeedAction(Fan *state) : state_(state) {} + TEMPLATABLE_VALUE(bool, no_off_cycle) + void play(Ts... x) override { // check to see if fan supports speeds and is on if (this->state_->get_traits().supported_speed_count()) { if (this->state_->state) { int speed = this->state_->speed + 1; int supported_speed_count = this->state_->get_traits().supported_speed_count(); - if (speed > supported_speed_count) { - // was running at max speed, so turn off + bool off_speed_cycle = no_off_cycle_.value(x...); + if (speed > supported_speed_count && off_speed_cycle) { + // was running at max speed, off speed cycle enabled, so turn off speed = 1; auto call = this->state_->turn_off(); call.set_speed(speed); call.perform(); + } else if (speed > supported_speed_count && !off_speed_cycle) { + // was running at max speed, off speed cycle disabled, so set to lowest speed + auto call = this->state_->turn_on(); + call.set_speed(1); + call.perform(); } else { auto call = this->state_->turn_on(); call.set_speed(speed); @@ -103,6 +111,13 @@ template class FanIsOffCondition : public Condition { Fan *state_; }; +class FanStateTrigger : public Trigger { + public: + FanStateTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { this->trigger(state); }); + } +}; + class FanTurnOnTrigger : public Trigger<> { public: FanTurnOnTrigger(Fan *state) { @@ -139,15 +154,51 @@ class FanTurnOffTrigger : public Trigger<> { bool last_on_; }; -class FanSpeedSetTrigger : public Trigger<> { +class FanDirectionSetTrigger : public Trigger { + public: + FanDirectionSetTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { + auto direction = state->direction; + auto should_trigger = direction != this->last_direction_; + this->last_direction_ = direction; + if (should_trigger) { + this->trigger(direction); + } + }); + this->last_direction_ = state->direction; + } + + protected: + FanDirection last_direction_; +}; + +class FanOscillatingSetTrigger : public Trigger { + public: + FanOscillatingSetTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { + auto oscillating = state->oscillating; + auto should_trigger = oscillating != this->last_oscillating_; + this->last_oscillating_ = oscillating; + if (should_trigger) { + this->trigger(oscillating); + } + }); + this->last_oscillating_ = state->oscillating; + } + + protected: + bool last_oscillating_; +}; + +class FanSpeedSetTrigger : public Trigger { public: FanSpeedSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { auto speed = state->speed; - auto should_trigger = speed != !this->last_speed_; + auto should_trigger = speed != this->last_speed_; this->last_speed_ = speed; if (should_trigger) { - this->trigger(); + this->trigger(speed); } }); this->last_speed_ = state->speed; @@ -157,5 +208,23 @@ class FanSpeedSetTrigger : public Trigger<> { int last_speed_; }; +class FanPresetSetTrigger : public Trigger { + public: + FanPresetSetTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { + auto preset_mode = state->preset_mode; + auto should_trigger = preset_mode != this->last_preset_mode_; + this->last_preset_mode_ = preset_mode; + if (should_trigger) { + this->trigger(preset_mode); + } + }); + this->last_preset_mode_ = state->preset_mode; + } + + protected: + std::string last_preset_mode_; +}; + } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 87566bad4a..1d560d2fc6 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -32,9 +32,12 @@ void FanCall::perform() { if (this->direction_.has_value()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } - + if (!this->preset_mode_.empty()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str()); + } this->parent_.control(*this); } + void FanCall::validate_() { auto traits = this->parent_.get_traits(); @@ -42,8 +45,8 @@ void FanCall::validate_() { this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); if (this->binary_state_.has_value() && *this->binary_state_) { - // when turning on, if current speed is zero, set speed to 100% - if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0) { + // when turning on, if neither current nor new speed available, set speed to 100% + if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) { this->speed_ = traits.supported_speed_count(); } } @@ -62,6 +65,15 @@ void FanCall::validate_() { ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); this->direction_.reset(); } + + if (!this->preset_mode_.empty()) { + const auto &preset_modes = traits.supported_preset_modes(); + if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { + ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), + this->preset_mode_.c_str()); + this->preset_mode_.clear(); + } + } } FanCall FanRestoreState::to_call(Fan &fan) { @@ -70,6 +82,14 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_oscillating(this->oscillating); call.set_speed(this->speed); call.set_direction(this->direction); + + if (fan.get_traits().supports_preset_modes()) { + // Use stored preset index to get preset name + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); + } + } return call; } void FanRestoreState::apply(Fan &fan) { @@ -77,6 +97,14 @@ void FanRestoreState::apply(Fan &fan) { fan.oscillating = this->oscillating; fan.speed = this->speed; fan.direction = this->direction; + + if (fan.get_traits().supports_preset_modes()) { + // Use stored preset index to get preset name + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); + } + } fan.publish_state(); } @@ -100,7 +128,9 @@ void Fan::publish_state() { if (traits.supports_direction()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } - + if (traits.supports_preset_modes() && !this->preset_mode.empty()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str()); + } this->state_callback_.call(); this->save_state_(); } @@ -143,20 +173,36 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + + if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { + const auto &preset_modes = this->get_traits().supported_preset_modes(); + // Store index of current preset mode + auto preset_iterator = preset_modes.find(this->preset_mode); + if (preset_iterator != preset_modes.end()) + state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); + } + this->rtc_.save(&state); } void Fan::dump_traits_(const char *tag, const char *prefix) { - if (this->get_traits().supports_speed()) { + auto traits = this->get_traits(); + + if (traits.supports_speed()) { ESP_LOGCONFIG(tag, "%s Speed: YES", prefix); - ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, this->get_traits().supported_speed_count()); + ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, traits.supported_speed_count()); } - if (this->get_traits().supports_oscillation()) { + if (traits.supports_oscillation()) { ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix); } - if (this->get_traits().supports_direction()) { + if (traits.supports_direction()) { ESP_LOGCONFIG(tag, "%s Direction: YES", prefix); } + if (traits.supports_preset_modes()) { + ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); + for (const std::string &s : traits.supported_preset_modes()) + ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str()); + } } } // namespace fan diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index f9d317e675..b74187eb4a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -72,6 +72,11 @@ class FanCall { return *this; } optional get_direction() const { return this->direction_; } + FanCall &set_preset_mode(const std::string &preset_mode) { + this->preset_mode_ = preset_mode; + return *this; + } + std::string get_preset_mode() const { return this->preset_mode_; } void perform(); @@ -83,6 +88,7 @@ class FanCall { optional oscillating_; optional speed_; optional direction_{}; + std::string preset_mode_{}; }; struct FanRestoreState { @@ -90,6 +96,7 @@ struct FanRestoreState { int speed; bool oscillating; FanDirection direction; + uint8_t preset_mode; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); @@ -107,6 +114,8 @@ class Fan : public EntityBase { int speed{0}; /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; + // The current preset mode of the fan + std::string preset_mode{}; FanCall turn_on(); FanCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index e69d8e2e53..2ef6f8b7cc 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,3 +1,6 @@ +#include +#include + #pragma once namespace esphome { @@ -25,12 +28,19 @@ class FanTraits { bool supports_direction() const { return this->direction_; } /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } + /// Return the preset modes supported by the fan. + std::set supported_preset_modes() const { return this->preset_modes_; } + /// Set the preset modes supported by the fan. + void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } + /// Return if preset modes are supported + bool supports_preset_modes() const { return !this->preset_modes_.empty(); } protected: bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; + std::set preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index 117c626f58..fa3166ba65 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -244,7 +244,7 @@ void FeedbackCover::loop() { // update current position at requested interval, regardless of who started the movement // so that we also update UI if there was an external movement - // don´t save intermediate positions + // don't save intermediate positions if (now - this->last_publish_time_ > this->update_interval_) { this->publish_state(false); this->last_publish_time_ = now; @@ -274,7 +274,7 @@ void FeedbackCover::control(const CoverCall &call) { if (pos == this->position) { // already at target, - // for covers with built in end stop, if we don´t have sensors we should send the command again + // for covers with built in end stop, if we don't have sensors we should send the command again // to make sure the assumed state is not wrong if (this->has_built_in_endstop_ && ((pos == COVER_OPEN #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index ecbbc3d477..23651bd049 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -13,8 +13,11 @@ from esphome.const import ( CONF_ON_ENROLLMENT_DONE, CONF_ON_ENROLLMENT_FAILED, CONF_ON_ENROLLMENT_SCAN, + CONF_ON_FINGER_SCAN_START, CONF_ON_FINGER_SCAN_MATCHED, CONF_ON_FINGER_SCAN_UNMATCHED, + CONF_ON_FINGER_SCAN_MISPLACED, + CONF_ON_FINGER_SCAN_INVALID, CONF_PASSWORD, CONF_SENSING_PIN, CONF_SPEED, @@ -22,18 +25,24 @@ from esphome.const import ( CONF_TRIGGER_ID, ) -CODEOWNERS = ["@OnFreund", "@loongyh"] +CODEOWNERS = ["@OnFreund", "@loongyh", "@alexborro"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor", "sensor"] MULTI_CONF = True CONF_FINGERPRINT_GROW_ID = "fingerprint_grow_id" +CONF_SENSOR_POWER_PIN = "sensor_power_pin" +CONF_IDLE_PERIOD_TO_SLEEP = "idle_period_to_sleep" fingerprint_grow_ns = cg.esphome_ns.namespace("fingerprint_grow") FingerprintGrowComponent = fingerprint_grow_ns.class_( "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice ) +FingerScanStartTrigger = fingerprint_grow_ns.class_( + "FingerScanStartTrigger", automation.Trigger.template() +) + FingerScanMatchedTrigger = fingerprint_grow_ns.class_( "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) ) @@ -42,6 +51,14 @@ FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( "FingerScanUnmatchedTrigger", automation.Trigger.template() ) +FingerScanMisplacedTrigger = fingerprint_grow_ns.class_( + "FingerScanMisplacedTrigger", automation.Trigger.template() +) + +FingerScanInvalidTrigger = fingerprint_grow_ns.class_( + "FingerScanInvalidTrigger", automation.Trigger.template() +) + EnrollmentScanTrigger = fingerprint_grow_ns.class_( "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) ) @@ -87,13 +104,35 @@ AURA_LED_COLORS = { } validate_aura_led_colors = cv.enum(AURA_LED_COLORS, upper=True) -CONFIG_SCHEMA = ( + +def validate(config): + if CONF_SENSOR_POWER_PIN in config and CONF_SENSING_PIN not in config: + raise cv.Invalid("You cannot use the Sensor Power Pin without a Sensing Pin") + if CONF_IDLE_PERIOD_TO_SLEEP in config and CONF_SENSOR_POWER_PIN not in config: + raise cv.Invalid( + "You cannot have an Idle Period to Sleep without a Sensor Power Pin" + ) + return config + + +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(FingerprintGrowComponent), cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_SENSOR_POWER_PIN): pins.gpio_output_pin_schema, + cv.Optional( + CONF_IDLE_PERIOD_TO_SLEEP + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_PASSWORD): cv.uint32_t, cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, + cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanStartTrigger + ), + } + ), cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -108,6 +147,20 @@ CONFIG_SCHEMA = ( ), } ), + cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanMisplacedTrigger + ), + } + ), + cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanInvalidTrigger + ), + } + ), cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -132,7 +185,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("500ms")) - .extend(uart.UART_DEVICE_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + validate, ) @@ -152,6 +206,18 @@ async def to_code(config): sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN]) cg.add(var.set_sensing_pin(sensing_pin)) + if CONF_SENSOR_POWER_PIN in config: + sensor_power_pin = await cg.gpio_pin_expression(config[CONF_SENSOR_POWER_PIN]) + cg.add(var.set_sensor_power_pin(sensor_power_pin)) + + if CONF_IDLE_PERIOD_TO_SLEEP in config: + idle_period_to_sleep_ms = config[CONF_IDLE_PERIOD_TO_SLEEP] + cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms)) + + for conf in config.get(CONF_ON_FINGER_SCAN_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( @@ -162,6 +228,14 @@ async def to_code(config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 4043f32dcb..0dfea49b8b 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -1,5 +1,6 @@ #include "fingerprint_grow.h" #include "esphome/core/log.h" +#include namespace esphome { namespace fingerprint_grow { @@ -14,16 +15,23 @@ void FingerprintGrowComponent::update() { return; } - if (this->sensing_pin_ != nullptr) { + if (this->has_sensing_pin_) { + // A finger touch results in a low level (digital_read() == false) if (this->sensing_pin_->digital_read()) { ESP_LOGV(TAG, "No touch sensing"); this->waiting_removal_ = false; + if ((this->enrollment_image_ == 0) && // Not in enrolment process + (millis() - this->last_transfer_ms_ > this->idle_period_to_sleep_ms_) && (this->is_sensor_awake_)) { + this->sensor_sleep_(); + } return; + } else if (!this->waiting_removal_) { + this->finger_scan_start_callback_.call(); } } if (this->waiting_removal_) { - if (this->scan_image_(1) == NO_FINGER) { + if ((!this->has_sensing_pin_) && (this->scan_image_(1) == NO_FINGER)) { ESP_LOGD(TAG, "Finger removed"); this->waiting_removal_ = false; } @@ -50,6 +58,29 @@ void FingerprintGrowComponent::update() { void FingerprintGrowComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); + + this->has_sensing_pin_ = (this->sensing_pin_ != nullptr); + this->has_power_pin_ = (this->sensor_power_pin_ != nullptr); + + // Call pins setup, so we effectively apply the config generated from the yaml file. + if (this->has_sensing_pin_) { + this->sensing_pin_->setup(); + } + if (this->has_power_pin_) { + // Starts with output low (disabling power) to avoid glitches in the sensor + this->sensor_power_pin_->digital_write(false); + this->sensor_power_pin_->setup(); + + // If the user didn't specify an idle period to sleep, applies the default. + if (this->idle_period_to_sleep_ms_ == UINT32_MAX) { + this->idle_period_to_sleep_ms_ = DEFAULT_IDLE_PERIOD_TO_SLEEP_MS; + } + } + + // Place the sensor in a known (sleep/off) state and sync internal var state. + this->sensor_sleep_(); + delay(20); // This delay guarantees the sensor will in fact be powered power. + if (this->check_password_()) { if (this->new_password_ != -1) { if (this->set_password_()) @@ -90,7 +121,7 @@ void FingerprintGrowComponent::finish_enrollment(uint8_t result) { } void FingerprintGrowComponent::scan_and_match_() { - if (this->sensing_pin_ != nullptr) { + if (this->has_sensing_pin_) { ESP_LOGD(TAG, "Scan and match"); } else { ESP_LOGV(TAG, "Scan and match"); @@ -121,43 +152,52 @@ void FingerprintGrowComponent::scan_and_match_() { } uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { - if (this->sensing_pin_ != nullptr) { + if (this->has_sensing_pin_) { ESP_LOGD(TAG, "Getting image %d", buffer); } else { ESP_LOGV(TAG, "Getting image %d", buffer); } this->data_ = {GET_IMAGE}; - switch (this->send_command_()) { + uint8_t send_result = this->send_command_(); + switch (send_result) { case OK: break; case NO_FINGER: - if (this->sensing_pin_ != nullptr) { - ESP_LOGD(TAG, "No finger"); + if (this->has_sensing_pin_) { + this->waiting_removal_ = true; + ESP_LOGD(TAG, "Finger Misplaced"); + this->finger_scan_misplaced_callback_.call(); } else { ESP_LOGV(TAG, "No finger"); } - return this->data_[0]; + return send_result; case IMAGE_FAIL: ESP_LOGE(TAG, "Imaging error"); + this->finger_scan_invalid_callback_.call(); + return send_result; default: - return this->data_[0]; + ESP_LOGD(TAG, "Unknown Scan Error: %d", send_result); + return send_result; } ESP_LOGD(TAG, "Processing image %d", buffer); this->data_ = {IMAGE_2_TZ, buffer}; - switch (this->send_command_()) { + send_result = this->send_command_(); + switch (send_result) { case OK: ESP_LOGI(TAG, "Processed image %d", buffer); break; case IMAGE_MESS: ESP_LOGE(TAG, "Image too messy"); + this->finger_scan_invalid_callback_.call(); break; case FEATURE_FAIL: case INVALID_IMAGE: ESP_LOGE(TAG, "Could not find fingerprint features"); + this->finger_scan_invalid_callback_.call(); break; } - return this->data_[0]; + return send_result; } uint8_t FingerprintGrowComponent::save_fingerprint_() { @@ -204,7 +244,7 @@ bool FingerprintGrowComponent::check_password_() { } bool FingerprintGrowComponent::set_password_() { - ESP_LOGI(TAG, "Setting new password: %d", this->new_password_); + ESP_LOGI(TAG, "Setting new password: %" PRIu32, this->new_password_); this->data_ = {SET_PASSWORD, (uint8_t) (this->new_password_ >> 24), (uint8_t) (this->new_password_ >> 16), (uint8_t) (this->new_password_ >> 8), (uint8_t) (this->new_password_ & 0xFF)}; if (this->send_command_() == OK) { @@ -220,10 +260,11 @@ bool FingerprintGrowComponent::get_parameters_() { ESP_LOGD(TAG, "Getting parameters"); this->data_ = {READ_SYS_PARAM}; if (this->send_command_() == OK) { - ESP_LOGD(TAG, "Got parameters"); - if (this->status_sensor_ != nullptr) { + ESP_LOGD(TAG, "Got parameters"); // Bear in mind data_[0] is the transfer status, + if (this->status_sensor_ != nullptr) { // the parameters table start at data_[1] this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); } + this->system_identifier_code_ = ((uint16_t) this->data_[3] << 8) | this->data_[4]; this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6]; if (this->capacity_sensor_ != nullptr) { this->capacity_sensor_->publish_state(this->capacity_); @@ -266,7 +307,7 @@ void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { void FingerprintGrowComponent::delete_all_fingerprints() { ESP_LOGI(TAG, "Deleting all stored fingerprints"); - this->data_ = {EMPTY}; + this->data_ = {DELETE_ALL}; switch (this->send_command_()) { case OK: ESP_LOGI(TAG, "Deleted all fingerprints"); @@ -321,7 +362,9 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui } } -uint8_t FingerprintGrowComponent::send_command_() { +uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) { + while (this->available()) + this->read(); this->write((uint8_t) (START_CODE >> 8)); this->write((uint8_t) (START_CODE & 0xFF)); this->write(this->address_[0]); @@ -330,12 +373,12 @@ uint8_t FingerprintGrowComponent::send_command_() { this->write(this->address_[3]); this->write(COMMAND); - uint16_t wire_length = this->data_.size() + 2; + uint16_t wire_length = p_data_buffer->size() + 2; this->write((uint8_t) (wire_length >> 8)); this->write((uint8_t) (wire_length & 0xFF)); - uint16_t sum = ((wire_length) >> 8) + ((wire_length) &0xFF) + COMMAND; - for (auto data : this->data_) { + uint16_t sum = (wire_length >> 8) + (wire_length & 0xFF) + COMMAND; + for (auto data : *p_data_buffer) { this->write(data); sum += data; } @@ -343,7 +386,7 @@ uint8_t FingerprintGrowComponent::send_command_() { this->write((uint8_t) (sum >> 8)); this->write((uint8_t) (sum & 0xFF)); - this->data_.clear(); + p_data_buffer->clear(); uint8_t byte; uint16_t idx = 0, length = 0; @@ -353,7 +396,9 @@ uint8_t FingerprintGrowComponent::send_command_() { delay(1); continue; } + byte = this->read(); + switch (idx) { case 0: if (byte != (uint8_t) (START_CODE >> 8)) @@ -387,9 +432,9 @@ uint8_t FingerprintGrowComponent::send_command_() { length |= byte; break; default: - this->data_.push_back(byte); + p_data_buffer->push_back(byte); if ((idx - 8) == length) { - switch (this->data_[0]) { + switch ((*p_data_buffer)[0]) { case OK: case NO_FINGER: case IMAGE_FAIL: @@ -409,29 +454,122 @@ uint8_t FingerprintGrowComponent::send_command_() { ESP_LOGE(TAG, "Reader failed to process request"); break; default: - ESP_LOGE(TAG, "Unknown response received from reader: %d", this->data_[0]); + ESP_LOGE(TAG, "Unknown response received from reader: 0x%.2X", (*p_data_buffer)[0]); break; } - return this->data_[0]; + this->last_transfer_ms_ = millis(); + return (*p_data_buffer)[0]; } break; } idx++; } ESP_LOGE(TAG, "No response received from reader"); - this->data_[0] = TIMEOUT; + (*p_data_buffer)[0] = TIMEOUT; + this->last_transfer_ms_ = millis(); return TIMEOUT; } +uint8_t FingerprintGrowComponent::send_command_() { + this->sensor_wakeup_(); + return this->transfer_(&this->data_); +} + +void FingerprintGrowComponent::sensor_wakeup_() { + // Immediately return if there is no power pin or the sensor is already on + if ((!this->has_power_pin_) || (this->is_sensor_awake_)) + return; + + this->sensor_power_pin_->digital_write(true); + this->is_sensor_awake_ = true; + + uint8_t byte = TIMEOUT; + + // Wait for the byte HANDSHAKE_SIGN from the sensor meaning it is operational. + for (uint16_t timer = 0; timer < WAIT_FOR_WAKE_UP_MS; timer++) { + if (this->available() > 0) { + byte = this->read(); + + /* If the received byte is zero, the UART probably misinterpreted a raising edge on + * the RX pin due the power up as byte "zero" - I verified this behaviour using + * the esp32-arduino lib. So here we just ignore this fake byte. + */ + if (byte != 0) + break; + } + delay(1); + } + + /* Lets check if the received by is a HANDSHAKE_SIGN, otherwise log an error + * message and try to continue on the best effort. + */ + if (byte == HANDSHAKE_SIGN) { + ESP_LOGD(TAG, "Sensor has woken up!"); + } else if (byte == TIMEOUT) { + ESP_LOGE(TAG, "Timed out waiting for sensor wake-up"); + } else { + ESP_LOGE(TAG, "Received wrong byte from the sensor during wake-up: 0x%.2X", byte); + } + + /* Next step, we must authenticate with the password. We cannot call check_password_ here + * neither use data_ to store the command because it might be already in use by the caller + * of send_command_() + */ + std::vector buffer = {VERIFY_PASSWORD, (uint8_t) (this->password_ >> 24), (uint8_t) (this->password_ >> 16), + (uint8_t) (this->password_ >> 8), (uint8_t) (this->password_ & 0xFF)}; + + if (this->transfer_(&buffer) != OK) { + ESP_LOGE(TAG, "Wrong password"); + } +} + +void FingerprintGrowComponent::sensor_sleep_() { + // Immediately return if the power pin feature is not implemented + if (!this->has_power_pin_) + return; + + this->sensor_power_pin_->digital_write(false); + this->is_sensor_awake_ = false; + ESP_LOGD(TAG, "Fingerprint sensor is now in sleep mode."); +} + void FingerprintGrowComponent::dump_config() { ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:"); + ESP_LOGCONFIG(TAG, " System Identifier Code: 0x%.4X", this->system_identifier_code_); + ESP_LOGCONFIG(TAG, " Touch Sensing Pin: %s", + this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None"); + ESP_LOGCONFIG(TAG, " Sensor Power Pin: %s", + this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None"); + if (this->idle_period_to_sleep_ms_ < UINT32_MAX) { + ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_); + } else { + ESP_LOGCONFIG(TAG, " Idle Period to Sleep: Never"); + } LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); - LOG_SENSOR(" ", "Status", this->status_sensor_); - LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); - LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); - LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); - LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); + if (this->fingerprint_count_sensor_) { + LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->fingerprint_count_sensor_->get_state()); + } + if (this->status_sensor_) { + LOG_SENSOR(" ", "Status", this->status_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state()); + } + if (this->capacity_sensor_) { + LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->capacity_sensor_->get_state()); + } + if (this->security_level_sensor_) { + LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->security_level_sensor_->get_state()); + } + if (this->last_finger_id_sensor_) { + LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %" PRIu32, (uint32_t) this->last_finger_id_sensor_->get_state()); + } + if (this->last_confidence_sensor_) { + LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %" PRIu32, (uint32_t) this->last_confidence_sensor_->get_state()); + } } } // namespace fingerprint_grow diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index f414146e64..1c3098ef14 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -15,6 +15,11 @@ static const uint16_t START_CODE = 0xEF01; static const uint16_t ENROLLMENT_SLOT_UNUSED = 0xFFFF; +// The datasheet says a max wake up time of of 200ms. +static const uint8_t WAIT_FOR_WAKE_UP_MS = 200; + +static const uint32_t DEFAULT_IDLE_PERIOD_TO_SLEEP_MS = 5000; + enum GrowPacketType { COMMAND = 0x01, DATA = 0x02, @@ -31,7 +36,7 @@ enum GrowCommand { LOAD = 0x07, UPLOAD = 0x08, DELETE = 0x0C, - EMPTY = 0x0D, + DELETE_ALL = 0x0D, // aka EMPTY READ_SYS_PARAM = 0x0F, SET_PASSWORD = 0x12, VERIFY_PASSWORD = 0x13, @@ -63,6 +68,7 @@ enum GrowResponse { INVALID_IMAGE = 0x15, FLASH_ERR = 0x18, INVALID_REG = 0x1A, + HANDSHAKE_SIGN = 0x55, BAD_PACKET = 0xFE, TIMEOUT = 0xFF, }; @@ -99,8 +105,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic this->address_[3] = (uint8_t) (address & 0xFF); } void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } + void set_sensor_power_pin(GPIOPin *sensor_power_pin) { this->sensor_power_pin_ = sensor_power_pin; } void set_password(uint32_t password) { this->password_ = password; } void set_new_password(uint32_t new_password) { this->new_password_ = new_password; } + void set_idle_period_to_sleep_ms(uint32_t period_ms) { this->idle_period_to_sleep_ms_ = period_ms; } void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) { this->fingerprint_count_sensor_ = fingerprint_count_sensor; } @@ -118,12 +126,21 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { this->enrolling_binary_sensor_ = enrolling_binary_sensor; } + void add_on_finger_scan_start_callback(std::function callback) { + this->finger_scan_start_callback_.add(std::move(callback)); + } void add_on_finger_scan_matched_callback(std::function callback) { this->finger_scan_matched_callback_.add(std::move(callback)); } void add_on_finger_scan_unmatched_callback(std::function callback) { this->finger_scan_unmatched_callback_.add(std::move(callback)); } + void add_on_finger_scan_misplaced_callback(std::function callback) { + this->finger_scan_misplaced_callback_.add(std::move(callback)); + } + void add_on_finger_scan_invalid_callback(std::function callback) { + this->finger_scan_invalid_callback_.add(std::move(callback)); + } void add_on_enrollment_scan_callback(std::function callback) { this->enrollment_scan_callback_.add(std::move(callback)); } @@ -151,7 +168,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic bool set_password_(); bool get_parameters_(); void get_fingerprint_count_(); + uint8_t transfer_(std::vector *p_data_buffer); uint8_t send_command_(); + void sensor_wakeup_(); + void sensor_sleep_(); std::vector data_ = {}; uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; @@ -159,12 +179,19 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint32_t password_ = 0x0; uint32_t new_password_ = -1; GPIOPin *sensing_pin_{nullptr}; + GPIOPin *sensor_power_pin_{nullptr}; uint8_t enrollment_image_ = 0; uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; uint8_t enrollment_buffers_ = 5; bool waiting_removal_ = false; + bool has_sensing_pin_ = false; + bool has_power_pin_ = false; + bool is_sensor_awake_ = false; + uint32_t last_transfer_ms_ = 0; uint32_t last_aura_led_control_ = 0; uint16_t last_aura_led_duration_ = 0; + uint16_t system_identifier_code_ = 0; + uint32_t idle_period_to_sleep_ms_ = UINT32_MAX; sensor::Sensor *fingerprint_count_sensor_{nullptr}; sensor::Sensor *status_sensor_{nullptr}; sensor::Sensor *capacity_sensor_{nullptr}; @@ -172,13 +199,23 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic sensor::Sensor *last_finger_id_sensor_{nullptr}; sensor::Sensor *last_confidence_sensor_{nullptr}; binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; + CallbackManager finger_scan_invalid_callback_; + CallbackManager finger_scan_start_callback_; CallbackManager finger_scan_matched_callback_; CallbackManager finger_scan_unmatched_callback_; + CallbackManager finger_scan_misplaced_callback_; CallbackManager enrollment_scan_callback_; CallbackManager enrollment_done_callback_; CallbackManager enrollment_failed_callback_; }; +class FingerScanStartTrigger : public Trigger<> { + public: + explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_start_callback([this]() { this->trigger(); }); + } +}; + class FingerScanMatchedTrigger : public Trigger { public: explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { @@ -194,6 +231,20 @@ class FingerScanUnmatchedTrigger : public Trigger<> { } }; +class FingerScanMisplacedTrigger : public Trigger<> { + public: + explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); }); + } +}; + +class FingerScanInvalidTrigger : public Trigger<> { + public: + explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); }); + } +}; + class EnrollmentScanTrigger : public Trigger { public: explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index aa165ebaa5..6fd2d7c310 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,62 +1,180 @@ +from collections.abc import Iterable import functools -from pathlib import Path import hashlib +import logging import os +from pathlib import Path import re +import freetype +import glyphsets +from packaging import version import requests -from esphome import core -from esphome.components import display -import esphome.config_validation as cv +from esphome import core, external_files import esphome.codegen as cg -from esphome.helpers import copy_file_if_changed +import esphome.config_validation as cv from esphome.const import ( CONF_FAMILY, CONF_FILE, CONF_GLYPHS, CONF_ID, - CONF_RAW_DATA_ID, - CONF_TYPE, - CONF_SIZE, CONF_PATH, + CONF_RAW_DATA_ID, + CONF_REFRESH, + CONF_SIZE, + CONF_TYPE, + CONF_URL, CONF_WEIGHT, ) from esphome.core import CORE, HexInt +from esphome.helpers import copy_file_if_changed, cpp_string_escape +_LOGGER = logging.getLogger(__name__) DOMAIN = "font" -DEPENDENCIES = ["display"] MULTI_CONF = True -Font = display.display_ns.class_("Font") -Glyph = display.display_ns.class_("Glyph") -GlyphData = display.display_ns.struct("GlyphData") +CODEOWNERS = ["@esphome/core", "@clydebarrow"] + +font_ns = cg.esphome_ns.namespace("font") + +Font = font_ns.class_("Font") +Glyph = font_ns.class_("Glyph") +GlyphData = font_ns.struct("GlyphData") + +CONF_BPP = "bpp" +CONF_EXTRAS = "extras" +CONF_FONTS = "fonts" +CONF_GLYPHSETS = "glyphsets" +CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs" -def validate_glyphs(value): - if isinstance(value, list): - value = cv.Schema([cv.string])(value) - value = cv.Schema([cv.string])(list(value)) +# Cache loaded freetype fonts +class FontCache(dict): + def __missing__(self, key): + res = self[key] = freetype.Face(key) + return res - def comparator(x, y): - x_ = x.encode("utf-8") - y_ = y.encode("utf-8") - for c in range(min(len(x_), len(y_))): - if x_[c] < y_[c]: - return -1 - if x_[c] > y_[c]: - return 1 +FONT_CACHE = FontCache() - if len(x_) < len(y_): + +def glyph_comparator(x, y): + x_ = x.encode("utf-8") + y_ = y.encode("utf-8") + + for c in range(min(len(x_), len(y_))): + if x_[c] < y_[c]: return -1 - if len(x_) > len(y_): + if x_[c] > y_[c]: return 1 - raise cv.Invalid(f"Found duplicate glyph {x}") - value.sort(key=functools.cmp_to_key(comparator)) - return value + if len(x_) < len(y_): + return -1 + if len(x_) > len(y_): + return 1 + return 0 + + +def flatten(lists) -> list: + """ + Given a list of lists, flatten it to a single list of all elements of all lists. + This wraps itertools.chain.from_iterable to make it more readable, and return a list + rather than a single use iterable. + """ + from itertools import chain + + return list(chain.from_iterable(lists)) + + +def check_missing_glyphs(file, codepoints: Iterable, warning: bool = False): + """ + Check that the given font file actually contains the requested glyphs + :param file: A Truetype font file + :param codepoints: A list of codepoints to check + :param warning: If true, log a warning instead of raising an exception + """ + + font = FONT_CACHE[file] + missing = [chr(x) for x in codepoints if font.get_char_index(x) == 0] + if missing: + # Only list up to 10 missing glyphs + missing.sort(key=functools.cmp_to_key(glyph_comparator)) + count = len(missing) + missing = missing[:10] + missing_str = "\n ".join( + f"{x} ({x.encode('unicode_escape')})" for x in missing + ) + if count > 10: + missing_str += f"\n and {count - 10} more." + message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}" + if warning: + _LOGGER.warning(message) + else: + raise cv.Invalid(message) + + +def validate_glyphs(config): + """ + Check for duplicate codepoints, then check that all requested codepoints actually + have glyphs defined in the appropriate font file. + """ + + # Collect all glyph codepoints and flatten to a list of chars + glyphspoints = flatten( + [x[CONF_GLYPHS] for x in config[CONF_EXTRAS]] + config[CONF_GLYPHS] + ) + # Convert a list of strings to a list of chars (one char strings) + glyphspoints = flatten([list(x) for x in glyphspoints]) + if len(set(glyphspoints)) != len(glyphspoints): + duplicates = {x for x in glyphspoints if glyphspoints.count(x) > 1} + dup_str = ", ".join(f"{x} ({x.encode('unicode_escape')})" for x in duplicates) + raise cv.Invalid( + f"Found duplicate glyph{'s' if len(duplicates) != 1 else ''}: {dup_str}" + ) + # convert to codepoints + glyphspoints = {ord(x) for x in glyphspoints} + fileconf = config[CONF_FILE] + setpoints = set( + flatten([glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]]) + ) + # Make setpoints and glyphspoints disjoint + setpoints.difference_update(glyphspoints) + if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP: + # Pillow only allows 256 glyphs per bitmap font. Not sure if that is a Pillow limitation + # or a file format limitation + if any(x >= 256 for x in setpoints.copy().union(glyphspoints)): + raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255") + else: + # for TT fonts, check that glyphs are actually present + # Check extras against their own font, exclude from parent font codepoints + for extra in config[CONF_EXTRAS]: + points = {ord(x) for x in flatten(extra[CONF_GLYPHS])} + glyphspoints.difference_update(points) + setpoints.difference_update(points) + check_missing_glyphs(extra[CONF_FILE][CONF_PATH], points) + + # A named glyph that can't be provided is an error + check_missing_glyphs(fileconf[CONF_PATH], glyphspoints) + # A missing glyph from a set is a warning. + if not config[CONF_IGNORE_MISSING_GLYPHS]: + check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True) + + # Populate the default after the above checks so that use of the default doesn't trigger errors + if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]: + if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP: + config[CONF_GLYPHS] = [DEFAULT_GLYPHS] + else: + # set a default glyphset, intersected with what the font actually offers + font = FONT_CACHE[fileconf[CONF_PATH]] + config[CONF_GLYPHS] = [ + chr(x) + for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET) + if font.get_char_index(x) != 0 + ] + + return config def validate_pillow_installed(value): @@ -65,46 +183,35 @@ def validate_pillow_installed(value): except ImportError as err: raise cv.Invalid( "Please install the pillow python package to use this feature. " - "(pip install pillow)" + '(pip install "pillow==10.4.0")' ) from err - if PIL.__version__[0] < "4": + if version.parse(PIL.__version__) != version.parse("10.4.0"): raise cv.Invalid( - "Please update your pillow installation to at least 4.0.x. " - "(pip install -U pillow)" + "Please update your pillow installation to 10.4.0. " + '(pip install "pillow==10.4.0")' ) return value +FONT_EXTENSIONS = (".ttf", ".woff", ".otf") + + def validate_truetype_file(value): - if value.endswith(".zip"): # for Google Fonts downloads + if value.lower().endswith(".zip"): # for Google Fonts downloads raise cv.Invalid( f"Please unzip the font archive '{value}' first and then use the .ttf files inside." ) - if not value.endswith(".ttf"): - raise cv.Invalid( - "Only truetype (.ttf) files are supported. Please make sure you're " - "using the correct format or rename the extension to .ttf" - ) - return cv.file_(value) - - -def _compute_local_font_dir(name) -> Path: - base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN - h = hashlib.new("sha256") - h.update(name.encode()) - return base_dir / h.hexdigest()[:8] - - -def _compute_gfonts_local_path(value) -> Path: - name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" - return _compute_local_font_dir(name) / "font.ttf" + if not any(map(value.lower().endswith, FONT_EXTENSIONS)): + raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.") + return CORE.relative_config_path(cv.file_(value)) TYPE_LOCAL = "local" TYPE_LOCAL_BITMAP = "local_bitmap" TYPE_GFONTS = "gfonts" +TYPE_WEB = "web" LOCAL_SCHEMA = cv.Schema( { cv.Required(CONF_PATH): validate_truetype_file, @@ -117,6 +224,10 @@ LOCAL_BITMAP_SCHEMA = cv.Schema( } ) +FULLPATH_SCHEMA = cv.maybe_simple_value( + {cv.Required(CONF_PATH): cv.string}, key=CONF_PATH +) + CONF_ITALIC = "italic" FONT_WEIGHTS = { "thin": 100, @@ -135,22 +246,39 @@ def validate_weight_name(value): return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)] -def download_gfonts(value): - wght = value[CONF_WEIGHT] - if value[CONF_ITALIC]: - wght = f"1,{wght}" - name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}" - url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}" +def _compute_local_font_path(value: dict) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + _LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key) + return base_dir / key + + +def get_font_path(value, font_type) -> Path: + if font_type == TYPE_GFONTS: + name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" + return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf" + if font_type == TYPE_WEB: + return _compute_local_font_path(value) / "font.ttf" + assert False + + +def download_gfont(value): + name = ( + f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}" + ) + url = f"https://fonts.googleapis.com/css2?family={name}" + path = get_font_path(value, TYPE_GFONTS) + _LOGGER.debug("download_gfont: path=%s", path) - path = _compute_gfonts_local_path(value) - if path.is_file(): - return value try: - req = requests.get(url, timeout=30) + req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT) req.raise_for_status() except requests.exceptions.RequestException as e: raise cv.Invalid( - f"Could not download font for {name}, please check the fonts exists " + f"Could not download font at {url}, please check the fonts exists " f"at google fonts ({e})" ) match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text) @@ -161,26 +289,47 @@ def download_gfonts(value): ) ttf_url = match.group(1) - try: - req = requests.get(ttf_url, timeout=30) - req.raise_for_status() - except requests.exceptions.RequestException as e: - raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}") + _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url) - path.parent.mkdir(exist_ok=True, parents=True) - path.write_bytes(req.content) - return value + external_files.download_content(ttf_url, path) + return FULLPATH_SCHEMA(path) -GFONTS_SCHEMA = cv.All( +def download_web_font(value): + url = value[CONF_URL] + path = get_font_path(value, TYPE_WEB) + + external_files.download_content(url, path) + _LOGGER.debug("download_web_font: path=%s", path) + return FULLPATH_SCHEMA(path) + + +EXTERNAL_FONT_SCHEMA = cv.Schema( { - cv.Required(CONF_FAMILY): cv.string_strict, cv.Optional(CONF_WEIGHT, default="regular"): cv.Any( cv.int_, validate_weight_name ), cv.Optional(CONF_ITALIC, default=False): cv.boolean, - }, - download_gfonts, + cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh), + } +) + +GFONTS_SCHEMA = cv.All( + EXTERNAL_FONT_SCHEMA.extend( + { + cv.Required(CONF_FAMILY): cv.string_strict, + } + ), + download_gfont, +) + +WEB_FONT_SCHEMA = cv.All( + EXTERNAL_FONT_SCHEMA.extend( + { + cv.Required(CONF_URL): cv.string_strict, + } + ), + download_web_font, ) @@ -198,17 +347,26 @@ def validate_file_shorthand(value): } if weight is not None: data[CONF_WEIGHT] = weight[1:] - return FILE_SCHEMA(data) + return font_file_schema(data) - if value.endswith(".pcf") or value.endswith(".bdf"): - return FILE_SCHEMA( + if value.startswith("http://") or value.startswith("https://"): + return font_file_schema( { - CONF_TYPE: TYPE_LOCAL_BITMAP, - CONF_PATH: value, + CONF_TYPE: TYPE_WEB, + CONF_URL: value, } ) - return FILE_SCHEMA( + if value.endswith(".pcf") or value.endswith(".bdf"): + value = convert_bitmap_to_pillow_font( + CORE.relative_config_path(cv.file_(value)) + ) + return { + CONF_TYPE: TYPE_LOCAL_BITMAP, + CONF_PATH: value, + } + + return font_file_schema( { CONF_TYPE: TYPE_LOCAL, CONF_PATH: value, @@ -221,36 +379,50 @@ TYPED_FILE_SCHEMA = cv.typed_schema( TYPE_LOCAL: LOCAL_SCHEMA, TYPE_GFONTS: GFONTS_SCHEMA, TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA, + TYPE_WEB: WEB_FONT_SCHEMA, } ) -def _file_schema(value): +def font_file_schema(value): if isinstance(value, str): return validate_file_shorthand(value) return TYPED_FILE_SCHEMA(value) -FILE_SCHEMA = cv.Schema(_file_schema) +# Default if no glyphs or glyphsets are provided +DEFAULT_GLYPHSET = "GF_Latin_Kernel" +# default for bitmap fonts +DEFAULT_GLYPHS = ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz' - -DEFAULT_GLYPHS = ( - ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' -) CONF_RAW_GLYPH_ID = "raw_glyph_id" FONT_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.declare_id(Font), - cv.Required(CONF_FILE): FILE_SCHEMA, - cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, + cv.Required(CONF_FILE): font_file_schema, + cv.Optional(CONF_GLYPHS, default=[]): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_GLYPHSETS, default=[]): cv.ensure_list( + cv.one_of(*glyphsets.defined_glyphsets()) + ), + cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean, cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), + cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8), + cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_FILE): font_file_schema, + cv.Required(CONF_GLYPHS): cv.ensure_list(cv.string_strict), + } + ) + ), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData), - } + }, ) -CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA) +CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, validate_glyphs) + # PIL doesn't provide a consistent interface for both TrueType and bitmap # fonts. So, we use our own wrappers to give us the consistency that we need. @@ -265,7 +437,7 @@ class TrueTypeFontWrapper: return offset_x, offset_y def getmask(self, glyph, **kwargs): - return self.font.getmask(glyph, **kwargs) + return self.font.getmask(str(glyph), **kwargs) def getmetrics(self, glyphs): return self.font.getmetrics() @@ -280,27 +452,40 @@ class BitmapFontWrapper: return 0, 0 def getmask(self, glyph, **kwargs): - return self.font.getmask(glyph, **kwargs) + return self.font.getmask(str(glyph), **kwargs) def getmetrics(self, glyphs): max_height = 0 for glyph in glyphs: mask = self.getmask(glyph, mode="1") _, height = mask.size - if height > max_height: - max_height = height - return (max_height, 0) + max_height = max(max_height, height) + return max_height, 0 + + +class EFont: + def __init__(self, file, size, codepoints): + self.codepoints = codepoints + path = file[CONF_PATH] + self.name = Path(path).name + ftype = file[CONF_TYPE] + if ftype == TYPE_LOCAL_BITMAP: + self.font = load_bitmap_font(path) + else: + self.font = load_ttf_font(path, size) + self.ascent, self.descent = self.font.getmetrics(codepoints) def convert_bitmap_to_pillow_font(filepath): - from PIL import PcfFontFile, BdfFontFile + from PIL import BdfFontFile, PcfFontFile - local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename( - filepath - ) + local_bitmap_font_file = external_files.compute_local_file_dir( + DOMAIN, + ) / os.path.basename(filepath) copy_file_if_changed(filepath, local_bitmap_font_file) + local_pil_font_file = local_bitmap_font_file.with_suffix(".pil") with open(local_bitmap_font_file, "rb") as fp: try: try: @@ -310,28 +495,22 @@ def convert_bitmap_to_pillow_font(filepath): p = BdfFontFile.BdfFontFile(fp) # Convert to pillow-formatted fonts, which have a .pil and .pbm extension. - p.save(local_bitmap_font_file) + p.save(local_pil_font_file) except (SyntaxError, OSError) as err: raise core.EsphomeError( f"Failed to parse as bitmap font: '{filepath}': {err}" ) - local_pil_font_file = os.path.splitext(local_bitmap_font_file)[0] + ".pil" - return cv.file_(local_pil_font_file) + return str(local_pil_font_file) def load_bitmap_font(filepath): from PIL import ImageFont - # Convert bpf and pcf files to pillow fonts, first. - pil_font_path = convert_bitmap_to_pillow_font(filepath) - try: - font = ImageFont.load(str(pil_font_path)) + font = ImageFont.load(str(filepath)) except Exception as e: - raise core.EsphomeError( - f"Failed to load bitmap font file: {pil_font_path} : {e}" - ) + raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}") return BitmapFontWrapper(font) @@ -342,65 +521,112 @@ def load_ttf_font(path, size): try: font = ImageFont.truetype(str(path), size) except Exception as e: - raise core.EsphomeError(f"Could not load truetype file {path}: {e}") + raise core.EsphomeError(f"Could not load TrueType file {path}: {e}") return TrueTypeFontWrapper(font) +class GlyphInfo: + def __init__(self, data_len, offset_x, offset_y, width, height): + self.data_len = data_len + self.offset_x = offset_x + self.offset_y = offset_y + self.width = width + self.height = height + + async def to_code(config): - conf = config[CONF_FILE] - if conf[CONF_TYPE] == TYPE_LOCAL_BITMAP: - font = load_bitmap_font(CORE.relative_config_path(conf[CONF_PATH])) - elif conf[CONF_TYPE] == TYPE_LOCAL: - path = CORE.relative_config_path(conf[CONF_PATH]) - font = load_ttf_font(path, config[CONF_SIZE]) - elif conf[CONF_TYPE] == TYPE_GFONTS: - path = _compute_gfonts_local_path(conf) - font = load_ttf_font(path, config[CONF_SIZE]) - else: - raise core.EsphomeError(f"Could not load font: unknown type: {conf[CONF_TYPE]}") + """ + Collect all glyph codepoints, construct a map from a codepoint to a font file. + Codepoints are either explicit (glyphs key in top level or extras) or part of a glyphset. + Codepoints listed in extras use the extra font and override codepoints from glyphsets. + Achieve this by processing the base codepoints first, then the extras + """ - ascent, descent = font.getmetrics(config[CONF_GLYPHS]) + # get the codepoints from glyphsets and flatten to a set of chrs. + point_set: set[str] = { + chr(x) + for x in flatten( + [glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]] + ) + } + # get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets + point_set.update(flatten(config[CONF_GLYPHS])) + size = config[CONF_SIZE] + # Create the codepoint to font file map + base_font = EFont(config[CONF_FILE], size, point_set) + point_font_map: dict[str, EFont] = {c: base_font for c in point_set} + # process extras, updating the map and extending the codepoint list + for extra in config[CONF_EXTRAS]: + extra_points = flatten(extra[CONF_GLYPHS]) + point_set.update(extra_points) + extra_font = EFont(extra[CONF_FILE], size, extra_points) + point_font_map.update({c: extra_font for c in extra_points}) + codepoints = list(point_set) + codepoints.sort(key=functools.cmp_to_key(glyph_comparator)) glyph_args = {} data = [] - for glyph in config[CONF_GLYPHS]: - mask = font.getmask(glyph, mode="1") - offset_x, offset_y = font.getoffset(glyph) + bpp = config[CONF_BPP] + if bpp == 1: + mode = "1" + scale = 1 + else: + mode = "L" + scale = 256 // (1 << bpp) + # create the data array for all glyphs + for codepoint in codepoints: + font = point_font_map[codepoint] + mask = font.font.getmask(codepoint, mode=mode) + offset_x, offset_y = font.font.getoffset(codepoint) width, height = mask.size - width8 = ((width + 7) // 8) * 8 - glyph_data = [0] * (height * width8 // 8) + glyph_data = [0] * ((height * width * bpp + 7) // 8) + pos = 0 for y in range(height): for x in range(width): - if not mask.getpixel((x, y)): - continue - pos = x + y * width8 - glyph_data[pos // 8] |= 0x80 >> (pos % 8) - glyph_args[glyph] = (len(data), offset_x, offset_y, width, height) + pixel = mask.getpixel((x, y)) // scale + for bit_num in range(bpp): + if pixel & (1 << (bpp - bit_num - 1)): + glyph_data[pos // 8] |= 0x80 >> (pos % 8) + pos += 1 + glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height) data += glyph_data rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + # Create the glyph table that points to data in the above array. glyph_initializer = [] - for glyph in config[CONF_GLYPHS]: + for codepoint in codepoints: glyph_initializer.append( cg.StructInitializer( GlyphData, - ("a_char", glyph), + ( + "a_char", + cg.RawExpression( + f"(const uint8_t *){cpp_string_escape(codepoint)}" + ), + ), ( "data", - cg.RawExpression(f"{str(prog_arr)} + {str(glyph_args[glyph][0])}"), + cg.RawExpression( + f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" + ), ), - ("offset_x", glyph_args[glyph][1]), - ("offset_y", glyph_args[glyph][2]), - ("width", glyph_args[glyph][3]), - ("height", glyph_args[glyph][4]), + ("offset_x", glyph_args[codepoint].offset_x), + ("offset_y", glyph_args[codepoint].offset_y), + ("width", glyph_args[codepoint].width), + ("height", glyph_args[codepoint].height), ) ) glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) cg.new_Pvariable( - config[CONF_ID], glyphs, len(glyph_initializer), ascent, ascent + descent + config[CONF_ID], + glyphs, + len(glyph_initializer), + base_font.ascent, + base_font.ascent + base_font.descent, + bpp, ) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp new file mode 100644 index 0000000000..aeca0f5cc0 --- /dev/null +++ b/esphome/components/font/font.cpp @@ -0,0 +1,170 @@ +#include "font.h" + +#include "esphome/core/color.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace font { + +static const char *const TAG = "font"; + +const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; } +// Compare the char at the string position with this char. +// Return true if this char is less than or equal the other. +bool Glyph::compare_to(const uint8_t *str) const { + // 1 -> this->char_ + // 2 -> str + for (uint32_t i = 0;; i++) { + if (this->glyph_data_->a_char[i] == '\0') + return true; + if (str[i] == '\0') + return false; + if (this->glyph_data_->a_char[i] > str[i]) + return false; + if (this->glyph_data_->a_char[i] < str[i]) + return true; + } + // this should not happen + return false; +} +int Glyph::match_length(const uint8_t *str) const { + for (uint32_t i = 0;; i++) { + if (this->glyph_data_->a_char[i] == '\0') + return i; + if (str[i] != this->glyph_data_->a_char[i]) + return 0; + } + // this should not happen + return 0; +} +void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { + *x1 = this->glyph_data_->offset_x; + *y1 = this->glyph_data_->offset_y; + *width = this->glyph_data_->width; + *height = this->glyph_data_->height; +} + +Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp) + : baseline_(baseline), height_(height), bpp_(bpp) { + glyphs_.reserve(data_nr); + for (int i = 0; i < data_nr; ++i) + glyphs_.emplace_back(&data[i]); +} +int Font::match_next_glyph(const uint8_t *str, int *match_length) { + int lo = 0; + int hi = this->glyphs_.size() - 1; + while (lo != hi) { + int mid = (lo + hi + 1) / 2; + if (this->glyphs_[mid].compare_to(str)) { + lo = mid; + } else { + hi = mid - 1; + } + } + *match_length = this->glyphs_[lo].match_length(str); + if (*match_length <= 0) + return -1; + return lo; +} +#ifdef USE_DISPLAY +void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { + *baseline = this->baseline_; + *height = this->height_; + int i = 0; + int min_x = 0; + bool has_char = false; + int x = 0; + while (str[i] != '\0') { + int match_length; + int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length); + if (glyph_n < 0) { + // Unknown char, skip + if (!this->get_glyphs().empty()) + x += this->get_glyphs()[0].glyph_data_->width; + i++; + continue; + } + + const Glyph &glyph = this->glyphs_[glyph_n]; + if (!has_char) { + min_x = glyph.glyph_data_->offset_x; + } else { + min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); + } + x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; + + i += match_length; + has_char = true; + } + *x_offset = min_x; + *width = x - min_x; +} +void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) { + int i = 0; + int x_at = x_start; + int scan_x1, scan_y1, scan_width, scan_height; + while (text[i] != '\0') { + int match_length; + int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length); + if (glyph_n < 0) { + // Unknown char, skip + ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); + if (!this->get_glyphs().empty()) { + uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width; + display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); + x_at += glyph_width; + } + + i++; + continue; + } + + const Glyph &glyph = this->get_glyphs()[glyph_n]; + glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); + + const uint8_t *data = glyph.glyph_data_->data; + const int max_x = x_at + scan_x1 + scan_width; + const int max_y = y_start + scan_y1 + scan_height; + + uint8_t bitmask = 0; + uint8_t pixel_data = 0; + uint8_t bpp_max = (1 << this->bpp_) - 1; + auto diff_r = (float) color.r - (float) background.r; + auto diff_g = (float) color.g - (float) background.g; + auto diff_b = (float) color.b - (float) background.b; + auto b_r = (float) background.r; + auto b_g = (float) background.g; + auto b_b = (float) background.g; + for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) { + for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { + uint8_t pixel = 0; + for (int bit_num = 0; bit_num != this->bpp_; bit_num++) { + if (bitmask == 0) { + pixel_data = progmem_read_byte(data++); + bitmask = 0x80; + } + pixel <<= 1; + if ((pixel_data & bitmask) != 0) + pixel |= 1; + bitmask >>= 1; + } + if (pixel == bpp_max) { + display->draw_pixel_at(glyph_x, glyph_y, color); + } else if (pixel != 0) { + auto on = (float) pixel / (float) bpp_max; + auto blended = + Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g), (uint8_t) (diff_b * on + b_b)); + display->draw_pixel_at(glyph_x, glyph_y, blended); + } + } + } + x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; + + i += match_length; + } +} +#endif + +} // namespace font +} // namespace esphome diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h new file mode 100644 index 0000000000..5cde694d91 --- /dev/null +++ b/esphome/components/font/font.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/core/color.h" +#include "esphome/core/datatypes.h" +#include "esphome/core/defines.h" +#ifdef USE_DISPLAY +#include "esphome/components/display/display.h" +#endif + +namespace esphome { +namespace font { + +class Font; + +struct GlyphData { + const uint8_t *a_char; + const uint8_t *data; + int offset_x; + int offset_y; + int width; + int height; +}; + +class Glyph { + public: + Glyph(const GlyphData *data) : glyph_data_(data) {} + + const uint8_t *get_char() const; + + bool compare_to(const uint8_t *str) const; + + int match_length(const uint8_t *str) const; + + void scan_area(int *x1, int *y1, int *width, int *height) const; + + const GlyphData *get_glyph_data() const { return this->glyph_data_; } + + protected: + friend Font; + + const GlyphData *glyph_data_; +}; + +class Font +#ifdef USE_DISPLAY + : public display::BaseFont +#endif +{ + public: + /** Construct the font with the given glyphs. + * + * @param glyphs A vector of glyphs, must be sorted lexicographically. + * @param baseline The y-offset from the top of the text to the baseline. + * @param bottom The y-offset from the top of the text to the bottom (i.e. height). + */ + Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1); + + int match_next_glyph(const uint8_t *str, int *match_length); + +#ifdef USE_DISPLAY + void print(int x_start, int y_start, display::Display *display, Color color, const char *text, + Color background) override; + void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override; +#endif + inline int get_baseline() { return this->baseline_; } + inline int get_height() { return this->height_; } + inline int get_bpp() { return this->bpp_; } + + const std::vector> &get_glyphs() const { return glyphs_; } + + protected: + std::vector> glyphs_; + int baseline_; + int height_; + uint8_t bpp_; // bits per pixel +}; + +} // namespace font +} // namespace esphome diff --git a/esphome/components/ft5x06/__init__.py b/esphome/components/ft5x06/__init__.py new file mode 100644 index 0000000000..dceea71dd0 --- /dev/null +++ b/esphome/components/ft5x06/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +ft5x06_ns = cg.esphome_ns.namespace("ft5x06") diff --git a/esphome/components/ft5x06/touchscreen/__init__.py b/esphome/components/ft5x06/touchscreen/__init__.py new file mode 100644 index 0000000000..4ceb50c709 --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/__init__.py @@ -0,0 +1,32 @@ +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN +from .. import ft5x06_ns + +FT5x06ButtonListener = ft5x06_ns.class_("FT5x06ButtonListener") +FT5x06Touchscreen = ft5x06_ns.class_( + "FT5x06Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FT5x06Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + } +).extend(i2c.i2c_device_schema(0x48)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await i2c.register_i2c_device(var, config) + await touchscreen.register_touchscreen(var, config) + + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + pin = await cg.gpio_pin_expression(interrupt_pin) + cg.add(var.set_interrupt_pin(pin)) diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp new file mode 100644 index 0000000000..bd603fdc10 --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp @@ -0,0 +1,102 @@ +#include "ft5x06_touchscreen.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace ft5x06 { + +static const char *const TAG = "ft5x06.touchscreen"; + +void FT5x06Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up FT5x06 Touchscreen..."); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + + // wait 200ms after reset. + this->set_timeout(200, [this] { this->continue_setup_(); }); +} + +void FT5x06Touchscreen::continue_setup_() { + uint8_t data[4]; + if (!this->set_mode_(FT5X06_OP_MODE)) + return; + + if (!this->err_check_(this->read_register(FT5X06_VENDOR_ID_REG, data, 1), "Read Vendor ID")) + return; + switch (data[0]) { + case FT5X06_ID_1: + case FT5X06_ID_2: + case FT5X06_ID_3: + this->vendor_id_ = (VendorId) data[0]; + ESP_LOGD(TAG, "Read vendor ID 0x%X", data[0]); + break; + + default: + ESP_LOGE(TAG, "Unknown vendor ID 0x%X", data[0]); + this->mark_failed(); + return; + } + // reading the chip registers to get max x/y does not seem to work. + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } + } + ESP_LOGCONFIG(TAG, "FT5x06 Touchscreen setup complete"); +} + +void FT5x06Touchscreen::update_touches() { + uint8_t touch_cnt; + uint8_t data[MAX_TOUCHES][6]; + + if (!this->read_byte(FT5X06_TD_STATUS, &touch_cnt) || touch_cnt > MAX_TOUCHES) { + ESP_LOGW(TAG, "Failed to read status"); + return; + } + if (touch_cnt == 0) + return; + + if (!this->read_bytes(FT5X06_TOUCH_DATA, (uint8_t *) data, touch_cnt * 6)) { + ESP_LOGW(TAG, "Failed to read touch data"); + return; + } + for (uint8_t i = 0; i != touch_cnt; i++) { + uint8_t status = data[i][0] >> 6; + uint8_t id = data[i][2] >> 3; + uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]); + uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]); + + ESP_LOGD(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); + if (status == 0 || status == 2) { + this->add_raw_touch_position_(id, x, y); + } + } +} + +void FT5x06Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "FT5x06 Touchscreen:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + ESP_LOGCONFIG(TAG, " Vendor ID: 0x%X", (int) this->vendor_id_); +} + +bool FT5x06Touchscreen::err_check_(i2c::ErrorCode err, const char *msg) { + if (err != i2c::ERROR_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "%s failed - err 0x%X", msg, err); + return false; + } + return true; +} +bool FT5x06Touchscreen::set_mode_(FTMode mode) { + return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode"); +} + +} // namespace ft5x06 +} // namespace esphome diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h new file mode 100644 index 0000000000..23e5a0c49f --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -0,0 +1,56 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ft5x06 { + +enum VendorId { + FT5X06_ID_UNKNOWN = 0, + FT5X06_ID_1 = 0x51, + FT5X06_ID_2 = 0x11, + FT5X06_ID_3 = 0xCD, +}; + +enum FTCmd : uint8_t { + FT5X06_MODE_REG = 0x00, + FT5X06_ORIGIN_REG = 0x08, + FT5X06_RESOLUTION_REG = 0x0C, + FT5X06_VENDOR_ID_REG = 0xA8, + FT5X06_TD_STATUS = 0x02, + FT5X06_TOUCH_DATA = 0x03, + FT5X06_I_MODE = 0xA4, + FT5X06_TOUCH_MAX = 0x4C, +}; + +enum FTMode : uint8_t { + FT5X06_OP_MODE = 0, + FT5X06_SYSINFO_MODE = 0x10, + FT5X06_TEST_MODE = 0x40, +}; + +static const size_t MAX_TOUCHES = 5; // max number of possible touches reported + +class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update_touches() override; + + void set_interrupt_pin(InternalGPIOPin *interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } + + protected: + void continue_setup_(); + bool err_check_(i2c::ErrorCode err, const char *msg); + bool set_mode_(FTMode mode); + VendorId vendor_id_{FT5X06_ID_UNKNOWN}; + + InternalGPIOPin *interrupt_pin_{nullptr}; +}; + +} // namespace ft5x06 +} // namespace esphome diff --git a/esphome/components/ft63x6/__init__.py b/esphome/components/ft63x6/__init__.py new file mode 100644 index 0000000000..b6d7d3580e --- /dev/null +++ b/esphome/components/ft63x6/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gpambrozio"] diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp new file mode 100644 index 0000000000..e5f7613901 --- /dev/null +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -0,0 +1,134 @@ +/**************************************************************************/ +/*! + Author: Gustavo Ambrozio + Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) +*/ +/**************************************************************************/ + +#include "ft63x6.h" +#include "esphome/core/log.h" + +// Registers +// Reference: https://focuslcds.com/content/FT6236.pdf +namespace esphome { +namespace ft63x6 { +static const uint8_t FT6X36_ADDR_DEVICE_MODE = 0x00; + +static const uint8_t FT63X6_ADDR_TD_STATUS = 0x02; +static const uint8_t FT63X6_ADDR_TOUCH1_STATE = 0x03; +static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03; +static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05; +static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05; +static const uint8_t FT63X6_ADDR_TOUCH1_WEIGHT = 0x07; +static const uint8_t FT63X6_ADDR_TOUCH1_MISC = 0x08; +static const uint8_t FT6X36_ADDR_THRESHHOLD = 0x80; +static const uint8_t FT6X36_ADDR_TOUCHRATE_ACTIVE = 0x88; +static const uint8_t FT63X6_ADDR_CHIP_ID = 0xA3; + +static const char *const TAG = "FT63X6"; + +void FT63X6Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up FT63X6 Touchscreen..."); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_ANY_EDGE); + } + + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->hard_reset_(); + } + + // Get touch resolution + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = 320; + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = 480; + } + uint8_t chip_id = this->read_byte_(FT63X6_ADDR_CHIP_ID); + if (chip_id != 0) { + ESP_LOGI(TAG, "FT6336U touch driver started chipid: %d", chip_id); + } else { + ESP_LOGE(TAG, "FT6336U touch driver failed to start"); + } + this->write_byte(FT6X36_ADDR_DEVICE_MODE, 0x00); + this->write_byte(FT6X36_ADDR_THRESHHOLD, this->threshold_); + this->write_byte(FT6X36_ADDR_TOUCHRATE_ACTIVE, 0x0E); +} + +void FT63X6Touchscreen::hard_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + } +} + +void FT63X6Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void FT63X6Touchscreen::update_touches() { + uint16_t touch_id, x, y; + + uint8_t touches = this->read_touch_number_(); + ESP_LOGV(TAG, "Touches found: %d", touches); + if ((touches == 0x00) || (touches == 0xff)) { + // ESP_LOGD(TAG, "No touches detected"); + return; + } + + for (auto point = 0; point < touches; point++) { + if (((this->read_touch_event_(point)) & 0x01) == 0) { // checking event flag bit 6 if it is null + touch_id = this->read_touch_id_(point); // id1 = 0 or 1 + x = this->read_touch_x_(point); + y = this->read_touch_y_(point); + if ((x == 0) && (y == 0)) { + ESP_LOGW(TAG, "Reporting a (0,0) touch on %d", touch_id); + } + this->add_raw_touch_position_(touch_id, x, y, this->read_touch_weight_(point)); + } + } +} + +uint8_t FT63X6Touchscreen::read_touch_number_() { return this->read_byte_(FT63X6_ADDR_TD_STATUS) & 0x0F; } +// Touch 1 functions +uint16_t FT63X6Touchscreen::read_touch_x_(uint8_t touch) { + uint8_t read_buf[2]; + read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6)); + read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + 1 + (touch * 6)); + return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; +} +uint16_t FT63X6Touchscreen::read_touch_y_(uint8_t touch) { + uint8_t read_buf[2]; + read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + (touch * 6)); + read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + 1 + (touch * 6)); + return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; +} +uint8_t FT63X6Touchscreen::read_touch_event_(uint8_t touch) { + return this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6)) >> 6; +} +uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t touch) { + return this->read_byte_(FT63X6_ADDR_TOUCH1_ID + (touch * 6)) >> 4; +} +uint8_t FT63X6Touchscreen::read_touch_weight_(uint8_t touch) { + return this->read_byte_(FT63X6_ADDR_TOUCH1_WEIGHT + (touch * 6)); +} +uint8_t FT63X6Touchscreen::read_touch_misc_(uint8_t touch) { + return this->read_byte_(FT63X6_ADDR_TOUCH1_MISC + (touch * 6)) >> 4; +} + +uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { + uint8_t byte = 0; + this->read_byte(addr, &byte); + return byte; +} + +} // namespace ft63x6 +} // namespace esphome diff --git a/esphome/components/ft63x6/ft63x6.h b/esphome/components/ft63x6/ft63x6.h new file mode 100644 index 0000000000..8000894294 --- /dev/null +++ b/esphome/components/ft63x6/ft63x6.h @@ -0,0 +1,51 @@ +/**************************************************************************/ +/*! + Author: Gustavo Ambrozio + Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) +*/ +/**************************************************************************/ + +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace ft63x6 { + +using namespace touchscreen; + +static const uint8_t FT6X36_DEFAULT_THRESHOLD = 22; + +class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + void set_threshold(uint8_t threshold) { this->threshold_ = threshold; } + + protected: + void hard_reset_(); + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + uint8_t threshold_{FT6X36_DEFAULT_THRESHOLD}; + + uint8_t read_touch_number_(); + + uint16_t read_touch_x_(uint8_t touch); + uint16_t read_touch_y_(uint8_t touch); + uint8_t read_touch_event_(uint8_t touch); + uint8_t read_touch_id_(uint8_t touch); + uint8_t read_touch_weight_(uint8_t touch); + uint8_t read_touch_misc_(uint8_t touch); + + uint8_t read_byte_(uint8_t addr); +}; + +} // namespace ft63x6 +} // namespace esphome diff --git a/esphome/components/ft63x6/touchscreen.py b/esphome/components/ft63x6/touchscreen.py new file mode 100644 index 0000000000..95fa371433 --- /dev/null +++ b/esphome/components/ft63x6/touchscreen.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN, CONF_THRESHOLD + +CODEOWNERS = ["@gpambrozio"] +DEPENDENCIES = ["i2c"] + +ft6336u_ns = cg.esphome_ns.namespace("ft63x6") +FT63X6Touchscreen = ft6336u_ns.class_( + "FT63X6Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONF_FT63X6_ID = "ft63x6_id" + + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FT63X6Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_THRESHOLD): cv.uint8_t, + } + ).extend(i2c.i2c_device_schema(0x38)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN): + interrupt_pin = await cg.gpio_pin_expression(interrupt_pin_config) + cg.add(var.set_interrupt_pin(interrupt_pin)) + if reset_pin_config := config.get(CONF_RESET_PIN): + reset_pin = await cg.gpio_pin_expression(reset_pin_config) + cg.add(var.set_reset_pin(reset_pin)) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 291af8c8cd..6c7adebfea 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -151,11 +151,13 @@ void FujitsuGeneralClimate::transmit_state() { case climate::CLIMATE_FAN_LOW: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW); break; + case climate::CLIMATE_FAN_QUIET: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT); + break; case climate::CLIMATE_FAN_AUTO: default: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO); break; - // TODO Quiet / Silent } // Set swing @@ -345,8 +347,9 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE); ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode); switch (recv_fan_mode) { - // TODO No Quiet / Silent in ESPH case FUJITSU_GENERAL_FAN_SILENT: + this->fan_mode = climate::CLIMATE_FAN_QUIET; + break; case FUJITSU_GENERAL_FAN_LOW: this->fan_mode = climate::CLIMATE_FAN_LOW; break; diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index ee83ae9d19..d7d01bf6f3 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -52,7 +52,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true, {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH}, + climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET}, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} diff --git a/esphome/components/gcja5/__init__.py b/esphome/components/gcja5/__init__.py new file mode 100644 index 0000000000..122ffaf6ed --- /dev/null +++ b/esphome/components/gcja5/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gcormier"] diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp new file mode 100644 index 0000000000..7f980ca0ad --- /dev/null +++ b/esphome/components/gcja5/gcja5.cpp @@ -0,0 +1,119 @@ +/* From snooping with a logic analyzer, the I2C on this sensor is broken. I was only able + * to receive 1's as a response from the sensor. I was able to get the UART working. + * + * The datasheet says the values should be divided by 1000, but this must only be for the I2C + * implementation. Comparing UART values with another sensor, there is no need to divide by 1000. + */ +#include "gcja5.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace gcja5 { + +static const char *const TAG = "gcja5"; + +void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); } + +void GCJA5Component::loop() { + const uint32_t now = millis(); + if (now - this->last_transmission_ >= 500) { + // last transmission too long ago. Reset RX index. + this->rx_message_.clear(); + } + + if (this->available() == 0) { + return; + } + + // There must now be data waiting + this->last_transmission_ = now; + uint8_t val; + while (this->available() != 0) { + this->read_byte(&val); + this->rx_message_.push_back(val); + + // check if rx_message_ has 32 bytes of data + if (this->rx_message_.size() == 32) { + this->parse_data_(); + + if (this->have_good_data_) { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(get_32_bit_uint_(1)); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(get_32_bit_uint_(5)); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(get_32_bit_uint_(9)); + if (this->pmc_0_3_sensor_ != nullptr) + this->pmc_0_3_sensor_->publish_state(get_16_bit_uint_(13)); + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(get_16_bit_uint_(15)); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(get_16_bit_uint_(17)); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(get_16_bit_uint_(21)); + if (this->pmc_5_0_sensor_ != nullptr) + this->pmc_5_0_sensor_->publish_state(get_16_bit_uint_(23)); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(get_16_bit_uint_(25)); + } else { + this->status_set_warning(); + ESP_LOGV(TAG, "Have 32 bytes but not good data. Skipping."); + } + + this->rx_message_.clear(); + } + } +} + +bool GCJA5Component::calculate_checksum_() { + uint8_t crc = 0; + + for (uint8_t i = 1; i < 30; i++) + crc = crc ^ this->rx_message_[i]; + + ESP_LOGVV(TAG, "Checksum packet was (0x%02X), calculated checksum was (0x%02X)", this->rx_message_[30], crc); + + return (crc == this->rx_message_[30]); +} + +uint32_t GCJA5Component::get_32_bit_uint_(uint8_t start_index) { + return (((uint32_t) this->rx_message_[start_index + 3]) << 24) | + (((uint32_t) this->rx_message_[start_index + 2]) << 16) | + (((uint32_t) this->rx_message_[start_index + 1]) << 8) | ((uint32_t) this->rx_message_[start_index]); +} + +uint16_t GCJA5Component::get_16_bit_uint_(uint8_t start_index) { + return (((uint32_t) this->rx_message_[start_index + 1]) << 8) | ((uint32_t) this->rx_message_[start_index]); +} + +void GCJA5Component::parse_data_() { + ESP_LOGVV(TAG, "GCJA5 Data: "); + for (uint8_t i = 0; i < 32; i++) { + ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->rx_message_[i]), + this->rx_message_[i]); + } + + if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) { + ESP_LOGVV(TAG, "Discarding bad packet - failed checks."); + return; + } else + ESP_LOGVV(TAG, "Good packet found."); + + this->have_good_data_ = true; + uint8_t status = this->rx_message_[29]; + 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); + } +} + +void GCJA5Component::dump_config() { ; } + +} // namespace gcja5 +} // namespace esphome diff --git a/esphome/components/gcja5/gcja5.h b/esphome/components/gcja5/gcja5.h new file mode 100644 index 0000000000..7593c90323 --- /dev/null +++ b/esphome/components/gcja5/gcja5.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace gcja5 { + +class GCJA5Component : public Component, public uart::UARTDevice { + public: + void setup() override; + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + + void set_pmc_0_3_sensor(sensor::Sensor *pmc_0_3) { pmc_0_3_sensor_ = pmc_0_3; } + void set_pmc_0_5_sensor(sensor::Sensor *pmc_0_5) { pmc_0_5_sensor_ = pmc_0_5; } + void set_pmc_1_0_sensor(sensor::Sensor *pmc_1_0) { pmc_1_0_sensor_ = pmc_1_0; } + void set_pmc_2_5_sensor(sensor::Sensor *pmc_2_5) { pmc_2_5_sensor_ = pmc_2_5; } + void set_pmc_5_0_sensor(sensor::Sensor *pmc_5_0) { pmc_5_0_sensor_ = pmc_5_0; } + void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } + + protected: + void parse_data_(); + bool calculate_checksum_(); + + uint32_t get_32_bit_uint_(uint8_t start_index); + uint16_t get_16_bit_uint_(uint8_t start_index); + uint32_t last_transmission_{0}; + std::vector rx_message_; + + bool have_good_data_{false}; + bool first_status_log_{false}; + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + + sensor::Sensor *pmc_0_3_sensor_{nullptr}; + sensor::Sensor *pmc_0_5_sensor_{nullptr}; + sensor::Sensor *pmc_1_0_sensor_{nullptr}; + sensor::Sensor *pmc_2_5_sensor_{nullptr}; + sensor::Sensor *pmc_5_0_sensor_{nullptr}; + sensor::Sensor *pmc_10_0_sensor_{nullptr}; +}; + +} // namespace gcja5 +} // namespace esphome diff --git a/esphome/components/gcja5/sensor.py b/esphome/components/gcja5/sensor.py new file mode 100644 index 0000000000..5bcdc572ff --- /dev/null +++ b/esphome/components/gcja5/sensor.py @@ -0,0 +1,118 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, sensor +from esphome.const import ( + CONF_ID, + CONF_PM_1_0, + CONF_PM_2_5, + CONF_PM_10_0, + CONF_PMC_0_5, + CONF_PMC_1_0, + CONF_PMC_2_5, + CONF_PMC_10_0, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + ICON_COUNTER, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + STATE_CLASS_MEASUREMENT, +) + +CODEOWNERS = ["@gcormier"] +DEPENDENCIES = ["uart"] + +gcja5_ns = cg.esphome_ns.namespace("gcja5") + +GCJA5Component = gcja5_ns.class_("GCJA5Component", cg.PollingComponent, uart.UARTDevice) + +CONF_PMC_0_3 = "pmc_0_3" +CONF_PMC_5_0 = "pmc_5_0" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GCJA5Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +).extend(uart.UART_DEVICE_SCHEMA) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "gcja5", baud_rate=9600, require_rx=True, parity="EVEN" +) +TYPES = { + CONF_PM_1_0: "set_pm_1_0_sensor", + CONF_PM_2_5: "set_pm_2_5_sensor", + CONF_PM_10_0: "set_pm_10_0_sensor", + CONF_PMC_0_3: "set_pmc_0_3_sensor", + CONF_PMC_0_5: "set_pmc_0_5_sensor", + CONF_PMC_1_0: "set_pmc_1_0_sensor", + CONF_PMC_2_5: "set_pmc_2_5_sensor", + CONF_PMC_5_0: "set_pmc_5_0_sensor", + CONF_PMC_10_0: "set_pmc_10_0_sensor", +} + + +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) + + for key, funcName in TYPES.items(): + if key in config: + sens = await sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/gdk101/__init__.py b/esphome/components/gdk101/__init__.py new file mode 100644 index 0000000000..0d90257964 --- /dev/null +++ b/esphome/components/gdk101/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@Szewcson"] + +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +CONF_GDK101_ID = "gdk101_id" + +gdk101_ns = cg.esphome_ns.namespace("gdk101") +GDK101Component = gdk101_ns.class_( + "GDK101Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GDK101Component), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x18)) +) + + +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) diff --git a/esphome/components/gdk101/binary_sensor.py b/esphome/components/gdk101/binary_sensor.py new file mode 100644 index 0000000000..2a3d6f07eb --- /dev/null +++ b/esphome/components/gdk101/binary_sensor.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_VIBRATIONS, + DEVICE_CLASS_VIBRATION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_VIBRATE, +) +from . import CONF_GDK101_ID, GDK101Component + +DEPENDENCIES = ["gdk101"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_GDK101_ID): cv.use_id(GDK101Component), + cv.Required(CONF_VIBRATIONS): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_VIBRATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_VIBRATE, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_GDK101_ID]) + var = await binary_sensor.new_binary_sensor(config[CONF_VIBRATIONS]) + cg.add(hub.set_vibration_binary_sensor(var)) diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp new file mode 100644 index 0000000000..93f3c20fa8 --- /dev/null +++ b/esphome/components/gdk101/gdk101.cpp @@ -0,0 +1,189 @@ +#include "gdk101.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace gdk101 { + +static const char *const TAG = "gdk101"; +static const uint8_t NUMBER_OF_READ_RETRIES = 5; + +void GDK101Component::update() { + uint8_t data[2]; + if (!this->read_dose_1m_(data)) { + this->status_set_warning("Failed to read dose 1m"); + return; + } + + if (!this->read_dose_10m_(data)) { + this->status_set_warning("Failed to read dose 10m"); + return; + } + + if (!this->read_status_(data)) { + this->status_set_warning("Failed to read status"); + return; + } + + if (!this->read_measurement_duration_(data)) { + this->status_set_warning("Failed to read measurement duration"); + return; + } + this->status_clear_warning(); +} + +void GDK101Component::setup() { + uint8_t data[2]; + ESP_LOGCONFIG(TAG, "Setting up GDK101..."); + // first, reset the sensor + if (!this->reset_sensor_(data)) { + this->status_set_error("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->mark_failed(); + return; + } + delay(10); + // read firmware version + if (!this->read_fw_version_(data)) { + this->status_set_error("Failed to read firmware version"); + this->mark_failed(); + return; + } +} + +void GDK101Component::dump_config() { + ESP_LOGCONFIG(TAG, "GDK101:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with GDK101 failed!"); + } +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Firmware Version", this->fw_version_sensor_); + LOG_SENSOR(" ", "Average Radaition Dose per 1 minute", this->rad_1m_sensor_); + LOG_SENSOR(" ", "Average Radaition Dose per 10 minutes", this->rad_10m_sensor_); + LOG_SENSOR(" ", "Status", this->status_sensor_); + LOG_SENSOR(" ", "Measurement Duration", this->measurement_duration_sensor_); +#endif // USE_SENSOR + +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Vibration Status", this->vibration_binary_sensor_); +#endif // USE_BINARY_SENSOR +} + +float GDK101Component::get_setup_priority() const { return setup_priority::DATA; } + +bool GDK101Component::read_bytes_with_retry_(uint8_t a_register, uint8_t *data, uint8_t len) { + uint8_t retry = NUMBER_OF_READ_RETRIES; + bool status = false; + while (!status && retry) { + status = this->read_bytes(a_register, data, len); + retry--; + } + return status; +} + +bool GDK101Component::reset_sensor_(uint8_t *data) { + // It looks like reset is not so well designed in that sensor + // After sending reset command it looks that sensor start performing reset and is unresponsible during read + // after a while we can send another reset command and read "0x01" as confirmation + // Documentation not going in to such details unfortunately + if (!this->read_bytes_with_retry_(GDK101_REG_RESET, data, 2)) { + ESP_LOGE(TAG, "Updating GDK101 failed!"); + return false; + } + + return true; +} + +bool GDK101Component::read_dose_1m_(uint8_t *data) { +#ifdef USE_SENSOR + if (this->rad_1m_sensor_ != nullptr) { + if (!this->read_bytes(GDK101_REG_READ_1MIN_AVG, data, 2)) { + ESP_LOGE(TAG, "Updating GDK101 failed!"); + return false; + } + + const float dose = data[0] + (data[1] / 100.0f); + + this->rad_1m_sensor_->publish_state(dose); + } +#endif // USE_SENSOR + return true; +} + +bool GDK101Component::read_dose_10m_(uint8_t *data) { +#ifdef USE_SENSOR + if (this->rad_10m_sensor_ != nullptr) { + if (!this->read_bytes(GDK101_REG_READ_10MIN_AVG, data, 2)) { + ESP_LOGE(TAG, "Updating GDK101 failed!"); + return false; + } + + const float dose = data[0] + (data[1] / 100.0f); + + this->rad_10m_sensor_->publish_state(dose); + } +#endif // USE_SENSOR + return true; +} + +bool GDK101Component::read_status_(uint8_t *data) { + if (!this->read_bytes(GDK101_REG_READ_STATUS, data, 2)) { + ESP_LOGE(TAG, "Updating GDK101 failed!"); + return false; + } + +#ifdef USE_SENSOR + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(data[0]); + } +#endif // USE_SENSOR + +#ifdef USE_BINARY_SENSOR + if (this->vibration_binary_sensor_ != nullptr) { + this->vibration_binary_sensor_->publish_state(data[1]); + } +#endif // USE_BINARY_SENSOR + + return true; +} + +bool GDK101Component::read_fw_version_(uint8_t *data) { +#ifdef USE_SENSOR + if (this->fw_version_sensor_ != nullptr) { + if (!this->read_bytes(GDK101_REG_READ_FIRMWARE, data, 2)) { + ESP_LOGE(TAG, "Updating GDK101 failed!"); + return false; + } + + const float fw_version = data[0] + (data[1] / 10.0f); + + this->fw_version_sensor_->publish_state(fw_version); + } +#endif // USE_SENSOR + return true; +} + +bool GDK101Component::read_measurement_duration_(uint8_t *data) { +#ifdef USE_SENSOR + if (this->measurement_duration_sensor_ != nullptr) { + if (!this->read_bytes(GDK101_REG_READ_MEASURING_TIME, data, 2)) { + ESP_LOGE(TAG, "Updating GDK101 failed!"); + return false; + } + + const float meas_time = (data[0] * 60) + data[1]; + + this->measurement_duration_sensor_->publish_state(meas_time); + } +#endif // USE_SENSOR + return true; +} + +} // namespace gdk101 +} // namespace esphome diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h new file mode 100644 index 0000000000..460e72ac89 --- /dev/null +++ b/esphome/components/gdk101/gdk101.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif // USE_SENSOR +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif // USE_BINARY_SENSOR +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace gdk101 { + +static const uint8_t GDK101_REG_READ_FIRMWARE = 0xB4; // Firmware version +static const uint8_t GDK101_REG_RESET = 0xA0; // Reset register - reading its value triggers reset +static const uint8_t GDK101_REG_READ_STATUS = 0xB0; // Status register +static const uint8_t GDK101_REG_READ_MEASURING_TIME = 0xB1; // Mesuring time +static const uint8_t GDK101_REG_READ_10MIN_AVG = 0xB2; // Average radiation dose per 10 min +static const uint8_t GDK101_REG_READ_1MIN_AVG = 0xB3; // Average radiation dose per 1 min + +class GDK101Component : public PollingComponent, public i2c::I2CDevice { +#ifdef USE_SENSOR + SUB_SENSOR(rad_1m) + SUB_SENSOR(rad_10m) + SUB_SENSOR(status) + SUB_SENSOR(fw_version) + SUB_SENSOR(measurement_duration) +#endif // USE_SENSOR +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(vibration) +#endif // USE_BINARY_SENSOR + + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + bool read_bytes_with_retry_(uint8_t a_register, uint8_t *data, uint8_t len); + bool reset_sensor_(uint8_t *data); + bool read_dose_1m_(uint8_t *data); + bool read_dose_10m_(uint8_t *data); + bool read_status_(uint8_t *data); + bool read_fw_version_(uint8_t *data); + bool read_measurement_duration_(uint8_t *data); +}; + +} // namespace gdk101 +} // namespace esphome diff --git a/esphome/components/gdk101/sensor.py b/esphome/components/gdk101/sensor.py new file mode 100644 index 0000000000..f782264615 --- /dev/null +++ b/esphome/components/gdk101/sensor.py @@ -0,0 +1,83 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_DURATION, + DEVICE_CLASS_EMPTY, + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_MEASUREMENT_DURATION, + CONF_STATUS, + CONF_VERSION, + ICON_RADIOACTIVE, + ICON_TIMER, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_MICROSILVERTS_PER_HOUR, + UNIT_SECOND, +) +from . import CONF_GDK101_ID, GDK101Component + +CONF_RADIATION_DOSE_PER_1M = "radiation_dose_per_1m" +CONF_RADIATION_DOSE_PER_10M = "radiation_dose_per_10m" + +DEPENDENCIES = ["gdk101"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_GDK101_ID): cv.use_id(GDK101Component), + cv.Optional(CONF_RADIATION_DOSE_PER_1M): sensor.sensor_schema( + icon=ICON_RADIOACTIVE, + unit_of_measurement=UNIT_MICROSILVERTS_PER_HOUR, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_RADIATION_DOSE_PER_10M): sensor.sensor_schema( + icon=ICON_RADIOACTIVE, + unit_of_measurement=UNIT_MICROSILVERTS_PER_HOUR, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + accuracy_decimals=1, + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + accuracy_decimals=0, + ), + cv.Optional(CONF_MEASUREMENT_DURATION): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=DEVICE_CLASS_DURATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_GDK101_ID]) + + if radiation_dose_per_1m := config.get(CONF_RADIATION_DOSE_PER_1M): + sens = await sensor.new_sensor(radiation_dose_per_1m) + cg.add(hub.set_rad_1m_sensor(sens)) + + if radiation_dose_per_10m := config.get(CONF_RADIATION_DOSE_PER_10M): + sens = await sensor.new_sensor(radiation_dose_per_10m) + cg.add(hub.set_rad_10m_sensor(sens)) + + if version_config := config.get(CONF_VERSION): + sens = await sensor.new_sensor(version_config) + cg.add(hub.set_fw_version_sensor(sens)) + + if status_config := config.get(CONF_STATUS): + sens = await sensor.new_sensor(status_config) + cg.add(hub.set_status_sensor(sens)) + + if measurement_duration_config := config.get(CONF_MEASUREMENT_DURATION): + sens = await sensor.new_sensor(measurement_duration_config) + cg.add(hub.set_measurement_duration_sensor(sens)) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index 97a7ba3d54..8defa4ac24 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -15,8 +15,14 @@ CODEOWNERS = ["@esphome/core"] globals_ns = cg.esphome_ns.namespace("globals") GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component) RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component) +RestoringGlobalStringComponent = globals_ns.class_( + "RestoringGlobalStringComponent", cg.Component +) GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action) +CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length" + + MULTI_CONF = True CONFIG_SCHEMA = cv.Schema( { @@ -24,6 +30,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_TYPE): cv.string_strict, cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, + cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254), } ).extend(cv.COMPONENT_SCHEMA) @@ -32,12 +39,19 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(-100.0) async def to_code(config): type_ = cg.RawExpression(config[CONF_TYPE]) - template_args = cg.TemplateArguments(type_) restore = config[CONF_RESTORE_VALUE] - type = RestoringGlobalsComponent if restore else GlobalsComponent - res_type = type.template(template_args) + # Special casing the strings to their own class with a different save/restore mechanism + if str(type_) == "std::string" and restore: + template_args = cg.TemplateArguments( + type_, config.get(CONF_MAX_RESTORE_DATA_LENGTH, 63) + 1 + ) + type = RestoringGlobalStringComponent + else: + template_args = cg.TemplateArguments(type_) + type = RestoringGlobalsComponent if restore else GlobalsComponent + res_type = type.template(template_args) initial_value = None if CONF_INITIAL_VALUE in config: initial_value = cg.RawExpression(config[CONF_INITIAL_VALUE]) diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 101adeb311..78808436af 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -65,6 +65,64 @@ template class RestoringGlobalsComponent : public Component { ESPPreferenceObject rtc_; }; +// Use with string or subclasses of strings +template class RestoringGlobalStringComponent : public Component { + public: + using value_type = T; + explicit RestoringGlobalStringComponent() = default; + explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; } + explicit RestoringGlobalStringComponent( + std::array::type, std::extent::value> initial_value) { + memcpy(this->value_, initial_value.data(), sizeof(T)); + } + + T &value() { return this->value_; } + + void setup() override { + char temp[SZ]; + this->rtc_ = global_preferences->make_preference(1944399030U ^ this->name_hash_); + bool hasdata = this->rtc_.load(&temp); + if (hasdata) { + this->value_.assign(temp + 1, temp[0]); + } + this->prev_value_.assign(this->value_); + } + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void loop() override { store_value_(); } + + void on_shutdown() override { store_value_(); } + + void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } + + protected: + void store_value_() { + int diff = this->value_.compare(this->prev_value_); + if (diff != 0) { + // Make it into a length prefixed thing + unsigned char temp[SZ]; + + // If string is bigger than the allocation, do not save it. + // We don't need to waste ram setting prev_value either. + int size = this->value_.size(); + // Less than, not less than or equal, SZ includes the length byte. + if (size < SZ) { + memcpy(temp + 1, this->value_.c_str(), size); + // SZ should be pre checked at the schema level, it can't go past the char range. + temp[0] = ((unsigned char) size); + this->rtc_.save(&temp); + this->prev_value_.assign(this->value_); + } + } + } + + T value_{}; + T prev_value_{}; + uint32_t name_hash_{}; + ESPPreferenceObject rtc_; +}; + template class GlobalVarSetAction : public Action { public: explicit GlobalVarSetAction(C *parent) : parent_(parent) {} @@ -81,6 +139,7 @@ template class GlobalVarSetAction : public Action T &id(GlobalsComponent *value) { return value->value(); } template T &id(RestoringGlobalsComponent *value) { return value->value(); } +template T &id(RestoringGlobalStringComponent *value) { return value->value(); } } // namespace globals } // namespace esphome diff --git a/esphome/components/gp2y1010au0f/__init__.py b/esphome/components/gp2y1010au0f/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp b/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp new file mode 100644 index 0000000000..95b7653e51 --- /dev/null +++ b/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp @@ -0,0 +1,67 @@ +#include "gp2y1010au0f.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace gp2y1010au0f { + +static const char *const TAG = "gp2y1010au0f"; +static const float MIN_VOLTAGE = 0.0f; +static const float MAX_VOLTAGE = 4.0f; + +void GP2Y1010AU0FSensor::dump_config() { + LOG_SENSOR("", "Sharp GP2Y1010AU0F PM2.5 Sensor", this); + ESP_LOGCONFIG(TAG, " Sampling duration: %" PRId32 " ms", this->sample_duration_); + ESP_LOGCONFIG(TAG, " ADC voltage multiplier: %.3f", this->voltage_multiplier_); + LOG_UPDATE_INTERVAL(this); +} + +void GP2Y1010AU0FSensor::update() { + is_sampling_ = true; + + this->set_timeout("read", this->sample_duration_, [this]() { + this->is_sampling_ = false; + if (this->num_samples_ == 0) + return; + + float mean = this->sample_sum_ / float(this->num_samples_); + ESP_LOGD(TAG, "ADC read voltage: %.3f V (mean from %" PRId32 " samples)", mean, this->num_samples_); + + // PM2.5 calculation + // ref: https://www.howmuchsnow.com/arduino/airquality/ + int16_t pm_2_5_value = 170 * mean; + this->publish_state(pm_2_5_value); + }); + + // reset readings + this->num_samples_ = 0; + this->sample_sum_ = 0.0f; +} + +void GP2Y1010AU0FSensor::loop() { + if (!this->is_sampling_) + return; + + // enable the internal IR LED + this->led_output_->turn_on(); + // wait for the sensor to stabilize + delayMicroseconds(this->sample_wait_before_); + // perform a single sample + float read_voltage = this->source_->sample(); + // disable the internal IR LED + this->led_output_->turn_off(); + + if (std::isnan(read_voltage)) + return; + read_voltage = read_voltage * this->voltage_multiplier_ - this->voltage_offset_; + if (read_voltage < MIN_VOLTAGE || read_voltage > MAX_VOLTAGE) + return; + + this->num_samples_++; + this->sample_sum_ += read_voltage; +} + +} // namespace gp2y1010au0f +} // namespace esphome diff --git a/esphome/components/gp2y1010au0f/gp2y1010au0f.h b/esphome/components/gp2y1010au0f/gp2y1010au0f.h new file mode 100644 index 0000000000..5ee58e68d2 --- /dev/null +++ b/esphome/components/gp2y1010au0f/gp2y1010au0f.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/components/output/binary_output.h" + +namespace esphome { +namespace gp2y1010au0f { + +class GP2Y1010AU0FSensor : public sensor::Sensor, public PollingComponent { + public: + void update() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { + // after the base sensor has been initialized + return setup_priority::DATA - 1.0f; + } + + void set_adc_source(voltage_sampler::VoltageSampler *source) { source_ = source; } + void set_voltage_refs(float offset, float multiplier) { + this->voltage_offset_ = offset; + this->voltage_multiplier_ = multiplier; + } + void set_led_output(output::BinaryOutput *output) { led_output_ = output; } + + protected: + // duration in ms of the sampling phase + uint32_t sample_duration_ = 100; + // duration in us of the wait before sampling + // ref: https://global.sharp/products/device/lineup/data/pdf/datasheet/gp2y1010au_appl_e.pdf + uint32_t sample_wait_before_ = 280; + // duration in us of the wait after sampling + // it seems no need to delay on purpose since one ADC sampling takes longer than that (300-400 us on ESP8266) + // uint32_t sample_wait_after_ = 40; + // the sampling source to read voltage from + voltage_sampler::VoltageSampler *source_; + // ADC voltage reading offset + float voltage_offset_ = 0.0f; + // ADC voltage reading multiplier + float voltage_multiplier_ = 1.0f; + // the binary output to control the sampling LED + output::BinaryOutput *led_output_; + + float sample_sum_ = 0.0f; + uint32_t num_samples_ = 0; + bool is_sampling_ = false; +}; + +} // namespace gp2y1010au0f +} // namespace esphome diff --git a/esphome/components/gp2y1010au0f/sensor.py b/esphome/components/gp2y1010au0f/sensor.py new file mode 100644 index 0000000000..7e1bd277a6 --- /dev/null +++ b/esphome/components/gp2y1010au0f/sensor.py @@ -0,0 +1,61 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler, output +from esphome.const import ( + CONF_SENSOR, + CONF_OUTPUT, + DEVICE_CLASS_PM25, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, +) + +DEPENDENCIES = ["output"] +AUTO_LOAD = ["voltage_sampler"] +CODEOWNERS = ["@zry98"] + +CONF_ADC_VOLTAGE_OFFSET = "adc_voltage_offset" +CONF_ADC_VOLTAGE_MULTIPLIER = "adc_voltage_multiplier" + +gp2y1010au0f_ns = cg.esphome_ns.namespace("gp2y1010au0f") +GP2Y1010AU0FSensor = gp2y1010au0f_ns.class_( + "GP2Y1010AU0FSensor", sensor.Sensor, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + GP2Y1010AU0FSensor, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_CHEMICAL_WEAPON, + ) + .extend( + { + cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), + cv.Optional(CONF_ADC_VOLTAGE_OFFSET, default=0.0): cv.float_, + cv.Optional(CONF_ADC_VOLTAGE_MULTIPLIER, default=1.0): cv.float_, + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + # the ADC sensor to read voltage from + adc_sensor = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_adc_source(adc_sensor)) + cg.add( + var.set_voltage_refs( + config[CONF_ADC_VOLTAGE_OFFSET], config[CONF_ADC_VOLTAGE_MULTIPLIER] + ) + ) + + # the binary output to control the module's internal IR LED + led_output = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_led_output(led_output)) diff --git a/esphome/components/gp8403/output/__init__.py b/esphome/components/gp8403/output/__init__.py index 1cf95ac6e5..7f17faa1b1 100644 --- a/esphome/components/gp8403/output/__init__.py +++ b/esphome/components/gp8403/output/__init__.py @@ -16,7 +16,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(GP8403Output), cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), - cv.Required(CONF_CHANNEL): cv.one_of(0, 1), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/gpio/one_wire/__init__.py b/esphome/components/gpio/one_wire/__init__.py new file mode 100644 index 0000000000..2166e92083 --- /dev/null +++ b/esphome/components/gpio/one_wire/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_ID, CONF_PIN +from esphome.components.one_wire import OneWireBus +from .. import gpio_ns + +CODEOWNERS = ["@ssieb"] + +GPIOOneWireBus = gpio_ns.class_("GPIOOneWireBus", OneWireBus, cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GPIOOneWireBus), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp new file mode 100644 index 0000000000..36eaf2160a --- /dev/null +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -0,0 +1,205 @@ +#include "gpio_one_wire.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace gpio { + +static const char *const TAG = "gpio.one_wire"; + +void GPIOOneWireBus::setup() { + ESP_LOGCONFIG(TAG, "Setting up 1-wire bus..."); + this->t_pin_->setup(); + // clear bus with 480µs high, otherwise initial reset in search might fail + this->t_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + delayMicroseconds(480); + this->search(); +} + +void GPIOOneWireBus::dump_config() { + ESP_LOGCONFIG(TAG, "GPIO 1-wire bus:"); + LOG_PIN(" Pin: ", this->t_pin_); + this->dump_devices_(TAG); +} + +bool HOT IRAM_ATTR GPIOOneWireBus::reset() { + // See reset here: + // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html + // Wait for communication to clear (delay G) + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + uint8_t retries = 125; + do { + if (--retries == 0) + return false; + delayMicroseconds(2); + } while (!pin_.digital_read()); + + bool r; + + // Send 480µs LOW TX reset pulse (drive bus low, delay H) + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); + delayMicroseconds(480); + + // Release the bus, delay I + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + delayMicroseconds(70); + + // sample bus, 0=device(s) present, 1=no device present + r = !pin_.digital_read(); + // delay J + delayMicroseconds(410); + return r; +} + +void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) { + // drive bus low + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); + + // from datasheet: + // write 0 low time: t_low0: min=60µs, max=120µs + // write 1 low time: t_low1: min=1µs, max=15µs + // time slot: t_slot: min=60µs, max=120µs + // recovery time: t_rec: min=1µs + // ds18b20 appears to read the bus after roughly 14µs + uint32_t delay0 = bit ? 6 : 60; + uint32_t delay1 = bit ? 59 : 5; + + // delay A/C + delayMicroseconds(delay0); + // release bus + pin_.digital_write(true); + // delay B/D + delayMicroseconds(delay1); +} + +bool HOT IRAM_ATTR GPIOOneWireBus::read_bit_() { + // drive bus low + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); + + // note: for reading we'll need very accurate timing, as the + // timing for the digital_read() is tight; according to the datasheet, + // we should read at the end of 16µs starting from the bus low + // typically, the ds18b20 pulls the line high after 11µs for a logical 1 + // and 29µs for a logical 0 + + uint32_t start = micros(); + // datasheet says >1µs + delayMicroseconds(2); + + // release bus, delay E + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + + // measure from start value directly, to get best accurate timing no matter + // how long pin_mode/delayMicroseconds took + uint32_t now = micros(); + if (now - start < 12) + delayMicroseconds(12 - (now - start)); + + // sample bus to read bit from peer + bool r = pin_.digital_read(); + + // read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked + now = micros(); + if (now - start < 60) + delayMicroseconds(60 - (now - start)); + + return r; +} + +void IRAM_ATTR GPIOOneWireBus::write8(uint8_t val) { + for (uint8_t i = 0; i < 8; i++) { + this->write_bit_(bool((1u << i) & val)); + } +} + +void IRAM_ATTR GPIOOneWireBus::write64(uint64_t val) { + for (uint8_t i = 0; i < 64; i++) { + this->write_bit_(bool((1ULL << i) & val)); + } +} + +uint8_t IRAM_ATTR GPIOOneWireBus::read8() { + uint8_t ret = 0; + for (uint8_t i = 0; i < 8; i++) { + ret |= (uint8_t(this->read_bit_()) << i); + } + return ret; +} + +uint64_t IRAM_ATTR GPIOOneWireBus::read64() { + uint64_t ret = 0; + for (uint8_t i = 0; i < 8; i++) { + ret |= (uint64_t(this->read_bit_()) << i); + } + return ret; +} + +void GPIOOneWireBus::reset_search() { + this->last_discrepancy_ = 0; + this->last_device_flag_ = false; + this->address_ = 0; +} + +uint64_t IRAM_ATTR GPIOOneWireBus::search_int() { + if (this->last_device_flag_) + return 0u; + + uint8_t last_zero = 0; + uint64_t bit_mask = 1; + uint64_t address = this->address_; + + // Initiate search + for (int bit_number = 1; bit_number <= 64; bit_number++, bit_mask <<= 1) { + // read bit + bool id_bit = this->read_bit_(); + // read its complement + bool cmp_id_bit = this->read_bit_(); + + if (id_bit && cmp_id_bit) { + // No devices participating in search + return 0; + } + + bool branch; + + if (id_bit != cmp_id_bit) { + // only chose one branch, the other one doesn't have any devices. + branch = id_bit; + } else { + // there are devices with both 0s and 1s at this bit + if (bit_number < this->last_discrepancy_) { + branch = (address & bit_mask) > 0; + } else { + branch = bit_number == this->last_discrepancy_; + } + + if (!branch) { + last_zero = bit_number; + } + } + + if (branch) { + address |= bit_mask; + } else { + address &= ~bit_mask; + } + + // choose/announce branch + this->write_bit_(branch); + } + + this->last_discrepancy_ = last_zero; + if (this->last_discrepancy_ == 0) { + // we're at root and have no choices left, so this was the last one. + this->last_device_flag_ = true; + } + + this->address_ = address; + return address; +} + +} // namespace gpio +} // namespace esphome diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.h b/esphome/components/gpio/one_wire/gpio_one_wire.h new file mode 100644 index 0000000000..fe949baec3 --- /dev/null +++ b/esphome/components/gpio/one_wire/gpio_one_wire.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/one_wire/one_wire.h" + +namespace esphome { +namespace gpio { + +class GPIOOneWireBus : public one_wire::OneWireBus, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_pin(InternalGPIOPin *pin) { + this->t_pin_ = pin; + this->pin_ = pin->to_isr(); + } + + bool reset() override; + void write8(uint8_t val) override; + void write64(uint64_t val) override; + uint8_t read8() override; + uint64_t read64() override; + + protected: + InternalGPIOPin *t_pin_; + ISRInternalGPIOPin pin_; + uint8_t last_discrepancy_{0}; + bool last_device_flag_{false}; + uint64_t address_; + + void reset_search() override; + uint64_t search_int() override; + void write_bit_(bool bit); + bool read_bit_(); +}; + +} // namespace gpio +} // namespace esphome diff --git a/esphome/components/gpio_expander/__init__.py b/esphome/components/gpio_expander/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h new file mode 100644 index 0000000000..784c5f0f4a --- /dev/null +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include "esphome/core/hal.h" + +namespace esphome { +namespace gpio_expander { + +/// @brief A class to cache the read state of a GPIO expander. +template class CachedGpioExpander { + public: + bool digital_read(T pin) { + if (!this->read_cache_invalidated_[pin]) { + this->read_cache_invalidated_[pin] = true; + return this->digital_read_cache(pin); + } + return this->digital_read_hw(pin); + } + + void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); } + + protected: + virtual bool digital_read_hw(T pin) = 0; + virtual bool digital_read_cache(T pin) = 0; + virtual void digital_write_hw(T pin, bool value) = 0; + + void reset_pin_cache_() { + for (T i = 0; i < N; i++) { + this->read_cache_invalidated_[i] = false; + } + } + + std::array read_cache_invalidated_{}; +}; + +} // namespace gpio_expander +} // namespace esphome diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index e46f24ba8e..0f1b989f77 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,7 +16,7 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { if (tiny_gps.date.year() < 2019) return; - time::ESPTime val{}; + ESPTime val{}; val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index 046f59ca1a..0b83b71fe4 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -61,6 +61,7 @@ VALUE_POSITION_TYPE = { "BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW, } +CONF_CONTINUOUS = "continuous" GRAPH_TRACE_SCHEMA = cv.Schema( { @@ -70,6 +71,7 @@ GRAPH_TRACE_SCHEMA = cv.Schema( cv.Optional(CONF_LINE_THICKNESS): cv.positive_int, cv.Optional(CONF_LINE_TYPE): cv.enum(LINE_TYPE, upper=True), cv.Optional(CONF_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_CONTINUOUS): cv.boolean, } ) @@ -186,6 +188,8 @@ async def to_code(config): if CONF_COLOR in trace: c = await cg.get_variable(trace[CONF_COLOR]) cg.add(tr.set_line_color(c)) + if CONF_CONTINUOUS in trace: + cg.add(tr.set_continuous(trace[CONF_CONTINUOUS])) cg.add(var.add_trace(tr)) # Add legend if CONF_LEGEND in config: diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index c229f17dd8..09f7557714 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -1,5 +1,5 @@ #include "graph.h" -#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display.h" #include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -56,7 +56,7 @@ void GraphTrace::init(Graph *g) { this->data_.set_update_time_ms(g->get_duration() * 1000 / g->get_width()); } -void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) { +void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) { /// Plot border if (this->border_) { buff->horizontal_line(x_offset, y_offset, this->width_, color); @@ -164,18 +164,47 @@ void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Colo ESP_LOGV(TAG, "Updating graph. ymin %f, ymax %f", ymin, ymax); for (auto *trace : traces_) { Color c = trace->get_line_color(); - uint16_t thick = trace->get_line_thickness(); + int16_t thick = trace->get_line_thickness(); + bool continuous = trace->get_continuous(); + bool has_prev = false; + bool prev_b = false; + int16_t prev_y = 0; for (uint32_t i = 0; i < this->width_; i++) { float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange; if (!std::isnan(v) && (thick > 0)) { - int16_t x = this->width_ - 1 - i; - uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick; - if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) { - int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2; - for (uint16_t t = 0; t < thick; t++) { - buff->draw_pixel_at(x_offset + x, y_offset + y + t, c); + int16_t x = this->width_ - 1 - i + x_offset; + uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick); + bool b = (trace->get_line_type() & bit) == bit; + if (b) { + int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; + auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) { + if (y >= y_offset && y < y_offset + this->height_) + buff->draw_pixel_at(x, y, c); + }; + if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { + for (int16_t t = 0; t < thick; t++) { + draw_pixel_at(x, y + t); + } + } else { + int16_t mid_y = (y + prev_y + thick) / 2; + if (y > prev_y) { + for (int16_t t = prev_y + thick; t <= mid_y; t++) + draw_pixel_at(x + 1, t); + for (int16_t t = mid_y + 1; t < y + thick; t++) + draw_pixel_at(x, t); + } else { + for (int16_t t = prev_y - 1; t >= mid_y; t--) + draw_pixel_at(x + 1, t); + for (int16_t t = mid_y - 1; t >= y; t--) + draw_pixel_at(x, t); + } } + prev_y = y; } + prev_b = b; + has_prev = true; + } else { + has_prev = false; } } } @@ -303,7 +332,7 @@ void GraphLegend::init(Graph *g) { } } -void Graph::draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) { +void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) { if (!legend_) return; diff --git a/esphome/components/graph/graph.h b/esphome/components/graph/graph.h index 69c1167f54..34accb7d3a 100644 --- a/esphome/components/graph/graph.h +++ b/esphome/components/graph/graph.h @@ -8,10 +8,10 @@ namespace esphome { -// forward declare DisplayBuffer +// forward declare Display namespace display { -class DisplayBuffer; -class Font; +class Display; +class BaseFont; } // namespace display namespace graph { @@ -45,8 +45,8 @@ enum ValuePositionType { class GraphLegend { public: void init(Graph *g); - void set_name_font(display::Font *font) { this->font_label_ = font; } - void set_value_font(display::Font *font) { this->font_value_ = font; } + void set_name_font(display::BaseFont *font) { this->font_label_ = font; } + void set_value_font(display::BaseFont *font) { this->font_value_ = font; } void set_width(uint32_t width) { this->width_ = width; } void set_height(uint32_t height) { this->height_ = height; } void set_border(bool val) { this->border_ = val; } @@ -63,8 +63,8 @@ class GraphLegend { ValuePositionType values_{VALUE_POSITION_TYPE_AUTO}; bool units_{true}; DirectionType direction_{DIRECTION_TYPE_AUTO}; - display::Font *font_label_{nullptr}; - display::Font *font_value_{nullptr}; + display::BaseFont *font_label_{nullptr}; + display::BaseFont *font_value_{nullptr}; // Calculated values Graph *parent_{nullptr}; // (x0) (xs,ys) (xs,ys) @@ -116,6 +116,8 @@ class GraphTrace { void set_line_type(enum LineType val) { this->line_type_ = val; } Color get_line_color() { return this->line_color_; } void set_line_color(Color val) { this->line_color_ = val; } + bool get_continuous() { return this->continuous_; } + void set_continuous(bool continuous) { this->continuous_ = continuous; } std::string get_name() { return name_; } const HistoryData *get_tracedata() { return &data_; } @@ -125,6 +127,7 @@ class GraphTrace { uint8_t line_thickness_{3}; enum LineType line_type_ { LINE_TYPE_SOLID }; Color line_color_{COLOR_ON}; + bool continuous_{false}; HistoryData data_; friend Graph; @@ -133,8 +136,8 @@ class GraphTrace { class Graph : public Component { public: - void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color); - void draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color); + void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color); + void draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color); void setup() override; float get_setup_priority() const override { return setup_priority::PROCESSOR; } diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py new file mode 100644 index 0000000000..f4d59b22b8 --- /dev/null +++ b/esphome/components/graphical_display_menu/__init__.py @@ -0,0 +1,98 @@ +from esphome import automation, core +import esphome.codegen as cg +from esphome.components import color, display, font +from esphome.components.display_menu_base import ( + DISPLAY_MENU_BASE_SCHEMA, + DisplayMenuComponent, + display_menu_to_code, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BACKGROUND_COLOR, + CONF_DISPLAY, + CONF_FONT, + CONF_FOREGROUND_COLOR, + CONF_ID, + CONF_TRIGGER_ID, +) + +CONF_MENU_ITEM_VALUE = "menu_item_value" +CONF_ON_REDRAW = "on_redraw" + +graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu") +GraphicalDisplayMenu = graphical_display_menu_ns.class_( + "GraphicalDisplayMenu", DisplayMenuComponent +) +GraphicalDisplayMenuConstPtr = GraphicalDisplayMenu.operator("ptr").operator("const") +MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArguments") +MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator( + "const" +) +GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_( + "GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger +) + +CODEOWNERS = ["@MrMDavidson"] + +AUTO_LOAD = ["display_menu_base"] + +CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GraphicalDisplayMenu), + cv.Optional(CONF_DISPLAY): cv.use_id(display.Display), + cv.Required(CONF_FONT): cv.use_id(font.Font), + cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string), + cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_ON_REDRAW): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + GraphicalDisplayMenuOnRedrawTrigger + ) + } + ), + } + ) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if display_config := config.get(CONF_DISPLAY): + drawing_display = await cg.get_variable(display_config) + cg.add(var.set_display(drawing_display)) + + menu_font = await cg.get_variable(config[CONF_FONT]) + cg.add(var.set_font(menu_font)) + + if (menu_item_value_config := config.get(CONF_MENU_ITEM_VALUE, None)) is not None: + if isinstance(menu_item_value_config, core.Lambda): + template_ = await cg.templatable( + menu_item_value_config, + [(MenuItemValueArgumentsConstPtr, "it")], + cg.std_string, + ) + cg.add(var.set_menu_item_value(template_)) + else: + cg.add(var.set_menu_item_value(menu_item_value_config)) + + if foreground_color_config := config.get(CONF_FOREGROUND_COLOR): + foreground_color = await cg.get_variable(foreground_color_config) + cg.add(var.set_foreground_color(foreground_color)) + + if background_color_config := config.get(CONF_BACKGROUND_COLOR): + background_color = await cg.get_variable(background_color_config) + cg.add(var.set_background_color(background_color)) + + for conf in config.get(CONF_ON_REDRAW, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf + ) + + await display_menu_to_code(var, config) + + cg.add_define("USE_GRAPHICAL_DISPLAY_MENU") diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp new file mode 100644 index 0000000000..4a4e519009 --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -0,0 +1,245 @@ +#include "graphical_display_menu.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include "esphome/components/display/display.h" + +namespace esphome { +namespace graphical_display_menu { + +static const char *const TAG = "graphical_display_menu"; + +void GraphicalDisplayMenu::setup() { + if (this->display_ != nullptr) { + display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); }; + this->display_page_ = make_unique(writer); + } + + if (!this->menu_item_value_.has_value()) { + this->menu_item_value_ = [](const MenuItemValueArguments *it) { + std::string label = " "; + if (it->is_item_selected && it->is_menu_editing) { + label.append(">"); + label.append(it->item->get_value_text()); + label.append("<"); + } else { + label.append("("); + label.append(it->item->get_value_text()); + label.append(")"); + } + return label; + }; + } + + display_menu_base::DisplayMenuComponent::setup(); +} + +void GraphicalDisplayMenu::dump_config() { + ESP_LOGCONFIG(TAG, "Graphical Display Menu"); + ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr)); + ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr)); + ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr)); + ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr)); + ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick"); + ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_)); + ESP_LOGCONFIG(TAG, "Menu items:"); + for (size_t i = 0; i < this->displayed_item_->items_size(); i++) { + auto *item = this->displayed_item_->get_item(i); + ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(), + LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())), + YESNO(item->get_immediate_edit())); + } +} + +void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; } + +void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; } + +void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; } +void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; } + +void GraphicalDisplayMenu::on_before_show() { + if (this->display_ != nullptr) { + this->previous_display_page_ = this->display_->get_active_page(); + this->display_->show_page(this->display_page_.get()); + this->display_->clear(); + } else { + this->update(); + } +} + +void GraphicalDisplayMenu::on_before_hide() { + if (this->previous_display_page_ != nullptr) { + this->display_->show_page((display::DisplayPage *) this->previous_display_page_); + this->display_->clear(); + this->update(); + this->previous_display_page_ = nullptr; + } else { + this->update(); + } +} + +void GraphicalDisplayMenu::draw_and_update() { + this->update(); + + // If we're in advanced drawing mode we won't have a display and will instead require the update callback to do + // our drawing + if (this->display_ != nullptr) { + draw_menu(); + } +} + +void GraphicalDisplayMenu::draw_menu() { + if (this->display_ == nullptr) { + ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode"); + return; + } + display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height()); + this->draw_menu_internal_(this->display_, &bounds); +} + +void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *bounds) { + this->draw_menu_internal_(display, bounds); +} + +void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) { + int16_t total_height = 0; + int16_t max_width = 0; + int y_padding = 2; + bool scroll_menu_items = false; + std::vector menu_dimensions; + int number_items_fit_to_screen = 0; + const int max_item_index = this->displayed_item_->items_size() - 1; + + for (size_t i = 0; i <= max_item_index; i++) { + const auto *item = this->displayed_item_->get_item(i); + const bool selected = i == this->cursor_index_; + const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); + + menu_dimensions.push_back(item_dimensions); + total_height += item_dimensions.h + (i == 0 ? 0 : y_padding); + max_width = std::max(max_width, item_dimensions.w); + + if (total_height <= bounds->h) { + number_items_fit_to_screen++; + } else { + // Scroll the display if the selected item or the item immediately after it overflows + if ((selected) || (i == this->cursor_index_ + 1)) { + scroll_menu_items = true; + } + } + } + + // Determine what items to draw + int first_item_index = 0; + int last_item_index = max_item_index; + + if (number_items_fit_to_screen <= 1) { + // If only one item can fit to the bounds draw the current cursor item + last_item_index = std::min(last_item_index, this->cursor_index_ + 1); + first_item_index = this->cursor_index_; + } else { + if (scroll_menu_items) { + // Attempt to draw the item after the current item (+1 for equality check in the draw loop) + last_item_index = std::min(last_item_index, this->cursor_index_ + 1); + + // Go back through the measurements to determine how many prior items we can fit + int height_left_to_use = bounds->h; + for (int i = last_item_index; i >= 0; i--) { + const display::Rect item_dimensions = menu_dimensions[i]; + height_left_to_use -= (item_dimensions.h + y_padding); + + if (height_left_to_use <= 0) { + // Ran out of space - this is our first item to draw + first_item_index = i; + break; + } + } + const int items_to_draw = last_item_index - first_item_index; + // Dont't draw last item partially if it is the selected item + if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) && + (first_item_index < max_item_index)) { + first_item_index++; + } + } + } + + // Render the items into the view port + display->start_clipping(*bounds); + + display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_); + auto y_offset = bounds->y; + for (size_t i = first_item_index; i <= last_item_index; i++) { + const auto *item = this->displayed_item_->get_item(i); + const bool selected = i == this->cursor_index_; + display::Rect dimensions = menu_dimensions[i]; + + dimensions.y = y_offset; + dimensions.x = bounds->x; + this->draw_item(display, item, &dimensions, selected); + + y_offset += dimensions.h + y_padding; + } + + display->end_clipping(); +} + +display::Rect GraphicalDisplayMenu::measure_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, const bool selected) { + display::Rect dimensions(0, 0, 0, 0); + + if (selected) { + // TODO: Support selection glyph + dimensions.w += 0; + dimensions.h += 0; + } + + std::string label = item->get_text(); + if (item->has_value()) { + // Append to label + MenuItemValueArguments args(item, selected, this->editing_); + label.append(this->menu_item_value_.value(&args)); + } + + int x1; + int y1; + int width; + int height; + display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height); + + dimensions.w = std::min((int16_t) width, bounds->w); + dimensions.h = std::min((int16_t) height, bounds->h); + + return dimensions; +} + +inline void GraphicalDisplayMenu::draw_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, const bool selected) { + const auto background_color = selected ? this->foreground_color_ : this->background_color_; + const auto foreground_color = selected ? this->background_color_ : this->foreground_color_; + + // int background_width = std::max(bounds->width, available_width); + int background_width = bounds->w; + + display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color); + + std::string label = item->get_text(); + if (item->has_value()) { + MenuItemValueArguments args(item, selected, this->editing_); + label.append(this->menu_item_value_.value(&args)); + } + + display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str(), + background_color); +} + +void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) { + ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific " + "draw_item should be called."); +} + +void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); } + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h new file mode 100644 index 0000000000..96f2bd79fd --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -0,0 +1,84 @@ +#pragma once + +#include "esphome/core/color.h" +#include "esphome/components/display_menu_base/display_menu_base.h" +#include "esphome/components/display_menu_base/menu_item.h" +#include "esphome/core/automation.h" +#include + +namespace esphome { + +// forward declare from display namespace +namespace display { +class Display; +class DisplayPage; +class BaseFont; +class Rect; +} // namespace display + +namespace graphical_display_menu { + +const Color COLOR_ON(255, 255, 255, 255); +const Color COLOR_OFF(0, 0, 0, 0); + +struct MenuItemValueArguments { + MenuItemValueArguments(const display_menu_base::MenuItem *item, bool is_item_selected, bool is_menu_editing) { + this->item = item; + this->is_item_selected = is_item_selected; + this->is_menu_editing = is_menu_editing; + } + + const display_menu_base::MenuItem *item; + bool is_item_selected; + bool is_menu_editing; +}; + +class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { + public: + void setup() override; + void dump_config() override; + + void set_display(display::Display *display); + void set_font(display::BaseFont *font); + template void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; } + void set_foreground_color(Color foreground_color); + void set_background_color(Color background_color); + + void add_on_redraw_callback(std::function &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); } + + void draw(display::Display *display, const display::Rect *bounds); + + protected: + void draw_and_update() override; + void draw_menu() override; + void draw_menu_internal_(display::Display *display, const display::Rect *bounds); + void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override; + virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, bool selected); + virtual void draw_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, bool selected); + void update() override; + + void on_before_show() override; + void on_before_hide() override; + + std::unique_ptr display_page_{nullptr}; + const display::DisplayPage *previous_display_page_{nullptr}; + display::Display *display_{nullptr}; + display::BaseFont *font_{nullptr}; + TemplatableValue menu_item_value_; + Color foreground_color_{COLOR_ON}; + Color background_color_{COLOR_OFF}; + + CallbackManager on_redraw_callbacks_{}; +}; + +class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { + public: + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { + parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + } +}; + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/components/gree/__init__.py b/esphome/components/gree/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/gree/climate.py b/esphome/components/gree/climate.py new file mode 100644 index 0000000000..75436f2cf5 --- /dev/null +++ b/esphome/components/gree/climate.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +from esphome.components import climate_ir +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MODEL + +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") +MODELS = { + "generic": Model.GREE_GENERIC, + "yan": Model.GREE_YAN, + "yaa": Model.GREE_YAA, + "yac": Model.GREE_YAC, + "yac1fb9": Model.GREE_YAC1FB9, + "yx1ff": Model.GREE_YX1FF, +} + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GreeClimate), + cv.Required(CONF_MODEL): cv.enum(MODELS), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_model(config[CONF_MODEL])) + + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp new file mode 100644 index 0000000000..6d179a947b --- /dev/null +++ b/esphome/components/gree/gree.cpp @@ -0,0 +1,216 @@ +#include "gree.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace gree { + +static const char *const TAG = "gree.climate"; + +void GreeClimate::set_model(Model model) { + if (model == GREE_YX1FF) { + this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed + this->presets_.insert(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode + this->presets_.insert(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode + } + + this->model_ = model; +} + +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) { + remote_state[2] = 0x60; + remote_state[3] = 0x50; + remote_state[4] = this->vertical_swing_(); + } + + if (this->model_ == GREE_YAC) { + remote_state[4] |= (this->horizontal_swing_() << 4); + } + + 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[3] = 0x50; // bits 4..7 always 0101 + remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010 + + if (this->vertical_swing_() == GREE_VDIR_SWING) { + remote_state[0] |= (1 << 6); // Enable swing by setting bit 6 + } else if (this->vertical_swing_() != GREE_VDIR_AUTO) { + remote_state[5] = this->vertical_swing_(); + } + } + + if (this->model_ == GREE_YX1FF) { + if (this->fan_speed_() == GREE_FAN_TURBO) { + remote_state[2] |= GREE_FAN_TURBO_BIT; + } + + if (this->preset_() == GREE_PRESET_SLEEP) { + remote_state[0] |= GREE_PRESET_SLEEP_BIT; + } + } + + // Calculate the checksum + if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { + remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0); + } else { + remote_state[7] = + ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) & + 0x0F) + << 4) | + (remote_state[7] & 0x0F); + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(GREE_IR_FREQUENCY); + + data->mark(GREE_HEADER_MARK); + if (this->model_ == GREE_YAC1FB9) { + data->space(GREE_YAC1FB9_HEADER_SPACE); + } else { + data->space(GREE_HEADER_SPACE); + } + + for (int i = 0; i < 4; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(GREE_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? GREE_ONE_SPACE : GREE_ZERO_SPACE); + } + } + + data->mark(GREE_BIT_MARK); + data->space(GREE_ZERO_SPACE); + data->mark(GREE_BIT_MARK); + data->space(GREE_ONE_SPACE); + data->mark(GREE_BIT_MARK); + data->space(GREE_ZERO_SPACE); + + data->mark(GREE_BIT_MARK); + if (this->model_ == GREE_YAC1FB9) { + data->space(GREE_YAC1FB9_MESSAGE_SPACE); + } else { + data->space(GREE_MESSAGE_SPACE); + } + + for (int i = 4; i < 8; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(GREE_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? GREE_ONE_SPACE : GREE_ZERO_SPACE); + } + } + + data->mark(GREE_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t GreeClimate::operation_mode_() { + uint8_t operating_mode = GREE_MODE_ON; + + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= GREE_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= GREE_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= GREE_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= GREE_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= GREE_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = GREE_MODE_OFF; + break; + } + + return operating_mode; +} + +uint8_t GreeClimate::fan_speed_() { + // YX1FF has 4 fan speeds -- we treat low as quiet and turbo as high + if (this->model_ == GREE_YX1FF) { + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_QUIET: + return GREE_FAN_1; + case climate::CLIMATE_FAN_LOW: + return GREE_FAN_2; + case climate::CLIMATE_FAN_MEDIUM: + return GREE_FAN_3; + case climate::CLIMATE_FAN_HIGH: + return GREE_FAN_TURBO; + case climate::CLIMATE_FAN_AUTO: + default: + return GREE_FAN_AUTO; + } + } + + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + return GREE_FAN_1; + case climate::CLIMATE_FAN_MEDIUM: + return GREE_FAN_2; + case climate::CLIMATE_FAN_HIGH: + return GREE_FAN_3; + case climate::CLIMATE_FAN_AUTO: + default: + return GREE_FAN_AUTO; + } +} + +uint8_t GreeClimate::horizontal_swing_() { + switch (this->swing_mode) { + case climate::CLIMATE_SWING_HORIZONTAL: + case climate::CLIMATE_SWING_BOTH: + return GREE_HDIR_SWING; + default: + return GREE_HDIR_MANUAL; + } +} + +uint8_t GreeClimate::vertical_swing_() { + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + case climate::CLIMATE_SWING_BOTH: + return GREE_VDIR_SWING; + default: + return GREE_VDIR_MANUAL; + } +} + +uint8_t GreeClimate::temperature_() { + return (uint8_t) roundf(clamp(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX)); +} + +uint8_t GreeClimate::preset_() { + // YX1FF has sleep preset + if (this->model_ == GREE_YX1FF) { + switch (this->preset.value()) { + case climate::CLIMATE_PRESET_NONE: + return GREE_PRESET_NONE; + case climate::CLIMATE_PRESET_SLEEP: + return GREE_PRESET_SLEEP; + default: + return GREE_PRESET_NONE; + } + } + + return GREE_PRESET_NONE; +} + +} // namespace gree +} // namespace esphome diff --git a/esphome/components/gree/gree.h b/esphome/components/gree/gree.h new file mode 100644 index 0000000000..6762b41eb0 --- /dev/null +++ b/esphome/components/gree/gree.h @@ -0,0 +1,109 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace gree { + +// Values for GREE IR Controllers +// Temperature +const uint8_t GREE_TEMP_MIN = 16; // Celsius +const 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; + +const uint8_t GREE_MODE_OFF = 0x00; +const 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; + +// 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; + +// 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; + +// Timing specific to YAC1FB9 +const uint32_t GREE_YAC1FB9_HEADER_SPACE = 4500; +const uint32_t GREE_YAC1FB9_MESSAGE_SPACE = 19980; + +// State Frame size +const 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; + +// Only available on YAC +// 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; + +// 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; + +// Model codes +enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF }; + +class GreeClimate : public climate_ir::ClimateIR { + public: + GreeClimate() + : climate_ir::ClimateIR(GREE_TEMP_MIN, GREE_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + + void set_model(Model model); + + protected: + // Transmit via IR the state of this climate controller. + void transmit_state() override; + + uint8_t operation_mode_(); + uint8_t fan_speed_(); + uint8_t horizontal_swing_(); + uint8_t vertical_swing_(); + uint8_t temperature_(); + uint8_t preset_(); + + Model model_{}; +}; + +} // namespace gree +} // namespace esphome diff --git a/esphome/components/grove_gas_mc_v2/__init__.py b/esphome/components/grove_gas_mc_v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp new file mode 100644 index 0000000000..ed40ba42a5 --- /dev/null +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp @@ -0,0 +1,88 @@ +#include "grove_gas_mc_v2.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace grove_gas_mc_v2 { + +static const char *const TAG = "grove_gas_mc_v2"; + +// I2C Commands for Grove Gas Multichannel V2 Sensor +// Taken from: +// https://github.com/Seeed-Studio/Seeed_Arduino_MultiGas/blob/master/src/Multichannel_Gas_GroveGasMultichannelV2.h +static const uint8_t GROVE_GAS_MC_V2_HEAT_ON = 0xFE; +static const uint8_t GROVE_GAS_MC_V2_HEAT_OFF = 0xFF; +static const uint8_t GROVE_GAS_MC_V2_READ_GM102B = 0x01; +static const uint8_t GROVE_GAS_MC_V2_READ_GM302B = 0x03; +static const uint8_t GROVE_GAS_MC_V2_READ_GM502B = 0x05; +static const uint8_t GROVE_GAS_MC_V2_READ_GM702B = 0x07; + +bool GroveGasMultichannelV2Component::read_sensor_(uint8_t address, sensor::Sensor *sensor) { + if (sensor == nullptr) { + return true; + } + uint32_t value = 0; + if (!this->read_bytes(address, (uint8_t *) &value, 4)) { + ESP_LOGW(TAG, "Reading Grove Gas Sensor data failed!"); + this->error_code_ = COMMUNICATION_FAILED; + this->status_set_warning(); + return false; + } + sensor->publish_state(value); + return true; +} + +void GroveGasMultichannelV2Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up Grove Multichannel Gas Sensor V2..."); + + // Before reading sensor values, must preheat sensor + if (!(this->write_bytes(GROVE_GAS_MC_V2_HEAT_ON, {}))) { + this->mark_failed(); + this->error_code_ = APP_START_FAILED; + } +} + +void GroveGasMultichannelV2Component::update() { + // Read from each of the gas sensors + if (!this->read_sensor_(GROVE_GAS_MC_V2_READ_GM102B, this->nitrogen_dioxide_sensor_)) + return; + if (!this->read_sensor_(GROVE_GAS_MC_V2_READ_GM302B, this->ethanol_sensor_)) + return; + if (!this->read_sensor_(GROVE_GAS_MC_V2_READ_GM502B, this->tvoc_sensor_)) + return; + if (!this->read_sensor_(GROVE_GAS_MC_V2_READ_GM702B, this->carbon_monoxide_sensor_)) + return; + + this->status_clear_warning(); +} + +void GroveGasMultichannelV2Component::dump_config() { + ESP_LOGCONFIG(TAG, "Grove Multichannel Gas Sensor V2"); + LOG_I2C_DEVICE(this) + LOG_UPDATE_INTERVAL(this) + LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_) + LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_) + LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_) + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_) + + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case APP_INVALID: + ESP_LOGW(TAG, "Sensor reported invalid APP installed."); + break; + case APP_START_FAILED: + ESP_LOGW(TAG, "Sensor reported APP start failed."); + break; + case UNKNOWN: + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } +} + +} // namespace grove_gas_mc_v2 +} // namespace esphome diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h new file mode 100644 index 0000000000..1987d33f37 --- /dev/null +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace grove_gas_mc_v2 { + +class GroveGasMultichannelV2Component : public PollingComponent, public i2c::I2CDevice { + SUB_SENSOR(tvoc) + SUB_SENSOR(carbon_monoxide) + SUB_SENSOR(nitrogen_dioxide) + SUB_SENSOR(ethanol) + + public: + /// Setup the sensor and test for a connection. + void setup() override; + /// Schedule temperature+pressure readings. + void update() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + enum ErrorCode { + UNKNOWN, + COMMUNICATION_FAILED, + APP_INVALID, + APP_START_FAILED, + } error_code_{UNKNOWN}; + + bool read_sensor_(uint8_t address, sensor::Sensor *sensor); +}; + +} // namespace grove_gas_mc_v2 +} // namespace esphome diff --git a/esphome/components/grove_gas_mc_v2/sensor.py b/esphome/components/grove_gas_mc_v2/sensor.py new file mode 100644 index 0000000000..0c35047850 --- /dev/null +++ b/esphome/components/grove_gas_mc_v2/sensor.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_CARBON_MONOXIDE, + CONF_ETHANOL, + CONF_ID, + CONF_NITROGEN_DIOXIDE, + CONF_TVOC, + DEVICE_CLASS_CARBON_MONOXIDE, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + ICON_AIR_FILTER, + ICON_FLASK_ROUND_BOTTOM, + ICON_GAS_CYLINDER, + ICON_MOLECULE_CO, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@YorkshireIoT"] +DEPENDENCIES = ["i2c"] + +grove_gas_mc_v2_ns = cg.esphome_ns.namespace("grove_gas_mc_v2") + +GroveGasMultichannelV2Component = grove_gas_mc_v2_ns.class_( + "GroveGasMultichannelV2Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GroveGasMultichannelV2Component), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_AIR_FILTER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CARBON_MONOXIDE): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_MONOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_NITROGEN_DIOXIDE): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_NITROGEN_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ETHANOL): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_FLASK_ROUND_BOTTOM, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x08)) +) + + +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) + + for key in [CONF_TVOC, CONF_CARBON_MONOXIDE, CONF_NITROGEN_DIOXIDE, CONF_ETHANOL]: + if sensor_config := config.get(key): + sensor_ = await sensor.new_sensor(sensor_config) + cg.add(getattr(var, f"set_{key}_sensor")(sensor_)) diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py new file mode 100644 index 0000000000..7db0198a89 --- /dev/null +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -0,0 +1,177 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import i2c + +from esphome.const import ( + CONF_ID, + CONF_CHANNEL, + CONF_SPEED, + CONF_DIRECTION, + CONF_ADDRESS, +) + +DEPENDENCIES = ["i2c"] + +CODEOWNERS = ["@max246"] + +MULTI_CONF = True + +grove_tb6612fng_ns = cg.esphome_ns.namespace("grove_tb6612fng") +GROVE_TB6612FNG = grove_tb6612fng_ns.class_( + "GroveMotorDriveTB6612FNG", cg.Component, i2c.I2CDevice +) +GROVETB6612FNGMotorRunAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorRunAction", automation.Action +) +GROVETB6612FNGMotorBrakeAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorBrakeAction", automation.Action +) +GROVETB6612FNGMotorStopAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorStopAction", automation.Action +) +GROVETB6612FNGMotorStandbyAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorStandbyAction", automation.Action +) +GROVETB6612FNGMotorNoStandbyAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorNoStandbyAction", automation.Action +) +GROVETB6612FNGMotorChangeAddressAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorChangeAddressAction", automation.Action +) + +DIRECTION_TYPE = { + "FORWARD": 1, + "BACKWARD": 2, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(GROVE_TB6612FNG), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x14)) +) + + +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) + + +@automation.register_action( + "grove_tb6612fng.run", + GROVETB6612FNGMotorRunAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + cv.Required(CONF_SPEED): cv.templatable(cv.int_range(min=0, max=255)), + cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True), + } + ), +) +async def grove_tb6612fng_run_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_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16) + template_speed = ( + template_speed if config[CONF_DIRECTION] == "FORWARD" else -template_speed + ) + cg.add(var.set_channel(template_channel)) + cg.add(var.set_speed(template_speed)) + return var + + +@automation.register_action( + "grove_tb6612fng.break", + GROVETB6612FNGMotorBrakeAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + } + ), +) +async def grove_tb6612fng_break_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_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + cg.add(var.set_channel(template_channel)) + return var + + +@automation.register_action( + "grove_tb6612fng.stop", + GROVETB6612FNGMotorStopAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + } + ), +) +async def grove_tb6612fng_stop_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_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + cg.add(var.set_channel(template_channel)) + return var + + +@automation.register_action( + "grove_tb6612fng.standby", + GROVETB6612FNGMotorStandbyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + } + ), +) +async def grove_tb6612fng_standby_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( + "grove_tb6612fng.no_standby", + GROVETB6612FNGMotorNoStandbyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + } + ), +) +async def grove_tb6612fng_no_standby_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( + "grove_tb6612fng.change_address", + GROVETB6612FNGMotorChangeAddressAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_ADDRESS): cv.i2c_address, + } + ), +) +async def grove_tb6612fng_change_address_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_channel = await cg.templatable(config[CONF_ADDRESS], args, int) + cg.add(var.set_address(template_channel)) + return var diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp new file mode 100644 index 0000000000..621b7968a4 --- /dev/null +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -0,0 +1,171 @@ +#include "grove_tb6612fng.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace grove_tb6612fng { + +static const char *const TAG = "GroveMotorDriveTB6612FNG"; + +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE = 0x00; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STOP = 0x01; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CW = 0x02; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CCW = 0x03; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY = 0x04; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY = 0x05; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN = 0x06; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP = 0x07; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN = 0x08; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR = 0x11; + +void GroveMotorDriveTB6612FNG::dump_config() { + ESP_LOGCONFIG(TAG, "GroveMotorDriveTB6612FNG:"); + LOG_I2C_DEVICE(this); +} + +void GroveMotorDriveTB6612FNG::setup() { + ESP_LOGCONFIG(TAG, "Setting up Grove Motor Drive TB6612FNG ..."); + if (!this->standby()) { + this->mark_failed(); + return; + } +} + +bool GroveMotorDriveTB6612FNG::standby() { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY, &status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set standby failed!"); + this->status_set_warning(); + return false; + } + return true; +} + +bool GroveMotorDriveTB6612FNG::not_standby() { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY, &status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set not standby failed!"); + this->status_set_warning(); + return false; + } + return true; +} + +void GroveMotorDriveTB6612FNG::set_i2c_addr(uint8_t addr) { + if (addr == 0x00 || addr >= 0x80) { + return; + } + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR, &addr, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set new i2c address failed!"); + this->status_set_warning(); + return; + } + this->set_i2c_address(addr); +} + +void GroveMotorDriveTB6612FNG::dc_motor_run(uint8_t channel, int16_t speed) { + speed = clamp(speed, -255, 255); + + buffer_[0] = channel; + if (speed >= 0) { + buffer_[1] = speed; + } else { + buffer_[1] = (uint8_t) (-speed); + } + + if (speed >= 0) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run motor failed!"); + this->status_set_warning(); + return; + } + } else { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, buffer_, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run motor failed!"); + this->status_set_warning(); + return; + } + } +} + +void GroveMotorDriveTB6612FNG::dc_motor_brake(uint8_t channel) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE, &channel, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Break motor failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::dc_motor_stop(uint8_t channel) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STOP, &channel, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Stop dc motor failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm) { + uint8_t cw = 0; + // 0.1ms_per_step + uint16_t ms_per_step = 0; + + if (steps > 0) { + cw = 1; + } + // stop + else if (steps == 0) { + this->stepper_stop(); + return; + } else if (steps == INT16_MIN) { + steps = INT16_MAX; + } else { + steps = -steps; + } + + rpm = clamp(rpm, 1, 300); + + ms_per_step = (uint16_t) (3000.0 / (float) rpm); + buffer_[0] = mode; + buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw + buffer_[2] = steps; + buffer_[3] = (steps >> 8); + buffer_[4] = ms_per_step; + buffer_[5] = (ms_per_step >> 8); + + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run stepper failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_stop() { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Send stop stepper failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw) { + // 4=>infinite ccw 5=>infinite cw + uint8_t cw = (is_cw) ? 5 : 4; + // 0.1ms_per_step + uint16_t ms_per_step = 0; + + rpm = clamp(rpm, 1, 300); + ms_per_step = (uint16_t) (3000.0 / (float) rpm); + + buffer_[0] = mode; + buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw + buffer_[2] = ms_per_step; + buffer_[3] = (ms_per_step >> 8); + + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN, buffer_, 4) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Write stepper keep run failed"); + this->status_set_warning(); + return; + } +} +} // namespace grove_tb6612fng +} // namespace esphome diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h new file mode 100644 index 0000000000..2743ef4ed7 --- /dev/null +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -0,0 +1,215 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/automation.h" +//#include "esphome/core/helpers.h" + +/* + Grove_Motor_Driver_TB6612FNG.h + A library for the Grove - Motor Driver(TB6612FNG) + Copyright (c) 2018 seeed technology co., ltd. + Website : www.seeed.cc + Author : Jerry Yip + Create Time: 2018-06 + Version : 0.1 + Change Log : + The MIT License (MIT) + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +namespace esphome { +namespace grove_tb6612fng { + +enum MotorChannelTypeT { + MOTOR_CHA = 0, + MOTOR_CHB = 1, +}; + +enum StepperModeTypeT { + FULL_STEP = 0, + WAVE_DRIVE = 1, + HALF_STEP = 2, + MICRO_STEPPING = 3, +}; + +class GroveMotorDriveTB6612FNG : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + /************************************************************* + Description + Enter standby mode. Normally you don't need to call this, except that + you have called notStandby() before. + Parameter + Null. + Return + True/False. + *************************************************************/ + bool standby(); + + /************************************************************* + Description + Exit standby mode. Motor driver does't do any action at this mode. + Parameter + Null. + Return + True/False. + *************************************************************/ + bool not_standby(); + + /************************************************************* + Description + Set an new I2C address. + Parameter + addr: 0x01~0x7f + Return + Null. + *************************************************************/ + void set_i2c_addr(uint8_t addr); + + /***********************************change_address + Drive a motor. + Parameter + chl: MOTOR_CHA or MOTOR_CHB + speed: -255~255, if speed > 0, motor moves clockwise. + Note that there is always a starting speed(a starting voltage) for motor. + If the input voltage is 5V, the starting speed should larger than 100 or + smaller than -100. + Return + Null. + *************************************************************/ + void dc_motor_run(uint8_t channel, int16_t speed); + + /************************************************************* + Description + Brake, stop the motor immediately + Parameter + chl: MOTOR_CHA or MOTOR_CHB + Return + Null. + *************************************************************/ + void dc_motor_brake(uint8_t channel); + + /************************************************************* + Description + Stop the motor slowly. + Parameter + chl: MOTOR_CHA or MOTOR_CHB + Return + Null. + *************************************************************/ + void dc_motor_stop(uint8_t channel); + + /************************************************************* + Description + Drive a stepper. + Parameter + mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING, + for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png + steps: The number of steps to run, range from -32768 to 32767. + When steps = 0, the stepper stops. + When steps > 0, the stepper runs clockwise. When steps < 0, the stepper runs anticlockwise. + rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300. + Note that high rpm will lead to step lose, so rpm should not be larger than 150. + Return + Null. + *************************************************************/ + void stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm); + + /************************************************************* + Description + Stop a stepper. + Parameter + Null. + Return + Null. + *************************************************************/ + void stepper_stop(); + + // keeps moving(direction same as the last move, default to clockwise) + /************************************************************* + Description + Keep a stepper running. + Parameter + mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING, + for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png + rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300. + Note that high rpm will lead to step lose, so rpm should not be larger than 150. + is_cw: Set the running direction, true for clockwise and false for anti-clockwise. + Return + Null. + *************************************************************/ + void stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw); + + private: + uint8_t buffer_[16]; +}; + +template +class GROVETB6612FNGMotorRunAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint16_t, speed) + + void play(Ts... x) override { + auto channel = this->channel_.value(x...); + auto speed = this->speed_.value(x...); + this->parent_->dc_motor_run(channel, speed); + } +}; + +template +class GROVETB6612FNGMotorBrakeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + + void play(Ts... x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); } +}; + +template +class GROVETB6612FNGMotorStopAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + + void play(Ts... x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); } +}; + +template +class GROVETB6612FNGMotorStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->standby(); } +}; + +template +class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->not_standby(); } +}; + +template +class GROVETB6612FNGMotorChangeAddressAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, address) + + void play(Ts... x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } +}; + +} // namespace grove_tb6612fng +} // namespace esphome diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index ed753c4d3f..c4ed5ab841 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -9,11 +9,42 @@ static const char *const TAG = "growatt_solar"; static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion +void GrowattSolar::loop() { + // If update() was unable to send we retry until we can send. + if (!this->waiting_to_update_) + return; + update(); +} + void GrowattSolar::update() { + // If our last send has had no reply yet, and it wasn't that long ago, do nothing. + uint32_t now = millis(); + if (now - this->last_send_ < this->get_update_interval() / 2) { + return; + } + + // The bus might be slow, or there might be other devices, or other components might be talking to our device. + if (this->waiting_for_response()) { + this->waiting_to_update_ = true; + return; + } + + this->waiting_to_update_ = false; this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]); + this->last_send_ = millis(); } void GrowattSolar::on_modbus_data(const std::vector &data) { + // Other components might be sending commands to our device. But we don't get called with enough + // context to know what is what. So if we didn't do a send, we ignore the data. + if (!this->last_send_) + return; + this->last_send_ = 0; + + // Also ignore the data if the message is too short. Otherwise we will publish invalid values. + if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2) + return; + auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { if (sensor == nullptr) return; diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index d1b08b534a..b0ddd4b99d 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -19,6 +19,7 @@ enum GrowattProtocolVersion { class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { public: + void loop() override; void update() override; void on_modbus_data(const std::vector &data) override; void dump_config() override; @@ -55,6 +56,9 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { } protected: + bool waiting_to_update_; + uint32_t last_send_; + struct GrowattPhase { sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index f95d679c3e..0db15ae53e 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -6,6 +6,9 @@ from esphome.const import ( CONF_CURRENT, CONF_FREQUENCY, CONF_ID, + CONF_PHASE_A, + CONF_PHASE_B, + CONF_PHASE_C, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -21,10 +24,6 @@ from esphome.const import ( UNIT_WATT, ) -CONF_PHASE_A = "phase_a" -CONF_PHASE_B = "phase_b" -CONF_PHASE_C = "phase_c" - CONF_ENERGY_PRODUCTION_DAY = "energy_production_day" CONF_TOTAL_ENERGY_PRODUCTION = "total_energy_production" CONF_TOTAL_GENERATION_TIME = "total_generation_time" diff --git a/esphome/components/gt911/__init__.py b/esphome/components/gt911/__init__.py new file mode 100644 index 0000000000..1f7ecd1d5e --- /dev/null +++ b/esphome/components/gt911/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@jesserockz", "@clydebarrow"] +DEPENDENCIES = ["i2c"] + +gt911_ns = cg.esphome_ns.namespace("gt911") diff --git a/esphome/components/gt911/binary_sensor/__init__.py b/esphome/components/gt911/binary_sensor/__init__.py new file mode 100644 index 0000000000..18f5c49dbd --- /dev/null +++ b/esphome/components/gt911/binary_sensor/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_INDEX + +from .. import gt911_ns +from ..touchscreen import GT911Touchscreen, GT911ButtonListener + +CONF_GT911_ID = "gt911_id" + +GT911Button = gt911_ns.class_( + "GT911Button", + binary_sensor.BinarySensor, + cg.Component, + GT911ButtonListener, + cg.Parented.template(GT911Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(GT911Button).extend( + { + cv.GenerateID(CONF_GT911_ID): cv.use_id(GT911Touchscreen), + cv.Optional(CONF_INDEX, default=0): cv.int_range(min=0, max=3), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_GT911_ID]) + cg.add(var.set_index(config[CONF_INDEX])) diff --git a/esphome/components/gt911/binary_sensor/gt911_button.cpp b/esphome/components/gt911/binary_sensor/gt911_button.cpp new file mode 100644 index 0000000000..35ffaecefc --- /dev/null +++ b/esphome/components/gt911/binary_sensor/gt911_button.cpp @@ -0,0 +1,27 @@ +#include "gt911_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace gt911 { + +static const char *const TAG = "GT911.binary_sensor"; + +void GT911Button::setup() { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); +} + +void GT911Button::dump_config() { + LOG_BINARY_SENSOR("", "GT911 Button", this); + ESP_LOGCONFIG(TAG, " Index: %u", this->index_); +} + +void GT911Button::update_button(uint8_t index, bool state) { + if (index != this->index_) + return; + + this->publish_state(state); +} + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/binary_sensor/gt911_button.h b/esphome/components/gt911/binary_sensor/gt911_button.h new file mode 100644 index 0000000000..556ed65f91 --- /dev/null +++ b/esphome/components/gt911/binary_sensor/gt911_button.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/gt911/touchscreen/gt911_touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace gt911 { + +class GT911Button : public binary_sensor::BinarySensor, + public Component, + public GT911ButtonListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_index(uint8_t index) { this->index_ = index; } + + void update_button(uint8_t index, bool state) override; + + protected: + uint8_t index_; +}; + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py new file mode 100644 index 0000000000..6c80ff280f --- /dev/null +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -0,0 +1,33 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c, touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +from .. import gt911_ns + +GT911ButtonListener = gt911_ns.class_("GT911ButtonListener") +GT911Touchscreen = gt911_ns.class_( + "GT911Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GT911Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } +).extend(i2c.i2c_device_schema(0x5D)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp new file mode 100644 index 0000000000..84811b818f --- /dev/null +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -0,0 +1,133 @@ +#include "gt911_touchscreen.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace gt911 { + +static const char *const TAG = "gt911.touchscreen"; + +static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; +static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; +static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; +static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; +static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48}; +static const size_t MAX_TOUCHES = 5; // max number of possible touches reported +static const size_t MAX_BUTTONS = 4; // max number of buttons scanned + +#define ERROR_CHECK(err) \ + if ((err) != i2c::ERROR_OK) { \ + ESP_LOGE(TAG, "Failed to communicate!"); \ + this->status_set_warning(); \ + return; \ + } + +void GT911Touchscreen::setup() { + i2c::ErrorCode err; + ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen..."); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + if (this->interrupt_pin_ != nullptr) { + // The interrupt pin is used as an input during reset to select the I2C address. + this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->interrupt_pin_->setup(); + this->interrupt_pin_->digital_write(false); + } + delay(2); + this->reset_pin_->digital_write(true); + delay(50); // NOLINT + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + } + } + + // check the configuration of the int line. + uint8_t data[4]; + err = this->write(GET_SWITCHES, 2); + if (err == i2c::ERROR_OK) { + err = this->read(data, 1); + if (err == i2c::ERROR_OK) { + ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]); + if (this->interrupt_pin_ != nullptr) { + // datasheet says NOT to use pullup/down on the int line. + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, + (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + } + } + } + if (err == i2c::ERROR_OK) { + err = this->write(GET_MAX_VALUES, 2); + if (err == i2c::ERROR_OK) { + err = this->read(data, sizeof(data)); + if (err == i2c::ERROR_OK) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = encode_uint16(data[1], data[0]); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = encode_uint16(data[3], data[2]); + } + esph_log_d(TAG, "calibration max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_); + } + } + } + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to communicate!"); + this->mark_failed(); + return; + } + + ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); +} + +void GT911Touchscreen::update_touches() { + i2c::ErrorCode err; + uint8_t touch_state = 0; + uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte + + err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); + ERROR_CHECK(err); + err = this->read(&touch_state, 1); + ERROR_CHECK(err); + this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE)); + uint8_t num_of_touches = touch_state & 0x07; + + if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) { + this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet. + return; + } + + err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); + ERROR_CHECK(err); + // num_of_touches is guaranteed to be 0..5. Also read the key data + err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); + ERROR_CHECK(err); + + for (uint8_t i = 0; i != num_of_touches; i++) { + uint16_t id = data[i][0]; + uint16_t x = encode_uint16(data[i][2], data[i][1]); + uint16_t y = encode_uint16(data[i][4], data[i][3]); + this->add_raw_touch_position_(id, x, y); + } + auto keys = data[num_of_touches][0] & ((1 << MAX_BUTTONS) - 1); + if (keys != this->button_state_) { + this->button_state_ = keys; + for (size_t i = 0; i != MAX_BUTTONS; i++) { + for (auto *listener : this->button_listeners_) + listener->update_button(i, (keys & (1 << i)) != 0); + } + } +} + +void GT911Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h new file mode 100644 index 0000000000..17636a2ada --- /dev/null +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace gt911 { + +class GT911ButtonListener { + public: + virtual void update_button(uint8_t index, bool state) = 0; +}; + +class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } + + protected: + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{}; + std::vector button_listeners_; + uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. +}; + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/haier/__init__.py b/esphome/components/haier/__init__.py index b9ea055a41..e69de29bb2 100644 --- a/esphome/components/haier/__init__.py +++ b/esphome/components/haier/__init__.py @@ -1 +0,0 @@ -CODEOWNERS = ["@Yarikx"] diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h new file mode 100644 index 0000000000..55df7ecc1d --- /dev/null +++ b/esphome/components/haier/automation.h @@ -0,0 +1,130 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "haier_base.h" +#include "hon_climate.h" + +namespace esphome { +namespace haier { + +template class DisplayOnAction : public Action { + public: + DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(true); } + + protected: + HaierClimateBase *parent_; +}; + +template class DisplayOffAction : public Action { + public: + DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(false); } + + protected: + HaierClimateBase *parent_; +}; + +template class BeeperOnAction : public Action { + public: + BeeperOnAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(true); } + + protected: + HonClimate *parent_; +}; + +template class BeeperOffAction : public Action { + public: + BeeperOffAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(false); } + + protected: + HonClimate *parent_; +}; + +template class VerticalAirflowAction : public Action { + public: + VerticalAirflowAction(HonClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(hon_protocol::VerticalSwingMode, direction) + void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } + + protected: + HonClimate *parent_; +}; + +template class HorizontalAirflowAction : public Action { + public: + HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(hon_protocol::HorizontalSwingMode, direction) + void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } + + protected: + HonClimate *parent_; +}; + +template class HealthOnAction : public Action { + public: + HealthOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_health_mode(true); } + + protected: + HaierClimateBase *parent_; +}; + +template class HealthOffAction : public Action { + public: + HealthOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_health_mode(false); } + + protected: + HaierClimateBase *parent_; +}; + +template class StartSelfCleaningAction : public Action { + public: + StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->start_self_cleaning(); } + + protected: + HonClimate *parent_; +}; + +template class StartSteriCleaningAction : public Action { + public: + StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->start_steri_cleaning(); } + + protected: + HonClimate *parent_; +}; + +template class PowerOnAction : public Action { + public: + PowerOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->send_power_on_command(); } + + protected: + HaierClimateBase *parent_; +}; + +template class PowerOffAction : public Action { + public: + PowerOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->send_power_off_command(); } + + protected: + HaierClimateBase *parent_; +}; + +template class PowerToggleAction : public Action { + public: + PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->toggle_power(); } + + protected: + HaierClimateBase *parent_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/binary_sensor/__init__.py b/esphome/components/haier/binary_sensor/__init__.py new file mode 100644 index 0000000000..3a4935b22d --- /dev/null +++ b/esphome/components/haier/binary_sensor/__init__.py @@ -0,0 +1,71 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_FAN, + ICON_RADIATOR, +) +from ..climate import ( + CONF_HAIER_ID, + HonClimate, +) + +CODEOWNERS = ["@paveldn"] +BinarySensorTypeEnum = HonClimate.enum("SubBinarySensorType", True) + +# Haier sensors +CONF_OUTDOOR_FAN_STATUS = "outdoor_fan_status" +CONF_DEFROST_STATUS = "defrost_status" +CONF_COMPRESSOR_STATUS = "compressor_status" +CONF_INDOOR_FAN_STATUS = "indoor_fan_status" +CONF_FOUR_WAY_VALVE_STATUS = "four_way_valve_status" +CONF_INDOOR_ELECTRIC_HEATING_STATUS = "indoor_electric_heating_status" + +# Additional icons +ICON_SNOWFLAKE_THERMOMETER = "mdi:snowflake-thermometer" +ICON_HVAC = "mdi:hvac" +ICON_VALVE = "mdi:valve" + +SENSOR_TYPES = { + CONF_OUTDOOR_FAN_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_FAN, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_DEFROST_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_SNOWFLAKE_THERMOMETER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_COMPRESSOR_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_HVAC, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_INDOOR_FAN_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_FAN, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_FOUR_WAY_VALVE_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_VALVE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_INDOOR_ELECTRIC_HEATING_STATUS: binary_sensor.binary_sensor_schema( + icon=ICON_RADIATOR, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate), + } +).extend({cv.Optional(type): schema for type, schema in SENSOR_TYPES.items()}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_HAIER_ID]) + + for type_ in SENSOR_TYPES: + if conf := config.get(type_): + sens = await binary_sensor.new_binary_sensor(conf) + binary_sensor_type = getattr(BinarySensorTypeEnum, type_.upper()) + cg.add(paren.set_sub_binary_sensor(binary_sensor_type, sens)) diff --git a/esphome/components/haier/button/__init__.py b/esphome/components/haier/button/__init__.py new file mode 100644 index 0000000000..745ad95fb6 --- /dev/null +++ b/esphome/components/haier/button/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from ..climate import ( + CONF_HAIER_ID, + HonClimate, + haier_ns, +) + +CODEOWNERS = ["@paveldn"] +SelfCleaningButton = haier_ns.class_("SelfCleaningButton", button.Button) +SteriCleaningButton = haier_ns.class_("SteriCleaningButton", button.Button) + + +# Haier buttons +CONF_SELF_CLEANING = "self_cleaning" +CONF_STERI_CLEANING = "steri_cleaning" + +# Additional icons +ICON_SPRAY_BOTTLE = "mdi:spray-bottle" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate), + cv.Optional(CONF_SELF_CLEANING): button.button_schema( + SelfCleaningButton, + icon=ICON_SPRAY_BOTTLE, + ), + cv.Optional(CONF_STERI_CLEANING): button.button_schema( + SteriCleaningButton, + icon=ICON_SPRAY_BOTTLE, + ), + } +) + + +async def to_code(config): + for button_type in [CONF_SELF_CLEANING, CONF_STERI_CLEANING]: + if conf := config.get(button_type): + btn = await button.new_button(conf) + await cg.register_parented(btn, config[CONF_HAIER_ID]) diff --git a/esphome/components/haier/button/self_cleaning.cpp b/esphome/components/haier/button/self_cleaning.cpp new file mode 100644 index 0000000000..128726036e --- /dev/null +++ b/esphome/components/haier/button/self_cleaning.cpp @@ -0,0 +1,9 @@ +#include "self_cleaning.h" + +namespace esphome { +namespace haier { + +void SelfCleaningButton::press_action() { this->parent_->start_self_cleaning(); } + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/button/self_cleaning.h b/esphome/components/haier/button/self_cleaning.h new file mode 100644 index 0000000000..308fb70f06 --- /dev/null +++ b/esphome/components/haier/button/self_cleaning.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../hon_climate.h" + +namespace esphome { +namespace haier { + +class SelfCleaningButton : public button::Button, public Parented { + public: + SelfCleaningButton() = default; + + protected: + void press_action() override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/button/steri_cleaning.cpp b/esphome/components/haier/button/steri_cleaning.cpp new file mode 100644 index 0000000000..02b723f1a4 --- /dev/null +++ b/esphome/components/haier/button/steri_cleaning.cpp @@ -0,0 +1,9 @@ +#include "steri_cleaning.h" + +namespace esphome { +namespace haier { + +void SteriCleaningButton::press_action() { this->parent_->start_steri_cleaning(); } + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/button/steri_cleaning.h b/esphome/components/haier/button/steri_cleaning.h new file mode 100644 index 0000000000..6cad313fb3 --- /dev/null +++ b/esphome/components/haier/button/steri_cleaning.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../hon_climate.h" + +namespace esphome { +namespace haier { + +class SteriCleaningButton : public button::Button, public Parented { + public: + SteriCleaningButton() = default; + + protected: + void press_action() override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index cee83232a1..f2dc7174cb 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -1,43 +1,522 @@ -from esphome.components import climate +import logging import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import uart -from esphome.components.climate import ClimateSwingMode -from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES - -DEPENDENCIES = ["uart"] - -haier_ns = cg.esphome_ns.namespace("haier") -HaierClimate = haier_ns.class_( - "HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice +import esphome.final_validate as fv +from esphome.components import uart, climate, logger +from esphome import automation +from esphome.const import ( + CONF_BEEPER, + CONF_DISPLAY, + CONF_ID, + CONF_LEVEL, + CONF_LOGGER, + CONF_LOGS, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_OUTDOOR_TEMPERATURE, + CONF_PROTOCOL, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, + CONF_TARGET_TEMPERATURE, + CONF_TEMPERATURE_STEP, + CONF_TRIGGER_ID, + CONF_VISUAL, + CONF_WIFI, +) +from esphome.components.climate import ( + ClimateMode, + ClimatePreset, + ClimateSwingMode, + CONF_CURRENT_TEMPERATURE, ) -ALLOWED_CLIMATE_SWING_MODES = { - "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, - "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, - "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +_LOGGER = logging.getLogger(__name__) + +PROTOCOL_MIN_TEMPERATURE = 16.0 +PROTOCOL_MAX_TEMPERATURE = 30.0 +PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 +PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 +PROTOCOL_CONTROL_PACKET_SIZE = 10 +PROTOCOL_MIN_SENSORS_PACKET_SIZE = 18 +PROTOCOL_DEFAULT_SENSORS_PACKET_SIZE = 22 +PROTOCOL_STATUS_MESSAGE_HEADER_SIZE = 0 + +CODEOWNERS = ["@paveldn"] +DEPENDENCIES = ["climate", "uart"] +CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control" +CONF_ANSWER_TIMEOUT = "answer_timeout" +CONF_CONTROL_METHOD = "control_method" +CONF_CONTROL_PACKET_SIZE = "control_packet_size" +CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" +CONF_ON_ALARM_START = "on_alarm_start" +CONF_ON_ALARM_END = "on_alarm_end" +CONF_ON_STATUS_MESSAGE = "on_status_message" +CONF_SENSORS_PACKET_SIZE = "sensors_packet_size" +CONF_STATUS_MESSAGE_HEADER_SIZE = "status_message_header_size" +CONF_VERTICAL_AIRFLOW = "vertical_airflow" +CONF_WIFI_SIGNAL = "wifi_signal" + +PROTOCOL_HON = "HON" +PROTOCOL_SMARTAIR2 = "SMARTAIR2" + +haier_ns = cg.esphome_ns.namespace("haier") +hon_protocol_ns = haier_ns.namespace("hon_protocol") +HaierClimateBase = haier_ns.class_( + "HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component +) +HonClimate = haier_ns.class_("HonClimate", HaierClimateBase) +Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase) + +CONF_HAIER_ID = "haier_id" + +AirflowVerticalDirection = hon_protocol_ns.enum("VerticalSwingMode", True) +AIRFLOW_VERTICAL_DIRECTION_OPTIONS = { + "HEALTH_UP": AirflowVerticalDirection.HEALTH_UP, + "MAX_UP": AirflowVerticalDirection.MAX_UP, + "UP": AirflowVerticalDirection.UP, + "CENTER": AirflowVerticalDirection.CENTER, + "DOWN": AirflowVerticalDirection.DOWN, + "HEALTH_DOWN": AirflowVerticalDirection.HEALTH_DOWN, } -validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) +AirflowHorizontalDirection = hon_protocol_ns.enum("HorizontalSwingMode", True) +AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = { + "MAX_LEFT": AirflowHorizontalDirection.MAX_LEFT, + "LEFT": AirflowHorizontalDirection.LEFT, + "CENTER": AirflowHorizontalDirection.CENTER, + "RIGHT": AirflowHorizontalDirection.RIGHT, + "MAX_RIGHT": AirflowHorizontalDirection.MAX_RIGHT, +} -CONFIG_SCHEMA = cv.All( +SUPPORTED_SWING_MODES_OPTIONS = { + "OFF": ClimateSwingMode.CLIMATE_SWING_OFF, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, +} + +SUPPORTED_CLIMATE_MODES_OPTIONS = { + "OFF": ClimateMode.CLIMATE_MODE_OFF, # always available + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, # always available + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + +SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = { + "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, +} + +SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { + "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, +} + +HonControlMethod = haier_ns.enum("HonControlMethod", True) +SUPPORTED_HON_CONTROL_METHODS = { + "MONITOR_ONLY": HonControlMethod.MONITOR_ONLY, + "SET_GROUP_PARAMETERS": HonControlMethod.SET_GROUP_PARAMETERS, + "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, +} + +HaierAlarmStartTrigger = haier_ns.class_( + "HaierAlarmStartTrigger", + automation.Trigger.template(cg.uint8, cg.const_char_ptr), +) + +HaierAlarmEndTrigger = haier_ns.class_( + "HaierAlarmEndTrigger", + automation.Trigger.template(cg.uint8, cg.const_char_ptr), +) + +StatusMessageTrigger = haier_ns.class_( + "StatusMessageTrigger", + automation.Trigger.template(cg.const_char_ptr, cg.size_t), +) + + +def validate_visual(config): + if CONF_VISUAL in config: + visual_config = config[CONF_VISUAL] + if CONF_MIN_TEMPERATURE in visual_config: + min_temp = visual_config[CONF_MIN_TEMPERATURE] + if min_temp < PROTOCOL_MIN_TEMPERATURE: + raise cv.Invalid( + f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}" + ) + else: + config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE + if CONF_MAX_TEMPERATURE in visual_config: + max_temp = visual_config[CONF_MAX_TEMPERATURE] + if max_temp > PROTOCOL_MAX_TEMPERATURE: + raise cv.Invalid( + f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}" + ) + else: + config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE + if CONF_TEMPERATURE_STEP in visual_config: + temp_step = config[CONF_VISUAL][CONF_TEMPERATURE_STEP][ + CONF_TARGET_TEMPERATURE + ] + if ((int)(temp_step * 2)) / 2 != temp_step: + raise cv.Invalid( + f"Configured visual temperature step {temp_step} is wrong, it should be a multiple of 0.5" + ) + else: + config[CONF_VISUAL][CONF_TEMPERATURE_STEP] = { + CONF_TARGET_TEMPERATURE: PROTOCOL_TARGET_TEMPERATURE_STEP, + CONF_CURRENT_TEMPERATURE: PROTOCOL_CURRENT_TEMPERATURE_STEP, + } + else: + config[CONF_VISUAL] = { + CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE, + CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE, + CONF_TEMPERATURE_STEP: { + CONF_TARGET_TEMPERATURE: PROTOCOL_TARGET_TEMPERATURE_STEP, + CONF_CURRENT_TEMPERATURE: PROTOCOL_CURRENT_TEMPERATURE_STEP, + }, + } + return config + + +BASE_CONFIG_SCHEMA = ( climate.CLIMATE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(HaierClimate), - cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( - validate_swing_modes + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) + ), + cv.Optional( + CONF_SUPPORTED_SWING_MODES, + default=[ + "VERTICAL", + "HORIZONTAL", + "BOTH", + ], + ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), + cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean, + cv.Optional(CONF_DISPLAY): cv.boolean, + cv.Optional( + CONF_ANSWER_TIMEOUT, + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StatusMessageTrigger), + } ), } ) - .extend(cv.polling_component_schema("5s")) - .extend(uart.UART_DEVICE_SCHEMA), + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Smartair2Climate), + cv.Optional( + CONF_ALTERNATIVE_SWING_CONTROL, default=False + ): cv.boolean, + cv.Optional( + CONF_SUPPORTED_PRESETS, + default=["BOOST", "COMFORT"], # No AWAY by default + ): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True) + ), + } + ), + PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HonClimate), + cv.Optional( + CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS" + ): cv.ensure_list( + cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True) + ), + cv.Optional(CONF_BEEPER): cv.invalid( + f"The {CONF_BEEPER} option is deprecated, use beeper_on/beeper_off actions or beeper switch for a haier platform instead" + ), + cv.Optional( + CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE + ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50), + cv.Optional( + CONF_SENSORS_PACKET_SIZE, + default=PROTOCOL_DEFAULT_SENSORS_PACKET_SIZE, + ): cv.int_range(min=PROTOCOL_MIN_SENSORS_PACKET_SIZE, max=50), + cv.Optional( + CONF_STATUS_MESSAGE_HEADER_SIZE, + default=PROTOCOL_STATUS_MESSAGE_HEADER_SIZE, + ): cv.int_range(min=PROTOCOL_STATUS_MESSAGE_HEADER_SIZE), + cv.Optional( + CONF_SUPPORTED_PRESETS, + default=["BOOST", "SLEEP"], # No AWAY by default + ): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True) + ), + cv.Optional(CONF_OUTDOOR_TEMPERATURE): cv.invalid( + f"The {CONF_OUTDOOR_TEMPERATURE} option is deprecated, use a sensor for a haier platform instead" + ), + cv.Optional(CONF_ON_ALARM_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + HaierAlarmStartTrigger + ), + } + ), + cv.Optional(CONF_ON_ALARM_END): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + HaierAlarmEndTrigger + ), + } + ), + } + ), + }, + key=CONF_PROTOCOL, + default_type=PROTOCOL_SMARTAIR2, + upper=True, + ), + validate_visual, ) +# Actions +DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action) +DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action) +BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action) +StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action) +StartSteriCleaningAction = haier_ns.class_( + "StartSteriCleaningAction", automation.Action +) +VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action) +HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action) +HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action) +HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action) +PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action) +PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action) + +HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HaierClimateBase), + } +) + +HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HonClimate), + } +) + + +@automation.register_action( + "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA +) +async def display_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA +) +async def beeper_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Start self cleaning or steri-cleaning action action +@automation.register_action( + "climate.haier.start_self_cleaning", + StartSelfCleaningAction, + HAIER_HON_BASE_ACTION_SCHEMA, +) +@automation.register_action( + "climate.haier.start_steri_cleaning", + StartSteriCleaningAction, + HAIER_HON_BASE_ACTION_SCHEMA, +) +async def start_cleaning_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Set vertical airflow direction action +@automation.register_action( + "climate.haier.set_vertical_airflow", + VerticalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HonClimate), + cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable( + cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True) + ), + } + ), +) +async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +# Set horizontal airflow direction action +@automation.register_action( + "climate.haier.set_horizontal_airflow", + HorizontalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HonClimate), + cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable( + cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True) + ), + } + ), +) +async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +@automation.register_action( + "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA +) +async def health_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA +) +async def power_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +def _final_validate(config): + full_config = fv.full_config.get() + if CONF_LOGGER in full_config: + _level = "NONE" + logger_config = full_config[CONF_LOGGER] + if CONF_LOGS in logger_config: + if "haier.protocol" in logger_config[CONF_LOGS]: + _level = logger_config[CONF_LOGS]["haier.protocol"] + else: + _level = logger_config[CONF_LEVEL] + _LOGGER.info("Detected log level for Haier protocol: %s", _level) + if _level not in logger.LOG_LEVEL_SEVERITY: + raise cv.Invalid("Unknown log level for Haier protocol") + _severity = logger.LOG_LEVEL_SEVERITY.index(_level) + cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}") + else: + _LOGGER.info( + "No logger component found, logging for Haier protocol is disabled" + ) + cg.add_build_flag("-DHAIER_LOG_LEVEL=0") + if config.get(CONF_WIFI_SIGNAL) and CONF_WIFI not in full_config: + raise cv.Invalid( + f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): + cg.add(haier_ns.init_haier_protocol_logging()) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - await climate.register_climate(var, config) await uart.register_uart_device(var, config) + await climate.register_climate(var, config) + + cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) + if CONF_CONTROL_METHOD in config: + cg.add(var.set_control_method(config[CONF_CONTROL_METHOD])) + if CONF_BEEPER in config: + cg.add(var.set_beeper_state(config[CONF_BEEPER])) + if CONF_DISPLAY in config: + cg.add(var.set_display_state(config[CONF_DISPLAY])) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) if CONF_SUPPORTED_SWING_MODES in config: cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_ANSWER_TIMEOUT in config: + cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) + if CONF_ALTERNATIVE_SWING_CONTROL in config: + cg.add( + var.set_alternative_swing_control(config[CONF_ALTERNATIVE_SWING_CONTROL]) + ) + if CONF_CONTROL_PACKET_SIZE in config: + cg.add( + var.set_extra_control_packet_bytes_size( + config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE + ) + ) + if CONF_SENSORS_PACKET_SIZE in config: + cg.add( + var.set_extra_sensors_packet_bytes_size( + config[CONF_SENSORS_PACKET_SIZE] - PROTOCOL_MIN_SENSORS_PACKET_SIZE + ) + ) + if CONF_STATUS_MESSAGE_HEADER_SIZE in config: + cg.add( + var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE]) + ) + for conf in config.get(CONF_ON_ALARM_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + ) + for conf in config.get(CONF_ON_ALARM_END, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + ) + for conf in config.get(CONF_ON_STATUS_MESSAGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], conf + ) + # https://github.com/paveldn/HaierProtocol + cg.add_library("pavlodn/HaierProtocol", "0.9.31") diff --git a/esphome/components/haier/haier.cpp b/esphome/components/haier/haier.cpp deleted file mode 100644 index cf69d483b5..0000000000 --- a/esphome/components/haier/haier.cpp +++ /dev/null @@ -1,302 +0,0 @@ -#include -#include "haier.h" -#include "esphome/core/macros.h" - -namespace esphome { -namespace haier { - -static const char *const TAG = "haier"; - -static const uint8_t TEMPERATURE = 13; -static const uint8_t HUMIDITY = 15; - -static const uint8_t MODE = 23; - -static const uint8_t FAN_SPEED = 25; - -static const uint8_t SWING = 27; - -static const uint8_t POWER = 29; -static const uint8_t POWER_MASK = 1; - -static const uint8_t SET_TEMPERATURE = 35; -static const uint8_t DECIMAL_MASK = (1 << 5); - -static const uint8_t CRC = 36; - -static const uint8_t COMFORT_PRESET_MASK = (1 << 3); - -static const uint8_t MIN_VALID_TEMPERATURE = 16; -static const uint8_t MAX_VALID_TEMPERATURE = 50; -static const float TEMPERATURE_STEP = 0.5f; - -static const uint8_t POLL_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 1, 90}; -static const uint8_t OFF_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 3, 92}; - -void HaierClimate::dump_config() { - ESP_LOGCONFIG(TAG, "Haier:"); - ESP_LOGCONFIG(TAG, " Update interval: %u", this->get_update_interval()); - this->dump_traits_(TAG); - this->check_uart_settings(9600); -} - -void HaierClimate::loop() { - if (this->available() >= sizeof(this->data_)) { - this->read_array(this->data_, sizeof(this->data_)); - if (this->data_[0] != 255 || this->data_[1] != 255) - return; - - read_state_(this->data_, sizeof(this->data_)); - } -} - -void HaierClimate::update() { - this->write_array(POLL_REQ, sizeof(POLL_REQ)); - dump_message_("Poll sent", POLL_REQ, sizeof(POLL_REQ)); -} - -climate::ClimateTraits HaierClimate::traits() { - auto traits = climate::ClimateTraits(); - - traits.set_visual_min_temperature(MIN_VALID_TEMPERATURE); - traits.set_visual_max_temperature(MAX_VALID_TEMPERATURE); - traits.set_visual_temperature_step(TEMPERATURE_STEP); - - traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL, climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY}); - - traits.set_supported_fan_modes({ - climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, - }); - - traits.set_supported_swing_modes(this->supported_swing_modes_); - traits.set_supports_current_temperature(true); - traits.set_supports_two_point_target_temperature(false); - - traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); - traits.add_supported_preset(climate::CLIMATE_PRESET_COMFORT); - - return traits; -} - -void HaierClimate::read_state_(const uint8_t *data, uint8_t size) { - dump_message_("Received state", data, size); - - uint8_t check = data[CRC]; - - uint8_t crc = get_checksum_(data, size); - - if (check != crc) { - ESP_LOGW(TAG, "Invalid checksum"); - return; - } - - this->current_temperature = data[TEMPERATURE]; - - this->target_temperature = data[SET_TEMPERATURE] + MIN_VALID_TEMPERATURE; - - if (data[POWER] & DECIMAL_MASK) { - this->target_temperature += 0.5f; - } - - switch (data[MODE]) { - case MODE_SMART: - this->mode = climate::CLIMATE_MODE_HEAT_COOL; - break; - case MODE_COOL: - this->mode = climate::CLIMATE_MODE_COOL; - break; - case MODE_HEAT: - this->mode = climate::CLIMATE_MODE_HEAT; - break; - case MODE_ONLY_FAN: - this->mode = climate::CLIMATE_MODE_FAN_ONLY; - break; - case MODE_DRY: - this->mode = climate::CLIMATE_MODE_DRY; - break; - default: // other modes are unsupported - this->mode = climate::CLIMATE_MODE_HEAT_COOL; - } - - switch (data[FAN_SPEED]) { - case FAN_AUTO: - this->fan_mode = climate::CLIMATE_FAN_AUTO; - break; - - case FAN_MIN: - this->fan_mode = climate::CLIMATE_FAN_LOW; - break; - - case FAN_MIDDLE: - this->fan_mode = climate::CLIMATE_FAN_MEDIUM; - break; - - case FAN_MAX: - this->fan_mode = climate::CLIMATE_FAN_HIGH; - break; - } - - switch (data[SWING]) { - case SWING_OFF: - this->swing_mode = climate::CLIMATE_SWING_OFF; - break; - - case SWING_VERTICAL: - this->swing_mode = climate::CLIMATE_SWING_VERTICAL; - break; - - case SWING_HORIZONTAL: - this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - break; - - case SWING_BOTH: - this->swing_mode = climate::CLIMATE_SWING_BOTH; - break; - } - - if (data[POWER] & COMFORT_PRESET_MASK) { - this->preset = climate::CLIMATE_PRESET_COMFORT; - } else { - this->preset = climate::CLIMATE_PRESET_NONE; - } - - if ((data[POWER] & POWER_MASK) == 0) { - this->mode = climate::CLIMATE_MODE_OFF; - } - - this->publish_state(); -} - -void HaierClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) { - switch (call.get_mode().value()) { - case climate::CLIMATE_MODE_OFF: - send_data_(OFF_REQ, sizeof(OFF_REQ)); - break; - - case climate::CLIMATE_MODE_HEAT_COOL: - case climate::CLIMATE_MODE_AUTO: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_SMART; - break; - case climate::CLIMATE_MODE_HEAT: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_HEAT; - break; - case climate::CLIMATE_MODE_COOL: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_COOL; - break; - - case climate::CLIMATE_MODE_FAN_ONLY: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_ONLY_FAN; - break; - - case climate::CLIMATE_MODE_DRY: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_DRY; - break; - } - } - - if (call.get_preset().has_value()) { - if (call.get_preset().value() == climate::CLIMATE_PRESET_COMFORT) { - data_[POWER] |= COMFORT_PRESET_MASK; - } else { - data_[POWER] &= ~COMFORT_PRESET_MASK; - } - } - - if (call.get_target_temperature().has_value()) { - float target = call.get_target_temperature().value() - MIN_VALID_TEMPERATURE; - - data_[SET_TEMPERATURE] = (uint8_t) target; - - if ((int) target == std::lroundf(target)) { - data_[POWER] &= ~DECIMAL_MASK; - } else { - data_[POWER] |= DECIMAL_MASK; - } - } - - if (call.get_fan_mode().has_value()) { - switch (call.get_fan_mode().value()) { - case climate::CLIMATE_FAN_AUTO: - data_[FAN_SPEED] = FAN_AUTO; - break; - case climate::CLIMATE_FAN_LOW: - data_[FAN_SPEED] = FAN_MIN; - break; - case climate::CLIMATE_FAN_MEDIUM: - data_[FAN_SPEED] = FAN_MIDDLE; - break; - case climate::CLIMATE_FAN_HIGH: - data_[FAN_SPEED] = FAN_MAX; - break; - - default: // other modes are unsupported - break; - } - } - - if (call.get_swing_mode().has_value()) { - switch (call.get_swing_mode().value()) { - case climate::CLIMATE_SWING_OFF: - data_[SWING] = SWING_OFF; - break; - case climate::CLIMATE_SWING_VERTICAL: - data_[SWING] = SWING_VERTICAL; - break; - case climate::CLIMATE_SWING_HORIZONTAL: - data_[SWING] = SWING_HORIZONTAL; - break; - case climate::CLIMATE_SWING_BOTH: - data_[SWING] = SWING_BOTH; - break; - } - } - - // Parts of the message that must have specific values for "send" command. - // The meaning of those values is unknown at the moment. - data_[9] = 1; - data_[10] = 77; - data_[11] = 95; - data_[17] = 0; - - // Compute checksum - uint8_t crc = get_checksum_(data_, sizeof(data_)); - data_[CRC] = crc; - - send_data_(data_, sizeof(data_)); -} - -void HaierClimate::send_data_(const uint8_t *message, uint8_t size) { - this->write_array(message, size); - - dump_message_("Sent message", message, size); -} - -void HaierClimate::dump_message_(const char *title, const uint8_t *message, uint8_t size) { - ESP_LOGV(TAG, "%s:", title); - for (int i = 0; i < size; i++) { - ESP_LOGV(TAG, " byte %02d - %d", i, message[i]); - } -} - -uint8_t HaierClimate::get_checksum_(const uint8_t *message, size_t size) { - uint8_t position = size - 1; - uint8_t crc = 0; - - for (int i = 2; i < position; i++) - crc += message[i]; - - return crc; -} - -} // namespace haier -} // namespace esphome diff --git a/esphome/components/haier/haier.h b/esphome/components/haier/haier.h deleted file mode 100644 index 5399fd187b..0000000000 --- a/esphome/components/haier/haier.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/uart/uart.h" - -namespace esphome { -namespace haier { - -enum Mode : uint8_t { MODE_SMART = 0, MODE_COOL = 1, MODE_HEAT = 2, MODE_ONLY_FAN = 3, MODE_DRY = 4 }; -enum FanSpeed : uint8_t { FAN_MAX = 0, FAN_MIDDLE = 1, FAN_MIN = 2, FAN_AUTO = 3 }; -enum SwingMode : uint8_t { SWING_OFF = 0, SWING_VERTICAL = 1, SWING_HORIZONTAL = 2, SWING_BOTH = 3 }; - -class HaierClimate : public climate::Climate, public uart::UARTDevice, public PollingComponent { - public: - void loop() override; - void update() override; - void dump_config() override; - void control(const climate::ClimateCall &call) override; - void set_supported_swing_modes(const std::set &modes) { - this->supported_swing_modes_ = modes; - } - - protected: - climate::ClimateTraits traits() override; - void read_state_(const uint8_t *data, uint8_t size); - void send_data_(const uint8_t *message, uint8_t size); - void dump_message_(const char *title, const uint8_t *message, uint8_t size); - uint8_t get_checksum_(const uint8_t *message, size_t size); - - private: - uint8_t data_[37]; - std::set supported_swing_modes_{}; -}; - -} // namespace haier -} // namespace esphome diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp new file mode 100644 index 0000000000..ba80c1ca1b --- /dev/null +++ b/esphome/components/haier/haier_base.cpp @@ -0,0 +1,427 @@ +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif +#include "haier_base.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000; +constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000; +constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; +constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; +constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; + +const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { + static const char *phase_names[] = { + "SENDING_INIT_1", + "SENDING_INIT_2", + "SENDING_FIRST_STATUS_REQUEST", + "SENDING_FIRST_ALARM_STATUS_REQUEST", + "IDLE", + "SENDING_STATUS_REQUEST", + "SENDING_UPDATE_SIGNAL_REQUEST", + "SENDING_SIGNAL_LEVEL", + "SENDING_CONTROL", + "SENDING_ACTION_COMMAND", + "SENDING_ALARM_STATUS_REQUEST", + "UNKNOWN" // Should be the last! + }; + static_assert( + (sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1), + "Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases"); + int phase_index = (int) phase; + if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) + phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; + return phase_names[phase_index]; +} + +bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, + size_t timeout) { + return std::chrono::duration_cast(now - tpoint).count() > timeout; +} + +HaierClimateBase::HaierClimateBase() + : haier_protocol_(*this), + protocol_phase_(ProtocolPhases::SENDING_INIT_1), + force_send_control_(false), + forced_request_status_(false), + reset_protocol_request_(false), + send_wifi_signal_(true), + use_crc_(false) { + this->traits_ = climate::ClimateTraits(); + this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_HEAT_COOL}); + this->traits_.set_supported_fan_modes( + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}); + this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); + this->traits_.set_supports_current_temperature(true); +} + +HaierClimateBase::~HaierClimateBase() {} + +void HaierClimateBase::set_phase(ProtocolPhases phase) { + if (this->protocol_phase_ != phase) { + ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); + this->protocol_phase_ = phase; + } +} + +void HaierClimateBase::reset_phase_() { + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); +} + +void HaierClimateBase::reset_to_idle_() { + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + this->forced_request_status_ = true; + this->set_phase(ProtocolPhases::IDLE); + this->action_request_.reset(); +} + +bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return check_timeout(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); +} + +bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return check_timeout(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); +} + +bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return check_timeout(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); +} + +bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return check_timeout(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); +} + +#ifdef USE_WIFI +haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() { + static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; + if (wifi::global_wifi_component->is_connected()) { + wifi_status_data[1] = 0; + int8_t rssi = wifi::global_wifi_component->wifi_rssi(); + wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f); + ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]); + } else { + ESP_LOGD(TAG, "WiFi is not connected"); + wifi_status_data[1] = 1; + wifi_status_data[3] = 0; + } + return haier_protocol::HaierMessage(haier_protocol::FrameType::REPORT_NETWORK_STATUS, wifi_status_data, + sizeof(wifi_status_data)); +} +#endif + +void HaierClimateBase::save_settings() { + HaierBaseSettings settings{this->get_health_mode(), this->get_display_state()}; + if (!this->base_rtc_.save(&settings)) { + ESP_LOGW(TAG, "Failed to save settings"); + } +} + +bool HaierClimateBase::get_display_state() const { + return (this->display_status_ == SwitchState::ON) || (this->display_status_ == SwitchState::PENDING_ON); +} + +void HaierClimateBase::set_display_state(bool state) { + if (state != this->get_display_state()) { + this->display_status_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; + this->force_send_control_ = true; + this->save_settings(); + } +} + +bool HaierClimateBase::get_health_mode() const { + return (this->health_mode_ == SwitchState::ON) || (this->health_mode_ == SwitchState::PENDING_ON); +} + +void HaierClimateBase::set_health_mode(bool state) { + if (state != this->get_health_mode()) { + this->health_mode_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; + this->force_send_control_ = true; + this->save_settings(); + } +} + +void HaierClimateBase::send_power_on_command() { + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_ON, esphome::optional()}); +} + +void HaierClimateBase::send_power_off_command() { + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional()}); +} + +void HaierClimateBase::toggle_power() { + this->action_request_ = + PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional()}); +} + +void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { + this->traits_.set_supported_swing_modes(modes); + if (!modes.empty()) + this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); +} + +void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } + +void HaierClimateBase::set_supported_modes(const std::set &modes) { + this->traits_.set_supported_modes(modes); + this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available + this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available +} + +void HaierClimateBase::set_supported_presets(const std::set &presets) { + this->traits_.set_supported_presets(presets); + if (!presets.empty()) + this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); +} + +void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } + +void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { + this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message}); +} + +void HaierClimateBase::add_status_message_callback(std::function &&callback) { + this->status_message_callback_.add(std::move(callback)); +} + +haier_protocol::HandlerError HaierClimateBase::answer_preprocess_( + haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type, + haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type, + ProtocolPhases expected_phase) { + haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; + if ((expected_request_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) && + (request_message_type != expected_request_message_type)) + result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + if ((expected_answer_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) && + (answer_message_type != expected_answer_message_type)) + result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + if (!this->haier_protocol_.is_waiting_for_answer() || + ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))) + result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + if (answer_message_type == haier_protocol::FrameType::INVALID) + result = haier_protocol::HandlerError::INVALID_ANSWER; + return result; +} + +haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, + haier_protocol::FrameType::CONFIRM, ProtocolPhases::SENDING_SIGNAL_LEVEL); + this->set_phase(ProtocolPhases::IDLE); + return result; +} + +haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) { + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type, + phase_to_string_(this->protocol_phase_)); + if (this->protocol_phase_ > ProtocolPhases::IDLE) { + this->set_phase(ProtocolPhases::IDLE); + } else { + this->set_phase(ProtocolPhases::SENDING_INIT_1); + } + return haier_protocol::HandlerError::HANDLER_OK; +} + +void HaierClimateBase::setup() { + ESP_LOGI(TAG, "Haier initialization..."); + // Set timestamp here to give AC time to boot + this->last_request_timestamp_ = std::chrono::steady_clock::now(); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + this->haier_protocol_.set_default_timeout_handler( + std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); + this->set_handlers(); + this->initialization(); +} + +void HaierClimateBase::dump_config() { + LOG_CLIMATE("", "Haier Climate", this); + ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none"); +} + +void HaierClimateBase::loop() { + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + if ((std::chrono::duration_cast(now - this->last_valid_status_timestamp_).count() > + COMMUNICATION_TIMEOUT_MS) || + (this->reset_protocol_request_ && (!this->haier_protocol_.is_waiting_for_answer()))) { + this->last_valid_status_timestamp_ = now; + if (this->protocol_phase_ >= ProtocolPhases::IDLE) { + // No status too long, reseting protocol + // No need to reset protocol if we didn't pass initialization phase + if (this->reset_protocol_request_) { + this->reset_protocol_request_ = false; + ESP_LOGW(TAG, "Protocol reset requested"); + } else { + ESP_LOGW(TAG, "Communication timeout, reseting protocol"); + } + this->process_protocol_reset(); + return; + } + }; + if ((!this->haier_protocol_.is_waiting_for_answer()) && + ((this->protocol_phase_ == ProtocolPhases::IDLE) || + (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL))) { + // If control message or action is pending we should send it ASAP unless we are in initialisation + // procedure or waiting for an answer + if (this->action_request_.has_value() && this->prepare_pending_action()) { + this->set_phase(ProtocolPhases::SENDING_ACTION_COMMAND); + } else if (this->next_hvac_settings_.valid || this->force_send_control_) { + ESP_LOGV(TAG, "Control packet is pending..."); + this->set_phase(ProtocolPhases::SENDING_CONTROL); + if (this->next_hvac_settings_.valid) { + this->current_hvac_settings_ = this->next_hvac_settings_; + this->next_hvac_settings_.reset(); + } else { + this->current_hvac_settings_.reset(); + } + } + } + this->process_phase(now); + this->haier_protocol_.loop(); +#ifdef USE_SWITCH + if ((this->display_switch_ != nullptr) && (this->display_switch_->state != this->get_display_state())) { + this->display_switch_->publish_state(this->get_display_state()); + } + if ((this->health_mode_switch_ != nullptr) && (this->health_mode_switch_->state != this->get_health_mode())) { + this->health_mode_switch_->publish_state(this->get_health_mode()); + } +#endif // USE_SWITCH +} + +void HaierClimateBase::process_protocol_reset() { + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + if (this->next_hvac_settings_.valid) + this->next_hvac_settings_.reset(); + this->mode = CLIMATE_MODE_OFF; + this->current_temperature = NAN; + this->target_temperature = NAN; + this->fan_mode.reset(); + this->preset.reset(); + this->publish_state(); + this->set_phase(ProtocolPhases::SENDING_INIT_1); +} + +bool HaierClimateBase::prepare_pending_action() { + if (this->action_request_.has_value()) { + switch (this->action_request_.value().action) { + case ActionRequest::SEND_CUSTOM_COMMAND: + return true; + case ActionRequest::TURN_POWER_ON: + this->action_request_.value().message = this->get_power_message(true); + return true; + case ActionRequest::TURN_POWER_OFF: + this->action_request_.value().message = this->get_power_message(false); + return true; + case ActionRequest::TOGGLE_POWER: + this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF); + return true; + default: + ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action); + this->action_request_.reset(); + return false; + } + } else + return false; +} + +ClimateTraits HaierClimateBase::traits() { return traits_; } + +void HaierClimateBase::initialization() { + constexpr uint32_t restore_settings_version = 0xA77D21EF; + this->base_rtc_ = + global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); + HaierBaseSettings recovered; + if (!this->base_rtc_.load(&recovered)) { + recovered = {false, true}; + } + this->display_status_ = recovered.display_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; + this->health_mode_ = recovered.health_mode ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; +#ifdef USE_SWITCH + if (this->display_switch_ != nullptr) { + this->display_switch_->publish_state(this->get_display_state()); + } + if (this->health_mode_switch_ != nullptr) { + this->health_mode_switch_->publish_state(this->get_health_mode()); + } +#endif +} + +void HaierClimateBase::control(const ClimateCall &call) { + ESP_LOGD("Control", "Control call"); + if (!this->valid_connection()) { + ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); + return; // cancel the control, we cant do it without a poll answer. + } + if (this->current_hvac_settings_.valid) { + ESP_LOGW(TAG, "New settings come faster then processed!"); + } + { + if (call.get_mode().has_value()) + this->next_hvac_settings_.mode = call.get_mode(); + if (call.get_fan_mode().has_value()) + this->next_hvac_settings_.fan_mode = call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->next_hvac_settings_.swing_mode = call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->next_hvac_settings_.target_temperature = call.get_target_temperature(); + if (call.get_preset().has_value()) + this->next_hvac_settings_.preset = call.get_preset(); + this->next_hvac_settings_.valid = true; + } +} + +#ifdef USE_SWITCH +void HaierClimateBase::set_display_switch(switch_::Switch *sw) { + this->display_switch_ = sw; + if ((this->display_switch_ != nullptr) && (this->valid_connection())) { + this->display_switch_->publish_state(this->get_display_state()); + } +} + +void HaierClimateBase::set_health_mode_switch(switch_::Switch *sw) { + this->health_mode_switch_ = sw; + if ((this->health_mode_switch_ != nullptr) && (this->valid_connection())) { + this->health_mode_switch_->publish_state(this->get_health_mode()); + } +} +#endif + +void HaierClimateBase::HvacSettings::reset() { + this->valid = false; + this->mode.reset(); + this->fan_mode.reset(); + this->swing_mode.reset(); + this->target_temperature.reset(); + this->preset.reset(); +} + +void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats, + std::chrono::milliseconds interval) { + this->haier_protocol_.send_message(command, use_crc, num_repeats, interval); + this->last_request_timestamp_ = std::chrono::steady_clock::now(); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h new file mode 100644 index 0000000000..f0597c49ff --- /dev/null +++ b/esphome/components/haier/haier_base.h @@ -0,0 +1,187 @@ +#pragma once + +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +// HaierProtocol +#include + +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif + +namespace esphome { +namespace haier { + +enum class ActionRequest : uint8_t { + SEND_CUSTOM_COMMAND = 0, + TURN_POWER_ON = 1, + TURN_POWER_OFF = 2, + TOGGLE_POWER = 3, + START_SELF_CLEAN = 4, // only hOn + START_STERI_CLEAN = 5, // only hOn +}; + +struct HaierBaseSettings { + bool health_mode; + bool display_state; +}; + +class HaierClimateBase : public esphome::Component, + public esphome::climate::Climate, + public esphome::uart::UARTDevice, + public haier_protocol::ProtocolStream { +#ifdef USE_SWITCH + public: + void set_display_switch(switch_::Switch *sw); + void set_health_mode_switch(switch_::Switch *sw); + + protected: + switch_::Switch *display_switch_{nullptr}; + switch_::Switch *health_mode_switch_{nullptr}; +#endif + public: + HaierClimateBase(); + HaierClimateBase(const HaierClimateBase &) = delete; + HaierClimateBase &operator=(const HaierClimateBase &) = delete; + ~HaierClimateBase(); + void setup() override; + void loop() override; + void control(const esphome::climate::ClimateCall &call) override; + void dump_config() override; + float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } + void set_display_state(bool state); + bool get_display_state() const; + void set_health_mode(bool state); + bool get_health_mode() const; + void send_power_on_command(); + void send_power_off_command(); + void toggle_power(); + void reset_protocol() { this->reset_protocol_request_ = true; }; + void set_supported_modes(const std::set &modes); + void set_supported_swing_modes(const std::set &modes); + void set_supported_presets(const std::set &presets); + bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; + size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; + size_t read_array(uint8_t *data, size_t len) noexcept override { + return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; + }; + void write_array(const uint8_t *data, size_t len) noexcept override { + esphome::uart::UARTDevice::write_array(data, len); + }; + bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; + void set_answer_timeout(uint32_t timeout); + void set_send_wifi(bool send_wifi); + void send_custom_command(const haier_protocol::HaierMessage &message); + void add_status_message_callback(std::function &&callback); + + protected: + enum class ProtocolPhases { + UNKNOWN = -1, + // INITIALIZATION + SENDING_INIT_1 = 0, + SENDING_INIT_2, + SENDING_FIRST_STATUS_REQUEST, + SENDING_FIRST_ALARM_STATUS_REQUEST, + // FUNCTIONAL STATE + IDLE, + SENDING_STATUS_REQUEST, + SENDING_UPDATE_SIGNAL_REQUEST, + SENDING_SIGNAL_LEVEL, + SENDING_CONTROL, + SENDING_ACTION_COMMAND, + SENDING_ALARM_STATUS_REQUEST, + NUM_PROTOCOL_PHASES + }; + const char *phase_to_string_(ProtocolPhases phase); + virtual void set_handlers() = 0; + virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; + virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming) + virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; // NOLINT(readability-identifier-naming) + virtual void save_settings(); + virtual void initialization(); + virtual bool prepare_pending_action(); + virtual void process_protocol_reset(); + esphome::climate::ClimateTraits traits() override; + // Answer handlers + haier_protocol::HandlerError answer_preprocess_(haier_protocol::FrameType request_message_type, + haier_protocol::FrameType expected_request_message_type, + haier_protocol::FrameType answer_message_type, + haier_protocol::FrameType expected_answer_message_type, + ProtocolPhases expected_phase); + haier_protocol::HandlerError report_network_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); + // Timeout handler + haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type); + // Helper functions + void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats = 0, + std::chrono::milliseconds interval = std::chrono::milliseconds::zero()); + virtual void set_phase(ProtocolPhases phase); + void reset_phase_(); + void reset_to_idle_(); + bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); +#ifdef USE_WIFI + haier_protocol::HaierMessage get_wifi_signal_message_(); +#endif + + struct HvacSettings { + esphome::optional mode; + esphome::optional fan_mode; + esphome::optional swing_mode; + esphome::optional target_temperature; + esphome::optional preset; + bool valid; + HvacSettings() : valid(false){}; + HvacSettings(const HvacSettings &) = default; + HvacSettings &operator=(const HvacSettings &) = default; + void reset(); + }; + struct PendingAction { + ActionRequest action; + esphome::optional message; + }; + enum class SwitchState { + OFF = 0b00, + ON = 0b01, + PENDING_OFF = 0b10, + PENDING_ON = 0b11, + }; + haier_protocol::ProtocolHandler haier_protocol_; + ProtocolPhases protocol_phase_; + esphome::optional action_request_; + uint8_t fan_mode_speed_; + uint8_t other_modes_fan_speed_; + SwitchState display_status_{SwitchState::ON}; + SwitchState health_mode_{SwitchState::OFF}; + bool force_send_control_; + bool forced_request_status_; + bool reset_protocol_request_; + bool send_wifi_signal_; + bool use_crc_; + esphome::climate::ClimateTraits traits_; + HvacSettings current_hvac_settings_; + HvacSettings next_hvac_settings_; + std::unique_ptr last_status_message_{nullptr}; + std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages + std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout + std::chrono::steady_clock::time_point last_status_request_; // To request AC status + std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level + CallbackManager status_message_callback_{}; + ESPPreferenceObject base_rtc_; +}; + +class StatusMessageTrigger : public Trigger { + public: + explicit StatusMessageTrigger(HaierClimateBase *parent) { + parent->add_status_message_callback([this](const char *data, size_t data_size) { this->trigger(data, data_size); }); + } +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp new file mode 100644 index 0000000000..e7be1fa418 --- /dev/null +++ b/esphome/components/haier/hon_climate.cpp @@ -0,0 +1,1377 @@ +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/helpers.h" +#include "hon_climate.h" +#include "hon_packet.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; +constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; +constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; +constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); +constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000; +const uint8_t ONE_BUF[] = {0x00, 0x01}; +const uint8_t ZERO_BUF[] = {0x00, 0x00}; + +HonClimate::HonClimate() + : cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00} { + this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; + this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; +} + +HonClimate::~HonClimate() {} + +void HonClimate::set_beeper_state(bool state) { + if (state != this->settings_.beeper_state) { + this->settings_.beeper_state = state; +#ifdef USE_SWITCH + this->beeper_switch_->publish_state(state); +#endif + this->hon_rtc_.save(&this->settings_); + } +} + +bool HonClimate::get_beeper_state() const { return this->settings_.beeper_state; } + +void HonClimate::set_quiet_mode_state(bool state) { + if (state != this->get_quiet_mode_state()) { + this->quiet_mode_state_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; + this->settings_.quiet_mode_state = state; +#ifdef USE_SWITCH + this->quiet_mode_switch_->publish_state(state); +#endif + this->hon_rtc_.save(&this->settings_); + } +} + +bool HonClimate::get_quiet_mode_state() const { + return (this->quiet_mode_state_ == SwitchState::ON) || (this->quiet_mode_state_ == SwitchState::PENDING_ON); +} + +esphome::optional HonClimate::get_vertical_airflow() const { + return this->current_vertical_swing_; +}; + +void HonClimate::set_vertical_airflow(hon_protocol::VerticalSwingMode direction) { + this->pending_vertical_direction_ = direction; + this->force_send_control_ = true; +} + +esphome::optional HonClimate::get_horizontal_airflow() const { + return this->current_horizontal_swing_; +} + +void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction) { + this->pending_horizontal_direction_ = direction; + this->force_send_control_ = true; +} + +std::string HonClimate::get_cleaning_status_text() const { + switch (this->cleaning_status_) { + case CleaningState::SELF_CLEAN: + return "Self clean"; + case CleaningState::STERI_CLEAN: + return "56°C Steri-Clean"; + default: + return "No cleaning"; + } +} + +CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; } + +void HonClimate::start_self_cleaning() { + if (this->cleaning_status_ == CleaningState::NO_CLEANING) { + ESP_LOGI(TAG, "Sending self cleaning start request"); + this->action_request_ = + PendingAction({ActionRequest::START_SELF_CLEAN, esphome::optional()}); + } +} + +void HonClimate::start_steri_cleaning() { + if (this->cleaning_status_ == CleaningState::NO_CLEANING) { + ESP_LOGI(TAG, "Sending steri cleaning start request"); + this->action_request_ = + PendingAction({ActionRequest::START_STERI_CLEAN, esphome::optional()}); + } +} + +void HonClimate::add_alarm_start_callback(std::function &&callback) { + this->alarm_start_callback_.add(std::move(callback)); +} + +void HonClimate::add_alarm_end_callback(std::function &&callback) { + this->alarm_end_callback_.add(std::move(callback)); +} + +haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size) { + // Should check this before preprocess + if (message_type == haier_protocol::FrameType::INVALID) { + ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " + "protocol instead of hOn"); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + return haier_protocol::HandlerError::INVALID_ANSWER; + } + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_VERSION, message_type, + haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::SENDING_INIT_1); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { + // Wrong structure + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + } + // All OK + hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data; + char tmp[9]; + tmp[8] = 0; + strncpy(tmp, answr->protocol_version, 8); + this->hvac_hardware_info_ = HardwareInfo(); + this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp); + strncpy(tmp, answr->software_version, 8); + this->hvac_hardware_info_.value().software_version_ = std::string(tmp); + strncpy(tmp, answr->hardware_version, 8); + this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp); + strncpy(tmp, answr->device_name, 8); + this->hvac_hardware_info_.value().device_name_ = std::string(tmp); +#ifdef USE_TEXT_SENSOR + this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, this->hvac_hardware_info_.value().device_name_); + this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, + this->hvac_hardware_info_.value().protocol_version_); +#endif + this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support + this->hvac_hardware_info_.value().functions_[1] = + (answr->functions[1] & 0x02) != 0; // controller-device mode support + this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support + this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support + this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support + this->use_crc_ = this->hvac_hardware_info_.value().functions_[2]; + this->set_phase(ProtocolPhases::SENDING_INIT_2); + return result; + } else { + this->reset_phase_(); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_ID, message_type, + haier_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::SENDING_INIT_2); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + return result; + } else { + this->reset_phase_(); + return result; + } +} + +haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type, + haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + result = this->process_status_message_(data, data_size); + if (result != haier_protocol::HandlerError::HANDLER_OK) { + ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); + this->reset_phase_(); + this->action_request_.reset(); + this->force_send_control_ = false; + } else { + if (!this->last_status_message_) { + this->real_control_packet_size_ = sizeof(hon_protocol::HaierPacketControl) + this->extra_control_packet_bytes_; + this->real_sensors_packet_size_ = sizeof(hon_protocol::HaierPacketSensors) + this->extra_sensors_packet_bytes_; + this->last_status_message_.reset(); + this->last_status_message_ = std::unique_ptr(new uint8_t[this->real_control_packet_size_]); + }; + if (data_size >= this->real_control_packet_size_ + 2) { + memcpy(this->last_status_message_.get(), data + 2 + this->status_message_header_size_, + this->real_control_packet_size_); + this->status_message_callback_.call((const char *) data, data_size); + } else { + ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_); + } + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase(ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST); + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + // Do nothing, phase will be changed in process_phase + break; + case ProtocolPhases::SENDING_STATUS_REQUEST: + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_CONTROL: + if (!this->control_messages_queue_.empty()) + this->control_messages_queue_.pop(); + if (this->control_messages_queue_.empty()) { + this->set_phase(ProtocolPhases::IDLE); + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + } else { + this->set_phase(ProtocolPhases::SENDING_CONTROL); + } + break; + default: + break; + } + } + return result; + } else { + this->action_request_.reset(); + this->force_send_control_ = false; + this->reset_phase_(); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, message_type, + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); + return result; + } else { + this->set_phase(ProtocolPhases::IDLE); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size) { + if (request_type == haier_protocol::FrameType::GET_ALARM_STATUS) { + if (message_type != haier_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { + // Unexpected answer to request + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + } + if ((this->protocol_phase_ != ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST) && + (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)) { + // Don't expect this answer now + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + } + if (data_size < sizeof(active_alarms_) + 2) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + this->process_alarm_message_(data, data_size, this->protocol_phase_ >= ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::HANDLER_OK; + } else { + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + } +} + +haier_protocol::HandlerError HonClimate::alarm_status_message_handler_(haier_protocol::FrameType type, + const uint8_t *buffer, size_t size) { + haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; + if (size < sizeof(this->active_alarms_) + 2) { + // Log error but confirm anyway to avoid to many messages + result = haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + } + this->process_alarm_message_(buffer, size, true); + this->haier_protocol_.send_answer(haier_protocol::HaierMessage(haier_protocol::FrameType::CONFIRM)); + this->last_alarm_request_ = std::chrono::steady_clock::now(); + return result; +} + +void HonClimate::set_handlers() { + // Set handlers + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::GET_DEVICE_VERSION, + std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::GET_DEVICE_ID, + std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::CONTROL, + std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, + std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::GET_ALARM_STATUS, + std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::REPORT_NETWORK_STATUS, + std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_message_handler( + haier_protocol::FrameType::ALARM_STATUS, + std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3)); +} + +void HonClimate::dump_config() { + HaierClimateBase::dump_config(); + ESP_LOGCONFIG(TAG, " Protocol version: hOn"); + ESP_LOGCONFIG(TAG, " Control method: %d", (uint8_t) this->control_method_); + if (this->hvac_hardware_info_.has_value()) { + ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_hardware_info_.value().protocol_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_hardware_info_.value().software_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_info_.value().hardware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_hardware_info_.value().device_name_.c_str()); + ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", + (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""), + (this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""), + (this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""), + (this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""), + (this->hvac_hardware_info_.value().functions_[4] ? " role" : "")); + ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); + } +} + +void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_INIT_1: + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { + // Indicate device capabilities: + // bit 0 - if 1 module support interactive mode + // bit 1 - if 1 module support controller-device mode + // bit 2 - if 1 module support crc + // bit 3 - if 1 module support multiple devices + // bit 4..bit 15 - not used + uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; + static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( + haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); + } + break; + case ProtocolPhases::SENDING_INIT_2: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage DEVICEID_REQUEST(haier_protocol::FrameType::GET_DEVICE_ID); + this->send_message_(DEVICEID_REQUEST, this->use_crc_); + } + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + case ProtocolPhases::SENDING_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); + static const haier_protocol::HaierMessage BIG_DATA_REQUEST( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_BIG_DATA); + if ((this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) || + (!this->should_get_big_data_())) { + this->send_message_(STATUS_REQUEST, this->use_crc_); + } else { + this->send_message_(BIG_DATA_REQUEST, this->use_crc_); + } + this->last_status_request_ = now; + } + break; +#ifdef USE_WIFI + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION); + this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); + this->last_signal_request_ = now; + } + break; + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + this->send_message_(this->get_wifi_signal_message_(), this->use_crc_); + } + break; +#else + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + this->set_phase(ProtocolPhases::IDLE); + break; +#endif + case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST: + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS); + this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); + this->last_alarm_request_ = now; + } + break; + case ProtocolPhases::SENDING_CONTROL: + if (this->control_messages_queue_.empty()) { + switch (this->control_method_) { + case HonControlMethod::SET_GROUP_PARAMETERS: { + haier_protocol::HaierMessage control_message = this->get_control_message(); + this->control_messages_queue_.push(control_message); + } break; + case HonControlMethod::SET_SINGLE_PARAMETER: + this->fill_control_messages_queue_(); + break; + case HonControlMethod::MONITOR_ONLY: + ESP_LOGI(TAG, "AC control is disabled, monitor only"); + this->reset_to_idle_(); + return; + default: + ESP_LOGW(TAG, "Unsupported control method for hOn protocol!"); + this->reset_to_idle_(); + return; + } + } + if (this->control_messages_queue_.empty()) { + ESP_LOGW(TAG, "Control message queue is empty!"); + this->reset_to_idle_(); + } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { + ESP_LOGI(TAG, "Sending control packet, queue size %d", this->control_messages_queue_.size()); + this->send_message_(this->control_messages_queue_.front(), this->use_crc_, CONTROL_MESSAGE_RETRIES, + CONTROL_MESSAGE_RETRIES_INTERVAL); + } + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + if (this->action_request_.has_value()) { + if (this->action_request_.value().message.has_value()) { + this->send_message_(this->action_request_.value().message.value(), this->use_crc_); + this->action_request_.value().message.reset(); + } else { + // Message already sent, reseting request and return to idle + this->action_request_.reset(); + this->set_phase(ProtocolPhases::IDLE); + } + } else { + ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!"); + this->set_phase(ProtocolPhases::IDLE); + } + break; + case ProtocolPhases::IDLE: { + if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { + this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); + this->forced_request_status_ = false; + } else if (std::chrono::duration_cast(now - this->last_alarm_request_).count() > + ALARM_STATUS_REQUEST_INTERVAL_MS) { + this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); + } +#ifdef USE_WIFI + else if (this->send_wifi_signal_ && + (std::chrono::duration_cast(now - this->last_signal_request_).count() > + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) { + this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); + } +#endif + } break; + default: + // Shouldn't get here + ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", + phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + break; + } +} + +haier_protocol::HaierMessage HonClimate::get_power_message(bool state) { + if (state) { + static haier_protocol::HaierMessage power_on_message( + haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, + std::initializer_list({0x00, 0x01}).begin(), 2); + return power_on_message; + } else { + static haier_protocol::HaierMessage power_off_message( + haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, + std::initializer_list({0x00, 0x00}).begin(), 2); + return power_off_message; + } +} + +void HonClimate::initialization() { + HaierClimateBase::initialization(); + constexpr uint32_t restore_settings_version = 0x57EB59DDUL; + this->hon_rtc_ = + global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); + HonSettings recovered; + if (this->hon_rtc_.load(&recovered)) { + this->settings_ = recovered; + } else { + this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false}; + } + this->current_vertical_swing_ = this->settings_.last_vertiacal_swing; + this->current_horizontal_swing_ = this->settings_.last_horizontal_swing; + this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; +} + +haier_protocol::HaierMessage HonClimate::get_control_message() { + uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE]; + memcpy(control_out_buffer, this->last_status_message_.get(), this->real_control_packet_size_); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + control_out_buffer[4] = 0; // This byte should be cleared before setting values + bool has_hvac_settings = false; + if (this->current_hvac_settings_.valid) { + has_hvac_settings = true; + HvacSettings &climate_control = this->current_hvac_settings_; + if (climate_control.mode.has_value()) { + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + out_data->ac_power = 0; + break; + case CLIMATE_MODE_HEAT_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_HEAT: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::HEAT; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_DRY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_FAN_ONLY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN; + out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode + // Disabling boost for Fan only + out_data->fast_mode = 0; + break; + case CLIMATE_MODE_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Set fan speed, if we are in fan mode, reject auto in fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + } + // Set swing mode + if (climate_control.swing_mode.has_value()) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->horizontal_swing_mode = (uint8_t) this->settings_.last_horizontal_swing; + out_data->vertical_swing_mode = (uint8_t) this->settings_.last_vertiacal_swing; + break; + case CLIMATE_SWING_VERTICAL: + out_data->horizontal_swing_mode = (uint8_t) this->settings_.last_horizontal_swing; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO; + out_data->vertical_swing_mode = (uint8_t) this->settings_.last_vertiacal_swing; + break; + case CLIMATE_SWING_BOTH: + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO; + break; + } + } + if (climate_control.target_temperature.has_value()) { + float target_temp = climate_control.target_temperature.value(); + out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16 + out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; + } + if (out_data->ac_power == 0) { + // If AC is off - no presets allowed + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + out_data->ten_degree = 0; + break; + case CLIMATE_PRESET_BOOST: + // Boost is not supported in Fan only mode + out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; + out_data->sleep_mode = 0; + out_data->ten_degree = 0; + break; + case CLIMATE_PRESET_AWAY: + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + // 10 degrees allowed only in heat mode + out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0; + break; + case CLIMATE_PRESET_SLEEP: + out_data->fast_mode = 0; + out_data->sleep_mode = 1; + out_data->ten_degree = 0; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + out_data->ten_degree = 0; + break; + } + } + } + if (this->pending_vertical_direction_.has_value()) { + out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value(); + this->pending_vertical_direction_.reset(); + } + if (this->pending_horizontal_direction_.has_value()) { + out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value(); + this->pending_horizontal_direction_.reset(); + } + { + // Quiet mode + if ((out_data->ac_power == 0) || (out_data->ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN)) { + // If AC is off or in fan only mode - no quiet mode allowed + out_data->quiet_mode = 0; + } else { + out_data->quiet_mode = this->get_quiet_mode_state() ? 1 : 0; + } + // Clean quiet mode state pending flag + this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01); + } + out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0; + control_out_buffer[4] = 0; // This byte should be cleared before setting values + out_data->display_status = this->get_display_state() ? 1 : 0; + this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01); + out_data->health_mode = this->get_health_mode() ? 1 : 0; + this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01); + return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, this->real_control_packet_size_); +} + +void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new) { + constexpr size_t active_alarms_size = sizeof(this->active_alarms_); + if (size >= active_alarms_size + 2) { + if (check_new) { + size_t alarm_code = 0; + for (int i = active_alarms_size - 1; i >= 0; i--) { + if (packet[2 + i] != active_alarms_[i]) { + uint8_t alarm_bit = 1; + for (int b = 0; b < 8; b++) { + if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) { + bool alarm_status = (packet[2 + i] & alarm_bit) != 0; + int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO; + const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT + ? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str() + : "Unknown"; + esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated", + alarm_code, alarm_message); + if (alarm_status) { + this->alarm_start_callback_.call(alarm_code, alarm_message); + this->active_alarm_count_ += 1.0f; + } else { + this->alarm_end_callback_.call(alarm_code, alarm_message); + this->active_alarm_count_ -= 1.0f; + } + } + alarm_bit <<= 1; + alarm_code++; + } + active_alarms_[i] = packet[2 + i]; + } else + alarm_code += 8; + } + } else { + float alarm_count = 0.0f; + static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}; + for (size_t i = 0; i < sizeof(this->active_alarms_); i++) { + alarm_count += (float) (nibble_bits_count[packet[2 + i] & 0x0F] + nibble_bits_count[packet[2 + i] >> 4]); + } + this->active_alarm_count_ = alarm_count; + memcpy(this->active_alarms_, packet + 2, sizeof(this->active_alarms_)); + } + } +} + +#ifdef USE_SENSOR +void HonClimate::set_sub_sensor(SubSensorType type, sensor::Sensor *sens) { + if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) { + if (type >= SubSensorType::BIG_DATA_FRAME_SUB_SENSORS) { + if ((this->sub_sensors_[(size_t) type] != nullptr) && (sens == nullptr)) { + this->big_data_sensors_--; + } else if ((this->sub_sensors_[(size_t) type] == nullptr) && (sens != nullptr)) { + this->big_data_sensors_++; + } + } + this->sub_sensors_[(size_t) type] = sens; + } +} + +void HonClimate::update_sub_sensor_(SubSensorType type, float value) { + if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) { + size_t index = (size_t) type; + if ((this->sub_sensors_[index] != nullptr) && + ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value))) + this->sub_sensors_[index]->publish_state(value); + } +} +#endif // USE_SENSOR + +#ifdef USE_BINARY_SENSOR +void HonClimate::set_sub_binary_sensor(SubBinarySensorType type, binary_sensor::BinarySensor *sens) { + if (type < SubBinarySensorType::SUB_BINARY_SENSOR_TYPE_COUNT) { + if ((this->sub_binary_sensors_[(size_t) type] != nullptr) && (sens == nullptr)) { + this->big_data_sensors_--; + } else if ((this->sub_binary_sensors_[(size_t) type] == nullptr) && (sens != nullptr)) { + this->big_data_sensors_++; + } + this->sub_binary_sensors_[(size_t) type] = sens; + } +} + +void HonClimate::update_sub_binary_sensor_(SubBinarySensorType type, uint8_t value) { + if (value < 2) { + bool converted_value = value == 1; + size_t index = (size_t) type; + if ((this->sub_binary_sensors_[index] != nullptr) && ((!this->sub_binary_sensors_[index]->has_state()) || + (this->sub_binary_sensors_[index]->state != converted_value))) + this->sub_binary_sensors_[index]->publish_state(converted_value); + } +} +#endif // USE_BINARY_SENSOR + +#ifdef USE_TEXT_SENSOR +void HonClimate::set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens) { + this->sub_text_sensors_[(size_t) type] = sens; + switch (type) { + case SubTextSensorType::APPLIANCE_NAME: + if (this->hvac_hardware_info_.has_value()) + sens->publish_state(this->hvac_hardware_info_.value().device_name_); + break; + case SubTextSensorType::PROTOCOL_VERSION: + if (this->hvac_hardware_info_.has_value()) + sens->publish_state(this->hvac_hardware_info_.value().protocol_version_); + break; + case SubTextSensorType::CLEANING_STATUS: + sens->publish_state(this->get_cleaning_status_text()); + break; + default: + break; + } +} + +void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::string &value) { + size_t index = (size_t) type; + if (this->sub_text_sensors_[index] != nullptr) + this->sub_text_sensors_[index]->publish_state(value); +} +#endif // USE_TEXT_SENSOR + +#ifdef USE_SWITCH +void HonClimate::set_beeper_switch(switch_::Switch *sw) { + this->beeper_switch_ = sw; + if (this->beeper_switch_ != nullptr) { + this->beeper_switch_->publish_state(this->get_beeper_state()); + } +} + +void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) { + this->quiet_mode_switch_ = sw; + if (this->quiet_mode_switch_ != nullptr) { + this->quiet_mode_switch_->publish_state(this->settings_.quiet_mode_state); + } +} +#endif // USE_SWITCH + +haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { + size_t expected_size = + 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; + if (size < expected_size) { + ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size); + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + } + uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1]; + if ((subtype == 0x7D01) && (size >= expected_size + sizeof(hon_protocol::HaierPacketBigData))) { + // Got BigData packet + const hon_protocol::HaierPacketBigData *bd_packet = + (const hon_protocol::HaierPacketBigData *) (&packet_buffer[expected_size]); +#ifdef USE_SENSOR + this->update_sub_sensor_(SubSensorType::INDOOR_COIL_TEMPERATURE, bd_packet->indoor_coil_temperature / 2.0 - 20); + this->update_sub_sensor_(SubSensorType::OUTDOOR_COIL_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64); + this->update_sub_sensor_(SubSensorType::OUTDOOR_DEFROST_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64); + this->update_sub_sensor_(SubSensorType::OUTDOOR_IN_AIR_TEMPERATURE, bd_packet->outdoor_in_air_temperature - 64); + this->update_sub_sensor_(SubSensorType::OUTDOOR_OUT_AIR_TEMPERATURE, bd_packet->outdoor_out_air_temperature - 64); + this->update_sub_sensor_(SubSensorType::POWER, encode_uint16(bd_packet->power[0], bd_packet->power[1])); + this->update_sub_sensor_(SubSensorType::COMPRESSOR_FREQUENCY, bd_packet->compressor_frequency); + this->update_sub_sensor_(SubSensorType::COMPRESSOR_CURRENT, + encode_uint16(bd_packet->compressor_current[0], bd_packet->compressor_current[1]) / 10.0); + this->update_sub_sensor_( + SubSensorType::EXPANSION_VALVE_OPEN_DEGREE, + encode_uint16(bd_packet->expansion_valve_open_degree[0], bd_packet->expansion_valve_open_degree[1]) / 4095.0); +#endif // USE_SENSOR +#ifdef USE_BINARY_SENSOR + this->update_sub_binary_sensor_(SubBinarySensorType::OUTDOOR_FAN_STATUS, bd_packet->outdoor_fan_status); + this->update_sub_binary_sensor_(SubBinarySensorType::DEFROST_STATUS, bd_packet->defrost_status); + this->update_sub_binary_sensor_(SubBinarySensorType::COMPRESSOR_STATUS, bd_packet->compressor_status); + this->update_sub_binary_sensor_(SubBinarySensorType::INDOOR_FAN_STATUS, bd_packet->indoor_fan_status); + this->update_sub_binary_sensor_(SubBinarySensorType::FOUR_WAY_VALVE_STATUS, bd_packet->four_way_valve_status); + this->update_sub_binary_sensor_(SubBinarySensorType::INDOOR_ELECTRIC_HEATING_STATUS, + bd_packet->indoor_electric_heating_status); +#endif // USE_BINARY_SENSOR + } + struct { + hon_protocol::HaierPacketControl control; + hon_protocol::HaierPacketSensors sensors; + } packet; + memcpy(&packet.control, packet_buffer + 2 + this->status_message_header_size_, + sizeof(hon_protocol::HaierPacketControl)); + memcpy(&packet.sensors, packet_buffer + 2 + this->status_message_header_size_ + this->real_control_packet_size_, + sizeof(hon_protocol::HaierPacketSensors)); + if (packet.sensors.error_status != 0) { + ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); + } +#ifdef USE_SENSOR + if ((this->sub_sensors_[(size_t) SubSensorType::OUTDOOR_TEMPERATURE] != nullptr) && + (this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { + this->got_valid_outdoor_temp_ = true; + this->update_sub_sensor_(SubSensorType::OUTDOOR_TEMPERATURE, + (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET)); + } + if ((this->sub_sensors_[(size_t) SubSensorType::HUMIDITY] != nullptr) && (packet.sensors.room_humidity <= 100)) { + this->update_sub_sensor_(SubSensorType::HUMIDITY, (float) packet.sensors.room_humidity); + } +#endif // USE_SENSOR + bool should_publish = false; + { + // Extra modes/presets + optional old_preset = this->preset; + if (packet.control.fast_mode != 0) { + this->preset = CLIMATE_PRESET_BOOST; + } else if (packet.control.sleep_mode != 0) { + this->preset = CLIMATE_PRESET_SLEEP; + } else if (packet.control.ten_degree != 0) { + this->preset = CLIMATE_PRESET_AWAY; + } else { + this->preset = CLIMATE_PRESET_NONE; + } + should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + } + { + // Target temperature + float old_target_temperature = this->target_temperature; + this->target_temperature = packet.control.set_point + 16.0f + ((packet.control.half_degree == 1) ? 0.5f : 0.0f); + should_publish = should_publish || (old_target_temperature != this->target_temperature); + } + { + // Current temperature + float old_current_temperature = this->current_temperature; + this->current_temperature = packet.sensors.room_temperature / 2.0f; + should_publish = should_publish || (old_current_temperature != this->current_temperature); + } + { + // Fan mode + optional old_fan_mode = this->fan_mode; + // remember the fan speed we last had for climate vs fan + if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) { + if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO) + this->fan_mode_speed_ = packet.control.fan_mode; + } else { + this->other_modes_fan_speed_ = packet.control.fan_mode; + } + switch (packet.control.fan_mode) { + case (uint8_t) hon_protocol::FanMode::FAN_AUTO: + if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) { + this->fan_mode = CLIMATE_FAN_AUTO; + } else { + // Shouldn't accept fan speed auto in fan-only mode even if AC reports it + ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring"); + } + break; + case (uint8_t) hon_protocol::FanMode::FAN_MID: + this->fan_mode = CLIMATE_FAN_MEDIUM; + break; + case (uint8_t) hon_protocol::FanMode::FAN_LOW: + this->fan_mode = CLIMATE_FAN_LOW; + break; + case (uint8_t) hon_protocol::FanMode::FAN_HIGH: + this->fan_mode = CLIMATE_FAN_HIGH; + break; + } + should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + } + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status != 0; + if (disp_status != this->get_display_state()) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->force_send_control_ = true; + } else if ((((uint8_t) this->health_mode_) & 0b10) == 0) { + this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF; + } + } + } + // Health mode + if ((((uint8_t) this->health_mode_) & 0b10) == 0) { + bool old_health_mode = this->get_health_mode(); + this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF; + should_publish = should_publish || (old_health_mode != this->get_health_mode()); + } + { + CleaningState new_cleaning; + if (packet.control.steri_clean == 1) { + // Steri-cleaning + new_cleaning = CleaningState::STERI_CLEAN; + } else if (packet.control.self_cleaning_status == 1) { + // Self-cleaning + new_cleaning = CleaningState::SELF_CLEAN; + } else { + // No cleaning + new_cleaning = CleaningState::NO_CLEANING; + } + if (new_cleaning != this->cleaning_status_) { + ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); + if (new_cleaning == CleaningState::NO_CLEANING) { + // Turning AC off after cleaning + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional()}); + } + this->cleaning_status_ = new_cleaning; +#ifdef USE_TEXT_SENSOR + this->update_sub_text_sensor_(SubTextSensorType::CLEANING_STATUS, this->get_cleaning_status_text()); +#endif // USE_TEXT_SENSOR + } + } + { + // Climate mode + ClimateMode old_mode = this->mode; + if (packet.control.ac_power == 0) { + this->mode = CLIMATE_MODE_OFF; + } else { + // Check current hvac mode + switch (packet.control.ac_mode) { + case (uint8_t) hon_protocol::ConditioningMode::COOL: + this->mode = CLIMATE_MODE_COOL; + break; + case (uint8_t) hon_protocol::ConditioningMode::HEAT: + this->mode = CLIMATE_MODE_HEAT; + break; + case (uint8_t) hon_protocol::ConditioningMode::DRY: + this->mode = CLIMATE_MODE_DRY; + break; + case (uint8_t) hon_protocol::ConditioningMode::FAN: + this->mode = CLIMATE_MODE_FAN_ONLY; + break; + case (uint8_t) hon_protocol::ConditioningMode::AUTO: + this->mode = CLIMATE_MODE_HEAT_COOL; + break; + } + } + should_publish = should_publish || (old_mode != this->mode); + } + { + // Quiet mode, should be after climate mode + if ((this->mode != CLIMATE_MODE_FAN_ONLY) && (this->mode != CLIMATE_MODE_OFF) && + ((((uint8_t) this->quiet_mode_state_) & 0b10) == 0)) { + // In proper mode and not in pending state + bool new_quiet_mode = packet.control.quiet_mode != 0; + if (new_quiet_mode != this->get_quiet_mode_state()) { + this->quiet_mode_state_ = new_quiet_mode ? SwitchState::ON : SwitchState::OFF; + this->settings_.quiet_mode_state = new_quiet_mode; + this->hon_rtc_.save(&this->settings_); + } + } + } + { + // Swing mode + ClimateSwingMode old_swing_mode = this->swing_mode; + const std::set &swing_modes = traits_.get_supported_swing_modes(); + bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); + bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); + if (horizontal_swing_supported && + (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { + if (vertical_swing_supported && + (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO)) { + this->swing_mode = CLIMATE_SWING_BOTH; + } else { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } + } else { + if (vertical_swing_supported && + (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO)) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } + // Saving last known non auto mode for vertical and horizontal swing + this->current_vertical_swing_ = (hon_protocol::VerticalSwingMode) packet.control.vertical_swing_mode; + this->current_horizontal_swing_ = (hon_protocol::HorizontalSwingMode) packet.control.horizontal_swing_mode; + bool save_settings = ((this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO) && + (this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO_SPECIAL) && + (this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing)) || + ((this->current_horizontal_swing_.value() != hon_protocol::HorizontalSwingMode::AUTO) && + (this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing)); + if (save_settings) { + this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value(); + this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value(); + this->hon_rtc_.save(&this->settings_); + } + should_publish = should_publish || (old_swing_mode != this->swing_mode); + } + this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); + if (should_publish) { + this->publish_state(); + } + if (should_publish) { + ESP_LOGI(TAG, "HVAC values changed"); + } + int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG; + esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point); + return haier_protocol::HandlerError::HANDLER_OK; +} + +void HonClimate::fill_control_messages_queue_() { + if (!this->current_hvac_settings_.valid && !this->force_send_control_) + return; + this->clear_control_messages_queue_(); + HvacSettings climate_control; + climate_control = this->current_hvac_settings_; + // Beeper command + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::BEEPER_STATUS, + this->get_beeper_state() ? ZERO_BUF : ONE_BUF, 2)); + } + // Health mode + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::HEALTH_MODE, + this->get_health_mode() ? ONE_BUF : ZERO_BUF, 2)); + this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01); + } + // Climate mode + ClimateMode climate_mode = this->mode; + bool new_power = this->mode != CLIMATE_MODE_OFF; + uint8_t fan_mode_buf[] = {0x00, 0xFF}; + uint8_t quiet_mode_buf[] = {0x00, 0xFF}; + if (climate_control.mode.has_value()) { + climate_mode = climate_control.mode.value(); + uint8_t buffer[2] = {0x00, 0x00}; + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + new_power = false; + break; + case CLIMATE_MODE_HEAT_COOL: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::AUTO; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_HEAT: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::HEAT; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_DRY: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::DRY; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_FAN_ONLY: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::FAN; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; // Auto doesn't work in fan only mode + break; + case CLIMATE_MODE_COOL: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::COOL; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Climate power + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_POWER, + new_power ? ONE_BUF : ZERO_BUF, 2)); + } + // CLimate preset + { + uint8_t fast_mode_buf[] = {0x00, 0xFF}; + uint8_t away_mode_buf[] = {0x00, 0xFF}; + if (!new_power) { + // If AC is off - no presets allowed + fast_mode_buf[1] = 0x00; + away_mode_buf[1] = 0x00; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + fast_mode_buf[1] = 0x00; + away_mode_buf[1] = 0x00; + break; + case CLIMATE_PRESET_BOOST: + // Boost is not supported in Fan only mode + fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; + away_mode_buf[1] = 0x00; + break; + case CLIMATE_PRESET_AWAY: + fast_mode_buf[1] = 0x00; + away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + break; + } + } + { + // Quiet mode + if (new_power && (climate_mode != CLIMATE_MODE_FAN_ONLY) && this->get_quiet_mode_state()) { + quiet_mode_buf[1] = 0x01; + } else { + quiet_mode_buf[1] = 0x00; + } + // Clean quiet mode state pending flag + this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01); + } + auto presets = this->traits_.get_supported_presets(); + if (quiet_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::QUIET_MODE, + quiet_mode_buf, 2)); + } + if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::FAST_MODE, + fast_mode_buf, 2)); + } + if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, + away_mode_buf, 2)); + } + } + // Target temperature + if (climate_control.target_temperature.has_value() && (this->mode != ClimateMode::CLIMATE_MODE_FAN_ONLY)) { + uint8_t buffer[2] = {0x00, 0x00}; + buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::SET_POINT, + buffer, 2)); + } + // Vertical swing mode + if (climate_control.swing_mode.has_value()) { + uint8_t vertical_swing_buf[] = {0x00, (uint8_t) hon_protocol::VerticalSwingMode::AUTO}; + uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) hon_protocol::HorizontalSwingMode::AUTO}; + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + horizontal_swing_buf[1] = (uint8_t) this->settings_.last_horizontal_swing; + vertical_swing_buf[1] = (uint8_t) this->settings_.last_vertiacal_swing; + break; + case CLIMATE_SWING_VERTICAL: + horizontal_swing_buf[1] = (uint8_t) this->settings_.last_horizontal_swing; + break; + case CLIMATE_SWING_HORIZONTAL: + vertical_swing_buf[1] = (uint8_t) this->settings_.last_vertiacal_swing; + break; + case CLIMATE_SWING_BOTH: + break; + } + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE, + horizontal_swing_buf, 2)); + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE, + vertical_swing_buf, 2)); + } + // Fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + if (fan_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::FAN_MODE, + fan_mode_buf, 2)); + } + } +} + +void HonClimate::clear_control_messages_queue_() { + while (!this->control_messages_queue_.empty()) + this->control_messages_queue_.pop(); +} + +bool HonClimate::prepare_pending_action() { + switch (this->action_request_.value().action) { + case ActionRequest::START_SELF_CLEAN: + if (this->control_method_ == HonControlMethod::SET_GROUP_PARAMETERS) { + uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE]; + memcpy(control_out_buffer, this->last_status_message_.get(), this->real_control_packet_size_); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + out_data->self_cleaning_status = 1; + out_data->steri_clean = 0; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + this->action_request_.value().message = haier_protocol::HaierMessage( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, this->real_control_packet_size_); + return true; + } else if (this->control_method_ == HonControlMethod::SET_SINGLE_PARAMETER) { + this->action_request_.value().message = + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::SELF_CLEANING, + ONE_BUF, 2); + return true; + } else { + this->action_request_.reset(); + return false; + } + case ActionRequest::START_STERI_CLEAN: + if (this->control_method_ == HonControlMethod::SET_GROUP_PARAMETERS) { + uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE]; + memcpy(control_out_buffer, this->last_status_message_.get(), this->real_control_packet_size_); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + out_data->self_cleaning_status = 0; + out_data->steri_clean = 1; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + this->action_request_.value().message = haier_protocol::HaierMessage( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, this->real_control_packet_size_); + return true; + } else { + // No Steri clean support (yet?) in SET_SINGLE_PARAMETER + this->action_request_.reset(); + return false; + } + default: + return HaierClimateBase::prepare_pending_action(); + } +} + +void HonClimate::process_protocol_reset() { + HaierClimateBase::process_protocol_reset(); +#ifdef USE_SENSOR + for (auto &sub_sensor : this->sub_sensors_) { + if ((sub_sensor != nullptr) && sub_sensor->has_state()) + sub_sensor->publish_state(NAN); + } +#endif // USE_SENSOR + this->got_valid_outdoor_temp_ = false; + this->hvac_hardware_info_.reset(); + this->last_status_message_.reset(nullptr); +} + +bool HonClimate::should_get_big_data_() { + if (this->big_data_sensors_ > 0) { + static uint8_t counter = 0; + counter = (counter + 1) % 3; + return counter == 1; + } + return false; +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h new file mode 100644 index 0000000000..58173f8154 --- /dev/null +++ b/esphome/components/haier/hon_climate.h @@ -0,0 +1,214 @@ +#pragma once + +#include +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#include "esphome/core/automation.h" +#include "haier_base.h" +#include "hon_packet.h" + +namespace esphome { +namespace haier { + +enum class CleaningState : uint8_t { + NO_CLEANING = 0, + SELF_CLEAN = 1, + STERI_CLEAN = 2, +}; + +enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; + +struct HonSettings { + hon_protocol::VerticalSwingMode last_vertiacal_swing; + hon_protocol::HorizontalSwingMode last_horizontal_swing; + bool beeper_state; + bool quiet_mode_state; +}; + +class HonClimate : public HaierClimateBase { +#ifdef USE_SENSOR + public: + enum class SubSensorType { + // Used data based sensors + OUTDOOR_TEMPERATURE = 0, + HUMIDITY, + // Big data based sensors + INDOOR_COIL_TEMPERATURE, + OUTDOOR_COIL_TEMPERATURE, + OUTDOOR_DEFROST_TEMPERATURE, + OUTDOOR_IN_AIR_TEMPERATURE, + OUTDOOR_OUT_AIR_TEMPERATURE, + POWER, + COMPRESSOR_FREQUENCY, + COMPRESSOR_CURRENT, + EXPANSION_VALVE_OPEN_DEGREE, + SUB_SENSOR_TYPE_COUNT, + BIG_DATA_FRAME_SUB_SENSORS = INDOOR_COIL_TEMPERATURE, + }; + void set_sub_sensor(SubSensorType type, sensor::Sensor *sens); + + protected: + void update_sub_sensor_(SubSensorType type, float value); + sensor::Sensor *sub_sensors_[(size_t) SubSensorType::SUB_SENSOR_TYPE_COUNT]{nullptr}; +#endif +#ifdef USE_BINARY_SENSOR + public: + enum class SubBinarySensorType { + OUTDOOR_FAN_STATUS = 0, + DEFROST_STATUS, + COMPRESSOR_STATUS, + INDOOR_FAN_STATUS, + FOUR_WAY_VALVE_STATUS, + INDOOR_ELECTRIC_HEATING_STATUS, + SUB_BINARY_SENSOR_TYPE_COUNT, + }; + void set_sub_binary_sensor(SubBinarySensorType type, binary_sensor::BinarySensor *sens); + + protected: + void update_sub_binary_sensor_(SubBinarySensorType type, uint8_t value); + binary_sensor::BinarySensor *sub_binary_sensors_[(size_t) SubBinarySensorType::SUB_BINARY_SENSOR_TYPE_COUNT]{nullptr}; +#endif +#ifdef USE_TEXT_SENSOR + public: + enum class SubTextSensorType { + CLEANING_STATUS = 0, + PROTOCOL_VERSION, + APPLIANCE_NAME, + SUB_TEXT_SENSOR_TYPE_COUNT, + }; + void set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens); + + protected: + void update_sub_text_sensor_(SubTextSensorType type, const std::string &value); + text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr}; +#endif +#ifdef USE_SWITCH + public: + void set_beeper_switch(switch_::Switch *sw); + void set_quiet_mode_switch(switch_::Switch *sw); + + protected: + switch_::Switch *beeper_switch_{nullptr}; + switch_::Switch *quiet_mode_switch_{nullptr}; +#endif + public: + HonClimate(); + HonClimate(const HonClimate &) = delete; + HonClimate &operator=(const HonClimate &) = delete; + ~HonClimate(); + void dump_config() override; + void set_beeper_state(bool state); + bool get_beeper_state() const; + void set_quiet_mode_state(bool state); + bool get_quiet_mode_state() const; + esphome::optional get_vertical_airflow() const; + void set_vertical_airflow(hon_protocol::VerticalSwingMode direction); + esphome::optional get_horizontal_airflow() const; + void set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction); + std::string get_cleaning_status_text() const; + CleaningState get_cleaning_status() const; + void start_self_cleaning(); + void start_steri_cleaning(); + void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; }; + void set_extra_sensors_packet_bytes_size(size_t size) { this->extra_sensors_packet_bytes_ = size; }; + void set_status_message_header_size(size_t size) { this->status_message_header_size_ = size; }; + void set_control_method(HonControlMethod method) { this->control_method_ = method; }; + void add_alarm_start_callback(std::function &&callback); + void add_alarm_end_callback(std::function &&callback); + float get_active_alarm_count() const { return this->active_alarm_count_; } + + protected: + void set_handlers() override; + void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_control_message() override; + haier_protocol::HaierMessage get_power_message(bool state) override; + void initialization() override; + bool prepare_pending_action() override; + void process_protocol_reset() override; + bool should_get_big_data_(); + + // Answers handlers + haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size); + haier_protocol::HandlerError get_management_information_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError alarm_status_message_handler_(haier_protocol::FrameType type, const uint8_t *buffer, + size_t size); + // Helper functions + haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + void process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new); + void fill_control_messages_queue_(); + void clear_control_messages_queue_(); + + struct HardwareInfo { + std::string protocol_version_; + std::string software_version_; + std::string hardware_version_; + std::string device_name_; + bool functions_[5]; + }; + + CleaningState cleaning_status_; + bool got_valid_outdoor_temp_; + esphome::optional pending_vertical_direction_{}; + esphome::optional pending_horizontal_direction_{}; + esphome::optional hvac_hardware_info_{}; + uint8_t active_alarms_[8]; + int extra_control_packet_bytes_{0}; + int extra_sensors_packet_bytes_{4}; + int status_message_header_size_{0}; + int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; + int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4}; + HonControlMethod control_method_; + std::queue control_messages_queue_; + CallbackManager alarm_start_callback_{}; + CallbackManager alarm_end_callback_{}; + float active_alarm_count_{NAN}; + std::chrono::steady_clock::time_point last_alarm_request_; + int big_data_sensors_{0}; + esphome::optional current_vertical_swing_{}; + esphome::optional current_horizontal_swing_{}; + HonSettings settings_; + ESPPreferenceObject hon_rtc_; + SwitchState quiet_mode_state_{SwitchState::OFF}; +}; + +class HaierAlarmStartTrigger : public Trigger { + public: + explicit HaierAlarmStartTrigger(HonClimate *parent) { + parent->add_alarm_start_callback( + [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); + } +}; + +class HaierAlarmEndTrigger : public Trigger { + public: + explicit HaierAlarmEndTrigger(HonClimate *parent) { + parent->add_alarm_end_callback( + [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); + } +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h new file mode 100644 index 0000000000..615f93528e --- /dev/null +++ b/esphome/components/haier/hon_packet.h @@ -0,0 +1,260 @@ +#pragma once + +#include + +namespace esphome { +namespace haier { +namespace hon_protocol { + +enum class VerticalSwingMode : uint8_t { + HEALTH_UP = 0x01, + MAX_UP = 0x02, + HEALTH_DOWN = 0x03, + UP = 0x04, + CENTER = 0x06, + DOWN = 0x08, + MAX_DOWN = 0x0A, + AUTO = 0x0C, + // Auto for special modes + AUTO_SPECIAL = 0x0E +}; + +enum class HorizontalSwingMode : uint8_t { + CENTER = 0x00, + MAX_LEFT = 0x03, + LEFT = 0x04, + RIGHT = 0x05, + MAX_RIGHT = 0x06, + AUTO = 0x07 +}; + +enum class ConditioningMode : uint8_t { + AUTO = 0x00, + COOL = 0x01, + DRY = 0x02, + HEALTHY_DRY = 0x03, + HEAT = 0x04, + ENERGY_SAVING = 0x05, + FAN = 0x06 +}; + +enum class DataParameters : uint8_t { + AC_POWER = 0x01, + SET_POINT = 0x02, + VERTICAL_SWING_MODE = 0x03, + AC_MODE = 0x04, + FAN_MODE = 0x05, + USE_FAHRENHEIT = 0x07, + DISPLAY_STATUS = 0x09, + TEN_DEGREE = 0x0A, + HEALTH_MODE = 0x0B, + HORIZONTAL_SWING_MODE = 0x0C, + SELF_CLEANING = 0x0D, + BEEPER_STATUS = 0x16, + LOCK_REMOTE = 0x17, + QUIET_MODE = 0x19, + FAST_MODE = 0x1A, + SLEEP_MODE = 0x1B, +}; + +enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; + +enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; + +struct HaierPacketControl { + // Control bytes starts here + // 1 + uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C) + // 2 + uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode + uint8_t : 0; + // 3 + uint8_t fan_mode : 3; // See enum FanMode + uint8_t special_mode : 2; // See enum SpecialMode + uint8_t ac_mode : 3; // See enum ConditioningMode + // 4 + uint8_t : 8; + // 5 + uint8_t ten_degree : 1; // 10 degree status + uint8_t display_status : 1; // If 0 disables AC's display + uint8_t half_degree : 1; // Use half degree + uint8_t intelligence_status : 1; // Intelligence status + uint8_t pmv_status : 1; // Comfort/PMV status + uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius + uint8_t : 1; + uint8_t steri_clean : 1; + // 6 + uint8_t ac_power : 1; // Is ac on or off + uint8_t health_mode : 1; // Health mode (negative ions) on or off + uint8_t electric_heating_status : 1; // Electric heating status + uint8_t fast_mode : 1; // Fast mode + uint8_t quiet_mode : 1; // Quiet mode + uint8_t sleep_mode : 1; // Sleep mode + uint8_t lock_remote : 1; // Disable remote + uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command) + // 7 + uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%) + // 8 + uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode + uint8_t : 3; + uint8_t human_sensing_status : 2; // Human sensing status + // 9 + uint8_t change_filter : 1; // Filter need replacement + uint8_t : 0; + // 10 + uint8_t fresh_air_status : 1; // Fresh air status + uint8_t humidification_status : 1; // Humidification status + uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status + uint8_t ch2o_cleaning_status : 1; // CH2O cleaning status + uint8_t self_cleaning_status : 1; // Self cleaning status + uint8_t light_status : 1; // Light status + uint8_t energy_saving_status : 1; // Energy saving status + uint8_t cleaning_time_status : 1; // Cleaning time (0 - accumulation, 1 - clear) +}; + +struct HaierPacketSensors { + // 11 + uint8_t room_temperature; // 0.5°C step + // 12 + uint8_t room_humidity; // 0%-100% with 1% step + // 13 + uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C) + // 14 + uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad) + uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad) + uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple) + uint8_t : 1; + uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only) + // 15 + uint8_t error_status; // See enum ErrorStatus + // 16 + uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP) + uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan) + uint8_t : 3; + uint8_t err_confirmation : 1; // If 1 clear error status + // 17 + uint16_t total_cleaning_time; // Cleaning cumulative time (1h step) + // 19 + uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) + // 21 + uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) + // 23 + uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step) + // 25 + uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step) + // 27 + uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) +}; + +struct HaierPacketBigData { + // 29 + uint8_t power[2]; // AC power consumption (0W - 65535W, 1W step) + // 31 + uint8_t indoor_coil_temperature; // 0.5°C step, -20°C offset (0=-20°C) + // 32 + uint8_t outdoor_out_air_temperature; // 1°C step, -64°C offset (0=-64°C) + // 33 + uint8_t outdoor_coil_temperature; // 1°C step, -64°C offset (0=-64°C) + // 34 + uint8_t outdoor_in_air_temperature; // 1°C step, -64°C offset (0=-64°C) + // 35 + uint8_t outdoor_defrost_temperature; // 1°C step, -64°C offset (0=-64°C) + // 36 + uint8_t compressor_frequency; // 1Hz step, 0Hz - 127Hz + // 37 + uint8_t compressor_current[2]; // 0.1A step, 0.0A - 51.1A (0x0000 - 0x01FF) + // 39 + uint8_t outdoor_fan_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t defrost_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t : 0; + // 40 + uint8_t compressor_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t indoor_fan_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t four_way_valve_status : 2; // 0 - off, 1 - on, 2 - information not available + uint8_t indoor_electric_heating_status : 2; // 0 - off, 1 - on, 2 - information not available + // 41 + uint8_t expansion_valve_open_degree[2]; // 0 - 4095 +}; + +struct DeviceVersionAnswer { + char protocol_version[8]; + char software_version[8]; + uint8_t encryption[3]; + char hardware_version[8]; + uint8_t : 8; + char device_name[8]; + uint8_t functions[2]; +}; + +enum class SubcommandsControl : uint16_t { + GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) + GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) + GET_BIG_DATA = 0x4DFE, // Request big data information from device (packet content: None) + SET_PARAMETERS = 0x5C01, // Set parameters of the device and device return parameters (packet content: parameter ID1 + // + parameter data1 + parameter ID2 + parameter data 2 + ...) + SET_SINGLE_PARAMETER = 0x5D00, // Set single parameter (0x5DXX second byte define parameter ID) and return all user + // data (packet content: ???) + SET_GROUP_PARAMETERS = 0x6001, // Set group parameters to device (0x60XX second byte define parameter is group ID, + // the only group mentioned in document is 1) and return all user data (packet + // content: all values like in status packet) +}; + +const std::string HON_ALARM_MESSAGES[] = { + "Outdoor module failure", + "Outdoor defrost sensor failure", + "Outdoor compressor exhaust sensor failure", + "Outdoor EEPROM abnormality", + "Indoor coil sensor failure", + "Indoor-outdoor communication failure", + "Power supply overvoltage protection", + "Communication failure between panel and indoor unit", + "Outdoor compressor overheat protection", + "Outdoor environmental sensor abnormality", + "Full water protection", + "Indoor EEPROM failure", + "Outdoor out air sensor failure", + "CBD and module communication failure", + "Indoor DC fan failure", + "Outdoor DC fan failure", + "Door switch failure", + "Dust filter needs cleaning reminder", + "Water shortage protection", + "Humidity sensor failure", + "Indoor temperature sensor failure", + "Manipulator limit failure", + "Indoor PM2.5 sensor failure", + "Outdoor PM2.5 sensor failure", + "Indoor heating overload/high load alarm", + "Outdoor AC current protection", + "Outdoor compressor operation abnormality", + "Outdoor DC current protection", + "Outdoor no-load failure", + "CT current abnormality", + "Indoor cooling freeze protection", + "High and low pressure protection", + "Compressor out air temperature is too high", + "Outdoor evaporator sensor failure", + "Outdoor cooling overload", + "Water pump drainage failure", + "Three-phase power supply failure", + "Four-way valve failure", + "External alarm/scraper flow switch failure", + "Temperature cutoff protection alarm", + "Different mode operation failure", + "Electronic expansion valve failure", + "Dual heat source sensor Tw failure", + "Communication failure with the wired controller", + "Indoor unit address duplication failure", + "50Hz zero crossing failure", + "Outdoor unit failure", + "Formaldehyde sensor failure", + "VOC sensor failure", + "CO2 sensor failure", + "Firewall failure", +}; + +constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]); + +} // namespace hon_protocol +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/logger_handler.cpp b/esphome/components/haier/logger_handler.cpp new file mode 100644 index 0000000000..f886318097 --- /dev/null +++ b/esphome/components/haier/logger_handler.cpp @@ -0,0 +1,33 @@ +#include "logger_handler.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace haier { + +void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) { + switch (level) { + case haier_protocol::HaierLogLevel::LEVEL_ERROR: + esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_WARNING: + esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_INFO: + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_DEBUG: + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_VERBOSE: + esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, "%s", message); + break; + default: + // Just ignore everything else + break; + } +} + +void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); }; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/logger_handler.h b/esphome/components/haier/logger_handler.h new file mode 100644 index 0000000000..2955468f37 --- /dev/null +++ b/esphome/components/haier/logger_handler.h @@ -0,0 +1,14 @@ +#pragma once + +// HaierProtocol +#include + +namespace esphome { +namespace haier { + +// This file is called in the code generated by python script +// Do not use it directly! +void init_haier_protocol_logging(); + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/sensor/__init__.py b/esphome/components/haier/sensor/__init__.py new file mode 100644 index 0000000000..23c1d6f008 --- /dev/null +++ b/esphome/components/haier/sensor/__init__.py @@ -0,0 +1,152 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_OUTDOOR_TEMPERATURE, + CONF_POWER, + CONF_HUMIDITY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_FREQUENCY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_CURRENT_AC, + ICON_FLASH, + ICON_GAUGE, + ICON_HEATING_COIL, + ICON_PULSE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + ICON_WEATHER_WINDY, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_PERCENT, + UNIT_WATT, +) +from ..climate import ( + CONF_HAIER_ID, + HonClimate, +) + +CODEOWNERS = ["@paveldn"] +SensorTypeEnum = HonClimate.enum("SubSensorType", True) + +# Haier sensors +CONF_COMPRESSOR_CURRENT = "compressor_current" +CONF_COMPRESSOR_FREQUENCY = "compressor_frequency" +CONF_EXPANSION_VALVE_OPEN_DEGREE = "expansion_valve_open_degree" +CONF_INDOOR_COIL_TEMPERATURE = "indoor_coil_temperature" +CONF_OUTDOOR_COIL_TEMPERATURE = "outdoor_coil_temperature" +CONF_OUTDOOR_DEFROST_TEMPERATURE = "outdoor_defrost_temperature" +CONF_OUTDOOR_IN_AIR_TEMPERATURE = "outdoor_in_air_temperature" +CONF_OUTDOOR_OUT_AIR_TEMPERATURE = "outdoor_out_air_temperature" + +# Additional icons +ICON_SNOWFLAKE_THERMOMETER = "mdi:snowflake-thermometer" + +SENSOR_TYPES = { + CONF_COMPRESSOR_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_COMPRESSOR_FREQUENCY: sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_PULSE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_EXPANSION_VALVE_OPEN_DEGREE: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_GAUGE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_HUMIDITY: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_INDOOR_COIL_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_HEATING_COIL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_COIL_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_HEATING_COIL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_DEFROST_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_SNOWFLAKE_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_IN_AIR_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_WEATHER_WINDY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_OUT_AIR_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_WEATHER_WINDY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_OUTDOOR_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_FLASH, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate), + } +).extend({cv.Optional(type_): schema for type_, schema in SENSOR_TYPES.items()}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_HAIER_ID]) + + for type_ in SENSOR_TYPES: + if conf := config.get(type_): + sens = await sensor.new_sensor(conf) + sensor_type = getattr(SensorTypeEnum, type_.upper()) + cg.add(paren.set_sub_sensor(sensor_type, sens)) diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp new file mode 100644 index 0000000000..63c22821b3 --- /dev/null +++ b/esphome/components/haier/smartair2_climate.cpp @@ -0,0 +1,553 @@ +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "smartair2_climate.h" +#include "smartair2_packet.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; +constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; +constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); +constexpr uint8_t INIT_REQUESTS_RETRY = 2; +constexpr std::chrono::milliseconds INIT_REQUESTS_RETRY_INTERVAL = std::chrono::milliseconds(2000); + +Smartair2Climate::Smartair2Climate() { + last_status_message_ = std::unique_ptr(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]); +} + +haier_protocol::HandlerError Smartair2Climate::status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type, + haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + result = this->process_status_message_(data, data_size); + if (result != haier_protocol::HandlerError::HANDLER_OK) { + ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); + this->reset_phase_(); + this->action_request_.reset(); + this->force_send_control_ = false; + } else { + if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { + memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); + this->status_message_callback_.call((const char *) data, data_size); + } else { + ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, + sizeof(smartair2_protocol::HaierPacketControl)); + } + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + // Do nothing, phase will be changed in process_phase + break; + case ProtocolPhases::SENDING_STATUS_REQUEST: + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_CONTROL: + this->set_phase(ProtocolPhases::IDLE); + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + break; + default: + break; + } + } + return result; + } else { + this->action_request_.reset(); + this->force_send_control_ = false; + this->reset_phase_(); + return result; + } +} + +haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + if (request_type != haier_protocol::FrameType::GET_DEVICE_VERSION) + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + if (ProtocolPhases::SENDING_INIT_1 != this->protocol_phase_) + return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + // Invalid packet is expected answer + if ((message_type == haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && + ((data[37] & 0x04) != 0)) { + ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " + "instead of smartAir2"); + } + this->set_phase(ProtocolPhases::SENDING_INIT_2); + return haier_protocol::HandlerError::HANDLER_OK; +} + +haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cycle_for_init_( + haier_protocol::FrameType message_type) { + if (this->protocol_phase_ >= ProtocolPhases::IDLE) + return HaierClimateBase::timeout_default_handler_(message_type); + ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type, + phase_to_string_(this->protocol_phase_)); + ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); + if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST) + new_phase = ProtocolPhases::SENDING_INIT_1; + this->set_phase(new_phase); + return haier_protocol::HandlerError::HANDLER_OK; +} + +void Smartair2Climate::set_handlers() { + // Set handlers + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::GET_DEVICE_VERSION, + std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::CONTROL, + std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + haier_protocol::FrameType::REPORT_NETWORK_STATUS, + std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_default_timeout_handler( + std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1)); +} + +void Smartair2Climate::dump_config() { + HaierClimateBase::dump_config(); + ESP_LOGCONFIG(TAG, " Protocol version: smartAir2"); +} + +void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_INIT_1: + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { + // Indicate device capabilities: + // bit 0 - if 1 module support interactive mode + // bit 1 - if 1 module support controller-device mode + // bit 2 - if 1 module support crc + // bit 3 - if 1 module support multiple devices + // bit 4..bit 15 - not used + uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; + static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( + haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL); + } + break; + case ProtocolPhases::SENDING_INIT_2: + this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + case ProtocolPhases::SENDING_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST(haier_protocol::FrameType::CONTROL, 0x4D01); + if (this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) { + this->send_message_(STATUS_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL); + } else { + this->send_message_(STATUS_REQUEST, this->use_crc_); + } + this->last_status_request_ = now; + } + break; +#ifdef USE_WIFI + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + this->send_message_(this->get_wifi_signal_message_(), this->use_crc_); + this->last_signal_request_ = now; + } + break; +#else + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + this->set_phase(ProtocolPhases::IDLE); + break; +#endif + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); + break; + case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST: + this->set_phase(ProtocolPhases::SENDING_INIT_1); + break; + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_CONTROL: + if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { + ESP_LOGI(TAG, "Sending control packet"); + this->send_message_(get_control_message(), this->use_crc_, CONTROL_MESSAGE_RETRIES, + CONTROL_MESSAGE_RETRIES_INTERVAL); + } + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + if (this->action_request_.has_value()) { + if (this->action_request_.value().message.has_value()) { + this->send_message_(this->action_request_.value().message.value(), this->use_crc_); + this->action_request_.value().message.reset(); + } else { + // Message already sent, reseting request and return to idle + this->action_request_.reset(); + this->set_phase(ProtocolPhases::IDLE); + } + } else { + ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!"); + this->set_phase(ProtocolPhases::IDLE); + } + break; + case ProtocolPhases::IDLE: { + if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { + this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); + this->forced_request_status_ = false; + } +#ifdef USE_WIFI + else if (this->send_wifi_signal_ && + (std::chrono::duration_cast(now - this->last_signal_request_).count() > + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) + this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); +#endif + } break; + default: + // Shouldn't get here + ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", + phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + break; + } +} + +haier_protocol::HaierMessage Smartair2Climate::get_power_message(bool state) { + if (state) { + static haier_protocol::HaierMessage power_on_message(haier_protocol::FrameType::CONTROL, 0x4D02); + return power_on_message; + } else { + static haier_protocol::HaierMessage power_off_message(haier_protocol::FrameType::CONTROL, 0x4D03); + return power_off_message; + } +} + +haier_protocol::HaierMessage Smartair2Climate::get_control_message() { + uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl)); + smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; + out_data->cntrl = 0; + if (this->current_hvac_settings_.valid) { + HvacSettings &climate_control = this->current_hvac_settings_; + if (climate_control.mode.has_value()) { + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + out_data->ac_power = 0; + break; + case CLIMATE_MODE_HEAT_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_HEAT: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_DRY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_FAN_ONLY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; + out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode + break; + case CLIMATE_MODE_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Set fan speed, if we are in fan mode, reject auto in fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (this->mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + } + // Set swing mode + if (climate_control.swing_mode.has_value()) { + if (this->use_alternative_swing_control_) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->swing_mode = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_mode = 1; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_mode = 2; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_mode = 3; + break; + } + } else { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->use_swing_bits = 0; + out_data->swing_mode = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_mode = 0; + out_data->vertical_swing = 1; + out_data->horizontal_swing = 0; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_mode = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 1; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_mode = 1; + out_data->use_swing_bits = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 0; + break; + } + } + } + if (climate_control.target_temperature.has_value()) { + float target_temp = climate_control.target_temperature.value(); + out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16 + out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; + } + if (out_data->ac_power == 0) { + // If AC is off - no presets allowed + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + out_data->ten_degree = 0; + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + case CLIMATE_PRESET_BOOST: + out_data->ten_degree = 0; + out_data->turbo_mode = 1; + out_data->quiet_mode = 0; + break; + case CLIMATE_PRESET_COMFORT: + out_data->ten_degree = 0; + out_data->turbo_mode = 0; + out_data->quiet_mode = 1; + break; + case CLIMATE_PRESET_AWAY: + // Only allowed in heat mode + out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0; + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + out_data->ten_degree = 0; + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + } + } + } + out_data->display_status = this->get_display_state() ? 0 : 1; + this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01); + out_data->health_mode = this->get_health_mode() ? 1 : 0; + this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01); + return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, + sizeof(smartair2_protocol::HaierPacketControl)); +} + +haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { + if (size < sizeof(smartair2_protocol::HaierStatus)) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + smartair2_protocol::HaierStatus packet; + memcpy(&packet, packet_buffer, size); + bool should_publish = false; + { + // Extra modes/presets + optional old_preset = this->preset; + if (packet.control.turbo_mode != 0) { + this->preset = CLIMATE_PRESET_BOOST; + } else if (packet.control.quiet_mode != 0) { + this->preset = CLIMATE_PRESET_COMFORT; + } else if (packet.control.ten_degree != 0) { + this->preset = CLIMATE_PRESET_AWAY; + } else { + this->preset = CLIMATE_PRESET_NONE; + } + should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + } + { + // Target temperature + float old_target_temperature = this->target_temperature; + this->target_temperature = packet.control.set_point + 16.0f + ((packet.control.half_degree == 1) ? 0.5f : 0.0f); + should_publish = should_publish || (old_target_temperature != this->target_temperature); + } + { + // Current temperature + float old_current_temperature = this->current_temperature; + this->current_temperature = packet.control.room_temperature; + should_publish = should_publish || (old_current_temperature != this->current_temperature); + } + { + // Fan mode + optional old_fan_mode = this->fan_mode; + // remember the fan speed we last had for climate vs fan + if (packet.control.ac_mode == (uint8_t) smartair2_protocol::ConditioningMode::FAN) { + if (packet.control.fan_mode != (uint8_t) smartair2_protocol::FanMode::FAN_AUTO) + this->fan_mode_speed_ = packet.control.fan_mode; + } else { + this->other_modes_fan_speed_ = packet.control.fan_mode; + } + switch (packet.control.fan_mode) { + case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO: + // Sometimes AC reports in fan only mode that fan speed is auto + // but never accept this value back + if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) { + this->fan_mode = CLIMATE_FAN_AUTO; + } else { + should_publish = true; + } + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_MID: + this->fan_mode = CLIMATE_FAN_MEDIUM; + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_LOW: + this->fan_mode = CLIMATE_FAN_LOW; + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_HIGH: + this->fan_mode = CLIMATE_FAN_HIGH; + break; + } + should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + } + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status == 0; + if (disp_status != this->get_display_state()) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->force_send_control_ = true; + } else if ((((uint8_t) this->health_mode_) & 0b10) == 0) { + this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF; + } + } + } + // Health mode + if ((((uint8_t) this->health_mode_) & 0b10) == 0) { + bool old_health_mode = this->get_health_mode(); + this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF; + should_publish = should_publish || (old_health_mode != this->get_health_mode()); + } + { + // Climate mode + ClimateMode old_mode = this->mode; + if (packet.control.ac_power == 0) { + this->mode = CLIMATE_MODE_OFF; + } else { + // Check current hvac mode + switch (packet.control.ac_mode) { + case (uint8_t) smartair2_protocol::ConditioningMode::COOL: + this->mode = CLIMATE_MODE_COOL; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::HEAT: + this->mode = CLIMATE_MODE_HEAT; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::DRY: + this->mode = CLIMATE_MODE_DRY; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::FAN: + this->mode = CLIMATE_MODE_FAN_ONLY; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::AUTO: + this->mode = CLIMATE_MODE_HEAT_COOL; + break; + } + } + should_publish = should_publish || (old_mode != this->mode); + } + { + // Swing mode + ClimateSwingMode old_swing_mode = this->swing_mode; + if (this->use_alternative_swing_control_) { + switch (packet.control.swing_mode) { + case 1: + this->swing_mode = CLIMATE_SWING_VERTICAL; + break; + case 2: + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + break; + case 3: + this->swing_mode = CLIMATE_SWING_BOTH; + break; + default: + this->swing_mode = CLIMATE_SWING_OFF; + break; + } + } else { + if (packet.control.swing_mode == 0) { + if (packet.control.vertical_swing != 0) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else if (packet.control.horizontal_swing != 0) { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } else { + swing_mode = CLIMATE_SWING_BOTH; + } + } + should_publish = should_publish || (old_swing_mode != this->swing_mode); + } + this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); + if (should_publish) { + this->publish_state(); + } + if (should_publish) { + ESP_LOGI(TAG, "HVAC values changed"); + } + int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG; + esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); + esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing); + esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point); + return haier_protocol::HandlerError::HANDLER_OK; +} + +void Smartair2Climate::set_alternative_swing_control(bool swing_control) { + this->use_alternative_swing_control_ = swing_control; +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.h b/esphome/components/haier/smartair2_climate.h new file mode 100644 index 0000000000..6914d8a1fb --- /dev/null +++ b/esphome/components/haier/smartair2_climate.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "haier_base.h" + +namespace esphome { +namespace haier { + +class Smartair2Climate : public HaierClimateBase { + public: + Smartair2Climate(); + Smartair2Climate(const Smartair2Climate &) = delete; + Smartair2Climate &operator=(const Smartair2Climate &) = delete; + ~Smartair2Climate(); + void dump_config() override; + void set_alternative_swing_control(bool swing_control); + + protected: + void set_handlers() override; + void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_power_message(bool state) override; + haier_protocol::HaierMessage get_control_message() override; + // Answer handlers + haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size); + haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError messages_timeout_handler_with_cycle_for_init_(haier_protocol::FrameType message_type); + // Helper functions + haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + bool use_alternative_swing_control_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_packet.h b/esphome/components/haier/smartair2_packet.h new file mode 100644 index 0000000000..22570ff048 --- /dev/null +++ b/esphome/components/haier/smartair2_packet.h @@ -0,0 +1,88 @@ +#pragma once + +namespace esphome { +namespace haier { +namespace smartair2_protocol { + +enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 }; + +enum class FanMode : uint8_t { FAN_HIGH = 0x00, FAN_MID = 0x01, FAN_LOW = 0x02, FAN_AUTO = 0x03 }; + +struct HaierPacketControl { + // Control bytes starts here + // 10 + uint8_t : 8; // Temperature high byte + // 11 + uint8_t room_temperature; // current room temperature 1°C step + // 12 + uint8_t : 8; // Humidity high byte + // 13 + uint8_t room_humidity; // Humidity 0%-100% with 1% step + // 14 + uint8_t : 8; + // 15 + uint8_t cntrl; // In AC => ESP packets - 0x7F, in ESP => AC packets - 0x00 + // 16 + uint8_t : 8; + // 17 + uint8_t : 8; + // 18 + uint8_t : 8; + // 19 + uint8_t : 8; + // 20 + uint8_t : 8; + // 21 + uint8_t ac_mode; // See enum ConditioningMode + // 22 + uint8_t : 8; + // 23 + uint8_t fan_mode; // See enum FanMode + // 24 + uint8_t : 8; + // 25 + uint8_t swing_mode; // In normal mode: If 1 - swing both direction, if 0 - horizontal_swing and + // vertical_swing define vertical/horizontal/off + // In alternative mode: 0 - off, 01 - vertical, 02 - horizontal, 03 - both + // 26 + uint8_t : 3; + uint8_t use_fahrenheit : 1; + uint8_t : 3; + uint8_t lock_remote : 1; // Disable remote + // 27 + uint8_t ac_power : 1; // Is ac on or off + uint8_t : 2; + uint8_t health_mode : 1; // Health mode on or off + uint8_t compressor : 1; // Compressor on or off ??? + uint8_t half_degree : 1; // Use half degree + uint8_t ten_degree : 1; // 10 degree status (only work in heat mode) + uint8_t : 0; + // 28 + uint8_t : 8; + // 29 + uint8_t use_swing_bits : 1; // Indicate if horizontal_swing and vertical_swing should be used + uint8_t turbo_mode : 1; // Turbo mode + uint8_t quiet_mode : 1; // Sleep mode + uint8_t horizontal_swing : 1; // Horizontal swing (if swing_both == 0) + uint8_t vertical_swing : 1; // Vertical swing (if swing_both == 0) if vertical_swing and horizontal_swing both 0 => + // swing off + uint8_t display_status : 1; // Led on or off + uint8_t : 0; + // 30 + uint8_t : 8; + // 31 + uint8_t : 8; + // 32 + uint8_t : 8; // Target temperature high byte + // 33 + uint8_t set_point; // Target temperature with 16°C offset, 1°C step +}; + +struct HaierStatus { + uint16_t subcommand; + HaierPacketControl control; +}; + +} // namespace smartair2_protocol +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/__init__.py b/esphome/components/haier/switch/__init__.py new file mode 100644 index 0000000000..6076cb0bd5 --- /dev/null +++ b/esphome/components/haier/switch/__init__.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.components import switch +from esphome.const import ( + CONF_BEEPER, + CONF_DISPLAY, + ENTITY_CATEGORY_CONFIG, +) +from ..climate import ( + CONF_HAIER_ID, + CONF_PROTOCOL, + HaierClimateBase, + haier_ns, + PROTOCOL_HON, +) + +CODEOWNERS = ["@paveldn"] +BeeperSwitch = haier_ns.class_("BeeperSwitch", switch.Switch) +HealthModeSwitch = haier_ns.class_("HealthModeSwitch", switch.Switch) +DisplaySwitch = haier_ns.class_("DisplaySwitch", switch.Switch) +QuietModeSwitch = haier_ns.class_("QuietModeSwitch", switch.Switch) + +# Haier switches +CONF_HEALTH_MODE = "health_mode" +CONF_QUIET_MODE = "quiet_mode" + +# Additional icons +ICON_LEAF = "mdi:leaf" +ICON_LED_ON = "mdi:led-on" +ICON_VOLUME_HIGH = "mdi:volume-high" +ICON_VOLUME_OFF = "mdi:volume-off" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HAIER_ID): cv.use_id(HaierClimateBase), + cv.Optional(CONF_DISPLAY): switch.switch_schema( + DisplaySwitch, + icon=ICON_LED_ON, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="DISABLED", + ), + cv.Optional(CONF_HEALTH_MODE): switch.switch_schema( + HealthModeSwitch, + icon=ICON_LEAF, + default_restore_mode="DISABLED", + ), + # Beeper switch is only supported for HonClimate + cv.Optional(CONF_BEEPER): switch.switch_schema( + BeeperSwitch, + icon=ICON_VOLUME_HIGH, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="DISABLED", + ), + # Quiet mode is only supported for HonClimate + cv.Optional(CONF_QUIET_MODE): switch.switch_schema( + QuietModeSwitch, + icon=ICON_VOLUME_OFF, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="DISABLED", + ), + } +) + + +def _final_validate(config): + full_config = fv.full_config.get() + for switch_type in [CONF_BEEPER, CONF_QUIET_MODE]: + # Check switches that are only supported for HonClimate + if config.get(switch_type): + climate_path = full_config.get_path_for_id(config[CONF_HAIER_ID])[:-1] + climate_conf = full_config.get_config_for_path(climate_path) + protocol_type = climate_conf.get(CONF_PROTOCOL) + if protocol_type.casefold() != PROTOCOL_HON.casefold(): + raise cv.Invalid( + f"{switch_type} switch is only supported for hon climate" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_HAIER_ID]) + + for switch_type in [CONF_DISPLAY, CONF_HEALTH_MODE, CONF_BEEPER, CONF_QUIET_MODE]: + if conf := config.get(switch_type): + sw_var = await switch.new_switch(conf) + await cg.register_parented(sw_var, parent) + cg.add(getattr(parent, f"set_{switch_type}_switch")(sw_var)) diff --git a/esphome/components/haier/switch/beeper.cpp b/esphome/components/haier/switch/beeper.cpp new file mode 100644 index 0000000000..1ce64d0848 --- /dev/null +++ b/esphome/components/haier/switch/beeper.cpp @@ -0,0 +1,14 @@ +#include "beeper.h" + +namespace esphome { +namespace haier { + +void BeeperSwitch::write_state(bool state) { + if (this->parent_->get_beeper_state() != state) { + this->parent_->set_beeper_state(state); + } + this->publish_state(state); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/beeper.h b/esphome/components/haier/switch/beeper.h new file mode 100644 index 0000000000..7396a7a0dd --- /dev/null +++ b/esphome/components/haier/switch/beeper.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../hon_climate.h" + +namespace esphome { +namespace haier { + +class BeeperSwitch : public switch_::Switch, public Parented { + public: + BeeperSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/display.cpp b/esphome/components/haier/switch/display.cpp new file mode 100644 index 0000000000..5e24843dcf --- /dev/null +++ b/esphome/components/haier/switch/display.cpp @@ -0,0 +1,14 @@ +#include "display.h" + +namespace esphome { +namespace haier { + +void DisplaySwitch::write_state(bool state) { + if (this->parent_->get_display_state() != state) { + this->parent_->set_display_state(state); + } + this->publish_state(state); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/display.h b/esphome/components/haier/switch/display.h new file mode 100644 index 0000000000..f93ccfcdb7 --- /dev/null +++ b/esphome/components/haier/switch/display.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../haier_base.h" + +namespace esphome { +namespace haier { + +class DisplaySwitch : public switch_::Switch, public Parented { + public: + DisplaySwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/health_mode.cpp b/esphome/components/haier/switch/health_mode.cpp new file mode 100644 index 0000000000..3715759bdd --- /dev/null +++ b/esphome/components/haier/switch/health_mode.cpp @@ -0,0 +1,14 @@ +#include "health_mode.h" + +namespace esphome { +namespace haier { + +void HealthModeSwitch::write_state(bool state) { + if (this->parent_->get_health_mode() != state) { + this->parent_->set_health_mode(state); + } + this->publish_state(state); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/health_mode.h b/esphome/components/haier/switch/health_mode.h new file mode 100644 index 0000000000..cfd2aa2f22 --- /dev/null +++ b/esphome/components/haier/switch/health_mode.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../haier_base.h" + +namespace esphome { +namespace haier { + +class HealthModeSwitch : public switch_::Switch, public Parented { + public: + HealthModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/quiet_mode.cpp b/esphome/components/haier/switch/quiet_mode.cpp new file mode 100644 index 0000000000..056312b5f0 --- /dev/null +++ b/esphome/components/haier/switch/quiet_mode.cpp @@ -0,0 +1,14 @@ +#include "quiet_mode.h" + +namespace esphome { +namespace haier { + +void QuietModeSwitch::write_state(bool state) { + if (this->parent_->get_quiet_mode_state() != state) { + this->parent_->set_quiet_mode_state(state); + } + this->publish_state(state); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/quiet_mode.h b/esphome/components/haier/switch/quiet_mode.h new file mode 100644 index 0000000000..bad5289500 --- /dev/null +++ b/esphome/components/haier/switch/quiet_mode.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../hon_climate.h" + +namespace esphome { +namespace haier { + +class QuietModeSwitch : public switch_::Switch, public Parented { + public: + QuietModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/text_sensor/__init__.py b/esphome/components/haier/text_sensor/__init__.py new file mode 100644 index 0000000000..d28c5a8c0e --- /dev/null +++ b/esphome/components/haier/text_sensor/__init__.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_NONE, +) +from ..climate import ( + CONF_HAIER_ID, + HonClimate, +) + +CODEOWNERS = ["@paveldn"] +TextSensorTypeEnum = HonClimate.enum("SubTextSensorType", True) + +# Haier text sensors +CONF_CLEANING_STATUS = "cleaning_status" +CONF_PROTOCOL_VERSION = "protocol_version" +CONF_APPLIANCE_NAME = "appliance_name" + +# Additional icons +ICON_SPRAY_BOTTLE = "mdi:spray-bottle" +ICON_TEXT_BOX = "mdi:text-box-outline" + +TEXT_SENSOR_TYPES = { + CONF_CLEANING_STATUS: text_sensor.text_sensor_schema( + icon=ICON_SPRAY_BOTTLE, + entity_category=ENTITY_CATEGORY_NONE, + ), + CONF_PROTOCOL_VERSION: text_sensor.text_sensor_schema( + icon=ICON_TEXT_BOX, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + CONF_APPLIANCE_NAME: text_sensor.text_sensor_schema( + icon=ICON_TEXT_BOX, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate), + } +).extend({cv.Optional(type): schema for type, schema in TEXT_SENSOR_TYPES.items()}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_HAIER_ID]) + + for type_ in TEXT_SENSOR_TYPES: + if conf := config.get(type_): + sens = await text_sensor.new_text_sensor(conf) + text_sensor_type = getattr(TextSensorTypeEnum, type_.upper()) + cg.add(paren.set_sub_text_sensor(text_sensor_type, sens)) diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index d7c8d544f9..66b72f9e3e 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -6,6 +6,9 @@ from esphome.const import ( CONF_CURRENT, CONF_FREQUENCY, CONF_ID, + CONF_PHASE_A, + CONF_PHASE_B, + CONF_PHASE_C, CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, @@ -24,9 +27,6 @@ from esphome.const import ( UNIT_WATT, ) -CONF_PHASE_A = "phase_a" -CONF_PHASE_B = "phase_b" -CONF_PHASE_C = "phase_c" CONF_ENERGY_PRODUCTION_DAY = "energy_production_day" CONF_TOTAL_ENERGY_PRODUCTION = "total_energy_production" CONF_TOTAL_GENERATION_TIME = "total_generation_time" diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 421883a1ff..424e944290 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import fan, output +from esphome.components.fan import validate_preset_modes from esphome.const import ( CONF_ID, CONF_DECAY_MODE, @@ -10,6 +11,7 @@ from esphome.const import ( CONF_PIN_A, CONF_PIN_B, CONF_ENABLE_PIN, + CONF_PRESET_MODES, ) from .. import hbridge_ns @@ -28,7 +30,6 @@ DECAY_MODE_OPTIONS = { # Actions BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) - CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( { cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), @@ -39,6 +40,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( ), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ).extend(cv.COMPONENT_SCHEMA) @@ -69,3 +71,6 @@ async def to_code(config): if CONF_ENABLE_PIN in config: enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable_pin)) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 44cf5ae049..605a9d4ef3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -33,7 +33,12 @@ void HBridgeFan::setup() { restore->apply(*this); this->write_state_(); } + + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } + void HBridgeFan::dump_config() { LOG_FAN("", "H-Bridge Fan", this); if (this->decay_mode_ == DECAY_MODE_SLOW) { @@ -42,9 +47,7 @@ void HBridgeFan::dump_config() { ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); } } -fan::FanTraits HBridgeFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); -} + void HBridgeFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); @@ -54,10 +57,12 @@ void HBridgeFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); this->write_state_(); this->publish_state(); } + void HBridgeFan::write_state_() { float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; if (speed == 0.0f) { // off means idle diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4389b97ccb..4234fccae3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/automation.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -20,10 +22,11 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } + void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; - fan::FanTraits get_traits() override; + fan::FanTraits get_traits() override { return this->traits_; } fan::FanCall brake(); @@ -34,6 +37,8 @@ class HBridgeFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; + fan::FanTraits traits_; + std::set preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/he60r/__init__.py b/esphome/components/he60r/__init__.py new file mode 100644 index 0000000000..c58ce8a01e --- /dev/null +++ b/esphome/components/he60r/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/he60r/cover.py b/esphome/components/he60r/cover.py new file mode 100644 index 0000000000..fd4c746016 --- /dev/null +++ b/esphome/components/he60r/cover.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import cover, uart +from esphome.const import ( + CONF_CLOSE_DURATION, + CONF_ID, + CONF_OPEN_DURATION, +) + +he60r_ns = cg.esphome_ns.namespace("he60r") +HE60rCover = he60r_ns.class_("HE60rCover", cover.Cover, cg.Component) + +CONFIG_SCHEMA = ( + cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(HE60rCover), + cv.Optional( + CONF_OPEN_DURATION, default="15s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_CLOSE_DURATION, default="15s" + ): cv.positive_time_period_milliseconds, + } + ) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "he60r", + baud_rate=1200, + require_tx=True, + require_rx=True, + data_bits=8, + parity="EVEN", + stop_bits=1, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cover.register_cover(var, config) + await uart.register_uart_device(var, config) + + cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) + cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp new file mode 100644 index 0000000000..83e895543d --- /dev/null +++ b/esphome/components/he60r/he60r.cpp @@ -0,0 +1,263 @@ +#include "he60r.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace he60r { + +static const char *const TAG = "he60r.cover"; +static const uint8_t QUERY_BYTE = 0x38; +static const uint8_t TOGGLE_BYTE = 0x30; + +using namespace esphome::cover; + +void HE60rCover::setup() { + auto restore = this->restore_state_(); + + if (restore.has_value()) { + restore->apply(this); + this->publish_state(false); + } else { + // if no other information, assume half open + this->position = 0.5f; + } + this->current_operation = COVER_OPERATION_IDLE; + this->last_recompute_time_ = this->start_dir_time_ = millis(); + this->set_interval(300, [this]() { this->update_(); }); +} + +CoverTraits HE60rCover::get_traits() { + auto traits = CoverTraits(); + traits.set_supports_stop(true); + traits.set_supports_position(true); + traits.set_supports_toggle(true); + traits.set_is_assumed_state(false); + return traits; +} + +void HE60rCover::dump_config() { + LOG_COVER("", "HE60R Cover", this); + this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8); + ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); + auto restore = this->restore_state_(); + if (restore.has_value()) + ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f)); +} + +void HE60rCover::endstop_reached_(CoverOperation operation) { + const uint32_t now = millis(); + + this->set_current_operation_(COVER_OPERATION_IDLE); + auto new_position = operation == COVER_OPERATION_OPENING ? COVER_OPEN : COVER_CLOSED; + if (new_position != this->position || this->current_operation != COVER_OPERATION_IDLE) { + this->position = new_position; + this->current_operation = COVER_OPERATION_IDLE; + if (this->last_command_ == operation) { + float dur = (float) (now - this->start_dir_time_) / 1e3f; + ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), + operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur); + } + this->publish_state(); + } +} + +void HE60rCover::set_current_operation_(cover::CoverOperation operation) { + if (this->current_operation != operation) { + this->current_operation = operation; + if (operation != COVER_OPERATION_IDLE) + this->last_recompute_time_ = millis(); + } +} + +void HE60rCover::process_rx_(uint8_t data) { + ESP_LOGV(TAG, "Process RX data %X", data); + if (!this->query_seen_) { + this->query_seen_ = data == QUERY_BYTE; + if (!this->query_seen_) + ESP_LOGD(TAG, "RX Byte %02X", data); + return; + } + switch (data) { + case 0xB5: // at closed endstop, jammed? + case 0xF5: // at closed endstop, jammed? + case 0x55: // at closed endstop + this->next_direction_ = COVER_OPERATION_OPENING; + this->endstop_reached_(COVER_OPERATION_CLOSING); + break; + + case 0x52: // at opened endstop + this->next_direction_ = COVER_OPERATION_CLOSING; + this->endstop_reached_(COVER_OPERATION_OPENING); + break; + + case 0x51: // travelling up after encountering obstacle + case 0x01: // travelling up + case 0x11: // travelling up, triggered by remote + this->set_current_operation_(COVER_OPERATION_OPENING); + this->next_direction_ = COVER_OPERATION_IDLE; + break; + + case 0x44: // travelling down + case 0x14: // travelling down, triggered by remote + this->next_direction_ = COVER_OPERATION_IDLE; + this->set_current_operation_(COVER_OPERATION_CLOSING); + break; + + case 0x86: // Stopped, jammed? + case 0x16: // stopped midway while opening, by remote + case 0x06: // stopped midway while opening + this->next_direction_ = COVER_OPERATION_CLOSING; + this->set_current_operation_(COVER_OPERATION_IDLE); + break; + + case 0x10: // stopped midway while closing, by remote + case 0x00: // stopped midway while closing + this->next_direction_ = COVER_OPERATION_OPENING; + this->set_current_operation_(COVER_OPERATION_IDLE); + break; + + default: + break; + } +} + +void HE60rCover::update_() { + if (this->toggles_needed_ != 0) { + if ((this->counter_++ & 0x3) == 0) { + this->toggles_needed_--; + ESP_LOGD(TAG, "Writing byte 0x30, still needed=%u", this->toggles_needed_); + this->write_byte(TOGGLE_BYTE); + } else { + this->write_byte(QUERY_BYTE); + } + } else { + this->write_byte(QUERY_BYTE); + this->counter_ = 0; + } + if (this->current_operation != COVER_OPERATION_IDLE) { + this->recompute_position_(); + + // if we initiated the move, check if we reached the target position + if (this->last_command_ != COVER_OPERATION_IDLE) { + if (this->is_at_target_()) { + this->start_direction_(COVER_OPERATION_IDLE); + } + } + } +} + +void HE60rCover::loop() { + uint8_t data; + + while (this->available() > 0) { + if (this->read_byte(&data)) { + this->process_rx_(data); + } + } +} + +void HE60rCover::control(const CoverCall &call) { + if (call.get_stop()) { + this->start_direction_(COVER_OPERATION_IDLE); + } else if (call.get_toggle().has_value()) { + // toggle action logic: OPEN - STOP - CLOSE + if (this->last_command_ != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + this->toggles_needed_++; + } + } else if (call.get_position().has_value()) { + // go to position action + auto pos = *call.get_position(); + // are we at the target? + if (pos == this->position) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + this->target_position_ = pos; + this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); + } + } +} + +/** + * Check if the cover has reached or passed the target position. This is used only + * for partial open/close requests - endstops are used for full open/close. + * @return True if the cover has reached or passed its target position. For full open/close target always return false. + */ +bool HE60rCover::is_at_target_() const { + // equality of floats is fraught with peril - this is reliable since the values are 0.0 or 1.0 which are + // exactly representable. + if (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED) + return false; + // aiming for an intermediate position - exact comparison here will not work and we need to allow for overshoot + switch (this->last_command_) { + case COVER_OPERATION_OPENING: + return this->position >= this->target_position_; + case COVER_OPERATION_CLOSING: + return this->position <= this->target_position_; + case COVER_OPERATION_IDLE: + return this->current_operation == COVER_OPERATION_IDLE; + default: + return true; + } +} +void HE60rCover::start_direction_(CoverOperation dir) { + this->last_command_ = dir; + if (this->current_operation == dir) + return; + ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(), + dir == COVER_OPERATION_OPENING ? "OPEN" + : dir == COVER_OPERATION_CLOSING ? "CLOSE" + : "STOP"); + + if (dir == this->next_direction_) { + // either moving and needs to stop, or stopped and will move correctly on one trigger + this->toggles_needed_ = 1; + } else { + if (this->current_operation == COVER_OPERATION_IDLE) { + // if stopped, but will go the wrong way, need 3 triggers. + this->toggles_needed_ = 3; + } else { + // just stop and reverse + this->toggles_needed_ = 2; + } + ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); + } + this->start_dir_time_ = millis(); +} + +void HE60rCover::recompute_position_() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + + const uint32_t now = millis(); + if (now > this->last_recompute_time_) { + auto diff = (unsigned) (now - last_recompute_time_); + float delta; + switch (this->current_operation) { + case COVER_OPERATION_OPENING: + delta = (float) diff / (float) this->open_duration_; + break; + case COVER_OPERATION_CLOSING: + delta = -(float) diff / (float) this->close_duration_; + break; + default: + return; + } + + // make sure our guesstimate never reaches full open or close. + auto new_position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f); + ESP_LOGD(TAG, "Recompute %ums, dir=%u, delta=%f, pos=%f", diff, this->current_operation, delta, new_position); + this->last_recompute_time_ = now; + if (this->position != new_position) { + this->position = new_position; + this->publish_state(); + } + } +} + +} // namespace he60r +} // namespace esphome diff --git a/esphome/components/he60r/he60r.h b/esphome/components/he60r/he60r.h new file mode 100644 index 0000000000..e41e2203c1 --- /dev/null +++ b/esphome/components/he60r/he60r.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace he60r { + +class HE60rCover : public cover::Cover, 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; }; + + void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } + void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } + + cover::CoverTraits get_traits() override; + + protected: + void update_(); + void control(const cover::CoverCall &call) override; + bool is_at_target_() const; + void start_direction_(cover::CoverOperation dir); + void endstop_reached_(cover::CoverOperation operation); + void recompute_position_(); + void set_current_operation_(cover::CoverOperation operation); + void process_rx_(uint8_t data); + + unsigned open_duration_{0}; + unsigned close_duration_{0}; + unsigned toggles_needed_{0}; + cover::CoverOperation next_direction_{cover::COVER_OPERATION_IDLE}; + cover::CoverOperation last_command_{cover::COVER_OPERATION_IDLE}; + uint32_t last_recompute_time_{0}; + uint32_t start_dir_time_{0}; + float target_position_{0}; + bool query_seen_{}; + uint8_t counter_{}; +}; + +} // namespace he60r +} // namespace esphome diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index cc8e75dcbd..9d31668deb 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -33,6 +33,8 @@ PROTOCOLS = { "greeya": Protocol.PROTOCOL_GREEYAA, "greeyan": Protocol.PROTOCOL_GREEYAN, "greeyac": Protocol.PROTOCOL_GREEYAC, + "greeyt": Protocol.PROTOCOL_GREEYT, + "greeyap": Protocol.PROTOCOL_GREEYAP, "hisense_aud": Protocol.PROTOCOL_HISENSE_AUD, "hitachi": Protocol.PROTOCOL_HITACHI, "hyundai": Protocol.PROTOCOL_HYUNDAI, @@ -60,6 +62,16 @@ PROTOCOLS = { "toshiba_daiseikai": Protocol.PROTOCOL_TOSHIBA_DAISEIKAI, "toshiba": Protocol.PROTOCOL_TOSHIBA, "zhlt01": Protocol.PROTOCOL_ZHLT01, + "nibe": Protocol.PROTOCOL_NIBE, + "carrier_qlima_1": Protocol.PROTOCOL_QLIMA_1, + "carrier_qlima_2": Protocol.PROTOCOL_QLIMA_2, + "samsung_aqv12msan": Protocol.PROTOCOL_SAMSUNG_AQV12MSAN, + "zhjg01": Protocol.PROTOCOL_ZHJG01, + "airway": Protocol.PROTOCOL_AIRWAY, + "bgh_aud": Protocol.PROTOCOL_BGH_AUD, + "panasonic_altdke": Protocol.PROTOCOL_PANASONIC_ALTDKE, + "vaillantvai8": Protocol.PROTOCOL_VAILLANTVAI8, + "r51m": Protocol.PROTOCOL_R51M, } CONF_HORIZONTAL_DEFAULT = "horizontal_default" @@ -115,7 +127,6 @@ def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_library("tonia/HeatpumpIR", "1.0.20") - - if CORE.is_esp8266 or CORE.is_esp32: - cg.add_library("crankyoldgit/IRremoteESP8266", "2.7.12") + cg.add_library("tonia/HeatpumpIR", "1.0.27") + if CORE.is_libretiny: + CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index bed1dc76c0..144dcc9bfa 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -27,6 +27,8 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT {PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT {PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT + {PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT + {PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT {PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT {PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT {PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT @@ -54,6 +56,16 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT {PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT {PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT + {PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT + {PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT + {PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT + {PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT + {PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT + {PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT + {PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT + {PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT + {PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT + {PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT }; void HeatpumpIRClimate::setup() { diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index c60b944111..f6e7ff3cd6 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -27,6 +27,8 @@ enum Protocol { PROTOCOL_GREEYAA, PROTOCOL_GREEYAN, PROTOCOL_GREEYAC, + PROTOCOL_GREEYT, + PROTOCOL_GREEYAP, PROTOCOL_HISENSE_AUD, PROTOCOL_HITACHI, PROTOCOL_HYUNDAI, @@ -54,6 +56,16 @@ enum Protocol { PROTOCOL_TOSHIBA_DAISEIKAI, PROTOCOL_TOSHIBA, PROTOCOL_ZHLT01, + PROTOCOL_NIBE, + PROTOCOL_QLIMA_1, + PROTOCOL_QLIMA_2, + PROTOCOL_SAMSUNG_AQV12MSAN, + PROTOCOL_ZHJG01, + PROTOCOL_AIRWAY, + PROTOCOL_BGH_AUD, + PROTOCOL_PANASONIC_ALTDKE, + PROTOCOL_VAILLANTVAI8, + PROTOCOL_R51M, }; // Simple enum to represent horizontal directios diff --git a/esphome/components/heatpumpir/ir_sender_esphome.h b/esphome/components/heatpumpir/ir_sender_esphome.h index 7546d990ea..944d0e859c 100644 --- a/esphome/components/heatpumpir/ir_sender_esphome.h +++ b/esphome/components/heatpumpir/ir_sender_esphome.h @@ -3,7 +3,6 @@ #ifdef USE_ARDUINO #include "esphome/components/remote_base/remote_base.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" #include // arduino-heatpump library namespace esphome { @@ -11,14 +10,13 @@ namespace heatpumpir { class IRSenderESPHome : public IRSender { public: - IRSenderESPHome(remote_transmitter::RemoteTransmitterComponent *transmitter) - : IRSender(0), transmit_(transmitter->transmit()){}; + IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){}; void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming) void space(int space_length) override; void mark(int mark_length) override; protected: - remote_transmitter::RemoteTransmitterComponent::TransmitCall transmit_; + remote_base::RemoteTransmitterBase::TransmitCall transmit_; }; } // namespace heatpumpir diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index ecdaa07ab2..14e83f60e1 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -38,7 +38,7 @@ void HLW8012Component::dump_config() { LOG_PIN(" SEL Pin: ", this->sel_pin_) LOG_PIN(" CF Pin: ", this->cf_pin_) LOG_PIN(" CF1 Pin: ", this->cf1_pin_) - ESP_LOGCONFIG(TAG, " Change measurement mode every %u", this->change_mode_every_); + ESP_LOGCONFIG(TAG, " Change measurement mode every %" PRIu32, this->change_mode_every_); ESP_LOGCONFIG(TAG, " Current resistor: %.1f mΩ", this->current_resistor_ * 1000.0f); ESP_LOGCONFIG(TAG, " Voltage Divider: %.1f", this->voltage_divider_); LOG_UPDATE_INTERVAL(this) @@ -96,7 +96,7 @@ void HLW8012Component::update() { this->energy_sensor_->publish_state(energy); } - if (this->change_mode_at_++ == this->change_mode_every_) { + if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) { this->current_mode_ = !this->current_mode_; ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE"); this->change_mode_at_ = 0; diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index adb49ffb66..312391f533 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -5,6 +5,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/pulse_counter/pulse_counter_sensor.h" +#include + namespace esphome { namespace hlw8012 { diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 033cccc3d4..2687edaca2 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -79,8 +79,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, cv.Optional(CONF_MODEL, default="HLW8012"): cv.enum(MODELS, upper=True), - cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All( - cv.uint32_t, cv.Range(min=1) + cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.Any( + "never", + cv.All(cv.uint32_t, cv.Range(min=1)), ), cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of( *INITIAL_MODES, lower=True @@ -114,6 +115,10 @@ async def to_code(config): cg.add(var.set_energy_sensor(sens)) cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR])) cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER])) - cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY])) cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]])) cg.add(var.set_sensor_model(config[CONF_MODEL])) + + interval = config[CONF_CHANGE_MODE_EVERY] + if interval == "never": + interval = 0 + cg.add(var.set_change_mode_every(interval)) diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index 6c830f9bad..c1b47826a2 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -1,6 +1,7 @@ #pragma once #include "abstract_aqi_calculator.h" +// https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf namespace esphome { namespace hm3301 { @@ -15,14 +16,16 @@ class AQICalculator : public AbstractAQICalculator { } protected: - static const int AMOUNT_OF_LEVELS = 6; + static const int AMOUNT_OF_LEVELS = 7; - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 51}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, + {201, 300}, {301, 400}, {401, 500}}; - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, + {151, 250}, {251, 350}, {351, 500}}; - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, - {255, 354}, {355, 424}, {425, 604}}; + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, {255, 354}, + {355, 424}, {425, 504}, {505, 604}}; int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { int grid_index = get_grid_index_(value, array); diff --git a/esphome/components/hmac_md5/__init__.py b/esphome/components/hmac_md5/__init__.py new file mode 100644 index 0000000000..fe245c0cfd --- /dev/null +++ b/esphome/components/hmac_md5/__init__.py @@ -0,0 +1,2 @@ +AUTO_LOAD = ["md5"] +CODEOWNERS = ["@dwmw2"] diff --git a/esphome/components/hmac_md5/hmac_md5.cpp b/esphome/components/hmac_md5/hmac_md5.cpp new file mode 100644 index 0000000000..d766a55fab --- /dev/null +++ b/esphome/components/hmac_md5/hmac_md5.cpp @@ -0,0 +1,58 @@ +#include +#include +#include "hmac_md5.h" +#ifdef USE_MD5 +#include "esphome/core/helpers.h" + +namespace esphome { +namespace hmac_md5 { +void HmacMD5::init(const uint8_t *key, size_t len) { + uint8_t ipad[64], opad[64]; + + memset(ipad, 0, sizeof(ipad)); + if (len > 64) { + md5::MD5Digest keymd5; + keymd5.init(); + keymd5.add(key, len); + keymd5.calculate(); + keymd5.get_bytes(ipad); + } else { + memcpy(ipad, key, len); + } + memcpy(opad, ipad, sizeof(opad)); + + for (int i = 0; i < 64; 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 HmacMD5::add(const uint8_t *data, size_t len) { this->ihash_.add(data, len); } + +void HmacMD5::calculate() { + uint8_t ibytes[16]; + + this->ihash_.calculate(); + this->ihash_.get_bytes(ibytes); + + this->ohash_.add(ibytes, sizeof(ibytes)); + this->ohash_.calculate(); +} + +void HmacMD5::get_bytes(uint8_t *output) { this->ohash_.get_bytes(output); } + +void HmacMD5::get_hex(char *output) { this->ohash_.get_hex(output); } + +bool HmacMD5::equals_bytes(const uint8_t *expected) { return this->ohash_.equals_bytes(expected); } + +bool HmacMD5::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); } + +} // namespace hmac_md5 +} // namespace esphome +#endif diff --git a/esphome/components/hmac_md5/hmac_md5.h b/esphome/components/hmac_md5/hmac_md5.h new file mode 100644 index 0000000000..b83b9d5421 --- /dev/null +++ b/esphome/components/hmac_md5/hmac_md5.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_MD5 +#include "esphome/components/md5/md5.h" +#include + +namespace esphome { +namespace hmac_md5 { + +class HmacMD5 { + public: + HmacMD5() = default; + ~HmacMD5() = default; + + /// Initialize a new MD5 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-MD5 digest as bytes. + /// The output must be able to hold 16 bytes or more. + 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. + void get_hex(char *output); + + /// Compare the digest against a provided byte-encoded digest (16 bytes). + bool equals_bytes(const uint8_t *expected); + + /// Compare the digest against a provided hex-encoded digest (32 bytes). + bool equals_hex(const char *expected); + + protected: + md5::MD5Digest ihash_; + md5::MD5Digest ohash_; +}; + +} // namespace hmac_md5 +} // namespace esphome +#endif diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index de3903d7e2..24f4b3f8f1 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -1,5 +1,6 @@ #include "hmc5883l.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace hmc5883l { @@ -31,6 +32,10 @@ void HMC5883LComponent::setup() { return; } + if (this->get_update_interval() < App.get_loop_interval()) { + high_freq_.start(); + } + if (id[0] != 0x48 || id[1] != 0x34 || id[2] != 0x33) { this->error_code_ = ID_REGISTERS; this->mark_failed(); diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 3481f45dc8..06fba2af9d 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -63,6 +63,7 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { COMMUNICATION_FAILED, ID_REGISTERS, } error_code_; + HighFrequencyLoopRequester high_freq_; }; } // namespace hmc5883l diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 26e8e2b60c..f2decea150 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -3,6 +3,10 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ADDRESS, + CONF_FIELD_STRENGTH_X, + CONF_FIELD_STRENGTH_Y, + CONF_FIELD_STRENGTH_Z, + CONF_HEADING, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, @@ -18,10 +22,6 @@ DEPENDENCIES = ["i2c"] hmc5883l_ns = cg.esphome_ns.namespace("hmc5883l") -CONF_FIELD_STRENGTH_X = "field_strength_x" -CONF_FIELD_STRENGTH_Y = "field_strength_y" -CONF_FIELD_STRENGTH_Z = "field_strength_z" -CONF_HEADING = "heading" HMC5883LComponent = hmc5883l_ns.class_( "HMC5883LComponent", cg.PollingComponent, i2c.I2CDevice diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 776aa7fd7b..223d6c18c3 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -2,9 +2,22 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@OttoWinter", "@esphome/core"] homeassistant_ns = cg.esphome_ns.namespace("homeassistant") + +def validate_entity_domain(platform, supported_domains): + def validator(config): + domain = config[CONF_ENTITY_ID].split(".", 1)[0] + if domain not in supported_domains: + raise cv.Invalid( + f"Entity ID {config[CONF_ENTITY_ID]} is not supported by the {platform} platform." + ) + return config + + return validator + + HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( { cv.Required(CONF_ENTITY_ID): cv.entity_id, @@ -13,6 +26,13 @@ HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( } ) +HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema( + { + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_INTERNAL, default=True): cv.boolean, + } +) + def setup_home_assistant_entity(var, config): cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) diff --git a/esphome/components/homeassistant/number/__init__.py b/esphome/components/homeassistant/number/__init__.py new file mode 100644 index 0000000000..a6cc615a64 --- /dev/null +++ b/esphome/components/homeassistant/number/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv + +from .. import ( + HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, +) + +CODEOWNERS = ["@landonr"] +DEPENDENCIES = ["api"] + +HomeassistantNumber = homeassistant_ns.class_( + "HomeassistantNumber", number.Number, cg.Component +) + +CONFIG_SCHEMA = ( + number.number_schema(HomeassistantNumber) + .extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await number.new_number( + config, + min_value=0, + max_value=0, + step=0, + ) + await cg.register_component(var, config) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp new file mode 100644 index 0000000000..d3e285f4ac --- /dev/null +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -0,0 +1,100 @@ +#include "homeassistant_number.h" + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_server.h" +#include "esphome/core/log.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); + if (!number_value.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; + } + if (this->state == number_value.value()) { + return; + } + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), state.c_str()); + this->publish_state(number_value.value()); +} + +void HomeassistantNumber::min_retrieved_(const std::string &min) { + auto min_value = parse_number(min); + 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_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); + 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_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); + 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_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str()); + this->traits.set_step(step_value.value()); +} + +void HomeassistantNumber::setup() { + api::global_api_server->subscribe_home_assistant_state( + this->entity_id_, nullopt, 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)); + api::global_api_server->get_home_assistant_state( + this->entity_id_, optional("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)); +} + +void HomeassistantNumber::dump_config() { + LOG_NUMBER("", "Homeassistant Number", this); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); +} + +float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +void HomeassistantNumber::control(float value) { + if (!api::global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients connected to API server"); + return; + } + + this->publish_state(value); + + api::HomeassistantServiceResponse resp; + resp.service = "number.set_value"; + + api::HomeassistantServiceMap entity_id; + entity_id.key = "entity_id"; + entity_id.value = this->entity_id_; + resp.data.push_back(entity_id); + + api::HomeassistantServiceMap entity_value; + entity_value.key = "value"; + entity_value.value = to_string(value); + resp.data.push_back(entity_value); + + api::global_api_server->send_homeassistant_service_call(resp); +} + +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/homeassistant/number/homeassistant_number.h b/esphome/components/homeassistant/number/homeassistant_number.h new file mode 100644 index 0000000000..0860b4e91c --- /dev/null +++ b/esphome/components/homeassistant/number/homeassistant_number.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "esphome/components/number/number.h" +#include "esphome/core/component.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 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 control(float value) override; + + std::string entity_id_; +}; +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index f5e73c8854..35e660f7c1 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -12,7 +12,7 @@ void HomeassistantSensor::setup() { this->entity_id_, this->attribute_, [this](const std::string &state) { auto val = parse_number(state); if (!val.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); this->publish_state(NAN); return; } diff --git a/esphome/components/homeassistant/switch/__init__.py b/esphome/components/homeassistant/switch/__init__.py new file mode 100644 index 0000000000..384f82bbad --- /dev/null +++ b/esphome/components/homeassistant/switch/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import CONF_ID + +from .. import ( + HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, + validate_entity_domain, +) + +CODEOWNERS = ["@Links2004"] +DEPENDENCIES = ["api"] + +SUPPORTED_DOMAINS = [ + "automation", + "fan", + "humidifier", + "input_boolean", + "light", + "remote", + "siren", + "switch", +] + +HomeassistantSwitch = homeassistant_ns.class_( + "HomeassistantSwitch", switch.Switch, cg.Component +) + +CONFIG_SCHEMA = cv.All( + switch.switch_schema(HomeassistantSwitch) + .extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + validate_entity_domain("switch", SUPPORTED_DOMAINS), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await switch.register_switch(var, config) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp new file mode 100644 index 0000000000..0451c95069 --- /dev/null +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -0,0 +1,59 @@ +#include "homeassistant_switch.h" +#include "esphome/components/api/api_server.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace homeassistant { + +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) { + 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; + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + this->publish_state(new_state); + break; + } + }); +} + +void HomeassistantSwitch::dump_config() { + LOG_SWITCH("", "Homeassistant Switch", this); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); +} + +float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +void HomeassistantSwitch::write_state(bool state) { + if (!api::global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients connected to API server"); + return; + } + + api::HomeassistantServiceResponse resp; + if (state) { + resp.service = "homeassistant.turn_on"; + } else { + resp.service = "homeassistant.turn_off"; + } + + api::HomeassistantServiceMap entity_id_kv; + entity_id_kv.key = "entity_id"; + entity_id_kv.value = this->entity_id_; + resp.data.push_back(entity_id_kv); + + api::global_api_server->send_homeassistant_service_call(resp); +} + +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.h b/esphome/components/homeassistant/switch/homeassistant_switch.h new file mode 100644 index 0000000000..a4da257960 --- /dev/null +++ b/esphome/components/homeassistant/switch/homeassistant_switch.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +namespace esphome { +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 setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + void write_state(bool state) override; + std::string entity_id_; +}; + +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/__init__.py b/esphome/components/honeywell_hih_i2c/__init__.py new file mode 100644 index 0000000000..8d13fcb152 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/__init__.py @@ -0,0 +1,3 @@ +"""Support for Honeywell HumidIcon HIH""" + +CODEOWNERS = ["@Benichou34"] diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp new file mode 100644 index 0000000000..64d5ddb541 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -0,0 +1,97 @@ +// Honeywell HumidIcon I2C Sensors +// https://prod-edam.honeywell.com/content/dam/honeywell-edam/sps/siot/en-us/products/sensors/humidity-with-temperature-sensors/common/documents/sps-siot-i2c-comms-humidicon-tn-009061-2-en-ciid-142171.pdf +// + +#include "honeywell_hih.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace honeywell_hih_i2c { + +static const char *const TAG = "honeywell_hih.i2c"; + +static const uint8_t REQUEST_CMD[1] = {0x00}; // Measurement Request Format +static const uint16_t MAX_COUNT = 0x3FFE; // 2^14 - 2 + +void HoneywellHIComponent::read_sensor_data_() { + uint8_t data[4]; + + if (this->read(data, sizeof(data)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return; + } + + const uint16_t raw_humidity = (static_cast(data[0] & 0x3F) << 8) | data[1]; + float humidity = (static_cast(raw_humidity) / MAX_COUNT) * 100; + + const uint16_t raw_temperature = (static_cast(data[2]) << 6) | (data[3] >> 2); + float temperature = (static_cast(raw_temperature) / MAX_COUNT) * 165 - 40; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); +} + +void HoneywellHIComponent::start_measurement_() { + if (this->write(REQUEST_CMD, sizeof(REQUEST_CMD)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return; + } + + this->measurement_running_ = true; +} + +bool HoneywellHIComponent::is_measurement_ready_() { + uint8_t data[1]; + + if (this->read(data, sizeof(data)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return false; + } + + // Check status bits + return ((data[0] & 0xC0) == 0x00); +} + +void HoneywellHIComponent::measurement_timeout_() { + ESP_LOGE(TAG, "Honeywell HIH Timeout!"); + this->measurement_running_ = false; + this->mark_failed(); +} + +void HoneywellHIComponent::update() { + ESP_LOGV(TAG, "Update Honeywell HIH Sensor"); + + this->start_measurement_(); + // The measurement cycle duration is typically 36.65 ms for temperature and humidity readings. + this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout_(); }); +} + +void HoneywellHIComponent::loop() { + if (this->measurement_running_ && this->is_measurement_ready_()) { + this->measurement_running_ = false; + this->cancel_timeout("meas_timeout"); + this->read_sensor_data_(); + } +} + +void HoneywellHIComponent::dump_config() { + ESP_LOGD(TAG, "Honeywell HIH:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + } + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_UPDATE_INTERVAL(this); +} + +float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace honeywell_hih_i2c +} // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.h b/esphome/components/honeywell_hih_i2c/honeywell_hih.h new file mode 100644 index 0000000000..4457eab1da --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.h @@ -0,0 +1,34 @@ +// Honeywell HumidIcon I2C Sensors +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace honeywell_hih_i2c { + +class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + protected: + bool measurement_running_{false}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + private: + void read_sensor_data_(); + void start_measurement_(); + bool is_measurement_ready_(); + void measurement_timeout_(); +}; + +} // namespace honeywell_hih_i2c +} // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/sensor.py b/esphome/components/honeywell_hih_i2c/sensor.py new file mode 100644 index 0000000000..f5a6ad2398 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, +) + +DEPENDENCIES = ["i2c"] + +honeywell_hih_ns = cg.esphome_ns.namespace("honeywell_hih_i2c") +HONEYWELLHIComponent = honeywell_hih_ns.class_( + "HoneywellHIComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HONEYWELLHIComponent), + 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_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x27)) +) + + +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) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/honeywellabp2_i2c/__init__.py b/esphome/components/honeywellabp2_i2c/__init__.py new file mode 100644 index 0000000000..29a910eca9 --- /dev/null +++ b/esphome/components/honeywellabp2_i2c/__init__.py @@ -0,0 +1,3 @@ +"""Support for Honeywell ABP2""" + +CODEOWNERS = ["@jpfaff"] diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp new file mode 100644 index 0000000000..e2910032cc --- /dev/null +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp @@ -0,0 +1,108 @@ +#include "honeywellabp2.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace honeywellabp2_i2c { + +static const uint8_t STATUS_BIT_POWER = 6; +static const uint8_t STATUS_BIT_BUSY = 5; +static const uint8_t STATUS_BIT_ERROR = 2; +static const uint8_t STATUS_MATH_SAT = 0; + +static const char *const TAG = "honeywellabp2"; + +void HONEYWELLABP2Sensor::read_sensor_data() { + if (this->read(raw_data_, 7) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with ABP2 failed!"); + this->mark_failed(); + return; + } + float press_counts = encode_uint24(raw_data_[1], raw_data_[2], raw_data_[3]); // calculate digital pressure counts + float temp_counts = encode_uint24(raw_data_[4], raw_data_[5], raw_data_[6]); // calculate digital temperature counts + + this->last_pressure_ = (((press_counts - this->min_count_) / (this->max_count_ - this->min_count_)) * + (this->max_pressure_ - this->min_pressure_)) + + this->min_pressure_; + this->last_temperature_ = (temp_counts * 200 / 16777215) - 50; +} + +void HONEYWELLABP2Sensor::start_measurement() { + if (this->write(i2c_cmd_, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with ABP2 failed!"); + this->mark_failed(); + return; + } + this->measurement_running_ = true; +} + +bool HONEYWELLABP2Sensor::is_measurement_ready() { + if (this->read(raw_data_, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with ABP2 failed!"); + this->mark_failed(); + return false; + } + if ((raw_data_[0] & (0x1 << STATUS_BIT_BUSY)) > 0) { + return false; + } + this->measurement_running_ = false; + return true; +} + +void HONEYWELLABP2Sensor::measurement_timeout() { + ESP_LOGE(TAG, "Timeout!"); + this->measurement_running_ = false; + this->mark_failed(); +} + +float HONEYWELLABP2Sensor::get_pressure() { return this->last_pressure_; } + +float HONEYWELLABP2Sensor::get_temperature() { return this->last_temperature_; } + +void HONEYWELLABP2Sensor::loop() { + if (this->measurement_running_) { + if (this->is_measurement_ready()) { + this->cancel_timeout("meas_timeout"); + + this->read_sensor_data(); + if (pressure_sensor_ != nullptr) { + this->pressure_sensor_->publish_state(this->get_pressure()); + } + if (temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(this->get_temperature()); + } + } + } +} + +void HONEYWELLABP2Sensor::update() { + ESP_LOGV(TAG, "Update Honeywell ABP2 Sensor"); + + this->start_measurement(); + this->set_timeout("meas_timeout", 50, [this] { this->measurement_timeout(); }); +} + +void HONEYWELLABP2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, " Min Pressure Range: %0.1f", this->min_pressure_); + ESP_LOGCONFIG(TAG, " Max Pressure Range: %0.1f", this->max_pressure_); + if (this->transfer_function_ == ABP2_TRANS_FUNC_A) { + ESP_LOGCONFIG(TAG, " Transfer function A"); + } else { + ESP_LOGCONFIG(TAG, " Transfer function B"); + } + LOG_UPDATE_INTERVAL(this); +} + +void HONEYWELLABP2Sensor::set_transfer_function(ABP2TRANFERFUNCTION transfer_function) { + this->transfer_function_ = transfer_function; + if (this->transfer_function_ == ABP2_TRANS_FUNC_B) { + this->max_count_ = this->max_count_b_; + this->min_count_ = this->min_count_b_; + } else { + this->max_count_ = this->max_count_a_; + this->min_count_ = this->min_count_a_; + } +} + +} // namespace honeywellabp2_i2c +} // namespace esphome diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h new file mode 100644 index 0000000000..bc81524ac2 --- /dev/null +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -0,0 +1,60 @@ +// for Honeywell ABP sensor +// adapting code from https://github.com/vwls/Honeywell_pressure_sensors +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace honeywellabp2_i2c { + +enum ABP2TRANFERFUNCTION { ABP2_TRANS_FUNC_A = 0, ABP2_TRANS_FUNC_B = 1 }; + +class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { + public: + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }; + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }; + void loop() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + void dump_config() override; + + void read_sensor_data(); + void start_measurement(); + bool is_measurement_ready(); + void measurement_timeout(); + + float get_pressure(); + float get_temperature(); + + void set_min_pressure(float min_pressure) { this->min_pressure_ = min_pressure; }; + void set_max_pressure(float max_pressure) { this->max_pressure_ = max_pressure; }; + void set_transfer_function(ABP2TRANFERFUNCTION transfer_function); + + protected: + float min_pressure_ = 0.0; + float max_pressure_ = 0.0; + ABP2TRANFERFUNCTION transfer_function_ = ABP2_TRANS_FUNC_A; + + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + + const float max_count_a_ = 15099494.4; // (90% of 2^24 counts or 0xE66666) + const float min_count_a_ = 1677721.6; // (10% of 2^24 counts or 0x19999A) + const float max_count_b_ = 11744051.2; // (70% of 2^24 counts or 0xB33333) + const float min_count_b_ = 5033164.8; // (30% of 2^24 counts or 0x4CCCCC) + + float max_count_; + float min_count_; + bool measurement_running_ = false; + + uint8_t raw_data_[7]; // holds output data + uint8_t i2c_cmd_[3] = {0xAA, 0x00, 0x00}; // command to be sent + float last_pressure_; + float last_temperature_; +}; + +} // namespace honeywellabp2_i2c +} // namespace esphome diff --git a/esphome/components/honeywellabp2_i2c/sensor.py b/esphome/components/honeywellabp2_i2c/sensor.py new file mode 100644 index 0000000000..c38a380127 --- /dev/null +++ b/esphome/components/honeywellabp2_i2c/sensor.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["i2c"] + +honeywellabp2_ns = cg.esphome_ns.namespace("honeywellabp2_i2c") + +CONF_MIN_PRESSURE = "min_pressure" +CONF_MAX_PRESSURE = "max_pressure" +TRANSFER_FUNCTION = "transfer_function" +ABP2TRANFERFUNCTION = honeywellabp2_ns.enum("ABP2TRANFERFUNCTION") +TRANS_FUNC_OPTIONS = { + "A": ABP2TRANFERFUNCTION.ABP2_TRANS_FUNC_A, + "B": ABP2TRANFERFUNCTION.ABP2_TRANS_FUNC_B, +} + +HONEYWELLABP2Sensor = honeywellabp2_ns.class_( + "HONEYWELLABP2Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HONEYWELLABP2Sensor), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement="Pa", + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Required(CONF_MIN_PRESSURE): cv.float_, + cv.Required(CONF_MAX_PRESSURE): cv.float_, + cv.Required(TRANSFER_FUNCTION): cv.enum(TRANS_FUNC_OPTIONS), + } + ), + 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, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +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) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_min_pressure(pressure_config[CONF_MIN_PRESSURE])) + cg.add(var.set_max_pressure(pressure_config[CONF_MAX_PRESSURE])) + cg.add(var.set_transfer_function(pressure_config[TRANSFER_FUNCTION])) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 46f763d255..eb8cfbd984 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -1,38 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_MAC_ADDRESS, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + PLATFORM_HOST, ) from esphome.core import CORE -import esphome.config_validation as cv -import esphome.codegen as cg from .const import KEY_HOST # force import gpio to register pin schema from .gpio import host_pin_to_code # noqa - -CODEOWNERS = ["@esphome/core"] -AUTO_LOAD = ["network"] +CODEOWNERS = ["@esphome/core", "@clydebarrow"] +AUTO_LOAD = ["network", "preferences"] def set_core_data(config): CORE.data[KEY_HOST] = {} - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "host" + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_HOST CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(1, 0, 0) return config CONFIG_SCHEMA = cv.All( - cv.Schema({}), + cv.Schema( + { + cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address, + } + ), set_core_data, ) async def to_code(config): cg.add_build_flag("-DUSE_HOST") + cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) + cg.add_build_flag("-std=c++17") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index d523d28ee5..0f22a790bd 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -1,5 +1,8 @@ import logging +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, @@ -11,16 +14,11 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) -from esphome import pins -import esphome.config_validation as cv -import esphome.codegen as cg from .const import host_ns - _LOGGER = logging.getLogger(__name__) - HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin) @@ -30,8 +28,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: @@ -45,21 +45,10 @@ def validate_gpio_pin(value): return _translate_pin(value) -HOST_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(HostGPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, +HOST_PIN_SCHEMA = pins.gpio_base_schema( + HostGPIOPin, + validate_gpio_pin, + modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN, CONF_PULLUP, CONF_PULLDOWN], ) diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index bf45893e40..7b939cdebb 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -1,36 +1,87 @@ #ifdef USE_HOST +#include +#include #include "preferences.h" -#include -#include "esphome/core/preferences.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" -#include "esphome/core/defines.h" +#include "esphome/core/application.h" namespace esphome { namespace host { +namespace fs = std::filesystem; static const char *const TAG = "host.preferences"; -class HostPreferences : public ESPPreferences { - public: - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { return {}; } +void HostPreferences::setup_() { + if (this->setup_complete_) + return; + this->filename_.append(getenv("HOME")); + this->filename_.append("/.esphome"); + this->filename_.append("/prefs"); + fs::create_directories(this->filename_); + this->filename_.append("/"); + this->filename_.append(App.get_name()); + this->filename_.append(".prefs"); + FILE *fp = fopen(this->filename_.c_str(), "rb"); + if (fp != nullptr) { + while (!feof((fp))) { + uint32_t key; + uint8_t len; + if (fread(&key, sizeof(key), 1, fp) != 1) + break; + if (fread(&len, sizeof(len), 1, fp) != 1) + break; + uint8_t data[len]; + if (fread(data, sizeof(uint8_t), len, fp) != len) + break; + std::vector vec(data, data + len); + this->data[key] = vec; + } + fclose(fp); + } + this->setup_complete_ = true; +} - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { return {}; } +bool HostPreferences::sync() { + this->setup_(); + FILE *fp = fopen(this->filename_.c_str(), "wb"); + std::map>::iterator it; - bool sync() override { return true; } - bool reset() override { return true; } + for (it = this->data.begin(); it != this->data.end(); ++it) { + fwrite(&it->first, sizeof(uint32_t), 1, fp); + uint8_t len = it->second.size(); + fwrite(&len, sizeof(len), 1, fp); + fwrite(it->second.data(), sizeof(uint8_t), it->second.size(), fp); + } + fclose(fp); + return true; +} + +bool HostPreferences::reset() { + host_preferences->data.clear(); + return true; +} + +ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t type, bool in_flash) { + auto backend = new HostPreferenceBackend(type); + return ESPPreferenceObject(backend); }; void setup_preferences() { auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory) + host_preferences = pref; global_preferences = pref; } +bool HostPreferenceBackend::save(const uint8_t *data, size_t len) { + return host_preferences->save(this->key_, data, len); +} + +bool HostPreferenceBackend::load(uint8_t *data, size_t len) { return host_preferences->load(this->key_, data, len); } + +HostPreferences *host_preferences; } // namespace host ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif // USE_HOST diff --git a/esphome/components/host/preferences.h b/esphome/components/host/preferences.h index 7462360ec3..6707366517 100644 --- a/esphome/components/host/preferences.h +++ b/esphome/components/host/preferences.h @@ -2,10 +2,63 @@ #ifdef USE_HOST +#include "esphome/core/preferences.h" +#include + namespace esphome { namespace host { +class HostPreferenceBackend : public ESPPreferenceBackend { + public: + explicit HostPreferenceBackend(uint32_t key) { this->key_ = key; } + + bool save(const uint8_t *data, size_t len) override; + bool load(uint8_t *data, size_t len) override; + + protected: + uint32_t key_{}; +}; + +class HostPreferences : public ESPPreferences { + public: + bool sync() override; + bool reset() override; + + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override; + ESPPreferenceObject make_preference(size_t length, uint32_t type) override { + return make_preference(length, type, false); + } + + bool save(uint32_t key, const uint8_t *data, size_t len) { + if (len > 255) + return false; + this->setup_(); + std::vector vec(data, data + len); + this->data[key] = vec; + return true; + } + + bool load(uint32_t key, uint8_t *data, size_t len) { + if (len > 255) + return false; + this->setup_(); + if (this->data.count(key) == 0) + return false; + auto vec = this->data[key]; + if (vec.size() != len) + return false; + memcpy(data, vec.data(), len); + return true; + } + + protected: + void setup_(); + bool setup_complete_{}; + std::string filename_{}; + std::map> data{}; +}; void setup_preferences(); +extern HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace host } // namespace esphome diff --git a/esphome/components/host/time/__init__.py b/esphome/components/host/time/__init__.py new file mode 100644 index 0000000000..76a88d98a1 --- /dev/null +++ b/esphome/components/host/time/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +from esphome.const import CONF_ID +import esphome.config_validation as cv +from esphome.components import time as time_ + +CODEOWNERS = ["@clydebarrow"] + +time_ns = cg.esphome_ns.namespace("host") +HostTime = time_ns.class_("HostTime", time_.RealTimeClock) +CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HostTime), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await time_.register_time(var, config) diff --git a/esphome/components/host/time/host_time.h b/esphome/components/host/time/host_time.h new file mode 100644 index 0000000000..4f1473b809 --- /dev/null +++ b/esphome/components/host/time/host_time.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace host { + +class HostTime : public time::RealTimeClock { + public: + void update() override {} +}; + +} // namespace host +} // namespace esphome diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp index bd1c82c96b..b56e96badc 100644 --- a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -1,9 +1,10 @@ // Official Datasheet: -// https://www.maxbotix.com/documents/HRXL-MaxSonar-WR_Datasheet.pdf +// HRXL: https://www.maxbotix.com/documents/HRXL-MaxSonar-WR_Datasheet.pdf +// XL: https://www.maxbotix.com/documents/XL-MaxSonar-WR_Datasheet.pdf // // This implementation is designed to work with the TTL Versions of the -// MaxBotix HRXL MaxSonar WR sensor series. The sensor's TTL Pin (5) should be -// wired to one of the ESP's input pins and configured as uart rx_pin. +// MaxBotix HRXL and XL MaxSonar WR sensor series. The sensor's TTL Pin (5) +// should be wired to one of the ESP's input pins and configured as uart rx_pin. #include "hrxl_maxsonar_wr.h" #include "esphome/core/log.h" @@ -17,8 +18,10 @@ static const uint8_t ASCII_NBSP = 0xFF; static const int MAX_DATA_LENGTH_BYTES = 6; /** - * The sensor outputs something like "R1234\r" at a fixed rate of 6 Hz. Where - * 1234 means a distance of 1,234 m. + * HRXL sensors output the format "R1234\r" at 6Hz + * The 1234 means 1234mm + * XL sensors output the format "R123\r" at 5 to 10Hz + * The 123 means 123cm */ void HrxlMaxsonarWrComponent::loop() { uint8_t data; @@ -42,9 +45,17 @@ void HrxlMaxsonarWrComponent::check_buffer_() { if (this->buffer_.back() == static_cast(ASCII_CR) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) { ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.c_str()); - if (this->buffer_.length() == MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && - this->buffer_.back() == static_cast(ASCII_CR)) { - int millimeters = parse_number(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2)).value_or(0); + size_t rpos = this->buffer_.find(static_cast(ASCII_CR)); + + if (this->buffer_.length() <= MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && rpos != std::string::npos) { + std::string distance = this->buffer_.substr(1, rpos - 1); + int millimeters = parse_number(distance).value_or(0); + + // XL reports in cm instead of mm and reports 3 digits instead of 4 + if (distance.length() == 3) { + millimeters = millimeters * 10; + } + float meters = float(millimeters) / 1000.0; ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters); this->publish_state(meters); diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 0c3e249512..78064fb4b4 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -1,92 +1,133 @@ -import urllib.parse as urlparse - -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg +from esphome.components import esp32 +import esphome.config_validation as cv from esphome.const import ( + CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ID, - CONF_TIMEOUT, CONF_METHOD, + CONF_ON_ERROR, + CONF_TIMEOUT, CONF_TRIGGER_ID, CONF_URL, - CONF_ESP8266_DISABLE_SSL_SUPPORT, + __version__, ) -from esphome.core import Lambda, CORE +from esphome.core import CORE, Lambda DEPENDENCIES = ["network"] -AUTO_LOAD = ["json"] +AUTO_LOAD = ["json", "watchdog"] http_request_ns = cg.esphome_ns.namespace("http_request") HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component) +HttpRequestArduino = http_request_ns.class_("HttpRequestArduino", HttpRequestComponent) +HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent) + +HttpContainer = http_request_ns.class_("HttpContainer") + HttpRequestSendAction = http_request_ns.class_( "HttpRequestSendAction", automation.Action ) HttpRequestResponseTrigger = http_request_ns.class_( - "HttpRequestResponseTrigger", automation.Trigger + "HttpRequestResponseTrigger", + automation.Trigger.template( + cg.std_shared_ptr.template(HttpContainer), cg.std_string + ), ) -CONF_HEADERS = "headers" +CONF_HTTP_REQUEST_ID = "http_request_id" + CONF_USERAGENT = "useragent" -CONF_BODY = "body" -CONF_JSON = "json" CONF_VERIFY_SSL = "verify_ssl" -CONF_ON_RESPONSE = "on_response" CONF_FOLLOW_REDIRECTS = "follow_redirects" CONF_REDIRECT_LIMIT = "redirect_limit" +CONF_WATCHDOG_TIMEOUT = "watchdog_timeout" +CONF_BUFFER_SIZE_RX = "buffer_size_rx" +CONF_BUFFER_SIZE_TX = "buffer_size_tx" + +CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" +CONF_ON_RESPONSE = "on_response" +CONF_HEADERS = "headers" +CONF_BODY = "body" +CONF_JSON = "json" +CONF_CAPTURE_RESPONSE = "capture_response" def validate_url(value): - value = cv.string(value) - try: - parsed = list(urlparse.urlparse(value)) - except Exception as err: - raise cv.Invalid("Invalid URL") from err - - if not parsed[0] or not parsed[1]: - raise cv.Invalid("URL must have a URL scheme and host") - - if parsed[0] not in ["http", "https"]: - raise cv.Invalid("Scheme must be http or https") - - if not parsed[2]: - parsed[2] = "/" - - return urlparse.urlunparse(parsed) + value = cv.url(value) + if value.startswith("http://") or value.startswith("https://"): + return value + raise cv.Invalid("URL must start with 'http://' or 'https://'") -def validate_secure_url(config): - url_ = config[CONF_URL] +def validate_ssl_verification(config): + error_message = "" + + if CORE.is_esp32: + if 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" + if ( - config.get(CONF_VERIFY_SSL) - and not isinstance(url_, Lambda) - and url_.lower().startswith("https:") + CORE.is_esp8266 + and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT] + and config[CONF_VERIFY_SSL] ): + error_message = "ESPHome does not support certificate verification on ESP8266" + + if len(error_message) > 0: raise cv.Invalid( - "Currently ESPHome doesn't support SSL verification. " - "Set 'verify_ssl: false' to make insecure HTTPS requests." + f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections." ) + return config +def _declare_request_class(value): + if CORE.using_esp_idf: + return cv.declare_id(HttpRequestIDF)(value) + if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040: + return cv.declare_id(HttpRequestArduino)(value) + return NotImplementedError + + CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.GenerateID(): cv.declare_id(HttpRequestComponent), - cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string, + cv.GenerateID(): _declare_request_class, + cv.Optional( + CONF_USERAGENT, f"ESPHome/{__version__} (https://esphome.io)" + ): cv.string, cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean, cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_, cv.Optional( - CONF_TIMEOUT, default="5s" + CONF_TIMEOUT, default="4.5s" ): cv.positive_time_period_milliseconds, cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All( cv.only_on_esp8266, cv.boolean ), + cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All( + cv.Any(cv.only_on_esp32, cv.only_on_rp2040), + 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_TX, esp32_idf=512): cv.All( + cv.uint16_t, cv.only_with_esp_idf + ), } ).extend(cv.COMPONENT_SCHEMA), cv.require_framework_version( esp8266_arduino=cv.Version(2, 5, 1), esp32_arduino=cv.Version(0, 0, 0), + esp_idf=cv.Version(0, 0, 0), + rp2040_arduino=cv.Version(0, 0, 0), ), + validate_ssl_verification, ) @@ -100,11 +141,33 @@ async def to_code(config): if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]: cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") + if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT): + cg.add(var.set_watchdog_timeout(timeout_ms)) + if CORE.is_esp32: - cg.add_library("WiFiClientSecure", None) - cg.add_library("HTTPClient", None) + 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])) + + 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("WiFiClientSecure", None) + cg.add_library("HTTPClient", None) if CORE.is_esp8266: cg.add_library("ESP8266HTTPClient", None) + if CORE.is_rp2040 and CORE.using_arduino: + cg.add_library("HTTPClient", None) await cg.register_component(var, config) @@ -116,12 +179,23 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_HEADERS): cv.All( cv.Schema({cv.string: cv.templatable(cv.string)}) ), - cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + cv.Optional(CONF_VERIFY_SSL): cv.invalid( + f"{CONF_VERIFY_SSL} has moved to the base component configuration." + ), + cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + automation.Trigger.template() + ) + } + ), + cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes, } -).add_extra(validate_secure_url) +) HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf( CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend( @@ -173,6 +247,9 @@ async def http_request_action_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) cg.add(var.set_method(config[CONF_METHOD])) + cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE])) + cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE])) + if CONF_BODY in config: template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string) cg.add(var.set_body(template_)) @@ -196,7 +273,16 @@ async def http_request_action_to_code(config, action_id, template_arg, args): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) cg.add(var.register_response_trigger(trigger)) await automation.build_automation( - trigger, [(int, "status_code"), (cg.uint32, "duration_ms")], conf + trigger, + [ + (cg.std_shared_ptr.template(HttpContainer), "response"), + (cg.std_string_ref, "body"), + ], + conf, ) + for conf in config.get(CONF_ON_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_error_trigger(trigger)) + await automation.build_automation(trigger, [], conf) return var diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 46894a9afd..be8bef006e 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -1,9 +1,8 @@ -#ifdef USE_ARDUINO - #include "http_request.h" -#include "esphome/core/defines.h" + #include "esphome/core/log.h" -#include "esphome/components/network/util.h" + +#include namespace esphome { namespace http_request { @@ -14,131 +13,12 @@ void HttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "HTTP Request:"); ESP_LOGCONFIG(TAG, " Timeout: %ums", this->timeout_); ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_); - ESP_LOGCONFIG(TAG, " Follow Redirects: %d", this->follow_redirects_); + ESP_LOGCONFIG(TAG, " Follow redirects: %s", YESNO(this->follow_redirects_)); ESP_LOGCONFIG(TAG, " Redirect limit: %d", this->redirect_limit_); -} - -void HttpRequestComponent::set_url(std::string url) { - this->url_ = std::move(url); - this->secure_ = this->url_.compare(0, 6, "https:") == 0; - - if (!this->last_url_.empty() && this->url_ != this->last_url_) { - // Close connection if url has been changed - this->client_.setReuse(false); - this->client_.end(); + if (this->watchdog_timeout_ > 0) { + ESP_LOGCONFIG(TAG, " Watchdog Timeout: %" PRIu32 "ms", this->watchdog_timeout_); } - this->client_.setReuse(true); -} - -void HttpRequestComponent::send(const std::vector &response_triggers) { - if (!network::is_connected()) { - this->client_.end(); - this->status_set_warning(); - ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); - return; - } - - bool begin_status = false; - const String url = this->url_.c_str(); -#if defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)) -#if defined(USE_ESP32) || USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) - if (this->follow_redirects_) { - this->client_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); - } else { - this->client_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS); - } -#else - this->client_.setFollowRedirects(this->follow_redirects_); -#endif - this->client_.setRedirectLimit(this->redirect_limit_); -#endif -#if defined(USE_ESP32) - begin_status = this->client_.begin(url); -#elif defined(USE_ESP8266) - begin_status = this->client_.begin(*this->get_wifi_client_(), url); -#endif - - if (!begin_status) { - this->client_.end(); - this->status_set_warning(); - ESP_LOGW(TAG, "HTTP Request failed at the begin phase. Please check the configuration"); - return; - } - - this->client_.setTimeout(this->timeout_); -#if defined(USE_ESP32) - this->client_.setConnectTimeout(this->timeout_); -#endif - if (this->useragent_ != nullptr) { - this->client_.setUserAgent(this->useragent_); - } - for (const auto &header : this->headers_) { - this->client_.addHeader(header.name, header.value, false, true); - } - - uint32_t start_time = millis(); - int http_code = this->client_.sendRequest(this->method_, this->body_.c_str()); - uint32_t duration = millis() - start_time; - for (auto *trigger : response_triggers) - trigger->process(http_code, duration); - - if (http_code < 0) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s; Duration: %u ms", this->url_.c_str(), - HTTPClient::errorToString(http_code).c_str(), duration); - this->status_set_warning(); - return; - } - - if (http_code < 200 || http_code >= 300) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration); - this->status_set_warning(); - return; - } - - this->status_clear_warning(); - ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration); -} - -#ifdef USE_ESP8266 -std::shared_ptr HttpRequestComponent::get_wifi_client_() { -#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS - if (this->secure_) { - if (this->wifi_client_secure_ == nullptr) { - this->wifi_client_secure_ = std::make_shared(); - this->wifi_client_secure_->setInsecure(); - this->wifi_client_secure_->setBufferSizes(512, 512); - } - return this->wifi_client_secure_; - } -#endif - - if (this->wifi_client_ == nullptr) { - this->wifi_client_ = std::make_shared(); - } - return this->wifi_client_; -} -#endif - -void HttpRequestComponent::close() { - this->last_url_ = this->url_; - this->client_.end(); -} - -const char *HttpRequestComponent::get_string() { -#if defined(ESP32) - // The static variable is here because HTTPClient::getString() returns a String on ESP32, - // and we need something to keep a buffer alive. - static String str; -#else - // However on ESP8266, HTTPClient::getString() returns a String& to a member variable. - // Leaving this the default so that any new platform either doesn't copy, or encounters a compilation error. - auto & -#endif - str = this->client_.getString(); - return str.c_str(); } } // namespace http_request } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 0958c07683..4ed2c834f8 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -1,27 +1,18 @@ #pragma once -#ifdef USE_ARDUINO - -#include "esphome/components/json/json_util.h" -#include "esphome/core/automation.h" -#include "esphome/core/component.h" -#include "esphome/core/defines.h" - #include #include #include #include #include -#ifdef USE_ESP32 -#include -#endif -#ifdef USE_ESP8266 -#include -#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS -#include -#endif -#endif +#include "esphome/components/json/json_util.h" +#include "esphome/core/application.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace http_request { @@ -31,9 +22,89 @@ struct Header { const char *value; }; -class HttpRequestResponseTrigger : public Trigger { +// Some common HTTP status codes +enum HttpStatus { + HTTP_STATUS_OK = 200, + HTTP_STATUS_NO_CONTENT = 204, + HTTP_STATUS_PARTIAL_CONTENT = 206, + + /* 3xx - Redirection */ + HTTP_STATUS_MULTIPLE_CHOICES = 300, + HTTP_STATUS_MOVED_PERMANENTLY = 301, + HTTP_STATUS_FOUND = 302, + HTTP_STATUS_SEE_OTHER = 303, + HTTP_STATUS_NOT_MODIFIED = 304, + HTTP_STATUS_TEMPORARY_REDIRECT = 307, + HTTP_STATUS_PERMANENT_REDIRECT = 308, + + /* 4XX - CLIENT ERROR */ + HTTP_STATUS_BAD_REQUEST = 400, + HTTP_STATUS_UNAUTHORIZED = 401, + HTTP_STATUS_FORBIDDEN = 403, + HTTP_STATUS_NOT_FOUND = 404, + HTTP_STATUS_METHOD_NOT_ALLOWED = 405, + HTTP_STATUS_NOT_ACCEPTABLE = 406, + HTTP_STATUS_LENGTH_REQUIRED = 411, + + /* 5xx - Server Error */ + HTTP_STATUS_INTERNAL_ERROR = 500 +}; + +/** + * @brief Returns true if the HTTP status code is a redirect. + * + * @param status the HTTP status code to check + * @return true if the status code is a redirect, false otherwise + */ +inline bool is_redirect(int const status) { + switch (status) { + case HTTP_STATUS_MOVED_PERMANENTLY: + case HTTP_STATUS_FOUND: + case HTTP_STATUS_SEE_OTHER: + case HTTP_STATUS_TEMPORARY_REDIRECT: + case HTTP_STATUS_PERMANENT_REDIRECT: + return true; + default: + return false; + } +} + +/** + * @brief Checks if the given HTTP status code indicates a successful request. + * + * A successful request is one where the status code is in the range 200-299 + * + * @param status the HTTP status code to check + * @return true if the status code indicates a successful request, false otherwise + */ +inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; } + +class HttpRequestComponent; + +class HttpContainer : public Parented { public: - void process(int32_t status_code, uint32_t duration_ms) { this->trigger(status_code, duration_ms); } + virtual ~HttpContainer() = default; + size_t content_length; + int status_code; + uint32_t duration_ms; + + virtual int read(uint8_t *buf, size_t max_len) = 0; + virtual void end() = 0; + + void set_secure(bool secure) { this->secure_ = secure; } + + size_t get_bytes_read() const { return this->bytes_read_; } + + protected: + size_t bytes_read_{0}; + bool secure_{false}; +}; + +class HttpRequestResponseTrigger : public Trigger, std::string &> { + public: + void process(std::shared_ptr container, std::string &response_body) { + this->trigger(std::move(container), response_body); + } }; class HttpRequestComponent : public Component { @@ -41,37 +112,33 @@ class HttpRequestComponent : public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - void set_url(std::string url); - void set_method(const char *method) { this->method_ = method; } void set_useragent(const char *useragent) { this->useragent_ = useragent; } void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } + void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; } + uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; } void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; } - void set_body(const std::string &body) { this->body_ = body; } - void set_headers(std::list
headers) { this->headers_ = std::move(headers); } - void send(const std::vector &response_triggers); - void close(); - const char *get_string(); + + std::shared_ptr get(std::string url) { return this->start(std::move(url), "GET", "", {}); } + std::shared_ptr get(std::string url, std::list
headers) { + return this->start(std::move(url), "GET", "", std::move(headers)); + } + std::shared_ptr post(std::string url, std::string body) { + return this->start(std::move(url), "POST", std::move(body), {}); + } + std::shared_ptr post(std::string url, std::string body, std::list
headers) { + return this->start(std::move(url), "POST", std::move(body), std::move(headers)); + } + + virtual std::shared_ptr start(std::string url, std::string method, std::string body, + std::list
headers) = 0; protected: - HTTPClient client_{}; - std::string url_; - std::string last_url_; - const char *method_; const char *useragent_{nullptr}; - bool secure_; - bool follow_redirects_; - uint16_t redirect_limit_; - uint16_t timeout_{5000}; - std::string body_; - std::list
headers_; -#ifdef USE_ESP8266 - std::shared_ptr wifi_client_; -#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS - std::shared_ptr wifi_client_secure_; -#endif - std::shared_ptr get_wifi_client_(); -#endif + bool follow_redirects_{}; + uint16_t redirect_limit_{}; + uint16_t timeout_{4500}; + uint32_t watchdog_timeout_{0}; }; template class HttpRequestSendAction : public Action { @@ -80,8 +147,7 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(const char *, method) TEMPLATABLE_VALUE(std::string, body) - TEMPLATABLE_VALUE(const char *, useragent) - TEMPLATABLE_VALUE(uint16_t, timeout) + TEMPLATABLE_VALUE(bool, capture_response) void add_header(const char *key, TemplatableValue value) { this->headers_.insert({key, value}); } @@ -91,39 +157,76 @@ template class HttpRequestSendAction : public Action { void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } + void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); } + + void set_max_response_buffer_size(size_t max_response_buffer_size) { + this->max_response_buffer_size_ = max_response_buffer_size; + } + void play(Ts... x) override { - this->parent_->set_url(this->url_.value(x...)); - this->parent_->set_method(this->method_.value(x...)); + std::string body; if (this->body_.has_value()) { - this->parent_->set_body(this->body_.value(x...)); + body = this->body_.value(x...); } if (!this->json_.empty()) { auto f = std::bind(&HttpRequestSendAction::encode_json_, this, x..., std::placeholders::_1); - this->parent_->set_body(json::build_json(f)); + body = json::build_json(f); } if (this->json_func_ != nullptr) { auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); - this->parent_->set_body(json::build_json(f)); + body = json::build_json(f); } - if (this->useragent_.has_value()) { - this->parent_->set_useragent(this->useragent_.value(x...)); + std::list
headers; + for (const auto &item : this->headers_) { + auto val = item.second; + Header header; + header.name = item.first; + header.value = val.value(x...); + headers.push_back(header); } - if (this->timeout_.has_value()) { - this->parent_->set_timeout(this->timeout_.value(x...)); + + auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, headers); + + if (container == nullptr) { + for (auto *trigger : this->error_triggers_) + trigger->trigger(x...); + return; } - if (!this->headers_.empty()) { - std::list
headers; - for (const auto &item : this->headers_) { - auto val = item.second; - Header header; - header.name = item.first; - header.value = val.value(x...); - headers.push_back(header); + + size_t content_length = container->content_length; + size_t max_length = std::min(content_length, this->max_response_buffer_size_); + + std::string response_body; + if (this->capture_response_.value(x...)) { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + uint8_t *buf = allocator.allocate(max_length); + if (buf != nullptr) { + 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)); + App.feed_wdt(); + yield(); + read_index += read; + } + response_body.reserve(read_index); + response_body.assign((char *) buf, read_index); + allocator.deallocate(buf, max_length); } - this->parent_->set_headers(headers); } - this->parent_->send(this->response_triggers_); - this->parent_->close(); + + if (this->response_triggers_.size() == 1) { + // if there is only one trigger, no need to copy the response body + this->response_triggers_[0]->process(container, response_body); + } else { + for (auto *trigger : this->response_triggers_) { + // with multiple triggers, pass a copy of the response body to each + // one so that modifications made in one trigger are not visible to + // the others + auto response_body_copy = std::string(response_body); + trigger->process(container, response_body_copy); + } + } + container->end(); } protected: @@ -138,10 +241,11 @@ template class HttpRequestSendAction : public Action { std::map> headers_{}; std::map> json_{}; std::function json_func_{nullptr}; - std::vector response_triggers_; + std::vector response_triggers_{}; + std::vector *> error_triggers_{}; + + size_t max_response_buffer_size_{SIZE_MAX}; }; } // namespace http_request } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp new file mode 100644 index 0000000000..af1eb6f459 --- /dev/null +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -0,0 +1,165 @@ +#include "http_request_arduino.h" + +#ifdef USE_ARDUINO + +#include "esphome/components/network/util.h" +#include "esphome/components/watchdog/watchdog.h" + +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace http_request { + +static const char *const TAG = "http_request.arduino"; + +std::shared_ptr HttpRequestArduino::start(std::string url, std::string method, std::string body, + std::list
headers) { + if (!network::is_connected()) { + this->status_momentary_error("failed", 1000); + ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); + return nullptr; + } + + std::shared_ptr container = std::make_shared(); + container->set_parent(this); + + const uint32_t start = millis(); + + bool secure = url.find("https:") != std::string::npos; + container->set_secure(secure); + + watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); + + if (this->follow_redirects_) { + container->client_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); + container->client_.setRedirectLimit(this->redirect_limit_); + } else { + container->client_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS); + } + +#if defined(USE_ESP8266) + std::unique_ptr stream_ptr; +#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS + if (secure) { + ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); + stream_ptr = std::make_unique(); + WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); + secure_client->setBufferSizes(512, 512); + secure_client->setInsecure(); + } else { + stream_ptr = std::make_unique(); + } +#else + ESP_LOGV(TAG, "ESP8266 HTTP connection with WiFiClient"); + if (secure) { + ESP_LOGE(TAG, "Can't use HTTPS connection with esp8266_disable_ssl_support"); + return nullptr; + } + stream_ptr = std::make_unique(); +#endif // USE_HTTP_REQUEST_ESP8266_HTTPS + +#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0) // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?) + if (!secure) { + ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 " + "in your YAML, or use HTTPS"); + } +#endif // USE_ARDUINO_VERSION_CODE + bool status = container->client_.begin(*stream_ptr, url.c_str()); + +#elif defined(USE_RP2040) + if (secure) { + 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(); + + if (!status) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url.c_str()); + container->end(); + this->status_momentary_error("failed", 1000); + return nullptr; + } + + 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_); + } + for (const auto &header : headers) { + container->client_.addHeader(header.name, header.value, false, true); + } + + // returned needed headers must be collected before the requests + static const char *header_keys[] = {"Content-Length", "Content-Type"}; + static const size_t HEADER_COUNT = sizeof(header_keys) / sizeof(header_keys[0]); + container->client_.collectHeaders(header_keys, HEADER_COUNT); + + container->status_code = container->client_.sendRequest(method.c_str(), body.c_str()); + if (container->status_code < 0) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(), + HTTPClient::errorToString(container->status_code).c_str()); + this->status_momentary_error("failed", 1000); + container->end(); + return nullptr; + } + + if (!is_success(container->status_code)) { + ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); + this->status_momentary_error("failed", 1000); + // Still return the container, so it can be used to get the status code and error message + } + + int content_length = container->client_.getSize(); + ESP_LOGD(TAG, "Content-Length: %d", content_length); + container->content_length = (size_t) content_length; + container->duration_ms = millis() - start; + + return container; +} + +int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { + const uint32_t start = millis(); + watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); + + WiFiClient *stream_ptr = this->client_.getStreamPtr(); + if (stream_ptr == nullptr) { + ESP_LOGE(TAG, "Stream pointer vanished!"); + return -1; + } + + int available_data = stream_ptr->available(); + int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data)); + + if (bufsize == 0) { + this->duration_ms += (millis() - start); + return 0; + } + + App.feed_wdt(); + int read_len = stream_ptr->readBytes(buf, bufsize); + this->bytes_read_ += read_len; + + this->duration_ms += (millis() - start); + + return read_len; +} + +void HttpContainerArduino::end() { + watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); + this->client_.end(); +} + +} // namespace http_request +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h new file mode 100644 index 0000000000..dfdf4a35e2 --- /dev/null +++ b/esphome/components/http_request/http_request_arduino.h @@ -0,0 +1,40 @@ +#pragma once + +#include "http_request.h" + +#ifdef USE_ARDUINO + +#if defined(USE_ESP32) || defined(USE_RP2040) +#include +#endif +#ifdef USE_ESP8266 +#include +#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS +#include +#endif +#endif + +namespace esphome { +namespace http_request { + +class HttpRequestArduino; +class HttpContainerArduino : public HttpContainer { + public: + int read(uint8_t *buf, size_t max_len) override; + void end() override; + + protected: + friend class HttpRequestArduino; + HTTPClient client_{}; +}; + +class HttpRequestArduino : public HttpRequestComponent { + public: + std::shared_ptr start(std::string url, std::string method, std::string body, + std::list
headers) override; +}; + +} // namespace http_request +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp new file mode 100644 index 0000000000..c6c567b620 --- /dev/null +++ b/esphome/components/http_request/http_request_idf.cpp @@ -0,0 +1,201 @@ +#include "http_request_idf.h" + +#ifdef USE_ESP_IDF + +#include "esphome/components/network/util.h" +#include "esphome/components/watchdog/watchdog.h" + +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE +#include "esp_crt_bundle.h" +#endif + +namespace esphome { +namespace http_request { + +static const char *const TAG = "http_request.idf"; + +void HttpRequestIDF::dump_config() { + HttpRequestComponent::dump_config(); + ESP_LOGCONFIG(TAG, " Buffer Size RX: %u", this->buffer_size_rx_); + ESP_LOGCONFIG(TAG, " Buffer Size TX: %u", this->buffer_size_tx_); +} + +std::shared_ptr HttpRequestIDF::start(std::string url, std::string method, std::string body, + std::list
headers) { + if (!network::is_connected()) { + this->status_momentary_error("failed", 1000); + ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); + return nullptr; + } + + esp_http_client_method_t method_idf; + if (method == "GET") { + method_idf = HTTP_METHOD_GET; + } else if (method == "POST") { + method_idf = HTTP_METHOD_POST; + } else if (method == "PUT") { + method_idf = HTTP_METHOD_PUT; + } else if (method == "DELETE") { + method_idf = HTTP_METHOD_DELETE; + } else if (method == "PATCH") { + method_idf = HTTP_METHOD_PATCH; + } else { + this->status_momentary_error("failed", 1000); + ESP_LOGE(TAG, "HTTP Request failed; Unsupported method"); + return nullptr; + } + + bool secure = url.find("https:") != std::string::npos; + + esp_http_client_config_t config = {}; + + config.url = url.c_str(); + config.method = method_idf; + config.timeout_ms = this->timeout_; + config.disable_auto_redirect = !this->follow_redirects_; + config.max_redirection_count = this->redirect_limit_; + config.auth_type = HTTP_AUTH_TYPE_BASIC; +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + if (secure) { + config.crt_bundle_attach = esp_crt_bundle_attach; + } +#endif + + if (this->useragent_ != nullptr) { + config.user_agent = this->useragent_; + } + + config.buffer_size = this->buffer_size_rx_; + config.buffer_size_tx = this->buffer_size_tx_; + + const uint32_t start = millis(); + watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); + + esp_http_client_handle_t client = esp_http_client_init(&config); + + std::shared_ptr container = std::make_shared(client); + container->set_parent(this); + + container->set_secure(secure); + + for (const auto &header : headers) { + esp_http_client_set_header(client, header.name, header.value); + } + + const int body_len = body.length(); + + esp_err_t err = esp_http_client_open(client, body_len); + if (err != ESP_OK) { + this->status_momentary_error("failed", 1000); + ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err)); + esp_http_client_cleanup(client); + return nullptr; + } + + if (body_len > 0) { + int write_left = body_len; + int write_index = 0; + const char *buf = body.c_str(); + while (write_left > 0) { + int written = esp_http_client_write(client, buf + write_index, write_left); + if (written < 0) { + err = ESP_FAIL; + break; + } + write_left -= written; + write_index += written; + } + } + + if (err != ESP_OK) { + this->status_momentary_error("failed", 1000); + ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err)); + esp_http_client_cleanup(client); + return nullptr; + } + + container->content_length = esp_http_client_fetch_headers(client); + container->status_code = esp_http_client_get_status_code(client); + if (is_success(container->status_code)) { + container->duration_ms = millis() - start; + return container; + } + + if (this->follow_redirects_) { + auto num_redirects = this->redirect_limit_; + while (is_redirect(container->status_code) && num_redirects > 0) { + err = esp_http_client_set_redirection(client); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err)); + this->status_momentary_error("failed", 1000); + esp_http_client_cleanup(client); + return nullptr; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char redirect_url[256]{}; + if (esp_http_client_get_url(client, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) { + ESP_LOGV(TAG, "redirecting to url: %s", redirect_url); + } +#endif + err = esp_http_client_open(client, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err)); + this->status_momentary_error("failed", 1000); + esp_http_client_cleanup(client); + return nullptr; + } + + container->content_length = esp_http_client_fetch_headers(client); + container->status_code = esp_http_client_get_status_code(client); + if (is_success(container->status_code)) { + container->duration_ms = millis() - start; + return container; + } + + num_redirects--; + } + + if (num_redirects == 0) { + ESP_LOGW(TAG, "Reach redirect limit count=%d", this->redirect_limit_); + } + } + + ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); + this->status_momentary_error("failed", 1000); + return container; +} + +int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { + const uint32_t start = millis(); + watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); + + int bufsize = std::min(max_len, this->content_length - this->bytes_read_); + + if (bufsize == 0) { + this->duration_ms += (millis() - start); + return 0; + } + + App.feed_wdt(); + int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize); + this->bytes_read_ += read_len; + + this->duration_ms += (millis() - start); + + return read_len; +} + +void HttpContainerIDF::end() { + watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); + + esp_http_client_close(this->client_); + esp_http_client_cleanup(this->client_); +} + +} // namespace http_request +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h new file mode 100644 index 0000000000..431794924b --- /dev/null +++ b/esphome/components/http_request/http_request_idf.h @@ -0,0 +1,44 @@ +#pragma once + +#include "http_request.h" + +#ifdef USE_ESP_IDF + +#include +#include +#include +#include + +namespace esphome { +namespace http_request { + +class HttpContainerIDF : public HttpContainer { + public: + HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {} + int read(uint8_t *buf, size_t max_len) override; + void end() override; + + protected: + esp_http_client_handle_t client_; +}; + +class HttpRequestIDF : public HttpRequestComponent { + public: + void dump_config() override; + + std::shared_ptr start(std::string url, std::string method, std::string body, + std::list
headers) override; + + void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; } + void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; } + + protected: + // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE + uint16_t buffer_size_rx_{}; + uint16_t buffer_size_tx_{}; +}; + +} // namespace http_request +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py new file mode 100644 index 0000000000..0ef1fc2348 --- /dev/null +++ b/esphome/components/http_request/ota/__init__.py @@ -0,0 +1,100 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import ( + CONF_ID, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, +) +from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent +from esphome.core import coroutine_with_priority +from .. import CONF_HTTP_REQUEST_ID, http_request_ns, HttpRequestComponent + +CODEOWNERS = ["@oarcher"] + +AUTO_LOAD = ["md5"] +DEPENDENCIES = ["network", "http_request"] + +CONF_MD5 = "md5" +CONF_MD5_URL = "md5_url" + +OtaHttpRequestComponent = http_request_ns.class_( + "OtaHttpRequestComponent", OTAComponent +) +OtaHttpRequestComponentFlashAction = http_request_ns.class_( + "OtaHttpRequestComponentFlashAction", automation.Action +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(OtaHttpRequestComponent), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 5, 1), + esp32_arduino=cv.Version(0, 0, 0), + esp_idf=cv.Version(0, 0, 0), + rp2040_arduino=cv.Version(0, 0, 0), + ), +) + + +@coroutine_with_priority(52.0) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ota_to_code(var, config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) + + +OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(OtaHttpRequestComponent), + cv.Optional(CONF_MD5_URL): cv.templatable(cv.url), + cv.Optional(CONF_MD5): cv.templatable( + cv.All(cv.string, cv.Length(min=32, max=32)) + ), + cv.Optional(CONF_PASSWORD): cv.templatable(cv.string), + cv.Optional(CONF_USERNAME): cv.templatable(cv.string), + cv.Required(CONF_URL): cv.templatable(cv.url), + } + ), + cv.has_exactly_one_key(CONF_MD5, CONF_MD5_URL), +) + + +@automation.register_action( + "ota.http_request.flash", + OtaHttpRequestComponentFlashAction, + OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA, +) +async def ota_http_request_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if md5_url := config.get(CONF_MD5_URL): + template_ = await cg.templatable(md5_url, args, cg.std_string) + cg.add(var.set_md5_url(template_)) + + if md5_str := config.get(CONF_MD5): + template_ = await cg.templatable(md5_str, args, cg.std_string) + cg.add(var.set_md5(template_)) + + if password_str := config.get(CONF_PASSWORD): + template_ = await cg.templatable(password_str, args, cg.std_string) + cg.add(var.set_password(template_)) + + if username_str := config.get(CONF_USERNAME): + template_ = await cg.templatable(username_str, args, cg.std_string) + cg.add(var.set_username(template_)) + + template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) + cg.add(var.set_url(template_)) + + return var diff --git a/esphome/components/http_request/ota/automation.h b/esphome/components/http_request/ota/automation.h new file mode 100644 index 0000000000..d4c21f1c72 --- /dev/null +++ b/esphome/components/http_request/ota/automation.h @@ -0,0 +1,42 @@ +#pragma once +#include "ota_http_request.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace http_request { + +template class OtaHttpRequestComponentFlashAction : public Action { + public: + OtaHttpRequestComponentFlashAction(OtaHttpRequestComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, md5_url) + TEMPLATABLE_VALUE(std::string, md5) + TEMPLATABLE_VALUE(std::string, password) + TEMPLATABLE_VALUE(std::string, url) + TEMPLATABLE_VALUE(std::string, username) + + void play(Ts... x) override { + if (this->md5_url_.has_value()) { + this->parent_->set_md5_url(this->md5_url_.value(x...)); + } + if (this->md5_.has_value()) { + this->parent_->set_md5(this->md5_.value(x...)); + } + if (this->password_.has_value()) { + this->parent_->set_password(this->password_.value(x...)); + } + if (this->username_.has_value()) { + this->parent_->set_username(this->username_.value(x...)); + } + this->parent_->set_url(this->url_.value(x...)); + + this->parent_->flash(); + // Normally never reached due to reboot + } + + protected: + OtaHttpRequestComponent *parent_; +}; + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp new file mode 100644 index 0000000000..cec30d72ec --- /dev/null +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -0,0 +1,269 @@ +#include "ota_http_request.h" + +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" + +#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_arduino_rp2040.h" +#include "esphome/components/ota/ota_backend_esp_idf.h" + +namespace esphome { +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) { + if (!this->validate_url_(url)) { + this->md5_url_.clear(); // URL was not valid; prevent flashing until it is + return; + } + this->md5_url_ = url; + this->md5_expected_.clear(); // to be retrieved later +} + +void OtaHttpRequestComponent::set_url(const std::string &url) { + if (!this->validate_url_(url)) { + this->url_.clear(); // URL was not valid; prevent flashing until it is + return; + } + this->url_ = url; +} + +void OtaHttpRequestComponent::flash() { + if (this->url_.empty()) { + ESP_LOGE(TAG, "URL not set; cannot start update"); + return; + } + + ESP_LOGI(TAG, "Starting update..."); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(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); +#endif + delay(10); + App.safe_reboot(); + break; + + default: +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(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 + break; + } +} + +void OtaHttpRequestComponent::cleanup_(std::unique_ptr backend, + const std::shared_ptr &container) { + if (this->update_started_) { + ESP_LOGV(TAG, "Aborting OTA backend"); + backend->abort(); + } + ESP_LOGV(TAG, "Aborting HTTP connection"); + container->end(); +}; + +uint8_t OtaHttpRequestComponent::do_ota_() { + uint8_t buf[OtaHttpRequestComponent::HTTP_RECV_BUFFER + 1]; + uint32_t last_progress = 0; + uint32_t update_start_time = millis(); + md5::MD5Digest md5_receive; + std::unique_ptr md5_receive_str(new char[33]); + + if (this->md5_expected_.empty() && !this->http_get_md5_()) { + return OTA_MD5_INVALID; + } + + ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str()); + + auto url_with_auth = this->get_url_with_auth_(this->url_); + if (url_with_auth.empty()) { + return OTA_BAD_URL; + } + ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str()); + ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str()); + + auto container = this->parent_->get(url_with_auth); + + if (container == nullptr || container->status_code != HTTP_STATUS_OK) { + return OTA_CONNECTION_ERROR; + } + + // 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"); + auto backend = ota::make_ota_backend(); + auto error_code = backend->begin(container->content_length); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "backend->begin error: %d", error_code); + this->cleanup_(std::move(backend), container); + return error_code; + } + + while (container->get_bytes_read() < container->content_length) { + // read a maximum of chunk_size bytes into buf. (real read size returned) + int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER); + ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(), + container->content_length, bufsize); + + // feed watchdog and give other tasks a chance to run + 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) { + // add read bytes to MD5 + md5_receive.add(buf, bufsize); + + // write bytes to OTA backend + this->update_started_ = true; + error_code = backend->write(buf, bufsize); + if (error_code != ota::OTA_RESPONSE_OK) { + // error code explanation available at + // https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h + ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code, + container->get_bytes_read() - bufsize, container->content_length); + this->cleanup_(std::move(backend), container); + return error_code; + } + } + + uint32_t now = millis(); + if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) { + 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); +#endif + } + } // while + + ESP_LOGI(TAG, "Done in %.0f seconds", float(millis() - update_start_time) / 1000); + + // verify MD5 is as expected and act accordingly + md5_receive.calculate(); + md5_receive.get_hex(md5_receive_str.get()); + this->md5_computed_ = md5_receive_str.get(); + if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) { + ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str()); + this->cleanup_(std::move(backend), container); + return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH; + } else { + backend->set_update_md5(md5_receive_str.get()); + } + + container->end(); + + // feed watchdog and give other tasks a chance to run + App.feed_wdt(); + yield(); + delay(100); // NOLINT + + error_code = backend->end(); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code); + this->cleanup_(std::move(backend), container); + return error_code; + } + + ESP_LOGI(TAG, "Update complete"); + return ota::OTA_RESPONSE_OK; +} + +std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) { + if (this->username_.empty() || this->password_.empty()) { + return url; + } + + auto start_char = url.find("://"); + if ((start_char == std::string::npos) || (start_char < 4)) { + ESP_LOGE(TAG, "Incorrect URL prefix"); + return {}; + } + + ESP_LOGD(TAG, "Using basic HTTP authentication"); + + start_char += 3; // skip '://' characters + auto url_with_auth = + url.substr(0, start_char) + this->username_ + ":" + this->password_ + "@" + url.substr(start_char); + return url_with_auth; +} + +bool OtaHttpRequestComponent::http_get_md5_() { + if (this->md5_url_.empty()) { + return false; + } + + auto url_with_auth = this->get_url_with_auth_(this->md5_url_); + if (url_with_auth.empty()) { + return false; + } + + ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str()); + ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str()); + auto container = this->parent_->get(url_with_auth); + if (container == nullptr) { + ESP_LOGE(TAG, "Failed to connect to MD5 URL"); + return false; + } + size_t length = container->content_length; + if (length == 0) { + container->end(); + return false; + } + if (length < MD5_SIZE) { + ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length); + container->end(); + return false; + } + + this->md5_expected_.resize(MD5_SIZE); + int read_len = 0; + while (container->get_bytes_read() < MD5_SIZE) { + read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE); + App.feed_wdt(); + yield(); + } + container->end(); + + ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE); + return read_len == MD5_SIZE; +} + +bool OtaHttpRequestComponent::validate_url_(const std::string &url) { + if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); + return false; + } + return true; +} + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h new file mode 100644 index 0000000000..6a86b4ab43 --- /dev/null +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/components/ota/ota_backend.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#include +#include +#include + +#include "../http_request.h" + +namespace esphome { +namespace http_request { + +static const uint8_t MD5_SIZE = 32; + +enum OtaHttpRequestError : uint8_t { + OTA_MD5_INVALID = 0x10, + OTA_BAD_URL = 0x11, + OTA_CONNECTION_ERROR = 0x12, +}; + +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; } + + void set_md5_url(const std::string &md5_url); + void set_md5(const std::string &md5) { this->md5_expected_ = md5; } + void set_password(const std::string &password) { this->password_ = password; } + void set_url(const std::string &url); + void set_username(const std::string &username) { this->username_ = username; } + + std::string md5_computed() { return this->md5_computed_; } + std::string md5_expected() { return this->md5_expected_; } + + void flash(); + + protected: + void cleanup_(std::unique_ptr backend, const std::shared_ptr &container); + uint8_t do_ota_(); + std::string get_url_with_auth_(const std::string &url); + bool http_get_md5_(); + bool validate_url_(const std::string &url); + + std::string md5_computed_{}; + std::string md5_expected_{}; + std::string md5_url_{}; + std::string password_{}; + std::string username_{}; + std::string url_{}; + int status_ = -1; + bool update_started_ = false; + static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size +}; + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/update/__init__.py b/esphome/components/http_request/update/__init__.py new file mode 100644 index 0000000000..356afa1432 --- /dev/null +++ b/esphome/components/http_request/update/__init__.py @@ -0,0 +1,44 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.components import update +from esphome.const import ( + CONF_SOURCE, +) + +from .. import http_request_ns, CONF_HTTP_REQUEST_ID, HttpRequestComponent +from ..ota import OtaHttpRequestComponent + + +AUTO_LOAD = ["json"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["ota.http_request"] + +HttpRequestUpdate = http_request_ns.class_( + "HttpRequestUpdate", update.UpdateEntity, cg.PollingComponent +) + +CONF_OTA_ID = "ota_id" + +CONFIG_SCHEMA = update.UPDATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HttpRequestUpdate), + cv.GenerateID(CONF_OTA_ID): cv.use_id(OtaHttpRequestComponent), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + cv.Required(CONF_SOURCE): cv.url, + } +).extend(cv.polling_component_schema("6h")) + + +async def to_code(config): + var = await update.new_update(config) + ota_parent = await cg.get_variable(config[CONF_OTA_ID]) + cg.add(var.set_ota_parent(ota_parent)) + request_parent = await cg.get_variable(config[CONF_HTTP_REQUEST_ID]) + cg.add(var.set_request_parent(request_parent)) + + cg.add(var.set_source_url(config[CONF_SOURCE])) + + cg.add_define("USE_OTA_STATE_CALLBACK") + + 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 new file mode 100644 index 0000000000..0e0966c22b --- /dev/null +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -0,0 +1,156 @@ +#include "http_request_update.h" + +#include "esphome/core/application.h" +#include "esphome/core/version.h" + +#include "esphome/components/json/json_util.h" +#include "esphome/components/network/util.h" + +namespace esphome { +namespace http_request { + +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::update() { + auto container = this->request_parent_->get(this->source_url_); + + if (container == nullptr || container->status_code != HTTP_STATUS_OK) { + std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str()); + this->status_set_error(msg.c_str()); + return; + } + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + uint8_t *data = allocator.allocate(container->content_length); + if (data == nullptr) { + std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); + this->status_set_error(msg.c_str()); + container->end(); + return; + } + + size_t read_index = 0; + while (container->get_bytes_read() < container->content_length) { + int read_bytes = container->read(data + read_index, MAX_READ_SIZE); + + App.feed_wdt(); + yield(); + + read_index += read_bytes; + } + + std::string response((char *) data, read_index); + allocator.deallocate(data, container->content_length); + + container->end(); + + bool valid = json::parse_json(response, [this](JsonObject root) -> bool { + if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + this->update_info_.title = root["name"].as(); + this->update_info_.latest_version = root["version"].as(); + + for (auto build : root["builds"].as()) { + if (!build.containsKey("chipFamily")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + if (build["chipFamily"] == ESPHOME_VARIANT) { + if (!build.containsKey("ota")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + auto ota = build["ota"]; + if (!ota.containsKey("path") || !ota.containsKey("md5")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + this->update_info_.firmware_url = ota["path"].as(); + this->update_info_.md5 = ota["md5"].as(); + + if (ota.containsKey("summary")) + this->update_info_.summary = ota["summary"].as(); + if (ota.containsKey("release_url")) + this->update_info_.release_url = ota["release_url"].as(); + + return true; + } + } + return false; + }); + + if (!valid) { + std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str()); + this->status_set_error(msg.c_str()); + return; + } + + // Merge source_url_ and this->update_info_.firmware_url + if (this->update_info_.firmware_url.find("http") == std::string::npos) { + std::string path = this->update_info_.firmware_url; + if (path[0] == '/') { + std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8)); + this->update_info_.firmware_url = domain + path; + } else { + std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1); + this->update_info_.firmware_url = domain + path; + } + } + + std::string current_version; +#ifdef ESPHOME_PROJECT_VERSION + current_version = ESPHOME_PROJECT_VERSION; +#else + current_version = ESPHOME_VERSION; +#endif + + this->update_info_.current_version = current_version; + + if (this->update_info_.latest_version.empty() || this->update_info_.latest_version == update_info_.current_version) { + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } else { + this->state_ = update::UPDATE_STATE_AVAILABLE; + } + + this->update_info_.has_progress = false; + this->update_info_.progress = 0.0f; + + this->status_clear_error(); + this->publish_state(); +} + +void HttpRequestUpdate::perform(bool force) { + if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) { + return; + } + + this->state_ = update::UPDATE_STATE_INSTALLING; + this->publish_state(); + + this->ota_parent_->set_md5(this->update_info.md5); + this->ota_parent_->set_url(this->update_info.firmware_url); + // Flash in the next loop + this->defer([this]() { this->ota_parent_->flash(); }); +} + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h new file mode 100644 index 0000000000..45c7e6a447 --- /dev/null +++ b/esphome/components/http_request/update/http_request_update.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/http_request/http_request.h" +#include "esphome/components/http_request/ota/ota_http_request.h" +#include "esphome/components/update/update_entity.h" + +namespace esphome { +namespace http_request { + +class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { + public: + void setup() override; + void update() override; + + void perform(bool force) override; + void check() override { this->update(); } + + void set_source_url(const std::string &source_url) { this->source_url_ = source_url; } + + void set_request_parent(HttpRequestComponent *request_parent) { this->request_parent_ = request_parent; } + void set_ota_parent(OtaHttpRequestComponent *ota_parent) { this->ota_parent_ = ota_parent; } + + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + HttpRequestComponent *request_parent_; + OtaHttpRequestComponent *ota_parent_; + std::string source_url_; +}; + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index a38ec73019..411d1e1d6a 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -11,7 +11,11 @@ static const uint8_t HTU21D_ADDRESS = 0x40; static const uint8_t HTU21D_REGISTER_RESET = 0xFE; static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3; static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5; +static const uint8_t HTU21D_WRITERHT_REG_CMD = 0xE6; /**< Write RH/T User Register 1 */ static const uint8_t HTU21D_REGISTER_STATUS = 0xE7; +static const uint8_t HTU21D_WRITEHEATER_REG_CMD = 0x51; /**< Write Heater Control Register */ +static const uint8_t HTU21D_READHEATER_REG_CMD = 0x11; /**< Read Heater Control Register */ +static const uint8_t HTU21D_REG_HTRE_BIT = 0x02; /**< Control Register Heater Bit */ void HTU21DComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HTU21D..."); @@ -35,41 +39,117 @@ void HTU21DComponent::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_); } void HTU21DComponent::update() { - uint16_t raw_temperature; if (this->write(&HTU21D_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; } - delay(50); // NOLINT - if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_temperature = i2c::i2ctohs(raw_temperature); - float temperature = (float(raw_temperature & 0xFFFC)) * 175.72f / 65536.0f - 46.85f; + // According to the datasheet sht21 temperature readings can take up to 85ms + this->set_timeout(85, [this]() { + uint16_t raw_temperature; + if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_temperature = i2c::i2ctohs(raw_temperature); - uint16_t raw_humidity; - if (this->write(&HTU21D_REGISTER_HUMIDITY, 1) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - delay(50); // NOLINT - if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_humidity = i2c::i2ctohs(raw_humidity); + float temperature = (float(raw_temperature & 0xFFFC)) * 175.72f / 65536.0f - 46.85f; - float humidity = (float(raw_humidity & 0xFFFC)) * 125.0f / 65536.0f - 6.0f; - ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); + ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature); - if (this->temperature_ != nullptr) - this->temperature_->publish_state(temperature); - if (this->humidity_ != nullptr) - this->humidity_->publish_state(humidity); - this->status_clear_warning(); + if (this->temperature_ != nullptr) + this->temperature_->publish_state(temperature); + this->status_clear_warning(); + + if (this->write(&HTU21D_REGISTER_HUMIDITY, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_humidity; + if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_humidity = i2c::i2ctohs(raw_humidity); + + float humidity = (float(raw_humidity & 0xFFFC)) * 125.0f / 65536.0f - 6.0f; + + ESP_LOGD(TAG, "Got Humidity=%.1f%%", humidity); + + if (this->humidity_ != nullptr) + this->humidity_->publish_state(humidity); + + int8_t heater_level; + + // HTU21D does have a heater module but does not have heater level + // Setting heater level to 1 in case the heater is ON + if (this->sensor_model_ == HTU21D_SENSOR_MODEL_HTU21D) { + if (this->is_heater_enabled()) { + heater_level = 1; + } else { + heater_level = 0; + } + } else { + heater_level = this->get_heater_level(); + } + + ESP_LOGD(TAG, "Heater Level=%d", heater_level); + + if (this->heater_ != nullptr) + this->heater_->publish_state(heater_level); + this->status_clear_warning(); + }); + }); } + +bool HTU21DComponent::is_heater_enabled() { + uint8_t raw_heater; + if (this->read_register(HTU21D_REGISTER_STATUS, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return false; + } + raw_heater = i2c::i2ctohs(raw_heater); + return (bool) (((raw_heater) >> (HTU21D_REG_HTRE_BIT)) & 0x01); +} + +void HTU21DComponent::set_heater(bool status) { + uint8_t raw_heater; + if (this->read_register(HTU21D_REGISTER_STATUS, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_heater = i2c::i2ctohs(raw_heater); + if (status) { + raw_heater |= (1 << (HTU21D_REG_HTRE_BIT)); + } else { + raw_heater &= ~(1 << (HTU21D_REG_HTRE_BIT)); + } + + if (this->write_register(HTU21D_WRITERHT_REG_CMD, &raw_heater, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } +} + +void HTU21DComponent::set_heater_level(uint8_t level) { + if (this->write_register(HTU21D_WRITEHEATER_REG_CMD, &level, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } +} + +int8_t HTU21DComponent::get_heater_level() { + int8_t raw_heater; + if (this->read_register(HTU21D_READHEATER_REG_CMD, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return 0; + } + raw_heater = i2c::i2ctohs(raw_heater); + return raw_heater; +} + float HTU21DComponent::get_setup_priority() const { return setup_priority::DATA; } } // namespace htu21d diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index a408f06d01..8533875d43 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -3,26 +3,60 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/automation.h" namespace esphome { namespace htu21d { +enum HTU21DSensorModels { HTU21D_SENSOR_MODEL_HTU21D = 0, HTU21D_SENSOR_MODEL_SI7021, HTU21D_SENSOR_MODEL_SHT21 }; + class HTU21DComponent : public PollingComponent, public i2c::I2CDevice { public: void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_heater(sensor::Sensor *heater) { heater_ = heater; } /// Setup (reset) the sensor and check connection. void setup() override; void dump_config() override; + void set_sensor_model(HTU21DSensorModels sensor_model) { sensor_model_ = sensor_model; } /// Update the sensor values (temperature+humidity). void update() override; + bool is_heater_enabled(); + void set_heater(bool status); + void set_heater_level(uint8_t level); + int8_t get_heater_level(); + float get_setup_priority() const override; protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *heater_{nullptr}; + HTU21DSensorModels sensor_model_{HTU21D_SENSOR_MODEL_HTU21D}; +}; + +template class SetHeaterLevelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, level) + + void play(Ts... x) override { + auto level = this->level_.value(x...); + + this->parent_->set_heater_level(level); + } +}; + +template class SetHeaterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, status) + + void play(Ts... x) override { + auto status = this->status_.value(x...); + + this->parent_->set_heater(status); + } }; } // namespace htu21d diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 37422f0329..bf0b9a23fb 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -1,15 +1,21 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor +from esphome import automation from esphome.const import ( CONF_HUMIDITY, CONF_ID, + CONF_MODEL, CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, + CONF_HEATER, + UNIT_EMPTY, + CONF_LEVEL, + CONF_STATUS, ) DEPENDENCIES = ["i2c"] @@ -18,23 +24,38 @@ htu21d_ns = cg.esphome_ns.namespace("htu21d") HTU21DComponent = htu21d_ns.class_( "HTU21DComponent", cg.PollingComponent, i2c.I2CDevice ) +SetHeaterLevelAction = htu21d_ns.class_("SetHeaterLevelAction", automation.Action) +SetHeaterAction = htu21d_ns.class_("SetHeaterAction", automation.Action) +HTU21DSensorModels = htu21d_ns.enum("HTU21DSensorModels") + +MODELS = { + "HTU21D": HTU21DSensorModels.HTU21D_SENSOR_MODEL_HTU21D, + "SI7021": HTU21DSensorModels.HTU21D_SENSOR_MODEL_SI7021, + "SHT21": HTU21DSensorModels.HTU21D_SENSOR_MODEL_SHT21, +} CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(HTU21DComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + 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.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_HEATER): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MODEL, default="HTU21D"): cv.enum(MODELS, upper=True), } ) .extend(cv.polling_component_schema("60s")) @@ -54,3 +75,47 @@ async def to_code(config): if CONF_HUMIDITY in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) + + if CONF_HEATER in config: + sens = await sensor.new_sensor(config[CONF_HEATER]) + cg.add(var.set_heater(sens)) + + cg.add(var.set_sensor_model(config[CONF_MODEL])) + + +@automation.register_action( + "htu21d.set_heater_level", + SetHeaterLevelAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HTU21DComponent), + cv.Required(CONF_LEVEL): cv.templatable(cv.int_), + }, + key=CONF_LEVEL, + ), +) +async def set_heater_level_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + level_ = await cg.templatable(config[CONF_LEVEL], args, int) + cg.add(var.set_level(level_)) + return var + + +@automation.register_action( + "htu21d.set_heater", + SetHeaterAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HTU21DComponent), + cv.Required(CONF_STATUS): cv.templatable(cv.boolean), + }, + key=CONF_STATUS, + ), +) +async def set_heater_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + status_ = await cg.templatable(config[CONF_LEVEL], args, bool) + cg.add(var.set_status(status_)) + return var diff --git a/esphome/components/htu31d/__init__.py b/esphome/components/htu31d/__init__.py new file mode 100644 index 0000000000..039693cb30 --- /dev/null +++ b/esphome/components/htu31d/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@betterengineering"] diff --git a/esphome/components/htu31d/htu31d.cpp b/esphome/components/htu31d/htu31d.cpp new file mode 100644 index 0000000000..bf4689d837 --- /dev/null +++ b/esphome/components/htu31d/htu31d.cpp @@ -0,0 +1,271 @@ +/* + * This file contains source code derived from Adafruit_HTU31D which is under + * the BSD license: + * Written by Limor Fried/Ladyada for Adafruit Industries. + * BSD license, all text above must be included in any redistribution. + * + * Modifications made by Mark Spicer. + */ + +#include "htu31d.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace htu31d { + +/** Logging prefix */ +static const char *const TAG = "htu31d"; + +/** Default I2C address for the HTU31D. */ +static const uint8_t HTU31D_DEFAULT_I2CADDR = 0x40; + +/** Read temperature and humidity. */ +static const uint8_t HTU31D_READTEMPHUM = 0x00; + +/** Start a conversion! */ +static const uint8_t HTU31D_CONVERSION = 0x40; + +/** Read serial number command. */ +static const uint8_t HTU31D_READSERIAL = 0x0A; + +/** Enable heater */ +static const uint8_t HTU31D_HEATERON = 0x04; + +/** Disable heater */ +static const uint8_t HTU31D_HEATEROFF = 0x02; + +/** Reset command. */ +static const uint8_t HTU31D_RESET = 0x1E; + +/** Diagnostics command. */ +static const uint8_t HTU31D_DIAGNOSTICS = 0x08; + +/** + * Computes a CRC result for the provided input. + * + * @returns the computed CRC result for the provided input + */ +uint8_t compute_crc(uint32_t value) { + uint32_t polynom = 0x98800000; // x^8 + x^5 + x^4 + 1 + uint32_t msb = 0x80000000; + uint32_t mask = 0xFF800000; + uint32_t threshold = 0x00000080; + uint32_t result = value; + + while (msb != threshold) { + // Check if msb of current value is 1 and apply XOR mask + if (result & msb) + result = ((result ^ polynom) & mask) | (result & ~mask); + + // Shift by one + msb >>= 1; + mask >>= 1; + polynom >>= 1; + } + + return result; +} + +/** + * Resets the sensor and ensures that the devices serial number can be read over + * I2C. + */ +void HTU31DComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up esphome/components/htu31d HTU31D..."); + + if (!this->reset_()) { + this->mark_failed(); + return; + } + + if (this->read_serial_num_() == 0) { + this->mark_failed(); + return; + } +} + +/** + * Called once every update interval (user configured, defaults to 60s) and sets + * the current temperature and humidity. + */ +void HTU31DComponent::update() { + ESP_LOGD(TAG, "Checking temperature and humidty values"); + + // Trigger a conversion. From the spec sheet: The conversion command triggers + // a single temperature and humidity conversion. + if (this->write_register(HTU31D_CONVERSION, nullptr, 0) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Received errror writing conversion register"); + return; + } + + // Wait conversion time. + this->set_timeout(20, [this]() { + uint8_t thdata[6]; + if (this->read_register(HTU31D_READTEMPHUM, thdata, 6) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Error reading temperature/humidty register"); + return; + } + + // Calculate temperature value. + uint16_t raw_temp = encode_uint16(thdata[0], thdata[1]); + + uint8_t crc = compute_crc((uint32_t) raw_temp << 8); + if (crc != thdata[2]) { + this->status_set_warning(); + ESP_LOGE(TAG, "Error validating temperature CRC"); + return; + } + + float temperature = raw_temp; + temperature /= 65535.0f; + temperature *= 165; + temperature -= 40; + + if (this->temperature_ != nullptr) { + this->temperature_->publish_state(temperature); + } + + // Calculate humidty value. + uint16_t raw_hum = encode_uint16(thdata[3], thdata[4]); + + crc = compute_crc((uint32_t) raw_hum << 8); + if (crc != thdata[5]) { + this->status_set_warning(); + ESP_LOGE(TAG, "Error validating humidty CRC"); + return; + } + + float humidity = raw_hum; + humidity /= 65535.0f; + humidity *= 100; + + if (this->humidity_ != nullptr) { + this->humidity_->publish_state(humidity); + } + + ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); + this->status_clear_warning(); + }); +} + +/** + * Logs the current compoenent config. + */ +void HTU31DComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HTU31D:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with HTU31D failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); +} + +/** + * Sends a 'reset' request to the HTU31D, followed by a 15ms delay. + * + * @returns True if was able to write the command successfully + */ +bool HTU31DComponent::reset_() { + if (this->write_register(HTU31D_RESET, nullptr, 0) != i2c::ERROR_OK) { + return false; + } + + delay(15); + return true; +} + +/** + * Reads the serial number from the device and checks the CRC. + * + * @returns the 24bit serial number from the device + */ +uint32_t HTU31DComponent::read_serial_num_() { + uint8_t reply[4]; + uint32_t serial = 0; + uint8_t padding = 0; + + // Verify we can read the device serial. + if (this->read_register(HTU31D_READSERIAL, reply, 4) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Error reading device serial"); + return 0; + } + + serial = encode_uint32(reply[0], reply[1], reply[2], padding); + + uint8_t crc = compute_crc(serial); + if (crc != reply[3]) { + ESP_LOGE(TAG, "Error validating serial CRC"); + return 0; + } + + ESP_LOGD(TAG, "Found serial: 0x%" PRIX32, serial); + + return serial; +} + +/** + * Checks the diagnostics register to determine if the heater is currently + * enabled. + * + * @returns True if the heater is currently enabled, False otherwise + */ +bool HTU31DComponent::is_heater_enabled() { + uint8_t reply[1]; + uint8_t heater_enabled_position = 0; + uint8_t mask = 1 << heater_enabled_position; + uint8_t diagnostics = 0; + + if (this->read_register(HTU31D_DIAGNOSTICS, reply, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Error reading device serial"); + return false; + } + + diagnostics = reply[0]; + return (diagnostics & mask) != 0; +} + +/** + * Sets the heater state on or off. + * + * @param desired True for on, and False for off. + */ +void HTU31DComponent::set_heater_state(bool desired) { + bool current = this->is_heater_enabled(); + + // If the current state matches the desired state, there is nothing to do. + if (current == desired) { + return; + } + + // Update heater state. + esphome::i2c::ErrorCode err; + if (desired) { + err = this->write_register(HTU31D_HEATERON, nullptr, 0); + } else { + err = this->write_register(HTU31D_HEATEROFF, nullptr, 0); + } + + // Record any error. + if (err != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Received error updating heater state"); + return; + } +} + +/** + * Sets the startup priority for this component. + * + * @returns The startup priority + */ +float HTU31DComponent::get_setup_priority() const { return setup_priority::DATA; } +} // namespace htu31d +} // namespace esphome diff --git a/esphome/components/htu31d/htu31d.h b/esphome/components/htu31d/htu31d.h new file mode 100644 index 0000000000..9462133ced --- /dev/null +++ b/esphome/components/htu31d/htu31d.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace htu31d { + +class HTU31DComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; /// Setup (reset) the sensor and check connection. + void update() override; /// Update the sensor values (temperature+humidity). + void dump_config() override; /// Dumps the configuration values. + + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + + void set_heater_state(bool desired); + bool is_heater_enabled(); + + float get_setup_priority() const override; + + protected: + bool reset_(); + uint32_t read_serial_num_(); + + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; +}; +} // namespace htu31d +} // namespace esphome diff --git a/esphome/components/htu31d/sensor.py b/esphome/components/htu31d/sensor.py new file mode 100644 index 0000000000..fe53aa376e --- /dev/null +++ b/esphome/components/htu31d/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +htu31d_ns = cg.esphome_ns.namespace("htu31d") +HTU31DComponent = htu31d_ns.class_( + "HTU31DComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HTU31DComponent), + 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_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +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) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 62adc4ae86..1a7169eed7 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -28,7 +28,7 @@ void HX711Sensor::update() { uint32_t result; if (this->read_sensor_(&result)) { int32_t value = static_cast(result); - ESP_LOGD(TAG, "'%s': Got value %d", this->name_.c_str(), value); + ESP_LOGD(TAG, "'%s': Got value %" PRId32, this->name_.c_str(), value); this->publish_state(value); } } @@ -39,8 +39,8 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { return false; } - this->status_clear_warning(); uint32_t data = 0; + bool final_dout; { InterruptLock lock; @@ -59,8 +59,17 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { this->sck_pin_->digital_write(false); delayMicroseconds(1); } + final_dout = this->dout_pin_->digital_read(); } + if (!final_dout) { + ESP_LOGW(TAG, "HX711 DOUT pin not high after reading (data 0x%" PRIx32 ")!", data); + this->status_set_warning(); + return false; + } + + this->status_clear_warning(); + if (data & 0x800000ULL) { data |= 0xFF000000ULL; } diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h index 9fef649b03..0cb6868ab5 100644 --- a/esphome/components/hx711/hx711.h +++ b/esphome/components/hx711/hx711.h @@ -4,6 +4,8 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" +#include + namespace esphome { namespace hx711 { diff --git a/esphome/components/hydreon_rgxx/__init__.py b/esphome/components/hydreon_rgxx/__init__.py index 5fe050edf2..b488bfc1b4 100644 --- a/esphome/components/hydreon_rgxx/__init__.py +++ b/esphome/components/hydreon_rgxx/__init__.py @@ -6,6 +6,7 @@ DEPENDENCIES = ["uart"] hydreon_rgxx_ns = cg.esphome_ns.namespace("hydreon_rgxx") RGModel = hydreon_rgxx_ns.enum("RGModel") +RG15Resolution = hydreon_rgxx_ns.enum("RG15Resolution") HydreonRGxxComponent = hydreon_rgxx_ns.class_( "HydreonRGxxComponent", cg.PollingComponent, uart.UARTDevice ) diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index da4345e136..92d7774193 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -17,6 +17,17 @@ void HydreonRGxxComponent::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!"); } + if (model_ == RG9) { + ESP_LOGCONFIG(TAG, " Model: RG9"); + ESP_LOGCONFIG(TAG, " Disable Led: %s", TRUEFALSE(this->disable_led_)); + } else { + ESP_LOGCONFIG(TAG, " Model: RG15"); + if (this->resolution_ == FORCE_HIGH) { + ESP_LOGCONFIG(TAG, " Resolution: high"); + } else { + ESP_LOGCONFIG(TAG, " Resolution: low"); + } + } LOG_UPDATE_INTERVAL(this); int i = 0; @@ -187,7 +198,24 @@ void HydreonRGxxComponent::process_line_() { this->cancel_interval("reboot"); this->no_response_count_ = 0; ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); - this->write_str("P\nH\nM\n"); // set sensor to polling mode, high res mode, metric mode + + if (this->model_ == RG15) { + if (this->resolution_ == FORCE_HIGH) { + this->write_str("P\nH\nM\n"); // set sensor to (P)polling mode, (H)high res mode, (M)metric mode + } else { + this->write_str("P\nL\nM\n"); // set sensor to (P)polling mode, (L)low res mode, (M)metric mode + } + } + + if (this->model_ == RG9) { + this->write_str("P\n"); // set sensor to (P)polling mode + + if (this->disable_led_) { + this->write_str("D 1\n"); // set sensor (D 1)rain detection LED disabled + } else { + this->write_str("D 0\n"); // set sensor (D 0)rain detection LED enabled + } + } return; } if (this->buffer_starts_with_("SW")) { @@ -208,7 +236,7 @@ void HydreonRGxxComponent::process_line_() { } bool is_data_line = false; for (int i = 0; i < NUM_SENSORS; i++) { - if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) { + if (this->sensors_[i] != nullptr && this->buffer_.find(PROTOCOL_NAMES[i]) != std::string::npos) { is_data_line = true; break; } @@ -227,7 +255,22 @@ void HydreonRGxxComponent::process_line_() { if (n == std::string::npos) { continue; } - float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr); + + if (n == this->buffer_.find('t', n)) { + // The device temperature ('t') response contains both °C and °F values: + // "t 72F 22C". + // ESPHome uses only °C, only parse °C value (move past 'F'). + n = this->buffer_.find('F', n); + if (n == std::string::npos) { + continue; + } + n += 1; // move past 'F' + } else { + n += strlen(PROTOCOL_NAMES[i]); // move past protocol name + } + + // parse value, starting at str position n + float data = strtof(this->buffer_.substr(n).c_str(), nullptr); this->sensors_[i]->publish_state(data); ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state()); this->sensors_received_ |= (1 << i); diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h index 34b9bd8d5e..76b0985a24 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.h +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -16,6 +16,11 @@ enum RGModel { RG15 = 2, }; +enum RG15Resolution { + FORCE_LOW = 1, + FORCE_HIGH = 2, +}; + #ifdef HYDREON_RGXX_NUM_SENSORS static const uint8_t NUM_SENSORS = HYDREON_RGXX_NUM_SENSORS; #else @@ -37,6 +42,7 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { void set_em_sat_sensor(binary_sensor::BinarySensor *sensor) { this->em_sat_sensor_ = sensor; } #endif void set_model(RGModel model) { model_ = model; } + void set_resolution(RG15Resolution resolution) { resolution_ = resolution; } void set_request_temperature(bool b) { request_temperature_ = b; } /// Schedule data readings. @@ -49,6 +55,8 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { float get_setup_priority() const override; + void set_disable_led(bool disable_led) { this->disable_led_ = disable_led; } + protected: void process_line_(); void schedule_reboot_(); @@ -66,12 +74,16 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { int16_t boot_count_ = 0; int16_t no_response_count_ = 0; std::string buffer_; + RGModel model_ = RG9; + RG15Resolution resolution_ = FORCE_HIGH; + int sw_version_ = 0; bool too_cold_ = false; bool lens_bad_ = false; bool em_sat_ = false; bool request_temperature_ = false; + bool disable_led_ = false; // bit field showing which sensors we have received data for int sensors_received_ = -1; diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index c2dbbd6737..fb2099c85e 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -5,19 +5,20 @@ from esphome.const import ( CONF_ID, CONF_MODEL, CONF_MOISTURE, + CONF_RESOLUTION, CONF_TEMPERATURE, DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRECIPITATION, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, + UNIT_MILLIMETER, ICON_THERMOMETER, ) -from . import RGModel, HydreonRGxxComponent +from . import RGModel, RG15Resolution, HydreonRGxxComponent UNIT_INTENSITY = "intensity" -UNIT_MILLIMETERS = "mm" UNIT_MILLIMETERS_PER_HOUR = "mm/h" CONF_ACC = "acc" @@ -25,20 +26,33 @@ CONF_EVENT_ACC = "event_acc" CONF_TOTAL_ACC = "total_acc" CONF_R_INT = "r_int" +CONF_DISABLE_LED = "disable_led" + RG_MODELS = { "RG_9": RGModel.RG9, "RG_15": RGModel.RG15, - # https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf - # https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf - # https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf + # RG-15 + # 1.000 - https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf + # RG-9 + # 1.000 - https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf + # 1.100 - https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf + # 1.200 - https://rainsensors.com/wp-content/uploads/sites/3/2022/03/2022.02.17-rev-1.200-rg-9_instructions.pdf } -SUPPORTED_SENSORS = { + +RG15_RESOLUTION = { + "low": RG15Resolution.FORCE_LOW, + "high": RG15Resolution.FORCE_HIGH, +} + +SUPPORTED_OPTIONS = { CONF_ACC: ["RG_15"], CONF_EVENT_ACC: ["RG_15"], CONF_TOTAL_ACC: ["RG_15"], CONF_R_INT: ["RG_15"], + CONF_RESOLUTION: ["RG_15"], CONF_MOISTURE: ["RG_9"], CONF_TEMPERATURE: ["RG_9"], + CONF_DISABLE_LED: ["RG_9"], } PROTOCOL_NAMES = { CONF_MOISTURE: "R", @@ -51,7 +65,7 @@ PROTOCOL_NAMES = { def _validate(config): - for conf, models in SUPPORTED_SENSORS.items(): + for conf, models in SUPPORTED_OPTIONS.items(): if conf in config: if config[CONF_MODEL] not in models: raise cv.Invalid( @@ -69,20 +83,21 @@ CONFIG_SCHEMA = cv.All( upper=True, space="_", ), + cv.Optional(CONF_RESOLUTION): cv.enum(RG15_RESOLUTION, upper=False), cv.Optional(CONF_ACC): sensor.sensor_schema( - unit_of_measurement=UNIT_MILLIMETERS, + unit_of_measurement=UNIT_MILLIMETER, accuracy_decimals=2, device_class=DEVICE_CLASS_PRECIPITATION, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema( - unit_of_measurement=UNIT_MILLIMETERS, + unit_of_measurement=UNIT_MILLIMETER, accuracy_decimals=2, device_class=DEVICE_CLASS_PRECIPITATION, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema( - unit_of_measurement=UNIT_MILLIMETERS, + unit_of_measurement=UNIT_MILLIMETER, accuracy_decimals=2, device_class=DEVICE_CLASS_PRECIPITATION, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -105,6 +120,7 @@ CONFIG_SCHEMA = cv.All( icon=ICON_THERMOMETER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_DISABLE_LED): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -131,4 +147,11 @@ async def to_code(config): sens = await sensor.new_sensor(config[conf]) cg.add(var.set_sensor(sens, i)) + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESOLUTION in config: + cg.add(var.set_resolution(config[CONF_RESOLUTION])) + cg.add(var.set_request_temperature(CONF_TEMPERATURE in config)) + + if CONF_DISABLE_LED in config: + cg.add(var.set_disable_led(config[CONF_DISABLE_LED])) diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp index 94558fff04..3b81294cfc 100644 --- a/esphome/components/hyt271/hyt271.cpp +++ b/esphome/components/hyt271/hyt271.cpp @@ -17,7 +17,7 @@ void HYT271Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_); } void HYT271Component::update() { - uint8_t raw_data[4]; + uint8_t raw_data[4] = {0, 0, 0, 0}; if (this->write(&raw_data[0], 0) != i2c::ERROR_OK) { this->status_set_warning(); diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index a04e63e789..f52a0edb9f 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -4,6 +4,7 @@ import esphome.final_validate as fv from esphome import pins from esphome.const import ( CONF_FREQUENCY, + CONF_TIMEOUT, CONF_ID, CONF_INPUT, CONF_OUTPUT, @@ -12,6 +13,9 @@ from esphome.const import ( CONF_SDA, CONF_ADDRESS, CONF_I2C_ID, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, ) from esphome.core import coroutine_with_priority, CORE @@ -36,29 +40,32 @@ def _bus_declare_type(value): raise NotImplementedError -pin_with_input_and_output_support = cv.All( - pins.internal_gpio_pin_number({CONF_INPUT: True}), - pins.internal_gpio_pin_number({CONF_OUTPUT: True}), +pin_with_input_and_output_support = pins.internal_gpio_pin_number( + {CONF_OUTPUT: True, CONF_INPUT: True} ) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): _bus_declare_type, - cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, - cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean - ), - cv.Optional(CONF_SCL, default="SCL"): pin_with_input_and_output_support, - cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean - ), - cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( - cv.frequency, cv.Range(min=0, min_included=False) - ), - cv.Optional(CONF_SCAN, default=True): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): _bus_declare_type, + cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, + cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( + cv.only_with_esp_idf, cv.boolean + ), + cv.Optional(CONF_SCL, default="SCL"): pin_with_input_and_output_support, + cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( + cv.only_with_esp_idf, cv.boolean + ), + cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( + cv.frequency, cv.Range(min=0, min_included=False) + ), + cv.Optional(CONF_TIMEOUT): cv.positive_time_period, + cv.Optional(CONF_SCAN, default=True): cv.boolean, + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), +) @coroutine_with_priority(1.0) @@ -76,6 +83,8 @@ async def to_code(config): cg.add(var.set_frequency(int(config[CONF_FREQUENCY]))) 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: cg.add_library("Wire", None) @@ -114,23 +123,56 @@ async def register_i2c_device(var, config): def final_validate_device_schema( - name: str, *, min_frequency: cv.frequency = None, max_frequency: cv.frequency = None + name: str, + *, + min_frequency: cv.frequency = None, + max_frequency: cv.frequency = None, + min_timeout: cv.time_period = None, + max_timeout: cv.time_period = None, ): hub_schema = {} - if min_frequency is not None: + if (min_frequency is not None) and (max_frequency is not None): + hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range( + min=cv.frequency(min_frequency), + min_included=True, + max=cv.frequency(max_frequency), + max_included=True, + msg=f"Component {name} requires a frequency between {min_frequency} and {max_frequency} for the I2C bus", + ) + elif min_frequency is not None: hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range( min=cv.frequency(min_frequency), min_included=True, msg=f"Component {name} requires a minimum frequency of {min_frequency} for the I2C bus", ) - - if max_frequency is not None: + elif max_frequency is not None: hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range( max=cv.frequency(max_frequency), max_included=True, msg=f"Component {name} cannot be used with a frequency of over {max_frequency} for the I2C bus", ) + if (min_timeout is not None) and (max_timeout is not None): + hub_schema[cv.Required(CONF_TIMEOUT)] = cv.Range( + min=cv.time_period(min_timeout), + min_included=True, + max=cv.time_period(max_timeout), + max_included=True, + msg=f"Component {name} requires a timeout between {min_timeout} and {max_timeout} for the I2C bus", + ) + elif min_timeout is not None: + hub_schema[cv.Required(CONF_TIMEOUT)] = cv.Range( + min=cv.time_period(min_timeout), + min_included=True, + msg=f"Component {name} requires a minimum timeout of {min_timeout} for the I2C bus", + ) + elif max_timeout is not None: + hub_schema[cv.Required(CONF_TIMEOUT)] = cv.Range( + max=cv.time_period(max_timeout), + max_included=True, + msg=f"Component {name} cannot be used with a timeout of over {max_timeout} for the I2C bus", + ) + return cv.Schema( {cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)}, extra=cv.ALLOW_EXTRA, diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index fdc9fd1ddf..2b2190d28b 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -14,6 +14,14 @@ ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len return bus_->read(address_, data, len); } +ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { + a_register = convert_big_endian(a_register); + ErrorCode const err = this->write(reinterpret_cast(&a_register), 2, stop); + if (err != ERROR_OK) + return err; + return bus_->read(address_, data, len); +} + ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { WriteBuffer buffers[2]; buffers[0].data = &a_register; @@ -23,6 +31,16 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz return bus_->writev(address_, buffers, 2, stop); } +ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) { + a_register = convert_big_endian(a_register); + WriteBuffer buffers[2]; + buffers[0].data = reinterpret_cast(&a_register); + buffers[0].len = 2; + buffers[1].data = data; + buffers[1].len = len; + return bus_->writev(address_, buffers, 2, stop); +} + bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { if (read_register(a_register, reinterpret_cast(data), len * 2) != ERROR_OK) return false; @@ -60,5 +78,26 @@ uint8_t I2CRegister::get() const { return value; } +I2CRegister16 &I2CRegister16::operator=(uint8_t value) { + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} +I2CRegister16 &I2CRegister16::operator&=(uint8_t value) { + value &= get(); + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} +I2CRegister16 &I2CRegister16::operator|=(uint8_t value) { + value |= get(); + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} + +uint8_t I2CRegister16::get() const { + uint8_t value = 0x00; + this->parent_->read_register16(this->register_, &value, 1); + return value; +} + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 780528a5c7..8d8e139c61 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -11,24 +11,116 @@ namespace i2c { #define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); -class I2CDevice; +class I2CDevice; // forward declaration + +/// @brief This class is used to create I2CRegister objects that act as proxies to read/write internal registers on an +/// I2C device. +/// @details +/// @n typical usage: +/// @code +/// constexpr uint8_t ADDR_REGISTER_1 = 0x12; +/// i2c::I2CRegister reg_1 = this->reg(ADDR_REGISTER_1); // declare +/// reg_1 |= 0x01; // set bit +/// reg_1 &= ~0x01; // reset bit +/// reg_1 = 10; // Set value +/// uint val = reg_1.get(); // get value +/// @endcode +/// @details The I²C protocol specifies how to read/write in sets of 8-bits followed by an Acknowledgement (ACK/NACK) +/// from the device receiving the data. How the device interprets the bits read/written can vary greatly from +/// device to device. However most of the devices follow the same protocol for reading/writing 8 bit registers using as +/// implemented in the I2CRegister: after sending the device address, the controller sends one byte with the internal +/// register address and then read or write the specified register content. class I2CRegister { public: + /// @brief overloads the = operator. This allows to set the value of an i2c register + /// @param value value to be set in the register + /// @return pointer to current object I2CRegister &operator=(uint8_t value); + + /// @brief overloads the compound &= operator. This allows to reset specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister &operator&=(uint8_t value); + + /// @brief overloads the compound |= operator. This allows to set specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object I2CRegister &operator|=(uint8_t value); + /// @brief overloads the uint8_t() cast operator to return the I²C register value + /// @return pointer to current object explicit operator uint8_t() const { return get(); } + /// @brief returns the register value + /// @return the register value uint8_t get() const; protected: friend class I2CDevice; + /// @brief protected constructor that stores the owning object and the register address. Note as only friends can + /// create an I2CRegister @see I2CDevice::reg() + /// @param parent our parent + /// @param a_register address of the i2c register I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} - I2CDevice *parent_; - uint8_t register_; + I2CDevice *parent_; ///< I2CDevice object pointer + uint8_t register_; ///< the internal address of the register +}; + +/// @brief This class is used to create I2CRegister16 objects that act as proxies to read/write internal registers +/// (specified with a 16 bit address) on an I2C device. +/// @details +/// @n typical usage: +/// @code +/// constexpr uint16_t X16_BIT_ADDR_REGISTER_1 = 0x1234; +/// i2c::I2CRegister16 reg_1 = this->reg16(X16_BIT_ADDR_REGISTER_1); // declare +/// reg_1 |= 0x01; // set bit +/// reg_1 &= ~0x01; // reset bit +/// reg_1 = 10; // Set value +/// uint val = reg_1.get(); // get value +/// @endcode +/// @details The I²C protocol specification, reads/writes in sets of 8-bits followed by an Acknowledgement (ACK/NACK) +/// from the device receiving the data. How the device interprets the bits read/written to it can vary greatly from +/// device to device. This class can be used to access in the device 8 bits registers that uses a 16 bits internal +/// address. After sending the device address, the controller sends the internal register address (using two consecutive +/// bytes following the big indian convention) and then read or write the register content. +class I2CRegister16 { + public: + /// @brief overloads the = operator. This allows to set the value of an I²C register + /// @param value value to be set in the register + /// @return pointer to current object + I2CRegister16 &operator=(uint8_t value); + + /// @brief overloads the compound &= operator. This allows to reset specific bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object + I2CRegister16 &operator&=(uint8_t value); + + /// @brief overloads the compound |= operator. This allows to set bits of an I²C register + /// @param value used for the & operation + /// @return pointer to current object + I2CRegister16 &operator|=(uint8_t value); + + /// @brief overloads the uint8_t() cast operator to return the I²C register value + /// @return the register value + explicit operator uint8_t() const { return get(); } + + /// @brief returns the register value + /// @return the register value + uint8_t get() const; + + protected: + friend class I2CDevice; + + /// @brief protected constructor that store the owning object and the register address. Only friends can create an + /// I2CRegister16 @see I2CDevice::reg16() + /// @param parent our parent + /// @param a_register 16 bits address of the i2c register + I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {} + + I2CDevice *parent_; ///< I2CDevice object pointer + uint16_t register_; ///< the internal 16 bits address of the register }; // like ntohs/htons but without including networking headers. @@ -36,26 +128,91 @@ class I2CRegister { inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } inline uint16_t htoi2cs(uint16_t hostshort) { return convert_big_endian(hostshort); } +/// @brief This Class provides the methods to read/write bytes from/to an i2c device. +/// Objects keep a list of devices found on bus as well as a pointer to the I2CBus in use. class I2CDevice { public: + /// @brief we use the C++ default constructor I2CDevice() = default; + /// @brief We store the address of the device on the bus + /// @param address of the device void set_i2c_address(uint8_t address) { address_ = address; } + + /// @brief we store the pointer to the I2CBus to use + /// @param bus pointer to the I2CBus object void set_i2c_bus(I2CBus *bus) { bus_ = bus; } + /// @brief calls the I2CRegister constructor + /// @param a_register address of the I²C register + /// @return an I2CRegister proxy object I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + /// @brief calls the I2CRegister16 constructor + /// @param a_register 16 bits address of the I²C register + /// @return an I2CRegister16 proxy object + I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; } + + /// @brief reads an array of bytes from the device using an I2CBus + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } + + /// @brief reads an array of bytes from a specific register in the I²C device + /// @param a_register an 8 bits internal address of the I²C register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); - ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + /// @brief reads an array of bytes from a specific register in the I²C device + /// @param a_register the 16 bits internal address of the I²C register to read from + /// @param data pointer to an array of bytes to store the information + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); + + /// @brief writes an array of bytes to a device using an I2CBus + /// @param data pointer to an array that contains the bytes to send + /// @param len length of the buffer = number of bytes to write + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode + ErrorCode write(const uint8_t *data, size_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + + /// @brief writes an array of bytes to a specific register in the I²C device + /// @param a_register the internal address of the register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); - // Compat APIs + /// @brief write an array of bytes to a specific register in the I²C device + /// @param a_register the 16 bits internal address of the register to read from + /// @param data pointer to an array to store the bytes + /// @param len length of the buffer = number of bytes to read + /// @param stop (true/false): True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); + + /// + /// Compat APIs + /// All methods below have been added for compatibility reasons. They do not bring any functionality and therefore on + /// new code it is not recommend to use them. + /// bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len) { return read_register(a_register, data, len) == ERROR_OK; } + bool read_bytes_raw(uint8_t *data, uint8_t len) { return read(data, len) == ERROR_OK; } template optional> read_bytes(uint8_t a_register) { @@ -109,8 +266,8 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } protected: - uint8_t address_{0x00}; - I2CBus *bus_{nullptr}; + uint8_t address_{0x00}; ///< store the address of the device on the bus + I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance }; } // namespace i2c diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 2633a7adf6..fbfc88323e 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -7,50 +7,93 @@ namespace esphome { namespace i2c { +/// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { - ERROR_OK = 0, - ERROR_INVALID_ARGUMENT = 1, - ERROR_NOT_ACKNOWLEDGED = 2, - ERROR_TIMEOUT = 3, - ERROR_NOT_INITIALIZED = 4, - ERROR_TOO_LARGE = 5, - ERROR_UNKNOWN = 6, - ERROR_CRC = 7, + NO_ERROR = 0, ///< No error found during execution of method + ERROR_OK = 0, ///< No error found during execution of method + ERROR_INVALID_ARGUMENT = 1, ///< method called invalid argument(s) + ERROR_NOT_ACKNOWLEDGED = 2, ///< I2C bus acknowledgment not received + ERROR_TIMEOUT = 3, ///< timeout while waiting to receive bytes + ERROR_NOT_INITIALIZED = 4, ///< call method to a not initialized bus + ERROR_TOO_LARGE = 5, ///< requested a transfer larger than buffers can hold + ERROR_UNKNOWN = 6, ///< miscellaneous I2C error during execution + ERROR_CRC = 7, ///< bytes received with a CRC error }; +/// @brief the ReadBuffer structure stores a pointer to a read buffer and its length struct ReadBuffer { - uint8_t *data; - size_t len; -}; -struct WriteBuffer { - const uint8_t *data; - size_t len; + uint8_t *data; ///< pointer to the read buffer + size_t len; ///< length of the buffer }; +/// @brief the WriteBuffer structure stores a pointer to a write buffer and its length +struct WriteBuffer { + const uint8_t *data; ///< pointer to the write buffer + size_t len; ///< length of the buffer +}; + +/// @brief This Class provides the methods to read and write bytes from an I2CBus. +/// @note The I2CBus virtual class follows a *Factory design pattern* that provides all the interfaces methods required +/// by clients while deferring the actual implementation of these methods to a subclasses. I2C-bus specification and +/// user manual can be found here https://www.nxp.com/docs/en/user-guide/UM10204.pdf and an interesting I²C Application +/// note https://www.nxp.com/docs/en/application-note/AN10216.pdf class I2CBus { public: + /// @brief Creates a ReadBuffer and calls the virtual readv() method to read bytes into this buffer + /// @param address address of the I²C component on the i2c bus + /// @param buffer pointer to an array of bytes that will be used to store the data received + /// @param len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode virtual ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { ReadBuffer buf; buf.data = buffer; buf.len = len; return readv(address, &buf, 1); } - virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0; + + /// @brief This virtual method reads bytes from an I2CBus into an array of ReadBuffer. + /// @param address address of the I²C component on the i2c bus + /// @param buffers pointer to an array of ReadBuffer + /// @param count number of ReadBuffer to read + /// @return an i2c::ErrorCode + /// @details This is a pure virtual method that must be implemented in a subclass. + virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t count) = 0; + virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { return write(address, buffer, len, true); } + + /// @brief Creates a WriteBuffer and calls the writev() method to send the bytes from this buffer + /// @param address address of the I²C component on the i2c bus + /// @param buffer pointer to an array of bytes that contains the data to be sent + /// @param len length of the buffer = number of bytes to write + /// @param stop true or false: True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { WriteBuffer buf; buf.data = buffer; buf.len = len; return writev(address, &buf, 1, stop); } + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { return writev(address, buffers, cnt, true); } - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) = 0; + + /// @brief This virtual method writes bytes to an I2CBus from an array of WriteBuffer. + /// @param address address of the I²C component on the i2c bus + /// @param buffers pointer to an array of WriteBuffer + /// @param count number of WriteBuffer to write + /// @param stop true or false: True will send a stop message, releasing the bus after + /// transmission. False will send a restart, keeping the connection active. + /// @return an i2c::ErrorCode + /// @details This is a pure virtual method that must be implemented in the subclass. + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t count, bool stop) = 0; protected: + /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair + /// that contains the address and the corresponding bool presence flag. void i2c_scan_() { for (uint8_t address = 8; address < 120; address++) { auto err = writev(address, nullptr, 0); @@ -61,8 +104,8 @@ class I2CBus { } } } - std::vector> scan_results_; - bool scan_{false}; + std::vector> scan_results_; ///< array containing scan results + bool scan_{false}; ///< Should we scan ? Can be set in the yaml }; } // namespace i2c diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index d80ab1fd1d..cd1b2aacc7 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -24,7 +24,7 @@ void ArduinoI2CBus::setup() { } next_bus_num++; #elif defined(USE_ESP8266) - wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer) + wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) static bool first = true; if (first) { @@ -35,6 +35,16 @@ void ArduinoI2CBus::setup() { } #endif + this->set_pins_and_clock_(); + + this->initialized_ = true; + if (this->scan_) { + ESP_LOGV(TAG, "Scanning i2c bus for active devices..."); + this->i2c_scan_(); + } +} + +void ArduinoI2CBus::set_pins_and_clock_() { #ifdef USE_RP2040 wire_->setSDA(this->sda_pin_); wire_->setSCL(this->scl_pin_); @@ -42,18 +52,35 @@ void ArduinoI2CBus::setup() { #else wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); #endif - wire_->setClock(frequency_); - initialized_ = true; - if (this->scan_) { - ESP_LOGV(TAG, "Scanning i2c bus for active devices..."); - this->i2c_scan_(); + 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) + // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h + wire_->setClockStretchLimit(timeout_); // unit: us +#elif defined(USE_RP2040) + // https://github.com/earlephilhower/ArduinoCore-API/blob/e37df85425e0ac020bfad226d927f9b00d2e0fb7/api/Stream.h + wire_->setTimeout(timeout_ / 1000); // unit: ms +#endif } + wire_->setClock(frequency_); } + void ArduinoI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, " SDA Pin: GPIO%u", this->sda_pin_); ESP_LOGCONFIG(TAG, " SCL Pin: GPIO%u", this->scl_pin_); ESP_LOGCONFIG(TAG, " Frequency: %u Hz", this->frequency_); + if (timeout_ > 0) { +#if defined(USE_ESP32) + ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); +#elif defined(USE_ESP8266) + ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_); +#elif defined(USE_RP2040) + ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); +#endif + } switch (this->recovery_result_) { case RECOVERY_COMPLETED: ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); @@ -82,6 +109,10 @@ void ArduinoI2CBus::dump_config() { } ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { +#if defined(USE_ESP8266) + this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances +#endif + // logging is only enabled with vv level, if warnings are shown the caller // should log them if (!initialized_) { @@ -120,6 +151,10 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) return ERROR_OK; } ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { +#if defined(USE_ESP8266) + this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances +#endif + // logging is only enabled with vv level, if warnings are shown the caller // should log them if (!initialized_) { @@ -164,7 +199,7 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn return ERROR_UNKNOWN; case 2: case 3: - ESP_LOGVV(TAG, "TX failed: not acknowledged"); + ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status); return ERROR_NOT_ACKNOWLEDGED; case 5: ESP_LOGVV(TAG, "TX failed: timeout"); diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 7298c3a1c9..6a670a2a05 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -27,9 +27,11 @@ class ArduinoI2CBus : public I2CBus, public Component { void set_sda_pin(uint8_t sda_pin) { sda_pin_ = sda_pin; } void set_scl_pin(uint8_t scl_pin) { scl_pin_ = scl_pin; } void set_frequency(uint32_t frequency) { frequency_ = frequency; } + void set_timeout(uint32_t timeout) { timeout_ = timeout; } private: void recover_(); + void set_pins_and_clock_(); RecoveryCode recovery_result_; protected: @@ -37,6 +39,7 @@ class ArduinoI2CBus : public I2CBus, public Component { uint8_t sda_pin_; uint8_t scl_pin_; uint32_t frequency_; + uint32_t timeout_ = 0; bool initialized_ = false; }; diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 51688322f6..3a9c229778 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,12 +1,12 @@ #ifdef USE_ESP_IDF #include "i2c_bus_esp_idf.h" -#include "esphome/core/hal.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" -#include "esphome/core/application.h" -#include #include +#include +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace i2c { @@ -14,8 +14,20 @@ namespace i2c { static const char *const TAG = "i2c.idf"; void IDFI2CBus::setup() { - static i2c_port_t next_port = 0; - port_ = next_port++; + ESP_LOGCONFIG(TAG, "Setting up I2C bus..."); + static i2c_port_t next_port = I2C_NUM_0; + port_ = next_port; +#if I2C_NUM_MAX > 1 + next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; +#else + next_port = I2C_NUM_MAX; +#endif + + if (port_ == I2C_NUM_MAX) { + ESP_LOGE(TAG, "Too many I2C buses configured"); + this->mark_failed(); + return; + } recover_(); @@ -33,6 +45,20 @@ void IDFI2CBus::setup() { this->mark_failed(); return; } + if (timeout_ > 0) { // if timeout specified in yaml: + if (timeout_ > 13000) { + ESP_LOGW(TAG, "i2c timeout of %" PRIu32 "us greater than max of 13ms on esp-idf, setting to max", timeout_); + timeout_ = 13000; + } + err = i2c_set_timeout(port_, timeout_ * 80); // unit: APB 80MHz clock cycle + if (err != ESP_OK) { + ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } else { + ESP_LOGV(TAG, "i2c_timeout set to %" PRIu32 " ticks (%" PRIu32 " us)", timeout_ * 80, timeout_); + } + } err = i2c_driver_install(port_, I2C_MODE_MASTER, 0, 0, ESP_INTR_FLAG_IRAM); if (err != ESP_OK) { ESP_LOGW(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err)); @@ -50,6 +76,9 @@ void IDFI2CBus::dump_config() { ESP_LOGCONFIG(TAG, " SDA Pin: GPIO%u", this->sda_pin_); ESP_LOGCONFIG(TAG, " SCL Pin: GPIO%u", this->scl_pin_); ESP_LOGCONFIG(TAG, " Frequency: %" PRIu32 " Hz", this->frequency_); + if (timeout_ > 0) { + ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 "us", this->timeout_); + } switch (this->recovery_result_) { case RECOVERY_COMPLETED: ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); @@ -115,6 +144,8 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { return ERROR_UNKNOWN; } err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); + // i2c_master_cmd_begin() will block for a whole second if no ack: + // https://github.com/espressif/esp-idf/issues/4999 i2c_cmd_link_delete(cmd); if (err == ESP_FAIL) { // transfer not acked @@ -190,11 +221,13 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b return ERROR_UNKNOWN; } } - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; + if (stop) { + err = i2c_master_stop(cmd); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + return ERROR_UNKNOWN; + } } err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); i2c_cmd_link_delete(cmd); diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index c80ea8c99d..afb4c2d22b 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -29,6 +29,7 @@ class IDFI2CBus : public I2CBus, public Component { void set_scl_pin(uint8_t scl_pin) { scl_pin_ = scl_pin; } void set_scl_pullup_enabled(bool scl_pullup_enabled) { scl_pullup_enabled_ = scl_pullup_enabled; } void set_frequency(uint32_t frequency) { frequency_ = frequency; } + void set_timeout(uint32_t timeout) { timeout_ = timeout; } private: void recover_(); @@ -41,6 +42,7 @@ class IDFI2CBus : public I2CBus, public Component { uint8_t scl_pin_; bool scl_pullup_enabled_; uint32_t frequency_; + uint32_t timeout_ = 0; bool initialized_ = false; }; diff --git a/esphome/components/i2c_device/__init__.py b/esphome/components/i2c_device/__init__.py new file mode 100644 index 0000000000..e145ba56f8 --- /dev/null +++ b/esphome/components/i2c_device/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@gabest11"] +MULTI_CONF = True + +i2c_device_ns = cg.esphome_ns.namespace("i2c_device") + +I2CDeviceComponent = i2c_device_ns.class_( + "I2CDeviceComponent", cg.Component, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(I2CDeviceComponent), + } +).extend(i2c.i2c_device_schema(None)) + + +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) diff --git a/esphome/components/i2c_device/i2c_device.cpp b/esphome/components/i2c_device/i2c_device.cpp new file mode 100644 index 0000000000..455c68fbed --- /dev/null +++ b/esphome/components/i2c_device/i2c_device.cpp @@ -0,0 +1,17 @@ +#include "i2c_device.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace i2c_device { + +static const char *const TAG = "i2c_device"; + +void I2CDeviceComponent::dump_config() { + ESP_LOGCONFIG(TAG, "I2CDevice"); + LOG_I2C_DEVICE(this); +} + +} // namespace i2c_device +} // namespace esphome diff --git a/esphome/components/i2c_device/i2c_device.h b/esphome/components/i2c_device/i2c_device.h new file mode 100644 index 0000000000..ab118e3e89 --- /dev/null +++ b/esphome/components/i2c_device/i2c_device.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace i2c_device { + +class I2CDeviceComponent : public Component, public i2c::I2CDevice { + public: + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: +}; + +} // namespace i2c_device +} // namespace esphome diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index d72e13630f..fa515a585f 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,16 +1,16 @@ -import esphome.config_validation as cv -import esphome.final_validate as fv -import esphome.codegen as cg - from esphome import pins -from esphome.const import CONF_ID +import esphome.codegen as cg from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, + VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32C3, ) +import esphome.config_validation as cv +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE +from esphome.cpp_generator import MockObjClass +import esphome.final_validate as fv CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] @@ -25,12 +25,30 @@ CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" CONF_I2S_AUDIO = "i2s_audio" CONF_I2S_AUDIO_ID = "i2s_audio_id" +CONF_I2S_MODE = "i2s_mode" +CONF_PRIMARY = "primary" +CONF_SECONDARY = "secondary" + +CONF_USE_APLL = "use_apll" +CONF_BITS_PER_CHANNEL = "bits_per_channel" +CONF_MONO = "mono" +CONF_LEFT = "left" +CONF_RIGHT = "right" +CONF_STEREO = "stereo" + i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) -I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", cg.Parented.template(I2SAudioComponent)) -I2SAudioOut = i2s_audio_ns.class_( - "I2SAudioOut", cg.Parented.template(I2SAudioComponent) +I2SAudioBase = i2s_audio_ns.class_( + "I2SAudioBase", cg.Parented.template(I2SAudioComponent) ) +I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", I2SAudioBase) +I2SAudioOut = i2s_audio_ns.class_("I2SAudioOut", I2SAudioBase) + +i2s_mode_t = cg.global_ns.enum("i2s_mode_t") +I2S_MODE_OPTIONS = { + CONF_PRIMARY: i2s_mode_t.I2S_MODE_MASTER, # NOLINT + CONF_SECONDARY: i2s_mode_t.I2S_MODE_SLAVE, # NOLINT +} # https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h I2S_PORTS = { @@ -40,6 +58,75 @@ I2S_PORTS = { VARIANT_ESP32C3: 1, } +i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") +I2S_CHANNELS = { + CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT, + CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, + CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, + CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT, +} + +i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") +I2S_BITS_PER_SAMPLE = { + 8: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_8BIT, + 16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT, + 24: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_24BIT, + 32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT, +} + +i2s_bits_per_chan_t = cg.global_ns.enum("i2s_bits_per_chan_t") +I2S_BITS_PER_CHANNEL = { + "default": i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_DEFAULT, + 8: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_8BIT, + 16: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_16BIT, + 24: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_24BIT, + 32: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_32BIT, +} + +_validate_bits = cv.float_with_unit("bits", "bit") + + +def i2s_audio_component_schema( + class_: MockObjClass, + *, + default_sample_rate: int, + default_channel: str, + default_bits_per_sample: str, +): + return cv.Schema( + { + cv.GenerateID(): cv.declare_id(class_), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Optional(CONF_CHANNEL, default=default_channel): cv.enum(I2S_CHANNELS), + cv.Optional(CONF_SAMPLE_RATE, default=default_sample_rate): cv.int_range( + min=1 + ), + cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits_per_sample): cv.All( + _validate_bits, cv.enum(I2S_BITS_PER_SAMPLE) + ), + cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum( + I2S_MODE_OPTIONS, lower=True + ), + cv.Optional(CONF_USE_APLL, default=False): cv.boolean, + cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All( + cv.Any(cv.float_with_unit("bits", "bit"), "default"), + cv.enum(I2S_BITS_PER_CHANNEL), + ), + } + ) + + +async def register_i2s_audio_component(var, config): + await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + + cg.add(var.set_i2s_mode(config[CONF_I2S_MODE])) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) + cg.add(var.set_bits_per_channel(config[CONF_BITS_PER_CHANNEL])) + cg.add(var.set_use_apll(config[CONF_USE_APLL])) + + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(I2SAudioComponent), diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp index c1a608c064..ad73b383fe 100644 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -9,6 +9,10 @@ namespace i2s_audio { static const char *const TAG = "i2s_audio"; +#if defined(USE_ESP_IDF) && (ESP_IDF_VERSION_MAJOR >= 5) +static const uint8_t I2S_NUM_MAX = SOC_I2S_NUM; // because IDF 5+ took this away :( +#endif + void I2SAudioComponent::setup() { static i2s_port_t next_port_num = I2S_NUM_0; diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index d8d4a23dde..7e2798c33d 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -11,9 +11,27 @@ namespace i2s_audio { class I2SAudioComponent; -class I2SAudioIn : public Parented {}; +class I2SAudioBase : public Parented { + public: + void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; } + void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } + void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } + void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } + void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; } + void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } -class I2SAudioOut : public Parented {}; + protected: + i2s_mode_t i2s_mode_{}; + i2s_channel_fmt_t channel_; + uint32_t sample_rate_; + i2s_bits_per_sample_t bits_per_sample_; + i2s_bits_per_chan_t bits_per_channel_; + bool use_apll_; +}; + +class I2SAudioIn : public I2SAudioBase {}; + +class I2SAudioOut : public I2SAudioBase {}; class I2SAudioComponent : public Component { public: diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index dfc3fb2be2..dfa69ecadd 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -12,6 +12,10 @@ from .. import ( I2SAudioOut, CONF_I2S_AUDIO_ID, CONF_I2S_DOUT_PIN, + CONF_LEFT, + CONF_RIGHT, + CONF_MONO, + CONF_STEREO, ) CODEOWNERS = ["@jesserockz"] @@ -27,17 +31,20 @@ i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") CONF_MUTE_PIN = "mute_pin" CONF_AUDIO_ID = "audio_id" CONF_DAC_TYPE = "dac_type" +CONF_I2S_COMM_FMT = "i2s_comm_fmt" INTERNAL_DAC_OPTIONS = { - "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, - "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, - "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, + CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, + CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, + CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, } -EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] +EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO] NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] +I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] + def validate_esp32_variant(config): if config[CONF_DAC_TYPE] != "internal": @@ -69,6 +76,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MODE, default="mono"): cv.one_of( *EXTERNAL_DAC_OPTIONS, lower=True ), + cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of( + *I2C_COMM_FMT_OPTIONS, lower=True + ), } ).extend(cv.COMPONENT_SCHEMA), }, @@ -94,6 +104,7 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) cg.add(var.set_mute_pin(pin)) cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) + cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp index 6eaa32c23c..34ed5b02a0 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -10,55 +10,41 @@ namespace i2s_audio { static const char *const TAG = "audio"; void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { + media_player::MediaPlayerState play_state = media_player::MEDIA_PLAYER_STATE_PLAYING; + if (call.get_announcement().has_value()) { + play_state = call.get_announcement().value() ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING + : media_player::MEDIA_PLAYER_STATE_PLAYING; + } if (call.get_media_url().has_value()) { this->current_url_ = call.get_media_url(); - - if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && this->audio_ != nullptr) { + if (this->i2s_state_ != I2S_STATE_STOPPED && this->audio_ != nullptr) { if (this->audio_->isRunning()) { this->audio_->stopSong(); } this->audio_->connecttohost(this->current_url_.value().c_str()); + this->state = play_state; } else { this->start(); } } + + if (play_state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) { + this->is_announcement_ = true; + } + if (call.get_volume().has_value()) { this->volume = call.get_volume().value(); this->set_volume_(volume); this->unmute_(); } - if (this->i2s_state_ != I2S_STATE_RUNNING) { - return; - } if (call.get_command().has_value()) { switch (call.get_command().value()) { - case media_player::MEDIA_PLAYER_COMMAND_PLAY: - if (!this->audio_->isRunning()) - this->audio_->pauseResume(); - this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; - break; - case media_player::MEDIA_PLAYER_COMMAND_PAUSE: - if (this->audio_->isRunning()) - this->audio_->pauseResume(); - this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; - break; - case media_player::MEDIA_PLAYER_COMMAND_STOP: - this->stop(); - break; case media_player::MEDIA_PLAYER_COMMAND_MUTE: this->mute_(); break; case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: this->unmute_(); break; - case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: - this->audio_->pauseResume(); - if (this->audio_->isRunning()) { - this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; - } else { - this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; - } - break; case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: { float new_volume = this->volume + 0.1f; if (new_volume > 1.0f) @@ -75,6 +61,36 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { this->unmute_(); break; } + default: + break; + } + if (this->i2s_state_ != I2S_STATE_RUNNING) { + return; + } + switch (call.get_command().value()) { + case media_player::MEDIA_PLAYER_COMMAND_PLAY: + if (!this->audio_->isRunning()) + this->audio_->pauseResume(); + this->state = play_state; + break; + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: + if (this->audio_->isRunning()) + this->audio_->pauseResume(); + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + break; + case media_player::MEDIA_PLAYER_COMMAND_STOP: + this->stop(); + break; + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: + this->audio_->pauseResume(); + if (this->audio_->isRunning()) { + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + } else { + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + } + break; + default: + break; } } this->publish_state(); @@ -126,7 +142,9 @@ void I2SAudioMediaPlayer::loop() { void I2SAudioMediaPlayer::play_() { this->audio_->loop(); - if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && !this->audio_->isRunning()) { + if ((this->state == media_player::MEDIA_PLAYER_STATE_PLAYING || + this->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) && + !this->audio_->isRunning()) { this->stop(); } } @@ -148,6 +166,7 @@ void I2SAudioMediaPlayer::start_() { pin_config.data_out_num = this->dout_pin_; i2s_set_pin(this->parent_->get_port(), &pin_config); + this->audio_->setI2SCommFMT_LSB(this->i2s_comm_fmt_lsb_); this->audio_->forceMono(this->external_dac_channels_ == 1); if (this->mute_pin_ != nullptr) { this->mute_pin_->setup(); @@ -163,6 +182,9 @@ void I2SAudioMediaPlayer::start_() { if (this->current_url_.has_value()) { this->audio_->connecttohost(this->current_url_.value().c_str()); this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + if (this->is_announcement_) { + this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; + } this->publish_state(); } } @@ -190,6 +212,7 @@ void I2SAudioMediaPlayer::stop_() { this->high_freq_.stop(); this->state = media_player::MEDIA_PLAYER_STATE_IDLE; this->publish_state(); + this->is_announcement_ = false; } media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() { diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h index dab9a85d7c..4672f94d7e 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h @@ -23,7 +23,7 @@ enum I2SState : uint8_t { I2S_STATE_STOPPING, }; -class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, public I2SAudioOut { +class I2SAudioMediaPlayer : public Component, public Parented, public media_player::MediaPlayer { public: void setup() override; float get_setup_priority() const override { return esphome::setup_priority::LATE; } @@ -39,6 +39,8 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, #endif void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } + void set_i2s_comm_fmt_lsb(bool lsb) { this->i2s_comm_fmt_lsb_ = lsb; } + media_player::MediaPlayerTraits get_traits() override; bool is_muted() const override { return this->muted_; } @@ -71,9 +73,12 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, #endif uint8_t external_dac_channels_; + bool i2s_comm_fmt_lsb_; + HighFrequencyLoopRequester high_freq_; optional current_url_{}; + bool is_announcement_{false}; }; } // namespace i2s_audio diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 07f5158188..161046e962 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -1,17 +1,17 @@ -import esphome.config_validation as cv -import esphome.codegen as cg - from esphome import pins -from esphome.const import CONF_CHANNEL, CONF_ID, CONF_NUMBER -from esphome.components import microphone, esp32 +import esphome.codegen as cg +from esphome.components import esp32, microphone from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_NUMBER from .. import ( - i2s_audio_ns, - I2SAudioComponent, - I2SAudioIn, - CONF_I2S_AUDIO_ID, CONF_I2S_DIN_PIN, + CONF_RIGHT, + I2SAudioIn, + i2s_audio_component_schema, + i2s_audio_ns, + register_i2s_audio_component, ) CODEOWNERS = ["@jesserockz"] @@ -20,28 +20,14 @@ DEPENDENCIES = ["i2s_audio"] CONF_ADC_PIN = "adc_pin" CONF_ADC_TYPE = "adc_type" CONF_PDM = "pdm" -CONF_BITS_PER_SAMPLE = "bits_per_sample" I2SAudioMicrophone = i2s_audio_ns.class_( "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component ) -i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") -CHANNELS = { - "left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, - "right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, -} -i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") -BITS_PER_SAMPLE = { - 16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT, - 32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT, -} - INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] -_validate_bits = cv.float_with_unit("bits", "bit") - def validate_esp32_variant(config): variant = esp32.get_esp32_variant() @@ -58,16 +44,15 @@ def validate_esp32_variant(config): BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), - cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS), - cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( - _validate_bits, cv.enum(BITS_PER_SAMPLE) - ), - } + i2s_audio_component_schema( + I2SAudioMicrophone, + default_sample_rate=16000, + default_channel=CONF_RIGHT, + default_bits_per_sample="32bit", + ) ).extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA = cv.All( cv.typed_schema( { @@ -79,7 +64,7 @@ CONFIG_SCHEMA = cv.All( "external": BASE_SCHEMA.extend( { cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_PDM): cv.boolean, + cv.Optional(CONF_PDM, default=False): cv.boolean, } ), }, @@ -92,8 +77,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - - await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + await register_i2s_audio_component(var, config) + await microphone.register_microphone(var, config) if config[CONF_ADC_TYPE] == "internal": variant = esp32.get_esp32_variant() @@ -103,8 +88,3 @@ async def to_code(config): else: cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) cg.add(var.set_pdm(config[CONF_PDM])) - - cg.add(var.set_channel(config[CONF_CHANNEL])) - cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) - - await microphone.register_microphone(var, config) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 9c661c3ac2..23689afb91 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -16,14 +16,6 @@ static const char *const TAG = "i2s_audio.microphone"; void I2SAudioMicrophone::setup() { ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - this->buffer_ = allocator.allocate(BUFFER_SIZE); - if (this->buffer_ == nullptr) { - ESP_LOGE(TAG, "Failed to allocate buffer!"); - this->mark_failed(); - return; - } - #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { if (this->parent_->get_port() != I2S_NUM_0) { @@ -45,6 +37,8 @@ void I2SAudioMicrophone::setup() { void I2SAudioMicrophone::start() { if (this->is_failed()) return; + if (this->state_ == microphone::STATE_RUNNING) + return; // Already running this->state_ = microphone::STATE_STARTING; } void I2SAudioMicrophone::start_() { @@ -52,44 +46,72 @@ void I2SAudioMicrophone::start_() { return; // Waiting for another i2s to return lock } i2s_driver_config_t config = { - .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), - .sample_rate = 16000, + .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX), + .sample_rate = this->sample_rate_, .bits_per_sample = this->bits_per_sample_, .channel_format = this->channel_, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 4, .dma_buf_len = 256, - .use_apll = false, + .use_apll = this->use_apll_, .tx_desc_auto_clear = false, .fixed_mclk = 0, - .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, - .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + .bits_per_chan = this->bits_per_channel_, }; + esp_err_t err; + #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); - i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } - i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); - i2s_adc_enable(this->parent_->get_port()); - } else { + err = i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error setting ADC mode: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + err = i2s_adc_enable(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error enabling ADC: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + + } else #endif + { if (this->pdm_) config.mode = (i2s_mode_t) (config.mode | I2S_MODE_PDM); - i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } i2s_pin_config_t pin_config = this->parent_->get_pin_config(); pin_config.data_in_num = this->din_pin_; - i2s_set_pin(this->parent_->get_port(), &pin_config); -#if SOC_I2S_SUPPORTS_ADC + err = i2s_set_pin(this->parent_->get_port(), &pin_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error setting I2S pin: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } } -#endif this->state_ = microphone::STATE_RUNNING; this->high_freq_.start(); + this->status_clear_error(); } void I2SAudioMicrophone::stop() { @@ -103,44 +125,74 @@ void I2SAudioMicrophone::stop() { } void I2SAudioMicrophone::stop_() { - i2s_stop(this->parent_->get_port()); - i2s_driver_uninstall(this->parent_->get_port()); + esp_err_t err; +#if SOC_I2S_SUPPORTS_ADC + if (this->adc_) { + err = i2s_adc_disable(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + } +#endif + err = i2s_stop(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error stopping I2S microphone: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + err = i2s_driver_uninstall(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error uninstalling I2S driver: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } this->parent_->unlock(); this->state_ = microphone::STATE_STOPPED; this->high_freq_.stop(); + this->status_clear_error(); } -void I2SAudioMicrophone::read_() { +size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) { size_t bytes_read = 0; - esp_err_t err = - i2s_read(this->parent_->get_port(), this->buffer_, BUFFER_SIZE, &bytes_read, (100 / portTICK_PERIOD_MS)); + esp_err_t err = i2s_read(this->parent_->get_port(), buf, len, &bytes_read, (100 / portTICK_PERIOD_MS)); if (err != ESP_OK) { ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); this->status_set_warning(); - return; + return 0; + } + if (bytes_read == 0) { + this->status_set_warning(); + return 0; } this->status_clear_warning(); - - std::vector samples; - size_t samples_read = 0; - if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { - samples_read = bytes_read / sizeof(int16_t); - } else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) { - samples_read = bytes_read / sizeof(int32_t); - } else { - ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_); - return; - } - samples.resize(samples_read); - if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { - memcpy(samples.data(), this->buffer_, bytes_read); - } else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) { - for (size_t i = 0; i < samples_read; i++) { - int32_t temp = reinterpret_cast(this->buffer_)[i] >> 14; - samples[i] = clamp(temp, INT16_MIN, INT16_MAX); + // ESP-IDF I2S implementation right-extends 8-bit data to 16 bits, + // and 24-bit data to 32 bits. + switch (this->bits_per_sample_) { + case I2S_BITS_PER_SAMPLE_8BIT: + case I2S_BITS_PER_SAMPLE_16BIT: + return bytes_read; + case I2S_BITS_PER_SAMPLE_24BIT: + case I2S_BITS_PER_SAMPLE_32BIT: { + size_t samples_read = bytes_read / sizeof(int32_t); + for (size_t i = 0; i < samples_read; i++) { + int32_t temp = reinterpret_cast(buf)[i] >> 14; + buf[i] = clamp(temp, INT16_MIN, INT16_MAX); + } + return samples_read * sizeof(int16_t); } + default: + ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_); + return 0; } +} +void I2SAudioMicrophone::read_() { + std::vector samples; + samples.resize(BUFFER_SIZE); + size_t bytes_read = this->read(samples.data(), BUFFER_SIZE / sizeof(int16_t)); + samples.resize(bytes_read / sizeof(int16_t)); this->data_callbacks_.call(samples); } @@ -152,7 +204,9 @@ void I2SAudioMicrophone::loop() { this->start_(); break; case microphone::STATE_RUNNING: - this->read_(); + if (this->data_callbacks_.size() > 0) { + this->read_(); + } break; case microphone::STATE_STOPPING: this->stop_(); diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 0cb87d42fd..ea3f357624 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -21,6 +21,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub void set_din_pin(int8_t pin) { this->din_pin_ = pin; } void set_pdm(bool pdm) { this->pdm_ = pdm; } + size_t read(int16_t *buf, size_t len) override; + #if SOC_I2S_SUPPORTS_ADC void set_adc_channel(adc1_channel_t channel) { this->adc_channel_ = channel; @@ -28,9 +30,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub } #endif - void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } - void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } - protected: void start_(); void stop_(); @@ -42,9 +41,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool adc_{false}; #endif bool pdm_{false}; - uint8_t *buffer_; - i2s_channel_fmt_t channel_; - i2s_bits_per_sample_t bits_per_sample_; HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 72455af1b7..dd43d6cb39 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -1,36 +1,53 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ID, CONF_MODE +import esphome.codegen as cg from esphome.components import esp32, speaker +import esphome.config_validation as cv +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_MODE, CONF_TIMEOUT from .. import ( - CONF_I2S_AUDIO_ID, CONF_I2S_DOUT_PIN, - I2SAudioComponent, + CONF_LEFT, + CONF_MONO, + CONF_RIGHT, + CONF_STEREO, I2SAudioOut, + i2s_audio_component_schema, i2s_audio_ns, + register_i2s_audio_component, ) -CODEOWNERS = ["@jesserockz"] +AUTO_LOAD = ["audio"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] DEPENDENCIES = ["i2s_audio"] I2SAudioSpeaker = i2s_audio_ns.class_( "I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut ) -i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") -CONF_MUTE_PIN = "mute_pin" CONF_DAC_TYPE = "dac_type" +CONF_I2S_COMM_FMT = "i2s_comm_fmt" +i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") INTERNAL_DAC_OPTIONS = { - "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, - "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, - "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, + CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, + CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, + CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, } -EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] +i2s_comm_format_t = cg.global_ns.enum("i2s_comm_format_t") +I2C_COMM_FMT_OPTIONS = { + "stand_i2s": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_I2S, + "stand_msb": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MSB, + "stand_pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_SHORT, + "stand_pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_LONG, + "stand_max": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MAX, + "i2s_msb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_MSB, + "i2s_lsb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_LSB, + "pcm": i2s_comm_format_t.I2S_COMM_FORMAT_PCM, + "pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_SHORT, + "pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG, +} NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] @@ -44,28 +61,43 @@ def validate_esp32_variant(config): return config +BASE_SCHEMA = ( + speaker.SPEAKER_SCHEMA.extend( + i2s_audio_component_schema( + I2SAudioSpeaker, + default_sample_rate=16000, + default_channel=CONF_MONO, + default_bits_per_sample="16bit", + ) + ) + .extend( + { + cv.Optional( + CONF_TIMEOUT, default="500ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + CONFIG_SCHEMA = cv.All( cv.typed_schema( { - "internal": speaker.SPEAKER_SCHEMA.extend( + "internal": BASE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(I2SAudioSpeaker), - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), } - ).extend(cv.COMPONENT_SCHEMA), - "external": speaker.SPEAKER_SCHEMA.extend( + ), + "external": BASE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(I2SAudioSpeaker), - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required( CONF_I2S_DOUT_PIN ): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_MODE, default="mono"): cv.one_of( - *EXTERNAL_DAC_OPTIONS, lower=True + cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.enum( + I2C_COMM_FMT_OPTIONS, lower=True ), } - ).extend(cv.COMPONENT_SCHEMA), + ), }, key=CONF_DAC_TYPE, ), @@ -76,12 +108,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + await register_i2s_audio_component(var, config) await speaker.register_speaker(var, config) - await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - if config[CONF_DAC_TYPE] == "internal": - cg.add(var.set_internal_dac_mode(config[CONF_MODE])) + cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) else: cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) + cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT])) + cg.add(var.set_timeout(config[CONF_TIMEOUT])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 5ae597dc7c..cf6c3bbbba 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -4,6 +4,8 @@ #include +#include "esphome/components/audio/audio.h" + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -11,199 +13,516 @@ namespace esphome { namespace i2s_audio { -static const size_t BUFFER_COUNT = 10; +static const size_t DMA_BUFFER_SIZE = 512; +static const size_t DMA_BUFFERS_COUNT = 4; +static const size_t FRAMES_IN_ALL_DMA_BUFFERS = DMA_BUFFER_SIZE * DMA_BUFFERS_COUNT; +static const size_t RING_BUFFER_SAMPLES = 8192; +static const size_t TASK_DELAY_MS = 10; +static const size_t TASK_STACK_SIZE = 4096; +static const ssize_t TASK_PRIORITY = 23; static const char *const TAG = "i2s_audio.speaker"; +enum SpeakerEventGroupBits : uint32_t { + COMMAND_START = (1 << 0), // Starts the main task purpose + COMMAND_STOP = (1 << 1), // stops the main task + COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the task once all data has been written + MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE = (1 << 5), // Locks the ring buffer when not set + STATE_STARTING = (1 << 10), + STATE_RUNNING = (1 << 11), + STATE_STOPPING = (1 << 12), + STATE_STOPPED = (1 << 13), + ERR_INVALID_FORMAT = (1 << 14), + ERR_TASK_FAILED_TO_START = (1 << 15), + ERR_ESP_INVALID_STATE = (1 << 16), + ERR_ESP_INVALID_ARG = (1 << 17), + ERR_ESP_INVALID_SIZE = (1 << 18), + ERR_ESP_NO_MEM = (1 << 19), + ERR_ESP_FAIL = (1 << 20), + ALL_ERR_ESP_BITS = ERR_ESP_INVALID_STATE | ERR_ESP_INVALID_ARG | ERR_ESP_INVALID_SIZE | ERR_ESP_NO_MEM | ERR_ESP_FAIL, + ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits +}; + +// Translates a SpeakerEventGroupBits ERR_ESP bit to the coressponding esp_err_t +static esp_err_t err_bit_to_esp_err(uint32_t bit) { + switch (bit) { + case SpeakerEventGroupBits::ERR_ESP_INVALID_STATE: + return ESP_ERR_INVALID_STATE; + case SpeakerEventGroupBits::ERR_ESP_INVALID_ARG: + return ESP_ERR_INVALID_ARG; + case SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE: + return ESP_ERR_INVALID_SIZE; + case SpeakerEventGroupBits::ERR_ESP_NO_MEM: + return ESP_ERR_NO_MEM; + default: + return ESP_FAIL; + } +} + +/// @brief Multiplies the input array of Q15 numbers by a Q15 constant factor +/// +/// Based on `dsps_mulc_s16_ansi` from the esp-dsp library: +/// https://github.com/espressif/esp-dsp/blob/master/modules/math/mulc/fixed/dsps_mulc_s16_ansi.c +/// (accessed on 2024-09-30). +/// @param input Array of Q15 numbers +/// @param output Array of Q15 numbers +/// @param len Length of array +/// @param c Q15 constant factor +static void q15_multiplication(const int16_t *input, int16_t *output, size_t len, int16_t c) { + for (int i = 0; i < len; i++) { + int32_t acc = (int32_t) input[i] * (int32_t) c; + output[i] = (int16_t) (acc >> 15); + } +} + +// Lists the Q15 fixed point scaling factor for volume reduction. +// Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB. +// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) +// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15) +static const std::vector Q15_VOLUME_SCALING_FACTORS = { + 0, 116, 122, 130, 137, 146, 154, 163, 173, 183, 194, 206, 218, 231, 244, + 259, 274, 291, 308, 326, 345, 366, 388, 411, 435, 461, 488, 517, 548, 580, + 615, 651, 690, 731, 774, 820, 868, 920, 974, 1032, 1094, 1158, 1227, 1300, 1377, + 1459, 1545, 1637, 1734, 1837, 1946, 2061, 2184, 2313, 2450, 2596, 2750, 2913, 3085, 3269, + 3462, 3668, 3885, 4116, 4360, 4619, 4893, 5183, 5490, 5816, 6161, 6527, 6914, 7324, 7758, + 8218, 8706, 9222, 9770, 10349, 10963, 11613, 12302, 13032, 13805, 14624, 15491, 16410, 17384, 18415, + 19508, 20665, 21891, 23189, 24565, 26022, 27566, 29201, 30933, 32767}; + void I2SAudioSpeaker::setup() { ESP_LOGCONFIG(TAG, "Setting up I2S Audio Speaker..."); - this->buffer_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(DataEvent)); - this->event_queue_ = xQueueCreate(20, sizeof(TaskEvent)); -} - -void I2SAudioSpeaker::start() { this->state_ = speaker::STATE_STARTING; } -void I2SAudioSpeaker::start_() { - if (!this->parent_->try_lock()) { - return; // Waiting for another i2s component to return lock - } - this->state_ = speaker::STATE_RUNNING; - - xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 0, &this->player_task_handle_); -} - -void I2SAudioSpeaker::player_task(void *params) { - I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; - - TaskEvent event; - event.type = TaskEventType::STARTING; - xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); - - i2s_driver_config_t config = { - .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX), - .sample_rate = 16000, - .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, - .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, - .communication_format = I2S_COMM_FORMAT_STAND_I2S, - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .dma_buf_count = 8, - .dma_buf_len = 1024, - .use_apll = false, - .tx_desc_auto_clear = true, - .fixed_mclk = I2S_PIN_NO_CHANGE, - .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, - .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, - }; -#if SOC_I2S_SUPPORTS_DAC - if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { - config.mode = (i2s_mode_t) (config.mode | I2S_MODE_DAC_BUILT_IN); - } -#endif - - i2s_driver_install(this_speaker->parent_->get_port(), &config, 0, nullptr); - -#if SOC_I2S_SUPPORTS_DAC - if (this_speaker->internal_dac_mode_ == I2S_DAC_CHANNEL_DISABLE) { -#endif - i2s_pin_config_t pin_config = this_speaker->parent_->get_pin_config(); - pin_config.data_out_num = this_speaker->dout_pin_; - - i2s_set_pin(this_speaker->parent_->get_port(), &pin_config); -#if SOC_I2S_SUPPORTS_DAC - } else { - i2s_set_dac_mode(this_speaker->internal_dac_mode_); - } -#endif - - DataEvent data_event; - - event.type = TaskEventType::STARTED; - xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); - - int16_t buffer[BUFFER_SIZE / 2]; - - while (true) { - if (xQueueReceive(this_speaker->buffer_queue_, &data_event, 100 / portTICK_PERIOD_MS) != pdTRUE) { - break; // End of audio from main thread - } - if (data_event.stop) { - // Stop signal from main thread - while (xQueueReceive(this_speaker->buffer_queue_, &data_event, 0) == pdTRUE) { - // Flush queue - } - break; - } - size_t bytes_written; - - memmove(buffer, data_event.data, data_event.len); - size_t remaining = data_event.len / 2; - size_t current = 0; - - while (remaining > 0) { - uint32_t sample = (buffer[current] << 16) | (buffer[current] & 0xFFFF); - - esp_err_t err = i2s_write(this_speaker->parent_->get_port(), &sample, sizeof(sample), &bytes_written, - (100 / portTICK_PERIOD_MS)); - if (err != ESP_OK) { - event = {.type = TaskEventType::WARNING, .err = err}; - xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); - continue; - } - remaining--; - current++; - } - - event.type = TaskEventType::PLAYING; - xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); + if (this->event_group_ == nullptr) { + this->event_group_ = xEventGroupCreate(); } - i2s_zero_dma_buffer(this_speaker->parent_->get_port()); - - event.type = TaskEventType::STOPPING; - xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); - - i2s_stop(this_speaker->parent_->get_port()); - i2s_driver_uninstall(this_speaker->parent_->get_port()); - - event.type = TaskEventType::STOPPED; - xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); - - while (true) { - delay(10); - } -} - -void I2SAudioSpeaker::stop() { - if (this->state_ == speaker::STATE_STOPPED) + if (this->event_group_ == nullptr) { + ESP_LOGE(TAG, "Failed to create event group"); + this->mark_failed(); return; - if (this->state_ == speaker::STATE_STARTING) { - this->state_ = speaker::STATE_STOPPED; - return; - } - this->state_ = speaker::STATE_STOPPING; - DataEvent data; - data.stop = true; - xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY); -} - -void I2SAudioSpeaker::watch_() { - TaskEvent event; - if (xQueueReceive(this->event_queue_, &event, 0) == pdTRUE) { - switch (event.type) { - case TaskEventType::STARTING: - case TaskEventType::STARTED: - case TaskEventType::STOPPING: - break; - case TaskEventType::PLAYING: - this->status_clear_warning(); - break; - case TaskEventType::STOPPED: - this->parent_->unlock(); - this->state_ = speaker::STATE_STOPPED; - vTaskDelete(this->player_task_handle_); - this->player_task_handle_ = nullptr; - break; - case TaskEventType::WARNING: - ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(event.err)); - this->status_set_warning(); - break; - } } } void I2SAudioSpeaker::loop() { - switch (this->state_) { - case speaker::STATE_STARTING: - this->start_(); - break; - case speaker::STATE_RUNNING: - this->watch_(); - break; - case speaker::STATE_STOPPING: - case speaker::STATE_STOPPED: - break; + uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); + + if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { + ESP_LOGD(TAG, "Starting Speaker"); + this->state_ = speaker::STATE_STARTING; + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING); + } + if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) { + ESP_LOGD(TAG, "Started Speaker"); + this->state_ = speaker::STATE_RUNNING; + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING); + this->status_clear_warning(); + this->status_clear_error(); + } + if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) { + ESP_LOGD(TAG, "Stopping Speaker"); + this->state_ = speaker::STATE_STOPPING; + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING); + } + if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) { + if (!this->task_created_) { + ESP_LOGD(TAG, "Stopped Speaker"); + this->state_ = speaker::STATE_STOPPED; + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); + this->speaker_task_handle_ = nullptr; + } + } + + if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { + this->status_set_error("Failed to start speaker task"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); + } + + if (event_group_bits & SpeakerEventGroupBits::ERR_INVALID_FORMAT) { + this->status_set_error("Failed to adjust I2S bus to match the incoming audio"); + ESP_LOGE(TAG, + "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8, + this->audio_stream_info_.sample_rate, this->audio_stream_info_.channels, + this->audio_stream_info_.bits_per_sample); + } + + if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { + uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; + ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); + this->status_set_warning(); } } -bool I2SAudioSpeaker::play(const uint8_t *data, size_t length) { +void I2SAudioSpeaker::set_volume(float volume) { + this->volume_ = volume; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + if (volume > 0.0) { + this->audio_dac_->set_mute_off(); + } + this->audio_dac_->set_volume(volume); + } else +#endif + { + // Fallback to software volume control by using a Q15 fixed point scaling factor + ssize_t decibel_index = remap(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); + this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; + } +} + +void I2SAudioSpeaker::set_mute_state(bool mute_state) { + this->mute_state_ = mute_state; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_) { + if (mute_state) { + this->audio_dac_->set_mute_on(); + } else { + this->audio_dac_->set_mute_off(); + } + } else +#endif + { + if (mute_state) { + // Fallback to software volume control and scale by 0 + this->q15_volume_factor_ = 0; + } else { + // Revert to previous volume when unmuting + this->set_volume(this->volume_); + } + } +} + +size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { + if (this->is_failed()) { + ESP_LOGE(TAG, "Cannot play audio, speaker failed to setup"); + return 0; + } if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { this->start(); } - size_t remaining = length; - size_t index = 0; - while (remaining > 0) { - DataEvent event; - event.stop = false; - size_t to_send_length = std::min(remaining, BUFFER_SIZE); - event.len = to_send_length; - memcpy(event.data, data + index, to_send_length); - if (xQueueSend(this->buffer_queue_, &event, 100 / portTICK_PERIOD_MS) == pdTRUE) { - remaining -= to_send_length; - index += to_send_length; - } - App.feed_wdt(); + + // Wait for the ring buffer to be available + uint32_t event_bits = + xEventGroupWaitBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE, pdFALSE, + pdFALSE, pdMS_TO_TICKS(TASK_DELAY_MS)); + + if (event_bits & SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE) { + // Ring buffer is available to write + + // Lock the ring buffer, write to it, then unlock it + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE); + size_t bytes_written = this->audio_ring_buffer_->write_without_replacement((void *) data, length, ticks_to_wait); + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE); + + return bytes_written; } - return true; + + return 0; +} + +bool I2SAudioSpeaker::has_buffered_data() const { + if (this->audio_ring_buffer_ != nullptr) { + return this->audio_ring_buffer_->available() > 0; + } + return false; +} + +void I2SAudioSpeaker::speaker_task(void *params) { + I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; + uint32_t event_group_bits = + xEventGroupWaitBits(this_speaker->event_group_, + SpeakerEventGroupBits::COMMAND_START | SpeakerEventGroupBits::COMMAND_STOP | + SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY, // Bit message to read + pdTRUE, // Clear the bits on exit + pdFALSE, // Don't wait for all the bits, + portMAX_DELAY); // Block indefinitely until a bit is set + + if (event_group_bits & (SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY)) { + // Received a stop signal before the task was requested to start + this_speaker->delete_task_(0); + } + + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STARTING); + + audio::AudioStreamInfo audio_stream_info = this_speaker->audio_stream_info_; + const ssize_t bytes_per_sample = audio_stream_info.get_bytes_per_sample(); + const uint8_t number_of_channels = audio_stream_info.channels; + + const size_t dma_buffers_size = FRAMES_IN_ALL_DMA_BUFFERS * bytes_per_sample * number_of_channels; + + if (this_speaker->send_esp_err_to_event_group_( + this_speaker->allocate_buffers_(dma_buffers_size, RING_BUFFER_SAMPLES * bytes_per_sample))) { + // Failed to allocate buffers + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); + this_speaker->delete_task_(dma_buffers_size); + } + + if (this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_())) { + // Failed to start I2S driver + this_speaker->delete_task_(dma_buffers_size); + } else { + // Ring buffer is allocated, so indicate its can be written to + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE); + } + + if (!this_speaker->send_esp_err_to_event_group_(this_speaker->reconfigure_i2s_stream_info_(audio_stream_info))) { + // Successfully set the I2S stream info, ready to write audio data to the I2S port + + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_RUNNING); + + bool stop_gracefully = false; + uint32_t last_data_received_time = millis(); + + while ((millis() - last_data_received_time) <= this_speaker->timeout_) { + event_group_bits = xEventGroupGetBits(this_speaker->event_group_); + + if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { + break; + } + if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) { + stop_gracefully = true; + } + + size_t bytes_to_read = dma_buffers_size; + size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, bytes_to_read, + pdMS_TO_TICKS(TASK_DELAY_MS)); + + if (bytes_read > 0) { + last_data_received_time = millis(); + size_t bytes_written = 0; + + if ((audio_stream_info.bits_per_sample == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) { + // Scale samples by the volume factor in place + q15_multiplication((int16_t *) this_speaker->data_buffer_, (int16_t *) this_speaker->data_buffer_, + bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_); + } + + if (audio_stream_info.bits_per_sample == (uint8_t) this_speaker->bits_per_sample_) { + i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_, bytes_read, &bytes_written, + portMAX_DELAY); + } else if (audio_stream_info.bits_per_sample < (uint8_t) this_speaker->bits_per_sample_) { + i2s_write_expand(this_speaker->parent_->get_port(), this_speaker->data_buffer_, bytes_read, + audio_stream_info.bits_per_sample, this_speaker->bits_per_sample_, &bytes_written, + portMAX_DELAY); + } + + if (bytes_written != bytes_read) { + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); + } + + } else { + // No data received + + if (stop_gracefully) { + break; + } + + i2s_zero_dma_buffer(this_speaker->parent_->get_port()); + } + } + } else { + // Couldn't configure the I2S port to be compatible with the incoming audio + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_INVALID_FORMAT); + } + i2s_zero_dma_buffer(this_speaker->parent_->get_port()); + + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING); + + i2s_stop(this_speaker->parent_->get_port()); + i2s_driver_uninstall(this_speaker->parent_->get_port()); + + this_speaker->parent_->unlock(); + this_speaker->delete_task_(dma_buffers_size); +} + +void I2SAudioSpeaker::start() { + if (this->is_failed() || this->status_has_error()) + return; + if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) + return; + + if (this->speaker_task_handle_ == nullptr) { + xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, + &this->speaker_task_handle_); + } + + if (this->speaker_task_handle_ != nullptr) { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); + this->task_created_ = true; + } else { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); + } +} + +void I2SAudioSpeaker::stop() { this->stop_(false); } + +void I2SAudioSpeaker::finish() { this->stop_(true); } + +void I2SAudioSpeaker::stop_(bool wait_on_empty) { + if (this->is_failed()) + return; + if (this->state_ == speaker::STATE_STOPPED) + return; + + if (wait_on_empty) { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); + } else { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); + } +} + +bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) { + switch (err) { + case ESP_OK: + return false; + case ESP_ERR_INVALID_STATE: + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_STATE); + return true; + case ESP_ERR_INVALID_ARG: + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_ARG); + return true; + case ESP_ERR_INVALID_SIZE: + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); + return true; + case ESP_ERR_NO_MEM: + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); + return true; + default: + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_FAIL); + return true; + } +} + +esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) { + if (this->data_buffer_ == nullptr) { + // Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->data_buffer_ = allocator.allocate(data_buffer_size); + } + + if (this->data_buffer_ == nullptr) { + return ESP_ERR_NO_MEM; + } + + if (this->audio_ring_buffer_ == nullptr) { + // Allocate ring buffer + this->audio_ring_buffer_ = RingBuffer::create(ring_buffer_size); + } + + if (this->audio_ring_buffer_ == nullptr) { + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + +esp_err_t I2SAudioSpeaker::start_i2s_driver_() { + if (!this->parent_->try_lock()) { + return ESP_ERR_INVALID_STATE; + } + + i2s_driver_config_t config = { + .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX), + .sample_rate = this->sample_rate_, + .bits_per_sample = this->bits_per_sample_, + .channel_format = this->channel_, + .communication_format = this->i2s_comm_fmt_, + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .dma_buf_count = DMA_BUFFERS_COUNT, + .dma_buf_len = DMA_BUFFER_SIZE, + .use_apll = this->use_apll_, + .tx_desc_auto_clear = true, + .fixed_mclk = I2S_PIN_NO_CHANGE, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + .bits_per_chan = this->bits_per_channel_, +#if SOC_I2S_SUPPORTS_TDM + .chan_mask = (i2s_channel_t) (I2S_TDM_ACTIVE_CH0 | I2S_TDM_ACTIVE_CH1), + .total_chan = 2, + .left_align = false, + .big_edin = false, + .bit_order_msb = false, + .skip_msk = false, +#endif + }; +#if SOC_I2S_SUPPORTS_DAC + if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { + config.mode = (i2s_mode_t) (config.mode | I2S_MODE_DAC_BUILT_IN); + } +#endif + + esp_err_t err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + if (err != ESP_OK) { + // Failed to install the driver, so unlock the I2S port + this->parent_->unlock(); + return err; + } + +#if SOC_I2S_SUPPORTS_DAC + if (this->internal_dac_mode_ == I2S_DAC_CHANNEL_DISABLE) { +#endif + i2s_pin_config_t pin_config = this->parent_->get_pin_config(); + pin_config.data_out_num = this->dout_pin_; + + err = i2s_set_pin(this->parent_->get_port(), &pin_config); +#if SOC_I2S_SUPPORTS_DAC + } else { + i2s_set_dac_mode(this->internal_dac_mode_); + } +#endif + + if (err != ESP_OK) { + // Failed to set the data out pin, so uninstall the driver and unlock the I2S port + i2s_driver_uninstall(this->parent_->get_port()); + this->parent_->unlock(); + } + + return err; +} + +esp_err_t I2SAudioSpeaker::reconfigure_i2s_stream_info_(audio::AudioStreamInfo &audio_stream_info) { + if (this->i2s_mode_ & I2S_MODE_MASTER) { + // ESP controls for the the I2S bus, so adjust the sample rate and bits per sample to match the incoming audio + this->sample_rate_ = audio_stream_info.sample_rate; + this->bits_per_sample_ = (i2s_bits_per_sample_t) audio_stream_info.bits_per_sample; + } else if (this->sample_rate_ != audio_stream_info.sample_rate) { + // Can't reconfigure I2S bus, so the sample rate must match the configured value + return ESP_ERR_INVALID_ARG; + } + + if ((i2s_bits_per_sample_t) audio_stream_info.bits_per_sample > this->bits_per_sample_) { + // Currently can't handle the case when the incoming audio has more bits per sample than the configured value + return ESP_ERR_INVALID_ARG; + } + + if (audio_stream_info.channels == 1) { + return i2s_set_clk(this->parent_->get_port(), this->sample_rate_, this->bits_per_sample_, I2S_CHANNEL_MONO); + } else if (audio_stream_info.channels == 2) { + return i2s_set_clk(this->parent_->get_port(), this->sample_rate_, this->bits_per_sample_, I2S_CHANNEL_STEREO); + } + + return ESP_ERR_INVALID_ARG; +} + +void I2SAudioSpeaker::delete_task_(size_t buffer_size) { + if (this->audio_ring_buffer_ != nullptr) { + xEventGroupWaitBits(this->event_group_, + MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE, // Bit message to read + pdFALSE, // Don't clear the bits on exit + pdTRUE, // Don't wait for all the bits, + portMAX_DELAY); // Block indefinitely until a command bit is set + + this->audio_ring_buffer_.reset(); // Deallocates the ring buffer stored in the unique_ptr + this->audio_ring_buffer_ = nullptr; + } + + if (this->data_buffer_ != nullptr) { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(this->data_buffer_, buffer_size); + this->data_buffer_ = nullptr; + } + + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED); + + this->task_created_ = false; + vTaskDelete(nullptr); } } // namespace i2s_audio diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 4f1d2172d7..3c512d4d4d 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -5,74 +5,132 @@ #include "../i2s_audio.h" #include -#include -#include +#include +#include + +#include "esphome/components/audio/audio.h" #include "esphome/components/speaker/speaker.h" + #include "esphome/core/component.h" #include "esphome/core/gpio.h" #include "esphome/core/helpers.h" +#include "esphome/core/ring_buffer.h" namespace esphome { namespace i2s_audio { -static const size_t BUFFER_SIZE = 1024; - -enum class TaskEventType : uint8_t { - STARTING = 0, - STARTED, - PLAYING, - STOPPING, - STOPPED, - WARNING = 255, -}; - -struct TaskEvent { - TaskEventType type; - esp_err_t err; -}; - -struct DataEvent { - bool stop; - size_t len; - uint8_t data[BUFFER_SIZE]; -}; - -class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAudioOut { +class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Component { public: float get_setup_priority() const override { return esphome::setup_priority::LATE; } void setup() override; void loop() override; + void set_timeout(uint32_t ms) { this->timeout_ = ms; } void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } #if SOC_I2S_SUPPORTS_DAC void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } #endif - void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } + void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; } - void start(); + void start() override; void stop() override; + void finish() override; - bool play(const uint8_t *data, size_t length) override; + /// @brief Plays the provided audio data. + /// Starts the speaker task, if necessary. Writes the audio data to the ring buffer. + /// @param data Audio data in the format set by the parent speaker classes ``set_audio_stream_info`` method. + /// @param length The length of the audio data in bytes. + /// @param ticks_to_wait The FreeRTOS ticks to wait before writing as much data as possible to the ring buffer. + /// @return The number of bytes that were actually written to the ring buffer. + size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override; + size_t play(const uint8_t *data, size_t length) override { return play(data, length, 0); } + + bool has_buffered_data() const override; + + /// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is + /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a + /// Q15 fixed-point factor. + /// @param volume between 0.0 and 1.0 + void set_volume(float volume) override; + + /// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is + /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a + /// Q15 fixed-point factor. + /// @param mute_state true for muting, false for unmuting + void set_mute_state(bool mute_state) override; protected: - void start_(); - // void stop_(); - void watch_(); + /// @brief Function for the FreeRTOS task handling audio output. + /// After receiving the COMMAND_START signal, allocates space for the buffers, starts the I2S driver, and reads + /// audio from the ring buffer and writes audio to the I2S port. Stops immmiately after receiving the COMMAND_STOP + /// signal and stops only after the ring buffer is empty after receiving the COMMAND_STOP_GRACEFULLY signal. Stops if + /// the ring buffer hasn't read data for more than timeout_ milliseconds. When stopping, it deallocates the buffers, + /// stops the I2S driver, unlocks the I2S port, and deletes the task. It communicates the state and any errors via + /// event_group_. + /// @param params I2SAudioSpeaker component + static void speaker_task(void *params); - static void player_task(void *params); + /// @brief Sends a stop command to the speaker task via event_group_. + /// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal. + void stop_(bool wait_on_empty); - TaskHandle_t player_task_handle_{nullptr}; - QueueHandle_t buffer_queue_; - QueueHandle_t event_queue_; + /// @brief Sets the corresponding ERR_ESP event group bits. + /// @param err esp_err_t error code. + /// @return True if an ERR_ESP bit is set and false if err == ESP_OK + bool send_esp_err_to_event_group_(esp_err_t err); - uint8_t dout_pin_{0}; + /// @brief Allocates the data buffer and ring buffer + /// @param data_buffer_size Number of bytes to allocate for the data buffer. + /// @param ring_buffer_size Number of bytes to allocate for the ring buffer. + /// @return ESP_ERR_NO_MEM if either buffer fails to allocate + /// ESP_OK if successful + esp_err_t allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size); + + /// @brief Starts the ESP32 I2S driver. + /// Attempts to lock the I2S port, starts the I2S driver, and sets the data out pin. If it fails, it will unlock + /// the I2S port and uninstall the driver, if necessary. + /// @return ESP_ERR_INVALID_STATE if the I2S port is already locked. + /// ESP_ERR_INVALID_ARG if installing the driver or setting the data out pin fails due to a parameter error. + /// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error. + /// ESP_FAIL if setting the data out pin fails due to an IO error + /// ESP_OK if successful + esp_err_t start_i2s_driver_(); + + /// @brief Adjusts the I2S driver configuration to match the incoming audio stream. + /// Modifies I2S driver's sample rate, bits per sample, and number of channel settings. If the I2S is in secondary + /// mode, it only modifies the number of channels. + /// @param audio_stream_info Describes the incoming audio stream + /// @return ESP_ERR_INVALID_ARG if there is a parameter error, if there is more than 2 channels in the stream, or if + /// the audio settings are incompatible with the configuration. + /// ESP_ERR_NO_MEM if the driver fails to reconfigure due to a memory allocation error. + /// ESP_OK if successful. + esp_err_t reconfigure_i2s_stream_info_(audio::AudioStreamInfo &audio_stream_info); + + /// @brief Deletes the speaker's task. + /// Deallocates the data_buffer_ and audio_ring_buffer_, if necessary, and deletes the task. Should only be called by + /// the speaker_task itself. + /// @param buffer_size The allocated size of the data_buffer_. + void delete_task_(size_t buffer_size); + + TaskHandle_t speaker_task_handle_{nullptr}; + EventGroupHandle_t event_group_{nullptr}; + + uint8_t *data_buffer_; + std::unique_ptr audio_ring_buffer_; + + uint32_t timeout_; + uint8_t dout_pin_; + + bool task_created_{false}; + + int16_t q15_volume_factor_{INT16_MAX}; #if SOC_I2S_SUPPORTS_DAC i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; #endif - uint8_t external_dac_channels_; + i2s_comm_format_t i2s_comm_fmt_; }; } // namespace i2s_audio diff --git a/esphome/components/iaqcore/__init__.py b/esphome/components/iaqcore/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp new file mode 100644 index 0000000000..810e8da0b2 --- /dev/null +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -0,0 +1,99 @@ +#include "iaqcore.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace iaqcore { + +static const char *const TAG = "iaqcore"; + +enum IAQCoreErrorCode : uint8_t { ERROR_OK = 0, ERROR_RUNIN = 0x10, ERROR_BUSY = 0x01, ERROR_ERROR = 0x80 }; + +struct SensorData { + uint16_t co2; + IAQCoreErrorCode status; + int32_t resistance; + uint16_t tvoc; + + SensorData(const uint8_t *buffer) { + this->co2 = encode_uint16(buffer[0], buffer[1]); + this->status = static_cast(buffer[2]); + this->resistance = encode_uint32(buffer[3], buffer[4], buffer[5], buffer[6]); + this->tvoc = encode_uint16(buffer[7], buffer[8]); + } +}; + +void IAQCore::setup() { + if (this->write(nullptr, 0) != i2c::ERROR_OK) { + ESP_LOGD(TAG, "Communication failed!"); + this->mark_failed(); + return; + } +} + +void IAQCore::update() { + uint8_t buffer[sizeof(SensorData)]; + + if (this->read_register(0xB5, buffer, sizeof(buffer), false) != i2c::ERROR_OK) { + ESP_LOGD(TAG, "Read failed"); + this->status_set_warning(); + this->publish_nans_(); + return; + } + + SensorData data(buffer); + + switch (data.status) { + case ERROR_OK: + ESP_LOGD(TAG, "OK"); + break; + case ERROR_RUNIN: + ESP_LOGI(TAG, "Warming up"); + break; + case ERROR_BUSY: + ESP_LOGI(TAG, "Busy"); + break; + case ERROR_ERROR: + ESP_LOGE(TAG, "Error"); + break; + } + + if (data.status != ERROR_OK) { + this->status_set_warning(); + this->publish_nans_(); + return; + } + + if (this->co2_ != nullptr) { + this->co2_->publish_state(data.co2); + } + if (this->tvoc_ != nullptr) { + this->tvoc_->publish_state(data.tvoc); + } + + this->status_clear_warning(); +} + +void IAQCore::publish_nans_() { + if (this->co2_ != nullptr) { + this->co2_->publish_state(NAN); + } + if (this->tvoc_ != nullptr) { + this->tvoc_->publish_state(NAN); + } +} + +void IAQCore::dump_config() { + ESP_LOGCONFIG(TAG, "AMS iAQ Core:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AMS iAQ Core failed!"); + } + LOG_SENSOR(" ", "CO2", this->co2_); + LOG_SENSOR(" ", "TVOC", this->tvoc_); +} + +} // namespace iaqcore +} // namespace esphome diff --git a/esphome/components/iaqcore/iaqcore.h b/esphome/components/iaqcore/iaqcore.h new file mode 100644 index 0000000000..f343c2a705 --- /dev/null +++ b/esphome/components/iaqcore/iaqcore.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace iaqcore { + +class IAQCore : public PollingComponent, public i2c::I2CDevice { + public: + void set_co2(sensor::Sensor *co2) { co2_ = co2; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + + void setup() override; + void update() override; + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + sensor::Sensor *co2_{nullptr}; + sensor::Sensor *tvoc_{nullptr}; + + void publish_nans_(); +}; + +} // namespace iaqcore +} // namespace esphome diff --git a/esphome/components/iaqcore/sensor.py b/esphome/components/iaqcore/sensor.py new file mode 100644 index 0000000000..51c5b283b7 --- /dev/null +++ b/esphome/components/iaqcore/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_CO2, + CONF_ID, + CONF_TVOC, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, + UNIT_PARTS_PER_BILLION, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@yozik04"] + + +iaqcore_ns = cg.esphome_ns.namespace("iaqcore") +iAQCore = iaqcore_ns.class_("IAQCore", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(iAQCore), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5A)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if co2_config := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2_config) + cg.add(var.set_co2(sens)) + + if tvoc_config := config.get(CONF_TVOC): + sens = await sensor.new_sensor(tvoc_config) + cg.add(var.set_tvoc(sens)) + + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 780c64ec70..739ad07843 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -1,19 +1,32 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import core, pins -from esphome.components import display, spi -from esphome.core import CORE, HexInt +import esphome.codegen as cg +from esphome.components import display, font, spi +from esphome.components.display import validate_rotation +import esphome.config_validation as cv from esphome.const import ( + CONF_COLOR_ORDER, CONF_COLOR_PALETTE, CONF_DC_PIN, - CONF_ID, - CONF_LAMBDA, - CONF_MODEL, - CONF_RAW_DATA_ID, - CONF_PAGES, - CONF_RESET_PIN, CONF_DIMENSIONS, + CONF_HEIGHT, + CONF_ID, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_PAGES, + CONF_RAW_DATA_ID, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_WIDTH, ) +from esphome.core import CORE, HexInt DEPENDENCIES = ["spi"] @@ -24,40 +37,88 @@ def AUTO_LOAD(): return [] -CODEOWNERS = ["@nielsnl68"] +CODEOWNERS = ["@nielsnl68", "@clydebarrow"] -ili9XXX_ns = cg.esphome_ns.namespace("ili9xxx") -ili9XXXSPI = ili9XXX_ns.class_( - "ILI9XXXDisplay", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") +ILI9XXXDisplay = ili9xxx_ns.class_( + "ILI9XXXDisplay", + cg.PollingComponent, + spi.SPIDevice, + display.Display, + display.DisplayBuffer, ) -ILI9XXXColorMode = ili9XXX_ns.enum("ILI9XXXColorMode") +PixelMode = ili9xxx_ns.enum("PixelMode") +PIXEL_MODES = { + "16bit": PixelMode.PIXEL_MODE_16, + "18bit": PixelMode.PIXEL_MODE_18, +} + +ILI9XXXColorMode = ili9xxx_ns.enum("ILI9XXXColorMode") +ColorOrder = display.display_ns.enum("ColorMode") MODELS = { - "M5STACK": ili9XXX_ns.class_("ILI9XXXM5Stack", ili9XXXSPI), - "M5CORE": ili9XXX_ns.class_("ILI9XXXM5CORE", ili9XXXSPI), - "TFT_2.4": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), - "TFT_2.4R": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), - "ILI9341": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), - "ILI9342": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), - "ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI), - "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), - "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), - "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), + "GC9A01A": ili9xxx_ns.class_("ILI9XXXGC9A01A", ILI9XXXDisplay), + "M5STACK": ili9xxx_ns.class_("ILI9XXXM5Stack", ILI9XXXDisplay), + "M5CORE": ili9xxx_ns.class_("ILI9XXXM5CORE", ILI9XXXDisplay), + "TFT_2.4": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay), + "TFT_2.4R": ili9xxx_ns.class_("ILI9XXXILI9342", ILI9XXXDisplay), + "ILI9341": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay), + "ILI9342": ili9xxx_ns.class_("ILI9XXXILI9342", ILI9XXXDisplay), + "ILI9481": ili9xxx_ns.class_("ILI9XXXILI9481", ILI9XXXDisplay), + "ILI9481-18": ili9xxx_ns.class_("ILI9XXXILI948118", ILI9XXXDisplay), + "ILI9486": ili9xxx_ns.class_("ILI9XXXILI9486", ILI9XXXDisplay), + "ILI9488": ili9xxx_ns.class_("ILI9XXXILI9488", ILI9XXXDisplay), + "ILI9488_A": ili9xxx_ns.class_("ILI9XXXILI9488A", ILI9XXXDisplay), + "ST7735": ili9xxx_ns.class_("ILI9XXXST7735", ILI9XXXDisplay), + "ST7796": ili9xxx_ns.class_("ILI9XXXST7796", ILI9XXXDisplay), + "ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay), + "S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay), + "S3BOX_LITE": ili9xxx_ns.class_("ILI9XXXS3BoxLite", ILI9XXXDisplay), + "WAVESHARE_RES_3_5": ili9xxx_ns.class_("WAVESHARERES35", ILI9XXXDisplay), + "CUSTOM": ILI9XXXDisplay, +} + +COLOR_ORDERS = { + "RGB": ColorOrder.COLOR_ORDER_RGB, + "BGR": ColorOrder.COLOR_ORDER_BGR, } COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" +CONF_INVERT_DISPLAY = "invert_display" +CONF_PIXEL_MODE = "pixel_mode" + + +def cmd(c, *args): + """ + Create a command sequence + :param c: The command (8 bit) + :param args: zero or more arguments (8 bit values) + :return: a list with the command, the argument count and the arguments + """ + return [c, len(args)] + list(args) + + +def map_sequence(value): + """ + An initialisation sequence is a literal array of data bytes. + The format is a repeated sequence of [CMD, ] + """ + if len(value) == 0: + raise cv.Invalid("Empty sequence") + return cmd(*value) def _validate(config): - if config.get(CONF_COLOR_PALETTE) == "IMAGE_ADAPTIVE" and not config.get( - CONF_COLOR_PALETTE_IMAGES + if ( + config.get(CONF_COLOR_PALETTE) == "IMAGE_ADAPTIVE" + and CONF_COLOR_PALETTE_IMAGES not in config ): raise cv.Invalid( - "Color palette in IMAGE_ADAPTIVE mode requires at least one 'color_palette_images' entry to generate palette" + "IMAGE_ADAPTIVE palette requires at least one 'color_palette_images' entry" ) if ( config.get(CONF_COLOR_PALETTE_IMAGES) @@ -66,25 +127,43 @@ def _validate(config): raise cv.Invalid( "Providing color palette images requires palette mode to be 'IMAGE_ADAPTIVE'" ) - if CORE.is_esp8266 and config.get(CONF_MODEL) not in [ + model = config[CONF_MODEL] + if CORE.is_esp8266 and model not in [ "M5STACK", "TFT_2.4", "TFT_2.4R", "ILI9341", "ILI9342", + "ST7789V", + "ST7735", ]: - raise cv.Invalid( - "Provided model can't run on ESP8266. Use an ESP32 with PSRAM onboard" - ) + raise cv.Invalid("Selected model can't run on ESP8266.") + + if model == "CUSTOM": + if CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config: + raise cv.Invalid("CUSTOM model requires init_sequence and dimensions") + return config CONFIG_SCHEMA = cv.All( + font.validate_pillow_installed, display.FULL_DISPLAY_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(ili9XXXSPI), + cv.GenerateID(): cv.declare_id(ILI9XXXDisplay), cv.Required(CONF_MODEL): cv.enum(MODELS, upper=True, space="_"), - cv.Optional(CONF_DIMENSIONS): cv.dimensions, + cv.Optional(CONF_PIXEL_MODE): cv.enum(PIXEL_MODES), + cv.Optional(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_, + cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_, + } + ), + ), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_LED_PIN): cv.invalid( @@ -95,28 +174,60 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list( cv.file_ ), + cv.Optional(CONF_INVERT_DISPLAY): cv.invalid( + "'invert_display' has been replaced by 'invert_colors'" + ), + cv.Required(CONF_INVERT_COLORS): cv.boolean, + cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True), + cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation, + cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), + cv.Optional(CONF_INIT_SEQUENCE): cv.ensure_list(map_sequence), } ) .extend(cv.polling_component_schema("1s")) - .extend(spi.spi_device_schema(False)), + .extend(spi.spi_device_schema(False, "40MHz")), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), _validate, ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ili9xxx", require_miso=False, require_mosi=True +) + async def to_code(config): rhs = MODELS[config[CONF_MODEL]].new() var = cg.Pvariable(config[CONF_ID], rhs) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) + if init_sequences := config.get(CONF_INIT_SEQUENCE): + sequence = [] + for seq in init_sequences: + sequence.extend(seq) + cg.add(var.add_init_sequence(sequence)) + + if pixel_mode := config.get(CONF_PIXEL_MODE): + cg.add(var.set_pixel_mode(pixel_mode)) + if CONF_COLOR_ORDER in config: + cg.add(var.set_color_order(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) @@ -125,9 +236,17 @@ async def to_code(config): cg.add(var.set_reset_pin(reset)) if CONF_DIMENSIONS in config: - cg.add( - var.set_dimentions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]) - ) + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT])) + cg.add( + var.set_offsets( + dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT] + ) + ) + else: + (width, height) = dimensions + cg.add(var.set_dimensions(width, height)) rhs = None if config[CONF_COLOR_PALETTE] == "GRAYSCALE": @@ -135,8 +254,6 @@ async def to_code(config): rhs = [] for x in range(256): rhs.extend([HexInt(x), HexInt(x), HexInt(x)]) - prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.add(var.set_palette(prog_arr)) elif config[CONF_COLOR_PALETTE] == "IMAGE_ADAPTIVE": cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8_INDEXED)) from PIL import Image @@ -160,7 +277,7 @@ async def to_code(config): x = x + i.width # reduce the colors on combined image to 256. - converted = ref_image.convert("P", palette=Image.ADAPTIVE, colors=256) + converted = ref_image.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) # if you want to verify how the images look use # ref_image.save("ref_in.png") # converted.save("ref_out.png") @@ -173,3 +290,5 @@ async def to_code(config): if rhs is not None: prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_palette(prog_arr)) + + cg.add(var.invert_colors(config[CONF_INVERT_COLORS])) diff --git a/esphome/components/ili9xxx/ili9xxx_defines.h b/esphome/components/ili9xxx/ili9xxx_defines.h index 29483ee15e..f4c5aad957 100644 --- a/esphome/components/ili9xxx/ili9xxx_defines.h +++ b/esphome/components/ili9xxx/ili9xxx_defines.h @@ -70,6 +70,7 @@ static const uint8_t ILI9XXX_PWCTR2 = 0xC1; static const uint8_t ILI9XXX_PWCTR3 = 0xC2; static const uint8_t ILI9XXX_PWCTR4 = 0xC3; static const uint8_t ILI9XXX_PWCTR5 = 0xC4; +static const uint8_t ILI9XXX_PWCTR6 = 0xF6; static const uint8_t ILI9XXX_VMCTR1 = 0xC5; static const uint8_t ILI9XXX_IFCTR = 0xC6; static const uint8_t ILI9XXX_VMCTR2 = 0xC7; @@ -91,6 +92,9 @@ static const uint8_t ILI9XXX_GMCTRN1 = 0xE1; static const uint8_t ILI9XXX_CSCON = 0xF0; static const uint8_t ILI9XXX_ADJCTL3 = 0xF7; +static const uint8_t ILI9XXX_DELAY_FLAG = 0xFF; +// special marker for delay - command byte reprents ms, length byte is an impossible value +#define ILI9XXX_DELAY(ms) ((uint8_t) ((ms) | 0x80)), ILI9XXX_DELAY_FLAG } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 67d643fe31..81976dd2c9 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -7,16 +7,63 @@ namespace esphome { namespace ili9xxx { -static const char *const TAG = "ili9xxx"; +static const uint16_t SPI_SETUP_US = 100; // estimated fixed overhead in microseconds for an SPI write +static const uint16_t SPI_MAX_BLOCK_SIZE = 4092; // Max size of continuous SPI transfer + +// store a 16 bit value in a buffer, big endian. +static inline void put16_be(uint8_t *buf, uint16_t value) { + buf[0] = value >> 8; + buf[1] = value; +} + +void ILI9XXXDisplay::set_madctl() { + // custom x/y transform and color order + uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB; + if (this->swap_xy_) + mad |= MADCTL_MV; + if (this->mirror_x_) + mad |= MADCTL_MX; + if (this->mirror_y_) + mad |= MADCTL_MY; + this->command(ILI9XXX_MADCTL); + this->data(mad); + esph_log_d(TAG, "Wrote MADCTL 0x%02X", mad); +} void ILI9XXXDisplay::setup() { - this->setup_pins_(); - this->initialize(); + ESP_LOGD(TAG, "Setting up ILI9xxx"); + this->setup_pins_(); + this->init_lcd_(this->init_sequence_); + this->init_lcd_(this->extra_init_sequence_.data()); + switch (this->pixel_mode_) { + case PIXEL_MODE_16: + if (this->is_18bitdisplay_) { + this->command(ILI9XXX_PIXFMT); + this->data(0x55); + this->is_18bitdisplay_ = false; + } + break; + case PIXEL_MODE_18: + if (!this->is_18bitdisplay_) { + this->command(ILI9XXX_PIXFMT); + this->data(0x66); + this->is_18bitdisplay_ = true; + } + break; + default: + break; + } + + this->set_madctl(); + this->command(this->pre_invertcolors_ ? ILI9XXX_INVON : ILI9XXX_INVOFF); this->x_low_ = this->width_; this->y_low_ = this->height_; this->x_high_ = 0; this->y_high_ = 0; +} + +void ILI9XXXDisplay::alloc_buffer_() { if (this->buffer_color_mode_ == BITS_16) { this->init_internal_(this->get_buffer_length_() * 2); if (this->buffer_ != nullptr) { @@ -45,6 +92,8 @@ void ILI9XXXDisplay::setup_pins_() { void ILI9XXXDisplay::dump_config() { LOG_DISPLAY("", "ili9xxx", this); + ESP_LOGCONFIG(TAG, " Width Offset: %u", this->offset_x_); + ESP_LOGCONFIG(TAG, " Height Offset: %u", this->offset_y_); switch (this->buffer_color_mode_) { case BITS_8_INDEXED: ESP_LOGCONFIG(TAG, " Color mode: 8bit Indexed"); @@ -59,10 +108,17 @@ void ILI9XXXDisplay::dump_config() { if (this->is_18bitdisplay_) { ESP_LOGCONFIG(TAG, " 18-Bit Mode: YES"); } + ESP_LOGCONFIG(TAG, " Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); + ESP_LOGCONFIG(TAG, " Color order: %s", this->color_order_ == display::COLOR_ORDER_BGR ? "BGR" : "RGB"); + ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_)); + 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->pre_invertcolors_)); if (this->is_failed()) { ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!"); @@ -73,6 +129,8 @@ void ILI9XXXDisplay::dump_config() { float ILI9XXXDisplay::get_setup_priority() const { return setup_priority::HARDWARE; } void ILI9XXXDisplay::fill(Color color) { + if (!this->check_buffer_()) + return; uint16_t new_color = 0; this->x_low_ = 0; this->y_low_ = 0; @@ -90,7 +148,6 @@ void ILI9XXXDisplay::fill(Color color) { // Upper and lower is equal can use quicker memset operation. Takes ~20ms. memset(this->buffer_, (uint8_t) new_color, buffer_length_16_bits); } else { - // Slower set of both buffers. Takes ~30ms. for (uint32_t i = 0; i < buffer_length_16_bits; i = i + 2) { this->buffer_[i] = (uint8_t) (new_color >> 8); this->buffer_[i + 1] = (uint8_t) new_color; @@ -98,7 +155,6 @@ void ILI9XXXDisplay::fill(Color color) { } } return; - break; default: new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); break; @@ -110,6 +166,8 @@ void HOT ILI9XXXDisplay::draw_absolute_pixel_internal(int x, int y, Color color) if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { return; } + if (!this->check_buffer_()) + return; uint32_t pos = (y * width_) + x; uint16_t new_color; bool updated = false; @@ -138,12 +196,14 @@ void HOT ILI9XXXDisplay::draw_absolute_pixel_internal(int x, int y, Color color) } if (updated) { // low and high watermark may speed up drawing from buffer - this->x_low_ = (x < this->x_low_) ? x : this->x_low_; - this->y_low_ = (y < this->y_low_) ? y : this->y_low_; - this->x_high_ = (x > this->x_high_) ? x : this->x_high_; - this->y_high_ = (y > this->y_high_) ? y : this->y_high_; - // ESP_LOGVV(TAG, "=>>> pixel (x:%d, y:%d) (xl:%d, xh:%d, yl:%d, yh:%d", x, y, this->x_low_, this->x_high_, - // this->y_low_, this->y_high_); + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; } } @@ -152,71 +212,90 @@ void ILI9XXXDisplay::update() { this->need_update_ = true; return; } + this->prossing_update_ = true; do { - this->prossing_update_ = true; this->need_update_ = false; - if (!this->need_update_) { - this->do_update_(); - } + this->do_update_(); } while (this->need_update_); this->prossing_update_ = false; this->display_(); } void ILI9XXXDisplay::display_() { - // we will only update the changed window to the display - uint16_t w = this->x_high_ - this->x_low_ + 1; // NOLINT - uint16_t h = this->y_high_ - this->y_low_ + 1; // NOLINT - uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); - // check if something was displayed if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { - ESP_LOGV(TAG, "Nothing to display"); return; } - set_addr_window_(this->x_low_, this->y_low_, w, h); + // we will only update the changed rows to the display + size_t const w = this->x_high_ - this->x_low_ + 1; + size_t const h = this->y_high_ - this->y_low_ + 1; + size_t mhz = this->data_rate_ / 1000000; + // estimate time for a single write + size_t sw_time = this->width_ * h * 16 / mhz + this->width_ * h * 2 / SPI_MAX_BLOCK_SIZE * SPI_SETUP_US * 2; + // estimate time for multiple writes + size_t mw_time = (w * h * 16) / mhz + w * h * 2 / ILI9XXX_TRANSFER_BUFFER_SIZE * SPI_SETUP_US; ESP_LOGV(TAG, "Start display(xlow:%d, ylow:%d, xhigh:%d, yhigh:%d, width:%d, " - "heigth:%d, start_pos:%d)", - this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, start_pos); - - this->start_data_(); - for (uint16_t row = 0; row < h; row++) { - uint32_t pos = start_pos + (row * width_); - uint32_t rem = w; - - while (rem > 0) { - uint32_t sz = std::min(rem, ILI9XXX_TRANSFER_BUFFER_SIZE); - // ESP_LOGVV(TAG, "Send to display(pos:%d, rem:%d, zs:%d)", pos, rem, sz); - buffer_to_transfer_(pos, sz); - if (this->is_18bitdisplay_) { - for (uint32_t i = 0; i < sz; ++i) { - uint16_t color_val = transfer_buffer_[i]; - - uint8_t red = color_val & 0x1F; - uint8_t green = (color_val & 0x7E0) >> 5; - uint8_t blue = (color_val & 0xF800) >> 11; - - uint8_t pass_buff[3]; - - pass_buff[2] = (uint8_t) ((red / 32.0) * 64) << 2; - pass_buff[1] = (uint8_t) green << 2; - pass_buff[0] = (uint8_t) ((blue / 32.0) * 64) << 2; - - this->write_array(pass_buff, sizeof(pass_buff)); - } - } else { - this->write_array16(transfer_buffer_, sz); + "height:%zu, mode=%d, 18bit=%d, sw_time=%zuus, mw_time=%zuus)", + this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, this->buffer_color_mode_, + this->is_18bitdisplay_, sw_time, mw_time); + auto now = millis(); + if (this->buffer_color_mode_ == BITS_16 && !this->is_18bitdisplay_ && sw_time < mw_time) { + // 16 bit mode maps directly to display format + ESP_LOGV(TAG, "Doing single write of %zu bytes", this->width_ * h * 2); + set_addr_window_(0, this->y_low_, this->width_ - 1, this->y_high_); + this->write_array(this->buffer_ + this->y_low_ * this->width_ * 2, h * this->width_ * 2); + } else { + ESP_LOGV(TAG, "Doing multiple write"); + uint8_t transfer_buffer[ILI9XXX_TRANSFER_BUFFER_SIZE]; + size_t rem = h * w; // remaining number of pixels to write + set_addr_window_(this->x_low_, this->y_low_, this->x_high_, this->y_high_); + size_t idx = 0; // index into transfer_buffer + size_t pixel = 0; // pixel number offset + size_t pos = this->y_low_ * this->width_ + this->x_low_; + while (rem-- != 0) { + uint16_t color_val; + switch (this->buffer_color_mode_) { + case BITS_8: + color_val = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos++])); + break; + case BITS_8_INDEXED: + color_val = display::ColorUtil::color_to_565( + display::ColorUtil::index8_to_color_palette888(this->buffer_[pos++], this->palette_)); + break; + default: // case BITS_16: + color_val = (this->buffer_[pos * 2] << 8) + this->buffer_[pos * 2 + 1]; + pos++; + break; + } + if (this->is_18bitdisplay_) { + transfer_buffer[idx++] = (uint8_t) ((color_val & 0xF800) >> 8); // Blue + transfer_buffer[idx++] = (uint8_t) ((color_val & 0x7E0) >> 3); // Green + transfer_buffer[idx++] = (uint8_t) (color_val << 3); // Red + } else { + put16_be(transfer_buffer + idx, color_val); + idx += 2; + } + if (idx == sizeof(transfer_buffer)) { + this->write_array(transfer_buffer, idx); + idx = 0; + App.feed_wdt(); + } + // end of line? Skip to the next. + if (++pixel == w) { + pixel = 0; + pos += this->width_ - w; } - pos += sz; - rem -= sz; } - App.feed_wdt(); + // flush any balance. + if (idx != 0) { + this->write_array(transfer_buffer, idx); + } } this->end_data_(); - + ESP_LOGV(TAG, "Data write took %dms", (unsigned) (millis() - now)); // invalidate watermarks this->x_low_ = this->width_; this->y_low_ = this->height_; @@ -224,24 +303,62 @@ void ILI9XXXDisplay::display_() { this->y_high_ = 0; } -uint32_t ILI9XXXDisplay::buffer_to_transfer_(uint32_t pos, uint32_t sz) { - for (uint32_t i = 0; i < sz; ++i) { - switch (this->buffer_color_mode_) { - case BITS_8_INDEXED: - transfer_buffer_[i] = display::ColorUtil::color_to_565( - display::ColorUtil::index8_to_color_palette888(this->buffer_[pos + i], this->palette_)); - break; - case BITS_16: - transfer_buffer_[i] = ((uint16_t) this->buffer_[(pos + i) * 2] << 8) | this->buffer_[((pos + i) * 2) + 1]; - continue; - break; - default: - transfer_buffer_[i] = - display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos + i])); - break; +// note that this bypasses the buffer and writes directly to the display. +void ILI9XXXDisplay::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) { + if (w <= 0 || h <= 0) + return; + // if color mapping or software rotation is required, hand this off to the parent implementation. This will + // do color conversion pixel-by-pixel into the buffer and draw it later. If this is happening the user has not + // configured the renderer well. + if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || !big_endian) { + return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, + x_pad); + } + this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + auto stride = x_offset + w + x_pad; + if (!this->is_18bitdisplay_) { + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother + this->write_array(ptr, w * h * 2); + } else { + for (size_t y = 0; y != h; y++) { + this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2); + } + } + } else { + // 18 bit mode + uint8_t transfer_buffer[ILI9XXX_TRANSFER_BUFFER_SIZE * 4]; + ESP_LOGV(TAG, "Doing multiple write"); + size_t rem = h * w; // remaining number of pixels to write + size_t idx = 0; // index into transfer_buffer + size_t pixel = 0; // pixel number offset + ptr += (y_offset * stride + x_offset) * 2; + while (rem-- != 0) { + uint8_t hi_byte = *ptr++; + uint8_t lo_byte = *ptr++; + transfer_buffer[idx++] = hi_byte & 0xF8; // Blue + transfer_buffer[idx++] = ((hi_byte << 5) | (lo_byte) >> 5); // Green + transfer_buffer[idx++] = lo_byte << 3; // Red + if (idx == sizeof(transfer_buffer)) { + this->write_array(transfer_buffer, idx); + idx = 0; + App.feed_wdt(); + } + // end of line? Skip to the next. + if (++pixel == w) { + pixel = 0; + ptr += (x_pad + x_offset) * 2; + } + } + // flush any balance. + if (idx != 0) { + this->write_array(transfer_buffer, idx); } } - return sz; + this->end_data_(); } // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color @@ -267,20 +384,6 @@ void ILI9XXXDisplay::send_command(uint8_t command_byte, const uint8_t *data_byte this->end_data_(); } -uint8_t ILI9XXXDisplay::read_command(uint8_t command_byte, uint8_t index) { - uint8_t data = 0x10 + index; - this->send_command(0xD9, &data, 1); // Set Index Register - uint8_t result; - this->start_command_(); - this->write_byte(command_byte); - this->start_data_(); - do { - result = this->read_byte(); - } while (index--); - this->end_data_(); - return result; -} - void ILI9XXXDisplay::start_command_() { this->dc_pin_->digital_write(false); this->enable(); @@ -296,130 +399,64 @@ void ILI9XXXDisplay::end_data_() { this->disable(); } void ILI9XXXDisplay::reset_() { if (this->reset_pin_ != nullptr) { this->reset_pin_->digital_write(false); - delay(10); + delay(20); this->reset_pin_->digital_write(true); - delay(10); + delay(20); } } -void ILI9XXXDisplay::init_lcd_(const uint8_t *init_cmd) { +void ILI9XXXDisplay::init_lcd_(const uint8_t *addr) { + if (addr == nullptr) + return; uint8_t cmd, x, num_args; - const uint8_t *addr = init_cmd; - while ((cmd = progmem_read_byte(addr++)) > 0) { - x = progmem_read_byte(addr++); - num_args = x & 0x7F; - send_command(cmd, addr, num_args); - addr += num_args; - if (x & 0x80) - delay(150); // NOLINT + while ((cmd = *addr++) != 0) { + x = *addr++; + if (x == ILI9XXX_DELAY_FLAG) { + cmd &= 0x7F; + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + num_args = x & 0x7F; + ESP_LOGV(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, *addr); + this->send_command(cmd, addr, num_args); + addr += num_args; + if (x & 0x80) { + ESP_LOGV(TAG, "Delay 150ms"); + delay(150); // NOLINT + } + } } } -void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h) { - uint16_t x2 = (x1 + w - 1), y2 = (y1 + h - 1); - this->command(ILI9XXX_CASET); // Column address set - this->start_data_(); - this->write_byte(x1 >> 8); - this->write_byte(x1); - this->write_byte(x2 >> 8); - this->write_byte(x2); - this->end_data_(); - this->command(ILI9XXX_PASET); // Row address set - this->start_data_(); - this->write_byte(y1 >> 8); - this->write_byte(y1); - this->write_byte(y2 >> 8); - this->write_byte(y2); - this->end_data_(); +// Tell the display controller where we want to draw pixels. +void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { + x1 += this->offset_x_; + x2 += this->offset_x_; + y1 += this->offset_y_; + y2 += this->offset_y_; + this->command(ILI9XXX_CASET); + this->data(x1 >> 8); + this->data(x1 & 0xFF); + this->data(x2 >> 8); + this->data(x2 & 0xFF); + this->command(ILI9XXX_PASET); // Page address set + this->data(y1 >> 8); + this->data(y1 & 0xFF); + this->data(y2 >> 8); + this->data(y2 & 0xFF); this->command(ILI9XXX_RAMWR); // Write to RAM + this->start_data_(); } -void ILI9XXXDisplay::invert_display_(bool invert) { this->command(invert ? ILI9XXX_INVON : ILI9XXX_INVOFF); } +void ILI9XXXDisplay::invert_colors(bool invert) { + this->pre_invertcolors_ = invert; + if (is_ready()) { + this->command(invert ? ILI9XXX_INVON : ILI9XXX_INVOFF); + } +} int ILI9XXXDisplay::get_width_internal() { return this->width_; } int ILI9XXXDisplay::get_height_internal() { return this->height_; } -// M5Stack display -void ILI9XXXM5Stack::initialize() { - this->init_lcd_(INITCMD_M5STACK); - if (this->width_ == 0) - this->width_ = 320; - if (this->height_ == 0) - this->height_ = 240; - this->invert_display_(true); -} - -// M5CORE display // Based on the configuration settings of M5stact's M5GFX code. -void ILI9XXXM5CORE::initialize() { - this->init_lcd_(INITCMD_M5CORE); - if (this->width_ == 0) - this->width_ = 320; - if (this->height_ == 0) - this->height_ = 240; - this->invert_display_(true); -} - -// 24_TFT display -void ILI9XXXILI9341::initialize() { - this->init_lcd_(INITCMD_ILI9341); - if (this->width_ == 0) - this->width_ = 240; - if (this->height_ == 0) - this->height_ = 320; -} -// 24_TFT rotated display -void ILI9XXXILI9342::initialize() { - this->init_lcd_(INITCMD_ILI9341); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 240; - } -} - -// 35_TFT display -void ILI9XXXILI9481::initialize() { - this->init_lcd_(INITCMD_ILI9481); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } -} - -// 35_TFT display -void ILI9XXXILI9486::initialize() { - this->init_lcd_(INITCMD_ILI9486); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } -} -// 40_TFT display -void ILI9XXXILI9488::initialize() { - this->init_lcd_(INITCMD_ILI9488); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } - this->is_18bitdisplay_ = true; -} -// 40_TFT display -void ILI9XXXST7796::initialize() { - this->init_lcd_(INITCMD_ST7796); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 480; - } -} - } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 8a8cd4bb44..c141739d2a 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -1,13 +1,15 @@ #pragma once #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display_color_utils.h" #include "ili9xxx_defines.h" #include "ili9xxx_init.h" namespace esphome { namespace ili9xxx { -const uint32_t ILI9XXX_TRANSFER_BUFFER_SIZE = 64; +static const char *const TAG = "ili9xxx"; +const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6 enum ILI9XXXColorMode { BITS_8 = 0x08, @@ -15,24 +17,71 @@ enum ILI9XXXColorMode { BITS_16 = 0x10, }; -class ILI9XXXDisplay : public PollingComponent, - public display::DisplayBuffer, +enum PixelMode { + PIXEL_MODE_UNSPECIFIED, + PIXEL_MODE_16, + PIXEL_MODE_18, +}; + +class ILI9XXXDisplay : public display::DisplayBuffer, public spi::SPIDevice { public: + ILI9XXXDisplay() = default; + ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height) + : init_sequence_{init_sequence}, width_{width}, height_{height} { + uint8_t cmd, num_args, bits; + const uint8_t *addr = init_sequence; + while ((cmd = *addr++) != 0) { + num_args = *addr++; + if (num_args == ILI9XXX_DELAY_FLAG) + continue; + bits = *addr; + switch (cmd) { + case ILI9XXX_MADCTL: { + this->swap_xy_ = (bits & MADCTL_MV) != 0; + this->mirror_x_ = (bits & MADCTL_MX) != 0; + this->mirror_y_ = (bits & MADCTL_MY) != 0; + this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; + break; + } + + case ILI9XXX_PIXFMT: { + if ((bits & 0xF) == 6) + this->is_18bitdisplay_ = true; + break; + } + + default: + break; + } + addr += (num_args & 0x7F); + } + } + + void add_init_sequence(const std::vector &sequence) { this->extra_init_sequence_ = sequence; } void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_palette(const uint8_t *palette) { this->palette_ = palette; } void set_buffer_color_mode(ILI9XXXColorMode color_mode) { this->buffer_color_mode_ = color_mode; } - void set_dimentions(int16_t width, int16_t height) { + void set_dimensions(int16_t width, int16_t height) { this->height_ = height; this->width_ = width; } - void command(uint8_t value); - void data(uint8_t value); + void set_offsets(int16_t offset_x, int16_t offset_y) { + this->offset_x_ = offset_x; + this->offset_y_ = offset_y; + } + void invert_colors(bool invert); + virtual void command(uint8_t value); + virtual void data(uint8_t value); void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); - uint8_t read_command(uint8_t command_byte, uint8_t index); + void set_color_order(display::ColorOrder color_order) { this->color_order_ = color_order; } + void set_swap_xy(bool swap_xy) { this->swap_xy_ = swap_xy; } + void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } + void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } + void set_pixel_mode(PixelMode mode) { this->pixel_mode_ = mode; } void update() override; @@ -40,27 +89,41 @@ class ILI9XXXDisplay : public PollingComponent, void dump_config() override; void setup() override; + void on_shutdown() override { this->command(ILI9XXX_SLPIN); } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + 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; protected: + inline bool check_buffer_() { + if (this->buffer_ == nullptr) { + this->alloc_buffer_(); + return !this->is_failed(); + } + return true; + } + void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); - virtual void initialize() = 0; + virtual void set_madctl(); void display_(); - void init_lcd_(const uint8_t *init_cmd); - void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); - void invert_display_(bool invert); + void init_lcd_(const uint8_t *addr); + void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2); void reset_(); + uint8_t const *init_sequence_{}; + std::vector extra_init_sequence_; int16_t width_{0}; ///< Display width as modified by current rotation int16_t height_{0}; ///< Display height as modified by current rotation + int16_t offset_x_{0}; + int16_t offset_y_{0}; uint16_t x_low_{0}; uint16_t y_low_{0}; uint16_t x_high_{0}; uint16_t y_high_{0}; - const uint8_t *palette_; + const uint8_t *palette_{}; ILI9XXXColorMode buffer_color_mode_{BITS_16}; @@ -72,10 +135,7 @@ class ILI9XXXDisplay : public PollingComponent, void end_command_(); void start_data_(); void end_data_(); - - uint16_t transfer_buffer_[ILI9XXX_TRANSFER_BUFFER_SIZE]; - - uint32_t buffer_to_transfer_(uint32_t pos, uint32_t sz); + void alloc_buffer_(); GPIOPin *reset_pin_{nullptr}; GPIOPin *dc_pin_{nullptr}; @@ -84,54 +144,137 @@ class ILI9XXXDisplay : public PollingComponent, bool prossing_update_ = false; bool need_update_ = false; bool is_18bitdisplay_ = false; + PixelMode pixel_mode_{}; + bool pre_invertcolors_{}; + display::ColorOrder color_order_{display::COLOR_ORDER_BGR}; + bool swap_xy_{}; + bool mirror_x_{}; + bool mirror_y_{}; }; //----------- M5Stack display -------------- class ILI9XXXM5Stack : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXM5Stack() : ILI9XXXDisplay(INITCMD_M5STACK, 320, 240) {} }; //----------- M5Stack display -------------- class ILI9XXXM5CORE : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXM5CORE() : ILI9XXXDisplay(INITCMD_M5CORE, 320, 240) {} +}; + +//----------- ST7789V display -------------- +class ILI9XXXST7789V : public ILI9XXXDisplay { + public: + ILI9XXXST7789V() : ILI9XXXDisplay(INITCMD_ST7789V, 240, 320) {} }; //----------- ILI9XXX_24_TFT display -------------- class ILI9XXXILI9341 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9341() : ILI9XXXDisplay(INITCMD_ILI9341, 240, 320) {} }; //----------- ILI9XXX_24_TFT rotated display -------------- class ILI9XXXILI9342 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9342() : ILI9XXXDisplay(INITCMD_ILI9341, 320, 240) {} }; //----------- ILI9XXX_??_TFT rotated display -------------- class ILI9XXXILI9481 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9481() : ILI9XXXDisplay(INITCMD_ILI9481, 480, 320) {} +}; + +//----------- ILI9481 in 18 bit mode -------------- +class ILI9XXXILI948118 : public ILI9XXXDisplay { + public: + ILI9XXXILI948118() : ILI9XXXDisplay(INITCMD_ILI9481_18, 320, 480) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9486 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320) {} }; -//----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9488 : public ILI9XXXDisplay { + public: + ILI9XXXILI9488(const uint8_t *seq = INITCMD_ILI9488) : ILI9XXXDisplay(seq, 480, 320) {} + protected: - void initialize() override; + void set_madctl() override { + uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB; + uint8_t dfun = 0x22; + this->width_ = 320; + this->height_ = 480; + if (!(this->swap_xy_ || this->mirror_x_ || this->mirror_y_)) { + // no transforms + } else if (this->mirror_y_ && this->mirror_x_) { + // rotate 180 + dfun = 0x42; + } else if (this->swap_xy_) { + this->width_ = 480; + this->height_ = 320; + mad |= 0x20; + if (this->mirror_x_) { + dfun = 0x02; + } else { + dfun = 0x62; + } + } + this->command(ILI9XXX_DFUNCTR); + this->data(0); + this->data(dfun); + this->command(ILI9XXX_MADCTL); + this->data(mad); + } +}; +//----------- Waveshare 3.5 Res Touch - ILI9488 interfaced via 16 bit shift register to parallel */ +class WAVESHARERES35 : public ILI9XXXILI9488 { + public: + WAVESHARERES35() : ILI9XXXILI9488(INITCMD_WAVESHARE_RES_3_5) {} + void data(uint8_t value) override { + this->start_data_(); + this->write_byte(0); + this->write_byte(value); + this->end_data_(); + } +}; + +//----------- ILI9XXX_35_TFT origin colors rotated display -------------- +class ILI9XXXILI9488A : public ILI9XXXDisplay { + public: + ILI9XXXILI9488A() : ILI9XXXDisplay(INITCMD_ILI9488_A, 480, 320) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXST7796 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXST7796() : ILI9XXXDisplay(INITCMD_ST7796, 320, 480) {} +}; + +class ILI9XXXS3Box : public ILI9XXXDisplay { + public: + ILI9XXXS3Box() : ILI9XXXDisplay(INITCMD_S3BOX, 320, 240) {} +}; + +class ILI9XXXS3BoxLite : public ILI9XXXDisplay { + public: + ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240) {} +}; + +class ILI9XXXGC9A01A : public ILI9XXXDisplay { + public: + ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240) {} +}; + +//----------- ILI9XXX_24_TFT display -------------- +class ILI9XXXST7735 : public ILI9XXXDisplay { + public: + ILI9XXXST7735() : ILI9XXXDisplay(INITCMD_ST7735, 128, 160) {} }; } // namespace ili9xxx diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 593b9a79ce..b176680f43 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -1,6 +1,8 @@ #pragma once #include "esphome/core/helpers.h" +#include + namespace esphome { namespace ili9xxx { @@ -94,12 +96,34 @@ static const uint8_t PROGMEM INITCMD_ILI9481[] = { ILI9XXX_IFCTR , 1, 0x83, ILI9XXX_GMCTR ,12, 0x00, 0x26, 0x21, 0x00, 0x00, 0x1F, 0x65, 0x23, 0x77, 0x00, 0x0F, 0x00, ILI9XXX_IFMODE , 1, 0x00, // CommandAccessProtect + ILI9XXX_PTLAR , 4, 0, 0, 1, 0xDF, 0xE4 , 1, 0xA0, + ILI9XXX_MADCTL , 1, MADCTL_MV | MADCTL_BGR, // Memory Access Control ILI9XXX_CSCON , 1, 0x01, + ILI9XXX_PIXFMT, 1, 0x55, // 16 bit mode ILI9XXX_DISPON, 0x80, // Set display on 0x00 // end }; +static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { + ILI9XXX_SLPOUT , 0x80, // Exit sleep mode + ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, + ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, + ILI9XXX_PWSETN , 2, 0x01, 0x11, + ILI9XXX_PWCTR1 , 5, 0x10, 0x3B, 0x00, 0x02, 0x11, + ILI9XXX_VMCTR1 , 1, 0x03, + ILI9XXX_IFCTR , 1, 0x83, + ILI9XXX_GMCTR ,12, 0x00, 0x26, 0x21, 0x00, 0x00, 0x1F, 0x65, 0x23, 0x77, 0x00, 0x0F, 0x00, + ILI9XXX_IFMODE , 1, 0x00, // CommandAccessProtect + ILI9XXX_PTLAR , 4, 0, 0, 1, 0xDF, + 0xE4 , 1, 0xA0, + ILI9XXX_MADCTL , 1, MADCTL_MX| MADCTL_BGR, // Memory Access Control + ILI9XXX_CSCON , 1, 0x01, + ILI9XXX_PIXFMT, 1, 0x66, // 18 bit mode + ILI9XXX_DISPON, 0x80, // Set display on + 0x00 // end +}; + static const uint8_t PROGMEM INITCMD_ILI9486[] = { ILI9XXX_SLPOUT, 0x80, ILI9XXX_PIXFMT, 1, 0x55, @@ -115,7 +139,8 @@ static const uint8_t PROGMEM INITCMD_ILI9486[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9488[] = { + +static const uint8_t INITCMD_ILI9488[] = { ILI9XXX_GMCTRP1,15, 0x0f, 0x24, 0x1c, 0x0a, 0x0f, 0x08, 0x43, 0x88, 0x32, 0x0f, 0x10, 0x06, 0x0f, 0x07, 0x00, ILI9XXX_GMCTRN1,15, 0x0F, 0x38, 0x30, 0x09, 0x0f, 0x0f, 0x4e, 0x77, 0x3c, 0x07, 0x10, 0x05, 0x23, 0x1b, 0x00, @@ -127,6 +152,39 @@ static const uint8_t PROGMEM INITCMD_ILI9488[] = { ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot + 0xE9, 1, 0x00, // Set Image Functio. Disable 24 bit data + + ILI9XXX_ADJCTL3, 4, 0xA9, 0x51, 0x2C, 0x82, // Adjust Control 3 + ILI9XXX_PIXFMT, 1, 0x66, //ILI9488 only supports 18-bit pixel format in 4/3 wire SPI mode + ILI9XXX_SLPOUT, 0x80, // Exit sleep mode + ILI9XXX_DISPON, 0x80, // Set display on + 0x00 // end +}; + +static const uint8_t INITCMD_WAVESHARE_RES_3_5[] = { + ILI9XXX_PWCTR3, 1, 0x33, + ILI9XXX_VMCTR1, 3, 0x00, 0x1e, 0x80, + ILI9XXX_FRMCTR1, 1, 0xA0, + ILI9XXX_GMCTRP1, 15, 0x0, 0x13, 0x18, 0x04, 0x0F, 0x06, 0x3a, 0x56, 0x4d, 0x03, 0x0a, 0x06, 0x30, 0x3e, 0x0f, + ILI9XXX_GMCTRN1, 15, 0x0, 0x13, 0x18, 0x01, 0x11, 0x06, 0x38, 0x34, 0x4d, 0x06, 0x0d, 0x0b, 0x31, 0x37, 0x0f, + ILI9XXX_PIXFMT, 1, 0x55, + ILI9XXX_SLPOUT, 0x80, // slpout, delay + ILI9XXX_DISPON, 0, + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { + ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, + ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, + + ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2 + ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL + ILI9XXX_VMCTR1, 3, 0x00, 0x12, 0x80, // nVM VCM_REG VCM_REG_EN + + ILI9XXX_IFMODE, 1, 0x00, + ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz + ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot + ILI9XXX_DFUNCTR, 2, 0x02, 0x02, // Nomal scan 0xE9, 1, 0x00, // Set Image Functio. Disable 24 bit data @@ -144,7 +202,6 @@ static const uint8_t PROGMEM INITCMD_ILI9488[] = { ILI9XXX_SLPOUT, 0x80, // Exit sleep mode - //ILI9XXX_INVON , 0, ILI9XXX_DISPON, 0x80, // Set display on 0x00 // end }; @@ -169,6 +226,198 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = { 0x00 // End of list }; +static const uint8_t PROGMEM INITCMD_S3BOX[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9XXX_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9XXX_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9XXX_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9XXX_VMCTR2 , 1, 0x86, // VCM control2 + ILI9XXX_MADCTL , 1, 0xC8, // Memory Access Control + ILI9XXX_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9XXX_PIXFMT , 1, 0x55, + ILI9XXX_FRMCTR1 , 2, 0x00, 0x18, + ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x0E, 0x09, 0x00, + ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0x36, 0x0F, + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9XXX_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9XXX_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9XXX_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9XXX_VMCTR2 , 1, 0x86, // VCM control2 + ILI9XXX_MADCTL , 1, 0x40, // Memory Access Control + ILI9XXX_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9XXX_PIXFMT , 1, 0x55, + ILI9XXX_FRMCTR1 , 2, 0x00, 0x18, + ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9XXX_GMCTRP1 , 14, 0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, // Set Gamma + 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, + 0x18, 0x1B, + ILI9XXX_GMCTRN1 , 14, 0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, // Set Gamma + 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, + 0x17, 0x1B, + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_ST7789V[] = { + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + ILI9XXX_MADCTL , 1, 0x08, // Memory Access Control, BGR + ILI9XXX_DFUNCTR, 2, 0x0A, 0x82, + ILI9XXX_PIXFMT , 1, 0x55, + ILI9XXX_FRMCTR2, 5, 0x0C, 0x0C, 0x00, 0x33, 0x33, + ILI9XXX_ETMOD, 1, 0x35, 0xBB, 1, 0x28, + ILI9XXX_PWCTR1 , 1, 0x0C, // Power control VRH[5:0] + ILI9XXX_PWCTR3 , 2, 0x01, 0xFF, + ILI9XXX_PWCTR4 , 1, 0x10, + ILI9XXX_PWCTR5 , 1, 0x20, + ILI9XXX_IFCTR , 1, 0x0F, + ILI9XXX_PWSET, 2, 0xA4, 0xA1, + ILI9XXX_GMCTRP1 , 14, + 0xd0, 0x00, 0x02, 0x07, 0x0a, + 0x28, 0x32, 0x44, 0x42, 0x06, 0x0e, + 0x12, 0x14, 0x17, + ILI9XXX_GMCTRN1 , 14, + 0xd0, 0x00, 0x02, 0x07, 0x0a, + 0x28, 0x31, 0x54, 0x47, + 0x0e, 0x1c, 0x17, 0x1b, + 0x1e, + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_GC9A01A[] = { + 0xEF, 0, + 0xEB, 1, 0x14, // ? + 0xFE, 0, + 0xEF, 0, + 0xEB, 1, 0x14, // ? + 0x84, 1, 0x40, // ? + 0x85, 1, 0xFF, // ? + 0x86, 1, 0xFF, // ? + 0x87, 1, 0xFF, // ? + 0x88, 1, 0x0A, // ? + 0x89, 1, 0x21, // ? + 0x8A, 1, 0x00, // ? + 0x8B, 1, 0x80, // ? + 0x8C, 1, 0x01, // ? + 0x8D, 1, 0x01, // ? + 0x8E, 1, 0xFF, // ? + 0x8F, 1, 0xFF, // ? + 0xB6, 2, 0x00, 0x00, // ? + 0x90, 4, 0x08, 0x08, 0x08, 0x08, // ? + ILI9XXX_PIXFMT , 1, 0x05, + ILI9XXX_MADCTL , 1, MADCTL_MX| MADCTL_BGR, // Memory Access Control + 0xBD, 1, 0x06, // ? + 0xBC, 1, 0x00, // ? + 0xFF, 3, 0x60, 0x01, 0x04, // ? + 0xC3, 1, 0x13, + 0xC4, 1, 0x13, + 0xF9, 1, 0x22, + 0xBE, 1, 0x11, // ? + 0xE1, 2, 0x10, 0x0E, // ? + 0xDF, 3, 0x21, 0x0c, 0x02, // ? + 0xF0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A, + 0xF1, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F, + 0xF2, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A, + 0xF3, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F, + 0xED, 2, 0x1B, 0x0B, // ? + 0xAE, 1, 0x77, // ? + 0xCD, 1, 0x63, // ? + 0xE8, 1, 0x34, + 0x62, 12, 0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, // ? + 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70, + 0x63, 12, 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, // ? + 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70, + 0x64, 7, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07, // ? + 0x66, 10, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00, // ? + 0x67, 10, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98, // ? + 0x74, 7, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00, // ? + 0x98, 2, 0x3e, 0x07, // ? + 0x35, 0, + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_ST7735[] = { + ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms + ILI9XXX_DELAY(10), + ILI9XXX_SLPOUT , 0, // Exit Sleep, delay + ILI9XXX_DELAY(10), + ILI9XXX_PIXFMT , 1, 0x05, + ILI9XXX_FRMCTR1, 3, // 4: Frame rate control, 3 args + delay: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ILI9XXX_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ILI9XXX_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: + 0x01, 0x2C, 0x2D, // Dot inversion mode + 0x01, 0x2C, 0x2D, // Line inversion mode + + ILI9XXX_INVCTR, 1, // 7: Display inversion control, 1 arg: + 0x7, // Line inversion + ILI9XXX_PWCTR1, 3, // 7: Power control, 3 args, no delay: + 0xA2, + 0x02, // -4.6V + 0x84, // AUTO mode + ILI9XXX_PWCTR2, 1, // 8: Power control, 1 arg, no delay: + 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD + ILI9XXX_PWCTR3, 2, // 9: Power control, 2 args, no delay: + 0x0A, // Opamp current small + 0x00, // Boost frequency + ILI9XXX_PWCTR4, 2, // 10: Power control, 2 args, no delay: + 0x8A, // BCLK/2, + 0x2A, // opamp current small & medium low + ILI9XXX_PWCTR5, 2, // 11: Power control, 2 args, no delay: + 0x8A, 0xEE, + + ILI9XXX_VMCTR1, 1, // 11: Power control, 2 args + delay: + 0x0E, + ILI9XXX_GMCTRP1, 16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides + 0x37, 0x32, 0x29, 0x2d, // accurate colors) + 0x29, 0x25, 0x2B, 0x39, + 0x00, 0x01, 0x03, 0x10, + ILI9XXX_GMCTRN1, 16, // 14: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides + 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) + 0x2E, 0x2E, 0x37, 0x3F, + 0x00, 0x00, 0x02, 0x10, + ILI9XXX_MADCTL , 1, 0x00, // Memory Access Control, BGR + ILI9XXX_NORON , 0, + ILI9XXX_DELAY(10), + ILI9XXX_DISPON , 0, // Display on + ILI9XXX_DELAY(10), + 00, // endo of list +}; + // clang-format on } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 4260636fa9..8742540067 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,25 +1,41 @@ -import logging +from __future__ import annotations -from esphome import core -from esphome.components import display, font -import esphome.config_validation as cv +import hashlib +import io +import logging +from pathlib import Path +import re + +import puremagic + +from esphome import core, external_files import esphome.codegen as cg +from esphome.components import font +import esphome.config_validation as cv from esphome.const import ( CONF_DITHER, CONF_FILE, + CONF_ICON, CONF_ID, + CONF_PATH, CONF_RAW_DATA_ID, CONF_RESIZE, + CONF_SOURCE, CONF_TYPE, + CONF_URL, ) from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) +DOMAIN = "image" DEPENDENCIES = ["display"] MULTI_CONF = True +MULTI_CONF_NO_DEFAULT = True -ImageType = display.display_ns.enum("ImageType") +image_ns = cg.esphome_ns.namespace("image") + +ImageType = image_ns.enum("ImageType") IMAGE_TYPE = { "BINARY": ImageType.IMAGE_TYPE_BINARY, "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, @@ -31,7 +47,71 @@ IMAGE_TYPE = { CONF_USE_TRANSPARENCY = "use_transparency" -Image_ = display.display_ns.class_("Image") +# If the MDI file cannot be downloaded within this time, abort. +IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds + +SOURCE_LOCAL = "local" +SOURCE_MDI = "mdi" +SOURCE_WEB = "web" + + +Image_ = image_ns.class_("Image") + + +def _compute_local_icon_path(value: dict) -> Path: + base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" + return base_dir / f"{value[CONF_ICON]}.svg" + + +def compute_local_image_path(value: dict) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + return base_dir / key + + +def download_mdi(value): + validate_cairosvg_installed(value) + + mdi_id = value[CONF_ICON] + path = _compute_local_icon_path(value) + + url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" + + external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) + + return value + + +def download_image(value): + url = value[CONF_URL] + path = compute_local_image_path(value) + + external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) + + return value + + +def validate_cairosvg_installed(value): + """Validate that cairosvg is installed""" + try: + import cairosvg + except ImportError as err: + raise cv.Invalid( + "Please install the cairosvg python package to use this feature. " + "(pip install cairosvg)" + ) from err + + major, minor, _ = cairosvg.__version__.split(".") + if major < "2" or major == "2" and minor < "2": + raise cv.Invalid( + "Please update your cairosvg installation to at least 2.2.0. " + "(pip install -U cairosvg)" + ) + + return value def validate_cross_dependencies(config): @@ -41,6 +121,13 @@ def validate_cross_dependencies(config): have "use_transparency" set to True. Also set the default value for those kind of dependent fields. """ + is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI + if CONF_TYPE not in config: + if is_mdi: + config[CONF_TYPE] = "TRANSPARENT_BINARY" + else: + config[CONF_TYPE] = "BINARY" + image_type = config[CONF_TYPE] is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] @@ -51,16 +138,89 @@ def validate_cross_dependencies(config): if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: raise cv.Invalid(f"Image type {image_type} must always be transparent.") + if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]: + raise cv.Invalid("MDI images must be binary images.") + return config +def validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("mdi:"): + validate_cairosvg_installed(value) + + match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value) + if match is None: + raise cv.Invalid("Could not parse mdi icon name.") + icon = match.group(1) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_MDI, + CONF_ICON: icon, + } + ) + if value.startswith("http://") or value.startswith("https://"): + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_WEB, + CONF_URL: value, + } + ) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_LOCAL, + CONF_PATH: value, + } + ) + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +MDI_SCHEMA = cv.All( + { + cv.Required(CONF_ICON): cv.string, + }, + download_mdi, +) + +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.string, + }, + download_image, +) + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + SOURCE_LOCAL: LOCAL_SCHEMA, + SOURCE_MDI: MDI_SCHEMA, + SOURCE_WEB: WEB_SCHEMA, + }, + key=CONF_SOURCE, +) + + +def _file_schema(value): + if isinstance(value, str): + return validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +FILE_SCHEMA = cv.Schema(_file_schema) + IMAGE_SCHEMA = cv.Schema( cv.All( { cv.Required(CONF_ID): cv.declare_id(Image_), - cv.Required(CONF_FILE): cv.file_, + cv.Required(CONF_FILE): FILE_SCHEMA, cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + # Not setting default here on purpose; the default depends on the source type + # (file or mdi), and will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True), # Not setting default here on purpose; the default depends on the image type, # and thus will be set in the "validate_cross_dependencies" validator. cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, @@ -76,31 +236,77 @@ IMAGE_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) -async def to_code(config): +def load_svg_image(file: bytes, resize: tuple[int, int]): + # Local imports only to allow "validate_pillow_installed" to run *before* importing it + # cairosvg is only needed in case of SVG images; adding it + # to the top would force configurations not using SVG to also have it + # installed for no reason. + from cairosvg import svg2png from PIL import Image - path = CORE.relative_config_path(config[CONF_FILE]) + if resize: + req_width, req_height = resize + svg_image = svg2png( + file, + output_width=req_width, + output_height=req_height, + ) + else: + svg_image = svg2png(file) + + return Image.open(io.BytesIO(svg_image)) + + +async def to_code(config): + # Local import only to allow "validate_pillow_installed" to run *before* importing it + from PIL import Image + + conf_file = config[CONF_FILE] + + if conf_file[CONF_SOURCE] == SOURCE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + + elif conf_file[CONF_SOURCE] == SOURCE_MDI: + path = _compute_local_icon_path(conf_file).as_posix() + + elif conf_file[CONF_SOURCE] == SOURCE_WEB: + path = compute_local_image_path(conf_file).as_posix() + + else: + raise core.EsphomeError(f"Unknown image source: {conf_file[CONF_SOURCE]}") + try: - image = Image.open(path) + with open(path, "rb") as f: + file_contents = f.read() except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") + file_type = puremagic.from_string(file_contents, mime=True) + + resize = config.get(CONF_RESIZE) + if "svg" in file_type: + image = load_svg_image(file_contents, resize) + else: + image = Image.open(io.BytesIO(file_contents)) + if resize: + image.thumbnail(resize) + width, height = image.size - if CONF_RESIZE in config: - image.thumbnail(config[CONF_RESIZE]) - width, height = image.size - else: - if width > 500 or height > 500: - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, - ) + if CONF_RESIZE not in config and (width > 500 or height > 500): + _LOGGER.warning( + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, + ) transparent = config[CONF_USE_TRANSPARENCY] - dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG + dither = ( + Image.Dither.NONE + if config[CONF_DITHER] == "NONE" + else Image.Dither.FLOYDSTEINBERG + ) if config[CONF_TYPE] == "GRAYSCALE": image = image.convert("LA", dither=dither) pixels = list(image.getdata()) @@ -155,24 +361,21 @@ async def to_code(config): elif config[CONF_TYPE] in ["RGB565"]: image = image.convert("RGBA") pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 2)] + bytes_per_pixel = 3 if transparent else 2 + data = [0 for _ in range(height * width * bytes_per_pixel)] pos = 0 for r, g, b, a in pixels: R = r >> 3 G = g >> 2 B = b >> 3 rgb = (R << 11) | (G << 5) | B - - if transparent: - if rgb == 0x0020: - rgb = 0 - if a < 0x80: - rgb = 0x0020 - data[pos] = rgb >> 8 pos += 1 data[pos] = rgb & 0xFF pos += 1 + if transparent: + data[pos] = a + pos += 1 elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: if transparent: diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp new file mode 100644 index 0000000000..ca2f659fb0 --- /dev/null +++ b/esphome/components/image/image.cpp @@ -0,0 +1,178 @@ +#include "image.h" + +#include "esphome/core/hal.h" + +namespace esphome { +namespace image { + +void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + switch (type_) { + case IMAGE_TYPE_BINARY: { + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + if (this->get_binary_pixel_(img_x, img_y)) { + display->draw_pixel_at(x + img_x, y + img_y, color_on); + } else if (!this->transparent_) { + display->draw_pixel_at(x + img_x, y + img_y, color_off); + } + } + } + break; + } + case IMAGE_TYPE_GRAYSCALE: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_grayscale_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGB565: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgb565_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGB24: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgb24_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGBA: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgba_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + } +} +Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return color_off; + switch (this->type_) { + case IMAGE_TYPE_BINARY: + return this->get_binary_pixel_(x, y) ? color_on : color_off; + case IMAGE_TYPE_GRAYSCALE: + return this->get_grayscale_pixel_(x, y); + case IMAGE_TYPE_RGB565: + return this->get_rgb565_pixel_(x, y); + case IMAGE_TYPE_RGB24: + return this->get_rgb24_pixel_(x, y); + case IMAGE_TYPE_RGBA: + return this->get_rgba_pixel_(x, y); + default: + return color_off; + } +} +#ifdef USE_LVGL +lv_img_dsc_t *Image::get_lv_img_dsc() { + // lazily construct lvgl image_dsc. + if (this->dsc_.data != this->data_start_) { + this->dsc_.data = this->data_start_; + this->dsc_.header.always_zero = 0; + this->dsc_.header.reserved = 0; + this->dsc_.header.w = this->width_; + this->dsc_.header.h = this->height_; + this->dsc_.data_size = this->get_width_stride() * this->get_height(); + switch (this->get_type()) { + case IMAGE_TYPE_BINARY: + this->dsc_.header.cf = LV_IMG_CF_ALPHA_1BIT; + break; + + case IMAGE_TYPE_GRAYSCALE: + this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT; + break; + + case IMAGE_TYPE_RGB24: + this->dsc_.header.cf = LV_IMG_CF_RGB888; + break; + + case IMAGE_TYPE_RGB565: +#if LV_COLOR_DEPTH == 16 + this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR; +#else + this->dsc_.header.cf = LV_IMG_CF_RGB565; +#endif + break; + + case IMAGE_TYPE_RGBA: +#if LV_COLOR_DEPTH == 32 + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; +#else + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; +#endif + break; + } + } + return &this->dsc_; +} +#endif // USE_LVGL + +bool Image::get_binary_pixel_(int x, int y) const { + const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; + const uint32_t pos = x + y * width_8; + return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); +} +Color Image::get_rgba_pixel_(int x, int y) const { + const uint32_t pos = (x + y * this->width_) * 4; + return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); +} +Color Image::get_rgb24_pixel_(int x, int y) const { + const uint32_t pos = (x + y * this->width_) * 3; + Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 2)); + if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { + // (0, 0, 1) has been defined as transparent color for non-alpha images. + // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) + color.w = 0; + } else { + color.w = 0xFF; + } + return color; +} +Color Image::get_rgb565_pixel_(int x, int y) const { + const uint8_t *pos = this->data_start_; + if (this->transparent_) { + pos += (x + y * this->width_) * 3; + } else { + pos += (x + y * this->width_) * 2; + } + uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1)); + auto r = (rgb565 & 0xF800) >> 11; + auto g = (rgb565 & 0x07E0) >> 5; + auto b = rgb565 & 0x001F; + auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF; + Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); + return color; +} + +Color Image::get_grayscale_pixel_(int x, int y) const { + const uint32_t pos = (x + y * this->width_); + const uint8_t gray = progmem_read_byte(this->data_start_ + pos); + uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; + return Color(gray, gray, gray, alpha); +} +int Image::get_width() const { return this->width_; } +int Image::get_height() const { return this->height_; } +ImageType Image::get_type() const { return this->type_; } +Image::Image(const uint8_t *data_start, int width, int height, ImageType type) + : width_(width), height_(height), type_(type), data_start_(data_start) {} + +} // namespace image +} // namespace esphome diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h new file mode 100644 index 0000000000..40370d18da --- /dev/null +++ b/esphome/components/image/image.h @@ -0,0 +1,74 @@ +#pragma once +#include "esphome/core/color.h" +#include "esphome/components/display/display.h" + +#ifdef USE_LVGL +#include "esphome/components/lvgl/lvgl_proxy.h" +#endif // USE_LVGL + +namespace esphome { +namespace image { + +enum ImageType { + IMAGE_TYPE_BINARY = 0, + IMAGE_TYPE_GRAYSCALE = 1, + IMAGE_TYPE_RGB24 = 2, + IMAGE_TYPE_RGB565 = 3, + IMAGE_TYPE_RGBA = 4, +}; + +class Image : public display::BaseImage { + public: + Image(const uint8_t *data_start, int width, int height, ImageType type); + Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const; + int get_width() const override; + int get_height() const override; + const uint8_t *get_data_start() const { return this->data_start_; } + ImageType get_type() const; + + int get_bpp() const { + switch (this->type_) { + case IMAGE_TYPE_BINARY: + return 1; + case IMAGE_TYPE_GRAYSCALE: + return 8; + case IMAGE_TYPE_RGB565: + return this->transparent_ ? 24 : 16; + case IMAGE_TYPE_RGB24: + return 24; + case IMAGE_TYPE_RGBA: + return 32; + } + return 0; + } + + /// Return the stride of the image in bytes, that is, the distance in bytes + /// between two consecutive rows of pixels. + uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; } + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + + void set_transparency(bool transparent) { transparent_ = transparent; } + bool has_transparency() const { return transparent_; } + +#ifdef USE_LVGL + lv_img_dsc_t *get_lv_img_dsc(); +#endif + protected: + bool get_binary_pixel_(int x, int y) const; + Color get_rgb24_pixel_(int x, int y) const; + Color get_rgba_pixel_(int x, int y) const; + Color get_rgb565_pixel_(int x, int y) const; + Color get_grayscale_pixel_(int x, int y) const; + + int width_; + int height_; + ImageType type_; + const uint8_t *data_start_; + bool transparent_; +#ifdef USE_LVGL + lv_img_dsc_t dsc_{}; +#endif +}; + +} // namespace image +} // namespace esphome diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py index 5c2853a5c6..aa75f4d89c 100644 --- a/esphome/components/improv_base/__init__.py +++ b/esphome/components/improv_base/__init__.py @@ -1,8 +1,7 @@ import re -import esphome.config_validation as cv import esphome.codegen as cg - +import esphome.config_validation as cv from esphome.const import __version__ CODEOWNERS = ["@esphome/core"] @@ -39,4 +38,4 @@ def _process_next_url(url: str): async def setup_improv_core(var, config): if CONF_NEXT_URL in config: cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL]))) - cg.add_library("esphome/Improv", "1.2.3") + cg.add_library("improv/Improv", "1.2.4") diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp index f18a1061fb..e890187d1a 100644 --- a/esphome/components/improv_base/improv_base.cpp +++ b/esphome/components/improv_base/improv_base.cpp @@ -21,8 +21,13 @@ std::string ImprovBase::get_formatted_next_url_() { // Ip address pos = this->next_url_.find("{{ip_address}}"); if (pos != std::string::npos) { - std::string ip = network::get_ip_address().str(); - copy.replace(pos, 14, ip); + for (auto &ip : network::get_ip_addresses()) { + if (ip.is_ip4()) { + std::string ipa = ip.str(); + copy.replace(pos, 14, ipa); + break; + } + } } return copy; diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 311256804b..544af212e0 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,10 +1,12 @@ -from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG -from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER 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.logger import USB_CDC import esphome.config_validation as cv +from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER from esphome.core import CORE import esphome.final_validate as fv -from esphome.components import improv_base AUTO_LOAD = ["improv_base"] CODEOWNERS = ["@esphome/core"] @@ -15,11 +17,7 @@ improv_serial_ns = cg.esphome_ns.namespace("improv_serial") ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component) CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(ImprovSerialComponent), - } - ) + cv.Schema({cv.GenerateID(): cv.declare_id(ImprovSerialComponent)}) .extend(improv_base.IMPROV_SCHEMA) .extend(cv.COMPONENT_SCHEMA) ) @@ -30,7 +28,10 @@ def validate_logger(config): 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: - if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]: + if ( + logger_conf[CONF_HARDWARE_UART] == USB_CDC + and get_esp32_variant() == VARIANT_ESP32S3 + ): raise cv.Invalid( "improv_serial does not support the selected logger hardware_uart" ) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index fe19e2f085..c3a0f2eacc 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -1,5 +1,5 @@ #include "improv_serial_component.h" - +#ifdef USE_WIFI #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -31,26 +31,57 @@ void ImprovSerialComponent::setup() { void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } -int ImprovSerialComponent::available_() { +optional ImprovSerialComponent::read_byte_() { + optional byte; + uint8_t data = 0; #ifdef USE_ARDUINO - return this->hw_serial_->available(); + if (this->hw_serial_->available()) { + this->hw_serial_->readBytes(&data, 1); + byte = data; + } #endif #ifdef USE_ESP_IDF - size_t available; - uart_get_buffered_data_len(this->uart_num_, &available); - return available; + switch (logger::global_logger->get_uart()) { + 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) + case logger::UART_SELECTION_UART2: +#endif // !USE_ESP32_VARIANT_ESP32C3 && !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); + if (available) { + uart_read_bytes(this->uart_num_, &data, 1, 0); + byte = data; + } + } + break; +#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) + case logger::UART_SELECTION_USB_CDC: +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + if (esp_usb_console_available_for_read()) { +#else + if (esp_usb_console_read_available()) { #endif -} - -uint8_t ImprovSerialComponent::read_byte_() { - uint8_t data; -#ifdef USE_ARDUINO - this->hw_serial_->readBytes(&data, 1); + esp_usb_console_read_buf((char *) &data, 1); + byte = data; + } + break; +#endif // USE_LOGGER_USB_CDC +#ifdef USE_LOGGER_USB_SERIAL_JTAG + case logger::UART_SELECTION_USB_SERIAL_JTAG: { + if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) { + byte = data; + } + break; + } +#endif // USE_LOGGER_USB_SERIAL_JTAG + default: + break; + } #endif -#ifdef USE_ESP_IDF - uart_read_bytes(this->uart_num_, &data, 1, 20 / portTICK_RATE_MS); -#endif - return data; + return byte; } void ImprovSerialComponent::write_data_(std::vector &data) { @@ -59,24 +90,51 @@ void ImprovSerialComponent::write_data_(std::vector &data) { this->hw_serial_->write(data.data(), data.size()); #endif #ifdef USE_ESP_IDF - uart_write_bytes(this->uart_num_, data.data(), data.size()); + switch (logger::global_logger->get_uart()) { + 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) + case logger::UART_SELECTION_UART2: +#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 + uart_write_bytes(this->uart_num_, data.data(), data.size()); + break; +#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) + case logger::UART_SELECTION_USB_CDC: { + const char *msg = (char *) data.data(); + esp_usb_console_write_buf(msg, data.size()); + break; + } +#endif // USE_LOGGER_USB_CDC +#ifdef USE_LOGGER_USB_SERIAL_JTAG + case logger::UART_SELECTION_USB_SERIAL_JTAG: + usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); + delay(10); + usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7 + break; +#endif // USE_LOGGER_USB_SERIAL_JTAG + default: + break; + } #endif } void ImprovSerialComponent::loop() { - const uint32_t now = millis(); - if (now - this->last_read_byte_ > 50) { + if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { + this->last_read_byte_ = 0; this->rx_buffer_.clear(); - this->last_read_byte_ = now; + ESP_LOGV(TAG, "Improv Serial timeout"); } - while (this->available_()) { - uint8_t byte = this->read_byte_(); - if (this->parse_improv_serial_byte_(byte)) { - this->last_read_byte_ = now; + auto byte = this->read_byte_(); + while (byte.has_value()) { + if (this->parse_improv_serial_byte_(byte.value())) { + this->last_read_byte_ = millis(); } else { + this->last_read_byte_ = 0; this->rx_buffer_.clear(); } + byte = this->read_byte_(); } if (this->state_ == improv::STATE_PROVISIONING) { @@ -99,16 +157,24 @@ std::vector ImprovSerialComponent::build_rpc_settings_response_(improv: urls.push_back(this->get_formatted_next_url_()); } #ifdef USE_WEBSERVER - auto ip = wifi::global_wifi_component->wifi_sta_ip(); - std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); - urls.push_back(webserver_url); + for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { + if (ip.is_ip4()) { + std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); + urls.push_back(webserver_url); + break; + } + } #endif std::vector data = improv::build_rpc_response(command, urls, false); return data; } std::vector ImprovSerialComponent::build_version_info_() { +#ifdef ESPHOME_PROJECT_NAME + std::vector infos = {ESPHOME_PROJECT_NAME, ESPHOME_PROJECT_VERSION, ESPHOME_VARIANT, App.get_name()}; +#else std::vector infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()}; +#endif std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); return data; }; @@ -136,7 +202,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_scanning(); + wifi::global_wifi_component->start_connecting(sta, false); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); @@ -247,3 +313,4 @@ ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidel } // namespace improv_serial } // namespace esphome +#endif diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 731f9f9984..5d2534c2fc 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -5,7 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" - +#ifdef USE_WIFI #include #include @@ -14,6 +14,14 @@ #endif #ifdef USE_ESP_IDF #include +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + defined(USE_ESP32_VARIANT_ESP32H2) +#include +#include +#endif +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include +#endif #endif namespace esphome { @@ -26,6 +34,7 @@ enum ImprovSerialType : uint8_t { TYPE_RPC_RESPONSE = 0x04 }; +static const uint16_t IMPROV_SERIAL_TIMEOUT = 100; static const uint8_t IMPROV_SERIAL_VERSION = 1; class ImprovSerialComponent : public Component, public improv_base::ImprovBase { @@ -48,8 +57,7 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { std::vector build_rpc_settings_response_(improv::Command command); std::vector build_version_info_(); - int available_(); - uint8_t read_byte_(); + optional read_byte_(); void write_data_(std::vector &data); #ifdef USE_ARDUINO @@ -70,3 +78,4 @@ extern ImprovSerialComponent } // namespace improv_serial } // namespace esphome +#endif diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 609f3d0f08..2fb3bea356 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -122,7 +122,7 @@ void INA219Component::setup() { this->calibration_lsb_ = lsb; auto calibration = uint32_t(0.04096f / (0.000001 * lsb * this->shunt_resistance_ohm_)); - ESP_LOGV(TAG, " Using LSB=%u calibration=%u", lsb, calibration); + ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration); if (!this->write_byte_16(INA219_REGISTER_CALIBRATION, calibration)) { this->mark_failed(); return; diff --git a/esphome/components/ina219/ina219.h b/esphome/components/ina219/ina219.h index 31cd22375e..a6c0f2bc4c 100644 --- a/esphome/components/ina219/ina219.h +++ b/esphome/components/ina219/ina219.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include + namespace esphome { namespace ina219 { diff --git a/esphome/components/ina226/__init__.py b/esphome/components/ina226/__init__.py index e69de29bb2..ca1f2caa31 100644 --- a/esphome/components/ina226/__init__.py +++ b/esphome/components/ina226/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Sergio303", "@latonita"] diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index 2e30a5ac01..7a57c118af 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -1,6 +1,7 @@ #include "ina226.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include namespace esphome { namespace ina226 { @@ -32,31 +33,37 @@ static const uint8_t INA226_REGISTER_POWER = 0x03; static const uint8_t INA226_REGISTER_CURRENT = 0x04; static const uint8_t INA226_REGISTER_CALIBRATION = 0x05; +static const uint16_t INA226_ADC_TIMES[] = {140, 204, 332, 588, 1100, 2116, 4156, 8244}; +static const uint16_t INA226_ADC_AVG_SAMPLES[] = {1, 4, 16, 64, 128, 256, 512, 1024}; + void INA226Component::setup() { ESP_LOGCONFIG(TAG, "Setting up INA226..."); - // Config Register - // 0bx000000000000000 << 15 RESET Bit (1 -> trigger reset) - if (!this->write_byte_16(INA226_REGISTER_CONFIG, 0x8000)) { + + ConfigurationRegister config; + + config.reset = 1; + if (!this->write_byte_16(INA226_REGISTER_CONFIG, config.raw)) { this->mark_failed(); return; } delay(1); - uint16_t config = 0x0000; + config.raw = 0; + config.reserved = 0b100; // as per datasheet // Averaging Mode AVG Bit Settings[11:9] (000 -> 1 sample, 001 -> 4 sample, 111 -> 1024 samples) - config |= 0b0000001000000000; + config.avg_samples = this->adc_avg_samples_; // Bus Voltage Conversion Time VBUSCT Bit Settings [8:6] (100 -> 1.1ms, 111 -> 8.244 ms) - config |= 0b0000000100000000; + config.bus_voltage_conversion_time = this->adc_time_voltage_; // Shunt Voltage Conversion Time VSHCT Bit Settings [5:3] (100 -> 1.1ms, 111 -> 8.244 ms) - config |= 0b0000000000100000; + config.shunt_voltage_conversion_time = this->adc_time_current_; // Mode Settings [2:0] Combinations (111 -> Shunt and Bus, Continuous) - config |= 0b0000000000000111; + config.mode = 0b111; - if (!this->write_byte_16(INA226_REGISTER_CONFIG, config)) { + if (!this->write_byte_16(INA226_REGISTER_CONFIG, config.raw)) { this->mark_failed(); return; } @@ -68,7 +75,7 @@ void INA226Component::setup() { auto calibration = uint32_t(0.00512 / (lsb * this->shunt_resistance_ohm_ / 1000000.0f)); - ESP_LOGV(TAG, " Using LSB=%u calibration=%u", lsb, calibration); + ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration); if (!this->write_byte_16(INA226_REGISTER_CALIBRATION, calibration)) { this->mark_failed(); @@ -86,6 +93,10 @@ void INA226Component::dump_config() { } LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " ADC Conversion Time Bus Voltage: %d", INA226_ADC_TIMES[this->adc_time_voltage_ & 0b111]); + ESP_LOGCONFIG(TAG, " ADC Conversion Time Shunt Voltage: %d", INA226_ADC_TIMES[this->adc_time_current_ & 0b111]); + ESP_LOGCONFIG(TAG, " ADC Averaging Samples: %d", INA226_ADC_AVG_SAMPLES[this->adc_avg_samples_ & 0b111]); + LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); LOG_SENSOR(" ", "Shunt Voltage", this->shunt_voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); @@ -101,7 +112,9 @@ void INA226Component::update() { this->status_set_warning(); return; } - float bus_voltage_v = int16_t(raw_bus_voltage) * 0.00125f; + // Convert for 2's compliment and signed value (though always positive) + float bus_voltage_v = this->twos_complement_(raw_bus_voltage, 16); + bus_voltage_v *= 0.00125f; this->bus_voltage_sensor_->publish_state(bus_voltage_v); } @@ -111,7 +124,9 @@ void INA226Component::update() { this->status_set_warning(); return; } - float shunt_voltage_v = int16_t(raw_shunt_voltage) * 0.0000025f; + // Convert for 2's compliment and signed value + float shunt_voltage_v = this->twos_complement_(raw_shunt_voltage, 16); + shunt_voltage_v *= 0.0000025f; this->shunt_voltage_sensor_->publish_state(shunt_voltage_v); } @@ -121,7 +136,9 @@ void INA226Component::update() { this->status_set_warning(); return; } - float current_ma = int16_t(raw_current) * (this->calibration_lsb_ / 1000.0f); + // Convert for 2's compliment and signed value + float current_ma = this->twos_complement_(raw_current, 16); + current_ma *= (this->calibration_lsb_ / 1000.0f); this->current_sensor_->publish_state(current_ma / 1000.0f); } @@ -138,5 +155,12 @@ void INA226Component::update() { this->status_clear_warning(); } +int32_t INA226Component::twos_complement_(int32_t val, uint8_t bits) { + if (val & ((uint32_t) 1 << (bits - 1))) { + val -= (uint32_t) 1 << bits; + } + return val; +} + } // namespace ina226 } // namespace esphome diff --git a/esphome/components/ina226/ina226.h b/esphome/components/ina226/ina226.h index a551cb3430..61214fea0e 100644 --- a/esphome/components/ina226/ina226.h +++ b/esphome/components/ina226/ina226.h @@ -7,6 +7,40 @@ namespace esphome { namespace ina226 { +enum AdcTime : uint16_t { + ADC_TIME_140US = 0, + ADC_TIME_204US = 1, + ADC_TIME_332US = 2, + ADC_TIME_588US = 3, + ADC_TIME_1100US = 4, + ADC_TIME_2116US = 5, + ADC_TIME_4156US = 6, + ADC_TIME_8244US = 7 +}; + +enum AdcAvgSamples : uint16_t { + ADC_AVG_SAMPLES_1 = 0, + ADC_AVG_SAMPLES_4 = 1, + ADC_AVG_SAMPLES_16 = 2, + ADC_AVG_SAMPLES_64 = 3, + ADC_AVG_SAMPLES_128 = 4, + ADC_AVG_SAMPLES_256 = 5, + ADC_AVG_SAMPLES_512 = 6, + ADC_AVG_SAMPLES_1024 = 7 +}; + +union ConfigurationRegister { + uint16_t raw; + struct { + uint16_t mode : 3; + AdcTime shunt_voltage_conversion_time : 3; + AdcTime bus_voltage_conversion_time : 3; + AdcAvgSamples avg_samples : 3; + uint16_t reserved : 3; + uint16_t reset : 1; + } __attribute__((packed)); +}; + class INA226Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; @@ -16,6 +50,10 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice { void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; } + void set_adc_time_voltage(AdcTime time) { adc_time_voltage_ = time; } + void set_adc_time_current(AdcTime time) { adc_time_current_ = time; } + void set_adc_avg_samples(AdcAvgSamples samples) { adc_avg_samples_ = samples; } + void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { bus_voltage_sensor_ = bus_voltage_sensor; } void set_shunt_voltage_sensor(sensor::Sensor *shunt_voltage_sensor) { shunt_voltage_sensor_ = shunt_voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } @@ -24,11 +62,16 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice { protected: float shunt_resistance_ohm_; float max_current_a_; + AdcTime adc_time_voltage_{AdcTime::ADC_TIME_1100US}; + AdcTime adc_time_current_{AdcTime::ADC_TIME_1100US}; + AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_4}; uint32_t calibration_lsb_; sensor::Sensor *bus_voltage_sensor_{nullptr}; sensor::Sensor *shunt_voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; + + int32_t twos_complement_(int32_t val, uint8_t bits); }; } // namespace ina226 diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py index ee4036ce7e..0accc58c00 100644 --- a/esphome/components/ina226/sensor.py +++ b/esphome/components/ina226/sensor.py @@ -16,15 +16,49 @@ from esphome.const import ( UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, + CONF_VOLTAGE, ) DEPENDENCIES = ["i2c"] +CONF_ADC_AVERAGING = "adc_averaging" +CONF_ADC_TIME = "adc_time" + ina226_ns = cg.esphome_ns.namespace("ina226") INA226Component = ina226_ns.class_( "INA226Component", cg.PollingComponent, i2c.I2CDevice ) +AdcTime = ina226_ns.enum("AdcTime") +ADC_TIMES = { + 140: AdcTime.ADC_TIME_140US, + 204: AdcTime.ADC_TIME_204US, + 332: AdcTime.ADC_TIME_332US, + 588: AdcTime.ADC_TIME_588US, + 1100: AdcTime.ADC_TIME_1100US, + 2116: AdcTime.ADC_TIME_2116US, + 4156: AdcTime.ADC_TIME_4156US, + 8244: AdcTime.ADC_TIME_8244US, +} + +AdcAvgSamples = ina226_ns.enum("AdcAvgSamples") +ADC_AVG_SAMPLES = { + 1: AdcAvgSamples.ADC_AVG_SAMPLES_1, + 4: AdcAvgSamples.ADC_AVG_SAMPLES_4, + 16: AdcAvgSamples.ADC_AVG_SAMPLES_16, + 64: AdcAvgSamples.ADC_AVG_SAMPLES_64, + 128: AdcAvgSamples.ADC_AVG_SAMPLES_128, + 256: AdcAvgSamples.ADC_AVG_SAMPLES_256, + 512: AdcAvgSamples.ADC_AVG_SAMPLES_512, + 1024: AdcAvgSamples.ADC_AVG_SAMPLES_1024, +} + + +def validate_adc_time(value): + value = cv.positive_time_period_microseconds(value).total_microseconds + return cv.enum(ADC_TIMES, int=True)(value) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -59,6 +93,18 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All( cv.current, cv.Range(min=0.0) ), + cv.Optional(CONF_ADC_TIME, default="1100 us"): cv.Any( + validate_adc_time, + cv.Schema( + { + cv.Required(CONF_VOLTAGE): validate_adc_time, + cv.Required(CONF_CURRENT): validate_adc_time, + } + ), + ), + cv.Optional(CONF_ADC_AVERAGING, default=4): cv.enum( + ADC_AVG_SAMPLES, int=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -72,9 +118,18 @@ async def to_code(config): await i2c.register_i2c_device(var, config) cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE])) - cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT])) + adc_time_config = config[CONF_ADC_TIME] + if isinstance(adc_time_config, dict): + cg.add(var.set_adc_time_voltage(adc_time_config[CONF_VOLTAGE])) + cg.add(var.set_adc_time_current(adc_time_config[CONF_CURRENT])) + else: + cg.add(var.set_adc_time_voltage(adc_time_config)) + cg.add(var.set_adc_time_current(adc_time_config)) + + cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING])) + if CONF_BUS_VOLTAGE in config: sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE]) cg.add(var.set_bus_voltage_sensor(sens)) diff --git a/esphome/components/ina260/sensor.py b/esphome/components/ina260/sensor.py index 048e713afa..732d15d9ca 100644 --- a/esphome/components/ina260/sensor.py +++ b/esphome/components/ina260/sensor.py @@ -16,7 +16,7 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] -CODEOWNERS = ["@MrEditor97"] +CODEOWNERS = ["@mreditor97"] ina260_ns = cg.esphome_ns.namespace("ina260") INA260Component = ina260_ns.class_( diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py new file mode 100644 index 0000000000..35b5baa83e --- /dev/null +++ b/esphome/components/ina2xx_base/__init__.py @@ -0,0 +1,255 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_BUS_VOLTAGE, + CONF_CURRENT, + CONF_ENERGY, + CONF_MAX_CURRENT, + CONF_MODEL, + CONF_NAME, + CONF_POWER, + CONF_SHUNT_RESISTANCE, + CONF_SHUNT_VOLTAGE, + CONF_TEMPERATURE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_VOLT, + UNIT_WATT_HOURS, + UNIT_WATT, +) + +CODEOWNERS = ["@latonita"] + +CONF_ADC_AVERAGING = "adc_averaging" +CONF_ADC_RANGE = "adc_range" +CONF_ADC_TIME = "adc_time" +CONF_CHARGE = "charge" +CONF_CHARGE_COULOMBS = "charge_coulombs" +CONF_ENERGY_JOULES = "energy_joules" +CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient" +UNIT_AMPERE_HOURS = "Ah" +UNIT_COULOMB = "C" +UNIT_JOULE = "J" +UNIT_MILLIVOLT = "mV" + +ina2xx_base_ns = cg.esphome_ns.namespace("ina2xx_base") +INA2XX = ina2xx_base_ns.class_("INA2XX", cg.PollingComponent) + +AdcTime = ina2xx_base_ns.enum("AdcTime") +ADC_TIMES = { + 50: AdcTime.ADC_TIME_50US, + 84: AdcTime.ADC_TIME_84US, + 150: AdcTime.ADC_TIME_150US, + 280: AdcTime.ADC_TIME_280US, + 540: AdcTime.ADC_TIME_540US, + 1052: AdcTime.ADC_TIME_1052US, + 2074: AdcTime.ADC_TIME_2074US, + 4120: AdcTime.ADC_TIME_4120US, +} + +AdcAvgSamples = ina2xx_base_ns.enum("AdcAvgSamples") +ADC_SAMPLES = { + 1: AdcAvgSamples.ADC_AVG_SAMPLES_1, + 4: AdcAvgSamples.ADC_AVG_SAMPLES_4, + 16: AdcAvgSamples.ADC_AVG_SAMPLES_16, + 64: AdcAvgSamples.ADC_AVG_SAMPLES_64, + 128: AdcAvgSamples.ADC_AVG_SAMPLES_128, + 256: AdcAvgSamples.ADC_AVG_SAMPLES_256, + 512: AdcAvgSamples.ADC_AVG_SAMPLES_512, + 1024: AdcAvgSamples.ADC_AVG_SAMPLES_1024, +} + +SENSOR_MODEL_OPTIONS = { + CONF_ENERGY: ["INA228", "INA229"], + CONF_ENERGY_JOULES: ["INA228", "INA229"], + CONF_CHARGE: ["INA228", "INA229"], + CONF_CHARGE_COULOMBS: ["INA228", "INA229"], +} + + +def validate_model_config(config): + model = config[CONF_MODEL] + + for key in config: + if key in SENSOR_MODEL_OPTIONS: + if model not in SENSOR_MODEL_OPTIONS[key]: + raise cv.Invalid( + f"Device model '{model}' does not support '{key}' sensor" + ) + + tempco = config[CONF_TEMPERATURE_COEFFICIENT] + if tempco > 0 and model not in ["INA228", "INA229"]: + raise cv.Invalid( + f"Device model '{model}' does not support temperature coefficient" + ) + + return config + + +def validate_adc_time(value): + value = cv.positive_time_period_microseconds(value).total_microseconds + return cv.enum(ADC_TIMES, int=True)(value) + + +INA2XX_SCHEMA = cv.Schema( + { + cv.Required(CONF_SHUNT_RESISTANCE): cv.All(cv.resistance, cv.Range(min=0.0)), + cv.Required(CONF_MAX_CURRENT): cv.All(cv.current, cv.Range(min=0.0)), + cv.Optional(CONF_ADC_RANGE, default=0): cv.int_range(min=0, max=1), + cv.Optional(CONF_ADC_TIME, default="4120 us"): cv.Any( + validate_adc_time, + { + cv.Optional(CONF_BUS_VOLTAGE, default="4120 us"): validate_adc_time, + cv.Optional(CONF_SHUNT_VOLTAGE, default="4120 us"): validate_adc_time, + cv.Optional(CONF_TEMPERATURE, default="4120 us"): validate_adc_time, + }, + ), + cv.Optional(CONF_ADC_AVERAGING, default=128): cv.enum(ADC_SAMPLES, int=True), + cv.Optional(CONF_TEMPERATURE_COEFFICIENT, default=0): cv.int_range( + min=0, max=16383 + ), + cv.Optional(CONF_SHUNT_VOLTAGE): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIVOLT, + accuracy_decimals=5, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_BUS_VOLTAGE): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=5, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_TEMPERATURE): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=5, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_CURRENT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=8, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_POWER): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=6, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ENERGY): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=8, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ENERGY_JOULES): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_JOULE, + accuracy_decimals=8, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_CHARGE): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE_HOURS, + accuracy_decimals=8, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_CHARGE_COULOMBS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COULOMB, + accuracy_decimals=8, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def setup_ina2xx(var, config): + await cg.register_component(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + + cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE])) + cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT])) + cg.add(var.set_adc_range(config[CONF_ADC_RANGE])) + cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING])) + cg.add(var.set_shunt_tempco(config[CONF_TEMPERATURE_COEFFICIENT])) + + adc_time_config = config[CONF_ADC_TIME] + if isinstance(adc_time_config, dict): + cg.add(var.set_adc_time_bus_voltage(adc_time_config[CONF_BUS_VOLTAGE])) + cg.add(var.set_adc_time_shunt_voltage(adc_time_config[CONF_SHUNT_VOLTAGE])) + cg.add(var.set_adc_time_die_temperature(adc_time_config[CONF_TEMPERATURE])) + else: + cg.add(var.set_adc_time_bus_voltage(adc_time_config)) + cg.add(var.set_adc_time_shunt_voltage(adc_time_config)) + cg.add(var.set_adc_time_die_temperature(adc_time_config)) + + if conf := config.get(CONF_SHUNT_VOLTAGE): + sens = await sensor.new_sensor(conf) + cg.add(var.set_shunt_voltage_sensor(sens)) + + if conf := config.get(CONF_BUS_VOLTAGE): + sens = await sensor.new_sensor(conf) + cg.add(var.set_bus_voltage_sensor(sens)) + + if conf := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(conf) + cg.add(var.set_die_temperature_sensor(sens)) + + if conf := config.get(CONF_CURRENT): + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + + if conf := config.get(CONF_POWER): + sens = await sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) + + if conf := config.get(CONF_ENERGY): + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor_wh(sens)) + + if conf := config.get(CONF_ENERGY_JOULES): + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor_j(sens)) + + if conf := config.get(CONF_CHARGE): + sens = await sensor.new_sensor(conf) + cg.add(var.set_charge_sensor_ah(sens)) + + if conf := config.get(CONF_CHARGE_COULOMBS): + sens = await sensor.new_sensor(conf) + cg.add(var.set_charge_sensor_c(sens)) diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp new file mode 100644 index 0000000000..924bf91e5e --- /dev/null +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -0,0 +1,604 @@ +#include "ina2xx_base.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome { +namespace ina2xx_base { + +static const char *const TAG = "ina2xx"; + +#define OKFAILED(b) ((b) ? "OK" : "FAILED") + +static const uint16_t ADC_TIMES[8] = {50, 84, 150, 280, 540, 1052, 2074, 4120}; +static const uint16_t ADC_SAMPLES[8] = {1, 4, 16, 64, 128, 256, 512, 1024}; + +static const char *get_device_name(INAModel model) { + switch (model) { + case INAModel::INA_228: + return "INA228"; + case INAModel::INA_229: + return "INA229"; + case INAModel::INA_238: + return "INA238"; + case INAModel::INA_239: + return "INA239"; + case INAModel::INA_237: + return "INA237"; + default: + return "UNKNOWN"; + } +}; + +static bool check_model_and_device_match(INAModel model, uint16_t dev_id) { + switch (model) { + case INAModel::INA_228: + return dev_id == 0x228; + case INAModel::INA_229: + return dev_id == 0x229; + case INAModel::INA_238: + return dev_id == 0x238; + case INAModel::INA_239: + return dev_id == 0x239; + case INAModel::INA_237: + return dev_id == 0x237; + default: + return false; + } +} + +void INA2XX::setup() { + ESP_LOGCONFIG(TAG, "Setting up INA2xx..."); + + if (!this->reset_config_()) { + ESP_LOGE(TAG, "Reset failed, check connection"); + this->mark_failed(); + return; + } + delay(2); + + if (!this->check_device_model_()) { + ESP_LOGE(TAG, "Device not supported or model selected improperly in yaml file"); + this->mark_failed(); + return; + } + delay(1); + + this->configure_adc_range_(); + delay(1); + + this->configure_adc_(); + delay(1); + + this->configure_shunt_(); + delay(1); + + this->configure_shunt_tempco_(); + delay(1); + + this->state_ = State::IDLE; +} + +float INA2XX::get_setup_priority() const { return setup_priority::DATA; } + +void INA2XX::update() { + ESP_LOGD(TAG, "Updating"); + if (this->is_ready() && this->state_ == State::IDLE) { + ESP_LOGD(TAG, "Initiating new data collection"); + this->state_ = State::DATA_COLLECTION_1; + return; + } +} + +void INA2XX::loop() { + if (this->is_ready()) { + switch (this->state_) { + case State::NOT_INITIALIZED: + case State::IDLE: + break; + + case State::DATA_COLLECTION_1: + this->full_loop_is_okay_ = true; + + if (this->shunt_voltage_sensor_ != nullptr) { + float shunt_voltage{0}; + this->full_loop_is_okay_ &= this->read_shunt_voltage_mv_(shunt_voltage); + this->shunt_voltage_sensor_->publish_state(shunt_voltage); + } + this->state_ = State::DATA_COLLECTION_2; + break; + + case State::DATA_COLLECTION_2: + if (this->bus_voltage_sensor_ != nullptr) { + float bus_voltage{0}; + this->full_loop_is_okay_ &= this->read_bus_voltage_(bus_voltage); + this->bus_voltage_sensor_->publish_state(bus_voltage); + } + this->state_ = State::DATA_COLLECTION_3; + break; + + case State::DATA_COLLECTION_3: + if (this->die_temperature_sensor_ != nullptr) { + float die_temperature{0}; + this->full_loop_is_okay_ &= this->read_die_temp_c_(die_temperature); + this->die_temperature_sensor_->publish_state(die_temperature); + } + this->state_ = State::DATA_COLLECTION_4; + break; + + case State::DATA_COLLECTION_4: + if (this->current_sensor_ != nullptr) { + float current{0}; + this->full_loop_is_okay_ &= this->read_current_a_(current); + this->current_sensor_->publish_state(current); + } + this->state_ = State::DATA_COLLECTION_5; + break; + + case State::DATA_COLLECTION_5: + if (this->power_sensor_ != nullptr) { + float power{0}; + this->full_loop_is_okay_ &= this->read_power_w_(power); + this->power_sensor_->publish_state(power); + } + this->state_ = State::DATA_COLLECTION_6; + break; + + case State::DATA_COLLECTION_6: + if (this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) { + if (this->energy_sensor_j_ != nullptr || this->energy_sensor_wh_ != nullptr || + this->charge_sensor_c_ != nullptr || this->charge_sensor_ah_ != nullptr) { + this->read_diagnostics_and_act_(); + } + if (this->energy_sensor_j_ != nullptr || this->energy_sensor_wh_ != nullptr) { + double energy_j{0}, energy_wh{0}; + this->full_loop_is_okay_ &= this->read_energy_(energy_j, energy_wh); + if (this->energy_sensor_j_ != nullptr) + this->energy_sensor_j_->publish_state(energy_j); + if (this->energy_sensor_wh_ != nullptr) + this->energy_sensor_wh_->publish_state(energy_wh); + } + } + this->state_ = State::DATA_COLLECTION_7; + break; + + case State::DATA_COLLECTION_7: + if (this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) { + if (this->charge_sensor_c_ != nullptr || this->charge_sensor_ah_ != nullptr) { + double charge_c{0}, charge_ah{0}; + this->full_loop_is_okay_ &= this->read_charge_(charge_c, charge_ah); + if (this->charge_sensor_c_ != nullptr) + this->charge_sensor_c_->publish_state(charge_c); + if (this->charge_sensor_ah_ != nullptr) + this->charge_sensor_ah_->publish_state(charge_ah); + } + } + this->state_ = State::DATA_COLLECTION_8; + break; + + case State::DATA_COLLECTION_8: + if (this->full_loop_is_okay_) { + this->status_clear_warning(); + } else { + this->status_set_warning(); + } + this->state_ = State::IDLE; + break; + + default: + ESP_LOGW(TAG, "Unknown state of the component, might be due to memory corruption"); + break; + } + } +} + +void INA2XX::dump_config() { + ESP_LOGCONFIG(TAG, "INA2xx:"); + ESP_LOGCONFIG(TAG, " Device model = %s", get_device_name(this->ina_model_)); + + if (this->device_mismatch_) { + ESP_LOGE(TAG, " Device model mismatch. Found device with ID = %x. Please check your configuration.", + this->dev_id_); + } + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with INA2xx failed!"); + } + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Shunt resistance = %f Ohm", this->shunt_resistance_ohm_); + ESP_LOGCONFIG(TAG, " Max current = %f A", this->max_current_a_); + ESP_LOGCONFIG(TAG, " Shunt temp coeff = %d ppm/°C", this->shunt_tempco_ppm_c_); + ESP_LOGCONFIG(TAG, " ADCRANGE = %d (%s)", (uint8_t) this->adc_range_, this->adc_range_ ? "±40.96 mV" : "±163.84 mV"); + ESP_LOGCONFIG(TAG, " CURRENT_LSB = %f", this->current_lsb_); + ESP_LOGCONFIG(TAG, " SHUNT_CAL = %d", this->shunt_cal_); + + ESP_LOGCONFIG(TAG, " ADC Samples = %d; ADC times: Bus = %d μs, Shunt = %d μs, Temp = %d μs", + ADC_SAMPLES[0b111 & (uint8_t) this->adc_avg_samples_], + ADC_TIMES[0b111 & (uint8_t) this->adc_time_bus_voltage_], + ADC_TIMES[0b111 & (uint8_t) this->adc_time_shunt_voltage_], + ADC_TIMES[0b111 & (uint8_t) this->adc_time_die_temperature_]); + + ESP_LOGCONFIG(TAG, " Device is %s", get_device_name(this->ina_model_)); + + LOG_SENSOR(" ", "Shunt Voltage", this->shunt_voltage_sensor_); + LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); + LOG_SENSOR(" ", "Die Temperature", this->die_temperature_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + + if (this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) { + LOG_SENSOR(" ", "Energy J", this->energy_sensor_j_); + LOG_SENSOR(" ", "Energy Wh", this->energy_sensor_wh_); + LOG_SENSOR(" ", "Charge C", this->charge_sensor_c_); + LOG_SENSOR(" ", "Charge Ah", this->charge_sensor_ah_); + } +} + +bool INA2XX::reset_energy_counters() { + if (this->ina_model_ != INAModel::INA_228 && this->ina_model_ != INAModel::INA_229) { + return false; + } + ESP_LOGV(TAG, "reset_energy_counters"); + + ConfigurationRegister cfg{0}; + auto ret = this->read_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16); + cfg.RSTACC = true; + cfg.ADCRANGE = this->adc_range_; + ret = ret && this->write_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16); + + this->energy_overflows_count_ = 0; + this->charge_overflows_count_ = 0; + return ret; +} + +bool INA2XX::reset_config_() { + ESP_LOGV(TAG, "Reset"); + ConfigurationRegister cfg{0}; + cfg.RST = true; + return this->write_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16); +} + +bool INA2XX::check_device_model_() { + constexpr uint16_t manufacturer_ti = 0x5449; // "TI" + + uint16_t manufacturer_id{0}, rev_id{0}; + this->read_unsigned_16_(RegisterMap::REG_MANUFACTURER_ID, manufacturer_id); + if (!this->read_unsigned_16_(RegisterMap::REG_DEVICE_ID, this->dev_id_)) { + this->dev_id_ = 0; + ESP_LOGV(TAG, "Can't read device ID"); + }; + rev_id = this->dev_id_ & 0x0F; + this->dev_id_ >>= 4; + ESP_LOGI(TAG, "Manufacturer: 0x%04X, Device ID: 0x%04X, Revision: %d", manufacturer_id, this->dev_id_, rev_id); + + if (manufacturer_id != manufacturer_ti) { + ESP_LOGE(TAG, "Manufacturer ID doesn't match original 0x5449"); + this->device_mismatch_ = true; + return false; + } + + if (this->dev_id_ == 0x228 || this->dev_id_ == 0x229) { + ESP_LOGI(TAG, "Supported device found: INA%x, 85-V, 20-Bit, Ultra-Precise Power/Energy/Charge Monitor", + this->dev_id_); + } else if (this->dev_id_ == 0x238 || this->dev_id_ == 0x239) { + ESP_LOGI(TAG, "Supported device found: INA%x, 85-V, 16-Bit, High-Precision Power Monitor", this->dev_id_); + } else if (this->dev_id_ == 0x0 || this->dev_id_ == 0xFF) { + ESP_LOGI(TAG, "We assume device is: INA237 85-V, 16-Bit, Precision Power Monitor"); + this->dev_id_ = 0x237; + } else { + ESP_LOGE(TAG, "Unknown device ID %x.", this->dev_id_); + this->device_mismatch_ = true; + return false; + } + + // Check user-selected model agains what we have found. Mark as failed if selected model != found model + if (!check_model_and_device_match(this->ina_model_, this->dev_id_)) { + ESP_LOGE(TAG, "Selected model %s doesn't match found device INA%x", get_device_name(this->ina_model_), + this->dev_id_); + this->device_mismatch_ = true; + return false; + } + + // setup device coefficients + if (this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) { + this->cfg_.vbus_lsb = 0.0001953125f; + this->cfg_.v_shunt_lsb_range0 = 0.0003125f; + this->cfg_.v_shunt_lsb_range1 = 0.000078125f; + this->cfg_.shunt_cal_scale = 13107.2f * 1000000.0f; + this->cfg_.current_lsb_scale_factor = -19; + this->cfg_.die_temp_lsb = 0.0078125f; + this->cfg_.power_coeff = 3.2f; + this->cfg_.energy_coeff = 16.0f * 3.2f; + } else { + this->cfg_.vbus_lsb = 0.0031250000f; + this->cfg_.v_shunt_lsb_range0 = 0.0050000f; + this->cfg_.v_shunt_lsb_range1 = 0.001250000f; + this->cfg_.shunt_cal_scale = 819.2f * 1000000.0f; + this->cfg_.current_lsb_scale_factor = -15; + this->cfg_.die_temp_lsb = 0.1250000f; + this->cfg_.power_coeff = 0.2f; + this->cfg_.energy_coeff = 0.0f; // N/A + } + + return true; +} + +bool INA2XX::configure_adc_range_() { + ESP_LOGV(TAG, "Setting ADCRANGE = %d", (uint8_t) this->adc_range_); + ConfigurationRegister cfg{0}; + auto ret = this->read_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16); + cfg.ADCRANGE = this->adc_range_; + ret = ret && this->write_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16); + + return ret; +} + +bool INA2XX::configure_adc_() { + bool ret{false}; + AdcConfigurationRegister adc_cfg{0}; + adc_cfg.MODE = 0x0F; // Fh = Continuous bus voltage, shunt voltage and temperature + adc_cfg.VBUSCT = this->adc_time_bus_voltage_; + adc_cfg.VSHCT = this->adc_time_shunt_voltage_; + adc_cfg.VTCT = this->adc_time_die_temperature_; + adc_cfg.AVG = this->adc_avg_samples_; + ret = this->write_unsigned_16_(RegisterMap::REG_ADC_CONFIG, adc_cfg.raw_u16); + return ret; +} + +bool INA2XX::configure_shunt_() { + this->current_lsb_ = ldexp(this->max_current_a_, this->cfg_.current_lsb_scale_factor); + this->shunt_cal_ = (uint16_t) (this->cfg_.shunt_cal_scale * this->current_lsb_ * this->shunt_resistance_ohm_); + if (this->adc_range_) + this->shunt_cal_ *= 4; + + if (this->shunt_cal_ & 0x8000) { + // cant be more than 15 bits + 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_); + return this->write_unsigned_16_(RegisterMap::REG_SHUNT_CAL, this->shunt_cal_); +} + +bool INA2XX::configure_shunt_tempco_() { + // Only for 228/229 + // unsigned 14-bit value + // 0x0000 = 0 ppm/°C + // 0x3FFF = 16383 ppm/°C + if ((this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) && + this->shunt_tempco_ppm_c_ > 0) { + return this->write_unsigned_16_(RegisterMap::REG_SHUNT_TEMPCO, this->shunt_tempco_ppm_c_ & 0x3FFF); + } + return true; +} + +bool INA2XX::read_shunt_voltage_mv_(float &volt_out) { + // Two's complement value + // 228, 229 - 24bit: 20(23-4) + 4(3-0) res + // 237, 238, 239 - 16bit + + bool ret{false}; + float volt_reading{0}; + uint64_t raw{0}; + if (this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) { + ret = this->read_unsigned_(RegisterMap::REG_VSHUNT, 3, raw); + raw >>= 4; + volt_reading = this->two_complement_(raw, 20); + } else { + ret = this->read_unsigned_(RegisterMap::REG_VSHUNT, 2, raw); + volt_reading = this->two_complement_(raw, 16); + } + + if (ret) { + volt_out = (this->adc_range_ ? this->cfg_.v_shunt_lsb_range1 : this->cfg_.v_shunt_lsb_range0) * volt_reading; + } + + ESP_LOGV(TAG, "read_shunt_voltage_mv_ ret=%s, shunt_cal=%d, reading_lsb=%f", OKFAILED(ret), this->shunt_cal_, + volt_reading); + + return ret; +} + +bool INA2XX::read_bus_voltage_(float &volt_out) { + // Two's complement value + // 228, 229 - 24bit: 20(23-4) + 4(3-0) res + // 237, 238, 239 - 16bit + + bool ret{false}; + float volt_reading{0}; + uint64_t raw{0}; + if (this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) { + ret = this->read_unsigned_(RegisterMap::REG_VBUS, 3, raw); + raw >>= 4; + volt_reading = this->two_complement_(raw, 20); + } else { + ret = this->read_unsigned_(RegisterMap::REG_VBUS, 2, raw); + volt_reading = this->two_complement_(raw, 16); + } + if (ret) { + volt_out = this->cfg_.vbus_lsb * (float) volt_reading; + } + + ESP_LOGV(TAG, "read_bus_voltage_ ret=%s, reading_lsb=%f", OKFAILED(ret), volt_reading); + return ret; +} + +bool INA2XX::read_die_temp_c_(float &temp_out) { + // Two's complement value + // 228, 229 - 16bit + // 237, 238, 239 - 16bit: 12(15-4) + 4(3-0) res + + bool ret{false}; + float temp_reading{0}; + uint64_t raw{0}; + + if (this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) { + ret = this->read_unsigned_(RegisterMap::REG_DIETEMP, 2, raw); + temp_reading = this->two_complement_(raw, 16); + } else { + ret = this->read_unsigned_(RegisterMap::REG_DIETEMP, 2, raw); + raw >>= 4; + temp_reading = this->two_complement_(raw, 12); + } + if (ret) { + temp_out = this->cfg_.die_temp_lsb * (float) temp_reading; + } + + ESP_LOGV(TAG, "read_die_temp_c_ ret=%s, reading_lsb=%f", OKFAILED(ret), temp_reading); + return ret; +} + +bool INA2XX::read_current_a_(float &s_out) { + // Two's complement value + // 228, 229 - 24bit: 20(23-4) + 4(3-0) res + // 237, 238, 239 - 16bit + bool ret{false}; + float amps_reading{0}; + uint64_t raw{0}; + + if (this->ina_model_ == INAModel::INA_228 || this->ina_model_ == INAModel::INA_229) { + ret = this->read_unsigned_(RegisterMap::REG_CURRENT, 3, raw); + raw >>= 4; + amps_reading = this->two_complement_(raw, 20); + } else { + ret = this->read_unsigned_(RegisterMap::REG_CURRENT, 2, raw); + amps_reading = this->two_complement_(raw, 16); + } + + ESP_LOGV(TAG, "read_current_a_ ret=%s. current_lsb=%f. reading_lsb=%f", OKFAILED(ret), this->current_lsb_, + amps_reading); + if (ret) { + amps_out = this->current_lsb_ * (float) amps_reading; + } + + return ret; +} + +bool INA2XX::read_power_w_(float &power_out) { + // Unsigned value + // 228, 229 - 24bit + // 237, 238, 239 - 24bit + uint64_t power_reading{0}; + auto ret = this->read_unsigned_((uint8_t) RegisterMap::REG_POWER, 3, power_reading); + + ESP_LOGV(TAG, "read_power_w_ ret=%s, reading_lsb=%" PRIu32, OKFAILED(ret), (uint32_t) power_reading); + if (ret) { + power_out = this->cfg_.power_coeff * this->current_lsb_ * (float) power_reading; + } + + return ret; +} + +bool INA2XX::read_energy_(double &joules_out, double &watt_hours_out) { + // Unsigned value + // 228, 229 - 40bit + // 237, 238, 239 - not available + if (this->ina_model_ != INAModel::INA_228 && this->ina_model_ != INAModel::INA_229) { + joules_out = 0; + return false; + } + uint64_t joules_reading = 0; + uint64_t previous_energy = this->energy_overflows_count_ * (((uint64_t) 1) << 40); + auto ret = this->read_unsigned_((uint8_t) RegisterMap::REG_ENERGY, 5, joules_reading); + + ESP_LOGV(TAG, "read_energy_j_ ret=%s, reading_lsb=0x%" PRIX64 ", current_lsb=%f, overflow_cnt=%" PRIu32, + OKFAILED(ret), joules_reading, this->current_lsb_, this->energy_overflows_count_); + if (ret) { + joules_out = this->cfg_.energy_coeff * this->current_lsb_ * (double) joules_reading + (double) previous_energy; + watt_hours_out = joules_out / 3600.0; + } + return ret; +} + +bool INA2XX::read_charge_(double &coulombs_out, double &_hours_out) { + // Two's complement value + // 228, 229 - 40bit + // 237, 238, 239 - not available + if (this->ina_model_ != INAModel::INA_228 && this->ina_model_ != INAModel::INA_229) { + coulombs_out = 0; + return false; + } + + // and what to do with this? datasheet doesnt tell us what if charge is negative + uint64_t previous_charge = this->charge_overflows_count_ * (((uint64_t) 1) << 39); + double coulombs_reading = 0; + uint64_t raw{0}; + auto ret = this->read_unsigned_((uint8_t) RegisterMap::REG_CHARGE, 5, raw); + coulombs_reading = this->two_complement_(raw, 40); + + ESP_LOGV(TAG, "read_charge_c_ ret=%d, curr_charge=%f + 39-bit overflow_cnt=%" PRIu32, ret, coulombs_reading, + this->charge_overflows_count_); + if (ret) { + coulombs_out = this->current_lsb_ * (double) coulombs_reading + (double) previous_charge; + amp_hours_out = coulombs_out / 3600.0; + } + return ret; +} + +bool INA2XX::read_diagnostics_and_act_() { + if (this->ina_model_ != INAModel::INA_228 && this->ina_model_ != INAModel::INA_229) { + return false; + } + + DiagnosticRegister diag{0}; + auto ret = this->read_unsigned_16_(RegisterMap::REG_DIAG_ALRT, diag.raw_u16); + ESP_LOGV(TAG, "read_diagnostics_and_act_ ret=%s, 0x%04X", OKFAILED(ret), diag.raw_u16); + + if (diag.ENERGYOF) { + this->energy_overflows_count_++; // 40-bit overflow + } + + if (diag.CHARGEOF) { + this->charge_overflows_count_++; // 39-bit overflow + } + + return ret; +} + +bool INA2XX::write_unsigned_16_(uint8_t reg, uint16_t val) { + uint16_t data_out = byteswap(val); + auto ret = this->write_ina_register(reg, (uint8_t *) &data_out, 2); + if (!ret) { + ESP_LOGV(TAG, "write_unsigned_16_ FAILED reg=0x%02X, val=0x%04X", reg, val); + } + return ret; +} + +bool INA2XX::read_unsigned_(uint8_t reg, uint8_t reg_size, uint64_t &data_out) { + static uint8_t rx_buf[5] = {0}; // max buffer size + + if (reg_size > 5) { + return false; + } + + auto ret = this->read_ina_register(reg, rx_buf, reg_size); + + // Combine bytes + data_out = rx_buf[0]; + for (uint8_t i = 1; i < reg_size; i++) { + data_out = (data_out << 8) | rx_buf[i]; + } + ESP_LOGV(TAG, "read_unsigned_ reg=0x%02X, ret=%s, len=%d, val=0x%" PRIX64, reg, OKFAILED(ret), reg_size, data_out); + + return ret; +} + +bool INA2XX::read_unsigned_16_(uint8_t reg, uint16_t &out) { + uint16_t data_in{0}; + auto ret = this->read_ina_register(reg, (uint8_t *) &data_in, 2); + out = byteswap(data_in); + ESP_LOGV(TAG, "read_unsigned_16_ 0x%02X, ret= %s, val=0x%04X", reg, OKFAILED(ret), out); + return ret; +} + +int64_t INA2XX::two_complement_(uint64_t value, uint8_t bits) { + if (value > (1ULL << (bits - 1))) { + return (int64_t) (value - (1ULL << bits)); + } else { + return (int64_t) value; + } +} +} // namespace ina2xx_base +} // namespace esphome diff --git a/esphome/components/ina2xx_base/ina2xx_base.h b/esphome/components/ina2xx_base/ina2xx_base.h new file mode 100644 index 0000000000..261c5321bf --- /dev/null +++ b/esphome/components/ina2xx_base/ina2xx_base.h @@ -0,0 +1,253 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace ina2xx_base { + +enum RegisterMap : uint8_t { + REG_CONFIG = 0x00, + REG_ADC_CONFIG = 0x01, + REG_SHUNT_CAL = 0x02, + REG_SHUNT_TEMPCO = 0x03, + REG_VSHUNT = 0x04, + REG_VBUS = 0x05, + REG_DIETEMP = 0x06, + REG_CURRENT = 0x07, + REG_POWER = 0x08, + REG_ENERGY = 0x09, + REG_CHARGE = 0x0A, + REG_DIAG_ALRT = 0x0B, + REG_SOVL = 0x0C, + REG_SUVL = 0x0D, + REG_BOVL = 0x0E, + REG_BUVL = 0x0F, + REG_TEMP_LIMIT = 0x10, + REG_PWR_LIMIT = 0x11, + REG_MANUFACTURER_ID = 0x3E, + REG_DEVICE_ID = 0x3F +}; + +enum AdcRange : uint16_t { + ADC_RANGE_0 = 0, + ADC_RANGE_1 = 1, +}; + +enum AdcTime : uint16_t { + ADC_TIME_50US = 0, + ADC_TIME_84US = 1, + ADC_TIME_150US = 2, + ADC_TIME_280US = 3, + ADC_TIME_540US = 4, + ADC_TIME_1052US = 5, + ADC_TIME_2074US = 6, + ADC_TIME_4120US = 7, +}; + +enum AdcAvgSamples : uint16_t { + ADC_AVG_SAMPLES_1 = 0, + ADC_AVG_SAMPLES_4 = 1, + ADC_AVG_SAMPLES_16 = 2, + ADC_AVG_SAMPLES_64 = 3, + ADC_AVG_SAMPLES_128 = 4, + ADC_AVG_SAMPLES_256 = 5, + ADC_AVG_SAMPLES_512 = 6, + ADC_AVG_SAMPLES_1024 = 7, +}; + +union ConfigurationRegister { + uint16_t raw_u16; + struct { + uint16_t reserved_0_3 : 4; // Reserved + AdcRange ADCRANGE : 1; // Shunt measurement range 0: ±163.84 mV, 1: ±40.96 mV + bool TEMPCOMP : 1; // Temperature compensation enable + uint16_t CONVDLY : 8; // Sets the Delay for initial ADC conversion in steps of 2 ms. + bool RSTACC : 1; // Reset counters + bool RST : 1; // Full device reset + } __attribute__((packed)); +}; + +union AdcConfigurationRegister { + uint16_t raw_u16; + struct { + AdcAvgSamples AVG : 3; + AdcTime VTCT : 3; // Voltage conversion time + AdcTime VSHCT : 3; // Shunt voltage conversion time + AdcTime VBUSCT : 3; // Bus voltage conversion time + uint16_t MODE : 4; + } __attribute__((packed)); +}; + +union TempCompensationRegister { + uint16_t raw_u16; + struct { + uint16_t TEMPCO : 14; + uint16_t reserved : 2; + } __attribute__((packed)); +}; + +union DiagnosticRegister { + uint16_t raw_u16; + struct { + bool MEMSTAT : 1; + bool CNVRF : 1; + bool POL : 1; + bool BUSUL : 1; + bool BUSOL : 1; + bool SHNTUL : 1; + bool SHNTOL : 1; + bool TMPOL : 1; + bool RESERVED1 : 1; + bool MATHOF : 1; + bool CHARGEOF : 1; + bool ENERGYOF : 1; + bool APOL : 1; + bool SLOWALERT : 1; + bool CNVR : 1; + bool ALATCH : 1; + } __attribute__((packed)); +}; + +enum INAModel : uint8_t { INA_UNKNOWN = 0, INA_228, INA_229, INA_238, INA_239, INA_237 }; + +class INA2XX : public PollingComponent { + public: + void setup() override; + float get_setup_priority() const override; + void update() override; + void loop() override; + void dump_config() override; + + void set_shunt_resistance_ohm(float shunt_resistance_ohm) { this->shunt_resistance_ohm_ = shunt_resistance_ohm; } + void set_max_current_a(float max_current_a) { this->max_current_a_ = max_current_a; } + void set_adc_range(uint8_t range) { this->adc_range_ = (range == 0) ? AdcRange::ADC_RANGE_0 : AdcRange::ADC_RANGE_1; } + void set_adc_time_bus_voltage(AdcTime time) { this->adc_time_bus_voltage_ = time; } + void set_adc_time_shunt_voltage(AdcTime time) { this->adc_time_shunt_voltage_ = time; } + void set_adc_time_die_temperature(AdcTime time) { this->adc_time_die_temperature_ = time; } + void set_adc_avg_samples(AdcAvgSamples samples) { this->adc_avg_samples_ = samples; } + void set_shunt_tempco(uint16_t coeff) { this->shunt_tempco_ppm_c_ = coeff; } + + void set_shunt_voltage_sensor(sensor::Sensor *sensor) { this->shunt_voltage_sensor_ = sensor; } + void set_bus_voltage_sensor(sensor::Sensor *sensor) { this->bus_voltage_sensor_ = sensor; } + void set_die_temperature_sensor(sensor::Sensor *sensor) { this->die_temperature_sensor_ = sensor; } + void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; } + void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } + void set_energy_sensor_j(sensor::Sensor *sensor) { this->energy_sensor_j_ = sensor; } + void set_energy_sensor_wh(sensor::Sensor *sensor) { this->energy_sensor_wh_ = sensor; } + void set_charge_sensor_c(sensor::Sensor *sensor) { this->charge_sensor_c_ = sensor; } + void set_charge_sensor_ah(sensor::Sensor *sensor) { this->charge_sensor_ah_ = sensor; } + + void set_model(INAModel model) { this->ina_model_ = model; } + + bool reset_energy_counters(); + + protected: + bool reset_config_(); + bool check_device_model_(); + bool configure_adc_(); + + bool configure_shunt_(); + bool configure_shunt_tempco_(); + bool configure_adc_range_(); + + bool read_shunt_voltage_mv_(float &volt_out); + bool read_bus_voltage_(float &volt_out); + bool read_die_temp_c_(float &temp); + bool read_current_a_(float &s_out); + bool read_power_w_(float &power_out); + bool read_energy_(double &joules_out, double &watt_hours_out); + bool read_charge_(double &coulombs_out, double &_hours_out); + + bool read_diagnostics_and_act_(); + + // + // User configuration + // + float shunt_resistance_ohm_; + float max_current_a_; + AdcRange adc_range_{AdcRange::ADC_RANGE_0}; + AdcTime adc_time_bus_voltage_{AdcTime::ADC_TIME_4120US}; + AdcTime adc_time_shunt_voltage_{AdcTime::ADC_TIME_4120US}; + AdcTime adc_time_die_temperature_{AdcTime::ADC_TIME_4120US}; + AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_128}; + uint16_t shunt_tempco_ppm_c_{0}; + + // + // Calculated coefficients + // + uint16_t shunt_cal_{0}; + float current_lsb_{0}; + + uint32_t energy_overflows_count_{0}; + uint32_t charge_overflows_count_{0}; + + // + // Sensor objects + // + sensor::Sensor *shunt_voltage_sensor_{nullptr}; + sensor::Sensor *bus_voltage_sensor_{nullptr}; + sensor::Sensor *die_temperature_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *energy_sensor_j_{nullptr}; + sensor::Sensor *energy_sensor_wh_{nullptr}; + sensor::Sensor *charge_sensor_c_{nullptr}; + sensor::Sensor *charge_sensor_ah_{nullptr}; + + // + // FSM states + // + enum class State : uint8_t { + NOT_INITIALIZED = 0x0, + IDLE, + DATA_COLLECTION_1, + DATA_COLLECTION_2, + DATA_COLLECTION_3, + DATA_COLLECTION_4, + DATA_COLLECTION_5, + DATA_COLLECTION_6, + DATA_COLLECTION_7, + DATA_COLLECTION_8, + } state_{State::NOT_INITIALIZED}; + + bool full_loop_is_okay_{true}; + + // + // Device model + // + INAModel ina_model_{INAModel::INA_UNKNOWN}; + uint16_t dev_id_{0}; + bool device_mismatch_{false}; + + // + // Device specific parameters + // + struct { + float vbus_lsb; + float v_shunt_lsb_range0; + float v_shunt_lsb_range1; + float shunt_cal_scale; + int8_t current_lsb_scale_factor; + float die_temp_lsb; + float power_coeff; + float energy_coeff; + } cfg_; + + // + // Register read/write + // + bool read_unsigned_(uint8_t reg, uint8_t reg_size, uint64_t &data_out); + bool read_unsigned_16_(uint8_t reg, uint16_t &out); + bool write_unsigned_16_(uint8_t reg, uint16_t val); + + int64_t two_complement_(uint64_t value, uint8_t bits); + + // + // Interface-specific implementation + // + virtual bool read_ina_register(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool write_ina_register(uint8_t a_register, const uint8_t *data, size_t len) = 0; +}; +} // namespace ina2xx_base +} // namespace esphome diff --git a/esphome/components/ina2xx_i2c/__init__.py b/esphome/components/ina2xx_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp new file mode 100644 index 0000000000..d28525635d --- /dev/null +++ b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp @@ -0,0 +1,39 @@ +#include "ina2xx_i2c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ina2xx_i2c { + +static const char *const TAG = "ina2xx_i2c"; + +void INA2XXI2C::setup() { + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + INA2XX::setup(); +} + +void INA2XXI2C::dump_config() { + INA2XX::dump_config(); + LOG_I2C_DEVICE(this); +} + +bool INA2XXI2C::read_ina_register(uint8_t reg, uint8_t *data, size_t len) { + auto ret = this->read_register(reg, data, len, false); + if (ret != i2c::ERROR_OK) { + ESP_LOGE(TAG, "read_ina_register_ failed. Reg=0x%02X Err=%d", reg, ret); + } + return ret == i2c::ERROR_OK; +} + +bool INA2XXI2C::write_ina_register(uint8_t reg, const uint8_t *data, size_t len) { + auto ret = this->write_register(reg, data, len); + if (ret != i2c::ERROR_OK) { + ESP_LOGE(TAG, "write_register failed. Reg=0x%02X Err=%d", reg, ret); + } + return ret == i2c::ERROR_OK; +} +} // namespace ina2xx_i2c +} // namespace esphome diff --git a/esphome/components/ina2xx_i2c/ina2xx_i2c.h b/esphome/components/ina2xx_i2c/ina2xx_i2c.h new file mode 100644 index 0000000000..c90b9bf190 --- /dev/null +++ b/esphome/components/ina2xx_i2c/ina2xx_i2c.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ina2xx_base/ina2xx_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ina2xx_i2c { + +class INA2XXI2C : public ina2xx_base::INA2XX, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + protected: + bool read_ina_register(uint8_t reg, uint8_t *data, size_t len) override; + bool write_ina_register(uint8_t reg, const uint8_t *data, size_t len) override; +}; + +} // namespace ina2xx_i2c +} // namespace esphome diff --git a/esphome/components/ina2xx_i2c/sensor.py b/esphome/components/ina2xx_i2c/sensor.py new file mode 100644 index 0000000000..57ddcef17a --- /dev/null +++ b/esphome/components/ina2xx_i2c/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ina2xx_base, i2c +from esphome.const import CONF_ID, CONF_MODEL + +AUTO_LOAD = ["ina2xx_base"] +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +ina2xx_i2c = cg.esphome_ns.namespace("ina2xx_i2c") +INA2XX_I2C = ina2xx_i2c.class_("INA2XXI2C", ina2xx_base.INA2XX, i2c.I2CDevice) + +INAModel = ina2xx_base.ina2xx_base_ns.enum("INAModel") +INA_MODELS = { + "INA228": INAModel.INA_228, + "INA238": INAModel.INA_238, + "INA237": INAModel.INA_237, +} + +CONFIG_SCHEMA = cv.All( + ina2xx_base.INA2XX_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(INA2XX_I2C), + cv.Required(CONF_MODEL): cv.enum(INA_MODELS, upper=True), + } + ).extend(i2c.i2c_device_schema(0x40)), + ina2xx_base.validate_model_config, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ina2xx_base.setup_ina2xx(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ina2xx_spi/__init__.py b/esphome/components/ina2xx_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ina2xx_spi/ina2xx_spi.cpp b/esphome/components/ina2xx_spi/ina2xx_spi.cpp new file mode 100644 index 0000000000..3e04a87665 --- /dev/null +++ b/esphome/components/ina2xx_spi/ina2xx_spi.cpp @@ -0,0 +1,38 @@ +#include "ina2xx_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ina2xx_spi { + +static const char *const TAG = "ina2xx_spi"; + +void INA2XXSPI::setup() { + this->spi_setup(); + INA2XX::setup(); +} + +void INA2XXSPI::dump_config() { + INA2XX::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +bool INA2XXSPI::read_ina_register(uint8_t reg, uint8_t *data, size_t len) { + reg = (reg << 2); // top 6 bits + reg |= 0x01; // read + this->enable(); + this->write_byte(reg); + this->read_array(data, len); + this->disable(); + return true; +} + +bool INA2XXSPI::write_ina_register(uint8_t reg, const uint8_t *data, size_t len) { + reg = (reg << 2); // top 6 bits + this->enable(); + this->write_byte(reg); + this->write_array(data, len); + this->disable(); + return true; +} +} // namespace ina2xx_spi +} // namespace esphome diff --git a/esphome/components/ina2xx_spi/ina2xx_spi.h b/esphome/components/ina2xx_spi/ina2xx_spi.h new file mode 100644 index 0000000000..3b21518d34 --- /dev/null +++ b/esphome/components/ina2xx_spi/ina2xx_spi.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ina2xx_base/ina2xx_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ina2xx_spi { + +class INA2XXSPI : public ina2xx_base::INA2XX, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + + protected: + bool read_ina_register(uint8_t reg, uint8_t *data, size_t len) override; + bool write_ina_register(uint8_t reg, const uint8_t *data, size_t len) override; +}; +} // namespace ina2xx_spi +} // namespace esphome diff --git a/esphome/components/ina2xx_spi/sensor.py b/esphome/components/ina2xx_spi/sensor.py new file mode 100644 index 0000000000..e7ae51d516 --- /dev/null +++ b/esphome/components/ina2xx_spi/sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ina2xx_base, spi +from esphome.const import CONF_ID, CONF_MODEL + +AUTO_LOAD = ["ina2xx_base"] +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["spi"] + +ina2xx_spi = cg.esphome_ns.namespace("ina2xx_spi") +INA2XX_SPI = ina2xx_spi.class_("INA2XXSPI", ina2xx_base.INA2XX, spi.SPIDevice) + +INAModel = ina2xx_base.ina2xx_base_ns.enum("INAModel") +INA_MODELS = { + "INA229": INAModel.INA_229, + "INA239": INAModel.INA_239, +} + +CONFIG_SCHEMA = cv.All( + ina2xx_base.INA2XX_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(INA2XX_SPI), + cv.Required(CONF_MODEL): cv.enum(INA_MODELS, upper=True), + } + ).extend(spi.spi_device_schema(cs_pin_required=True)), + ina2xx_base.validate_model_config, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ina2xx_base.setup_ina2xx(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py index aa11fb3172..cdd0b5ade5 100644 --- a/esphome/components/inkbird_ibsth1_mini/sensor.py +++ b/esphome/components/inkbird_ibsth1_mini/sensor.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_BATTERY_LEVEL, + CONF_EXTERNAL_TEMPERATURE, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, @@ -19,8 +20,6 @@ from esphome.const import ( CODEOWNERS = ["@fkirill"] DEPENDENCIES = ["esp32_ble_tracker"] -CONF_EXTERNAL_TEMPERATURE = "external_temperature" - inkbird_ibsth1_mini_ns = cg.esphome_ns.namespace("inkbird_ibsth1_mini") InkbirdIbstH1Mini = inkbird_ibsth1_mini_ns.class_( "InkbirdIbstH1Mini", esp32_ble_tracker.ESPBTDeviceListener, cg.Component diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index a17f37c920..8fe7f7d41d 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_MODEL, + CONF_OE_PIN, CONF_PAGES, CONF_WAKEUP_PIN, ) @@ -29,7 +30,6 @@ CONF_GREYSCALE = "greyscale" CONF_GMOD_PIN = "gmod_pin" CONF_GPIO0_ENABLE_PIN = "gpio0_enable_pin" CONF_LE_PIN = "le_pin" -CONF_OE_PIN = "oe_pin" CONF_PARTIAL_UPDATING = "partial_updating" CONF_POWERUP_PIN = "powerup_pin" CONF_SPH_PIN = "sph_pin" @@ -39,7 +39,11 @@ CONF_VCOM_PIN = "vcom_pin" inkplate6_ns = cg.esphome_ns.namespace("inkplate6") Inkplate6 = inkplate6_ns.class_( - "Inkplate6", cg.PollingComponent, i2c.I2CDevice, display.DisplayBuffer + "Inkplate6", + cg.PollingComponent, + i2c.I2CDevice, + display.Display, + display.DisplayBuffer, ) InkplateModel = inkplate6_ns.enum("InkplateModel") @@ -48,6 +52,9 @@ MODELS = { "inkplate_6": InkplateModel.INKPLATE_6, "inkplate_10": InkplateModel.INKPLATE_10, "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, + "inkplate_6_v2": InkplateModel.INKPLATE_6_V2, + "inkplate_5": InkplateModel.INKPLATE_5, + "inkplate_5_v2": InkplateModel.INKPLATE_5_V2, } CONFIG_SCHEMA = cv.All( @@ -109,13 +116,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await i2c.register_i2c_device(var, config) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp index e6fb9b773c..f4d0fedf83 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -55,6 +55,9 @@ void Inkplate6::setup() { this->wakeup_pin_->digital_write(false); } +/** + * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed. + */ void Inkplate6::initialize_() { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); ExternalRAMAllocator allocator32(ExternalRAMAllocator::ALLOW_FAILURE); @@ -69,9 +72,9 @@ void Inkplate6::initialize_() { if (this->buffer_ != nullptr) allocator.deallocate(this->buffer_, buffer_size); if (this->glut_ != nullptr) - allocator32.deallocate(this->glut_, 256 * (this->model_ == INKPLATE_6_PLUS ? 9 : 8)); + allocator32.deallocate(this->glut_, 256 * 9); if (this->glut2_ != nullptr) - allocator32.deallocate(this->glut2_, 256 * (this->model_ == INKPLATE_6_PLUS ? 9 : 8)); + allocator32.deallocate(this->glut2_, 256 * 9); this->buffer_ = allocator.allocate(buffer_size); if (this->buffer_ == nullptr) { @@ -80,7 +83,7 @@ void Inkplate6::initialize_() { return; } if (this->greyscale_) { - uint8_t glut_size = (this->model_ == INKPLATE_6_PLUS ? 9 : 8); + uint8_t glut_size = 9; this->glut_ = allocator32.allocate(256 * glut_size); if (this->glut_ == nullptr) { @@ -95,12 +98,14 @@ void Inkplate6::initialize_() { return; } + const auto *const waveform3_bit = waveform3BitAll[this->model_]; + for (int i = 0; i < glut_size; i++) { for (uint32_t j = 0; j < 256; j++) { - uint8_t z = (waveform3Bit[j & 0x07][i] << 2) | (waveform3Bit[(j >> 4) & 0x07][i]); + uint8_t z = (waveform3_bit[j & 0x07][i] << 2) | (waveform3_bit[(j >> 4) & 0x07][i]); this->glut_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); - z = ((waveform3Bit[j & 0x07][i] << 2) | (waveform3Bit[(j >> 4) & 0x07][i])) << 4; + z = ((waveform3_bit[j & 0x07][i] << 2) | (waveform3_bit[(j >> 4) & 0x07][i])) << 4; this->glut2_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); } @@ -339,13 +344,16 @@ void Inkplate6::display1b_() { clean_fast_(1, 21); clean_fast_(2, 1); clean_fast_(0, 12); + clean_fast_(2, 1); } uint32_t clock = (1 << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); - for (int k = 0; k < 4; k++) { + int rep = (this->model_ == INKPLATE_6_V2) ? 5 : 4; + + for (int k = 0; k < rep; k++) { buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); for (int i = 0, im = this->get_height_internal(); i < im; i++) { @@ -365,8 +373,11 @@ void Inkplate6::display1b_() { GPIO.out_w1ts = this->pin_lut_[data] | clock; GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = clock; - GPIO.out_w1tc = data_mask | clock; + // New Inkplate6 panel doesn't need last clock + if (this->model_ != INKPLATE_6_V2) { + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; + } vscan_end_(); } delayMicroseconds(230); @@ -392,8 +403,11 @@ void Inkplate6::display1b_() { GPIO.out_w1ts = this->pin_lut_[data] | clock; GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = clock; - GPIO.out_w1tc = data_mask | clock; + // New Inkplate6 panel doesn't need last clock + if (this->model_ != INKPLATE_6_V2) { + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; + } vscan_end_(); } delayMicroseconds(230); @@ -415,8 +429,11 @@ void Inkplate6::display1b_() { GPIO.out_w1ts = send | clock; GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = send | clock; - GPIO.out_w1tc = data_mask | clock; + // New Inkplate6 panel doesn't need last clock + if (this->model_ != INKPLATE_6_V2) { + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; + } vscan_end_(); } delayMicroseconds(230); @@ -450,13 +467,14 @@ void Inkplate6::display3b_() { clean_fast_(1, 21); clean_fast_(2, 1); clean_fast_(0, 12); + clean_fast_(2, 1); } uint32_t clock = (1 << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); uint32_t pos; uint32_t data; - uint8_t glut_size = this->model_ == INKPLATE_6_PLUS ? 9 : 8; + uint8_t glut_size = 9; for (int k = 0; k < glut_size; k++) { pos = this->get_buffer_length_(); vscan_start_(); @@ -479,8 +497,11 @@ void Inkplate6::display3b_() { GPIO.out_w1ts = data | clock; GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = clock; - GPIO.out_w1tc = data_mask | clock; + // New Inkplate6 panel doesn't need last clock + if (this->model_ != INKPLATE_6_V2) { + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; + } vscan_end_(); } delayMicroseconds(230); @@ -517,10 +538,12 @@ bool Inkplate6::partial_update_() { } ESP_LOGV(TAG, "Partial update buffer built after (%ums)", millis() - start_time); + int rep = (this->model_ == INKPLATE_6_V2) ? 6 : 5; + eink_on_(); uint32_t clock = (1 << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); - for (int k = 0; k < 5; k++) { + for (int k = 0; k < rep; k++) { vscan_start_(); const uint8_t *data_ptr = &this->partial_buffer_2_[(this->get_buffer_length_() * 2) - 1]; for (int i = 0; i < this->get_height_internal(); i++) { @@ -531,8 +554,11 @@ bool Inkplate6::partial_update_() { GPIO.out_w1ts = this->pin_lut_[data] | clock; GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = clock; - GPIO.out_w1tc = data_mask | clock; + // New Inkplate6 panel doesn't need last clock + if (this->model_ != INKPLATE_6_V2) { + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; + } vscan_end_(); } delayMicroseconds(230); @@ -634,8 +660,11 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { GPIO.out_w1ts = clock; GPIO.out_w1tc = clock; } - GPIO.out_w1ts = send | clock; - GPIO.out_w1tc = clock; + // New Inkplate6 panel doesn't need last clock + if (this->model_ != INKPLATE_6_V2) { + GPIO.out_w1ts = send | clock; + GPIO.out_w1tc = clock; + } vscan_end_(); } delayMicroseconds(230); diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index e650b57631..ca2ad46f1e 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -14,9 +14,12 @@ enum InkplateModel : uint8_t { INKPLATE_6 = 0, INKPLATE_10 = 1, INKPLATE_6_PLUS = 2, + INKPLATE_6_V2 = 3, + INKPLATE_5 = 4, + INKPLATE_5_V2 = 5, }; -class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { +class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { public: const uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, 0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; @@ -28,18 +31,66 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public const uint8_t pixelMaskLUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; const uint8_t pixelMaskGLUT[2] = {0x0F, 0xF0}; - const uint8_t waveform3Bit[8][8] = {{0, 1, 1, 0, 0, 1, 1, 0}, {0, 1, 2, 1, 1, 2, 1, 0}, {1, 1, 1, 2, 2, 1, 0, 0}, - {0, 0, 0, 1, 1, 1, 2, 0}, {2, 1, 1, 1, 2, 1, 2, 0}, {2, 2, 1, 1, 2, 1, 2, 0}, - {1, 1, 1, 2, 1, 2, 2, 0}, {0, 0, 0, 0, 0, 0, 2, 0}}; - const uint8_t waveform3Bit6Plus[8][9] = {{0, 0, 0, 0, 0, 2, 1, 1, 0}, {0, 0, 2, 1, 1, 1, 2, 1, 0}, - {0, 2, 2, 2, 1, 1, 2, 1, 0}, {0, 0, 2, 2, 2, 1, 2, 1, 0}, - {0, 0, 0, 0, 2, 2, 2, 1, 0}, {0, 0, 2, 1, 2, 1, 1, 2, 0}, - {0, 0, 2, 2, 2, 1, 1, 2, 0}, {0, 0, 0, 0, 2, 2, 2, 2, 0}}; + const uint8_t waveform3BitAll[6][8][9] = {// INKPLATE_6 + {{0, 1, 1, 0, 0, 1, 1, 0, 0}, + {0, 1, 2, 1, 1, 2, 1, 0, 0}, + {1, 1, 1, 2, 2, 1, 0, 0, 0}, + {0, 0, 0, 1, 1, 1, 2, 0, 0}, + {2, 1, 1, 1, 2, 1, 2, 0, 0}, + {2, 2, 1, 1, 2, 1, 2, 0, 0}, + {1, 1, 1, 2, 1, 2, 2, 0, 0}, + {0, 0, 0, 0, 0, 0, 2, 0, 0}}, + // INKPLATE_10 + {{0, 0, 0, 0, 0, 0, 0, 1, 0}, + {0, 0, 0, 2, 2, 2, 1, 1, 0}, + {0, 0, 2, 1, 1, 2, 2, 1, 0}, + {0, 1, 2, 2, 1, 2, 2, 1, 0}, + {0, 0, 2, 1, 2, 2, 2, 1, 0}, + {0, 2, 2, 2, 2, 2, 2, 1, 0}, + {0, 0, 0, 0, 0, 2, 1, 2, 0}, + {0, 0, 0, 2, 2, 2, 2, 2, 0}}, + // INKPLATE_6_PLUS + {{0, 0, 0, 0, 0, 2, 1, 1, 0}, + {0, 0, 2, 1, 1, 1, 2, 1, 0}, + {0, 2, 2, 2, 1, 1, 2, 1, 0}, + {0, 0, 2, 2, 2, 1, 2, 1, 0}, + {0, 0, 0, 0, 2, 2, 2, 1, 0}, + {0, 0, 2, 1, 2, 1, 1, 2, 0}, + {0, 0, 2, 2, 2, 1, 1, 2, 0}, + {0, 0, 0, 0, 2, 2, 2, 2, 0}}, + // INKPLATE_6_V2 + {{1, 0, 1, 0, 1, 1, 1, 0, 0}, + {0, 0, 0, 1, 1, 1, 1, 0, 0}, + {1, 1, 1, 1, 0, 2, 1, 0, 0}, + {1, 1, 1, 2, 2, 1, 1, 0, 0}, + {1, 1, 1, 1, 2, 2, 1, 0, 0}, + {0, 1, 1, 1, 2, 2, 1, 0, 0}, + {0, 0, 0, 0, 1, 1, 2, 0, 0}, + {0, 0, 0, 0, 0, 1, 2, 0, 0}}, + // INKPLATE_5 + {{0, 0, 1, 1, 0, 1, 1, 1, 0}, + {0, 1, 1, 1, 1, 2, 0, 1, 0}, + {1, 2, 2, 0, 2, 1, 1, 1, 0}, + {1, 1, 1, 2, 0, 1, 1, 2, 0}, + {0, 1, 1, 1, 2, 0, 1, 2, 0}, + {0, 0, 0, 1, 1, 2, 1, 2, 0}, + {1, 1, 1, 2, 0, 2, 1, 2, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0}}, + // INKPLATE_5_V2 + {{0, 0, 1, 1, 2, 1, 1, 1, 0}, + {1, 1, 2, 2, 1, 2, 1, 1, 0}, + {0, 1, 2, 2, 1, 1, 2, 1, 0}, + {0, 0, 1, 1, 1, 1, 1, 2, 0}, + {1, 2, 1, 2, 1, 1, 1, 2, 0}, + {0, 1, 1, 1, 2, 0, 1, 2, 0}, + {1, 1, 1, 2, 2, 2, 1, 2, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0}}}; void set_greyscale(bool greyscale) { this->greyscale_ = greyscale; - this->initialize_(); this->block_partial_ = true; + if (this->is_ready()) + this->initialize_(); } void set_partial_updating(bool partial_updating) { this->partial_updating_ = partial_updating; } void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } @@ -111,10 +162,14 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public void pins_as_outputs_(); int get_width_internal() override { - if (this->model_ == INKPLATE_6) { + if (this->model_ == INKPLATE_6 || this->model_ == INKPLATE_6_V2) { return 800; } else if (this->model_ == INKPLATE_10) { return 1200; + } else if (this->model_ == INKPLATE_5) { + return 960; + } else if (this->model_ == INKPLATE_5_V2) { + return 1280; } else if (this->model_ == INKPLATE_6_PLUS) { return 1024; } @@ -122,8 +177,12 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public } int get_height_internal() override { - if (this->model_ == INKPLATE_6) { + if (this->model_ == INKPLATE_6 || this->model_ == INKPLATE_6_V2) { return 600; + } else if (this->model_ == INKPLATE_5) { + return 540; + } else if (this->model_ == INKPLATE_5_V2) { + return 720; } else if (this->model_ == INKPLATE_10) { return 825; } else if (this->model_ == INKPLATE_6_PLUS) { diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index a387708263..9ef5cbecd5 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -7,13 +7,19 @@ extern "C" { uint8_t temprature_sens_read(); } -#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ + defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "driver/temp_sensor.h" #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 #include "Arduino.h" #endif // USE_RP2040 +#ifdef USE_BK72XX +extern "C" { +uint32_t temp_single_get_current_temperature(uint32_t *temp_value); +} +#endif // USE_BK72XX namespace esphome { namespace internal_temperature { @@ -29,7 +35,8 @@ 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_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ + defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); temp_sensor_set_config(tsens); temp_sensor_start(); @@ -46,6 +53,18 @@ void InternalTemperatureSensor::update() { temperature = analogReadTemp(); success = (temperature != 0.0f); #endif // USE_RP2040 +#ifdef USE_BK72XX + uint32_t raw, result; + result = temp_single_get_current_temperature(&raw); + success = (result == 0); +#if defined(USE_LIBRETINY_VARIANT_BK7231N) + temperature = raw * -0.38f + 156.0f; +#elif defined(USE_LIBRETINY_VARIANT_BK7231T) + temperature = raw * 0.04f; +#else // USE_LIBRETINY_VARIANT + temperature = raw * 0.128f; +#endif // USE_LIBRETINY_VARIANT +#endif // USE_BK72XX if (success && std::isfinite(temperature)) { this->publish_state(temperature); } else { diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index 8d462bd801..809d7a40b9 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -12,6 +12,9 @@ from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, KEY_CORE, KEY_FRAMEWORK_VERSION, + PLATFORM_ESP32, + PLATFORM_RP2040, + PLATFORM_BK72XX, ) from esphome.core import CORE @@ -49,7 +52,7 @@ CONFIG_SCHEMA = cv.All( state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")), - cv.only_on(["esp32", "rp2040"]), + cv.only_on([PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX]), validate_config, ) diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py index 4514f80ba3..db3232c4b0 100644 --- a/esphome/components/interval/__init__.py +++ b/esphome/components/interval/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_INTERVAL +from esphome.const import CONF_ID, CONF_INTERVAL, CONF_STARTUP_DELAY CODEOWNERS = ["@esphome/core"] interval_ns = cg.esphome_ns.namespace("interval") @@ -13,6 +13,9 @@ CONFIG_SCHEMA = automation.validate_automation( cv.Schema( { cv.GenerateID(): cv.declare_id(IntervalTrigger), + cv.Optional( + CONF_STARTUP_DELAY, default="0s" + ): cv.positive_time_period_milliseconds, cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA) @@ -26,3 +29,4 @@ async def to_code(config): await automation.build_automation(var, [], conf) cg.add(var.set_update_interval(conf[CONF_INTERVAL])) + cg.add(var.set_startup_delay(conf[CONF_STARTUP_DELAY])) diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 605ac868f3..5b8bc3081f 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -8,8 +8,26 @@ namespace interval { class IntervalTrigger : public Trigger<>, public PollingComponent { public: - void update() override { this->trigger(); } + void update() override { + if (this->started_) + this->trigger(); + } + + void setup() override { + if (this->startup_delay_ == 0) { + this->started_ = true; + } else { + this->set_timeout(this->startup_delay_, [this] { this->started_ = true; }); + } + } + + void set_startup_delay(const uint32_t startup_delay) { this->startup_delay_ = startup_delay; } + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + uint32_t startup_delay_{0}; + bool started_{false}; }; } // namespace interval diff --git a/esphome/components/jsn_sr04t/__init__.py b/esphome/components/jsn_sr04t/__init__.py new file mode 100644 index 0000000000..ef6335f316 --- /dev/null +++ b/esphome/components/jsn_sr04t/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Mafus1"] diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.cpp b/esphome/components/jsn_sr04t/jsn_sr04t.cpp new file mode 100644 index 0000000000..077d4e58ea --- /dev/null +++ b/esphome/components/jsn_sr04t/jsn_sr04t.cpp @@ -0,0 +1,73 @@ +#include "jsn_sr04t.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +// Very basic support for JSN_SR04T V3.0 distance sensor in mode 2 + +namespace esphome { +namespace jsn_sr04t { + +static const char *const TAG = "jsn_sr04t.sensor"; + +void Jsnsr04tComponent::update() { + this->write_byte(0x55); + ESP_LOGV(TAG, "Request read out from sensor"); +} + +void Jsnsr04tComponent::loop() { + while (this->available() > 0) { + uint8_t data; + this->read_byte(&data); + + ESP_LOGV(TAG, "Read byte from sensor: %x", data); + + if (this->buffer_.empty() && data != 0xFF) + continue; + + this->buffer_.push_back(data); + if (this->buffer_.size() == 4) + this->check_buffer_(); + } +} + +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; + } + + if (this->buffer_[3] == checksum) { + uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]); + if (distance > 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()); + } + } else { + ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); + } + this->buffer_.clear(); +} + +void Jsnsr04tComponent::dump_config() { + LOG_SENSOR("", "JST_SR04T Sensor", this); + switch (this->model_) { + case JSN_SR04T: + ESP_LOGCONFIG(TAG, " sensor model: jsn_sr04t"); + break; + case AJ_SR04M: + ESP_LOGCONFIG(TAG, " sensor model: aj_sr04m"); + break; + } + LOG_UPDATE_INTERVAL(this); +} + +} // namespace jsn_sr04t +} // namespace esphome diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.h b/esphome/components/jsn_sr04t/jsn_sr04t.h new file mode 100644 index 0000000000..2a22ff92ec --- /dev/null +++ b/esphome/components/jsn_sr04t/jsn_sr04t.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace jsn_sr04t { + +enum Model { + JSN_SR04T, + AJ_SR04M, +}; + +class Jsnsr04tComponent : public sensor::Sensor, public PollingComponent, public uart::UARTDevice { + public: + void set_model(Model model) { this->model_ = model; } + + // ========== INTERNAL METHODS ========== + void update() override; + void loop() override; + void dump_config() override; + + protected: + void check_buffer_(); + Model model_; + + std::vector buffer_; +}; + +} // namespace jsn_sr04t +} // namespace esphome diff --git a/esphome/components/jsn_sr04t/sensor.py b/esphome/components/jsn_sr04t/sensor.py new file mode 100644 index 0000000000..682cf06570 --- /dev/null +++ b/esphome/components/jsn_sr04t/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, + CONF_MODEL, +) + +CODEOWNERS = ["@Mafus1"] +DEPENDENCIES = ["uart"] + +jsn_sr04t_ns = cg.esphome_ns.namespace("jsn_sr04t") +Jsnsr04tComponent = jsn_sr04t_ns.class_( + "Jsnsr04tComponent", sensor.Sensor, cg.PollingComponent, uart.UARTDevice +) +Model = jsn_sr04t_ns.enum("Model") +MODEL = { + "jsn_sr04t": Model.JSN_SR04T, + "aj_sr04m": Model.AJ_SR04M, +} + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + Jsnsr04tComponent, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) + .extend( + { + cv.Optional(CONF_MODEL, default="jsn_sr04t"): cv.enum(MODEL, upper=False), + } + ) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "jsn_sr04t", + baud_rate=9600, + require_tx=True, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index f27d441804..89ec13fe5b 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -29,6 +29,8 @@ std::string build_json(const json_build_t &f) { const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #elif defined(USE_RP2040) const size_t free_heap = rp2040.getFreeHeap(); +#elif defined(USE_LIBRETINY) + const size_t free_heap = lt_heap_get_free(); #endif size_t request_size = std::min(free_heap, (size_t) 512); @@ -60,7 +62,7 @@ std::string build_json(const json_build_t &f) { } } -void parse_json(const std::string &data, const json_parse_t &f) { +bool parse_json(const std::string &data, const json_parse_t &f) { // Here we are allocating 1.5 times the data size, // with the heap size minus 2kb to be safe if less than that // as we can not have a true dynamic sized document. @@ -71,15 +73,16 @@ void parse_json(const std::string &data, const json_parse_t &f) { const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #elif defined(USE_RP2040) const size_t free_heap = rp2040.getFreeHeap(); +#elif defined(USE_LIBRETINY) + const size_t free_heap = lt_heap_get_free(); #endif - bool pass = false; size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); - do { + while (true) { DynamicJsonDocument json_document(request_size); if (json_document.capacity() == 0) { ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, free_heap); - return; + return false; } DeserializationError err = deserializeJson(json_document, data); json_document.shrinkToFit(); @@ -87,21 +90,21 @@ void parse_json(const std::string &data, const json_parse_t &f) { JsonObject root = json_document.as(); if (err == DeserializationError::Ok) { - pass = true; - f(root); + return f(root); } else if (err == DeserializationError::NoMemory) { if (request_size * 2 >= free_heap) { ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); - return; + return false; } ESP_LOGV(TAG, "Increasing memory allocation."); request_size *= 2; continue; } else { ESP_LOGE(TAG, "JSON parse error: %s", err.c_str()); - return; + return false; } - } while (!pass); + }; + return false; } } // namespace json diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 2299a4cfed..72d31c8afe 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -14,7 +14,7 @@ namespace esphome { namespace json { /// Callback function typedef for parsing JsonObjects. -using json_parse_t = std::function; +using json_parse_t = std::function; /// Callback function typedef for building JsonObjects. using json_build_t = std::function; @@ -23,7 +23,7 @@ using json_build_t = std::function; std::string build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. -void parse_json(const std::string &data, const json_parse_t &f); +bool parse_json(const std::string &data, const json_parse_t &f); } // namespace json } // namespace esphome diff --git a/esphome/components/kalman_combinator/__init__.py b/esphome/components/kalman_combinator/__init__.py index 3356e61bb2..e69de29bb2 100644 --- a/esphome/components/kalman_combinator/__init__.py +++ b/esphome/components/kalman_combinator/__init__.py @@ -1 +0,0 @@ -CODEOWNERS = ["@Cat-Ion"] diff --git a/esphome/components/kalman_combinator/kalman_combinator.cpp b/esphome/components/kalman_combinator/kalman_combinator.cpp deleted file mode 100644 index 50d8f03a93..0000000000 --- a/esphome/components/kalman_combinator/kalman_combinator.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "kalman_combinator.h" -#include "esphome/core/hal.h" -#include -#include - -namespace esphome { -namespace kalman_combinator { - -void KalmanCombinatorComponent::dump_config() { - ESP_LOGCONFIG("kalman_combinator", "Kalman Combinator:"); - ESP_LOGCONFIG("kalman_combinator", " Update variance: %f per ms", this->update_variance_value_); - ESP_LOGCONFIG("kalman_combinator", " Sensors:"); - for (const auto &sensor : this->sensors_) { - auto &entity = *sensor.first; - ESP_LOGCONFIG("kalman_combinator", " - %s", entity.get_name().c_str()); - } -} - -void KalmanCombinatorComponent::setup() { - for (const auto &sensor : this->sensors_) { - const auto stddev = sensor.second; - sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); - } -} - -void KalmanCombinatorComponent::add_source(Sensor *sensor, std::function const &stddev) { - this->sensors_.emplace_back(sensor, stddev); -} - -void KalmanCombinatorComponent::add_source(Sensor *sensor, float stddev) { - this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); -} - -void KalmanCombinatorComponent::update_variance_() { - uint32_t now = millis(); - - // Variance increases by update_variance_ each millisecond - auto dt = now - this->last_update_; - auto dv = this->update_variance_value_ * dt; - this->variance_ += dv; - this->last_update_ = now; -} - -void KalmanCombinatorComponent::correct_(float value, float stddev) { - if (std::isnan(value) || std::isinf(stddev)) { - return; - } - - if (std::isnan(this->state_) || std::isinf(this->variance_)) { - this->state_ = value; - this->variance_ = stddev * stddev; - if (this->std_dev_sensor_ != nullptr) { - this->std_dev_sensor_->publish_state(stddev); - } - return; - } - - this->update_variance_(); - - // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu - // Use the value with the smaller variance as mu1 to prevent precision errors - const bool this_first = this->variance_ < (stddev * stddev); - const float mu1 = this_first ? this->state_ : value; - const float mu2 = this_first ? value : this->state_; - - const float var1 = this_first ? this->variance_ : stddev * stddev; - const float var2 = this_first ? stddev * stddev : this->variance_; - - const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2); - const float var = var1 - (var1 * var1) / (var1 + var2); - - // Update and publish state - this->state_ = mu; - this->variance_ = var; - - this->publish_state(mu); - if (this->std_dev_sensor_ != nullptr) { - this->std_dev_sensor_->publish_state(std::sqrt(var)); - } -} -} // namespace kalman_combinator -} // namespace esphome diff --git a/esphome/components/kalman_combinator/kalman_combinator.h b/esphome/components/kalman_combinator/kalman_combinator.h deleted file mode 100644 index afbe3ece92..0000000000 --- a/esphome/components/kalman_combinator/kalman_combinator.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include -#include - -namespace esphome { -namespace kalman_combinator { - -class KalmanCombinatorComponent : public Component, public sensor::Sensor { - public: - KalmanCombinatorComponent() = default; - - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - - void dump_config() override; - void setup() override; - - void add_source(Sensor *sensor, std::function const &stddev); - void add_source(Sensor *sensor, float stddev); - void set_process_std_dev(float process_std_dev) { - this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f; - } - void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; } - - private: - void update_variance_(); - void correct_(float value, float stddev); - - // Source sensors and their error functions - std::vector>> sensors_; - - // Optional sensor for publishing the current error - sensor::Sensor *std_dev_sensor_{nullptr}; - - // Tick of the last update - uint32_t last_update_{0}; - // Change of the variance, per ms - float update_variance_value_{0.f}; - - // Best guess for the state and its variance - float state_{NAN}; - float variance_{INFINITY}; -}; -} // namespace kalman_combinator -} // namespace esphome diff --git a/esphome/components/kalman_combinator/sensor.py b/esphome/components/kalman_combinator/sensor.py index 28b96077cc..c19a17462d 100644 --- a/esphome/components/kalman_combinator/sensor.py +++ b/esphome/components/kalman_combinator/sensor.py @@ -1,90 +1,6 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor -from esphome.const import ( - CONF_ID, - CONF_SOURCE, - CONF_ACCURACY_DECIMALS, - CONF_DEVICE_CLASS, - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_UNIT_OF_MEASUREMENT, + +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" ) -from esphome.core.entity_helpers import inherit_property_from - -kalman_combinator_ns = cg.esphome_ns.namespace("kalman_combinator") -KalmanCombinatorComponent = kalman_combinator_ns.class_( - "KalmanCombinatorComponent", cg.Component, sensor.Sensor -) - -CONF_ERROR = "error" -CONF_SOURCES = "sources" -CONF_PROCESS_STD_DEV = "process_std_dev" -CONF_STD_DEV = "std_dev" - - -CONFIG_SCHEMA = ( - sensor.sensor_schema(KalmanCombinatorComponent) - .extend(cv.COMPONENT_SCHEMA) - .extend( - { - cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, - cv.Required(CONF_SOURCES): cv.ensure_list( - cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), - } - ), - ), - cv.Optional(CONF_STD_DEV): sensor.sensor_schema(), - } - ) -) - -# Inherit some sensor values from the first source, for both the state and the error value -properties_to_inherit = [ - CONF_ACCURACY_DECIMALS, - CONF_DEVICE_CLASS, - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_UNIT_OF_MEASUREMENT, - # CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing" -] -inherit_schema_for_state = [ - inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE]) - for property in properties_to_inherit -] -inherit_schema_for_std_dev = [ - inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE]) - for property in properties_to_inherit -] - -FINAL_VALIDATE_SCHEMA = cv.All( - CONFIG_SCHEMA.extend( - {cv.Required(CONF_ID): cv.use_id(KalmanCombinatorComponent)}, - extra=cv.ALLOW_EXTRA, - ), - *inherit_schema_for_state, - *inherit_schema_for_std_dev, -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await sensor.register_sensor(var, config) - - cg.add(var.set_process_std_dev(config[CONF_PROCESS_STD_DEV])) - for source_conf in config[CONF_SOURCES]: - source = await cg.get_variable(source_conf[CONF_SOURCE]) - error = await cg.templatable( - source_conf[CONF_ERROR], - [(float, "x")], - cg.float_, - ) - cg.add(var.add_source(source, error)) - - if CONF_STD_DEV in config: - sens = await sensor.new_sensor(config[CONF_STD_DEV]) - cg.add(var.set_std_dev_sensor(sens)) diff --git a/esphome/components/kamstrup_kmp/__init__.py b/esphome/components/kamstrup_kmp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp new file mode 100644 index 0000000000..b870d1b56d --- /dev/null +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -0,0 +1,301 @@ +#include "kamstrup_kmp.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace kamstrup_kmp { + +static const char *const TAG = "kamstrup_kmp"; + +void KamstrupKMPComponent::dump_config() { + ESP_LOGCONFIG(TAG, "kamstrup_kmp:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with Kamstrup meter failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Heat Energy", this->heat_energy_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + LOG_SENSOR(" ", "Temperature 1", this->temp1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temp2_sensor_); + LOG_SENSOR(" ", "Temperature Difference", this->temp_diff_sensor_); + LOG_SENSOR(" ", "Flow", this->flow_sensor_); + LOG_SENSOR(" ", "Volume", this->volume_sensor_); + + for (int i = 0; i < this->custom_sensors_.size(); i++) { + LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]); + ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]); + } + + this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8); +} + +float KamstrupKMPComponent::get_setup_priority() const { return setup_priority::DATA; } + +void KamstrupKMPComponent::update() { + if (this->heat_energy_sensor_ != nullptr) { + this->command_queue_.push(CMD_HEAT_ENERGY); + } + + if (this->power_sensor_ != nullptr) { + this->command_queue_.push(CMD_POWER); + } + + if (this->temp1_sensor_ != nullptr) { + this->command_queue_.push(CMD_TEMP1); + } + + if (this->temp2_sensor_ != nullptr) { + this->command_queue_.push(CMD_TEMP2); + } + + if (this->temp_diff_sensor_ != nullptr) { + this->command_queue_.push(CMD_TEMP_DIFF); + } + + if (this->flow_sensor_ != nullptr) { + this->command_queue_.push(CMD_FLOW); + } + + if (this->volume_sensor_ != nullptr) { + this->command_queue_.push(CMD_VOLUME); + } + + for (uint16_t custom_command : this->custom_commands_) { + this->command_queue_.push(custom_command); + } +} + +void KamstrupKMPComponent::loop() { + if (!this->command_queue_.empty()) { + uint16_t command = this->command_queue_.front(); + this->send_command_(command); + this->command_queue_.pop(); + } +} + +void KamstrupKMPComponent::send_command_(uint16_t command) { + uint32_t msg_len = 5; + uint8_t msg[msg_len]; + + msg[0] = 0x3F; + msg[1] = 0x10; + msg[2] = 0x01; + msg[3] = command >> 8; + msg[4] = command & 0xFF; + + this->clear_uart_rx_buffer_(); + this->send_message_(msg, msg_len); + this->read_command_(command); +} + +void KamstrupKMPComponent::send_message_(const uint8_t *msg, int msg_len) { + int buffer_len = msg_len + 2; + uint8_t buffer[buffer_len]; + + // Prepare the basic message and appand CRC + for (int i = 0; i < msg_len; i++) { + buffer[i] = msg[i]; + } + + buffer[buffer_len - 2] = 0; + buffer[buffer_len - 1] = 0; + + uint16_t crc = crc16_ccitt(buffer, buffer_len); + buffer[buffer_len - 2] = crc >> 8; + buffer[buffer_len - 1] = crc & 0xFF; + + // Prepare actual TX message + uint8_t tx_msg[20]; + int tx_msg_len = 1; + tx_msg[0] = 0x80; // prefix + + for (int i = 0; i < buffer_len; i++) { + if (buffer[i] == 0x06 || buffer[i] == 0x0d || buffer[i] == 0x1b || buffer[i] == 0x40 || buffer[i] == 0x80) { + tx_msg[tx_msg_len++] = 0x1b; + tx_msg[tx_msg_len++] = buffer[i] ^ 0xff; + } else { + tx_msg[tx_msg_len++] = buffer[i]; + } + } + + tx_msg[tx_msg_len++] = 0x0D; // EOM + + this->write_array(tx_msg, tx_msg_len); +} + +void KamstrupKMPComponent::clear_uart_rx_buffer_() { + uint8_t tmp; + while (this->available()) { + this->read_byte(&tmp); + } +} + +void KamstrupKMPComponent::read_command_(uint16_t command) { + uint8_t buffer[20] = {0}; + int buffer_len = 0; + int data; + int timeout = 250; // ms + + // Read the data from the UART + while (timeout > 0) { + if (this->available()) { + data = this->read(); + if (data > -1) { + if (data == 0x40) { // start of message + buffer_len = 0; + } + buffer[buffer_len++] = (uint8_t) data; + if (data == 0x0D) { + break; + } + } else { + ESP_LOGE(TAG, "Error while reading from UART"); + } + } else { + delay(1); + timeout--; + } + } + + if (timeout == 0 || buffer_len == 0) { + ESP_LOGE(TAG, "Request timed out"); + return; + } + + // Validate message (prefix and suffix) + if (buffer[0] != 0x40) { + ESP_LOGE(TAG, "Received invalid message (prefix mismatch received 0x%02X, expected 0x40)", buffer[0]); + return; + } + + if (buffer[buffer_len - 1] != 0x0D) { + ESP_LOGE(TAG, "Received invalid message (EOM mismatch received 0x%02X, expected 0x0D)", buffer[buffer_len - 1]); + return; + } + + // Decode + uint8_t msg[20] = {0}; + int msg_len = 0; + for (int i = 1; i < buffer_len - 1; i++) { + if (buffer[i] == 0x1B) { + msg[msg_len++] = buffer[i + 1] ^ 0xFF; + i++; + } else { + msg[msg_len++] = buffer[i]; + } + } + + // Validate CRC + if (crc16_ccitt(msg, msg_len)) { + ESP_LOGE(TAG, "Received invalid message (CRC mismatch)"); + return; + } + + // All seems good. Now parse the message + this->parse_command_message_(command, msg, msg_len); +} + +void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_t *msg, int msg_len) { + // Validate the message + if (msg_len < 8) { + ESP_LOGE(TAG, "Received invalid message (message too small)"); + return; + } + + if (msg[0] != 0x3F || msg[1] != 0x10) { + ESP_LOGE(TAG, "Received invalid message (invalid header received 0x%02X%02X, expected 0x3F10)", msg[0], msg[1]); + return; + } + + uint16_t recv_command = msg[2] << 8 | msg[3]; + if (recv_command != command) { + ESP_LOGE(TAG, "Received invalid message (invalid unexpected command received 0x%04X, expected 0x%04X)", + recv_command, command); + return; + } + + uint8_t unit_idx = msg[4]; + uint8_t mantissa_range = msg[5]; + + if (mantissa_range > 4) { + ESP_LOGE(TAG, "Received invalid message (mantissa size too large %d, expected 4)", mantissa_range); + return; + } + + // Calculate exponent + float exponent = msg[6] & 0x3F; + if (msg[6] & 0x40) { + exponent = -exponent; + } + exponent = powf(10, exponent); + if (msg[6] & 0x80) { + exponent = -exponent; + } + + // Calculate mantissa + uint32_t mantissa = 0; + for (int i = 0; i < mantissa_range; i++) { + mantissa <<= 8; + mantissa |= msg[i + 7]; + } + + // Calculate the actual value + float value = mantissa * exponent; + + // Set sensor value + this->set_sensor_value_(command, value, unit_idx); +} + +void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) { + const char *unit = UNITS[unit_idx]; + + // Standard sensors + if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) { + this->heat_energy_sensor_->publish_state(value); + } else if (command == CMD_POWER && this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(value); + } else if (command == CMD_TEMP1 && this->temp1_sensor_ != nullptr) { + this->temp1_sensor_->publish_state(value); + } else if (command == CMD_TEMP2 && this->temp2_sensor_ != nullptr) { + this->temp2_sensor_->publish_state(value); + } else if (command == CMD_TEMP_DIFF && this->temp_diff_sensor_ != nullptr) { + this->temp_diff_sensor_->publish_state(value); + } else if (command == CMD_FLOW && this->flow_sensor_ != nullptr) { + this->flow_sensor_->publish_state(value); + } else if (command == CMD_VOLUME && this->volume_sensor_ != nullptr) { + this->volume_sensor_->publish_state(value); + } + + // Custom sensors + for (int i = 0; i < this->custom_commands_.size(); i++) { + if (command == this->custom_commands_[i]) { + this->custom_sensors_[i]->publish_state(value); + } + } + + ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value, unit); +} + +uint16_t crc16_ccitt(const uint8_t *buffer, int len) { + uint32_t poly = 0x1021; + uint32_t reg = 0x00; + for (int i = 0; i < len; i++) { + int mask = 0x80; + while (mask > 0) { + reg <<= 1; + if (buffer[i] & mask) { + reg |= 1; + } + mask >>= 1; + if (reg & 0x10000) { + reg &= 0xffff; + reg ^= poly; + } + } + } + return (uint16_t) reg; +} + +} // namespace kamstrup_kmp +} // namespace esphome diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h new file mode 100644 index 0000000000..c9cc9c5a39 --- /dev/null +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -0,0 +1,131 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace kamstrup_kmp { + +/* + =========================================================================== + === KAMSTRUP KMP === + =========================================================================== + + Kamstrup Meter Protocol (KMP) is a protocol used with Kamstrup district + heating meters, e.g. Kamstrup MULTICAL 403. + These devices register consumed heat from a district heating system. + It does this by measuring the incoming and outgoing water temperature + and by measuring the water flow. The temperature difference (delta T) + together with the water flow results in consumed energy, typically + in giga joule (GJ). + + The Kamstrup Multical has an optical interface just above the display. + This interface is essentially an RS-232 interface using a proprietary + protocol (Kamstrup Meter Protocol [KMP]). + + The integration uses this optical interface to periodically read the + configured values (sensors) from the meter. Supported sensors are: + - Heat Energy [GJ] + - Current Power Consumption [kW] + - Temperature 1 [°C] + - Temperature 2 [°C] + - Temperature Difference [°K] + - Water Flow [l/h] + - Volume [m3] + + Apart from these supported 'fixed' sensors, the user can configure up to + five custom sensors. The KMP command (16 bit unsigned int) has to be + provided in that case. + + Note: + The optical interface is enabled as soon as a button on the meter is pushed. + The interface stays active for a few minutes. To keep the interface 'alive' + magnets must be placed around the optical sensor. + + Units: + Units are set using the regular Sensor config in the user yaml. However, + KMP does also send the correct unit with every value. When DEBUG logging + is enabled, the received value with the received unit are logged. + + Acknowledgement: + This interface was inspired by: + - https://atomstar.tweakblogs.net/blog/19110/reading-out-kamstrup-multical-402-403-with-home-built-optical-head + - https://wiki.hal9k.dk/projects/kamstrup +*/ + +// KMP Commands +static const uint16_t CMD_HEAT_ENERGY = 0x003C; +static const uint16_t CMD_POWER = 0x0050; +static const uint16_t CMD_TEMP1 = 0x0056; +static const uint16_t CMD_TEMP2 = 0x0057; +static const uint16_t CMD_TEMP_DIFF = 0x0059; +static const uint16_t CMD_FLOW = 0x004A; +static const uint16_t CMD_VOLUME = 0x0044; + +// KMP units +static const char *const UNITS[] = { + "", "Wh", "kWh", "MWh", "GWh", "J", "kJ", "MJ", "GJ", "Cal", + "kCal", "Mcal", "Gcal", "varh", "kvarh", "Mvarh", "Gvarh", "VAh", "kVAh", "MVAh", + "GVAh", "kW", "kW", "MW", "GW", "kvar", "kvar", "Mvar", "Gvar", "VA", + "kVA", "MVA", "GVA", "V", "A", "kV", "kA", "C", "K", "l", + "m3", "l/h", "m3/h", "m3xC", "ton", "ton/h", "h", "hh:mm:ss", "yy:mm:dd", "yyyy:mm:dd", + "mm:dd", "", "bar", "RTC", "ASCII", "m3 x 10", "ton x 10", "GJ x 10", "minutes", "Bitfield", + "s", "ms", "days", "RTC-Q", "Datetime"}; + +class KamstrupKMPComponent : public PollingComponent, public uart::UARTDevice { + public: + void set_heat_energy_sensor(sensor::Sensor *sensor) { this->heat_energy_sensor_ = sensor; } + void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } + void set_temp1_sensor(sensor::Sensor *sensor) { this->temp1_sensor_ = sensor; } + void set_temp2_sensor(sensor::Sensor *sensor) { this->temp2_sensor_ = sensor; } + void set_temp_diff_sensor(sensor::Sensor *sensor) { this->temp_diff_sensor_ = sensor; } + void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } + void set_volume_sensor(sensor::Sensor *sensor) { this->volume_sensor_ = sensor; } + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void loop() override; + void add_custom_sensor(sensor::Sensor *sensor, uint16_t command) { + this->custom_sensors_.push_back(sensor); + this->custom_commands_.push_back(command); + } + + protected: + // Sensors + sensor::Sensor *heat_energy_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *temp1_sensor_{nullptr}; + sensor::Sensor *temp2_sensor_{nullptr}; + sensor::Sensor *temp_diff_sensor_{nullptr}; + sensor::Sensor *flow_sensor_{nullptr}; + sensor::Sensor *volume_sensor_{nullptr}; + + // Custom sensors and commands + std::vector custom_sensors_; + std::vector custom_commands_; + + // Command queue + std::queue command_queue_; + + // Methods + + // Sends a command to the meter and receives its response + void send_command_(uint16_t command); + // Sends a message to the meter. A prefix/suffix and CRC are added + void send_message_(const uint8_t *msg, int msg_len); + // Clears and data that might be in the UART Rx buffer + void clear_uart_rx_buffer_(); + // Reads and validates the response to a send command + void read_command_(uint16_t command); + // Parses a received message + void parse_command_message_(uint16_t command, const uint8_t *msg, int msg_len); + // Sets the received value to the correct sensor + void set_sensor_value_(uint16_t command, float value, uint8_t unit_idx); +}; + +// "true" CCITT CRC-16 +uint16_t crc16_ccitt(const uint8_t *buffer, int len); + +} // namespace kamstrup_kmp +} // namespace esphome diff --git a/esphome/components/kamstrup_kmp/sensor.py b/esphome/components/kamstrup_kmp/sensor.py new file mode 100644 index 0000000000..c9024e4a2b --- /dev/null +++ b/esphome/components/kamstrup_kmp/sensor.py @@ -0,0 +1,132 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_COMMAND, + CONF_CUSTOM, + CONF_FLOW, + CONF_ID, + CONF_POWER, + CONF_VOLUME, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLUME, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_CELSIUS, + UNIT_CUBIC_METER, + UNIT_EMPTY, + UNIT_KELVIN, + UNIT_KILOWATT, +) + +CODEOWNERS = ["@cfeenstra1024"] +DEPENDENCIES = ["uart"] + +kamstrup_kmp_ns = cg.esphome_ns.namespace("kamstrup_kmp") +KamstrupKMPComponent = kamstrup_kmp_ns.class_( + "KamstrupKMPComponent", cg.PollingComponent, uart.UARTDevice +) + +CONF_HEAT_ENERGY = "heat_energy" +CONF_TEMP1 = "temp1" +CONF_TEMP2 = "temp2" +CONF_TEMP_DIFF = "temp_diff" + +UNIT_GIGA_JOULE = "GJ" +UNIT_LITRE_PER_HOUR = "l/h" + +# Note: The sensor units are set automatically based un the received data from the meter +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(KamstrupKMPComponent), + cv.Optional(CONF_HEAT_ENERGY): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_GIGA_JOULE, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOWATT, + ), + cv.Optional(CONF_TEMP1): sensor.sensor_schema( + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + ), + cv.Optional(CONF_TEMP2): sensor.sensor_schema( + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + ), + cv.Optional(CONF_TEMP_DIFF): sensor.sensor_schema( + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KELVIN, + ), + cv.Optional(CONF_FLOW): sensor.sensor_schema( + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLUME, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LITRE_PER_HOUR, + ), + cv.Optional(CONF_VOLUME): sensor.sensor_schema( + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLUME, + state_class=STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_CUBIC_METER, + ), + cv.Optional(CONF_CUSTOM): cv.ensure_list( + sensor.sensor_schema( + accuracy_decimals=1, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_EMPTY, + ).extend({cv.Required(CONF_COMMAND): cv.hex_uint16_t}) + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "kamstrup_kmp", baud_rate=1200, 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) + + # Standard sensors + for key in [ + CONF_HEAT_ENERGY, + CONF_POWER, + CONF_TEMP1, + CONF_TEMP2, + CONF_TEMP_DIFF, + CONF_FLOW, + CONF_VOLUME, + ]: + if key not in config: + continue + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) + + # Custom sensors + if CONF_CUSTOM in config: + for conf in config[CONF_CUSTOM]: + sens = await sensor.new_sensor(conf) + cg.add(var.add_custom_sensor(sens, conf[CONF_COMMAND])) diff --git a/esphome/components/kmeteriso/__init__.py b/esphome/components/kmeteriso/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp new file mode 100644 index 0000000000..0276ab3f67 --- /dev/null +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -0,0 +1,82 @@ +#include "kmeteriso.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace kmeteriso { + +static const char *const TAG = "kmeteriso.sensor"; + +static const uint8_t KMETER_ERROR_STATUS_REG = 0x20; +static const uint8_t KMETER_TEMP_VAL_REG = 0x00; +static const uint8_t KMETER_INTERNAL_TEMP_VAL_REG = 0x10; +static const uint8_t KMETER_FIRMWARE_VERSION_REG = 0xFE; + +void KMeterISOComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up KMeterISO..."); + this->error_code_ = NONE; + + // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries + // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. + if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + } + + auto err = this->bus_->writev(this->address_, nullptr, 0); + if (err == esphome::i2c::ERROR_OK) { + ESP_LOGCONFIG(TAG, "Could write to the address %d.", this->address_); + } else { + ESP_LOGCONFIG(TAG, "Could not write to the address %d.", this->address_); + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + uint8_t read_buf[4] = {1}; + if (!this->read_bytes(KMETER_ERROR_STATUS_REG, read_buf, 1)) { + ESP_LOGCONFIG(TAG, "Could not read from the device."); + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + if (read_buf[0] != 0) { + ESP_LOGCONFIG(TAG, "The device is not ready."); + this->error_code_ = STATUS_FAILED; + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "The device was successfully setup."); +} + +float KMeterISOComponent::get_setup_priority() const { return setup_priority::DATA; } + +void KMeterISOComponent::update() { + uint8_t read_buf[4]; + + if (this->temperature_sensor_ != nullptr) { + if (!this->read_bytes(KMETER_TEMP_VAL_REG, read_buf, 4)) { + ESP_LOGW(TAG, "Error reading temperature."); + } else { + int32_t temp = encode_uint32(read_buf[3], read_buf[2], read_buf[1], read_buf[0]); + float temp_f = temp / 100.0; + ESP_LOGV(TAG, "Got temperature=%.2f °C", temp_f); + this->temperature_sensor_->publish_state(temp_f); + } + } + + if (this->internal_temperature_sensor_ != nullptr) { + if (!this->read_bytes(KMETER_INTERNAL_TEMP_VAL_REG, read_buf, 4)) { + ESP_LOGW(TAG, "Error reading internal temperature."); + return; + } else { + int32_t internal_temp = encode_uint32(read_buf[3], read_buf[2], read_buf[1], read_buf[0]); + float internal_temp_f = internal_temp / 100.0; + ESP_LOGV(TAG, "Got internal temperature=%.2f °C", internal_temp_f); + this->internal_temperature_sensor_->publish_state(internal_temp_f); + } + } +} + +} // namespace kmeteriso +} // namespace esphome diff --git a/esphome/components/kmeteriso/kmeteriso.h b/esphome/components/kmeteriso/kmeteriso.h new file mode 100644 index 0000000000..c8bed662b0 --- /dev/null +++ b/esphome/components/kmeteriso/kmeteriso.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/i2c/i2c_bus.h" + +namespace esphome { +namespace kmeteriso { + +/// This class implements support for the KMeterISO thermocouple sensor. +class KMeterISOComponent : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *t) { this->temperature_sensor_ = t; } + void set_internal_temperature_sensor(sensor::Sensor *t) { this->internal_temperature_sensor_ = t; } + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + float get_setup_priority() const override; + void update() override; + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *internal_temperature_sensor_{nullptr}; + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + STATUS_FAILED, + } error_code_{NONE}; +}; + +} // namespace kmeteriso +} // namespace esphome diff --git a/esphome/components/kmeteriso/sensor.py b/esphome/components/kmeteriso/sensor.py new file mode 100644 index 0000000000..082a055701 --- /dev/null +++ b/esphome/components/kmeteriso/sensor.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_INTERNAL_TEMPERATURE, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +DEPENDENCIES = ["i2c"] + +kmeteriso_ns = cg.esphome_ns.namespace("kmeteriso") + +KMeterISOComponent = kmeteriso_ns.class_( + "KMeterISOComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(KMeterISOComponent), + 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_INTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x66)) +) + + +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) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + if internal_temperature_config := config.get(CONF_INTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(internal_temperature_config) + cg.add(var.set_internal_temperature_sensor(sens)) diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index 92fd0b5563..693211c6fe 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -52,7 +52,6 @@ LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def setup_lcd_display(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) if CONF_USER_CHARACTERS in config: diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index 180d5e93ac..65d7aa508f 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -158,15 +158,13 @@ void LCDDisplay::clear() { for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) this->buffer_[i] = ' '; } -#ifdef USE_TIME -void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) { +void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(column, row, buffer); } -void LCDDisplay::strftime(const char *format, time::ESPTime time) { this->strftime(0, 0, format, time); } -#endif +void LCDDisplay::strftime(const char *format, ESPTime time) { this->strftime(0, 0, format, time); } void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) { location &= 0x7; // we only have 8 locations 0-7 this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (location << 3)); diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index c1dc54a9ed..473acb0bd3 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -1,11 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "esphome/core/time.h" #include #include @@ -44,13 +40,10 @@ class LCDDisplay : public PollingComponent { /// Evaluate the printf-format and print the text at column=0 and row=0. void printf(const char *format, ...) __attribute__((format(printf, 2, 3))); -#ifdef USE_TIME /// Evaluate the strftime-format and print the text at the specified column and row. - void strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) - __attribute__((format(strftime, 4, 0))); + void strftime(uint8_t column, uint8_t row, const char *format, ESPTime time) __attribute__((format(strftime, 4, 0))); /// Evaluate the strftime-format and print the text at column=0 and row=0. - void strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + void strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); /// Load custom char to given location void loadchar(uint8_t location, uint8_t charmap[]); diff --git a/esphome/components/lcd_menu/__init__.py b/esphome/components/lcd_menu/__init__.py index a356c85ba7..b57a4a0f6c 100644 --- a/esphome/components/lcd_menu/__init__.py +++ b/esphome/components/lcd_menu/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_DIMENSIONS, + CONF_DISPLAY_ID, ) from esphome.core.entity_helpers import inherit_property_from from esphome.components import lcd_base @@ -18,8 +19,6 @@ AUTO_LOAD = ["display_menu_base"] lcd_menu_ns = cg.esphome_ns.namespace("lcd_menu") -CONF_DISPLAY_ID = "display_id" - CONF_MARK_SELECTED = "mark_selected" CONF_MARK_EDITING = "mark_editing" CONF_MARK_SUBMENU = "mark_submenu" diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index be39cc2979..2b30b65f46 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -1,118 +1,68 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID, CONF_TIMEOUT +from esphome.const import CONF_ID, CONF_THROTTLE, CONF_TIMEOUT, CONF_PASSWORD from esphome import automation from esphome.automation import maybe_simple_id DEPENDENCIES = ["uart"] -CODEOWNERS = ["@sebcaps"] +CODEOWNERS = ["@sebcaps", "@regevbr"] MULTI_CONF = True ld2410_ns = cg.esphome_ns.namespace("ld2410") LD2410Component = ld2410_ns.class_("LD2410Component", cg.Component, uart.UARTDevice) -LD2410Restart = ld2410_ns.class_("LD2410Restart", automation.Action) + CONF_LD2410_ID = "ld2410_id" + CONF_MAX_MOVE_DISTANCE = "max_move_distance" CONF_MAX_STILL_DISTANCE = "max_still_distance" -CONF_G0_MOVE_THRESHOLD = "g0_move_threshold" -CONF_G0_STILL_THRESHOLD = "g0_still_threshold" -CONF_G1_MOVE_THRESHOLD = "g1_move_threshold" -CONF_G1_STILL_THRESHOLD = "g1_still_threshold" -CONF_G2_MOVE_THRESHOLD = "g2_move_threshold" -CONF_G2_STILL_THRESHOLD = "g2_still_threshold" -CONF_G3_MOVE_THRESHOLD = "g3_move_threshold" -CONF_G3_STILL_THRESHOLD = "g3_still_threshold" -CONF_G4_MOVE_THRESHOLD = "g4_move_threshold" -CONF_G4_STILL_THRESHOLD = "g4_still_threshold" -CONF_G5_MOVE_THRESHOLD = "g5_move_threshold" -CONF_G5_STILL_THRESHOLD = "g5_still_threshold" -CONF_G6_MOVE_THRESHOLD = "g6_move_threshold" -CONF_G6_STILL_THRESHOLD = "g6_still_threshold" -CONF_G7_MOVE_THRESHOLD = "g7_move_threshold" -CONF_G7_STILL_THRESHOLD = "g7_still_threshold" -CONF_G8_MOVE_THRESHOLD = "g8_move_threshold" -CONF_G8_STILL_THRESHOLD = "g8_still_threshold" +CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] +CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)] -DISTANCES = [0.75, 1.5, 2.25, 3, 3.75, 4.5, 5.25, 6] +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2410Component), + cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(min=cv.TimePeriod(milliseconds=1)), + ), + cv.Optional(CONF_MAX_MOVE_DISTANCE): cv.invalid( + f"The '{CONF_MAX_MOVE_DISTANCE}' option has been moved to the '{CONF_MAX_MOVE_DISTANCE}'" + f" number component" + ), + cv.Optional(CONF_MAX_STILL_DISTANCE): cv.invalid( + f"The '{CONF_MAX_STILL_DISTANCE}' option has been moved to the '{CONF_MAX_STILL_DISTANCE}'" + f" number component" + ), + cv.Optional(CONF_TIMEOUT): cv.invalid( + f"The '{CONF_TIMEOUT}' option has been moved to the '{CONF_TIMEOUT}'" + f" number component" + ), + } +) + +for i in range(9): + CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_MOVE_THRESHOLDS[i]): cv.invalid( + f"The '{CONF_MOVE_THRESHOLDS[i]}' option has been moved to the '{CONF_MOVE_THRESHOLDS[i]}'" + f" number component" + ), + cv.Optional(CONF_STILL_THRESHOLDS[i]): cv.invalid( + f"The '{CONF_STILL_THRESHOLDS[i]}' option has been moved to the '{CONF_STILL_THRESHOLDS[i]}'" + f" number component" + ), + } + ) + ) CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(LD2410Component), - cv.Optional(CONF_MAX_MOVE_DISTANCE, default="4.5m"): cv.All( - cv.distance, cv.one_of(*DISTANCES, float=True) - ), - cv.Optional(CONF_MAX_STILL_DISTANCE, default="4.5m"): cv.All( - cv.distance, cv.one_of(*DISTANCES, float=True) - ), - cv.Optional(CONF_TIMEOUT, default="5s"): cv.All( - cv.positive_time_period_seconds, - cv.Range(max=cv.TimePeriod(seconds=32767)), - ), - cv.Optional(CONF_G0_MOVE_THRESHOLD, default=50): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G0_STILL_THRESHOLD, default=0): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G1_MOVE_THRESHOLD, default=50): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G1_STILL_THRESHOLD, default=0): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G2_MOVE_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G2_STILL_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G3_MOVE_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G3_STILL_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G4_MOVE_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G4_STILL_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G5_MOVE_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G5_STILL_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G6_MOVE_THRESHOLD, default=30): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G6_STILL_THRESHOLD, default=15): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G7_MOVE_THRESHOLD, default=30): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G7_STILL_THRESHOLD, default=15): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G8_MOVE_THRESHOLD, default=30): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G8_STILL_THRESHOLD, default=15): cv.int_range( - min=0, max=100 - ), - } - ) - .extend(uart.UART_DEVICE_SCHEMA) - .extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA.extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "ld2410", - baud_rate=256000, require_tx=True, require_rx=True, parity="NONE", @@ -124,31 +74,7 @@ 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) - cg.add(var.set_timeout(config[CONF_TIMEOUT])) - cg.add(var.set_max_move_distance(int(config[CONF_MAX_MOVE_DISTANCE] / 0.75))) - cg.add(var.set_max_still_distance(int(config[CONF_MAX_STILL_DISTANCE] / 0.75))) - cg.add( - var.set_range_config( - config[CONF_G0_MOVE_THRESHOLD], - config[CONF_G0_STILL_THRESHOLD], - config[CONF_G1_MOVE_THRESHOLD], - config[CONF_G1_STILL_THRESHOLD], - config[CONF_G2_MOVE_THRESHOLD], - config[CONF_G2_STILL_THRESHOLD], - config[CONF_G3_MOVE_THRESHOLD], - config[CONF_G3_STILL_THRESHOLD], - config[CONF_G4_MOVE_THRESHOLD], - config[CONF_G4_STILL_THRESHOLD], - config[CONF_G5_MOVE_THRESHOLD], - config[CONF_G5_STILL_THRESHOLD], - config[CONF_G6_MOVE_THRESHOLD], - config[CONF_G6_STILL_THRESHOLD], - config[CONF_G7_MOVE_THRESHOLD], - config[CONF_G7_STILL_THRESHOLD], - config[CONF_G8_MOVE_THRESHOLD], - config[CONF_G8_STILL_THRESHOLD], - ) - ) + cg.add(var.set_throttle(config[CONF_THROTTLE])) CALIBRATION_ACTION_SCHEMA = maybe_simple_id( @@ -156,3 +82,28 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( cv.Required(CONF_ID): cv.use_id(LD2410Component), } ) + + +# Actions +BluetoothPasswordSetAction = ld2410_ns.class_( + "BluetoothPasswordSetAction", automation.Action +) + + +BLUETOOTH_PASSWORD_SET_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(LD2410Component), + cv.Required(CONF_PASSWORD): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action( + "bluetooth_password.set", BluetoothPasswordSetAction, BLUETOOTH_PASSWORD_SET_SCHEMA +) +async def bluetooth_password_set_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_PASSWORD], args, cg.std_string) + cg.add(var.set_password(template_)) + return var diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h new file mode 100644 index 0000000000..7cb9855f84 --- /dev/null +++ b/esphome/components/ld2410/automation.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "ld2410.h" + +namespace esphome { +namespace ld2410 { + +template class BluetoothPasswordSetAction : public Action { + public: + explicit BluetoothPasswordSetAction(LD2410Component *ld2410_comp) : ld2410_comp_(ld2410_comp) {} + TEMPLATABLE_VALUE(std::string, password) + + void play(Ts... x) override { this->ld2410_comp_->set_bluetooth_password(this->password_.value(x...)); } + + protected: + LD2410Component *ld2410_comp_; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/binary_sensor.py b/esphome/components/ld2410/binary_sensor.py index 02f73d57b7..e00ab93be2 100644 --- a/esphome/components/ld2410/binary_sensor.py +++ b/esphome/components/ld2410/binary_sensor.py @@ -1,36 +1,55 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY +from esphome.const import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_PRESENCE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_MOTION_SENSOR, + ICON_ACCOUNT, + CONF_HAS_TARGET, + CONF_HAS_MOVING_TARGET, + CONF_HAS_STILL_TARGET, +) from . import CONF_LD2410_ID, LD2410Component DEPENDENCIES = ["ld2410"] -CONF_HAS_TARGET = "has_target" -CONF_HAS_MOVING_TARGET = "has_moving_target" -CONF_HAS_STILL_TARGET = "has_still_target" +CONF_OUT_PIN_PRESENCE_STATUS = "out_pin_presence_status" CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( - device_class=DEVICE_CLASS_OCCUPANCY + device_class=DEVICE_CLASS_OCCUPANCY, + icon=ICON_ACCOUNT, ), cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( - device_class=DEVICE_CLASS_MOTION + device_class=DEVICE_CLASS_MOTION, + icon=ICON_MOTION_SENSOR, ), cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( - device_class=DEVICE_CLASS_OCCUPANCY + device_class=DEVICE_CLASS_OCCUPANCY, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_OUT_PIN_PRESENCE_STATUS): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PRESENCE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_ACCOUNT, ), } async def to_code(config): ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) - if CONF_HAS_TARGET in config: - sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_TARGET]) - cg.add(ld2410_component.set_target_sensor(sens)) - if CONF_HAS_MOVING_TARGET in config: - sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_MOVING_TARGET]) - cg.add(ld2410_component.set_moving_target_sensor(sens)) - if CONF_HAS_STILL_TARGET in config: - sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_STILL_TARGET]) - cg.add(ld2410_component.set_still_target_sensor(sens)) + if has_target_config := config.get(CONF_HAS_TARGET): + sens = await binary_sensor.new_binary_sensor(has_target_config) + cg.add(ld2410_component.set_target_binary_sensor(sens)) + if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET): + sens = await binary_sensor.new_binary_sensor(has_moving_target_config) + cg.add(ld2410_component.set_moving_target_binary_sensor(sens)) + if has_still_target_config := config.get(CONF_HAS_STILL_TARGET): + sens = await binary_sensor.new_binary_sensor(has_still_target_config) + cg.add(ld2410_component.set_still_target_binary_sensor(sens)) + if out_pin_presence_status_config := config.get(CONF_OUT_PIN_PRESENCE_STATUS): + sens = await binary_sensor.new_binary_sensor(out_pin_presence_status_config) + cg.add(ld2410_component.set_out_pin_presence_status_binary_sensor(sens)) diff --git a/esphome/components/ld2410/button/__init__.py b/esphome/components/ld2410/button/__init__.py new file mode 100644 index 0000000000..34b18e8bdd --- /dev/null +++ b/esphome/components/ld2410/button/__init__.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + CONF_FACTORY_RESET, + CONF_RESTART, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART, + ICON_RESTART_ALERT, + ICON_DATABASE, +) +from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns + +QueryButton = ld2410_ns.class_("QueryButton", button.Button) +ResetButton = ld2410_ns.class_("ResetButton", button.Button) +RestartButton = ld2410_ns.class_("RestartButton", button.Button) + +CONF_QUERY_PARAMS = "query_params" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + ResetButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_RESTART): button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_RESTART, + ), + cv.Optional(CONF_QUERY_PARAMS): button.button_schema( + QueryButton, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_DATABASE, + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if factory_reset_config := config.get(CONF_FACTORY_RESET): + b = await button.new_button(factory_reset_config) + await cg.register_parented(b, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_reset_button(b)) + if restart_config := config.get(CONF_RESTART): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_restart_button(b)) + if query_params_config := config.get(CONF_QUERY_PARAMS): + b = await button.new_button(query_params_config) + await cg.register_parented(b, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_query_button(b)) diff --git a/esphome/components/ld2410/button/query_button.cpp b/esphome/components/ld2410/button/query_button.cpp new file mode 100644 index 0000000000..47ab416f5a --- /dev/null +++ b/esphome/components/ld2410/button/query_button.cpp @@ -0,0 +1,9 @@ +#include "query_button.h" + +namespace esphome { +namespace ld2410 { + +void QueryButton::press_action() { this->parent_->read_all_info(); } + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/query_button.h b/esphome/components/ld2410/button/query_button.h new file mode 100644 index 0000000000..c7a47e32d8 --- /dev/null +++ b/esphome/components/ld2410/button/query_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class QueryButton : public button::Button, public Parented { + public: + QueryButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/reset_button.cpp b/esphome/components/ld2410/button/reset_button.cpp new file mode 100644 index 0000000000..f16c5faa79 --- /dev/null +++ b/esphome/components/ld2410/button/reset_button.cpp @@ -0,0 +1,9 @@ +#include "reset_button.h" + +namespace esphome { +namespace ld2410 { + +void ResetButton::press_action() { this->parent_->factory_reset(); } + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/reset_button.h b/esphome/components/ld2410/button/reset_button.h new file mode 100644 index 0000000000..78dd92c9f5 --- /dev/null +++ b/esphome/components/ld2410/button/reset_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class ResetButton : public button::Button, public Parented { + public: + ResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/restart_button.cpp b/esphome/components/ld2410/button/restart_button.cpp new file mode 100644 index 0000000000..de0d36c1ef --- /dev/null +++ b/esphome/components/ld2410/button/restart_button.cpp @@ -0,0 +1,9 @@ +#include "restart_button.h" + +namespace esphome { +namespace ld2410 { + +void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/restart_button.h b/esphome/components/ld2410/button/restart_button.h new file mode 100644 index 0000000000..d00dc05a53 --- /dev/null +++ b/esphome/components/ld2410/button/restart_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class RestartButton : public button::Button, public Parented { + public: + RestartButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 8e67ba54d7..c3b57815d6 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -1,5 +1,13 @@ #include "ld2410.h" +#include +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -8,48 +16,97 @@ namespace ld2410 { static const char *const TAG = "ld2410"; +LD2410Component::LD2410Component() {} + void LD2410Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2410:"); #ifdef USE_BINARY_SENSOR - LOG_BINARY_SENSOR(" ", "HasTargetSensor", this->target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "MovingSensor", this->moving_binary_sensor_); - LOG_BINARY_SENSOR(" ", "StillSensor", this->still_binary_sensor_); + LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "OutPinPresenceStatusBinarySensor", this->out_pin_presence_status_binary_sensor_); +#endif +#ifdef USE_SWITCH + LOG_SWITCH(" ", "EngineeringModeSwitch", this->engineering_mode_switch_); + LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_); +#endif +#ifdef USE_BUTTON + LOG_BUTTON(" ", "ResetButton", this->reset_button_); + LOG_BUTTON(" ", "RestartButton", this->restart_button_); + LOG_BUTTON(" ", "QueryButton", this->query_button_); #endif #ifdef USE_SENSOR - LOG_SENSOR(" ", "Moving Distance", this->moving_target_distance_sensor_); - LOG_SENSOR(" ", "Still Distance", this->still_target_distance_sensor_); - LOG_SENSOR(" ", "Moving Energy", this->moving_target_energy_sensor_); - LOG_SENSOR(" ", "Still Energy", this->still_target_energy_sensor_); - LOG_SENSOR(" ", "Detection Distance", this->detection_distance_sensor_); + LOG_SENSOR(" ", "LightSensor", this->light_sensor_); + LOG_SENSOR(" ", "MovingTargetDistanceSensor", this->moving_target_distance_sensor_); + LOG_SENSOR(" ", "StillTargetDistanceSensor", this->still_target_distance_sensor_); + LOG_SENSOR(" ", "MovingTargetEnergySensor", this->moving_target_energy_sensor_); + LOG_SENSOR(" ", "StillTargetEnergySensor", this->still_target_energy_sensor_); + LOG_SENSOR(" ", "DetectionDistanceSensor", this->detection_distance_sensor_); + for (sensor::Sensor *s : this->gate_still_sensors_) { + LOG_SENSOR(" ", "NthGateStillSesnsor", s); + } + for (sensor::Sensor *s : this->gate_move_sensors_) { + LOG_SENSOR(" ", "NthGateMoveSesnsor", s); + } #endif - this->set_config_mode_(true); - this->get_version_(); - this->set_config_mode_(false); - ESP_LOGCONFIG(TAG, " Firmware Version : %u.%u.%u%u%u%u", this->version_[0], this->version_[1], this->version_[2], - this->version_[3], this->version_[4], this->version_[5]); +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_); + LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_); +#endif +#ifdef USE_SELECT + LOG_SELECT(" ", "LightFunctionSelect", this->light_function_select_); + LOG_SELECT(" ", "OutPinLevelSelect", this->out_pin_level_select_); + LOG_SELECT(" ", "DistanceResolutionSelect", this->distance_resolution_select_); + LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_); +#endif +#ifdef USE_NUMBER + LOG_NUMBER(" ", "LightThresholdNumber", this->light_threshold_number_); + LOG_NUMBER(" ", "MaxStillDistanceGateNumber", this->max_still_distance_gate_number_); + LOG_NUMBER(" ", "MaxMoveDistanceGateNumber", this->max_move_distance_gate_number_); + LOG_NUMBER(" ", "TimeoutNumber", this->timeout_number_); + for (number::Number *n : this->gate_still_threshold_numbers_) { + LOG_NUMBER(" ", "Still Thresholds Number", n); + } + for (number::Number *n : this->gate_move_threshold_numbers_) { + LOG_NUMBER(" ", "Move Thresholds Number", n); + } +#endif + this->read_all_info(); + ESP_LOGCONFIG(TAG, " Throttle_ : %ums", this->throttle_); + ESP_LOGCONFIG(TAG, " MAC Address : %s", const_cast(this->mac_.c_str())); + ESP_LOGCONFIG(TAG, " Firmware Version : %s", const_cast(this->version_.c_str())); } void LD2410Component::setup() { ESP_LOGCONFIG(TAG, "Setting up LD2410..."); - this->set_config_mode_(true); - this->set_max_distances_timeout_(this->max_move_distance_, this->max_still_distance_, this->timeout_); - // Configure Gates sensitivity - this->set_gate_threshold_(0, this->rg0_move_threshold_, this->rg0_still_threshold_); - this->set_gate_threshold_(1, this->rg1_move_threshold_, this->rg1_still_threshold_); - this->set_gate_threshold_(2, this->rg2_move_threshold_, this->rg2_still_threshold_); - this->set_gate_threshold_(3, this->rg3_move_threshold_, this->rg3_still_threshold_); - this->set_gate_threshold_(4, this->rg4_move_threshold_, this->rg4_still_threshold_); - this->set_gate_threshold_(5, this->rg5_move_threshold_, this->rg5_still_threshold_); - this->set_gate_threshold_(6, this->rg6_move_threshold_, this->rg6_still_threshold_); - this->set_gate_threshold_(7, this->rg7_move_threshold_, this->rg7_still_threshold_); - this->set_gate_threshold_(8, this->rg8_move_threshold_, this->rg8_still_threshold_); - this->get_version_(); - this->set_config_mode_(false); - ESP_LOGCONFIG(TAG, "Firmware Version : %u.%u.%u%u%u%u", this->version_[0], this->version_[1], this->version_[2], - this->version_[3], this->version_[4], this->version_[5]); + this->read_all_info(); + ESP_LOGCONFIG(TAG, "Mac Address : %s", const_cast(this->mac_.c_str())); + ESP_LOGCONFIG(TAG, "Firmware Version : %s", const_cast(this->version_.c_str())); ESP_LOGCONFIG(TAG, "LD2410 setup complete."); } +void LD2410Component::read_all_info() { + this->set_config_mode_(true); + this->get_version_(); + this->get_mac_(); + this->get_distance_resolution_(); + this->get_light_control_(); + 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_->state != baud_rate) { + this->baud_rate_select_->publish_state(baud_rate); + } +#endif +} + +void LD2410Component::restart_and_read_all_info() { + this->set_config_mode_(true); + this->restart_(); + this->set_timeout(1000, [this]() { this->read_all_info(); }); +} + void LD2410Component::loop() { const int max_line_length = 80; static uint8_t buffer[max_line_length]; @@ -59,9 +116,8 @@ void LD2410Component::loop() { } } -void LD2410Component::send_command_(uint8_t command, uint8_t *command_value, int command_value_len) { - // lastCommandSuccess->publish_state(false); - +void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, int command_value_len) { + ESP_LOGV(TAG, "Sending COMMAND %02X", command); // frame start bytes this->write_array(CMD_FRAME_HEADER, 4); // length bytes @@ -95,40 +151,43 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { if (buffer[7] != HEAD || buffer[len - 6] != END || buffer[len - 5] != CHECK) // Check constant values return; // data head=0xAA, data end=0x55, crc=0x00 - /* - Data Type: 6th - 0x01: Engineering mode - 0x02: Normal mode - */ - // char data_type = buffer[DATA_TYPES]; - /* - Target states: 9th - 0x00 = No target - 0x01 = Moving targets - 0x02 = Still targets - 0x03 = Moving+Still targets - */ -#ifdef USE_BINARY_SENSOR - char target_state = buffer[TARGET_STATES]; - if (this->target_binary_sensor_ != nullptr) { - this->target_binary_sensor_->publish_state(target_state != 0x00); - } -#endif - /* Reduce data update rate to prevent home assistant database size grow fast */ int32_t current_millis = millis(); - if (current_millis - last_periodic_millis < 1000) + if (current_millis - last_periodic_millis_ < this->throttle_) return; - last_periodic_millis = current_millis; + last_periodic_millis_ = current_millis; -#ifdef USE_BINARY_SENSOR - if (this->moving_binary_sensor_ != nullptr) { - this->moving_binary_sensor_->publish_state(CHECK_BIT(target_state, 0)); + /* + Data Type: 7th + 0x01: Engineering mode + 0x02: Normal mode + */ + bool engineering_mode = buffer[DATA_TYPES] == 0x01; +#ifdef USE_SWITCH + if (this->engineering_mode_switch_ != nullptr && + current_millis - last_engineering_mode_change_millis_ > this->throttle_) { + this->engineering_mode_switch_->publish_state(engineering_mode); } - if (this->still_binary_sensor_ != nullptr) { - this->still_binary_sensor_->publish_state(CHECK_BIT(target_state, 1)); +#endif +#ifdef USE_BINARY_SENSOR + /* + Target states: 9th + 0x00 = No target + 0x01 = Moving targets + 0x02 = Still targets + 0x03 = Moving+Still targets + */ + char target_state = buffer[TARGET_STATES]; + if (this->target_binary_sensor_ != nullptr) { + this->target_binary_sensor_->publish_state(target_state != 0x00); + } + if (this->moving_target_binary_sensor_ != nullptr) { + this->moving_target_binary_sensor_->publish_state(CHECK_BIT(target_state, 0)); + } + if (this->still_target_binary_sensor_ != nullptr) { + this->still_target_binary_sensor_->publish_state(CHECK_BIT(target_state, 1)); } #endif /* @@ -164,26 +223,126 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { if (this->detection_distance_sensor_->get_state() != new_detect_distance) this->detection_distance_sensor_->publish_state(new_detect_distance); } + if (engineering_mode) { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving enery: 20~28th bytes + */ + for (std::vector::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { + sensor::Sensor *s = this->gate_move_sensors_[i]; + if (s != nullptr) { + s->publish_state(buffer[MOVING_SENSOR_START + i]); + } + } + /* + Still energy: 29~37th bytes + */ + for (std::vector::size_type i = 0; i != this->gate_still_sensors_.size(); i++) { + sensor::Sensor *s = this->gate_still_sensors_[i]; + if (s != nullptr) { + s->publish_state(buffer[STILL_SENSOR_START + i]); + } + } + /* + Light sensor: 38th bytes + */ + if (this->light_sensor_ != nullptr) { + int new_light_sensor = buffer[LIGHT_SENSOR]; + if (this->light_sensor_->get_state() != new_light_sensor) + this->light_sensor_->publish_state(new_light_sensor); + } + } else { + for (auto *s : this->gate_move_sensors_) { + if (s != nullptr && !std::isnan(s->get_state())) { + s->publish_state(NAN); + } + } + for (auto *s : this->gate_still_sensors_) { + if (s != nullptr && !std::isnan(s->get_state())) { + s->publish_state(NAN); + } + } + if (this->light_sensor_ != nullptr && !std::isnan(this->light_sensor_->get_state())) { + this->light_sensor_->publish_state(NAN); + } + } +#endif +#ifdef USE_BINARY_SENSOR + if (engineering_mode) { + if (this->out_pin_presence_status_binary_sensor_ != nullptr) { + this->out_pin_presence_status_binary_sensor_->publish_state(buffer[OUT_PIN_SENSOR] == 0x01); + } + } else { + if (this->out_pin_presence_status_binary_sensor_ != nullptr) { + this->out_pin_presence_status_binary_sensor_->publish_state(false); + } + } #endif } -void LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { - ESP_LOGV(TAG, "Handling ACK DATA for COMMAND"); +const char VERSION_FMT[] = "%u.%02X.%02X%02X%02X%02X"; + +std::string format_version(uint8_t *buffer) { + std::string::size_type version_size = 256; + std::string version; + do { + version.resize(version_size + 1); + version_size = std::snprintf(&version[0], version.size(), VERSION_FMT, buffer[13], buffer[12], buffer[17], + buffer[16], buffer[15], buffer[14]); + } while (version_size + 1 > version.size()); + version.resize(version_size); + return version; +} + +const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; + +const std::string UNKNOWN_MAC("unknown"); +const std::string NO_MAC("08:05:04:03:02:01"); + +std::string format_mac(uint8_t *buffer) { + std::string::size_type mac_size = 256; + std::string mac; + do { + mac.resize(mac_size + 1); + mac_size = std::snprintf(&mac[0], mac.size(), MAC_FMT, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], + buffer[15]); + } while (mac_size + 1 > mac.size()); + mac.resize(mac_size); + if (mac == NO_MAC) { + return UNKNOWN_MAC; + } + return mac; +} + +#ifdef USE_NUMBER +std::function set_number_value(number::Number *n, float value) { + float normalized_value = value * 1.0; + if (n != nullptr && (!n->has_state() || n->state != normalized_value)) { + n->state = normalized_value; + return [n, normalized_value]() { n->publish_state(normalized_value); }; + } + return []() {}; +} +#endif + +bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]); if (len < 10) { ESP_LOGE(TAG, "Error with last command : incorrect length"); - return; + return true; } if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // check 4 frame start bytes ESP_LOGE(TAG, "Error with last command : incorrect Header"); - return; + return true; } if (buffer[COMMAND_STATUS] != 0x01) { ESP_LOGE(TAG, "Error with last command : status != 0x01"); - return; + return true; } if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]); - return; + return true; } switch (buffer[COMMAND]) { @@ -193,49 +352,127 @@ void LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { case lowbyte(CMD_DISABLE_CONF): ESP_LOGV(TAG, "Handled Disabled conf command"); break; + case lowbyte(CMD_SET_BAUD_RATE): + ESP_LOGV(TAG, "Handled baud rate change command"); +#ifdef USE_SELECT + if (this->baud_rate_select_ != nullptr) { + ESP_LOGE(TAG, "Change baud rate component config to %s and reinstall", this->baud_rate_select_->state.c_str()); + } +#endif + break; case lowbyte(CMD_VERSION): - ESP_LOGV(TAG, "FW Version is: %u.%u.%u%u%u%u", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], - buffer[14]); - this->version_[0] = buffer[13]; - this->version_[1] = buffer[12]; - this->version_[2] = buffer[17]; - this->version_[3] = buffer[16]; - this->version_[4] = buffer[15]; - this->version_[5] = buffer[14]; - + this->version_ = format_version(buffer); + ESP_LOGV(TAG, "FW Version is: %s", const_cast(this->version_.c_str())); +#ifdef USE_TEXT_SENSOR + if (this->version_text_sensor_ != nullptr) { + this->version_text_sensor_->publish_state(this->version_); + } +#endif + break; + case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { + std::string distance_resolution = + DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); + ESP_LOGV(TAG, "Distance resolution is: %s", const_cast(distance_resolution.c_str())); +#ifdef USE_SELECT + if (this->distance_resolution_select_ != nullptr && + this->distance_resolution_select_->state != distance_resolution) { + this->distance_resolution_select_->publish_state(distance_resolution); + } +#endif + } break; + case lowbyte(CMD_QUERY_LIGHT_CONTROL): { + this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); + this->light_threshold_ = buffer[11] * 1.0; + this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); + ESP_LOGV(TAG, "Light function is: %s", const_cast(this->light_function_.c_str())); + ESP_LOGV(TAG, "Light threshold is: %f", this->light_threshold_); + ESP_LOGV(TAG, "Out pin level is: %s", const_cast(this->out_pin_level_.c_str())); +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) { + this->light_function_select_->publish_state(this->light_function_); + } + if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->state != this->out_pin_level_) { + this->out_pin_level_select_->publish_state(this->out_pin_level_); + } +#endif +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr && + (!this->light_threshold_number_->has_state() || + this->light_threshold_number_->state != this->light_threshold_)) { + this->light_threshold_number_->publish_state(this->light_threshold_); + } +#endif + } break; + case lowbyte(CMD_MAC): + if (len < 20) { + return false; + } + this->mac_ = format_mac(buffer); + ESP_LOGV(TAG, "MAC Address is: %s", const_cast(this->mac_.c_str())); +#ifdef USE_TEXT_SENSOR + if (this->mac_text_sensor_ != nullptr) { + this->mac_text_sensor_->publish_state(this->mac_); + } +#endif +#ifdef USE_SWITCH + if (this->bluetooth_switch_ != nullptr) { + this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); + } +#endif break; case lowbyte(CMD_GATE_SENS): ESP_LOGV(TAG, "Handled sensitivity command"); break; + case lowbyte(CMD_BLUETOOTH): + ESP_LOGV(TAG, "Handled bluetooth command"); + break; + case lowbyte(CMD_SET_DISTANCE_RESOLUTION): + ESP_LOGV(TAG, "Handled set distance resolution command"); + break; + case lowbyte(CMD_SET_LIGHT_CONTROL): + ESP_LOGV(TAG, "Handled set light control command"); + break; + case lowbyte(CMD_BT_PASSWORD): + ESP_LOGV(TAG, "Handled set bluetooth password command"); + break; case lowbyte(CMD_QUERY): // Query parameters response { if (buffer[10] != 0xAA) - return; // value head=0xAA + return true; // value head=0xAA +#ifdef USE_NUMBER /* Moving distance range: 13th byte Still distance range: 14th byte */ - // TODO - // maxMovingDistanceRange->publish_state(buffer[12]); - // maxStillDistanceRange->publish_state(buffer[13]); + std::vector> updates; + updates.push_back(set_number_value(this->max_move_distance_gate_number_, buffer[12])); + updates.push_back(set_number_value(this->max_still_distance_gate_number_, buffer[13])); /* Moving Sensitivities: 15~23th bytes + */ + for (std::vector::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], buffer[14 + i])); + } + /* Still Sensitivities: 24~32th bytes */ - for (int i = 0; i < 9; i++) { - moving_sensitivities[i] = buffer[14 + i]; - } - for (int i = 0; i < 9; i++) { - still_sensitivities[i] = buffer[23 + i]; + for (std::vector::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], buffer[23 + i])); } /* None Duration: 33~34th bytes */ - // noneDuration->publish_state(this->two_byte_to_int_(buffer[32], buffer[33])); + updates.push_back(set_number_value(this->timeout_number_, this->two_byte_to_int_(buffer[32], buffer[33]))); + for (auto &update : updates) { + update(); + } +#endif } break; default: break; } + + return true; } void LD2410Component::readline_(int readch, uint8_t *buffer, int len) { @@ -256,8 +493,11 @@ void LD2410Component::readline_(int readch, uint8_t *buffer, int len) { } else if (buffer[pos - 4] == 0x04 && buffer[pos - 3] == 0x03 && buffer[pos - 2] == 0x02 && buffer[pos - 1] == 0x01) { ESP_LOGV(TAG, "Will handle ACK Data"); - this->handle_ack_data_(buffer, pos); - pos = 0; // Reset position index ready for next time + if (this->handle_ack_data_(buffer, pos)) { + pos = 0; // Reset position index ready for next time + } else { + ESP_LOGV(TAG, "ACK Data incomplete"); + } } } } @@ -269,21 +509,85 @@ void LD2410Component::set_config_mode_(bool enable) { this->send_command_(cmd, enable ? cmd_value : nullptr, 2); } +void LD2410Component::set_bluetooth(bool enable) { + this->set_config_mode_(true); + uint8_t enable_cmd_value[2] = {0x01, 0x00}; + uint8_t disable_cmd_value[2] = {0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2410Component::set_distance_resolution(const std::string &state) { + this->set_config_mode_(true); + uint8_t cmd_value[2] = {DISTANCE_RESOLUTION_ENUM_TO_INT.at(state), 0x00}; + this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2410Component::set_baud_rate(const std::string &state) { + this->set_config_mode_(true); + uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); + this->set_timeout(200, [this]() { this->restart_(); }); +} + +void LD2410Component::set_bluetooth_password(const std::string &password) { + if (password.length() != 6) { + ESP_LOGE(TAG, "set_bluetooth_password(): invalid password length, must be exactly 6 chars '%s'", password.c_str()); + return; + } + this->set_config_mode_(true); + uint8_t cmd_value[6]; + std::copy(password.begin(), password.end(), std::begin(cmd_value)); + this->send_command_(CMD_BT_PASSWORD, cmd_value, 6); + this->set_config_mode_(false); +} + +void LD2410Component::set_engineering_mode(bool enable) { + this->set_config_mode_(true); + last_engineering_mode_change_millis_ = millis(); + uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; + this->send_command_(cmd, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2410Component::factory_reset() { + this->set_config_mode_(true); + this->send_command_(CMD_RESET, nullptr, 0); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2410Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } + void LD2410Component::query_parameters_() { this->send_command_(CMD_QUERY, nullptr, 0); } void LD2410Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } +void LD2410Component::get_mac_() { + uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(CMD_MAC, cmd_value, 2); +} +void LD2410Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } -void LD2410Component::set_max_distances_timeout_(uint8_t max_moving_distance_range, uint8_t max_still_distance_range, - uint16_t timeout) { +void LD2410Component::get_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } + +#ifdef USE_NUMBER +void LD2410Component::set_max_distances_timeout() { + if (!this->max_move_distance_gate_number_->has_state() || !this->max_still_distance_gate_number_->has_state() || + !this->timeout_number_->has_state()) { + return; + } + int max_moving_distance_gate_range = static_cast(this->max_move_distance_gate_number_->state); + int max_still_distance_gate_range = static_cast(this->max_still_distance_gate_number_->state); + int timeout = static_cast(this->timeout_number_->state); uint8_t value[18] = {0x00, 0x00, - lowbyte(max_moving_distance_range), - highbyte(max_moving_distance_range), + lowbyte(max_moving_distance_gate_range), + highbyte(max_moving_distance_gate_range), 0x00, 0x00, 0x01, 0x00, - lowbyte(max_still_distance_range), - highbyte(max_still_distance_range), + lowbyte(max_still_distance_gate_range), + highbyte(max_still_distance_gate_range), 0x00, 0x00, 0x02, @@ -292,10 +596,25 @@ void LD2410Component::set_max_distances_timeout_(uint8_t max_moving_distance_ran highbyte(timeout), 0x00, 0x00}; + this->set_config_mode_(true); this->send_command_(CMD_MAXDIST_DURATION, value, 18); + delay(50); // NOLINT this->query_parameters_(); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); + this->set_config_mode_(false); } -void LD2410Component::set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint8_t stillsens) { + +void LD2410Component::set_gate_threshold(uint8_t gate) { + number::Number *motionsens = this->gate_move_threshold_numbers_[gate]; + number::Number *stillsens = this->gate_still_threshold_numbers_[gate]; + + if (!motionsens->has_state() || !stillsens->has_state()) { + return; + } + int motion = static_cast(motionsens->state); + int still = static_cast(stillsens->state); + + this->set_config_mode_(true); // reference // https://drive.google.com/drive/folders/1p4dhbEJA3YubyIjIIC7wwVsSo8x29Fq-?spm=a2g0o.detail.1000023.17.93465697yFwVxH // Send data: configure the motion sensitivity of distance gate 3 to 40, and the static sensitivity of 40 @@ -305,11 +624,57 @@ void LD2410Component::set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint // 28 00 00 00 (value) // 02 00 (still sensitivtiy) // 28 00 00 00 (value) - uint8_t value[18] = {0x00, 0x00, lowbyte(gate), highbyte(gate), 0x00, 0x00, - 0x01, 0x00, lowbyte(motionsens), highbyte(motionsens), 0x00, 0x00, - 0x02, 0x00, lowbyte(stillsens), highbyte(stillsens), 0x00, 0x00}; + uint8_t value[18] = {0x00, 0x00, lowbyte(gate), highbyte(gate), 0x00, 0x00, + 0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00, + 0x02, 0x00, lowbyte(still), highbyte(still), 0x00, 0x00}; this->send_command_(CMD_GATE_SENS, value, 18); + delay(50); // NOLINT + this->query_parameters_(); + this->set_config_mode_(false); } +void LD2410Component::set_gate_still_threshold_number(int gate, number::Number *n) { + this->gate_still_threshold_numbers_[gate] = n; +} + +void LD2410Component::set_gate_move_threshold_number(int gate, number::Number *n) { + this->gate_move_threshold_numbers_[gate] = n; +} +#endif + +void LD2410Component::set_light_out_control() { +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { + this->light_threshold_ = this->light_threshold_number_->state; + } +#endif +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { + this->light_function_ = this->light_function_select_->state; + } + if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { + this->out_pin_level_ = this->out_pin_level_select_->state; + } +#endif + if (this->light_function_.empty() || this->out_pin_level_.empty() || this->light_threshold_ < 0) { + return; + } + this->set_config_mode_(true); + uint8_t light_function = LIGHT_FUNCTION_ENUM_TO_INT.at(this->light_function_); + uint8_t light_threshold = static_cast(this->light_threshold_); + uint8_t out_pin_level = OUT_PIN_LEVEL_ENUM_TO_INT.at(this->out_pin_level_); + uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; + this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); + delay(50); // NOLINT + this->get_light_control_(); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); + this->set_config_mode_(false); +} + +#ifdef USE_SENSOR +void LD2410Component::set_gate_move_sensor(int gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; } +void LD2410Component::set_gate_still_sensor(int gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; } +#endif + } // namespace ld2410 } // namespace esphome diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 8edb83a8d5..8084d4c33e 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -7,10 +7,27 @@ #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace ld2410 { @@ -19,10 +36,63 @@ namespace ld2410 { // Commands static const uint8_t CMD_ENABLE_CONF = 0x00FF; static const uint8_t CMD_DISABLE_CONF = 0x00FE; +static const uint8_t CMD_ENABLE_ENG = 0x0062; +static const uint8_t CMD_DISABLE_ENG = 0x0063; static const uint8_t CMD_MAXDIST_DURATION = 0x0060; static const uint8_t CMD_QUERY = 0x0061; static const uint8_t CMD_GATE_SENS = 0x0064; static const uint8_t CMD_VERSION = 0x00A0; +static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x00AB; +static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x00AA; +static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0x00AE; +static const uint8_t CMD_SET_LIGHT_CONTROL = 0x00AD; +static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; +static const uint8_t CMD_BT_PASSWORD = 0x00A9; +static const uint8_t CMD_MAC = 0x00A5; +static const uint8_t CMD_RESET = 0x00A2; +static const uint8_t CMD_RESTART = 0x00A3; +static const uint8_t CMD_BLUETOOTH = 0x00A4; + +enum BaudRateStructure : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8 +}; + +static const std::map BAUD_RATE_ENUM_TO_INT{ + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; + +enum DistanceResolutionStructure : uint8_t { DISTANCE_RESOLUTION_0_2 = 0x01, DISTANCE_RESOLUTION_0_75 = 0x00 }; + +static const std::map DISTANCE_RESOLUTION_ENUM_TO_INT{{"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.75m", DISTANCE_RESOLUTION_0_75}}; +static const std::map DISTANCE_RESOLUTION_INT_TO_ENUM{{DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}}; + +enum LightFunctionStructure : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02 +}; + +static const std::map LIGHT_FUNCTION_ENUM_TO_INT{ + {"off", LIGHT_FUNCTION_OFF}, {"below", LIGHT_FUNCTION_BELOW}, {"above", LIGHT_FUNCTION_ABOVE}}; +static const std::map LIGHT_FUNCTION_INT_TO_ENUM{ + {LIGHT_FUNCTION_OFF, "off"}, {LIGHT_FUNCTION_BELOW, "below"}, {LIGHT_FUNCTION_ABOVE, "above"}}; + +enum OutPinLevelStructure : uint8_t { OUT_PIN_LEVEL_LOW = 0x00, OUT_PIN_LEVEL_HIGH = 0x01 }; + +static const std::map OUT_PIN_LEVEL_ENUM_TO_INT{{"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}}; +static const std::map OUT_PIN_LEVEL_INT_TO_ENUM{{OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}}; // Commands values static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000; @@ -44,7 +114,7 @@ Target states: 9th byte Detect distance: 16~17th bytes */ enum PeriodicDataStructure : uint8_t { - DATA_TYPES = 5, + DATA_TYPES = 6, TARGET_STATES = 8, MOVING_TARGET_LOW = 9, MOVING_TARGET_HIGH = 10, @@ -54,6 +124,10 @@ enum PeriodicDataStructure : uint8_t { STILL_ENERGY = 14, DETECT_DISTANCE_LOW = 15, DETECT_DISTANCE_HIGH = 16, + MOVING_SENSOR_START = 19, + STILL_SENSOR_START = 28, + LIGHT_SENSOR = 37, + OUT_PIN_SENSOR = 38, }; enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 }; @@ -66,80 +140,97 @@ class LD2410Component : public Component, public uart::UARTDevice { SUB_SENSOR(still_target_distance) SUB_SENSOR(moving_target_energy) SUB_SENSOR(still_target_energy) + SUB_SENSOR(light) SUB_SENSOR(detection_distance) #endif +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(target) + SUB_BINARY_SENSOR(moving_target) + SUB_BINARY_SENSOR(still_target) + SUB_BINARY_SENSOR(out_pin_presence_status) +#endif +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(version) + SUB_TEXT_SENSOR(mac) +#endif +#ifdef USE_SELECT + SUB_SELECT(distance_resolution) + SUB_SELECT(baud_rate) + SUB_SELECT(light_function) + SUB_SELECT(out_pin_level) +#endif +#ifdef USE_SWITCH + SUB_SWITCH(engineering_mode) + SUB_SWITCH(bluetooth) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(reset) + SUB_BUTTON(restart) + SUB_BUTTON(query) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(max_still_distance_gate) + SUB_NUMBER(max_move_distance_gate) + SUB_NUMBER(timeout) + SUB_NUMBER(light_threshold) +#endif public: + LD2410Component(); void setup() override; void dump_config() override; void loop() override; - -#ifdef USE_BINARY_SENSOR - void set_target_sensor(binary_sensor::BinarySensor *sens) { this->target_binary_sensor_ = sens; }; - void set_moving_target_sensor(binary_sensor::BinarySensor *sens) { this->moving_binary_sensor_ = sens; }; - void set_still_target_sensor(binary_sensor::BinarySensor *sens) { this->still_binary_sensor_ = sens; }; + void set_light_out_control(); +#ifdef USE_NUMBER + void set_gate_still_threshold_number(int gate, number::Number *n); + void set_gate_move_threshold_number(int gate, number::Number *n); + void set_max_distances_timeout(); + void set_gate_threshold(uint8_t gate); #endif - - void set_timeout(uint16_t value) { this->timeout_ = value; }; - void set_max_move_distance(uint8_t value) { this->max_move_distance_ = value; }; - void set_max_still_distance(uint8_t value) { this->max_still_distance_ = value; }; - void set_range_config(int rg0_move, int rg0_still, int rg1_move, int rg1_still, int rg2_move, int rg2_still, - int rg3_move, int rg3_still, int rg4_move, int rg4_still, int rg5_move, int rg5_still, - int rg6_move, int rg6_still, int rg7_move, int rg7_still, int rg8_move, int rg8_still) { - this->rg0_move_threshold_ = rg0_move; - this->rg0_still_threshold_ = rg0_still; - this->rg1_move_threshold_ = rg1_move; - this->rg1_still_threshold_ = rg1_still; - this->rg2_move_threshold_ = rg2_move; - this->rg2_still_threshold_ = rg2_still; - this->rg3_move_threshold_ = rg3_move; - this->rg3_still_threshold_ = rg3_still; - this->rg4_move_threshold_ = rg4_move; - this->rg4_still_threshold_ = rg4_still; - this->rg5_move_threshold_ = rg5_move; - this->rg5_still_threshold_ = rg5_still; - this->rg6_move_threshold_ = rg6_move; - this->rg6_still_threshold_ = rg6_still; - this->rg7_move_threshold_ = rg7_move; - this->rg7_still_threshold_ = rg7_still; - this->rg8_move_threshold_ = rg8_move; - this->rg8_still_threshold_ = rg8_still; - }; - int moving_sensitivities[9] = {0}; - int still_sensitivities[9] = {0}; - - int32_t last_periodic_millis = millis(); +#ifdef USE_SENSOR + void set_gate_move_sensor(int gate, sensor::Sensor *s); + void set_gate_still_sensor(int gate, sensor::Sensor *s); +#endif + void set_throttle(uint16_t value) { this->throttle_ = value; }; + void set_bluetooth_password(const std::string &password); + void set_engineering_mode(bool enable); + void read_all_info(); + void restart_and_read_all_info(); + void set_bluetooth(bool enable); + void set_distance_resolution(const std::string &state); + void set_baud_rate(const std::string &state); + void factory_reset(); protected: -#ifdef USE_BINARY_SENSOR - binary_sensor::BinarySensor *target_binary_sensor_{nullptr}; - binary_sensor::BinarySensor *moving_binary_sensor_{nullptr}; - binary_sensor::BinarySensor *still_binary_sensor_{nullptr}; -#endif - - std::vector rx_buffer_; int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } - void send_command_(uint8_t command_str, uint8_t *command_value, int command_value_len); - - void set_max_distances_timeout_(uint8_t max_moving_distance_range, uint8_t max_still_distance_range, - uint16_t timeout); - void set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint8_t stillsens); + void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); void set_config_mode_(bool enable); void handle_periodic_data_(uint8_t *buffer, int len); - void handle_ack_data_(uint8_t *buffer, int len); + bool handle_ack_data_(uint8_t *buffer, int len); void readline_(int readch, uint8_t *buffer, int len); void query_parameters_(); void get_version_(); + void get_mac_(); + void get_distance_resolution_(); + void get_light_control_(); + void restart_(); - uint16_t timeout_; - uint8_t max_move_distance_; - uint8_t max_still_distance_; - - uint8_t version_[6]; - uint8_t rg0_move_threshold_, rg0_still_threshold_, rg1_move_threshold_, rg1_still_threshold_, rg2_move_threshold_, - rg2_still_threshold_, rg3_move_threshold_, rg3_still_threshold_, rg4_move_threshold_, rg4_still_threshold_, - rg5_move_threshold_, rg5_still_threshold_, rg6_move_threshold_, rg6_still_threshold_, rg7_move_threshold_, - rg7_still_threshold_, rg8_move_threshold_, rg8_still_threshold_; + int32_t last_periodic_millis_ = millis(); + int32_t last_engineering_mode_change_millis_ = millis(); + uint16_t throttle_; + std::string version_; + std::string mac_; + std::string out_pin_level_; + std::string light_function_; + float light_threshold_ = -1; +#ifdef USE_NUMBER + std::vector gate_still_threshold_numbers_ = std::vector(9); + std::vector gate_move_threshold_numbers_ = std::vector(9); +#endif +#ifdef USE_SENSOR + std::vector gate_still_sensors_ = std::vector(9); + std::vector gate_move_sensors_ = std::vector(9); +#endif }; } // namespace ld2410 diff --git a/esphome/components/ld2410/number/__init__.py b/esphome/components/ld2410/number/__init__.py new file mode 100644 index 0000000000..557b226dfe --- /dev/null +++ b/esphome/components/ld2410/number/__init__.py @@ -0,0 +1,128 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_TIMEOUT, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_ILLUMINANCE, + UNIT_SECOND, + UNIT_PERCENT, + ENTITY_CATEGORY_CONFIG, + ICON_MOTION_SENSOR, + ICON_TIMELAPSE, + ICON_LIGHTBULB, +) +from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns + +GateThresholdNumber = ld2410_ns.class_("GateThresholdNumber", number.Number) +LightThresholdNumber = ld2410_ns.class_("LightThresholdNumber", number.Number) +MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.Number) + +CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate" +CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate" +CONF_LIGHT_THRESHOLD = "light_threshold" +CONF_STILL_THRESHOLD = "still_threshold" +CONF_MOVE_THRESHOLD = "move_threshold" + +TIMEOUT_GROUP = "timeout" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Inclusive(CONF_TIMEOUT, TIMEOUT_GROUP): number.number_schema( + MaxDistanceTimeoutNumber, + unit_of_measurement=UNIT_SECOND, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + ), + cv.Inclusive(CONF_MAX_MOVE_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_MAX_STILL_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema( + LightThresholdNumber, + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"g{x}"): cv.Schema( + { + cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + unit_of_measurement=UNIT_PERCENT, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Required(CONF_STILL_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + unit_of_measurement=UNIT_PERCENT, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + } + ) + for x in range(9) + } +) + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if timeout_config := config.get(CONF_TIMEOUT): + n = await number.new_number( + timeout_config, min_value=0, max_value=65535, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_timeout_number(n)) + if max_move_distance_gate_config := config.get(CONF_MAX_MOVE_DISTANCE_GATE): + n = await number.new_number( + max_move_distance_gate_config, min_value=2, max_value=8, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_max_move_distance_gate_number(n)) + if max_still_distance_gate_config := config.get(CONF_MAX_STILL_DISTANCE_GATE): + n = await number.new_number( + max_still_distance_gate_config, min_value=2, max_value=8, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_max_still_distance_gate_number(n)) + if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD): + n = await number.new_number( + light_threshold_config, min_value=0, max_value=255, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_light_threshold_number(n)) + for x in range(9): + if gate_conf := config.get(f"g{x}"): + move_config = gate_conf[CONF_MOVE_THRESHOLD] + n = cg.new_Pvariable(move_config[CONF_ID], x) + await number.register_number( + n, move_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_gate_move_threshold_number(x, n)) + + still_config = gate_conf[CONF_STILL_THRESHOLD] + n = cg.new_Pvariable(still_config[CONF_ID], x) + await number.register_number( + n, still_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_gate_still_threshold_number(x, n)) diff --git a/esphome/components/ld2410/number/gate_threshold_number.cpp b/esphome/components/ld2410/number/gate_threshold_number.cpp new file mode 100644 index 0000000000..5d040554d7 --- /dev/null +++ b/esphome/components/ld2410/number/gate_threshold_number.cpp @@ -0,0 +1,14 @@ +#include "gate_threshold_number.h" + +namespace esphome { +namespace ld2410 { + +GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} + +void GateThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_gate_threshold(this->gate_); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/gate_threshold_number.h b/esphome/components/ld2410/number/gate_threshold_number.h new file mode 100644 index 0000000000..2806ecce63 --- /dev/null +++ b/esphome/components/ld2410/number/gate_threshold_number.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class GateThresholdNumber : public number::Number, public Parented { + public: + GateThresholdNumber(uint8_t gate); + + protected: + uint8_t gate_; + void control(float value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/light_threshold_number.cpp b/esphome/components/ld2410/number/light_threshold_number.cpp new file mode 100644 index 0000000000..0ff35782cd --- /dev/null +++ b/esphome/components/ld2410/number/light_threshold_number.cpp @@ -0,0 +1,12 @@ +#include "light_threshold_number.h" + +namespace esphome { +namespace ld2410 { + +void LightThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/light_threshold_number.h b/esphome/components/ld2410/number/light_threshold_number.h new file mode 100644 index 0000000000..8f014373c0 --- /dev/null +++ b/esphome/components/ld2410/number/light_threshold_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class LightThresholdNumber : public number::Number, public Parented { + public: + LightThresholdNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.cpp b/esphome/components/ld2410/number/max_distance_timeout_number.cpp new file mode 100644 index 0000000000..8a946f7ea9 --- /dev/null +++ b/esphome/components/ld2410/number/max_distance_timeout_number.cpp @@ -0,0 +1,12 @@ +#include "max_distance_timeout_number.h" + +namespace esphome { +namespace ld2410 { + +void MaxDistanceTimeoutNumber::control(float value) { + this->publish_state(value); + this->parent_->set_max_distances_timeout(); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.h b/esphome/components/ld2410/number/max_distance_timeout_number.h new file mode 100644 index 0000000000..7d91b4b5fe --- /dev/null +++ b/esphome/components/ld2410/number/max_distance_timeout_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class MaxDistanceTimeoutNumber : public number::Number, public Parented { + public: + MaxDistanceTimeoutNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/__init__.py b/esphome/components/ld2410/select/__init__.py new file mode 100644 index 0000000000..6c34a85ac6 --- /dev/null +++ b/esphome/components/ld2410/select/__init__.py @@ -0,0 +1,81 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ( + ENTITY_CATEGORY_CONFIG, + CONF_BAUD_RATE, + ICON_THERMOMETER, + ICON_SCALE, + ICON_LIGHTBULB, + ICON_RULER, +) +from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns + +BaudRateSelect = ld2410_ns.class_("BaudRateSelect", select.Select) +DistanceResolutionSelect = ld2410_ns.class_("DistanceResolutionSelect", select.Select) +LightOutControlSelect = ld2410_ns.class_("LightOutControlSelect", select.Select) + +CONF_DISTANCE_RESOLUTION = "distance_resolution" +CONF_LIGHT_FUNCTION = "light_function" +CONF_OUT_PIN_LEVEL = "out_pin_level" + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema( + DistanceResolutionSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RULER, + ), + cv.Optional(CONF_LIGHT_FUNCTION): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_BAUD_RATE): select.select_schema( + BaudRateSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_THERMOMETER, + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION): + s = await select.new_select( + distance_resolution_config, options=["0.2m", "0.75m"] + ) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_distance_resolution_select(s)) + if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL): + s = await select.new_select(out_pin_level_config, options=["low", "high"]) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_out_pin_level_select(s)) + if light_function_config := config.get(CONF_LIGHT_FUNCTION): + s = await select.new_select( + light_function_config, options=["off", "below", "above"] + ) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_light_function_select(s)) + if baud_rate_config := config.get(CONF_BAUD_RATE): + s = await select.new_select( + baud_rate_config, + options=[ + "9600", + "19200", + "38400", + "57600", + "115200", + "230400", + "256000", + "460800", + ], + ) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_baud_rate_select(s)) diff --git a/esphome/components/ld2410/select/baud_rate_select.cpp b/esphome/components/ld2410/select/baud_rate_select.cpp new file mode 100644 index 0000000000..f4e0b90e2e --- /dev/null +++ b/esphome/components/ld2410/select/baud_rate_select.cpp @@ -0,0 +1,12 @@ +#include "baud_rate_select.h" + +namespace esphome { +namespace ld2410 { + +void BaudRateSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_baud_rate(state); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h new file mode 100644 index 0000000000..3827b6a48a --- /dev/null +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class BaudRateSelect : public select::Select, public Parented { + public: + BaudRateSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp new file mode 100644 index 0000000000..eef34bda63 --- /dev/null +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -0,0 +1,12 @@ +#include "distance_resolution_select.h" + +namespace esphome { +namespace ld2410 { + +void DistanceResolutionSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_distance_resolution(state); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h new file mode 100644 index 0000000000..d6affb1020 --- /dev/null +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class DistanceResolutionSelect : public select::Select, public Parented { + public: + DistanceResolutionSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/light_out_control_select.cpp b/esphome/components/ld2410/select/light_out_control_select.cpp new file mode 100644 index 0000000000..ac23248a64 --- /dev/null +++ b/esphome/components/ld2410/select/light_out_control_select.cpp @@ -0,0 +1,12 @@ +#include "light_out_control_select.h" + +namespace esphome { +namespace ld2410 { + +void LightOutControlSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/light_out_control_select.h b/esphome/components/ld2410/select/light_out_control_select.h new file mode 100644 index 0000000000..5d72e1774e --- /dev/null +++ b/esphome/components/ld2410/select/light_out_control_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class LightOutControlSelect : public select::Select, public Parented { + public: + LightOutControlSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index b941263134..83361db58a 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -3,9 +3,15 @@ from esphome.components import sensor import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_DISTANCE, - DEVICE_CLASS_ENERGY, UNIT_CENTIMETER, UNIT_PERCENT, + CONF_LIGHT, + DEVICE_CLASS_ILLUMINANCE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_SIGNAL, + ICON_FLASH, + ICON_MOTION_SENSOR, + ICON_LIGHTBULB, ) from . import CONF_LD2410_ID, LD2410Component @@ -15,41 +21,88 @@ CONF_STILL_DISTANCE = "still_distance" CONF_MOVING_ENERGY = "moving_energy" CONF_STILL_ENERGY = "still_energy" CONF_DETECTION_DISTANCE = "detection_distance" +CONF_MOVE_ENERGY = "move_energy" -CONFIG_SCHEMA = { - cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), - cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( - device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER - ), - cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( - device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER - ), - cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=UNIT_PERCENT - ), - cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=UNIT_PERCENT - ), - cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( - device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER - ), -} +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + icon=ICON_SIGNAL, + ), + cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + icon=ICON_SIGNAL, + ), + cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_FLASH, + ), + cv.Optional(CONF_LIGHT): sensor.sensor_schema( + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + icon=ICON_SIGNAL, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"g{x}"): cv.Schema( + { + cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_FLASH, + ), + } + ) + for x in range(9) + } +) async def to_code(config): ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) - if CONF_MOVING_DISTANCE in config: - sens = await sensor.new_sensor(config[CONF_MOVING_DISTANCE]) + if moving_distance_config := config.get(CONF_MOVING_DISTANCE): + sens = await sensor.new_sensor(moving_distance_config) cg.add(ld2410_component.set_moving_target_distance_sensor(sens)) - if CONF_STILL_DISTANCE in config: - sens = await sensor.new_sensor(config[CONF_STILL_DISTANCE]) + if still_distance_config := config.get(CONF_STILL_DISTANCE): + sens = await sensor.new_sensor(still_distance_config) cg.add(ld2410_component.set_still_target_distance_sensor(sens)) - if CONF_MOVING_ENERGY in config: - sens = await sensor.new_sensor(config[CONF_MOVING_ENERGY]) + if moving_energy_config := config.get(CONF_MOVING_ENERGY): + sens = await sensor.new_sensor(moving_energy_config) cg.add(ld2410_component.set_moving_target_energy_sensor(sens)) - if CONF_STILL_ENERGY in config: - sens = await sensor.new_sensor(config[CONF_STILL_ENERGY]) + if still_energy_config := config.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_energy_config) cg.add(ld2410_component.set_still_target_energy_sensor(sens)) - if CONF_DETECTION_DISTANCE in config: - sens = await sensor.new_sensor(config[CONF_DETECTION_DISTANCE]) + if light_config := config.get(CONF_LIGHT): + sens = await sensor.new_sensor(light_config) + cg.add(ld2410_component.set_light_sensor(sens)) + if detection_distance_config := config.get(CONF_DETECTION_DISTANCE): + sens = await sensor.new_sensor(detection_distance_config) cg.add(ld2410_component.set_detection_distance_sensor(sens)) + for x in range(9): + if gate_conf := config.get(f"g{x}"): + if move_config := gate_conf.get(CONF_MOVE_ENERGY): + sens = await sensor.new_sensor(move_config) + cg.add(ld2410_component.set_gate_move_sensor(x, sens)) + if still_config := gate_conf.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_config) + cg.add(ld2410_component.set_gate_still_sensor(x, sens)) diff --git a/esphome/components/ld2410/switch/__init__.py b/esphome/components/ld2410/switch/__init__.py new file mode 100644 index 0000000000..096cb5f67a --- /dev/null +++ b/esphome/components/ld2410/switch/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_SWITCH, + ICON_BLUETOOTH, + ENTITY_CATEGORY_CONFIG, + ICON_PULSE, +) +from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns + +BluetoothSwitch = ld2410_ns.class_("BluetoothSwitch", switch.Switch) +EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch) + +CONF_ENGINEERING_MODE = "engineering_mode" +CONF_BLUETOOTH = "bluetooth" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema( + EngineeringModeSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), + cv.Optional(CONF_BLUETOOTH): switch.switch_schema( + BluetoothSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_BLUETOOTH, + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if engineering_mode_config := config.get(CONF_ENGINEERING_MODE): + s = await switch.new_switch(engineering_mode_config) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_engineering_mode_switch(s)) + if bluetooth_config := config.get(CONF_BLUETOOTH): + s = await switch.new_switch(bluetooth_config) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_bluetooth_switch(s)) diff --git a/esphome/components/ld2410/switch/bluetooth_switch.cpp b/esphome/components/ld2410/switch/bluetooth_switch.cpp new file mode 100644 index 0000000000..9bcee9b049 --- /dev/null +++ b/esphome/components/ld2410/switch/bluetooth_switch.cpp @@ -0,0 +1,12 @@ +#include "bluetooth_switch.h" + +namespace esphome { +namespace ld2410 { + +void BluetoothSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_bluetooth(state); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/switch/bluetooth_switch.h b/esphome/components/ld2410/switch/bluetooth_switch.h new file mode 100644 index 0000000000..35ae1ec0c9 --- /dev/null +++ b/esphome/components/ld2410/switch/bluetooth_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class BluetoothSwitch : public switch_::Switch, public Parented { + public: + BluetoothSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.cpp b/esphome/components/ld2410/switch/engineering_mode_switch.cpp new file mode 100644 index 0000000000..967c87c887 --- /dev/null +++ b/esphome/components/ld2410/switch/engineering_mode_switch.cpp @@ -0,0 +1,12 @@ +#include "engineering_mode_switch.h" + +namespace esphome { +namespace ld2410 { + +void EngineeringModeSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_engineering_mode(state); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.h b/esphome/components/ld2410/switch/engineering_mode_switch.h new file mode 100644 index 0000000000..e521200cd6 --- /dev/null +++ b/esphome/components/ld2410/switch/engineering_mode_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class EngineeringModeSwitch : public switch_::Switch, public Parented { + public: + EngineeringModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/text_sensor.py b/esphome/components/ld2410/text_sensor.py new file mode 100644 index 0000000000..d64472a7d3 --- /dev/null +++ b/esphome/components/ld2410/text_sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_VERSION, + CONF_MAC_ADDRESS, + ICON_BLUETOOTH, + ICON_CHIP, +) +from . import CONF_LD2410_ID, LD2410Component + +DEPENDENCIES = ["ld2410"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_BLUETOOTH + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if version_config := config.get(CONF_VERSION): + sens = await text_sensor.new_text_sensor(version_config) + cg.add(ld2410_component.set_version_text_sensor(sens)) + if mac_address_config := config.get(CONF_MAC_ADDRESS): + sens = await text_sensor.new_text_sensor(mac_address_config) + cg.add(ld2410_component.set_mac_text_sensor(sens)) diff --git a/esphome/components/ld2420/__init__.py b/esphome/components/ld2420/__init__.py new file mode 100644 index 0000000000..c701423081 --- /dev/null +++ b/esphome/components/ld2420/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@descipher"] + +DEPENDENCIES = ["uart"] + +MULTI_CONF = True + +ld2420_ns = cg.esphome_ns.namespace("ld2420") +LD2420Component = ld2420_ns.class_("LD2420Component", cg.Component, uart.UARTDevice) + +CONF_LD2420_ID = "ld2420_id" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2420Component), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "ld2420_uart", + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +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) diff --git a/esphome/components/ld2420/binary_sensor/__init__.py b/esphome/components/ld2420/binary_sensor/__init__.py new file mode 100644 index 0000000000..43e22d0348 --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID, DEVICE_CLASS_OCCUPANCY, CONF_HAS_TARGET +from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID + +LD2420BinarySensor = ld2420_ns.class_( + "LD2420BinarySensor", binary_sensor.BinarySensor, cg.Component +) + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(LD2420BinarySensor), + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY + ), + } + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if CONF_HAS_TARGET in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_TARGET]) + cg.add(var.set_presence_sensor(sens)) + ld2420 = await cg.get_variable(config[CONF_LD2420_ID]) + cg.add(ld2420.register_listener(var)) diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp new file mode 100644 index 0000000000..c6ea0a348b --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp @@ -0,0 +1,16 @@ +#include "ld2420_binary_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "LD2420.binary_sensor"; + +void LD2420BinarySensor::dump_config() { + ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:"); + LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_); +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h new file mode 100644 index 0000000000..ee06439090 --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../ld2420.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace ld2420 { + +class LD2420BinarySensor : public LD2420Listener, public Component, binary_sensor::BinarySensor { + public: + void dump_config() override; + void set_presence_sensor(binary_sensor::BinarySensor *bsensor) { this->presence_bsensor_ = bsensor; }; + void on_presence(bool presence) override { + if (this->presence_bsensor_ != nullptr) { + if (this->presence_bsensor_->state != presence) + this->presence_bsensor_->publish_state(presence); + } + } + + protected: + binary_sensor::BinarySensor *presence_bsensor_{nullptr}; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/button/__init__.py b/esphome/components/ld2420/button/__init__.py new file mode 100644 index 0000000000..df774ad7bc --- /dev/null +++ b/esphome/components/ld2420/button/__init__.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + CONF_FACTORY_RESET, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART, + ICON_RESTART_ALERT, + ICON_DATABASE, +) +from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns + +LD2420ApplyConfigButton = ld2420_ns.class_("LD2420ApplyConfigButton", button.Button) +LD2420RevertConfigButton = ld2420_ns.class_("LD2420RevertConfigButton", button.Button) +LD2420RestartModuleButton = ld2420_ns.class_("LD2420RestartModuleButton", button.Button) +LD2420FactoryResetButton = ld2420_ns.class_("LD2420FactoryResetButton", button.Button) + +CONF_APPLY_CONFIG = "apply_config" +CONF_REVERT_CONFIG = "revert_config" +CONF_RESTART_MODULE = "restart_module" + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Required(CONF_APPLY_CONFIG): button.button_schema( + LD2420ApplyConfigButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_REVERT_CONFIG): button.button_schema( + LD2420RevertConfigButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART, + ), + cv.Optional(CONF_RESTART_MODULE): button.button_schema( + LD2420RestartModuleButton, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_DATABASE, + ), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + LD2420FactoryResetButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_DATABASE, + ), +} + + +async def to_code(config): + ld2420_component = await cg.get_variable(config[CONF_LD2420_ID]) + if apply_config := config.get(CONF_APPLY_CONFIG): + b = await button.new_button(apply_config) + await cg.register_parented(b, config[CONF_LD2420_ID]) + cg.add(ld2420_component.set_apply_config_button(b)) + if revert_config := config.get(CONF_REVERT_CONFIG): + b = await button.new_button(revert_config) + await cg.register_parented(b, config[CONF_LD2420_ID]) + cg.add(ld2420_component.set_revert_config_button(b)) + if restart_config := config.get(CONF_RESTART_MODULE): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_LD2420_ID]) + cg.add(ld2420_component.set_restart_module_button(b)) + if factory_reset := config.get(CONF_FACTORY_RESET): + b = await button.new_button(factory_reset) + await cg.register_parented(b, config[CONF_LD2420_ID]) + cg.add(ld2420_component.set_factory_reset_button(b)) diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp new file mode 100644 index 0000000000..3537c1d64a --- /dev/null +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -0,0 +1,16 @@ +#include "reconfig_buttons.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +static const char *const TAG = "LD2420.button"; + +namespace esphome { +namespace 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 diff --git a/esphome/components/ld2420/button/reconfig_buttons.h b/esphome/components/ld2420/button/reconfig_buttons.h new file mode 100644 index 0000000000..4e9e7a3692 --- /dev/null +++ b/esphome/components/ld2420/button/reconfig_buttons.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2420.h" + +namespace esphome { +namespace ld2420 { + +class LD2420ApplyConfigButton : public button::Button, public Parented { + public: + LD2420ApplyConfigButton() = default; + + protected: + void press_action() override; +}; + +class LD2420RevertConfigButton : public button::Button, public Parented { + public: + LD2420RevertConfigButton() = default; + + protected: + void press_action() override; +}; + +class LD2420RestartModuleButton : public button::Button, public Parented { + public: + LD2420RestartModuleButton() = default; + + protected: + void press_action() override; +}; + +class LD2420FactoryResetButton : public button::Button, public Parented { + public: + LD2420FactoryResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp new file mode 100644 index 0000000000..e57fdbc84e --- /dev/null +++ b/esphome/components/ld2420/ld2420.cpp @@ -0,0 +1,778 @@ +#include "ld2420.h" +#include "esphome/core/helpers.h" + +/* +Configure commands - little endian + +No command can exceed 64 bytes, otherwise they would need be to be split up into multiple sends. + +All send command frames will have: + Header = FD FC FB FA, Bytes 0 - 3, uint32_t 0xFAFBFCFD + Length, bytes 4 - 5, uint16_t 0x0002, must be at least 2 for the command byte if no addon data. + Command bytes 6 - 7, uint16_t + Footer = 04 03 02 01 - uint32_t 0x01020304, Always last 4 Bytes. +Receive + Error bytes 8-9 uint16_t, 0 = success, all other positive values = error + +Enable config mode: +Send: + UART Tx: FD FC FB FA 04 00 FF 00 02 00 04 03 02 01 + Command = FF 00 - uint16_t 0x00FF + Protocol version = 02 00, can be 1 or 2 - uint16_t 0x0002 +Reply: + UART Rx: FD FC FB FA 06 00 FF 01 00 00 02 00 04 03 02 01 + +Disable config mode: +Send: + UART Tx: FD FC FB FA 02 00 FE 00 04 03 02 01 + Command = FE 00 - uint16_t 0x00FE +Receive: + UART Rx: FD FC FB FA 04 00 FE 01 00 00 04 03 02 01 + +Configure system parameters: + +UART Tx: FD FC FB FA 08 00 12 00 00 00 64 00 00 00 04 03 02 01 Set system parms +Command = 12 00 - uint16_t 0x0012, Param +There are three documented parameters for modes: + 00 64 = Basic status mode + This mode outputs text as presence "ON" or "OFF" and "Range XXXX" + where XXXX is a decimal value for distance in cm + 00 04 = Energy output mode + This mode outputs detailed signal energy values for each gate and the target distance. + The data format consist of the following. + Header HH, Length LL, Persence PP, Distance DD, 16 Gate Energies EE, Footer FF + HH HH HH HH LL LL PP DD DD EE EE .. 16x .. FF FF FF FF + F4 F3 F2 F1 23 00 00 00 00 00 00 .. .. .. .. F8 F7 F6 F5 + 00 00 = debug output mode + This mode outputs detailed values consisting of 20 Dopplers, 16 Ranges for a total 20 * 16 * 4 bytes + The data format consist of the following. + Header HH, Doppler DD, Range RR, Footer FF + HH HH HH HH DD DD DD DD .. 20x .. RR RR RR RR .. 16x .. FF FF FF FF + AA BF 10 14 00 00 00 00 .. .. .. .. 00 00 00 00 .. .. .. .. FD FC FB FA + +Configure gate sensitivity parameters: +UART Tx: FD FC FB FA 0E 00 07 00 10 00 60 EA 00 00 20 00 60 EA 00 00 04 03 02 01 +Command = 12 00 - uint16_t 0x0007 +Gate 0 high thresh = 10 00 uint16_t 0x0010, Threshold value = 60 EA 00 00 uint32_t 0x0000EA60 +Gate 0 low thresh = 20 00 uint16_t 0x0020, Threshold value = 60 EA 00 00 uint32_t 0x0000EA60 +*/ + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "ld2420"; + +float LD2420Component::get_setup_priority() const { return setup_priority::BUS; } + +void LD2420Component::dump_config() { + ESP_LOGCONFIG(TAG, "LD2420:"); + ESP_LOGCONFIG(TAG, " Firmware Version : %7s", this->ld2420_firmware_ver_); + ESP_LOGCONFIG(TAG, "LD2420 Number:"); +#ifdef USE_NUMBER + LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_); + LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_); + LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_); + LOG_NUMBER(TAG, " Gate Select:", this->gate_select_number_); + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + LOG_NUMBER(TAG, " Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]); + LOG_NUMBER(TAG, " Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]); + } +#endif +#ifdef USE_BUTTON + LOG_BUTTON(TAG, " Apply Config:", this->apply_config_button_); + LOG_BUTTON(TAG, " Revert Edits:", this->revert_config_button_); + LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_); + LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_); +#endif + ESP_LOGCONFIG(TAG, "LD2420 Select:"); + LOG_SELECT(TAG, " Operating Mode", this->operating_selector_); + if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + ESP_LOGW(TAG, "LD2420 Firmware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + } +} + +uint8_t LD2420Component::calc_checksum(void *data, size_t size) { + uint8_t checksum = 0; + uint8_t *data_bytes = (uint8_t *) data; + for (size_t i = 0; i < size; i++) { + checksum ^= data_bytes[i]; // XOR operation + } + return checksum; +} + +int LD2420Component::get_firmware_int_(const char *version_string) { + std::string version_str = version_string; + if (version_str[0] == 'v') { + version_str = version_str.substr(1); + } + version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end()); + int version_integer = stoi(version_str); + return version_integer; +} + +void LD2420Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up LD2420..."); + if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { + ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + this->mark_failed(); + return; + } + this->get_min_max_distances_timeout_(); +#ifdef USE_NUMBER + this->init_gate_config_numbers(); +#endif + this->get_firmware_version_(); + const char *pfw = this->ld2420_firmware_ver_; + std::string fw_str(pfw); + + for (auto &listener : listeners_) { + listener->on_fw_version(fw_str); + } + + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + delay_microseconds_safe(125); + this->get_gate_threshold_(gate); + } + + memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); + if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + this->set_operating_mode(OP_SIMPLE_MODE_STRING); + this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); + this->set_mode_(CMD_SYSTEM_MODE_SIMPLE); + ESP_LOGW(TAG, "LD2420 Frimware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + } else { + this->set_mode_(CMD_SYSTEM_MODE_ENERGY); + this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); + } +#ifdef USE_NUMBER + this->init_gate_config_numbers(); +#endif + this->set_system_mode(this->system_mode_); + this->set_config_mode(false); + ESP_LOGCONFIG(TAG, "LD2420 setup complete."); +} + +void LD2420Component::apply_config_action() { + const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config)); + if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) { + ESP_LOGCONFIG(TAG, "No configuration change detected"); + return; + } + ESP_LOGCONFIG(TAG, "Reconfiguring LD2420..."); + if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { + ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + this->mark_failed(); + return; + } + this->set_min_max_distances_timeout(this->new_config.max_gate, this->new_config.min_gate, this->new_config.timeout); + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + delay_microseconds_safe(125); + this->set_gate_threshold(gate); + } + memcpy(¤t_config, &new_config, sizeof(new_config)); +#ifdef USE_NUMBER + this->init_gate_config_numbers(); +#endif + this->set_system_mode(this->system_mode_); + this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm + this->set_operating_mode(OP_NORMAL_MODE_STRING); + ESP_LOGCONFIG(TAG, "LD2420 reconfig complete."); +} + +void LD2420Component::factory_reset_action() { + ESP_LOGCONFIG(TAG, "Setiing factory defaults..."); + if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { + ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + this->mark_failed(); + return; + } + this->set_min_max_distances_timeout(FACTORY_MAX_GATE, FACTORY_MIN_GATE, FACTORY_TIMEOUT); +#ifdef USE_NUMBER + this->gate_timeout_number_->state = FACTORY_TIMEOUT; + this->min_gate_distance_number_->state = FACTORY_MIN_GATE; + this->max_gate_distance_number_->state = FACTORY_MAX_GATE; +#endif + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate]; + this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate]; + delay_microseconds_safe(125); + this->set_gate_threshold(gate); + } + memcpy(&this->current_config, &this->new_config, sizeof(this->new_config)); + this->set_system_mode(this->system_mode_); + this->set_config_mode(false); +#ifdef USE_NUMBER + this->init_gate_config_numbers(); + this->refresh_gate_config_numbers(); +#endif + ESP_LOGCONFIG(TAG, "LD2420 factory reset complete."); +} + +void LD2420Component::restart_module_action() { + ESP_LOGCONFIG(TAG, "Restarting LD2420 module..."); + this->send_module_restart(); + this->set_timeout(250, [this]() { + this->set_config_mode(true); + this->set_system_mode(system_mode_); + this->set_config_mode(false); + }); + ESP_LOGCONFIG(TAG, "LD2420 Restarted."); +} + +void LD2420Component::revert_config_action() { + memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); +#ifdef USE_NUMBER + this->init_gate_config_numbers(); +#endif + ESP_LOGCONFIG(TAG, "Reverted config number edits."); +} + +void LD2420Component::loop() { + // If there is a active send command do not process it here, the send command call will handle it. + if (!get_cmd_active_()) { + if (!available()) + return; + static uint8_t buffer[2048]; + static uint8_t rx_data; + while (available()) { + rx_data = read(); + this->readline_(rx_data, buffer, sizeof(buffer)); + } + } +} + +void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) { + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + this->radar_data[gate][sample_number] = gate_energy[gate]; + } + this->total_sample_number_counter++; +} + +void LD2420Component::auto_calibrate_sensitivity() { + // Calculate average and peak values for each gate + const float move_factor = gate_move_sensitivity_factor + 1; + const float still_factor = (gate_still_sensitivity_factor / 2) + 1; + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + uint32_t sum = 0; + uint16_t peak = 0; + + for (uint8_t sample_number = 0; sample_number < CALIBRATE_SAMPLES; ++sample_number) { + // Calculate average + sum += this->radar_data[gate][sample_number]; + + // Calculate max value + if (this->radar_data[gate][sample_number] > peak) { + peak = this->radar_data[gate][sample_number]; + } + } + + // Store average and peak values + this->gate_avg[gate] = sum / CALIBRATE_SAMPLES; + if (this->gate_peak[gate] < peak) + this->gate_peak[gate] = peak; + + uint32_t calculated_value = + (static_cast(this->gate_peak[gate]) + (move_factor * static_cast(this->gate_peak[gate]))); + this->new_config.move_thresh[gate] = static_cast(calculated_value <= 65535 ? calculated_value : 65535); + calculated_value = + (static_cast(this->gate_peak[gate]) + (still_factor * static_cast(this->gate_peak[gate]))); + this->new_config.still_thresh[gate] = static_cast(calculated_value <= 65535 ? calculated_value : 65535); + } +} + +void LD2420Component::report_gate_data() { + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + // Output results + ESP_LOGI(TAG, "Gate: %2d Avg: %5d Peak: %5d", gate, this->gate_avg[gate], this->gate_peak[gate]); + } + ESP_LOGI(TAG, "Total samples: %d", this->total_sample_number_counter); +} + +void LD2420Component::set_operating_mode(const std::string &state) { + // If unsupported firmware ignore mode select + if (get_firmware_int_(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) { + this->current_operating_mode = OP_MODE_TO_UINT.at(state); + // Entering Auto Calibrate we need to clear the privoiuos data collection + this->operating_selector_->publish_state(state); + if (current_operating_mode == OP_CALIBRATE_MODE) { + this->set_calibration_(true); + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + this->gate_avg[gate] = 0; + this->gate_peak[gate] = 0; + for (uint8_t i = 0; i < CALIBRATE_SAMPLES; i++) { + this->radar_data[gate][i] = 0; + } + this->total_sample_number_counter = 0; + } + } else { + // Set the current data back so we don't have new data that can be applied in error. + if (this->get_calibration_()) + memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); + this->set_calibration_(false); + } + } else { + this->current_operating_mode = OP_SIMPLE_MODE; + this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); + } +} + +void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { + static int pos = 0; + + if (rx_data >= 0) { + if (pos < len - 1) { + buffer[pos++] = rx_data; + buffer[pos] = 0; + } else { + pos = 0; + } + if (pos >= 4) { + if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { + this->set_cmd_active_(false); // Set command state to inactive after responce. + this->handle_ack_data_(buffer, pos); + pos = 0; + } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && (get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { + this->handle_simple_mode_(buffer, pos); + pos = 0; + } else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && + (get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { + this->handle_energy_mode_(buffer, pos); + pos = 0; + } + } + } +} + +void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { + uint8_t index = 6; // Start at presence byte position + uint16_t range; + const uint8_t elements = sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]); + this->set_presence_(buffer[index]); + index++; + memcpy(&range, &buffer[index], sizeof(range)); + index += sizeof(range); + this->set_distance_(range); + for (uint8_t i = 0; i < elements; i++) { // NOLINT + memcpy(&this->gate_energy_[i], &buffer[index], sizeof(this->gate_energy_[0])); + index += sizeof(this->gate_energy_[0]); + } + + if (this->current_operating_mode == OP_CALIBRATE_MODE) { + this->update_radar_data(gate_energy_, sample_number_counter); + this->sample_number_counter > CALIBRATE_SAMPLES ? this->sample_number_counter = 0 : this->sample_number_counter++; + } + + // Resonable refresh rate for home assistant database size health + const int32_t current_millis = millis(); + if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) + return; + this->last_periodic_millis = current_millis; + for (auto &listener : this->listeners_) { + listener->on_distance(get_distance_()); + listener->on_presence(get_presence_()); + listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0])); + } + + if (this->current_operating_mode == OP_CALIBRATE_MODE) { + this->auto_calibrate_sensitivity(); + if (current_millis - this->report_periodic_millis > REFRESH_RATE_MS * CALIBRATE_REPORT_INTERVAL) { + this->report_periodic_millis = current_millis; + this->report_gate_data(); + } + } +} + +void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { + const uint8_t bufsize = 16; + uint8_t index{0}; + uint8_t pos{0}; + char *endptr{nullptr}; + char outbuf[bufsize]{0}; + while (true) { + if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { + set_presence_(false); + } else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { + set_presence_(true); + } + if (inbuf[pos] >= '0' && inbuf[pos] <= '9') { + if (index < bufsize - 1) { + outbuf[index++] = inbuf[pos]; + pos++; + } + } else { + if (pos < len - 1) { + pos++; + } else { + break; + } + } + } + outbuf[index] = '\0'; + if (index > 1) + set_distance_(strtol(outbuf, &endptr, 10)); + + if (get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { + // Resonable refresh rate for home assistant database size health + const int32_t current_millis = millis(); + if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) + return; + this->last_normal_periodic_millis = current_millis; + for (auto &listener : this->listeners_) + listener->on_distance(get_distance_()); + for (auto &listener : this->listeners_) + listener->on_presence(get_presence_()); + } +} + +void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { + this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND]; + this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH]; + uint8_t reg_element = 0; + uint8_t data_element = 0; + uint16_t data_pos = 0; + if (this->cmd_reply_.length > CMD_MAX_BYTES) { + ESP_LOGW(TAG, "LD2420 reply - received command reply frame is corrupt, length exceeds %d bytes.", CMD_MAX_BYTES); + return; + } else if (this->cmd_reply_.length < 2) { + ESP_LOGW(TAG, "LD2420 reply - received command frame is corrupt, length is less than 2 bytes."); + return; + } + memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error)); + const char *result = this->cmd_reply_.error ? "failure" : "success"; + if (this->cmd_reply_.error > 0) { + return; + }; + this->cmd_reply_.ack = true; + switch ((uint16_t) this->cmd_reply_.command) { + case (CMD_ENABLE_CONF): + ESP_LOGD(TAG, "LD2420 reply - set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result); + break; + case (CMD_DISABLE_CONF): + ESP_LOGD(TAG, "LD2420 reply - set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); + break; + case (CMD_READ_REGISTER): + ESP_LOGD(TAG, "LD2420 reply - read register: CMD = %2X %s", CMD_READ_REGISTER, result); + // TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file + data_pos = 0x0A; + for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT + ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE)); + index += CMD_REG_DATA_REPLY_SIZE) { + memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], sizeof(CMD_REG_DATA_REPLY_SIZE)); + byteswap(this->cmd_reply_.data[reg_element]); + reg_element++; + } + break; + case (CMD_WRITE_REGISTER): + ESP_LOGD(TAG, "LD2420 reply - write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); + break; + case (CMD_WRITE_ABD_PARAM): + ESP_LOGD(TAG, "LD2420 reply - write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); + break; + case (CMD_READ_ABD_PARAM): + ESP_LOGD(TAG, "LD2420 reply - read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); + data_pos = CMD_ABD_DATA_REPLY_START; + for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT + ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE)); + index += CMD_ABD_DATA_REPLY_SIZE) { + memcpy(&this->cmd_reply_.data[data_element], &buffer[data_pos + index], + sizeof(this->cmd_reply_.data[data_element])); + byteswap(this->cmd_reply_.data[data_element]); + data_element++; + } + break; + case (CMD_WRITE_SYS_PARAM): + ESP_LOGD(TAG, "LD2420 reply - set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); + break; + case (CMD_READ_VERSION): + memcpy(this->ld2420_firmware_ver_, &buffer[12], buffer[10]); + ESP_LOGD(TAG, "LD2420 reply - module firmware version: %7s %s", this->ld2420_firmware_ver_, result); + break; + default: + break; + } +} + +int LD2420Component::send_cmd_from_array(CmdFrameT frame) { + uint32_t start_millis = millis(); + uint8_t error = 0; + uint8_t ack_buffer[64]; + uint8_t cmd_buffer[64]; + this->cmd_reply_.ack = false; + if (frame.command != CMD_RESTART) + this->set_cmd_active_(true); // Restart does not reply, thus no ack state required. + uint8_t retry = 3; + while (retry) { + frame.length = 0; + uint16_t frame_data_bytes = frame.data_length + 2; // Always add two bytes for the cmd size + + memcpy(&cmd_buffer[frame.length], &frame.header, sizeof(frame.header)); + frame.length += sizeof(frame.header); + + memcpy(&cmd_buffer[frame.length], &frame_data_bytes, sizeof(frame.data_length)); + frame.length += sizeof(frame.data_length); + + memcpy(&cmd_buffer[frame.length], &frame.command, sizeof(frame.command)); + frame.length += sizeof(frame.command); + + for (uint16_t index = 0; index < frame.data_length; index++) { + memcpy(&cmd_buffer[frame.length], &frame.data[index], sizeof(frame.data[index])); + frame.length += sizeof(frame.data[index]); + } + + memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer)); + frame.length += sizeof(frame.footer); + for (uint16_t index = 0; index < frame.length; index++) { + this->write_byte(cmd_buffer[index]); + } + + error = 0; + if (frame.command == CMD_RESTART) { + return 0; // restart does not reply exit now + } + + while (!this->cmd_reply_.ack) { + while (available()) { + this->readline_(read(), ack_buffer, sizeof(ack_buffer)); + } + delay_microseconds_safe(1450); + // Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT. + if ((millis() - start_millis) > 1000) { + start_millis = millis(); + error = LD2420_ERROR_TIMEOUT; + retry--; + break; + } + } + if (this->cmd_reply_.ack) + retry = 0; + if (this->cmd_reply_.error > 0) + handle_cmd_error(error); + } + return error; +} + +uint8_t LD2420Component::set_config_mode(bool enable) { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + if (enable) { + memcpy(&cmd_frame.data[0], &CMD_PROTOCOL_VER, sizeof(CMD_PROTOCOL_VER)); + cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER); + } + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); + return this->send_cmd_from_array(cmd_frame); +} + +// Sends a restart and set system running mode to normal +void LD2420Component::send_module_restart() { this->ld2420_restart(); } + +void LD2420Component::ld2420_restart() { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_RESTART; + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending restart command: %2X", cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::get_reg_value_(uint16_t reg) { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_READ_REGISTER; + cmd_frame.data[1] = reg; + cmd_frame.data_length += 2; + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_WRITE_REGISTER; + memcpy(&cmd_frame.data[cmd_frame.data_length], ®, sizeof(CMD_REG_DATA_REPLY_SIZE)); + cmd_frame.data_length += 2; + memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE)); + cmd_frame.data_length += 2; + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGI(TAG, "Command failed: %s", ERR_MESSAGE[error]); } + +int LD2420Component::get_gate_threshold_(uint8_t gate) { + uint8_t error; + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_READ_ABD_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_MOVE_THRESH[gate], sizeof(CMD_GATE_MOVE_THRESH[gate])); + cmd_frame.data_length += 2; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate])); + cmd_frame.data_length += 2; + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending read gate %d high/low theshold command: %2X", gate, cmd_frame.command); + error = this->send_cmd_from_array(cmd_frame); + if (error == 0) { + this->current_config.move_thresh[gate] = cmd_reply_.data[0]; + this->current_config.still_thresh[gate] = cmd_reply_.data[1]; + } + return error; +} + +int LD2420Component::get_min_max_distances_timeout_() { + uint8_t error; + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_READ_ABD_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG, + sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number + cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG, + sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number + cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG, + sizeof(CMD_TIMEOUT_REG)); // Register: global delay time + cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG); + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command); + error = this->send_cmd_from_array(cmd_frame); + if (error == 0) { + this->current_config.min_gate = (uint16_t) cmd_reply_.data[0]; + this->current_config.max_gate = (uint16_t) cmd_reply_.data[1]; + this->current_config.timeout = (uint16_t) cmd_reply_.data[2]; + } + return error; +} + +void LD2420Component::set_system_mode(uint16_t mode) { + CmdFrameT cmd_frame; + uint16_t unknown_parm = 0x0000; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_WRITE_SYS_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_SYSTEM_MODE, sizeof(CMD_SYSTEM_MODE)); + cmd_frame.data_length += sizeof(CMD_SYSTEM_MODE); + memcpy(&cmd_frame.data[cmd_frame.data_length], &mode, sizeof(mode)); + cmd_frame.data_length += sizeof(mode); + memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm)); + cmd_frame.data_length += sizeof(unknown_parm); + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending write system mode command: %2X", cmd_frame.command); + if (this->send_cmd_from_array(cmd_frame) == 0) + set_mode_(mode); +} + +void LD2420Component::get_firmware_version_() { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_READ_VERSION; + cmd_frame.footer = CMD_FRAME_FOOTER; + + ESP_LOGD(TAG, "Sending read firmware version command: %2X", cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, // NOLINT + uint32_t timeout) { + // Header H, Length L, Register R, Value V, Footer F + // |Min Gate |Max Gate |Timeout | + // HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF + // FD FC FB FA 14 00 07 00 00 00 01 00 00 00 01 00 09 00 00 00 04 00 0A 00 00 00 04 03 02 01 e.g. + + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_WRITE_ABD_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG, + sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number + cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &min_gate_distance, sizeof(min_gate_distance)); + cmd_frame.data_length += sizeof(min_gate_distance); + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG, + sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number + cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &max_gate_distance, sizeof(max_gate_distance)); + cmd_frame.data_length += sizeof(max_gate_distance); + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG, + sizeof(CMD_TIMEOUT_REG)); // Register: global delay time + cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &timeout, sizeof(timeout)); + ; + cmd_frame.data_length += sizeof(timeout); + cmd_frame.footer = CMD_FRAME_FOOTER; + + ESP_LOGD(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::set_gate_threshold(uint8_t gate) { + // Header H, Length L, Command C, Register R, Value V, Footer F + // HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF + // FD FC FB FA 14 00 07 00 10 00 00 FF 00 00 00 01 00 0F 00 00 04 03 02 01 + + uint16_t move_threshold_gate = CMD_GATE_MOVE_THRESH[gate]; + uint16_t still_threshold_gate = CMD_GATE_STILL_THRESH[gate]; + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_WRITE_ABD_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &move_threshold_gate, sizeof(move_threshold_gate)); + cmd_frame.data_length += sizeof(move_threshold_gate); + memcpy(&cmd_frame.data[cmd_frame.data_length], &this->new_config.move_thresh[gate], + sizeof(this->new_config.move_thresh[gate])); + cmd_frame.data_length += sizeof(this->new_config.move_thresh[gate]); + memcpy(&cmd_frame.data[cmd_frame.data_length], &still_threshold_gate, sizeof(still_threshold_gate)); + cmd_frame.data_length += sizeof(still_threshold_gate); + memcpy(&cmd_frame.data[cmd_frame.data_length], &this->new_config.still_thresh[gate], + sizeof(this->new_config.still_thresh[gate])); + cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]); + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +#ifdef USE_NUMBER +void LD2420Component::init_gate_config_numbers() { + if (this->gate_timeout_number_ != nullptr) + this->gate_timeout_number_->publish_state(static_cast(this->current_config.timeout)); + if (this->gate_select_number_ != nullptr) + this->gate_select_number_->publish_state(0); + if (this->min_gate_distance_number_ != nullptr) + this->min_gate_distance_number_->publish_state(static_cast(this->current_config.min_gate)); + if (this->max_gate_distance_number_ != nullptr) + this->max_gate_distance_number_->publish_state(static_cast(this->current_config.max_gate)); + if (this->gate_move_sensitivity_factor_number_ != nullptr) + this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); + if (this->gate_still_sensitivity_factor_number_ != nullptr) + this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + if (this->gate_still_threshold_numbers_[gate] != nullptr) { + this->gate_still_threshold_numbers_[gate]->publish_state( + static_cast(this->current_config.still_thresh[gate])); + } + if (this->gate_move_threshold_numbers_[gate] != nullptr) { + this->gate_move_threshold_numbers_[gate]->publish_state( + static_cast(this->current_config.move_thresh[gate])); + } + } +} + +void LD2420Component::refresh_gate_config_numbers() { + this->gate_timeout_number_->publish_state(this->new_config.timeout); + this->min_gate_distance_number_->publish_state(this->new_config.min_gate); + this->max_gate_distance_number_->publish_state(this->new_config.max_gate); +} + +#endif + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h new file mode 100644 index 0000000000..2b50c7a1d4 --- /dev/null +++ b/esphome/components/ld2420/ld2420.h @@ -0,0 +1,271 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#include +#include + +namespace esphome { +namespace ld2420 { + +// Local const's +static const uint16_t REFRESH_RATE_MS = 1000; + +// Command sets +static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static const uint16_t CMD_DISABLE_CONF = 0x00FE; +static const uint16_t CMD_ENABLE_CONF = 0x00FF; +static const uint8_t CMD_MAX_BYTES = 0x64; +static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static const uint16_t CMD_PROTOCOL_VER = 0x0002; +static const uint16_t CMD_READ_ABD_PARAM = 0x0008; +static const uint16_t CMD_READ_REG_ADDR = 0x0020; +static const uint16_t CMD_READ_REGISTER = 0x0002; +static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static const uint16_t CMD_READ_SYS_PARAM = 0x0013; +static const uint16_t CMD_READ_VERSION = 0x0000; +static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static const uint16_t CMD_RESTART = 0x0068; +static const uint16_t CMD_SYSTEM_MODE = 0x0000; +static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static const uint16_t CMD_WRITE_REGISTER = 0x0001; +static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; + +static const uint8_t LD2420_ERROR_NONE = 0x00; +static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static const uint8_t LD2420_TOTAL_GATES = 16; +static const uint8_t CALIBRATE_SAMPLES = 64; + +// Register address values +static const uint16_t CMD_MIN_GATE_REG = 0x0000; +static const uint16_t CMD_MAX_GATE_REG = 0x0001; +static const uint16_t CMD_TIMEOUT_REG = 0x0004; +static const uint16_t CMD_GATE_MOVE_THRESH[LD2420_TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static const uint16_t CMD_GATE_STILL_THRESH[LD2420_TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static const uint32_t FACTORY_MOVE_THRESH[LD2420_TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static const uint32_t FACTORY_STILL_THRESH[LD2420_TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static const uint16_t FACTORY_TIMEOUT = 120; +static const uint16_t FACTORY_MIN_GATE = 1; +static const uint16_t FACTORY_MAX_GATE = 12; + +// COMMAND_BYTE Header & Footer +static const uint8_t CMD_FRAME_COMMAND = 6; +static const uint8_t CMD_FRAME_DATA_LENGTH = 4; +static const uint32_t CMD_FRAME_FOOTER = 0x01020304; +static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static const uint8_t CMD_FRAME_STATUS = 7; +static const uint8_t CMD_ERROR_WORD = 8; +static const uint8_t ENERGY_SENSOR_START = 9; +static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static const int CALIBRATE_VERSION_MIN = 154; +static const std::string OP_NORMAL_MODE_STRING = "Normal"; +static const std::string OP_SIMPLE_MODE_STRING = "Simple"; + +enum OpModeStruct : uint8_t { OP_NORMAL_MODE = 1, OP_CALIBRATE_MODE = 2, OP_SIMPLE_MODE = 3 }; +static const std::map OP_MODE_TO_UINT{ + {"Normal", OP_NORMAL_MODE}, {"Calibrate", OP_CALIBRATE_MODE}, {"Simple", OP_SIMPLE_MODE}}; +static constexpr const char *ERR_MESSAGE[] = {"None", "Unknown", "Timeout"}; + +class LD2420Listener { + public: + virtual void on_presence(bool presence){}; + virtual void on_distance(uint16_t distance){}; + virtual void on_energy(uint16_t *sensor_energy, size_t size){}; + virtual void on_fw_version(std::string &fw){}; +}; + +class LD2420Component : public Component, public uart::UARTDevice { + public: + void setup() override; + void dump_config() override; + void loop() override; +#ifdef USE_SELECT + void set_operating_mode_select(select::Select *selector) { this->operating_selector_ = selector; }; +#endif +#ifdef USE_NUMBER + void set_gate_timeout_number(number::Number *number) { this->gate_timeout_number_ = number; }; + void set_gate_select_number(number::Number *number) { this->gate_select_number_ = number; }; + void set_min_gate_distance_number(number::Number *number) { this->min_gate_distance_number_ = number; }; + void set_max_gate_distance_number(number::Number *number) { this->max_gate_distance_number_ = number; }; + void set_gate_move_sensitivity_factor_number(number::Number *number) { + this->gate_move_sensitivity_factor_number_ = number; + }; + void set_gate_still_sensitivity_factor_number(number::Number *number) { + this->gate_still_sensitivity_factor_number_ = number; + }; + void set_gate_still_threshold_numbers(int gate, number::Number *n) { this->gate_still_threshold_numbers_[gate] = n; }; + void set_gate_move_threshold_numbers(int gate, number::Number *n) { this->gate_move_threshold_numbers_[gate] = n; }; + bool is_gate_select() { return gate_select_number_ != nullptr; }; + uint8_t get_gate_select_value() { return static_cast(this->gate_select_number_->state); }; + float get_min_gate_distance_value() { return min_gate_distance_number_->state; }; + float get_max_gate_distance_value() { return max_gate_distance_number_->state; }; + void publish_gate_move_threshold(uint8_t gate) { + // With gate_select we only use 1 number pointer, thus we hard code [0] + this->gate_move_threshold_numbers_[0]->publish_state(this->new_config.move_thresh[gate]); + }; + void publish_gate_still_threshold(uint8_t gate) { + this->gate_still_threshold_numbers_[0]->publish_state(this->new_config.still_thresh[gate]); + }; + void init_gate_config_numbers(); + void refresh_gate_config_numbers(); +#endif +#ifdef USE_BUTTON + void set_apply_config_button(button::Button *button) { this->apply_config_button_ = button; }; + void set_revert_config_button(button::Button *button) { this->revert_config_button_ = button; }; + void set_restart_module_button(button::Button *button) { this->restart_module_button_ = button; }; + void set_factory_reset_button(button::Button *button) { this->factory_reset_button_ = button; }; +#endif + void register_listener(LD2420Listener *listener) { this->listeners_.push_back(listener); } + + struct CmdFrameT { + uint32_t header{0}; + uint16_t length{0}; + uint16_t command{0}; + uint8_t data[18]; + uint16_t data_length{0}; + uint32_t footer{0}; + }; + + struct RegConfigT { + uint16_t min_gate{0}; + uint16_t max_gate{0}; + uint16_t timeout{0}; + uint32_t move_thresh[LD2420_TOTAL_GATES]; + uint32_t still_thresh[LD2420_TOTAL_GATES]; + }; + + void send_module_restart(); + void restart_module_action(); + void apply_config_action(); + void factory_reset_action(); + void revert_config_action(); + float get_setup_priority() const override; + int send_cmd_from_array(CmdFrameT cmd_frame); + void report_gate_data(); + void handle_cmd_error(uint8_t error); + void set_operating_mode(const std::string &state); + void auto_calibrate_sensitivity(); + void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); + uint8_t calc_checksum(void *data, size_t size); + + RegConfigT current_config; + RegConfigT new_config; + int32_t last_periodic_millis = millis(); + int32_t report_periodic_millis = millis(); + int32_t monitor_periodic_millis = millis(); + int32_t last_normal_periodic_millis = millis(); + bool output_energy_state{false}; + uint8_t current_operating_mode{OP_NORMAL_MODE}; + uint16_t radar_data[LD2420_TOTAL_GATES][CALIBRATE_SAMPLES]; + uint16_t gate_avg[LD2420_TOTAL_GATES]; + uint16_t gate_peak[LD2420_TOTAL_GATES]; + uint8_t sample_number_counter{0}; + uint16_t total_sample_number_counter{0}; + float gate_move_sensitivity_factor{0.5}; + float gate_still_sensitivity_factor{0.5}; +#ifdef USE_SELECT + select::Select *operating_selector_{nullptr}; +#endif +#ifdef USE_BUTTON + button::Button *apply_config_button_{nullptr}; + button::Button *revert_config_button_{nullptr}; + button::Button *restart_module_button_{nullptr}; + button::Button *factory_reset_button_{nullptr}; +#endif + void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout); + void set_gate_threshold(uint8_t gate); + void set_reg_value(uint16_t reg, uint16_t value); + uint8_t set_config_mode(bool enable); + void set_system_mode(uint16_t mode); + void ld2420_restart(); + + protected: + struct CmdReplyT { + uint8_t command; + uint8_t status; + uint32_t data[4]; + uint8_t length; + uint16_t error; + volatile bool ack; + }; + + int get_firmware_int_(const char *version_string); + void get_firmware_version_(); + int get_gate_threshold_(uint8_t gate); + void get_reg_value_(uint16_t reg); + int get_min_max_distances_timeout_(); + uint16_t get_mode_() { return this->system_mode_; }; + void set_mode_(uint16_t mode) { this->system_mode_ = mode; }; + bool get_presence_() { return this->presence_; }; + void set_presence_(bool presence) { this->presence_ = presence; }; + uint16_t get_distance_() { return this->distance_; }; + void set_distance_(uint16_t distance) { this->distance_ = distance; }; + bool get_cmd_active_() { return this->cmd_active_; }; + void set_cmd_active_(bool active) { this->cmd_active_ = active; }; + void handle_simple_mode_(const uint8_t *inbuf, int len); + void handle_energy_mode_(uint8_t *buffer, int len); + void handle_ack_data_(uint8_t *buffer, int len); + void readline_(int rx_data, uint8_t *buffer, int len); + void set_calibration_(bool state) { this->calibration_ = state; }; + bool get_calibration_() { return this->calibration_; }; + +#ifdef USE_NUMBER + number::Number *gate_timeout_number_{nullptr}; + number::Number *gate_select_number_{nullptr}; + number::Number *min_gate_distance_number_{nullptr}; + number::Number *max_gate_distance_number_{nullptr}; + number::Number *gate_move_sensitivity_factor_number_{nullptr}; + number::Number *gate_still_sensitivity_factor_number_{nullptr}; + std::vector gate_still_threshold_numbers_ = std::vector(16); + std::vector gate_move_threshold_numbers_ = std::vector(16); +#endif + + uint16_t gate_energy_[LD2420_TOTAL_GATES]; + CmdReplyT cmd_reply_; + uint32_t max_distance_gate_; + uint32_t min_distance_gate_; + uint16_t system_mode_{CMD_SYSTEM_MODE_ENERGY}; + bool cmd_active_{false}; + char ld2420_firmware_ver_[8]{"v0.0.0"}; + bool presence_{false}; + bool calibration_{false}; + uint16_t distance_{0}; + uint8_t config_checksum_{0}; + std::vector listeners_{}; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/number/__init__.py b/esphome/components/ld2420/number/__init__.py new file mode 100644 index 0000000000..4ae08356fc --- /dev/null +++ b/esphome/components/ld2420/number/__init__.py @@ -0,0 +1,183 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_DISTANCE, + UNIT_SECOND, + ENTITY_CATEGORY_CONFIG, + ICON_MOTION_SENSOR, + ICON_TIMELAPSE, + ICON_SCALE, +) +from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns + +LD2420TimeoutNumber = ld2420_ns.class_("LD2420TimeoutNumber", number.Number) +LD2420MoveSensFactorNumber = ld2420_ns.class_( + "LD2420MoveSensFactorNumber", number.Number +) +LD2420StillSensFactorNumber = ld2420_ns.class_( + "LD2420StillSensFactorNumber", number.Number +) +LD2420MinDistanceNumber = ld2420_ns.class_("LD2420MinDistanceNumber", number.Number) +LD2420MaxDistanceNumber = ld2420_ns.class_("LD2420MaxDistanceNumber", number.Number) +LD2420GateSelectNumber = ld2420_ns.class_("LD2420GateSelectNumber", number.Number) +LD2420MoveThresholdNumbers = ld2420_ns.class_( + "LD2420MoveThresholdNumbers", number.Number +) +LD2420StillThresholdNumbers = ld2420_ns.class_( + "LD2420StillThresholdNumbers", number.Number +) +CONF_MIN_GATE_DISTANCE = "min_gate_distance" +CONF_MAX_GATE_DISTANCE = "max_gate_distance" +CONF_STILL_THRESHOLD = "still_threshold" +CONF_MOVE_THRESHOLD = "move_threshold" +CONF_GATE_MOVE_SENSITIVITY = "gate_move_sensitivity" +CONF_GATE_STILL_SENSITIVITY = "gate_still_sensitivity" +CONF_GATE_SELECT = "gate_select" +CONF_PRESENCE_TIMEOUT = "presence_timeout" +GATE_GROUP = "gate_group" +TIMEOUT_GROUP = "timeout_group" + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Inclusive(CONF_PRESENCE_TIMEOUT, TIMEOUT_GROUP): number.number_schema( + LD2420TimeoutNumber, + unit_of_measurement=UNIT_SECOND, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + ), + cv.Inclusive(CONF_MIN_GATE_DISTANCE, TIMEOUT_GROUP): number.number_schema( + LD2420MinDistanceNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_MAX_GATE_DISTANCE, TIMEOUT_GROUP): number.number_schema( + LD2420MaxDistanceNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_GATE_SELECT, GATE_GROUP): number.number_schema( + LD2420GateSelectNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_STILL_THRESHOLD, GATE_GROUP): number.number_schema( + LD2420StillThresholdNumbers, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_MOVE_THRESHOLD, GATE_GROUP): number.number_schema( + LD2420MoveThresholdNumbers, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_GATE_MOVE_SENSITIVITY): number.number_schema( + LD2420MoveSensFactorNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_GATE_STILL_SENSITIVITY): number.number_schema( + LD2420StillSensFactorNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + } +) +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( + LD2420MoveThresholdNumbers, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Required(CONF_STILL_THRESHOLD): number.number_schema( + LD2420StillThresholdNumbers, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + } + ) + for x in range(16) + } +) + + +async def to_code(config): + LD2420_component = await cg.get_variable(config[CONF_LD2420_ID]) + if gate_timeout_config := config.get(CONF_PRESENCE_TIMEOUT): + n = await number.new_number( + gate_timeout_config, min_value=0, max_value=255, step=5 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_timeout_number(n)) + if min_distance_gate_config := config.get(CONF_MIN_GATE_DISTANCE): + n = await number.new_number( + min_distance_gate_config, min_value=0, max_value=15, step=1 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_min_gate_distance_number(n)) + if max_distance_gate_config := config.get(CONF_MAX_GATE_DISTANCE): + n = await number.new_number( + max_distance_gate_config, min_value=1, max_value=15, step=1 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_max_gate_distance_number(n)) + if gate_move_sensitivity_config := config.get(CONF_GATE_MOVE_SENSITIVITY): + n = await number.new_number( + gate_move_sensitivity_config, min_value=0.05, max_value=1, step=0.025 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_move_sensitivity_factor_number(n)) + if gate_still_sensitivity_config := config.get(CONF_GATE_STILL_SENSITIVITY): + n = await number.new_number( + gate_still_sensitivity_config, min_value=0.05, max_value=1, step=0.025 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_still_sensitivity_factor_number(n)) + if config.get(CONF_GATE_SELECT): + if gate_number := config.get(CONF_GATE_SELECT): + n = await number.new_number(gate_number, min_value=0, max_value=15, step=1) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_select_number(n)) + if gate_still_threshold := config.get(CONF_STILL_THRESHOLD): + n = cg.new_Pvariable(gate_still_threshold[CONF_ID]) + await number.register_number( + n, gate_still_threshold, min_value=0, max_value=65535, step=25 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_still_threshold_numbers(0, n)) + if gate_move_threshold := config.get(CONF_MOVE_THRESHOLD): + n = cg.new_Pvariable(gate_move_threshold[CONF_ID]) + await number.register_number( + n, gate_move_threshold, min_value=0, max_value=65535, step=25 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_move_threshold_numbers(0, n)) + else: + for x in range(16): + if gate_conf := config.get(f"gate_{x}"): + move_config = gate_conf[CONF_MOVE_THRESHOLD] + n = cg.new_Pvariable(move_config[CONF_ID], x) + await number.register_number( + n, move_config, min_value=0, max_value=65535, step=25 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_move_threshold_numbers(x, n)) + + still_config = gate_conf[CONF_STILL_THRESHOLD] + n = cg.new_Pvariable(still_config[CONF_ID], x) + await number.register_number( + n, still_config, min_value=0, max_value=65535, step=25 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_still_threshold_numbers(x, n)) diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp new file mode 100644 index 0000000000..e5eaafb46d --- /dev/null +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -0,0 +1,73 @@ +#include "gate_config_number.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +static const char *const TAG = "LD2420.number"; + +namespace esphome { +namespace ld2420 { + +void LD2420TimeoutNumber::control(float timeout) { + this->publish_state(timeout); + this->parent_->new_config.timeout = timeout; +} + +void LD2420MinDistanceNumber::control(float min_gate) { + if ((uint16_t) min_gate > this->parent_->new_config.max_gate) { + min_gate = this->parent_->get_min_gate_distance_value(); + } else { + this->parent_->new_config.min_gate = (uint16_t) min_gate; + } + this->publish_state(min_gate); +} + +void LD2420MaxDistanceNumber::control(float max_gate) { + if ((uint16_t) max_gate < this->parent_->new_config.min_gate) { + max_gate = this->parent_->get_max_gate_distance_value(); + } else { + this->parent_->new_config.max_gate = (uint16_t) max_gate; + } + this->publish_state(max_gate); +} + +void LD2420GateSelectNumber::control(float gate_select) { + const uint8_t gate = (uint8_t) gate_select; + this->publish_state(gate_select); + this->parent_->publish_gate_move_threshold(gate); + this->parent_->publish_gate_still_threshold(gate); +} + +void LD2420MoveSensFactorNumber::control(float move_factor) { + this->publish_state(move_factor); + this->parent_->gate_move_sensitivity_factor = move_factor; +} + +void LD2420StillSensFactorNumber::control(float still_factor) { + this->publish_state(still_factor); + this->parent_->gate_still_sensitivity_factor = still_factor; +} + +LD2420MoveThresholdNumbers::LD2420MoveThresholdNumbers(uint8_t gate) : gate_(gate) {} + +void LD2420MoveThresholdNumbers::control(float move_threshold) { + this->publish_state(move_threshold); + if (!this->parent_->is_gate_select()) { + this->parent_->new_config.move_thresh[this->gate_] = move_threshold; + } else { + this->parent_->new_config.move_thresh[this->parent_->get_gate_select_value()] = move_threshold; + } +} + +LD2420StillThresholdNumbers::LD2420StillThresholdNumbers(uint8_t gate) : gate_(gate) {} + +void LD2420StillThresholdNumbers::control(float still_threshold) { + this->publish_state(still_threshold); + if (!this->parent_->is_gate_select()) { + this->parent_->new_config.still_thresh[this->gate_] = still_threshold; + } else { + this->parent_->new_config.still_thresh[this->parent_->get_gate_select_value()] = still_threshold; + } +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h new file mode 100644 index 0000000000..459a8026e3 --- /dev/null +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -0,0 +1,78 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2420.h" + +namespace esphome { +namespace ld2420 { + +class LD2420TimeoutNumber : public number::Number, public Parented { + public: + LD2420TimeoutNumber() = default; + + protected: + void control(float timeout) override; +}; + +class LD2420MinDistanceNumber : public number::Number, public Parented { + public: + LD2420MinDistanceNumber() = default; + + protected: + void control(float min_gate) override; +}; + +class LD2420MaxDistanceNumber : public number::Number, public Parented { + public: + LD2420MaxDistanceNumber() = default; + + protected: + void control(float max_gate) override; +}; + +class LD2420GateSelectNumber : public number::Number, public Parented { + public: + LD2420GateSelectNumber() = default; + + protected: + void control(float gate_select) override; +}; + +class LD2420MoveSensFactorNumber : public number::Number, public Parented { + public: + LD2420MoveSensFactorNumber() = default; + + protected: + void control(float move_factor) override; +}; + +class LD2420StillSensFactorNumber : public number::Number, public Parented { + public: + LD2420StillSensFactorNumber() = default; + + protected: + void control(float still_factor) override; +}; + +class LD2420StillThresholdNumbers : public number::Number, public Parented { + public: + LD2420StillThresholdNumbers() = default; + LD2420StillThresholdNumbers(uint8_t gate); + + protected: + uint8_t gate_; + void control(float still_threshold) override; +}; + +class LD2420MoveThresholdNumbers : public number::Number, public Parented { + public: + LD2420MoveThresholdNumbers() = default; + LD2420MoveThresholdNumbers(uint8_t gate); + + protected: + uint8_t gate_; + void control(float move_threshold) override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/select/__init__.py b/esphome/components/ld2420/select/__init__.py new file mode 100644 index 0000000000..554bd4147d --- /dev/null +++ b/esphome/components/ld2420/select/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_CONFIG +from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns + +CONF_OPERATING_MODE = "operating_mode" +CONF_SELECTS = [ + "Normal", + "Calibrate", + "Simple", +] + +LD2420Select = ld2420_ns.class_("LD2420Select", cg.Component) + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Required(CONF_OPERATING_MODE): select.select_schema( + LD2420Select, + entity_category=ENTITY_CATEGORY_CONFIG, + ), +} + + +async def to_code(config): + LD2420_component = await cg.get_variable(config[CONF_LD2420_ID]) + if operating_mode_config := config.get(CONF_OPERATING_MODE): + sel = await select.new_select( + operating_mode_config, + options=[CONF_SELECTS], + ) + await cg.register_parented(sel, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_operating_mode_select(sel)) diff --git a/esphome/components/ld2420/select/operating_mode_select.cpp b/esphome/components/ld2420/select/operating_mode_select.cpp new file mode 100644 index 0000000000..1c59f443a5 --- /dev/null +++ b/esphome/components/ld2420/select/operating_mode_select.cpp @@ -0,0 +1,16 @@ +#include "operating_mode_select.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "LD2420.select"; + +void LD2420Select::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_operating_mode(value); +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h new file mode 100644 index 0000000000..317b2af8c0 --- /dev/null +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "../ld2420.h" +#include "esphome/components/select/select.h" + +namespace esphome { +namespace ld2420 { + +class LD2420Select : public Component, public select::Select, public Parented { + public: + LD2420Select() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py new file mode 100644 index 0000000000..6a67d1fc41 --- /dev/null +++ b/esphome/components/ld2420/sensor/__init__.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, DEVICE_CLASS_DISTANCE, UNIT_CENTIMETER +from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID + +LD2420Sensor = ld2420_ns.class_("LD2420Sensor", sensor.Sensor, cg.Component) + +CONF_MOVING_DISTANCE = "moving_distance" +CONF_GATE_ENERGY = "gate_energy" + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(LD2420Sensor), + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER + ), + } + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if CONF_MOVING_DISTANCE in config: + sens = await sensor.new_sensor(config[CONF_MOVING_DISTANCE]) + cg.add(var.set_distance_sensor(sens)) + if CONF_GATE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_GATE_ENERGY]) + cg.add(var.set_energy_sensor(sens)) + ld2420 = await cg.get_variable(config[CONF_LD2420_ID]) + cg.add(ld2420.register_listener(var)) diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.cpp b/esphome/components/ld2420/sensor/ld2420_sensor.cpp new file mode 100644 index 0000000000..97f0c594b7 --- /dev/null +++ b/esphome/components/ld2420/sensor/ld2420_sensor.cpp @@ -0,0 +1,16 @@ +#include "ld2420_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "LD2420.sensor"; + +void LD2420Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "LD2420 Sensor:"); + LOG_SENSOR(" ", "Distance", this->distance_sensor_); +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h new file mode 100644 index 0000000000..4eebefe0e3 --- /dev/null +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -0,0 +1,34 @@ +#pragma once + +#include "../ld2420.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace ld2420 { + +class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { + public: + void dump_config() override; + void set_distance_sensor(sensor::Sensor *sensor) { this->distance_sensor_ = sensor; } + void on_distance(uint16_t distance) override { + if (this->distance_sensor_ != nullptr) { + if (this->distance_sensor_->get_state() != distance) { + this->distance_sensor_->publish_state(distance); + } + } + } + void on_energy(uint16_t *gate_energy, size_t size) override { + for (size_t active = 0; active < size; active++) { + if (this->energy_sensors_[active] != nullptr) { + this->energy_sensors_[active]->publish_state(gate_energy[active]); + } + } + } + + protected: + sensor::Sensor *distance_sensor_{nullptr}; + std::vector energy_sensors_ = std::vector(LD2420_TOTAL_GATES); +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/text_sensor/__init__.py b/esphome/components/ld2420/text_sensor/__init__.py new file mode 100644 index 0000000000..b6d8c7c0e4 --- /dev/null +++ b/esphome/components/ld2420/text_sensor/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_CHIP, +) + +from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID + +LD2420TextSensor = ld2420_ns.class_( + "LD2420TextSensor", text_sensor.TextSensor, cg.Component +) + +CONF_FW_VERSION = "fw_version" + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(LD2420TextSensor), + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Optional(CONF_FW_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + } + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if CONF_FW_VERSION in config: + sens = await text_sensor.new_text_sensor(config[CONF_FW_VERSION]) + cg.add(var.set_fw_version_text_sensor(sens)) + ld2420 = await cg.get_variable(config[CONF_LD2420_ID]) + cg.add(ld2420.register_listener(var)) diff --git a/esphome/components/ld2420/text_sensor/text_sensor.cpp b/esphome/components/ld2420/text_sensor/text_sensor.cpp new file mode 100644 index 0000000000..1dcdcf7d60 --- /dev/null +++ b/esphome/components/ld2420/text_sensor/text_sensor.cpp @@ -0,0 +1,16 @@ +#include "text_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "LD2420.text_sensor"; + +void LD2420TextSensor::dump_config() { + ESP_LOGCONFIG(TAG, "LD2420 TextSensor:"); + LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/text_sensor/text_sensor.h b/esphome/components/ld2420/text_sensor/text_sensor.h new file mode 100644 index 0000000000..073ddd5d0f --- /dev/null +++ b/esphome/components/ld2420/text_sensor/text_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "../ld2420.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace ld2420 { + +class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::TextSensor { + public: + void dump_config() override; + void set_fw_version_text_sensor(text_sensor::TextSensor *tsensor) { this->fw_version_text_sensor_ = tsensor; }; + void on_fw_version(std::string &fw) override { + if (this->fw_version_text_sensor_ != nullptr) { + this->fw_version_text_sensor_->publish_state(fw); + } + } + + protected: + text_sensor::TextSensor *fw_version_text_sensor_{nullptr}; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index d6241dd5b0..4ced4b8f76 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -8,6 +8,8 @@ #endif #include +#include + #define CLOCK_FREQUENCY 80e6f #ifdef USE_ARDUINO @@ -17,7 +19,11 @@ #define CLOCK_FREQUENCY 40e6f #endif #else +#ifdef SOC_LEDC_SUPPORT_APB_CLOCK #define DEFAULT_CLK LEDC_USE_APB_CLK +#else +#define DEFAULT_CLK LEDC_AUTO_CLK +#endif #endif static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; @@ -48,12 +54,12 @@ float ledc_min_frequency_for_bit_depth(uint8_t bit_depth, bool low_frequency) { } optional ledc_bit_depth_for_frequency(float frequency) { - ESP_LOGD(TAG, "Calculating resolution bit-depth for frequency %f", frequency); + ESP_LOGV(TAG, "Calculating resolution bit-depth for frequency %f", frequency); for (int i = MAX_RES_BITS; i >= 1; i--) { const float min_frequency = ledc_min_frequency_for_bit_depth(i, (frequency < 100)); const float max_frequency = ledc_max_frequency_for_bit_depth(i); if (min_frequency <= frequency && frequency <= max_frequency) { - ESP_LOGD(TAG, "Resolution calculated as %d", i); + ESP_LOGV(TAG, "Resolution calculated as %d", i); return i; } } @@ -92,6 +98,12 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n } #endif +#ifdef USE_ESP_IDF +constexpr int ledc_angle_to_htop(float angle, uint8_t bit_depth) { + return static_cast(angle * ((1U << bit_depth) - 1) / 360.); +} +#endif // USE_ESP_IDF + void LEDCOutput::write_state(float state) { if (!initialized_) { ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!"); @@ -105,16 +117,22 @@ void LEDCOutput::write_state(float state) { const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast(duty_rounded); - + ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); #ifdef USE_ARDUINO - ESP_LOGV(TAG, "Setting duty: %u on channel %u", duty, this->channel_); ledcWrite(this->channel_, duty); #endif #ifdef USE_ESP_IDF auto speed_mode = get_speed_mode(channel_); auto chan_num = static_cast(channel_ % 8); - ledc_set_duty(speed_mode, chan_num, duty); - ledc_update_duty(speed_mode, chan_num); + int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); + if (duty == max_duty) { + ledc_stop(speed_mode, chan_num, 1); + } else if (duty == 0) { + ledc_stop(speed_mode, chan_num, 0); + } else { + ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); + ledc_update_duty(speed_mode, chan_num); + } #endif } @@ -139,8 +157,10 @@ void LEDCOutput::setup() { this->status_set_error(); return; } + 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); ledc_channel_config_t chan_conf{}; chan_conf.gpio_num = pin_->get_pin(); @@ -149,7 +169,7 @@ void LEDCOutput::setup() { chan_conf.intr_type = LEDC_INTR_DISABLE; chan_conf.timer_sel = timer_num; chan_conf.duty = inverted_ == pin_->is_inverted() ? 0 : (1U << bit_depth_); - chan_conf.hpoint = 0; + chan_conf.hpoint = hpoint; ledc_channel_config(&chan_conf); initialized_ = true; this->status_clear_error(); @@ -161,6 +181,7 @@ void LEDCOutput::dump_config() { LOG_PIN(" Pin ", this->pin_); ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_); ESP_LOGCONFIG(TAG, " PWM Frequency: %.1f Hz", this->frequency_); + ESP_LOGCONFIG(TAG, " Phase angle: %.1f°", this->phase_angle_); ESP_LOGCONFIG(TAG, " Bit depth: %u", 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", diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index a78bf440a9..f04543bc5b 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -19,6 +19,7 @@ class LEDCOutput : public output::FloatOutput, public Component { void set_channel(uint8_t channel) { this->channel_ = channel; } void set_frequency(float frequency) { this->frequency_ = frequency; } + void set_phase_angle(float angle) { this->phase_angle_ = angle; } /// Dynamically change frequency at runtime void update_frequency(float frequency) override; @@ -35,6 +36,7 @@ class LEDCOutput : public output::FloatOutput, public Component { InternalGPIOPin *pin_; uint8_t channel_{}; uint8_t bit_depth_{}; + float phase_angle_{0.0f}; float frequency_{}; float duty_{0.0f}; bool initialized_ = false; diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index f6dc89cd9b..32c68f8d24 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -3,6 +3,7 @@ from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( + CONF_PHASE_ANGLE, CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, @@ -46,6 +47,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, 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) + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -58,6 +62,8 @@ async def to_code(config): if CONF_CHANNEL in config: cg.add(var.set_channel(config[CONF_CHANNEL])) cg.add(var.set_frequency(config[CONF_FREQUENCY])) + if CONF_PHASE_ANGLE in config: + cg.add(var.set_phase_angle(config[CONF_PHASE_ANGLE])) @automation.register_action( diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py new file mode 100644 index 0000000000..b29d2e309c --- /dev/null +++ b/esphome/components/libretiny/__init__.py @@ -0,0 +1,340 @@ +import json +import logging +from os.path import dirname, isfile, join + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_BOARD, + CONF_COMPONENT_ID, + CONF_DEBUG, + CONF_FAMILY, + CONF_FRAMEWORK, + CONF_ID, + CONF_NAME, + CONF_OPTIONS, + CONF_PROJECT, + CONF_SOURCE, + CONF_VERSION, + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + __version__, +) +from esphome.core import CORE + +from . import gpio # noqa +from .const import ( + CONF_GPIO_RECOVER, + CONF_LOGLEVEL, + CONF_SDK_SILENT, + CONF_UART_PORT, + FAMILIES, + FAMILY_COMPONENT, + FAMILY_FRIENDLY, + KEY_BOARD, + KEY_COMPONENT, + KEY_COMPONENT_DATA, + KEY_FAMILY, + KEY_LIBRETINY, + LT_DEBUG_MODULES, + LT_LOGLEVELS, + LibreTinyComponent, + LTComponent, +) + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@kuba2k2"] +AUTO_LOAD = ["preferences"] + + +def _detect_variant(value): + if KEY_LIBRETINY not in CORE.data: + raise cv.Invalid("Family component didn't populate core data properly!") + component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] + board = value[CONF_BOARD] + # read board-default family if not specified + if board not in component.boards: + if CONF_FAMILY not in value: + raise cv.Invalid( + "This board is unknown, if you are sure you want to compile with this board selection, " + f"override with option '{CONF_FAMILY}'", + path=[CONF_BOARD], + ) + _LOGGER.warning( + "This board is unknown. Make sure the chosen chip component is correct.", + ) + else: + family = component.boards[board][KEY_FAMILY] + if CONF_FAMILY in value and family != value[CONF_FAMILY]: + raise cv.Invalid( + f"Option '{CONF_FAMILY}' does not match selected board.", + path=[CONF_FAMILY], + ) + value = value.copy() + value[CONF_FAMILY] = family + # read component name matching this family + value[CONF_COMPONENT_ID] = FAMILY_COMPONENT[value[CONF_FAMILY]] + # make sure the chosen component matches the family + if value[CONF_COMPONENT_ID] != component.name: + raise cv.Invalid( + f"The chosen family doesn't belong to '{component.name}' component. The correct component is '{value[CONF_COMPONENT_ID]}'", + path=[CONF_FAMILY], + ) + return value + + +def _update_core_data(config): + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = config[CONF_COMPONENT_ID] + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( + config[CONF_FRAMEWORK][CONF_VERSION] + ) + CORE.data[KEY_LIBRETINY][KEY_BOARD] = config[CONF_BOARD] + CORE.data[KEY_LIBRETINY][KEY_COMPONENT] = config[CONF_COMPONENT_ID] + CORE.data[KEY_LIBRETINY][KEY_FAMILY] = config[CONF_FAMILY] + return config + + +def get_libretiny_component(core_obj=None): + return (core_obj or CORE).data[KEY_LIBRETINY][KEY_COMPONENT] + + +def get_libretiny_family(core_obj=None): + return (core_obj or CORE).data[KEY_LIBRETINY][KEY_FAMILY] + + +def only_on_family(*, supported=None, unsupported=None): + """Config validator for features only available on some LibreTiny families.""" + if supported is not None and not isinstance(supported, list): + supported = [supported] + if unsupported is not None and not isinstance(unsupported, list): + unsupported = [unsupported] + + def validator_(obj): + family = get_libretiny_family() + if supported is not None and family not in supported: + raise cv.Invalid( + f"This feature is only available on {', '.join(supported)}" + ) + if unsupported is not None and family in unsupported: + raise cv.Invalid( + f"This feature is not available on {', '.join(unsupported)}" + ) + return obj + + return validator_ + + +def get_download_types(storage_json=None): + types = [ + { + "title": "UF2 package (recommended)", + "description": "For flashing via web_server OTA or with ltchiptool (UART)", + "file": "firmware.uf2", + "download": f"{storage_json.name}.uf2", + }, + ] + + build_dir = dirname(storage_json.firmware_bin_path) + outputs = join(build_dir, "firmware.json") + if not isfile(outputs): + return types + with open(outputs, encoding="utf-8") as f: + outputs = json.load(f) + for output in outputs: + if not output["public"]: + continue + suffix = output["filename"].partition(".")[2] + suffix = f"-{suffix}" if "." in suffix else f".{suffix}" + types.append( + { + "title": output["title"], + "description": output["description"], + "file": output["filename"], + "download": storage_json.name + suffix, + } + ) + return types + + +def _notify_old_style(config): + if config: + raise cv.Invalid( + "The LibreTiny component is now split between supported chip families.\n" + "Migrate your config file to include a chip-based configuration, " + "instead of the 'libretiny:' block.\n" + "For example 'bk72xx:' or 'rtl87xx:'." + ) + return config + + +# The dev and latest branches will be at *least* this version, which is what matters. +ARDUINO_VERSIONS = { + "dev": (cv.Version(1, 7, 0), "https://github.com/libretiny-eu/libretiny.git"), + "latest": (cv.Version(1, 7, 0), "libretiny"), + "recommended": (cv.Version(1, 7, 0), None), +} + + +def _check_framework_version(value): + value = value.copy() + + if value[CONF_VERSION] in ARDUINO_VERSIONS: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = ARDUINO_VERSIONS[value[CONF_VERSION]] + else: + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) + + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source + + return value + + +def _check_debug_order(value): + debug = value[CONF_DEBUG] + if "NONE" in debug and "NONE" in debug[1:]: + raise cv.Invalid( + "'none' has to be specified before other modules, and only once", + path=[CONF_DEBUG], + ) + return value + + +FRAMEWORK_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_LOGLEVEL, default="warn"): ( + cv.one_of(*LT_LOGLEVELS, upper=True) + ), + cv.Optional(CONF_DEBUG, default=[]): cv.ensure_list( + cv.one_of("NONE", *LT_DEBUG_MODULES, upper=True) + ), + cv.Optional(CONF_SDK_SILENT, default="all"): ( + cv.one_of("all", "auto", "none", lower=True) + ), + cv.Optional(CONF_UART_PORT): cv.one_of(0, 1, 2, int=True), + cv.Optional(CONF_GPIO_RECOVER, default=True): cv.boolean, + cv.Optional(CONF_OPTIONS, default={}): { + cv.string_strict: cv.string, + }, + } + ), + _check_framework_version, + _check_debug_order, +) + +CONFIG_SCHEMA = cv.All(_notify_old_style) + +BASE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LTComponent), + cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_FAMILY): cv.one_of(*FAMILIES, upper=True), + cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, + }, +) + +BASE_SCHEMA.add_extra(_detect_variant) +BASE_SCHEMA.add_extra(_update_core_data) + + +# pylint: disable=use-dict-literal +async def component_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + # setup board config + cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_build_flag("-DUSE_LIBRETINY") + cg.add_build_flag(f"-DUSE_{config[CONF_COMPONENT_ID].upper()}") + 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]]) + + # force using arduino framework + cg.add_platformio_option("framework", "arduino") + cg.add_build_flag("-DUSE_ARDUINO") + + # disable library compatibility checks + cg.add_platformio_option("lib_ldf_mode", "off") + # include in every file + cg.add_platformio_option("build_src_flags", "-include Arduino.h") + # dummy version code + cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)")) + # decrease web server stack size (16k words -> 4k words) + cg.add_build_flag("-DCONFIG_ASYNC_TCP_STACK_SIZE=4096") + + # build framework version + # if platform version is a valid version constraint, prefix the default package + framework = config[CONF_FRAMEWORK] + cv.platformio_version_constraint(framework[CONF_VERSION]) + if framework[CONF_SOURCE]: + cg.add_platformio_option("platform", framework[CONF_SOURCE]) + elif str(framework[CONF_VERSION]) != "0.0.0": + cg.add_platformio_option("platform", f"libretiny @ {framework[CONF_VERSION]}") + else: + cg.add_platformio_option("platform", "libretiny") + + # apply LibreTiny options from framework: block + # setup LT logger to work nicely with ESPHome logger + lt_options = dict( + LT_LOGLEVEL="LT_LEVEL_" + framework[CONF_LOGLEVEL], + LT_LOGGER_CALLER=0, + LT_LOGGER_TASK=0, + LT_LOGGER_COLOR=1, + LT_USE_TIME=1, + ) + # enable/disable per-module debugging + for module in framework[CONF_DEBUG]: + if module == "NONE": + # disable all modules + for module in LT_DEBUG_MODULES: + lt_options[f"LT_DEBUG_{module}"] = 0 + else: + # enable one module + lt_options[f"LT_DEBUG_{module}"] = 1 + # set SDK silencing mode + if framework[CONF_SDK_SILENT] == "all": + lt_options["LT_UART_SILENT_ENABLED"] = 1 + lt_options["LT_UART_SILENT_ALL"] = 1 + elif framework[CONF_SDK_SILENT] == "auto": + lt_options["LT_UART_SILENT_ENABLED"] = 1 + lt_options["LT_UART_SILENT_ALL"] = 0 + else: + lt_options["LT_UART_SILENT_ENABLED"] = 0 + lt_options["LT_UART_SILENT_ALL"] = 0 + # set default UART port + if (uart_port := framework.get(CONF_UART_PORT, None)) is not None: + lt_options["LT_UART_DEFAULT_PORT"] = uart_port + # add custom options + lt_options.update(framework[CONF_OPTIONS]) + + # apply ESPHome options from framework: block + cg.add_define("LT_GPIO_RECOVER", int(framework[CONF_GPIO_RECOVER])) + + # build PlatformIO compiler flags + for name, value in sorted(lt_options.items()): + cg.add_build_flag(f"-D{name}={value}") + + # custom output firmware name and version + if CONF_PROJECT in config: + cg.add_platformio_option( + "custom_fw_name", "esphome." + config[CONF_PROJECT][CONF_NAME] + ) + cg.add_platformio_option( + "custom_fw_version", config[CONF_PROJECT][CONF_VERSION] + ) + else: + cg.add_platformio_option("custom_fw_name", "esphome") + cg.add_platformio_option("custom_fw_version", __version__) + + await cg.register_component(var, config) diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py new file mode 100644 index 0000000000..525d8b7786 --- /dev/null +++ b/esphome/components/libretiny/const.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass +from typing import Callable + +import esphome.codegen as cg + + +@dataclass +class LibreTinyComponent: + name: str + boards: dict[str, dict[str, str]] + board_pins: dict[str, dict[str, int]] + pin_validation: Callable[[int], int] + usage_validation: Callable[[dict], dict] + + +CONF_LIBRETINY = "libretiny" +CONF_LOGLEVEL = "loglevel" +CONF_SDK_SILENT = "sdk_silent" +CONF_GPIO_RECOVER = "gpio_recover" +CONF_UART_PORT = "uart_port" + +LT_LOGLEVELS = [ + "VERBOSE", + "TRACE", + "DEBUG", + "INFO", + "WARN", + "ERROR", + "FATAL", + "NONE", +] + +LT_DEBUG_MODULES = [ + "WIFI", + "CLIENT", + "SERVER", + "SSL", + "OTA", + "FDB", + "MDNS", + "LWIP", + "LWIP_ASSERT", +] + +KEY_LIBRETINY = "libretiny" +KEY_BOARD = "board" +KEY_COMPONENT = "component" +KEY_COMPONENT_DATA = "component_data" +KEY_FAMILY = "family" + +# COMPONENTS - auto-generated! Do not modify this block. +COMPONENT_BK72XX = "bk72xx" +COMPONENT_RTL87XX = "rtl87xx" +# COMPONENTS - end + +# FAMILIES - auto-generated! Do not modify this block. +FAMILY_BK7231N = "BK7231N" +FAMILY_BK7231Q = "BK7231Q" +FAMILY_BK7231T = "BK7231T" +FAMILY_BK7251 = "BK7251" +FAMILY_RTL8710B = "RTL8710B" +FAMILY_RTL8720C = "RTL8720C" +FAMILIES = [ + FAMILY_BK7231N, + FAMILY_BK7231Q, + FAMILY_BK7231T, + FAMILY_BK7251, + FAMILY_RTL8710B, + FAMILY_RTL8720C, +] +FAMILY_FRIENDLY = { + FAMILY_BK7231N: "BK7231N", + FAMILY_BK7231Q: "BK7231Q", + FAMILY_BK7231T: "BK7231T", + FAMILY_BK7251: "BK7251", + FAMILY_RTL8710B: "RTL8710B", + FAMILY_RTL8720C: "RTL8720C", +} +FAMILY_COMPONENT = { + FAMILY_BK7231N: COMPONENT_BK72XX, + FAMILY_BK7231Q: COMPONENT_BK72XX, + FAMILY_BK7231T: COMPONENT_BK72XX, + FAMILY_BK7251: COMPONENT_BK72XX, + FAMILY_RTL8710B: COMPONENT_RTL87XX, + FAMILY_RTL8720C: COMPONENT_RTL87XX, +} +# FAMILIES - end + +libretiny_ns = cg.esphome_ns.namespace("libretiny") +LTComponent = libretiny_ns.class_("LTComponent", cg.PollingComponent) diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp new file mode 100644 index 0000000000..b22740f02a --- /dev/null +++ b/esphome/components/libretiny/core.cpp @@ -0,0 +1,40 @@ +#ifdef USE_LIBRETINY + +#include "core.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "preferences.h" + +void setup(); +void loop(); + +namespace esphome { + +void IRAM_ATTR HOT yield() { ::yield(); } +uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint32_t IRAM_ATTR HOT micros() { return ::micros(); } +void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } + +void arch_init() { + libretiny::setup_preferences(); + lt_wdt_enable(10000L); +#if LT_GPIO_RECOVER + lt_gpio_recover(); +#endif +} + +void arch_restart() { + lt_reboot(); + while (1) { + } +} +void IRAM_ATTR HOT arch_feed_wdt() { lt_wdt_feed(); } +uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } +uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } +uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } + +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/core.h b/esphome/components/libretiny/core.h new file mode 100644 index 0000000000..9458df1f16 --- /dev/null +++ b/esphome/components/libretiny/core.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef USE_LIBRETINY + +#include + +namespace esphome { +namespace libretiny {} // namespace libretiny +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/generate_components.py b/esphome/components/libretiny/generate_components.py new file mode 100644 index 0000000000..ae55fd9e40 --- /dev/null +++ b/esphome/components/libretiny/generate_components.py @@ -0,0 +1,329 @@ +# Copyright (c) Kuba Szczodrzyński 2023-06-01. + +# pylint: skip-file +# flake8: noqa + +import json +import re +from pathlib import Path + +from black import FileMode, format_str +from ltchiptool import Board, Family +from ltchiptool.util.lvm import LVM + +BASE_CODE_INIT = """ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. +# For custom pin validators, put validate_pin() or validate_usage() +# in gpio.py file in this directory. +# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA +# in schema.py file in this directory. + +from esphome import pins +from esphome.components import libretiny +from esphome.components.libretiny.const import ( + COMPONENT_{COMPONENT}, + KEY_COMPONENT_DATA, + KEY_LIBRETINY, + LibreTinyComponent, +) +from esphome.core import CORE + +{IMPORTS} + +CODEOWNERS = ["@kuba2k2"] +AUTO_LOAD = ["libretiny"] + +COMPONENT_DATA = LibreTinyComponent( + name=COMPONENT_{COMPONENT}, + boards={COMPONENT}_BOARDS, + board_pins={COMPONENT}_BOARD_PINS, + pin_validation={PIN_VALIDATION}, + usage_validation={USAGE_VALIDATION}, +) + + +def _set_core_data(config): + CORE.data[KEY_LIBRETINY] = {} + CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA + return config + + +CONFIG_SCHEMA = {SCHEMA} + +PIN_SCHEMA = {PIN_SCHEMA} + +CONFIG_SCHEMA.prepend_extra(_set_core_data) + + +async def to_code(config): + return await libretiny.component_to_code(config) + + +@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA) +async def pin_to_code(config): + return await libretiny.gpio.component_pin_to_code(config) +""" + +BASE_CODE_BOARDS = """ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. + +from esphome.components.libretiny.const import {FAMILIES} + +{COMPONENT}_BOARDS = {BOARDS_JSON} + +{COMPONENT}_BOARD_PINS = {PINS_JSON} + +BOARDS = {COMPONENT}_BOARDS +""" + +# variable names in component extension code +VAR_SCHEMA = "COMPONENT_SCHEMA" +VAR_PIN_SCHEMA = "COMPONENT_PIN_SCHEMA" +VAR_GPIO_PIN = "validate_pin" +VAR_GPIO_USAGE = "validate_usage" + +# lines for code snippets +SCHEMA_BASE = "libretiny.BASE_SCHEMA" +SCHEMA_EXTRA = f"libretiny.BASE_SCHEMA.extend({VAR_SCHEMA})" +PIN_SCHEMA_BASE = "libretiny.gpio.BASE_PIN_SCHEMA" +PIN_SCHEMA_EXTRA = f"libretiny.BASE_PIN_SCHEMA.extend({VAR_PIN_SCHEMA})" + +# supported root components +COMPONENT_MAP = { + "rtl87xx": "realtek-amb", + "bk72xx": "beken-72xx", +} + + +def subst(code: str, key: str, value: str) -> str: + return code.replace(f"{{{key}}}", value) + + +def subst_all(code: str, value: str) -> str: + return re.sub(r"{.+?}", value, code) + + +def subst_many(code: str, *templates: tuple[str, str]) -> str: + while True: + prev_code = code + for key, value in templates: + code = subst(code, key, value) + if code == prev_code: + break + return code + + +def check_base_code(code: str) -> None: + code = subst_all(code, "DUMMY") + formatted = format_str(code, mode=FileMode()) + if code.strip() != formatted.strip(): + print(formatted) + raise RuntimeError("Base code is not formatted properly") + + +def write_component_code( + component_dir: Path, + component: str, +) -> None: + code = BASE_CODE_INIT + gpio_py = component_dir.joinpath("gpio.py") + schema_py = component_dir.joinpath("schema.py") + init_py = component_dir.joinpath("__init__.py") + + # gather all imports + imports = { + "gpio": set(), + "schema": set(), + "boards": {"{COMPONENT}_BOARDS", "{COMPONENT}_BOARD_PINS"}, + } + # substitution values + values = dict( + COMPONENT=component.upper(), + COMPONENT_LOWER=component.lower(), + SCHEMA=SCHEMA_BASE, + PIN_SCHEMA=PIN_SCHEMA_BASE, + PIN_VALIDATION="None", + USAGE_VALIDATION="None", + ) + + # parse gpio.py file to find custom validators + if gpio_py.is_file(): + gpio_code = gpio_py.read_text() + if VAR_GPIO_PIN in gpio_code: + values["PIN_VALIDATION"] = VAR_GPIO_PIN + imports["gpio"].add(VAR_GPIO_PIN) + + # parse schema.py file to find schema extension + if schema_py.is_file(): + schema_code = schema_py.read_text() + if VAR_SCHEMA in schema_code: + values["SCHEMA"] = SCHEMA_EXTRA + imports["schema"].add(VAR_SCHEMA) + if VAR_PIN_SCHEMA in schema_code: + values["PIN_SCHEMA"] = PIN_SCHEMA_EXTRA + imports["schema"].add(VAR_PIN_SCHEMA) + + # add import lines if needed + import_lines = "\n".join( + f"from .{m} import {', '.join(sorted(v))}" for m, v in imports.items() if v + ) + code = subst_many( + code, + ("IMPORTS", import_lines), + *values.items(), + ) + # format with black + code = format_str(code, mode=FileMode()) + # write back to file + init_py.write_text(code) + + +def write_component_boards( + component_dir: Path, + component: str, + boards: list[Board], +) -> list[Family]: + code = BASE_CODE_BOARDS + variants_dir = Path(LVM.path(), "boards", "variants") + boards_py = component_dir.joinpath("boards.py") + pin_regex = r"#define PIN_(\w+)\s+(\d+)" + pin_number_regex = r"0*(\d+)$" + + # families to import + families = set() + # found root families + root_families = [] + # substitution values + values = dict( + COMPONENT=component.upper(), + ) + # resulting JSON objects + boards_json = {} + pins_json = {} + + # go through all boards found for this root family + for board in boards: + family = "FAMILY_" + board.family.short_name + boards_json[board.name] = { + "name": board.title, + "family": family, + } + families.add(family) + if board.family not in root_families: + root_families.append(board.family) + + board_h = variants_dir.joinpath(f"{board.name}.h") + board_code = board_h.read_text() + board_pins = {} + for match in re.finditer(pin_regex, board_code): + pin_name = match[1] + pin_value = match[2] + board_pins[pin_name] = int(pin_value) + # trim leading zeroes in GPIO numbers + pin_name = re.sub(pin_number_regex, r"\1", pin_name) + board_pins[pin_name] = int(pin_value) + pins_json[board.name] = board_pins + + # make the JSONs format as non-inline + boards_json = json.dumps(boards_json).replace("}", ",}") + pins_json = json.dumps(pins_json).replace("}", ",}") + # remove quotes from family constants + for family in families: + boards_json = boards_json.replace(f'"{family}"', family) + code = subst_many( + code, + ("FAMILIES", ", ".join(sorted(families))), + ("BOARDS_JSON", boards_json), + ("PINS_JSON", pins_json), + *values.items(), + ) + # format with black + code = format_str(code, mode=FileMode()) + # write back to file + boards_py.write_text(code) + return root_families + + +def write_const( + components_dir: Path, + components: set[str], + families: dict[str, str], +) -> None: + const_py = components_dir.joinpath("libretiny").joinpath("const.py") + if not const_py.is_file(): + raise FileNotFoundError(const_py) + code = const_py.read_text() + components = sorted(components) + v2f = families + families = sorted(families) + + # regex for finding the component list block + comp_regex = r"(# COMPONENTS.+?\n)(.*?)(\n# COMPONENTS)" + # build component constants + comp_str = "\n".join(f'COMPONENT_{f} = "{f.lower()}"' for f in components) + # replace the 2nd regex group only + repl = lambda m: m.group(1) + comp_str + m.group(3) + code = re.sub(comp_regex, repl, code, flags=re.DOTALL | re.MULTILINE) + + # regex for finding the family list block + fam_regex = r"(# FAMILIES.+?\n)(.*?)(\n# FAMILIES)" + # build family constants + fam_defs = "\n".join(f'FAMILY_{v} = "{v}"' for v in families) + fam_list = ", ".join(f"FAMILY_{v}" for v in families) + fam_friendly = ", ".join(f'FAMILY_{v}: "{v}"' for v in families) + fam_component = ", ".join(f"FAMILY_{v}: COMPONENT_{v2f[v]}" for v in families) + fam_lines = [ + fam_defs, + "FAMILIES = [", + fam_list, + ",]", + "FAMILY_FRIENDLY = {", + fam_friendly, + ",}", + "FAMILY_COMPONENT = {", + fam_component, + ",}", + ] + var_str = "\n".join(fam_lines) + # replace the 2nd regex group only + repl = lambda m: m.group(1) + var_str + m.group(3) + code = re.sub(fam_regex, repl, code, flags=re.DOTALL | re.MULTILINE) + + # format with black + code = format_str(code, mode=FileMode()) + # write back to file + const_py.write_text(code) + + +if __name__ == "__main__": + # safety check if code is properly formatted + check_base_code(BASE_CODE_INIT) + # list all boards from ltchiptool + components_dir = Path(__file__).parent.parent + boards = [Board(b) for b in Board.get_list()] + # keep track of all supported root- and chip-families + components = set() + families = {} + # loop through supported components + for component, family_name in COMPONENT_MAP.items(): + family = Family.get(name=family_name) + # make family component directory + component_dir = components_dir.joinpath(component) + component_dir.mkdir(exist_ok=True) + # filter boards list + family_boards = [b for b in boards if family in b.family.inheritance] + # write __init__.py + write_component_code(component_dir, component) + # write boards.py + component_families = write_component_boards( + component_dir, component, family_boards + ) + # store current root component name + components.add(component.upper()) + # add all chip families + for family in component_families: + families[family.short_name] = component.upper() + # update libretiny/const.py + write_const(components_dir, components, families) diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py new file mode 100644 index 0000000000..07eb0ce133 --- /dev/null +++ b/esphome/components/libretiny/gpio.py @@ -0,0 +1,204 @@ +import logging + +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ANALOG, + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +from esphome.core import CORE + +from .const import ( + KEY_BOARD, + KEY_COMPONENT_DATA, + KEY_LIBRETINY, + LibreTinyComponent, + libretiny_ns, +) + +_LOGGER = logging.getLogger(__name__) + +ArduinoInternalGPIOPin = libretiny_ns.class_( + "ArduinoInternalGPIOPin", cg.InternalGPIOPin +) + + +def _is_name_deprecated(value): + return value[0] in "DA" and value[1:].isnumeric() + + +def _lookup_board_pins(board): + component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] + board_pins = component.board_pins.get(board, {}) + # Resolve aliased board pins (shorthand when two boards have the same pin configuration) + while isinstance(board_pins, str): + board_pins = board_pins[board_pins] + return board_pins + + +def _lookup_pin(value): + board: str = CORE.data[KEY_LIBRETINY][KEY_BOARD] + board_pins = _lookup_board_pins(board) + + # check numeric pin values + if isinstance(value, int): + if value in board_pins.values() or not board_pins: + # accept if pin number present in board pins + # if board is not found, just accept all numeric values + return value + raise cv.Invalid(f"Pin number '{value}' is not usable for board {board}.") + + # check textual pin names + if isinstance(value, str): + if not board_pins: + # can't remap without known pin name + raise cv.Invalid( + f"Board {board} wasn't found. " + f"Use 'GPIO#' (numeric value) instead of '{value}'." + ) + + if value in board_pins: + # pin name found, remap to numeric value + if _is_name_deprecated(value): + number = board_pins[value] + # find all alternative pin names (except the deprecated) + names = ( + k + for k, v in board_pins.items() + if v == number and not _is_name_deprecated(k) + ) + # sort by shortest + # favor P# or PA# names + names = sorted( + names, + key=lambda x: len(x) - 99 if x[0] == "P" else len(x), + ) + _LOGGER.warning( + "Using D# and A# pin numbering is deprecated. " + "Please replace '%s' with one of: %s", + value, + ", ".join(names), + ) + return board_pins[value] + + # pin name not found and not numeric + raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.") + + # unknown type of the value + raise cv.Invalid(f"Unrecognized pin value '{value}'.") + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + if isinstance(value, int) and not isinstance(value, bool): + return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") + try: + return int(value) + except ValueError: + pass + # translate GPIO* and P* to a number, if possible + # otherwise return unchanged value (i.e. pin PA05) + try: + if value.startswith("GPIO"): + value = int(value[4:]) + elif value.startswith("P"): + value = int(value[1:]) + except ValueError: + pass + return value + + +def validate_gpio_pin(value): + value = _translate_pin(value) + value = _lookup_pin(value) + + component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] + if component.pin_validation: + value = component.pin_validation(value) + + return value + + +def validate_gpio_usage(value): + mode = value[CONF_MODE] + is_analog = mode[CONF_ANALOG] + is_input = mode[CONF_INPUT] + is_output = mode[CONF_OUTPUT] + is_open_drain = mode[CONF_OPEN_DRAIN] + is_pullup = mode[CONF_PULLUP] + is_pulldown = mode[CONF_PULLDOWN] + + if is_open_drain and not is_output: + raise cv.Invalid( + "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] + ) + if is_analog and not is_input: + raise cv.Invalid("Analog pins must be an input", [CONF_MODE, CONF_ANALOG]) + if is_analog: + # expect analog pin numbers to be available as either ADC# or A# + number = value[CONF_NUMBER] + board: str = CORE.data[KEY_LIBRETINY][KEY_BOARD] + board_pins = _lookup_board_pins(board) + analog_pins = [ + v + for k, v in board_pins.items() + if k[0] == "A" and k[1:].isnumeric() or k[0:3] == "ADC" + ] + if number not in analog_pins: + raise cv.Invalid( + f"Pin '{number}' is not an analog pin", [CONF_MODE, CONF_ANALOG] + ) + + # (input, output, open_drain, pullup, pulldown) + supported_modes = { + # INPUT + (True, False, False, False, False), + # OUTPUT + (False, True, False, False, False), + # INPUT_PULLUP + (True, False, False, True, False), + # INPUT_PULLDOWN + (True, False, False, False, True), + # OUTPUT_OPEN_DRAIN + (False, True, True, False, False), + } + key = (is_input, is_output, is_open_drain, is_pullup, is_pulldown) + if key not in supported_modes: + raise cv.Invalid("This pin mode is not supported", [CONF_MODE]) + + component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] + if component.usage_validation: + value = component.usage_validation(value) + + return value + + +BASE_PIN_SCHEMA = pins.gpio_base_schema( + ArduinoInternalGPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), +).add_extra(validate_gpio_usage) + + +async def component_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/libretiny/gpio_arduino.cpp b/esphome/components/libretiny/gpio_arduino.cpp new file mode 100644 index 0000000000..7a1e014ea4 --- /dev/null +++ b/esphome/components/libretiny/gpio_arduino.cpp @@ -0,0 +1,105 @@ +#ifdef USE_LIBRETINY + +#include "gpio_arduino.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace libretiny { + +static const char *const TAG = "lt.gpio"; + +static int IRAM_ATTR flags_to_mode(gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + return INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + return OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + return INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return OUTPUT_OPEN_DRAIN; + } else { + return 0; + } +} + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + PinStatus arduino_mode = (PinStatus) 255; + switch (type) { + case gpio::INTERRUPT_RISING_EDGE: + arduino_mode = inverted_ ? FALLING : RISING; + break; + case gpio::INTERRUPT_FALLING_EDGE: + arduino_mode = inverted_ ? RISING : FALLING; + break; + case gpio::INTERRUPT_ANY_EDGE: + arduino_mode = CHANGE; + break; + case gpio::INTERRUPT_LOW_LEVEL: + arduino_mode = inverted_ ? HIGH : LOW; + break; + case gpio::INTERRUPT_HIGH_LEVEL: + arduino_mode = inverted_ ? LOW : HIGH; + break; + } + + attachInterruptParam(pin_, func, arduino_mode, arg); +} + +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; +} + +bool ArduinoInternalGPIOPin::digital_read() { + return bool(digitalRead(pin_)) ^ inverted_; // NOLINT +} +void ArduinoInternalGPIOPin::digital_write(bool value) { + digitalWrite(pin_, value ^ inverted_); // NOLINT +} +void ArduinoInternalGPIOPin::detach_interrupt() const { + detachInterrupt(pin_); // NOLINT +} + +} // namespace libretiny + +using namespace libretiny; + +bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { + auto *arg = reinterpret_cast(arg_); + return bool(digitalRead(arg->pin)) ^ arg->inverted; // NOLINT +} +void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { + auto *arg = reinterpret_cast(arg_); + digitalWrite(arg->pin, value ^ arg->inverted); // NOLINT +} +void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { + auto *arg = reinterpret_cast(arg_); + detachInterrupt(arg->pin); +} +void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { + auto *arg = reinterpret_cast(arg_); + pinMode(arg->pin, flags_to_mode(flags)); // NOLINT +} + +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h new file mode 100644 index 0000000000..a43ed28c5e --- /dev/null +++ b/esphome/components/libretiny/gpio_arduino.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef USE_LIBRETINY +#include "esphome/core/hal.h" + +namespace esphome { +namespace libretiny { + +class ArduinoInternalGPIOPin : public InternalGPIOPin { + public: + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + void setup() override { pin_mode(flags_); } + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + void detach_interrupt() const override; + ISRInternalGPIOPin to_isr() const override; + uint8_t get_pin() const override { return pin_; } + bool is_inverted() const override { return inverted_; } + + protected: + void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace libretiny +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp new file mode 100644 index 0000000000..ec4b60eaeb --- /dev/null +++ b/esphome/components/libretiny/lt_component.cpp @@ -0,0 +1,29 @@ +#include "lt_component.h" + +#ifdef USE_LIBRETINY + +#include "esphome/core/log.h" + +namespace esphome { +namespace libretiny { + +static const char *const TAG = "lt.component"; + +void LTComponent::dump_config() { + ESP_LOGCONFIG(TAG, "LibreTiny:"); + ESP_LOGCONFIG(TAG, " Version: %s", LT_BANNER_STR + 10); + ESP_LOGCONFIG(TAG, " Loglevel: %u", LT_LOGLEVEL); + +#ifdef USE_TEXT_SENSOR + if (this->version_ != nullptr) { + this->version_->publish_state(LT_BANNER_STR + 10); + } +#endif // USE_TEXT_SENSOR +} + +float LTComponent::get_setup_priority() const { return setup_priority::LATE; } + +} // namespace libretiny +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/lt_component.h b/esphome/components/libretiny/lt_component.h new file mode 100644 index 0000000000..3d4483ab5d --- /dev/null +++ b/esphome/components/libretiny/lt_component.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef USE_LIBRETINY + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif + +namespace esphome { +namespace libretiny { + +class LTComponent : public Component { + public: + float get_setup_priority() const override; + void dump_config() override; + +#ifdef USE_TEXT_SENSOR + void set_version_sensor(text_sensor::TextSensor *version) { version_ = version; } +#endif // USE_TEXT_SENSOR + + protected: +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *version_{nullptr}; +#endif // USE_TEXT_SENSOR +}; + +} // namespace libretiny +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp new file mode 100644 index 0000000000..ceeb30baf5 --- /dev/null +++ b/esphome/components/libretiny/preferences.cpp @@ -0,0 +1,182 @@ +#ifdef USE_LIBRETINY + +#include "esphome/core/preferences.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include +#include +#include + +namespace esphome { +namespace libretiny { + +static const char *const TAG = "lt.preferences"; + +struct NVSData { + std::string key; + std::vector data; +}; + +static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +class LibreTinyPreferenceBackend : public ESPPreferenceBackend { + public: + std::string 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) { + obj.data.assign(data, data + len); + return true; + } + } + NVSData save{}; + save.key = key; + save.data.assign(data, data + len); + s_pending_save.emplace_back(save); + ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), 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.data.size() != len) { + // size mismatch + return false; + } + memcpy(data, obj.data.data(), len); + return true; + } + } + + fdb_blob_make(blob, data, len); + size_t actual_len = fdb_kv_get_blob(db, key.c_str(), blob); + if (actual_len != len) { + ESP_LOGVV(TAG, "NVS length does not match (%u!=%u)", actual_len, len); + return false; + } else { + ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %d", key.c_str(), len); + } + return true; + } +}; + +class LibreTinyPreferences : public ESPPreferences { + public: + struct fdb_kvdb db; + struct fdb_blob blob; + + void open() { + // + fdb_err_t err = fdb_kvdb_init(&db, "esphome", "kvs", NULL, NULL); + if (err != FDB_NO_ERR) { + LT_E("fdb_kvdb_init(...) failed: %d", err); + } else { + LT_I("Preferences initialized"); + } + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { + return 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); + + return ESPPreferenceObject(pref); + } + + bool sync() override { + if (s_pending_save.empty()) + return true; + + ESP_LOGD(TAG, "Saving %d preferences to flash...", s_pending_save.size()); + // 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{}; + + // 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: %d", save.key.c_str(), save.data.size()); + fdb_blob_make(&blob, save.data.data(), save.data.size()); + fdb_err_t err = fdb_kv_set_blob(&db, save.key.c_str(), &blob); + if (err != FDB_NO_ERR) { + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%u) failed: %d", save.key.c_str(), save.data.size(), err); + failed++; + last_err = err; + last_key = save.key; + continue; + } + written++; + } else { + ESP_LOGD(TAG, "FDB data not changed; skipping %s len=%u", save.key.c_str(), save.data.size()); + cached++; + } + s_pending_save.erase(s_pending_save.begin() + i); + } + ESP_LOGD(TAG, "Saving %d preferences to flash: %d cached, %d written, %d failed", cached + written + failed, cached, + written, failed); + if (failed > 0) { + ESP_LOGE(TAG, "Error saving %d preferences to flash. Last error=%d for key=%s", failed, last_err, + last_key.c_str()); + } + + return failed == 0; + } + + bool is_changed(const fdb_kvdb_t db, const NVSData &to_save) { + NVSData stored_data{}; + struct fdb_kv kv; + fdb_kv_t kvp = fdb_kv_get_obj(db, to_save.key.c_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()); + return true; + } + stored_data.data.reserve(kv.value_len); + fdb_blob_make(&blob, stored_data.data.data(), kv.value_len); + size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &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); + return true; + } + return to_save.data != stored_data.data; + } + + bool reset() override { + ESP_LOGD(TAG, "Cleaning up preferences in flash..."); + s_pending_save.clear(); + + fdb_kv_set_default(&db); + fdb_kvdb_deinit(&db); + return true; + } +}; + +void setup_preferences() { + auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory) + prefs->open(); + global_preferences = prefs; +} + +} // namespace libretiny + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/preferences.h b/esphome/components/libretiny/preferences.h new file mode 100644 index 0000000000..8ec3cd31b1 --- /dev/null +++ b/esphome/components/libretiny/preferences.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_LIBRETINY + +namespace esphome { +namespace libretiny { + +void setup_preferences(); + +} // namespace libretiny +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/text_sensor.py b/esphome/components/libretiny/text_sensor.py new file mode 100644 index 0000000000..df10ee7229 --- /dev/null +++ b/esphome/components/libretiny/text_sensor.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_CELLPHONE_ARROW_DOWN, +) + +from .const import CONF_LIBRETINY, LTComponent + +DEPENDENCIES = ["libretiny"] + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LIBRETINY): cv.use_id(LTComponent), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + icon=ICON_CELLPHONE_ARROW_DOWN, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + lt_component = await cg.get_variable(config[CONF_LIBRETINY]) + + if CONF_VERSION in config: + sens = await text_sensor.new_text_sensor(config[CONF_VERSION]) + cg.add(lt_component.set_version_sensor(sens)) diff --git a/esphome/components/libretiny_pwm/__init__.py b/esphome/components/libretiny_pwm/__init__.py new file mode 100644 index 0000000000..4db724f8ad --- /dev/null +++ b/esphome/components/libretiny_pwm/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@kuba2k2"] diff --git a/esphome/components/libretiny_pwm/libretiny_pwm.cpp b/esphome/components/libretiny_pwm/libretiny_pwm.cpp new file mode 100644 index 0000000000..92e4097c0e --- /dev/null +++ b/esphome/components/libretiny_pwm/libretiny_pwm.cpp @@ -0,0 +1,53 @@ +#include "libretiny_pwm.h" +#include "esphome/core/log.h" + +#ifdef USE_LIBRETINY + +namespace esphome { +namespace libretiny_pwm { + +static const char *const TAG = "libretiny.pwm"; + +void LibreTinyPWM::write_state(float state) { + if (!this->initialized_) { + ESP_LOGW(TAG, "LibreTinyPWM output hasn't been initialized yet!"); + return; + } + + if (this->pin_->is_inverted()) + state = 1.0f - state; + + this->duty_ = state; + const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast(duty_rounded); + + analogWrite(this->pin_->get_pin(), duty); // NOLINT +} + +void LibreTinyPWM::setup() { + this->update_frequency(this->frequency_); + this->turn_off(); +} + +void LibreTinyPWM::dump_config() { + ESP_LOGCONFIG(TAG, "PWM Output:"); + LOG_PIN(" Pin ", this->pin_); + ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); +} + +void LibreTinyPWM::update_frequency(float frequency) { + this->frequency_ = frequency; + // force changing the frequency by removing PWM mode + this->pin_->pin_mode(gpio::FLAG_INPUT); + analogWriteResolution(this->bit_depth_); // NOLINT + analogWriteFrequency(frequency); // NOLINT + this->initialized_ = true; + // re-apply duty + this->write_state(this->duty_); +} + +} // namespace libretiny_pwm +} // namespace esphome + +#endif diff --git a/esphome/components/libretiny_pwm/libretiny_pwm.h b/esphome/components/libretiny_pwm/libretiny_pwm.h new file mode 100644 index 0000000000..42ce40ca39 --- /dev/null +++ b/esphome/components/libretiny_pwm/libretiny_pwm.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/automation.h" +#include "esphome/components/output/float_output.h" + +#ifdef USE_LIBRETINY + +namespace esphome { +namespace libretiny_pwm { + +class LibreTinyPWM : public output::FloatOutput, public Component { + public: + explicit LibreTinyPWM(InternalGPIOPin *pin) : pin_(pin) {} + + void set_frequency(float frequency) { this->frequency_ = frequency; } + /// Dynamically change frequency at runtime + void update_frequency(float frequency) override; + + /// Setup LibreTinyPWM. + void setup() override; + void dump_config() override; + /// HARDWARE setup priority + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Override FloatOutput's write_state. + void write_state(float state) override; + + protected: + InternalGPIOPin *pin_; + uint8_t bit_depth_{10}; + float frequency_{}; + float duty_{0.0f}; + bool initialized_ = false; +}; + +template class SetFrequencyAction : public Action { + public: + SetFrequencyAction(LibreTinyPWM *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, frequency); + + void play(Ts... x) { + float freq = this->frequency_.value(x...); + this->parent_->update_frequency(freq); + } + + protected: + LibreTinyPWM *parent_; +}; + +} // namespace libretiny_pwm +} // namespace esphome + +#endif diff --git a/esphome/components/libretiny_pwm/output.py b/esphome/components/libretiny_pwm/output.py new file mode 100644 index 0000000000..e74bc8f129 --- /dev/null +++ b/esphome/components/libretiny_pwm/output.py @@ -0,0 +1,49 @@ +from esphome import pins, automation +from esphome.components import output +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_FREQUENCY, + CONF_ID, + CONF_PIN, +) + +DEPENDENCIES = ["libretiny"] + +libretinypwm_ns = cg.esphome_ns.namespace("libretiny_pwm") +LibreTinyPWM = libretinypwm_ns.class_("LibreTinyPWM", output.FloatOutput, cg.Component) +SetFrequencyAction = libretinypwm_ns.class_("SetFrequencyAction", automation.Action) + +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, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + gpio = await cg.gpio_pin_expression(config[CONF_PIN]) + var = cg.new_Pvariable(config[CONF_ID], gpio) + await cg.register_component(var, config) + await output.register_output(var, config) + cg.add(var.set_frequency(config[CONF_FREQUENCY])) + + +@automation.register_action( + "output.libretiny_pwm.set_frequency", + SetFrequencyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(LibreTinyPWM), + cv.Required(CONF_FREQUENCY): cv.templatable(cv.int_), + } + ), +) +async def libretiny_pwm_set_frequency_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) + cg.add(var.set_frequency(template_)) + return var diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index ba3a26ebe5..feac385b66 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,44 +1,60 @@ -import esphome.codegen as cg -import esphome.config_validation as cv import esphome.automation as auto -from esphome.components import mqtt, power_supply +import esphome.codegen as cg +from esphome.components import mqtt, power_supply, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_BLUE, + CONF_BRIGHTNESS, + CONF_COLD_WHITE, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_COLOR_BRIGHTNESS, CONF_COLOR_CORRECT, + CONF_COLOR_MODE, + CONF_COLOR_TEMPERATURE, CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, CONF_FLASH_TRANSITION_LENGTH, CONF_GAMMA_CORRECT, + CONF_GREEN, CONF_ID, + CONF_INITIAL_STATE, CONF_MQTT_ID, - CONF_POWER_SUPPLY, - CONF_RESTORE_MODE, + CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_ON_STATE, + CONF_POWER_SUPPLY, + CONF_RED, + CONF_RESTORE_MODE, + CONF_STATE, CONF_TRIGGER_ID, - CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE, CONF_WARM_WHITE_COLOR_TEMPERATURE, + CONF_WEB_SERVER, + CONF_WHITE, ) from esphome.core import coroutine_with_priority from esphome.cpp_helpers import setup_entity -from .automation import light_control_to_code # noqa + +from .automation import LIGHT_STATE_SCHEMA from .effects import ( - validate_effects, + ADDRESSABLE_EFFECTS, BINARY_EFFECTS, + EFFECTS_REGISTRY, MONOCHROMATIC_EFFECTS, RGB_EFFECTS, - ADDRESSABLE_EFFECTS, - EFFECTS_REGISTRY, + validate_effects, ) from .types import ( # noqa - LightState, - AddressableLightState, - light_ns, - LightOutput, AddressableLight, - LightTurnOnTrigger, - LightTurnOffTrigger, + AddressableLightState, + ColorMode, + LightOutput, + LightState, + LightStateRTCState, LightStateTrigger, + LightTurnOffTrigger, + LightTurnOnTrigger, + light_ns, ) CODEOWNERS = ["@esphome/core"] @@ -56,29 +72,36 @@ RESTORE_MODES = { "RESTORE_AND_ON": LightRestoreMode.LIGHT_RESTORE_AND_ON, } -LIGHT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.GenerateID(): cv.declare_id(LightState), - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTJSONLightComponent), - cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( - RESTORE_MODES, upper=True, space="_" - ), - cv.Optional(CONF_ON_TURN_ON): auto.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): auto.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOffTrigger), - } - ), - cv.Optional(CONF_ON_STATE): auto.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightStateTrigger), - } - ), - } +LIGHT_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(LightState), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( + mqtt.MQTTJSONLightComponent + ), + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), + cv.Optional(CONF_ON_TURN_ON): auto.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOnTrigger), + } + ), + cv.Optional(CONF_ON_TURN_OFF): auto.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOffTrigger), + } + ), + cv.Optional(CONF_ON_STATE): auto.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightStateTrigger), + } + ), + cv.Optional(CONF_INITIAL_STATE): LIGHT_STATE_SCHEMA, + } + ) ) BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( @@ -137,18 +160,32 @@ async def setup_light_core_(light_var, output_var, config): cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) - if CONF_DEFAULT_TRANSITION_LENGTH in config: - cg.add( - light_var.set_default_transition_length( - config[CONF_DEFAULT_TRANSITION_LENGTH] - ) + if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None: + initial_state = LightStateRTCState( + initial_state_config.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), + initial_state_config.get(CONF_STATE, False), + initial_state_config.get(CONF_BRIGHTNESS, 1.0), + initial_state_config.get(CONF_COLOR_BRIGHTNESS, 1.0), + initial_state_config.get(CONF_RED, 1.0), + initial_state_config.get(CONF_GREEN, 1.0), + initial_state_config.get(CONF_BLUE, 1.0), + initial_state_config.get(CONF_WHITE, 1.0), + initial_state_config.get(CONF_COLOR_TEMPERATURE, 1.0), + initial_state_config.get(CONF_COLD_WHITE, 1.0), + initial_state_config.get(CONF_WARM_WHITE, 1.0), ) - if CONF_FLASH_TRANSITION_LENGTH in config: - cg.add( - light_var.set_flash_transition_length(config[CONF_FLASH_TRANSITION_LENGTH]) - ) - if CONF_GAMMA_CORRECT in config: - cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT])) + cg.add(light_var.set_initial_state(initial_state)) + + if ( + default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH) + ) is not None: + cg.add(light_var.set_default_transition_length(default_transition_length)) + if ( + flash_transition_length := config.get(CONF_FLASH_TRANSITION_LENGTH) + ) is not None: + cg.add(light_var.set_flash_transition_length(flash_transition_length)) + if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None: + cg.add(light_var.set_gamma_correct(gamma_correct)) effects = await cg.build_registry_list( EFFECTS_REGISTRY, config.get(CONF_EFFECTS, []) ) @@ -164,17 +201,20 @@ async def setup_light_core_(light_var, output_var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) await auto.build_automation(trigger, [], conf) - if CONF_COLOR_CORRECT in config: - cg.add(output_var.set_correction(*config[CONF_COLOR_CORRECT])) + if (color_correct := config.get(CONF_COLOR_CORRECT)) is not None: + cg.add(output_var.set_correction(*color_correct)) - if CONF_POWER_SUPPLY in config: - var_ = await cg.get_variable(config[CONF_POWER_SUPPLY]) + if (power_supply_id := config.get(CONF_POWER_SUPPLY)) is not None: + var_ = await cg.get_variable(power_supply_id) cg.add(output_var.set_power_supply(var_)) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], light_var) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, light_var) await mqtt.register_mqtt_component(mqtt_, config) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(light_var, web_server_config) + async def register_light(output_var, config): light_var = cg.new_Pvariable(config[CONF_ID], output_var) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 0482cf53b9..d622ec0375 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -57,7 +57,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); - if (now - this->last_run_ >= this->update_interval_) { + if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) { this->last_run_ = now; this->f_(it, current_color, this->initial_run_); this->initial_run_ = false; @@ -100,6 +100,7 @@ struct AddressableColorWipeEffectColor { uint8_t r, g, b, w; bool random; size_t num_leds; + bool gradient; }; class AddressableColorWipeEffect : public AddressableLightEffect { @@ -113,16 +114,25 @@ class AddressableColorWipeEffect : public AddressableLightEffect { if (now - this->last_add_ < this->add_led_interval_) return; this->last_add_ = now; - if (this->reverse_) + if (this->reverse_) { it.shift_left(1); - else + } else { it.shift_right(1); - const AddressableColorWipeEffectColor color = this->colors_[this->at_color_]; - const Color esp_color = Color(color.r, color.g, color.b, color.w); - if (this->reverse_) + } + const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_]; + Color esp_color = Color(color.r, color.g, color.b, color.w); + if (color.gradient) { + size_t next_color_index = (this->at_color_ + 1) % this->colors_.size(); + const AddressableColorWipeEffectColor &next_color = this->colors_[next_color_index]; + const Color next_esp_color = Color(next_color.r, next_color.g, next_color.b, next_color.w); + uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds); + esp_color = esp_color.gradient(next_esp_color, gradient); + } + if (this->reverse_) { it[-1] = esp_color; - else + } else { it[0] = esp_color; + } if (++this->leds_added_ >= color.num_leds) { this->leds_added_ = 0; this->at_color_ = (this->at_color_ + 1) % this->colors_.size(); @@ -199,10 +209,11 @@ class AddressableTwinkleEffect : public AddressableLightEffect { const uint8_t sine = half_sin8(view.get_effect_data()); view = current_color * sine; const uint8_t new_pos = view.get_effect_data() + pos_add; - if (new_pos < view.get_effect_data()) + if (new_pos < view.get_effect_data()) { view.set_effect_data(0); - else + } else { view.set_effect_data(new_pos); + } } else { view = Color::BLACK; } @@ -246,10 +257,11 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { view = Color(((color >> 2) & 1) * sine, ((color >> 1) & 1) * sine, ((color >> 0) & 1) * sine); } const uint8_t new_x = x + pos_add; - if (new_x > 0b11111) + if (new_x > 0b11111) { view.set_effect_data(0); - else + } else { view.set_effect_data((new_x << 3) | color); + } } else { view = Color(0, 0, 0, 0); } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index b63fc93dc5..6e055741da 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -7,6 +7,8 @@ namespace esphome { namespace light { +enum class LimitMode { CLAMP, DO_NOTHING }; + template class ToggleAction : public Action { public: explicit ToggleAction(LightState *state) : state_(state) {} @@ -77,7 +79,10 @@ template class DimRelativeAction : public Action { float rel = this->relative_brightness_.value(x...); float cur; this->parent_->remote_values.as_brightness(&cur); - float new_brightness = clamp(cur + rel, 0.0f, 1.0f); + if ((limit_mode_ == LimitMode::DO_NOTHING) && ((cur < min_brightness_) || (cur > max_brightness_))) { + return; + } + float new_brightness = clamp(cur + rel, min_brightness_, max_brightness_); call.set_state(new_brightness != 0.0f); call.set_brightness(new_brightness); @@ -85,8 +90,18 @@ template class DimRelativeAction : public Action { call.perform(); } + void set_min_max_brightness(float min, float max) { + this->min_brightness_ = min; + this->max_brightness_ = max; + } + + void set_limit_mode(LimitMode limit_mode) { this->limit_mode_ = limit_mode; } + protected: LightState *parent_; + float min_brightness_{0.0}; + float max_brightness_{1.0}; + LimitMode limit_mode_{LimitMode::CLAMP}; }; template class LightIsOnCondition : public Condition { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index cfba273565..e5aa8fa0e9 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,36 +1,42 @@ +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation from esphome.const import ( - CONF_ID, - CONF_COLOR_MODE, - CONF_TRANSITION_LENGTH, - CONF_STATE, - CONF_FLASH_LENGTH, - CONF_EFFECT, - CONF_BRIGHTNESS, - CONF_COLOR_BRIGHTNESS, - CONF_RED, - CONF_GREEN, CONF_BLUE, - CONF_WHITE, - CONF_COLOR_TEMPERATURE, + CONF_BRIGHTNESS, + CONF_BRIGHTNESS_LIMITS, CONF_COLD_WHITE, - CONF_WARM_WHITE, + CONF_COLOR_BRIGHTNESS, + CONF_COLOR_MODE, + CONF_COLOR_TEMPERATURE, + CONF_EFFECT, + CONF_FLASH_LENGTH, + CONF_GREEN, + CONF_ID, + CONF_LIMIT_MODE, + CONF_MAX_BRIGHTNESS, + CONF_MIN_BRIGHTNESS, CONF_RANGE_FROM, CONF_RANGE_TO, + CONF_RED, + CONF_STATE, + CONF_TRANSITION_LENGTH, + CONF_WARM_WHITE, + CONF_WHITE, ) + from .types import ( - ColorMode, COLOR_MODES, - DimRelativeAction, - ToggleAction, - LightState, - LightControlAction, + LIMIT_MODES, AddressableLightState, AddressableSet, - LightIsOnCondition, + ColorMode, + DimRelativeAction, + LightControlAction, LightIsOffCondition, + LightIsOnCondition, + LightState, + ToggleAction, ) @@ -57,18 +63,10 @@ async def light_toggle_to_code(config, action_id, template_arg, args): return var -LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( +LIGHT_STATE_SCHEMA = cv.Schema( { - cv.Required(CONF_ID): cv.use_id(LightState), cv.Optional(CONF_COLOR_MODE): cv.enum(COLOR_MODES, upper=True, space="_"), cv.Optional(CONF_STATE): cv.templatable(cv.boolean), - cv.Exclusive(CONF_TRANSITION_LENGTH, "transformer"): cv.templatable( - cv.positive_time_period_milliseconds - ), - cv.Exclusive(CONF_FLASH_LENGTH, "transformer"): cv.templatable( - cv.positive_time_period_milliseconds - ), - cv.Exclusive(CONF_EFFECT, "transformer"): cv.templatable(cv.string), cv.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage), cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage), cv.Optional(CONF_RED): cv.templatable(cv.percentage), @@ -80,6 +78,20 @@ LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_WARM_WHITE): cv.templatable(cv.percentage), } ) + +LIGHT_CONTROL_ACTION_SCHEMA = LIGHT_STATE_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.use_id(LightState), + cv.Exclusive(CONF_TRANSITION_LENGTH, "transformer"): cv.templatable( + cv.positive_time_period_milliseconds + ), + cv.Exclusive(CONF_FLASH_LENGTH, "transformer"): cv.templatable( + cv.positive_time_period_milliseconds + ), + cv.Exclusive(CONF_EFFECT, "transformer"): cv.templatable(cv.string), + } +) + LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(LightState), @@ -167,6 +179,15 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable( cv.positive_time_period_milliseconds ), + cv.Optional(CONF_BRIGHTNESS_LIMITS): cv.Schema( + { + cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage, + cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage, + cv.Optional(CONF_LIMIT_MODE, default="CLAMP"): cv.enum( + LIMIT_MODES, upper=True, space="_" + ), + } + ), } ) @@ -182,6 +203,13 @@ async def light_dim_relative_to_code(config, action_id, template_arg, args): if CONF_TRANSITION_LENGTH in config: templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) cg.add(var.set_transition_length(templ)) + if conf := config.get(CONF_BRIGHTNESS_LIMITS): + cg.add( + var.set_min_max_brightness( + conf[CONF_MIN_BRIGHTNESS], conf[CONF_MAX_BRIGHTNESS] + ) + ) + cg.add(var.set_limit_mode(conf[CONF_LIMIT_MODE])) return var diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 9211bba7c9..9e02e889c9 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -3,8 +3,8 @@ #include #include -#include "light_effect.h" #include "esphome/core/automation.h" +#include "light_effect.h" namespace esphome { namespace light { @@ -25,10 +25,10 @@ class PulseLightEffect : public LightEffect { return; } auto call = this->state_->turn_on(); - float out = this->on_ ? this->max_brightness : this->min_brightness; + float out = this->on_ ? this->max_brightness_ : this->min_brightness_; call.set_brightness_if_supported(out); + call.set_transition_length_if_supported(this->on_ ? this->transition_on_length_ : this->transition_off_length_); this->on_ = !this->on_; - call.set_transition_length_if_supported(this->transition_length_); // don't tell HA every change call.set_publish(false); call.set_save(false); @@ -37,22 +37,24 @@ class PulseLightEffect : public LightEffect { this->last_color_change_ = now; } - void set_transition_length(uint32_t transition_length) { this->transition_length_ = transition_length; } + void set_transition_on_length(uint32_t transition_length) { this->transition_on_length_ = transition_length; } + void set_transition_off_length(uint32_t transition_length) { this->transition_off_length_ = transition_length; } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_min_max_brightness(float min, float max) { - this->min_brightness = min; - this->max_brightness = max; + this->min_brightness_ = min; + this->max_brightness_ = max; } protected: bool on_ = false; uint32_t last_color_change_{0}; - uint32_t transition_length_{}; + uint32_t transition_on_length_{}; + uint32_t transition_off_length_{}; uint32_t update_interval_{}; - float min_brightness{0.0}; - float max_brightness{1.0}; + float min_brightness_{0.0}; + float max_brightness_{1.0}; }; /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. @@ -116,7 +118,7 @@ class LambdaLightEffect : public LightEffect { void start() override { this->initial_run_ = true; } void apply() override { const uint32_t now = millis(); - if (now - this->last_run_ >= this->update_interval_) { + if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) { this->last_run_ = now; this->f_(this->initial_run_); this->initial_run_ = false; @@ -148,6 +150,7 @@ class AutomationLightEffect : public LightEffect { struct StrobeLightEffectColor { LightColorValues color; uint32_t duration; + uint32_t transition_length; }; class StrobeLightEffect : public LightEffect { @@ -172,7 +175,7 @@ class StrobeLightEffect : public LightEffect { } call.set_publish(false); call.set_save(false); - call.set_transition_length_if_supported(0); + call.set_transition_length_if_supported(this->colors_[this->at_color_].transition_length); call.perform(); this->last_switch_ = now; } diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index c694d6f50c..67c318eb8e 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -1,63 +1,64 @@ -from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation - from esphome.const import ( - CONF_NAME, - CONF_LAMBDA, - CONF_UPDATE_INTERVAL, - CONF_TRANSITION_LENGTH, - CONF_COLORS, - CONF_STATE, - CONF_DURATION, - CONF_BRIGHTNESS, - CONF_COLOR_MODE, - CONF_COLOR_BRIGHTNESS, - CONF_RED, - CONF_GREEN, - CONF_BLUE, - CONF_WHITE, - CONF_COLOR_TEMPERATURE, - CONF_COLD_WHITE, - CONF_WARM_WHITE, CONF_ALPHA, + CONF_BLUE, + CONF_BRIGHTNESS, + CONF_COLD_WHITE, + CONF_COLOR_BRIGHTNESS, + CONF_COLOR_MODE, + CONF_COLOR_TEMPERATURE, + CONF_COLORS, + CONF_DURATION, + CONF_GREEN, CONF_INTENSITY, - CONF_SPEED, - CONF_WIDTH, - CONF_NUM_LEDS, - CONF_RANDOM, - CONF_SEQUENCE, + CONF_LAMBDA, CONF_MAX_BRIGHTNESS, CONF_MIN_BRIGHTNESS, + CONF_NAME, + CONF_NUM_LEDS, + CONF_RANDOM, + CONF_RED, + CONF_SEQUENCE, + CONF_SPEED, + CONF_STATE, + CONF_TRANSITION_LENGTH, + CONF_UPDATE_INTERVAL, + CONF_WARM_WHITE, + CONF_WHITE, + CONF_WIDTH, ) +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry + from .types import ( - ColorMode, COLOR_MODES, + AddressableColorWipeEffect, + AddressableColorWipeEffectColor, + AddressableFireworksEffect, + AddressableFlickerEffect, + AddressableLambdaLightEffect, + AddressableLightRef, + AddressableRainbowLightEffect, + AddressableRandomTwinkleEffect, + AddressableScanEffect, + AddressableTwinkleEffect, + AutomationLightEffect, + Color, + ColorMode, + FlickerLightEffect, LambdaLightEffect, + LightColorValues, PulseLightEffect, RandomLightEffect, StrobeLightEffect, StrobeLightEffectColor, - LightColorValues, - AddressableLightRef, - AddressableLambdaLightEffect, - FlickerLightEffect, - AddressableRainbowLightEffect, - AddressableColorWipeEffect, - AddressableColorWipeEffectColor, - AddressableScanEffect, - AddressableTwinkleEffect, - AddressableRandomTwinkleEffect, - AddressableFireworksEffect, - AddressableFlickerEffect, - AutomationLightEffect, - Color, ) CONF_ADD_LED_INTERVAL = "add_led_interval" CONF_REVERSE = "reverse" +CONF_GRADIENT = "gradient" CONF_MOVE_INTERVAL = "move_interval" CONF_SCAN_WIDTH = "scan_width" CONF_TWINKLE_PROBABILITY = "twinkle_probability" @@ -76,6 +77,8 @@ CONF_ADDRESSABLE_RANDOM_TWINKLE = "addressable_random_twinkle" CONF_ADDRESSABLE_FIREWORKS = "addressable_fireworks" CONF_ADDRESSABLE_FLICKER = "addressable_flicker" CONF_AUTOMATION = "automation" +CONF_ON_LENGTH = "on_length" +CONF_OFF_LENGTH = "off_length" BINARY_EFFECTS = [] MONOCHROMATIC_EFFECTS = [] @@ -170,9 +173,15 @@ async def automation_effect_to_code(config, effect_id): PulseLightEffect, "Pulse", { - cv.Optional( - CONF_TRANSITION_LENGTH, default="1s" - ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_TRANSITION_LENGTH, default="1s"): cv.Any( + cv.positive_time_period_milliseconds, + cv.Schema( + { + cv.Required(CONF_ON_LENGTH): cv.positive_time_period_milliseconds, + cv.Required(CONF_OFF_LENGTH): cv.positive_time_period_milliseconds, + } + ), + ), cv.Optional( CONF_UPDATE_INTERVAL, default="1s" ): cv.positive_time_period_milliseconds, @@ -182,7 +191,21 @@ async def automation_effect_to_code(config, effect_id): ) async def pulse_effect_to_code(config, effect_id): effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH])) + if isinstance(config[CONF_TRANSITION_LENGTH], dict): + cg.add( + effect.set_transition_on_length( + config[CONF_TRANSITION_LENGTH][CONF_ON_LENGTH] + ) + ) + cg.add( + effect.set_transition_off_length( + config[CONF_TRANSITION_LENGTH][CONF_OFF_LENGTH] + ) + ) + else: + transition_length = config[CONF_TRANSITION_LENGTH] + cg.add(effect.set_transition_on_length(transition_length)) + cg.add(effect.set_transition_off_length(transition_length)) cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL])) cg.add( effect.set_min_max_brightness( @@ -243,6 +266,9 @@ async def random_effect_to_code(config, effect_id): cv.Required( CONF_DURATION ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TRANSITION_LENGTH, default="0s" + ): cv.positive_time_period_milliseconds, } ), cv.has_at_least_one_key( @@ -287,6 +313,7 @@ async def strobe_effect_to_code(config, effect_id): ), ), ("duration", color[CONF_DURATION]), + ("transition_length", color[CONF_TRANSITION_LENGTH]), ) ) cg.add(var.set_colors(colors)) @@ -364,6 +391,7 @@ async def addressable_rainbow_effect_to_code(config, effect_id): cv.Optional(CONF_WHITE, default=1.0): cv.percentage, cv.Optional(CONF_RANDOM, default=False): cv.boolean, cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)), + cv.Optional(CONF_GRADIENT, default=False): cv.boolean, } ), cv.Optional( @@ -387,6 +415,7 @@ async def addressable_color_wipe_effect_to_code(config, effect_id): ("w", int(round(color[CONF_WHITE] * 255))), ("random", color[CONF_RANDOM]), ("num_leds", color[CONF_NUM_LEDS]), + ("gradient", color[CONF_GRADIENT]), ) ) cg.add(var.set_colors(colors)) diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 8788246cfc..979a1acb07 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -11,59 +11,59 @@ class ESPColorCorrection { void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; } void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } void calculate_gamma_table(float gamma); - inline Color color_correct(Color color) const ALWAYS_INLINE { + inline Color color_correct(Color color) const ESPHOME_ALWAYS_INLINE { // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma return Color(this->color_correct_red(color.red), this->color_correct_green(color.green), this->color_correct_blue(color.blue), this->color_correct_white(color.white)); } - inline uint8_t color_correct_red(uint8_t red) const ALWAYS_INLINE { + inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_); return this->gamma_table_[res]; } - inline uint8_t color_correct_green(uint8_t green) const ALWAYS_INLINE { + inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_); return this->gamma_table_[res]; } - inline uint8_t color_correct_blue(uint8_t blue) const ALWAYS_INLINE { + inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_); return this->gamma_table_[res]; } - inline uint8_t color_correct_white(uint8_t white) const ALWAYS_INLINE { + inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_); return this->gamma_table_[res]; } - inline Color color_uncorrect(Color color) const ALWAYS_INLINE { + inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green), this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white)); } - inline uint8_t color_uncorrect_red(uint8_t red) const ALWAYS_INLINE { + inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } - inline uint8_t color_uncorrect_green(uint8_t green) const ALWAYS_INLINE { + inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } - inline uint8_t color_uncorrect_blue(uint8_t blue) const ALWAYS_INLINE { + inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } - inline uint8_t color_uncorrect_white(uint8_t white) const ALWAYS_INLINE { + inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } protected: diff --git a/esphome/components/light/esp_hsv_color.h b/esphome/components/light/esp_hsv_color.h index e0aa388875..39f5e55707 100644 --- a/esphome/components/light/esp_hsv_color.h +++ b/esphome/components/light/esp_hsv_color.h @@ -24,11 +24,11 @@ struct ESPHSVColor { }; uint8_t raw[3]; }; - inline ESPHSVColor() ALWAYS_INLINE : h(0), s(0), v(0) { // NOLINT + inline ESPHSVColor() ESPHOME_ALWAYS_INLINE : h(0), s(0), v(0) { // NOLINT } - inline ESPHSVColor(uint8_t hue, uint8_t saturation, uint8_t value) ALWAYS_INLINE : hue(hue), - saturation(saturation), - value(value) {} + inline ESPHSVColor(uint8_t hue, uint8_t saturation, uint8_t value) ESPHOME_ALWAYS_INLINE : hue(hue), + saturation(saturation), + value(value) {} Color to_rgb() const; }; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index fb4e45b3b6..c2600d05c2 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -1,3 +1,4 @@ +#include #include "light_call.h" #include "light_state.h" #include "esphome/core/log.h" @@ -283,7 +284,7 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' - Invalid effect index %u!", name, *this->effect_); + ESP_LOGW(TAG, "'%s' - Invalid effect index %" PRIu32 "!", name, *this->effect_); this->effect_.reset(); } @@ -336,9 +337,12 @@ LightColorValues LightCall::validate_() { void LightCall::transform_parameters_() { auto traits = this->parent_->get_traits(); - // Allow CWWW modes to be set with a white value and/or color temperature. This is used by HA, - // which doesn't support CWWW modes (yet?), and for compatibility with the pre-colormode model, - // as CWWW and RGBWW lights used to represent their values as white + color temperature. + // Allow CWWW modes to be set with a white value and/or color temperature. + // This is used in three cases in HA: + // - CW/WW lights, which set the "brightness" and "color_temperature" + // - RGBWW lights with color_interlock=true, which also sets "brightness" and + // "color_temperature" (without color_interlock, CW/WW are set directly) + // - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature" if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) && // (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // !(*this->color_mode_ & ColorCapability::WHITE) && // @@ -346,21 +350,17 @@ void LightCall::transform_parameters_() { traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { ESP_LOGD(TAG, "'%s' - Setting cold/warm white channels using white/color temperature values.", this->parent_->get_name().c_str()); - auto current_values = this->parent_->remote_values; if (this->color_temperature_.has_value()) { - const float white = - this->white_.value_or(fmaxf(current_values.get_cold_white(), current_values.get_warm_white())); const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); const float ww_fraction = (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); const float cw_fraction = 1.0f - ww_fraction; const float max_cw_ww = std::max(ww_fraction, cw_fraction); - this->cold_white_ = white * gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); - this->warm_white_ = white * gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); - } else { - const float max_cw_ww = std::max(current_values.get_warm_white(), current_values.get_cold_white()); - this->cold_white_ = *this->white_ * current_values.get_cold_white() / max_cw_ww; - this->warm_white_ = *this->white_ * current_values.get_warm_white() / max_cw_ww; + this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + } + if (this->white_.has_value()) { + this->brightness_ = *this->white_; } } } diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 10a3c2f335..bad180ce6d 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -266,6 +266,21 @@ class LightColorValues { /// Set the color temperature property of these light color values in mired. void set_color_temperature(float color_temperature) { this->color_temperature_ = color_temperature; } + /// Get the color temperature property of these light color values in kelvin. + float get_color_temperature_kelvin() const { + if (this->color_temperature_ <= 0) { + return this->color_temperature_; + } + return 1000000.0 / this->color_temperature_; + } + /// Set the color temperature property of these light color values in kelvin. + void set_color_temperature_kelvin(float color_temperature) { + if (color_temperature <= 0) { + return; + } + this->color_temperature_ = 1000000.0 / color_temperature; + } + /// Get the cold white property of these light color values. In range 0.0 to 1.0. float get_cold_white() const { return this->cold_white_; } /// Set the cold white property of these light color values. In range 0.0 to 1.0. diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 50ebd8882b..16b78a17bd 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,6 +1,7 @@ #include "esphome/core/log.h" -#include "light_state.h" + #include "light_output.h" +#include "light_state.h" #include "transformers.h" namespace esphome { @@ -16,21 +17,6 @@ LightCall LightState::turn_off() { return this->make_call().set_state(false); } LightCall LightState::toggle() { return this->make_call().set_state(!this->remote_values.is_on()); } LightCall LightState::make_call() { return LightCall(this); } -struct LightStateRTCState { - ColorMode color_mode{ColorMode::UNKNOWN}; - bool state{false}; - float brightness{1.0f}; - float color_brightness{1.0f}; - float red{1.0f}; - float green{1.0f}; - float blue{1.0f}; - float white{1.0f}; - float color_temp{1.0f}; - float cold_white{1.0f}; - float warm_white{1.0f}; - uint32_t effect{0}; -}; - void LightState::setup() { ESP_LOGCONFIG(TAG, "Setting up light '%s'...", this->get_name().c_str()); @@ -48,6 +34,9 @@ void LightState::setup() { auto call = this->make_call(); LightStateRTCState recovered{}; + if (this->initial_state_.has_value()) { + recovered = *this->initial_state_; + } switch (this->restore_mode_) { case LIGHT_RESTORE_DEFAULT_OFF: case LIGHT_RESTORE_DEFAULT_ON: @@ -120,6 +109,7 @@ void LightState::loop() { // Apply transformer (if any) if (this->transformer_ != nullptr) { auto values = this->transformer_->apply(); + this->is_transformer_active_ = true; if (values.has_value()) { this->current_values = *values; this->output_->update_state(this); @@ -131,6 +121,7 @@ void LightState::loop() { this->current_values = this->transformer_->get_target_values(); this->transformer_->stop(); + this->is_transformer_active_ = false; this->transformer_ = nullptr; this->target_state_reached_callback_.call(); } @@ -173,6 +164,7 @@ void LightState::set_flash_transition_length(uint32_t flash_transition_length) { uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } +void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } bool LightState::supports_effects() { return !this->effects_.empty(); } const std::vector &LightState::get_effects() const { return this->effects_; } void LightState::add_effects(const std::vector &effects) { @@ -214,6 +206,8 @@ void LightState::current_values_as_ct(float *color_temperature, float *white_bri this->gamma_correct_); } +bool LightState::is_transformer_active() { return this->is_transformer_active_; } + void LightState::start_effect_(uint32_t effect_index) { this->stop_effect_(); if (effect_index == 0) @@ -263,6 +257,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { + this->is_transformer_active_ = false; this->transformer_ = nullptr; this->current_values = target; if (set_remote_values) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index ac4718ade5..acba986f24 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -28,6 +28,35 @@ enum LightRestoreMode { LIGHT_RESTORE_AND_ON, }; +struct LightStateRTCState { + LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green, + float blue, float white, float color_temp, float cold_white, float warm_white) + : color_mode(color_mode), + state(state), + brightness(brightness), + color_brightness(color_brightness), + red(red), + green(green), + blue(blue), + white(white), + color_temp(color_temp), + cold_white(cold_white), + warm_white(warm_white) {} + LightStateRTCState() = default; + ColorMode color_mode{ColorMode::UNKNOWN}; + bool state{false}; + float brightness{1.0f}; + float color_brightness{1.0f}; + float red{1.0f}; + float green{1.0f}; + float blue{1.0f}; + float white{1.0f}; + float color_temp{1.0f}; + float cold_white{1.0f}; + float warm_white{1.0f}; + uint32_t effect{0}; +}; + /** This class represents the communication layer between the front-end MQTT layer and the * hardware output layer. */ @@ -116,6 +145,9 @@ class LightState : public EntityBase, public Component { /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); + /// Set the initial state of this light + void set_initial_state(const LightStateRTCState &initial_state); + /// Return whether the light has any effects that meet the trait requirements. bool supports_effects(); @@ -144,6 +176,17 @@ class LightState : public EntityBase, public Component { void current_values_as_ct(float *color_temperature, float *white_brightness); + /** + * Indicator if a transformer (e.g. transition) is active. This is useful + * for effects e.g. at the start of the apply() method, add a check like: + * + * if (this->state_->is_transformer_active()) { + * // Something is already running. + * return; + * } + */ + bool is_transformer_active(); + protected: friend LightOutput; friend LightCall; @@ -201,8 +244,13 @@ class LightState : public EntityBase, public Component { float gamma_correct_{}; /// Restore mode of the light. LightRestoreMode restore_mode_; + /// Initial state of the light. + optional initial_state_{}; /// List of effects for this light. std::vector effects_; + + // for effects, true if a transformer (transition) is active. + bool is_transformer_active_ = false; }; } // namespace light diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index a453debd94..a586bcbd13 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -1,5 +1,5 @@ -import esphome.codegen as cg from esphome import automation +import esphome.codegen as cg # Base light_ns = cg.esphome_ns.namespace("light") @@ -12,6 +12,8 @@ AddressableLightRef = AddressableLight.operator("ref") Color = cg.esphome_ns.class_("Color") LightColorValues = light_ns.class_("LightColorValues") +LightStateRTCState = light_ns.struct("LightStateRTCState") + # Color modes ColorMode = light_ns.enum("ColorMode", is_class=True) COLOR_MODES = { @@ -26,6 +28,13 @@ COLOR_MODES = { "RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE, } +# Limit modes +LimitMode = light_ns.enum("LimitMode", is_class=True) +LIMIT_MODES = { + "CLAMP": LimitMode.CLAMP, + "DO_NOTHING": LimitMode.DO_NOTHING, +} + # Actions ToggleAction = light_ns.class_("ToggleAction", automation.Action) LightControlAction = light_ns.class_("LightControlAction", automation.Action) diff --git a/esphome/components/lightwaverf/LwRx.cpp b/esphome/components/lightwaverf/LwRx.cpp new file mode 100644 index 0000000000..2b1ad5e870 --- /dev/null +++ b/esphome/components/lightwaverf/LwRx.cpp @@ -0,0 +1,427 @@ +// LwRx.cpp +// +// LightwaveRF 434MHz receiver interface for Arduino +// +// Author: Bob Tidey (robert@tideys.net) + +#ifdef USE_ESP8266 + +#include "LwRx.h" +#include + +namespace esphome { +namespace lightwaverf { + +/** + Pin change interrupt routine that identifies 1 and 0 LightwaveRF bits + and constructs a message when a valid packet of data is received. +**/ + +void IRAM_ATTR LwRx::rx_process_bits(LwRx *args) { + uint8_t event = args->rx_pin_isr_.digital_read(); // start setting event to the current value + uint32_t curr = micros(); // the current time in microseconds + + uint16_t dur = (curr - args->rx_prev); // unsigned int + args->rx_prev = curr; + // set event based on input and duration of previous pulse + if (dur < 120) { // 120 very short + } else if (dur < 500) { // normal short pulse + event += 2; + } else if (dur < 2000) { // normal long pulse + event += 4; + } else if (dur > 5000) { // gap between messages + event += 6; + } else { // 2000 > 5000 + event = 8; // illegal gap + } + // state machine transitions + switch (args->rx_state) { + case RX_STATE_IDLE: + switch (event) { + case 7: // 1 after a message gap + args->rx_state = RX_STATE_MSGSTARTFOUND; + break; + } + break; + case RX_STATE_MSGSTARTFOUND: + switch (event) { + case 2: // 0 160->500 + // nothing to do wait for next positive edge + break; + case 3: // 1 160->500 + args->rx_num_bytes = 0; + args->rx_state = RX_STATE_BYTESTARTFOUND; + break; + default: + // not good start again + args->rx_state = RX_STATE_IDLE; + break; + } + break; + case RX_STATE_BYTESTARTFOUND: + switch (event) { + case 2: // 0 160->500 + // nothing to do wait for next positive edge + break; + case 3: // 1 160->500 + args->rx_state = RX_STATE_GETBYTE; + args->rx_num_bits = 0; + break; + case 5: // 0 500->1500 + args->rx_state = RX_STATE_GETBYTE; + // Starts with 0 so put this into uint8_t + args->rx_num_bits = 1; + args->rx_buf[args->rx_num_bytes] = 0; + break; + default: + // not good start again + args->rx_state = RX_STATE_IDLE; + break; + } + break; + case RX_STATE_GETBYTE: + switch (event) { + case 2: // 0 160->500 + // nothing to do wait for next positive edge but do stats + if (args->lwrx_stats_enable) { + args->lwrx_stats[RX_STAT_HIGH_MAX] = std::max(args->lwrx_stats[RX_STAT_HIGH_MAX], dur); + args->lwrx_stats[RX_STAT_HIGH_MIN] = std::min(args->lwrx_stats[RX_STAT_HIGH_MIN], dur); + args->lwrx_stats[RX_STAT_HIGH_AVE] = + args->lwrx_stats[RX_STAT_HIGH_AVE] - (args->lwrx_stats[RX_STAT_HIGH_AVE] >> 4) + dur; + } + break; + case 3: // 1 160->500 + // a single 1 + args->rx_buf[args->rx_num_bytes] = args->rx_buf[args->rx_num_bytes] << 1 | 1; + args->rx_num_bits++; + if (args->lwrx_stats_enable) { + args->lwrx_stats[RX_STAT_LOW1_MAX] = std::max(args->lwrx_stats[RX_STAT_LOW1_MAX], dur); + args->lwrx_stats[RX_STAT_LOW1_MIN] = std::min(args->lwrx_stats[RX_STAT_LOW1_MIN], dur); + args->lwrx_stats[RX_STAT_LOW1_AVE] = + args->lwrx_stats[RX_STAT_LOW1_AVE] - (args->lwrx_stats[RX_STAT_LOW1_AVE] >> 4) + dur; + } + break; + case 5: // 1 500->1500 + // a 1 followed by a 0 + args->rx_buf[args->rx_num_bytes] = args->rx_buf[args->rx_num_bytes] << 2 | 2; + args->rx_num_bits++; + args->rx_num_bits++; + if (args->lwrx_stats_enable) { + args->lwrx_stats[RX_STAT_LOW0_MAX] = std::max(args->lwrx_stats[RX_STAT_LOW0_MAX], dur); + args->lwrx_stats[RX_STAT_LOW0_MIN] = std::min(args->lwrx_stats[RX_STAT_LOW0_MIN], dur); + args->lwrx_stats[RX_STAT_LOW0_AVE] = + args->lwrx_stats[RX_STAT_LOW0_AVE] - (args->lwrx_stats[RX_STAT_LOW0_AVE] >> 4) + dur; + } + break; + default: + // not good start again + args->rx_state = RX_STATE_IDLE; + break; + } + if (args->rx_num_bits >= 8) { + args->rx_num_bytes++; + args->rx_num_bits = 0; + if (args->rx_num_bytes >= RX_MSGLEN) { + uint32_t curr_millis = millis(); + if (args->rx_repeats > 0) { + if ((curr_millis - args->rx_prevpkttime) / 100 > args->rx_timeout) { + args->rx_repeatcount = 1; + } else { + // Test message same as last one + int16_t i = RX_MSGLEN; // int + do { + i--; + } while ((i >= 0) && (args->rx_msg[i] == args->rx_buf[i])); + if (i < 0) { + args->rx_repeatcount++; + } else { + args->rx_repeatcount = 1; + } + } + } else { + args->rx_repeatcount = 0; + } + args->rx_prevpkttime = curr_millis; + // If last message hasn't been read it gets overwritten + memcpy(args->rx_msg, args->rx_buf, RX_MSGLEN); + if (args->rx_repeats == 0 || args->rx_repeatcount == args->rx_repeats) { + if (args->rx_pairtimeout != 0) { + if ((curr_millis - args->rx_pairstarttime) / 100 <= args->rx_pairtimeout) { + if (args->rx_msg[3] == RX_CMD_ON) { + args->rx_addpairfrommsg_(); + } else if (args->rx_msg[3] == RX_CMD_OFF) { + args->rx_remove_pair_(&args->rx_msg[2]); + } + } + } + if (args->rx_report_message_()) { + args->rx_msgcomplete = true; + } + args->rx_pairtimeout = 0; + } + // And cycle round for next one + args->rx_state = RX_STATE_IDLE; + } else { + args->rx_state = RX_STATE_BYTESTARTFOUND; + } + } + break; + } +} + +/** + Test if a message has arrived +**/ +bool LwRx::lwrx_message() { return (this->rx_msgcomplete); } + +/** + Set translate mode +**/ +void LwRx::lwrx_settranslate(bool rxtranslate) { this->rx_translate = rxtranslate; } +/** + Transfer a message to user buffer +**/ +bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { + bool ret = true; + int16_t j = 0; // int + if (this->rx_msgcomplete && len <= RX_MSGLEN) { + for (uint8_t i = 0; ret && i < RX_MSGLEN; i++) { + if (this->rx_translate || (len != RX_MSGLEN)) { + j = this->rx_find_nibble_(this->rx_msg[i]); + if (j < 0) + ret = false; + } else { + j = this->rx_msg[i]; + } + switch (len) { + case 4: + if (i == 9) + buf[2] = j; + if (i == 2) + buf[3] = j; + case 2: + if (i == 3) + buf[0] = j; + if (i == 0) + buf[1] = j << 4; + if (i == 1) + buf[1] += j; + break; + case 10: + buf[i] = j; + break; + } + } + this->rx_msgcomplete = false; + } else { + ret = false; + } + return ret; +} + +/** + Return time in milliseconds since last packet received +**/ +uint32_t LwRx::lwrx_packetinterval() { return millis() - this->rx_prevpkttime; } + +/** + Set up repeat filtering of received messages +**/ +void LwRx::lwrx_setfilter(uint8_t repeats, uint8_t timeout) { + this->rx_repeats = repeats; + this->rx_timeout = timeout; +} + +/** + Add a pair to filter received messages + pairdata is device,dummy,5*addr,room + pairdata is held in translated form to make comparisons quicker +**/ +uint8_t LwRx::lwrx_addpair(const uint8_t *pairdata) { + if (this->rx_paircount < RX_MAXPAIRS) { + for (uint8_t i = 0; i < 8; i++) { + this->rx_pairs[rx_paircount][i] = RX_NIBBLE[pairdata[i]]; + } + this->rx_paircommit_(); + } + return this->rx_paircount; +} + +/** + Make a pair from next message successfully received +**/ +void LwRx::lwrx_makepair(uint8_t timeout) { + this->rx_pairtimeout = timeout; + this->rx_pairstarttime = millis(); +} + +/** + Get pair data (translated back to nibble form +**/ +uint8_t LwRx::lwrx_getpair(uint8_t *pairdata, uint8_t pairnumber) { + if (pairnumber < this->rx_paircount) { + int16_t j; // int + for (uint8_t i = 0; i < 8; i++) { + j = this->rx_find_nibble_(this->rx_pairs[pairnumber][i]); + if (j >= 0) + pairdata[i] = j; + } + } + return this->rx_paircount; +} + +/** + Clear all pairing +**/ +void LwRx::lwrx_clearpairing_() { rx_paircount = 0; } + +/** + Return stats on high and low pulses +**/ +bool LwRx::lwrx_getstats_(uint16_t *stats) { // unsigned int + if (this->lwrx_stats_enable) { + memcpy(stats, this->lwrx_stats, 2 * RX_STAT_COUNT); + return true; + } else { + return false; + } +} + +/** + Set stats mode +**/ +void LwRx::lwrx_setstatsenable_(bool rx_stats_enable) { + this->lwrx_stats_enable = rx_stats_enable; + if (!this->lwrx_stats_enable) { + // clear down stats when disabling + memcpy(this->lwrx_stats, LWRX_STATSDFLT, sizeof(LWRX_STATSDFLT)); + } +} +/** + Set pairs behaviour +**/ +void LwRx::lwrx_set_pair_mode(bool pair_enforce, bool pair_base_only) { + this->rx_pairEnforce = pair_enforce; + this->rx_pairBaseOnly = pair_base_only; +} + +/** + Set things up to receive LightWaveRF 434Mhz messages + pin must be 2 or 3 to trigger interrupts + !!! For Spark, any pin will work +**/ +void LwRx::lwrx_setup(InternalGPIOPin *pin) { + // rx_pin = pin; + pin->setup(); + this->rx_pin_isr_ = pin->to_isr(); + pin->attach_interrupt(&LwRx::rx_process_bits, this, gpio::INTERRUPT_ANY_EDGE); + + memcpy(this->lwrx_stats, LWRX_STATSDFLT, sizeof(LWRX_STATSDFLT)); +} + +/** + Check a message to see if it should be reported under pairing / mood / all off rules + returns -1 if none found +**/ +bool LwRx::rx_report_message_() { + if (this->rx_pairEnforce && this->rx_paircount == 0) { + return false; + } else { + bool all_devices; + // True if mood to device 15 or Off cmd with Allof paramater + all_devices = ((this->rx_msg[3] == RX_CMD_MOOD && this->rx_msg[2] == RX_DEV_15) || + (this->rx_msg[3] == RX_CMD_OFF && this->rx_msg[0] == RX_PAR0_ALLOFF)); + return (rx_check_pairs_(&this->rx_msg[2], all_devices) != -1); + } +} +/** + Find nibble from byte + returns -1 if none found +**/ +int16_t LwRx::rx_find_nibble_(uint8_t data) { // int + int16_t i = 15; // int + do { + if (RX_NIBBLE[i] == data) + break; + i--; + } while (i >= 0); + return i; +} + +/** + add pair from message buffer +**/ +void LwRx::rx_addpairfrommsg_() { + if (this->rx_paircount < RX_MAXPAIRS) { + memcpy(this->rx_pairs[this->rx_paircount], &this->rx_msg[2], 8); + this->rx_paircommit_(); + } +} + +/** + check and commit pair +**/ +void LwRx::rx_paircommit_() { + if (this->rx_paircount == 0 || this->rx_check_pairs_(this->rx_pairs[this->rx_paircount], false) < 0) { + this->rx_paircount++; + } +} + +/** + Check to see if message matches one of the pairs + if mode is pairBase only then ignore device and room + if allDevices is true then ignore the device number + Returns matching pair number, -1 if not found, -2 if no pairs defined +**/ +int16_t LwRx::rx_check_pairs_(const uint8_t *buf, bool all_devices) { // int + if (this->rx_paircount == 0) { + return -2; + } else { + int16_t pair = this->rx_paircount; // int + int16_t j = -1; // int + int16_t jstart, jend; // int + if (this->rx_pairBaseOnly) { + // skip room(8) and dev/cmd (0,1) + jstart = 7; + jend = 2; + } else { + // include room in comparison + jstart = 8; + // skip device comparison if allDevices true + jend = (all_devices) ? 2 : 0; + } + while (pair > 0 && j < 0) { + pair--; + j = jstart; + while (j > jend) { + j--; + if (j != 1) { + if (this->rx_pairs[pair][j] != buf[j]) { + j = -1; + } + } + } + } + return (j >= 0) ? pair : -1; + } +} + +/** + Remove an existing pair matching the buffer +**/ +void LwRx::rx_remove_pair_(uint8_t *buf) { + int16_t pair = this->rx_check_pairs_(buf, false); // int + if (pair >= 0) { + while (pair < this->rx_paircount - 1) { + for (uint8_t j = 0; j < 8; j++) { + this->rx_pairs[pair][j] = this->rx_pairs[pair + 1][j]; + } + pair++; + } + this->rx_paircount--; + } +} + +} // namespace lightwaverf +} // namespace esphome +#endif diff --git a/esphome/components/lightwaverf/LwRx.h b/esphome/components/lightwaverf/LwRx.h new file mode 100644 index 0000000000..7200f9a51c --- /dev/null +++ b/esphome/components/lightwaverf/LwRx.h @@ -0,0 +1,142 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace lightwaverf { + +// LwRx.h +// +// LightwaveRF 434MHz receiver for Arduino +// +// Author: Bob Tidey (robert@tideys.net) + +static const uint8_t RX_STAT_HIGH_AVE = 0; +static const uint8_t RX_STAT_HIGH_MAX = 1; +static const uint8_t RX_STAT_HIGH_MIN = 2; +static const uint8_t RX_STAT_LOW0_AVE = 3; +static const uint8_t RX_STAT_LOW0_MAX = 4; +static const uint8_t RX_STAT_LOW0_MIN = 5; +static const uint8_t RX_STAT_LOW1_AVE = 6; +static const uint8_t RX_STAT_LOW1_MAX = 7; +static const uint8_t RX_STAT_LOW1_MIN = 8; +static const uint8_t RX_STAT_COUNT = 9; + +// sets maximum number of pairings which can be held +static const uint8_t RX_MAXPAIRS = 10; + +static const uint8_t RX_NIBBLE[] = {0xF6, 0xEE, 0xED, 0xEB, 0xDE, 0xDD, 0xDB, 0xBE, + 0xBD, 0xBB, 0xB7, 0x7E, 0x7D, 0x7B, 0x77, 0x6F}; +static const uint8_t RX_CMD_OFF = 0xF6; // raw 0 +static const uint8_t RX_CMD_ON = 0xEE; // raw 1 +static const uint8_t RX_CMD_MOOD = 0xED; // raw 2 +static const uint8_t RX_PAR0_ALLOFF = 0x7D; // param 192-255 all off (12 in msb) +static const uint8_t RX_DEV_15 = 0x6F; // device 15 + +static const uint8_t RX_MSGLEN = 10; // expected length of rx message + +static const uint8_t RX_STATE_IDLE = 0; +static const uint8_t RX_STATE_MSGSTARTFOUND = 1; +static const uint8_t RX_STATE_BYTESTARTFOUND = 2; +static const uint8_t RX_STATE_GETBYTE = 3; + +// Gather stats for pulse widths (ave is x 16) +static const uint16_t LWRX_STATSDFLT[RX_STAT_COUNT] = {5000, 0, 5000, 20000, 0, 2500, 4000, 0, 500}; // usigned int + +class LwRx { + public: + // Seup must be called once, set up pin used to receive data + void lwrx_setup(InternalGPIOPin *pin); + + // Set translate to determine whether translating from nibbles to bytes in message + // Translate off only applies to 10char message returns + void lwrx_settranslate(bool translate); + + // Check to see whether message available + bool lwrx_message(); + + // Get a message, len controls format (2 cmd+param, 4 cmd+param+room+device),10 full message + bool lwrx_getmessage(uint8_t *buf, uint8_t len); + + // Setup repeat filter + void lwrx_setfilter(uint8_t repeats, uint8_t timeout); + + // Add pair, if no pairing set then all messages are received, returns number of pairs + uint8_t lwrx_addpair(const uint8_t *pairdata); + + // Get pair data into buffer for the pairnumber. Returns current paircount + // Use pairnumber 255 to just get current paircount + uint8_t lwrx_getpair(uint8_t *pairdata, uint8_t pairnumber); + + // Make a pair from next message received within timeout 100mSec + // This call returns immediately whilst message checking continues + void lwrx_makepair(uint8_t timeout); + + // Set pair mode controls + void lwrx_set_pair_mode(bool pair_enforce, bool pair_base_only); + + // Returns time from last packet received in msec + // Can be used to determine if Rx may be still receiving repeats + uint32_t lwrx_packetinterval(); + + static void rx_process_bits(LwRx *arg); + + // Pairing data + uint8_t rx_paircount = 0; + uint8_t rx_pairs[RX_MAXPAIRS][8]; + // set false to responds to all messages if no pairs set up + bool rx_pairEnforce = false; + // set false to use Address, Room and Device in pairs, true just the Address part + bool rx_pairBaseOnly = false; + + uint8_t rx_pairtimeout = 0; // 100msec units + + // Repeat filters + uint8_t rx_repeats = 2; // msg must be repeated at least this number of times + uint8_t rx_repeatcount = 0; + uint8_t rx_timeout = 20; // reset repeat window after this in 100mSecs + uint32_t rx_prevpkttime = 0; // last packet time in milliseconds + uint32_t rx_pairstarttime = 0; // last msg time in milliseconds + + // Receive mode constants and variables + uint8_t rx_msg[RX_MSGLEN]; // raw message received + uint8_t rx_buf[RX_MSGLEN]; // message buffer during reception + + uint32_t rx_prev; // time of previous interrupt in microseconds + + bool rx_msgcomplete = false; // set high when message available + bool rx_translate = true; // Set false to get raw data + + uint8_t rx_state = 0; + + uint8_t rx_num_bits = 0; // number of bits in the current uint8_t + uint8_t rx_num_bytes = 0; // number of bytes received + + uint16_t lwrx_stats[RX_STAT_COUNT]; // unsigned int + + bool lwrx_stats_enable = true; + + protected: + void lwrx_clearpairing_(); + + // Return stats on pulse timings + bool lwrx_getstats_(uint16_t *stats); + + // Enable collection of stats on pulse timings + void lwrx_setstatsenable_(bool rx_stats_enable); + + // internal support functions + bool rx_report_message_(); + int16_t rx_find_nibble_(uint8_t data); // int + void rx_addpairfrommsg_(); + void rx_paircommit_(); + void rx_remove_pair_(uint8_t *buf); + int16_t rx_check_pairs_(const uint8_t *buf, bool all_devices); // int + + ISRInternalGPIOPin rx_pin_isr_; + InternalGPIOPin *rx_pin_; +}; + +} // namespace lightwaverf +} // namespace esphome diff --git a/esphome/components/lightwaverf/LwTx.cpp b/esphome/components/lightwaverf/LwTx.cpp new file mode 100644 index 0000000000..2f46b04b2d --- /dev/null +++ b/esphome/components/lightwaverf/LwTx.cpp @@ -0,0 +1,208 @@ +// LwTx.cpp +// +// LightwaveRF 434MHz tx interface for Arduino +// +// Author: Bob Tidey (robert@tideys.net) +#ifdef USE_ESP8266 + +#include "LwTx.h" +#include +#include +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace lightwaverf { + +static const uint8_t TX_NIBBLE[] = {0xF6, 0xEE, 0xED, 0xEB, 0xDE, 0xDD, 0xDB, 0xBE, + 0xBD, 0xBB, 0xB7, 0x7E, 0x7D, 0x7B, 0x77, 0x6F}; + +static const uint8_t TX_STATE_IDLE = 0; +static const uint8_t TX_STATE_MSGSTART = 1; +static const uint8_t TX_STATE_BYTESTART = 2; +static const uint8_t TX_STATE_SENDBYTE = 3; +static const uint8_t TX_STATE_MSGEND = 4; +static const uint8_t TX_STATE_GAPSTART = 5; +static const uint8_t TX_STATE_GAPEND = 6; +/** + Set translate mode +**/ +void LwTx::lwtx_settranslate(bool txtranslate) { tx_translate = txtranslate; } + +static void IRAM_ATTR isr_t_xtimer(LwTx *arg) { + // Set low after toggle count interrupts + arg->tx_toggle_count--; + if (arg->tx_toggle_count == arg->tx_trail_count) { + // ESP_LOGD("lightwaverf.sensor", "timer") + arg->tx_pin->digital_write(arg->txoff); + } else if (arg->tx_toggle_count == 0) { + arg->tx_toggle_count = arg->tx_high_count; // default high pulse duration + switch (arg->tx_state) { + case TX_STATE_IDLE: + if (arg->tx_msg_active) { + arg->tx_repeat = 0; + arg->tx_state = TX_STATE_MSGSTART; + } + break; + case TX_STATE_MSGSTART: + arg->tx_pin->digital_write(arg->txon); + arg->tx_num_bytes = 0; + arg->tx_state = TX_STATE_BYTESTART; + break; + case TX_STATE_BYTESTART: + arg->tx_pin->digital_write(arg->txon); + arg->tx_bit_mask = 0x80; + arg->tx_state = TX_STATE_SENDBYTE; + break; + case TX_STATE_SENDBYTE: + if (arg->tx_buf[arg->tx_num_bytes] & arg->tx_bit_mask) { + arg->tx_pin->digital_write(arg->txon); + } else { + // toggle count for the 0 pulse + arg->tx_toggle_count = arg->tx_low_count; + } + arg->tx_bit_mask >>= 1; + if (arg->tx_bit_mask == 0) { + arg->tx_num_bytes++; + if (arg->tx_num_bytes >= esphome::lightwaverf::LwTx::TX_MSGLEN) { + arg->tx_state = TX_STATE_MSGEND; + } else { + arg->tx_state = TX_STATE_BYTESTART; + } + } + break; + case TX_STATE_MSGEND: + arg->tx_pin->digital_write(arg->txon); + arg->tx_state = TX_STATE_GAPSTART; + arg->tx_gap_repeat = arg->tx_gap_multiplier; + break; + case TX_STATE_GAPSTART: + arg->tx_toggle_count = arg->tx_gap_count; + if (arg->tx_gap_repeat == 0) { + arg->tx_state = TX_STATE_GAPEND; + } else { + arg->tx_gap_repeat--; + } + break; + case TX_STATE_GAPEND: + arg->tx_repeat++; + if (arg->tx_repeat >= arg->tx_repeats) { + // disable timer nterrupt + arg->lw_timer_stop(); + arg->tx_msg_active = false; + arg->tx_state = TX_STATE_IDLE; + } else { + arg->tx_state = TX_STATE_MSGSTART; + } + break; + } + } +} + +/** + Check for send free +**/ +bool LwTx::lwtx_free() { return !this->tx_msg_active; } + +/** + Send a LightwaveRF message (10 nibbles in bytes) +**/ +void LwTx::lwtx_send(const std::vector &msg) { + if (this->tx_translate) { + for (uint8_t i = 0; i < TX_MSGLEN; i++) { + this->tx_buf[i] = TX_NIBBLE[msg[i] & 0xF]; + ESP_LOGD("lightwaverf.sensor", "%x ", msg[i]); + } + } else { + // memcpy(tx_buf, msg, tx_msglen); + } + this->lw_timer_start(); + this->tx_msg_active = true; +} + +/** + Set 5 char address for future messages +**/ +void LwTx::lwtx_setaddr(const uint8_t *addr) { + for (uint8_t i = 0; i < 5; i++) { + this->tx_buf[i + 4] = TX_NIBBLE[addr[i] & 0xF]; + } +} + +/** + Send a LightwaveRF message (10 nibbles in bytes) +**/ +void LwTx::lwtx_cmd(uint8_t command, uint8_t parameter, uint8_t room, uint8_t device) { + // enable timer 2 interrupts + this->tx_buf[0] = TX_NIBBLE[parameter >> 4]; + this->tx_buf[1] = TX_NIBBLE[parameter & 0xF]; + this->tx_buf[2] = TX_NIBBLE[device & 0xF]; + this->tx_buf[3] = TX_NIBBLE[command & 0xF]; + this->tx_buf[9] = TX_NIBBLE[room & 0xF]; + this->lw_timer_start(); + this->tx_msg_active = true; +} + +/** + Set things up to transmit LightWaveRF 434Mhz messages +**/ +void LwTx::lwtx_setup(InternalGPIOPin *pin, uint8_t repeats, bool inverted, int u_sec) { + pin->setup(); + tx_pin = pin; + + tx_pin->pin_mode(gpio::FLAG_OUTPUT); + tx_pin->digital_write(txoff); + + if (repeats > 0 && repeats < 40) { + this->tx_repeats = repeats; + } + if (inverted) { + this->txon = 0; + this->txoff = 1; + } else { + this->txon = 1; + this->txoff = 0; + } + + int period1 = 330; + /* + if (period > 32 && period < 1000) { + period1 = period; + } else { + // default 330 uSec + period1 = 330; + }*/ + this->espPeriod = 5 * period1; + timer1_isr_init(); +} + +void LwTx::lwtx_set_tick_counts(uint8_t low_count, uint8_t high_count, uint8_t trail_count, uint8_t gap_count) { + this->tx_low_count = low_count; + this->tx_high_count = high_count; + this->tx_trail_count = trail_count; + this->tx_gap_count = gap_count; +} + +void LwTx::lwtx_set_gap_multiplier(uint8_t gap_multiplier) { this->tx_gap_multiplier = gap_multiplier; } + +void LwTx::lw_timer_start() { + { + InterruptLock lock; + static LwTx *arg = this; // NOLINT + timer1_attachInterrupt([] { isr_t_xtimer(arg); }); + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); + timer1_write(this->espPeriod); + } +} + +void LwTx::lw_timer_stop() { + { + InterruptLock lock; + timer1_disable(); + timer1_detachInterrupt(); + } +} + +} // namespace lightwaverf +} // namespace esphome +#endif diff --git a/esphome/components/lightwaverf/LwTx.h b/esphome/components/lightwaverf/LwTx.h new file mode 100644 index 0000000000..fe7b942a3a --- /dev/null +++ b/esphome/components/lightwaverf/LwTx.h @@ -0,0 +1,94 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +#include + +namespace esphome { +namespace lightwaverf { + +// LxTx.h +// +// LightwaveRF 434MHz tx interface for Arduino +// +// Author: Bob Tidey (robert@tideys.net) + +// Include basic library header and set default TX pin +static const uint8_t TX_PIN_DEFAULT = 13; + +class LwTx { + public: + // Sets up basic parameters must be called at least once + void lwtx_setup(InternalGPIOPin *pin, uint8_t repeats, bool inverted, int u_sec); + + // Allows changing basic tick counts from their defaults + void lwtx_set_tick_counts(uint8_t low_count, uint8_t high_count, uint8_t trail_count, uint8_t gap_count); + + // Allws multiplying the gap period for creating very large gaps + void lwtx_set_gap_multiplier(uint8_t gap_multiplier); + + // determines whether incoming data or should be translated from nibble data + void lwtx_settranslate(bool txtranslate); + + // Checks whether tx is free to accept a new message + bool lwtx_free(); + + // Basic send of new 10 char message, not normally needed if setaddr and cmd are used. + void lwtx_send(const std::vector &msg); + + // Sets up 5 char address which will be used to form messages for lwtx_cmd + void lwtx_setaddr(const uint8_t *addr); + + // Send Command + void lwtx_cmd(uint8_t command, uint8_t parameter, uint8_t room, uint8_t device); + + // Allows changing basic tick counts from their defaults + void lw_timer_start(); + + // Allws multiplying the gap period for creating very large gaps + void lw_timer_stop(); + + // These set the pulse durationlws in ticks. ESP uses 330uSec base tick, else use 140uSec + uint8_t tx_low_count = 3; // total number of ticks in a low (990 uSec) + uint8_t tx_high_count = 2; // total number of ticks in a high (660 uSec) + uint8_t tx_trail_count = 1; // tick count to set line low (330 uSec) + + uint8_t tx_toggle_count = 3; + + static const uint8_t TX_MSGLEN = 10; // the expected length of the message + + // Transmit mode constants and variables + uint8_t tx_repeats = 12; // Number of repeats of message sent + uint8_t txon = 1; + uint8_t txoff = 0; + bool tx_msg_active = false; // set true to activate message sending + bool tx_translate = true; // Set false to send raw data + + uint8_t tx_buf[TX_MSGLEN]; // the message buffer during reception + uint8_t tx_repeat = 0; // counter for repeats + uint8_t tx_state = 0; + uint16_t tx_gap_repeat = 0; // unsigned int + + // Use with low repeat counts + uint8_t tx_gap_count = 33; // Inter-message gap count (10.9 msec) + uint32_t espPeriod = 0; // Holds interrupt timer0 period + uint32_t espNext = 0; // Holds interrupt next count + + // Gap multiplier byte is used to multiply gap if longer periods are needed for experimentation + // If gap is 255 (35msec) then this to give a max of 9 seconds + // Used with low repeat counts to find if device times out + uint8_t tx_gap_multiplier = 0; // Gap extension byte + + uint8_t tx_bit_mask = 0; // bit mask in current byte + uint8_t tx_num_bytes = 0; // number of bytes sent + + InternalGPIOPin *tx_pin; + + protected: + uint32_t duty_on_; + uint32_t duty_off_; +}; + +} // namespace lightwaverf +} // namespace esphome diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py new file mode 100644 index 0000000000..4e96dda663 --- /dev/null +++ b/esphome/components/lightwaverf/__init__.py @@ -0,0 +1,83 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome import automation + +from esphome.const import ( + CONF_READ_PIN, + CONF_ID, + CONF_NAME, + CONF_WRITE_PIN, + CONF_REPEAT, + CONF_INVERTED, + CONF_PULSE_LENGTH, + CONF_CODE, +) +from esphome.cpp_helpers import gpio_pin_expression + +CODEOWNERS = ["@max246"] + +lightwaverf_ns = cg.esphome_ns.namespace("lightwaverf") + + +LIGHTWAVERFComponent = lightwaverf_ns.class_( + "LightWaveRF", cg.Component, cg.PollingComponent +) +LightwaveRawAction = lightwaverf_ns.class_("SendRawAction", automation.Action) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LIGHTWAVERFComponent), + cv.Optional(CONF_READ_PIN, default=13): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_WRITE_PIN, default=14): pins.internal_gpio_input_pin_schema, + } +).extend(cv.polling_component_schema("1s")) + + +LIGHTWAVE_SEND_SCHEMA = cv.Any( + cv.int_range(min=1), + cv.Schema( + { + cv.GenerateID(): cv.use_id(LIGHTWAVERFComponent), + cv.Required(CONF_NAME): cv.string, + cv.Required(CONF_CODE): cv.All( + [cv.Any(cv.hex_uint8_t)], + cv.Length(min=10), + ), + cv.Optional(CONF_REPEAT, default=10): cv.int_, + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + cv.Optional(CONF_PULSE_LENGTH, default=330): cv.int_, + } + ), +) + + +@automation.register_action( + "lightwaverf.send_raw", + LightwaveRawAction, + LIGHTWAVE_SEND_SCHEMA, +) +async def send_raw_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + repeats = await cg.templatable(config[CONF_REPEAT], args, int) + inverted = await cg.templatable(config[CONF_INVERTED], args, bool) + pulse_length = await cg.templatable(config[CONF_PULSE_LENGTH], args, int) + code = config[CONF_CODE] + + cg.add(var.set_repeats(repeats)) + cg.add(var.set_inverted(inverted)) + cg.add(var.set_pulse_length(pulse_length)) + cg.add(var.set_data(code)) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + pin_read = await gpio_pin_expression(config[CONF_READ_PIN]) + pin_write = await gpio_pin_expression(config[CONF_WRITE_PIN]) + cg.add(var.set_pin(pin_write, pin_read)) diff --git a/esphome/components/lightwaverf/lightwaverf.cpp b/esphome/components/lightwaverf/lightwaverf.cpp new file mode 100644 index 0000000000..89cbdae6e1 --- /dev/null +++ b/esphome/components/lightwaverf/lightwaverf.cpp @@ -0,0 +1,67 @@ +#include "esphome/core/log.h" + +#ifdef USE_ESP8266 + +#include "lightwaverf.h" + +namespace esphome { +namespace lightwaverf { + +static const char *const TAG = "lightwaverf.sensor"; + +static const uint8_t DEFAULT_REPEAT = 10; +static const bool DEFAULT_INVERT = false; +static const uint32_t DEFAULT_TICK = 330; + +void LightWaveRF::setup() { + ESP_LOGCONFIG(TAG, "Setting up Lightwave RF..."); + + this->lwtx_.lwtx_setup(pin_tx_, DEFAULT_REPEAT, DEFAULT_INVERT, DEFAULT_TICK); + this->lwrx_.lwrx_setup(pin_rx_); +} + +void LightWaveRF::update() { this->read_tx(); } + +void LightWaveRF::read_tx() { + if (this->lwrx_.lwrx_message()) { + this->lwrx_.lwrx_getmessage(msg_, msglen_); + print_msg_(msg_, msglen_); + } +} + +void LightWaveRF::send_rx(const std::vector &msg, uint8_t repeats, bool inverted, int u_sec) { + this->lwtx_.lwtx_setup(pin_tx_, repeats, inverted, u_sec); + + uint32_t timeout = 0; + if (this->lwtx_.lwtx_free()) { + this->lwtx_.lwtx_send(msg); + timeout = millis(); + ESP_LOGD(TAG, "[%i] msg start", timeout); + } + while (!this->lwtx_.lwtx_free() && millis() < (timeout + 1000)) { + delay(10); + } + timeout = millis() - timeout; + ESP_LOGD(TAG, "[%u] msg sent: %i", millis(), timeout); +} + +void LightWaveRF::print_msg_(uint8_t *msg, uint8_t len) { + char buffer[65]; + ESP_LOGD(TAG, " Received code (len:%i): ", len); + + for (int i = 0; i < len; i++) { + sprintf(&buffer[i * 6], "0x%02x, ", msg[i]); + } + ESP_LOGD(TAG, "[%s]", buffer); +} + +void LightWaveRF::dump_config() { + ESP_LOGCONFIG(TAG, "Lightwave RF:"); + LOG_PIN(" Pin TX: ", this->pin_tx_); + LOG_PIN(" Pin RX: ", this->pin_rx_); + LOG_UPDATE_INTERVAL(this); +} +} // namespace lightwaverf +} // namespace esphome + +#endif diff --git a/esphome/components/lightwaverf/lightwaverf.h b/esphome/components/lightwaverf/lightwaverf.h new file mode 100644 index 0000000000..b9f2abfcb3 --- /dev/null +++ b/esphome/components/lightwaverf/lightwaverf.h @@ -0,0 +1,70 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/automation.h" + +#include + +#include "LwRx.h" +#include "LwTx.h" + +namespace esphome { +namespace lightwaverf { + +#ifdef USE_ESP8266 + +class LightWaveRF : public PollingComponent { + public: + void set_pin(InternalGPIOPin *pin_tx, InternalGPIOPin *pin_rx) { + pin_tx_ = pin_tx; + pin_rx_ = pin_rx; + } + void update() override; + void setup() override; + void dump_config() override; + void read_tx(); + void send_rx(const std::vector &msg, uint8_t repeats, bool inverted, int u_sec); + + protected: + void print_msg_(uint8_t *msg, uint8_t len); + uint8_t msg_[10]; + uint8_t msglen_ = 10; + InternalGPIOPin *pin_tx_; + InternalGPIOPin *pin_rx_; + LwRx lwrx_; + LwTx lwtx_; +}; + +template class SendRawAction : public Action { + public: + SendRawAction(LightWaveRF *parent) : parent_(parent){}; + TEMPLATABLE_VALUE(int, repeat); + TEMPLATABLE_VALUE(int, inverted); + TEMPLATABLE_VALUE(int, pulse_length); + TEMPLATABLE_VALUE(std::vector, code); + + void set_repeats(const int &data) { repeat_ = data; } + void set_inverted(const int &data) { inverted_ = data; } + void set_pulse_length(const int &data) { pulse_length_ = data; } + void set_data(const std::vector &data) { code_ = data; } + + void play(Ts... x) { + int repeats = this->repeat_.value(x...); + int inverted = this->inverted_.value(x...); + int pulse_length = this->pulse_length_.value(x...); + std::vector msg = this->code_.value(x...); + + this->parent_->send_rx(msg, repeats, inverted, pulse_length); + } + + protected: + LightWaveRF *parent_; +}; + +#endif +} // namespace lightwaverf +} // namespace esphome +#endif diff --git a/esphome/components/lilygo_t5_47/touchscreen/__init__.py b/esphome/components/lilygo_t5_47/touchscreen/__init__.py index fe94120644..17f7262785 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/__init__.py +++ b/esphome/components/lilygo_t5_47/touchscreen/__init__.py @@ -13,13 +13,12 @@ DEPENDENCIES = ["i2c"] LilygoT547Touchscreen = lilygo_t5_47_ns.class_( "LilygoT547Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) CONF_LILYGO_T5_47_TOUCHSCREEN_ID = "lilygo_t5_47_touchscreen_id" -CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( +CONFIG_SCHEMA = touchscreen.touchscreen_schema("250ms").extend( cv.Schema( { cv.GenerateID(): cv.declare_id(LilygoT547Touchscreen), @@ -27,17 +26,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( pins.internal_gpio_input_pin_schema ), } - ) - .extend(i2c.i2c_device_schema(0x5A)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x5A)) ) 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 touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b89cf2a724..58f2a42812 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -23,15 +23,12 @@ static const uint8_t READ_TOUCH[1] = {0x07}; return; \ } -void Store::gpio_intr(Store *store) { store->touch = true; } - void LilygoT547Touchscreen::setup() { ESP_LOGCONFIG(TAG, "Setting up Lilygo T5 4.7 Touchscreen..."); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); if (this->write(nullptr, 0) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Failed to communicate!"); @@ -41,19 +38,19 @@ void LilygoT547Touchscreen::setup() { } this->write_register(POWER_REGISTER, WAKEUP_CMD, 1); + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->x_raw_max_ = this->display_->get_native_height(); + } + } } -void LilygoT547Touchscreen::loop() { - if (!this->store_.touch) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - this->store_.touch = false; - +void LilygoT547Touchscreen::update_touches() { uint8_t point = 0; uint8_t buffer[40] = {0}; - uint32_t sum_l = 0, sum_h = 0; i2c::ErrorCode err; err = this->write_register(TOUCH_REGISTER, READ_FLAGS, 1); @@ -69,102 +66,30 @@ void LilygoT547Touchscreen::loop() { point = buffer[5] & 0xF; - if (point == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } else if (point == 1) { + if (point == 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); ERROR_CHECK(err); err = this->read(&buffer[5], 2); ERROR_CHECK(err); - sum_l = buffer[5] << 8 | buffer[6]; } else if (point > 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); ERROR_CHECK(err); err = this->read(&buffer[5], 5 * (point - 1) + 3); ERROR_CHECK(err); - - sum_l = buffer[5 * point + 1] << 8 | buffer[5 * point + 2]; } this->write_register(TOUCH_REGISTER, CLEAR_FLAGS, 2); - for (int i = 0; i < 5 * point; i++) - sum_h += buffer[i]; + if (point == 0) + point = 1; - if (sum_l != sum_h) - point = 0; - - if (point) { - uint8_t offset; - for (int i = 0; i < point; i++) { - if (i == 0) { - offset = 0; - } else { - offset = 4; - } - - TouchPoint tp; - - tp.id = (buffer[i * 5 + offset] >> 4) & 0x0F; - tp.state = buffer[i * 5 + offset] & 0x0F; - if (tp.state == 0x06) - tp.state = 0x07; - - uint16_t y = (uint16_t) ((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); - uint16_t x = (uint16_t) ((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); - - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = this->display_height_ - y; - tp.x = x; - break; - case ROTATE_90_DEGREES: - tp.x = this->display_height_ - y; - tp.y = this->display_width_ - x; - break; - case ROTATE_180_DEGREES: - tp.y = y; - tp.x = this->display_width_ - x; - break; - case ROTATE_270_DEGREES: - tp.x = y; - tp.y = x; - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); - } - } else { - TouchPoint tp; - tp.id = (buffer[0] >> 4) & 0x0F; - tp.state = 0x06; - - uint16_t y = (uint16_t) ((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); - uint16_t x = (uint16_t) ((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); - - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = this->display_height_ - y; - tp.x = x; - break; - case ROTATE_90_DEGREES: - tp.x = this->display_height_ - y; - tp.y = this->display_width_ - x; - break; - case ROTATE_180_DEGREES: - tp.y = y; - tp.x = this->display_width_ - x; - break; - case ROTATE_270_DEGREES: - tp.x = y; - tp.y = x; - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); + uint16_t id, x_raw, y_raw; + for (uint8_t i = 0; i < point; i++) { + id = (buffer[i * 5] >> 4) & 0x0F; + y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F)); + x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F)); + this->add_raw_touch_position_(id, x_raw, y_raw); } this->status_clear_warning(); diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h index 3d00e0b117..6767bf0a71 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h @@ -6,29 +6,25 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace lilygo_t5_47 { -struct Store { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(Store *store); -}; - using namespace touchscreen; -class LilygoT547Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class LilygoT547Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; + void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + void update_touches() override; + InternalGPIOPin *interrupt_pin_; - Store store_; }; } // namespace lilygo_t5_47 diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index f659c48a6e..6925861b52 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -1,14 +1,15 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition, maybe_simple_id -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MQTT_ID, CONF_ON_LOCK, CONF_ON_UNLOCK, CONF_TRIGGER_ID, - CONF_MQTT_ID, + CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -30,20 +31,24 @@ LockCondition = lock_ns.class_("LockCondition", Condition) LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template()) LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template()) -LOCK_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent), - cv.Optional(CONF_ON_LOCK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockLockTrigger), - } - ), - cv.Optional(CONF_ON_UNLOCK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockUnlockTrigger), - } - ), - } +LOCK_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent), + cv.Optional(CONF_ON_LOCK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockLockTrigger), + } + ), + cv.Optional(CONF_ON_UNLOCK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockUnlockTrigger), + } + ), + } + ) ) @@ -57,10 +62,13 @@ async def setup_lock_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if mqtt_id := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + async def register_lock(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index af96b03c8e..f30bc23e38 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,9 +1,21 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import LambdaAction +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +from esphome.components.libretiny import get_libretiny_component, get_libretiny_family +from esphome.components.libretiny.const import COMPONENT_BK72XX, COMPONENT_RTL87XX +import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, CONF_BAUD_RATE, @@ -17,18 +29,13 @@ from esphome.const import ( CONF_TAG, CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PLATFORM_RTL87XX, ) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant -from esphome.components.esp32.const import ( - VARIANT_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32C3, - VARIANT_ESP32S3, -) CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -68,17 +75,26 @@ UART2 = "UART2" UART0_SWAP = "UART0_SWAP" USB_SERIAL_JTAG = "USB_SERIAL_JTAG" USB_CDC = "USB_CDC" +DEFAULT = "DEFAULT" 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_SERIAL_JTAG], + VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32C2: [UART0, UART1], + VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], } UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] -ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] +UART_SELECTION_LIBRETINY = { + COMPONENT_BK72XX: [DEFAULT, UART1, UART2], + COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], +} + +ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] @@ -89,13 +105,22 @@ HARDWARE_UART_TO_UART_SELECTION = { UART2: logger_ns.UART_SELECTION_UART2, USB_CDC: logger_ns.UART_SELECTION_USB_CDC, USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG, + DEFAULT: logger_ns.UART_SELECTION_DEFAULT, } HARDWARE_UART_TO_SERIAL = { - UART0: cg.global_ns.Serial, - UART0_SWAP: cg.global_ns.Serial, - UART1: cg.global_ns.Serial1, - UART2: cg.global_ns.Serial2, + PLATFORM_ESP8266: { + UART0: cg.global_ns.Serial, + UART0_SWAP: cg.global_ns.Serial, + UART1: cg.global_ns.Serial1, + UART2: cg.global_ns.Serial2, + DEFAULT: cg.global_ns.Serial, + }, + PLATFORM_RP2040: { + UART0: cg.global_ns.Serial1, + UART1: cg.global_ns.Serial2, + USB_CDC: cg.global_ns.Serial, + }, } is_log_level = cv.one_of(*LOG_LEVELS, upper=True) @@ -103,15 +128,28 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: - if value.upper() in ESP_IDF_UARTS and not CORE.using_esp_idf: - raise cv.Invalid(f"Only esp-idf framework supports {value}.") + if CORE.using_arduino and value.upper() in ESP_ARDUINO_UNSUPPORTED_USB_UARTS: + raise cv.Invalid(f"Arduino framework does not support {value}.") variant = get_esp32_variant() + if CORE.using_esp_idf and variant == VARIANT_ESP32C3 and value == USB_CDC: + raise cv.Invalid( + f"{value} is not supported for variant {variant} when using ESP-IDF." + ) if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) if CORE.is_esp8266: return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) if CORE.is_rp2040: return cv.one_of(*UART_SELECTION_RP2040, upper=True)(value) + if CORE.is_libretiny: + family = get_libretiny_family() + if family in UART_SELECTION_LIBRETINY: + return cv.one_of(*UART_SELECTION_LIBRETINY[family], upper=True)(value) + component = get_libretiny_component() + if component in UART_SELECTION_LIBRETINY: + return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) + if CORE.is_host: + raise cv.Invalid("Uart selection not valid for host platform") raise NotImplementedError @@ -143,9 +181,24 @@ CONFIG_SCHEMA = cv.All( CONF_HARDWARE_UART, esp8266=UART0, esp32=UART0, + esp32_s2=USB_CDC, + esp32_s3_arduino=USB_CDC, + esp32_s3_idf=USB_SERIAL_JTAG, + esp32_c3_arduino=USB_CDC, + esp32_c3_idf=USB_SERIAL_JTAG, rp2040=USB_CDC, + bk72xx=DEFAULT, + rtl87xx=DEFAULT, ): cv.All( - cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32, PLATFORM_RP2040]), + cv.only_on( + [ + PLATFORM_ESP8266, + PLATFORM_ESP32, + PLATFORM_RP2040, + PLATFORM_BK72XX, + PLATFORM_RTL87XX, + ] + ), uart_selection, ), cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, @@ -195,8 +248,14 @@ async def to_code(config): is_at_least_very_verbose = this_severity >= very_verbose_severity has_serial_logging = baud_rate != 0 - if CORE.is_esp8266 and has_serial_logging and is_at_least_verbose: - debug_serial_port = HARDWARE_UART_TO_SERIAL[config.get(CONF_HARDWARE_UART)] + if ( + (CORE.is_esp8266 or CORE.is_rp2040) + and has_serial_logging + and is_at_least_verbose + ): + debug_serial_port = HARDWARE_UART_TO_SERIAL[CORE.target_platform][ + config.get(CONF_HARDWARE_UART) + ] cg.add_build_flag(f"-DDEBUG_ESP_PORT={debug_serial_port}") cg.add_build_flag("-DLWIP_DEBUG") DEBUG_COMPONENTS = { @@ -220,11 +279,27 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") + if CORE.using_arduino: + if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") + if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32C3: + cg.add_build_flag("-DARDUINO_USB_MODE=1") + if CORE.using_esp_idf: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + try: + uart_selection(USB_SERIAL_JTAG) + cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") + except cv.Invalid: + pass + try: + uart_selection(USB_CDC) + cg.add_define("USE_LOGGER_USB_CDC") + except cv.Invalid: + pass # Register at end for safe mode await cg.register_component(log, config) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 8fd39265fd..dac08fbbce 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -1,16 +1,9 @@ #include "logger.h" #include -#ifdef USE_ESP_IDF -#include -#include "freertos/FreeRTOS.h" -#endif // USE_ESP_IDF - -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) -#include -#endif // USE_ESP32_FRAMEWORK_ARDUINO || USE_ESP_IDF #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace logger { @@ -46,7 +39,23 @@ void Logger::write_header_(int level, const char *tag, int line) { const char *color = LOG_LEVEL_COLORS[level]; const char *letter = LOG_LEVEL_LETTERS[level]; - this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, line); +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); +#else + void *current_task = nullptr; +#endif + if (current_task == main_task_) { + this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, line); + } else { + const char *thread_name = ""; +#if defined(USE_ESP32) + thread_name = pcTaskGetName(current_task); +#elif defined(USE_LIBRETINY) + thread_name = pcTaskGetTaskName(current_task); +#endif + this->printf_to_buffer_("%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, + ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color); + } } void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT @@ -103,6 +112,7 @@ int HOT Logger::level_for(const char *tag) { } return ESPHOME_LOG_LEVEL; } + void HOT Logger::log_message_(int level, const char *tag, int offset) { // remove trailing newline if (this->tx_buffer_[this->tx_buffer_at_ - 1] == '\n') { @@ -112,28 +122,9 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { this->set_null_terminator_(); const char *msg = this->tx_buffer_ + offset; + if (this->baud_rate_ > 0) { -#ifdef USE_ARDUINO - this->hw_serial_->println(msg); -#endif // USE_ARDUINO -#ifdef USE_ESP_IDF - if ( -#if defined(USE_ESP32_VARIANT_ESP32S2) - uart_ == UART_SELECTION_USB_CDC -#elif defined(USE_ESP32_VARIANT_ESP32C3) - uart_ == UART_SELECTION_USB_SERIAL_JTAG -#elif defined(USE_ESP32_VARIANT_ESP32S3) - uart_ == UART_SELECTION_USB_CDC || uart_ == UART_SELECTION_USB_SERIAL_JTAG -#else - /* DISABLES CODE */ (false) -#endif - ) { - puts(msg); - } else { - uart_write_bytes(uart_num_, msg, strlen(msg)); - uart_write_bytes(uart_num_, "\n", 1); - } -#endif + this->write_msg_(msg); } #ifdef USE_ESP32 @@ -145,9 +136,6 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { if (xPortGetFreeHeapSize() < 2048) return; #endif -#ifdef USE_HOST - puts(msg); -#endif this->log_callback_.call(level, tag, msg); } @@ -155,118 +143,35 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { // add 1 to buffer size for null terminator this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + this->main_task_ = xTaskGetCurrentTaskHandle(); +#endif } -void Logger::pre_setup() { - if (this->baud_rate_ > 0) { +#ifdef USE_LOGGER_USB_CDC +void Logger::loop() { #ifdef USE_ARDUINO - switch (this->uart_) { - case UART_SELECTION_UART0: -#ifdef USE_ESP8266 - case UART_SELECTION_UART0_SWAP: -#endif -#ifdef USE_RP2040 - this->hw_serial_ = &Serial1; - Serial1.begin(this->baud_rate_); -#else - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); -#endif -#ifdef USE_ESP8266 - if (this->uart_ == UART_SELECTION_UART0_SWAP) { - Serial.swap(); - } - Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); -#endif - break; - case UART_SELECTION_UART1: -#ifdef USE_RP2040 - this->hw_serial_ = &Serial2; - Serial2.begin(this->baud_rate_); -#else - this->hw_serial_ = &Serial1; - Serial1.begin(this->baud_rate_); -#endif -#ifdef USE_ESP8266 - Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); -#endif - break; -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && \ - !defined(USE_ESP32_VARIANT_ESP32S3) - case UART_SELECTION_UART2: - this->hw_serial_ = &Serial2; - Serial2.begin(this->baud_rate_); - break; -#endif -#ifdef USE_RP2040 - case UART_SELECTION_USB_CDC: - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); - break; -#endif - } -#endif // USE_ARDUINO -#ifdef USE_ESP_IDF - uart_num_ = UART_NUM_0; - switch (uart_) { - case UART_SELECTION_UART0: - uart_num_ = UART_NUM_0; - break; - case UART_SELECTION_UART1: - uart_num_ = UART_NUM_1; - break; -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) - case UART_SELECTION_UART2: - uart_num_ = UART_NUM_2; - break; -#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - case UART_SELECTION_USB_CDC: - uart_num_ = -1; - break; -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) - case UART_SELECTION_USB_SERIAL_JTAG: - uart_num_ = -1; - break; -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 - } - if (uart_num_ >= 0) { - uart_config_t uart_config{}; - uart_config.baud_rate = (int) baud_rate_; - uart_config.data_bits = UART_DATA_8_BITS; - uart_config.parity = UART_PARITY_DISABLE; - uart_config.stop_bits = UART_STOP_BITS_1; - uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; - uart_param_config(uart_num_, &uart_config); - const int uart_buffer_size = tx_buffer_size_; - // Install UART driver using an event queue here - uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); - } -#endif // USE_ESP_IDF + if (this->uart_ != UART_SELECTION_USB_CDC) { + return; } -#ifdef USE_ESP8266 - else { - uart_set_debug(UART_NO); + static bool opened = false; + if (opened == Serial) { + return; } -#endif // USE_ESP8266 - - global_logger = this; -#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) - esp_log_set_vprintf(esp_idf_log_vprintf_); - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { - esp_log_level_set("*", ESP_LOG_VERBOSE); + if (false == opened) { + App.schedule_dump_config(); } -#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO - - ESP_LOGI(TAG, "Log initialized"); + opened = !opened; +#endif } +#endif + void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_.push_back(LogLevelOverride{tag, log_level}); } -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) UARTSelection Logger::get_uart() const { return this->uart_; } #endif @@ -275,34 +180,13 @@ void Logger::add_on_log_callback(std::functionbaud_rate_); -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) - ESP_LOGCONFIG(TAG, " Hardware UART: %s", UART_SELECTIONS[this->uart_]); + ESP_LOGCONFIG(TAG, " Hardware UART: %s", get_uart_selection_()); #endif for (auto &it : this->log_levels_) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 54a5236cd8..b55cfb0771 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -25,40 +25,40 @@ namespace esphome { namespace logger { -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) /** Enum for logging UART selection * * Advanced configuration (pin selection, etc) is not supported. */ enum UARTSelection { +#ifdef USE_LIBRETINY + UART_SELECTION_DEFAULT = 0, + UART_SELECTION_UART0, +#else UART_SELECTION_UART0 = 0, +#endif UART_SELECTION_UART1, -#if defined(USE_ESP32) -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_LIBRETINY) || defined(USE_ESP32_VARIANT_ESP32) UART_SELECTION_UART2, -#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 -#ifdef USE_ESP_IDF -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#endif +#ifdef USE_LOGGER_USB_CDC UART_SELECTION_USB_CDC, -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) +#endif +#ifdef USE_LOGGER_USB_SERIAL_JTAG UART_SELECTION_USB_SERIAL_JTAG, -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 -#endif // USE_ESP_IDF -#endif // USE_ESP32 +#endif #ifdef USE_ESP8266 UART_SELECTION_UART0_SWAP, #endif // USE_ESP8266 -#ifdef USE_RP2040 - UART_SELECTION_USB_CDC, -#endif // USE_RP2040 }; -#endif // USE_ESP32 || USE_ESP8266 +#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY class Logger : public Component { public: explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); - +#ifdef USE_LOGGER_USB_CDC + void loop() override; +#endif /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); uint32_t get_baud_rate() const { return baud_rate_; } @@ -68,7 +68,7 @@ class Logger : public Component { #ifdef USE_ESP_IDF uart_port_t get_uart_num() const { return uart_num_; } #endif -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. UARTSelection get_uart() const; @@ -99,6 +99,7 @@ class Logger : public Component { void write_header_(int level, const char *tag, int line); void write_footer_(); void log_message_(int level, const char *tag, int offset = 0); + void write_msg_(const char *msg); inline bool is_buffer_full_() const { return this->tx_buffer_at_ >= this->tx_buffer_size_; } inline int buffer_remaining_capacity_() const { return this->tx_buffer_size_ - this->tx_buffer_at_; } @@ -138,6 +139,10 @@ class Logger : public Component { va_end(arg); } +#ifndef USE_HOST + const char *get_uart_selection_(); +#endif + uint32_t baud_rate_; char *tx_buffer_{nullptr}; int tx_buffer_at_{0}; @@ -145,6 +150,9 @@ class Logger : public Component { #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) UARTSelection uart_{UART_SELECTION_UART0}; #endif +#ifdef USE_LIBRETINY + UARTSelection uart_{UART_SELECTION_DEFAULT}; +#endif #ifdef USE_ARDUINO Stream *hw_serial_{nullptr}; #endif @@ -159,6 +167,7 @@ class Logger : public Component { CallbackManager log_callback_{}; /// Prevents recursive log calls, if true a log message is already being processed. bool recursion_guard_ = false; + void *main_task_ = nullptr; }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp new file mode 100644 index 0000000000..c9de3d815a --- /dev/null +++ b/esphome/components/logger/logger_esp32.cpp @@ -0,0 +1,214 @@ +#ifdef USE_ESP32 +#include "logger.h" + +#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#include +#endif // USE_ESP32_FRAMEWORK_ARDUINO || USE_ESP_IDF + +#ifdef USE_ESP_IDF +#include + +#ifdef USE_LOGGER_USB_SERIAL_JTAG +#include +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) +#include +#include +#else +#include +#endif +#endif + +#include "freertos/FreeRTOS.h" +#include "esp_idf_version.h" + +#include +#include +#include + +#endif // USE_ESP_IDF + +#include "esphome/core/log.h" + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +#ifdef USE_ESP_IDF + +#ifdef USE_LOGGER_USB_SERIAL_JTAG +static void init_usb_serial_jtag_() { + setvbuf(stdin, NULL, _IONBF, 0); // Disable buffering on stdin + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) + // Minicom, screen, idf_monitor send CR when ENTER key is pressed + esp_vfs_dev_usb_serial_jtag_set_rx_line_endings(ESP_LINE_ENDINGS_CR); + // Move the caret to the beginning of the next line on '\n' + esp_vfs_dev_usb_serial_jtag_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); +#else + // Minicom, screen, idf_monitor send CR when ENTER key is pressed + usb_serial_jtag_vfs_set_rx_line_endings(ESP_LINE_ENDINGS_CR); + // Move the caret to the beginning of the next line on '\n' + usb_serial_jtag_vfs_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); +#endif + + // Enable non-blocking mode on stdin and stdout + fcntl(fileno(stdout), F_SETFL, 0); + fcntl(fileno(stdin), F_SETFL, 0); + + usb_serial_jtag_driver_config_t usb_serial_jtag_config{}; + usb_serial_jtag_config.rx_buffer_size = 512; + usb_serial_jtag_config.tx_buffer_size = 512; + + esp_err_t ret = ESP_OK; + // Install USB-SERIAL-JTAG driver for interrupt-driven reads and writes + ret = usb_serial_jtag_driver_install(&usb_serial_jtag_config); + if (ret != ESP_OK) { + return; + } + + // Tell vfs to use usb-serial-jtag driver +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) + esp_vfs_usb_serial_jtag_use_driver(); +#else + usb_serial_jtag_vfs_use_driver(); +#endif +} +#endif + +void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { + uart_config_t uart_config{}; + uart_config.baud_rate = (int) baud_rate; + uart_config.data_bits = UART_DATA_8_BITS; + uart_config.parity = UART_PARITY_DISABLE; + uart_config.stop_bits = UART_STOP_BITS_1; + uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + uart_config.source_clk = UART_SCLK_DEFAULT; +#endif + uart_param_config(uart_num, &uart_config); + const int uart_buffer_size = tx_buffer_size; + // Install UART driver using an event queue here + uart_driver_install(uart_num, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); +} + +#endif // USE_ESP_IDF + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { +#ifdef USE_ARDUINO + switch (this->uart_) { + case UART_SELECTION_UART0: +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial0; + Serial0.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif + break; + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + break; +#ifdef USE_ESP32_VARIANT_ESP32 + case UART_SELECTION_UART2: + this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); + break; +#endif + +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + this->hw_serial_ = &Serial; +#if ARDUINO_USB_CDC_ON_BOOT + Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection +#endif + Serial.begin(this->baud_rate_); + break; +#endif + } +#endif // USE_ARDUINO + +#ifdef USE_ESP_IDF + this->uart_num_ = UART_NUM_0; + switch (this->uart_) { + case UART_SELECTION_UART0: + this->uart_num_ = UART_NUM_0; + init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + break; + case UART_SELECTION_UART1: + this->uart_num_ = UART_NUM_1; + init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + break; +#ifdef USE_ESP32_VARIANT_ESP32 + case UART_SELECTION_UART2: + this->uart_num_ = UART_NUM_2; + init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + break; +#endif +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + break; +#endif +#ifdef USE_LOGGER_USB_SERIAL_JTAG + case UART_SELECTION_USB_SERIAL_JTAG: + init_usb_serial_jtag_(); + break; +#endif + } +#endif // USE_ESP_IDF + } + + global_logger = this; +#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) + esp_log_set_vprintf(esp_idf_log_vprintf_); + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { + esp_log_level_set("*", ESP_LOG_VERBOSE); + } +#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO + + ESP_LOGI(TAG, "Log initialized"); +} + +#ifdef USE_ESP_IDF +void HOT Logger::write_msg_(const char *msg) { + if ( +#if defined(USE_ESP32_VARIANT_ESP32S2) + this->uart_ == UART_SELECTION_USB_CDC +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) + this->uart_ == UART_SELECTION_USB_SERIAL_JTAG +#elif defined(USE_ESP32_VARIANT_ESP32S3) + this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG +#else + /* DISABLES CODE */ (false) // NOLINT +#endif + ) { + puts(msg); + } else { + uart_write_bytes(this->uart_num_, msg, strlen(msg)); + uart_write_bytes(this->uart_num_, "\n", 1); + } +} +#else +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +#endif + +const char *const UART_SELECTIONS[] = { + "UART0", "UART1", +#ifdef USE_ESP32_VARIANT_ESP32 + "UART2", +#endif +#ifdef USE_LOGGER_USB_CDC + "USB_CDC", +#endif +#ifdef USE_LOGGER_USB_SERIAL_JTAG + "USB_SERIAL_JTAG", +#endif +}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome +#endif diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp new file mode 100644 index 0000000000..5bfeb21007 --- /dev/null +++ b/esphome/components/logger/logger_esp8266.cpp @@ -0,0 +1,45 @@ +#ifdef USE_ESP8266 +#include "logger.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace 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); + } + + global_logger = this; + + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome +#endif diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp new file mode 100644 index 0000000000..edffb1a8c3 --- /dev/null +++ b/esphome/components/logger/logger_host.cpp @@ -0,0 +1,24 @@ +#if defined(USE_HOST) +#include "logger.h" + +namespace esphome { +namespace logger { + +void HOT Logger::write_msg_(const char *msg) { + time_t rawtime; + struct tm *timeinfo; + char buffer[80]; + + time(&rawtime); + timeinfo = localtime(&rawtime); + strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo); + fputs(buffer, stdout); + puts(msg); +} + +void Logger::pre_setup() { global_logger = this; } + +} // namespace logger +} // namespace esphome + +#endif diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp new file mode 100644 index 0000000000..12e55b7cef --- /dev/null +++ b/esphome/components/logger/logger_libretiny.cpp @@ -0,0 +1,62 @@ +#ifdef USE_LIBRETINY +#include "logger.h" + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + switch (this->uart_) { +#if LT_HW_UART0 + case UART_SELECTION_UART0: + this->hw_serial_ = &Serial0; + Serial0.begin(this->baud_rate_); + break; +#endif +#if LT_HW_UART1 + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + break; +#endif +#if LT_HW_UART2 + case UART_SELECTION_UART2: + this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); + break; +#endif + default: + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + if (this->uart_ != UART_SELECTION_DEFAULT) { + ESP_LOGW(TAG, " The chosen logger UART port is not available on this board." + "The default port was used instead."); + } + break; + } + + // change lt_log() port to match default Serial + if (this->uart_ == UART_SELECTION_DEFAULT) { + this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1); + lt_log_set_port(LT_UART_DEFAULT_SERIAL); + } else { + lt_log_set_port(this->uart_ - 1); + } + } + + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp new file mode 100644 index 0000000000..2783d77ac1 --- /dev/null +++ b/esphome/components/logger/logger_rp2040.cpp @@ -0,0 +1,39 @@ +#ifdef USE_RP2040 +#include "logger.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + switch (this->uart_) { + case UART_SELECTION_UART0: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + break; + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); + break; + case UART_SELECTION_USB_CDC: + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + break; + } + } + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; + +const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + +} // namespace logger +} // namespace esphome +#endif // USE_RP2040 diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index 959af68235..198d15ebd8 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -8,10 +8,27 @@ namespace ltr390 { static const char *const TAG = "ltr390"; +static const uint8_t LTR390_WAKEUP_TIME = 10; +static const uint8_t LTR390_SETTLE_TIME = 5; + +static const uint8_t LTR390_MAIN_CTRL = 0x00; +static const uint8_t LTR390_MEAS_RATE = 0x04; +static const uint8_t LTR390_GAIN = 0x05; +static const uint8_t LTR390_PART_ID = 0x06; +static const uint8_t LTR390_MAIN_STATUS = 0x07; + static const float GAINVALUES[5] = {1.0, 3.0, 6.0, 9.0, 18.0}; static const float RESOLUTIONVALUE[6] = {4.0, 2.0, 1.0, 0.5, 0.25, 0.125}; +static const uint8_t RESOLUTION_BITS[6] = {20, 19, 18, 17, 16, 13}; + +// Request fastest measurement rate - will be slowed by device if conversion rate is slower. +static const float RESOLUTION_SETTING[6] = {0x00, 0x10, 0x20, 0x30, 0x40, 0x50}; static const uint32_t MODEADDRESSES[2] = {0x0D, 0x10}; +static const float SENSITIVITY_MAX = 2300; +static const float INTG_MAX = RESOLUTIONVALUE[0] * 100; +static const int GAIN_MAX = GAINVALUES[4]; + uint32_t little_endian_bytes_to_int(const uint8_t *buffer, uint8_t num_bytes) { uint32_t value = 0; @@ -58,7 +75,7 @@ void LTR390Component::read_als_() { uint32_t als = *val; if (this->light_sensor_ != nullptr) { - float lux = (0.6 * als) / (GAINVALUES[this->gain_] * RESOLUTIONVALUE[this->res_]) * this->wfac_; + float lux = ((0.6 * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_; this->light_sensor_->publish_state(lux); } @@ -74,7 +91,7 @@ void LTR390Component::read_uvs_() { uint32_t uv = *val; if (this->uvi_sensor_ != nullptr) { - this->uvi_sensor_->publish_state(uv / LTR390_SENSITIVITY * this->wfac_); + this->uvi_sensor_->publish_state((uv / this->sensitivity_uv_) * this->wfac_); } if (this->uv_sensor_ != nullptr) { @@ -88,10 +105,26 @@ void LTR390Component::read_mode_(int mode_index) { std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); ctrl[LTR390_CTRL_MODE] = mode; + ctrl[LTR390_CTRL_EN] = true; this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); + uint32_t int_time{0}; + // Set gain, resolution and measurement rate + switch (mode) { + case LTR390_MODE_ALS: + this->reg(LTR390_GAIN) = this->gain_als_; + this->reg(LTR390_MEAS_RATE) = RESOLUTION_SETTING[this->res_als_]; + int_time = ((uint32_t) RESOLUTIONVALUE[this->res_als_]) * 100; + break; + case LTR390_MODE_UVS: + this->reg(LTR390_GAIN) = this->gain_uv_; + this->reg(LTR390_MEAS_RATE) = RESOLUTION_SETTING[this->res_uv_]; + int_time = ((uint32_t) RESOLUTIONVALUE[this->res_uv_]) * 100; + break; + } + // After the sensor integration time do the following - this->set_timeout(((uint32_t) RESOLUTIONVALUE[this->res_]) * 100, [this, mode_index]() { + 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])(); @@ -100,6 +133,10 @@ void LTR390Component::read_mode_(int mode_index) { if (mode_index + 1 < (int) this->mode_funcs_.size()) { this->read_mode_(mode_index + 1); } 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; } }); @@ -129,15 +166,10 @@ void LTR390Component::setup() { return; } - // Set gain - this->reg(LTR390_GAIN) = gain_; - - // Set resolution - uint8_t res = this->reg(LTR390_MEAS_RATE).get(); - // resolution is in bits 5-7 - res &= ~0b01110000; - res |= res << 4; - this->reg(LTR390_MEAS_RATE) = res; + // Set sensitivity by linearly scaling against known value in the datasheet + float gain_scale_uv = GAINVALUES[this->gain_uv_] / GAIN_MAX; + float intg_scale_uv = (RESOLUTIONVALUE[this->res_uv_] * 100) / INTG_MAX; + this->sensitivity_uv_ = SENSITIVITY_MAX * gain_scale_uv * intg_scale_uv; // Set sensor read state this->reading_ = false; @@ -153,7 +185,13 @@ void LTR390Component::setup() { } } -void LTR390Component::dump_config() { LOG_I2C_DEVICE(this); } +void LTR390Component::dump_config() { + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " ALS Gain: X%.0f", GAINVALUES[this->gain_als_]); + ESP_LOGCONFIG(TAG, " ALS Resolution: %u-bit", RESOLUTION_BITS[this->res_als_]); + ESP_LOGCONFIG(TAG, " UV Gain: X%.0f", GAINVALUES[this->gain_uv_]); + ESP_LOGCONFIG(TAG, " UV Resolution: %u-bit", RESOLUTION_BITS[this->res_uv_]); +} void LTR390Component::update() { if (!this->reading_ && !mode_funcs_.empty()) { diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 1bb7a8fa22..24afd3c411 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -17,14 +17,6 @@ enum LTR390CTRL { }; // enums from https://github.com/adafruit/Adafruit_LTR390/ - -static const uint8_t LTR390_MAIN_CTRL = 0x00; -static const uint8_t LTR390_MEAS_RATE = 0x04; -static const uint8_t LTR390_GAIN = 0x05; -static const uint8_t LTR390_PART_ID = 0x06; -static const uint8_t LTR390_MAIN_STATUS = 0x07; -static const float LTR390_SENSITIVITY = 2300.0; - // Sensing modes enum LTR390MODE { LTR390_MODE_ALS, @@ -57,8 +49,10 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - void set_gain_value(LTR390GAIN gain) { this->gain_ = gain; } - void set_res_value(LTR390RESOLUTION res) { this->res_ = res; } + void set_als_gain_value(LTR390GAIN gain) { this->gain_als_ = gain; } + void set_uv_gain_value(LTR390GAIN gain) { this->gain_uv_ = gain; } + void set_als_res_value(LTR390RESOLUTION res) { this->res_als_ = res; } + void set_uv_res_value(LTR390RESOLUTION res) { this->res_uv_ = res; } void set_wfac_value(float wfac) { this->wfac_ = wfac; } void set_light_sensor(sensor::Sensor *light_sensor) { this->light_sensor_ = light_sensor; } @@ -79,8 +73,11 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice { // a list of modes and corresponding read functions std::vector>> mode_funcs_; - LTR390GAIN gain_; - LTR390RESOLUTION res_; + LTR390GAIN gain_als_; + LTR390GAIN gain_uv_; + LTR390RESOLUTION res_als_; + LTR390RESOLUTION res_uv_; + float sensitivity_uv_; float wfac_; sensor::Sensor *light_sensor_{nullptr}; diff --git a/esphome/components/ltr390/sensor.py b/esphome/components/ltr390/sensor.py index 0a765dbe3d..62c3edf8cb 100644 --- a/esphome/components/ltr390/sensor.py +++ b/esphome/components/ltr390/sensor.py @@ -2,16 +2,18 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, + CONF_AMBIENT_LIGHT, CONF_GAIN, + CONF_ID, CONF_LIGHT, CONF_RESOLUTION, - UNIT_LUX, - ICON_BRIGHTNESS_5, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, + ICON_BRIGHTNESS_5, + UNIT_LUX, ) -CODEOWNERS = ["@sjtrny"] +CODEOWNERS = ["@sjtrny", "@latonita"] DEPENDENCIES = ["i2c"] ltr390_ns = cg.esphome_ns.namespace("ltr390") @@ -20,7 +22,6 @@ LTR390Component = ltr390_ns.class_( "LTR390Component", cg.PollingComponent, i2c.I2CDevice ) -CONF_AMBIENT_LIGHT = "ambient_light" CONF_UV_INDEX = "uv_index" CONF_UV = "uv" CONF_WINDOW_CORRECTION_FACTOR = "window_correction_factor" @@ -61,22 +62,38 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, ), cv.Optional(CONF_UV_INDEX): sensor.sensor_schema( unit_of_measurement=UNIT_UVI, icon=ICON_BRIGHTNESS_5, accuracy_decimals=5, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, ), cv.Optional(CONF_UV): sensor.sensor_schema( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_GAIN, default="X18"): cv.Any( + cv.enum(GAIN_OPTIONS), + cv.Schema( + { + cv.Required(CONF_AMBIENT_LIGHT): cv.enum(GAIN_OPTIONS), + cv.Required(CONF_UV): cv.enum(GAIN_OPTIONS), + } + ), + ), + cv.Optional(CONF_RESOLUTION, default=20): cv.Any( + cv.enum(RES_OPTIONS), + cv.Schema( + { + cv.Required(CONF_AMBIENT_LIGHT): cv.enum(RES_OPTIONS), + cv.Required(CONF_UV): cv.enum(RES_OPTIONS), + } + ), ), - cv.Optional(CONF_GAIN, default="X3"): cv.enum(GAIN_OPTIONS), - cv.Optional(CONF_RESOLUTION, default=18): cv.enum(RES_OPTIONS), cv.Optional(CONF_WINDOW_CORRECTION_FACTOR, default=1.0): cv.float_range( min=1.0 ), @@ -100,11 +117,25 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - cg.add(var.set_gain_value(config[CONF_GAIN])) - cg.add(var.set_res_value(config[CONF_RESOLUTION])) cg.add(var.set_wfac_value(config[CONF_WINDOW_CORRECTION_FACTOR])) for key, funcName in TYPES.items(): if key in config: sens = await sensor.new_sensor(config[key]) cg.add(getattr(var, funcName)(sens)) + + gain_value = config[CONF_GAIN] + if isinstance(gain_value, dict): + cg.add(var.set_als_gain_value(gain_value[CONF_AMBIENT_LIGHT])) + cg.add(var.set_uv_gain_value(gain_value[CONF_UV])) + else: + cg.add(var.set_als_gain_value(gain_value)) + cg.add(var.set_uv_gain_value(gain_value)) + + res_value = config[CONF_RESOLUTION] + if isinstance(res_value, dict): + cg.add(var.set_als_res_value(res_value[CONF_AMBIENT_LIGHT])) + cg.add(var.set_uv_res_value(res_value[CONF_UV])) + else: + cg.add(var.set_als_res_value(res_value)) + cg.add(var.set_uv_res_value(res_value)) diff --git a/esphome/components/ltr501/__init__.py b/esphome/components/ltr501/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/ltr501/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp new file mode 100644 index 0000000000..4f4e26f44f --- /dev/null +++ b/esphome/components/ltr501/ltr501.cpp @@ -0,0 +1,542 @@ +#include "ltr501.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +using esphome::i2c::ErrorCode; + +namespace esphome { +namespace ltr501 { + +static const char *const TAG = "ltr501"; + +static const uint8_t MAX_TRIES = 5; +static const uint8_t MAX_SENSITIVITY_ADJUSTMENTS = 10; + +struct GainTimePair { + AlsGain501 gain; + IntegrationTime501 time; +}; + +bool operator==(const GainTimePair &lhs, const GainTimePair &rhs) { + return lhs.gain == rhs.gain && lhs.time == rhs.time; +} + +bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) { + return !(lhs.gain == rhs.gain && lhs.time == rhs.time); +} + +template T get_next(const T (&array)[size], const T val) { + size_t i = 0; + size_t idx = -1; + while (idx == -1 && i < size) { + if (array[i] == val) { + idx = i; + break; + } + i++; + } + if (idx == -1 || i + 1 >= size) + return val; + return array[i + 1]; +} + +template T get_prev(const T (&array)[size], const T val) { + size_t i = size - 1; + size_t idx = -1; + while (idx == -1 && i > 0) { + if (array[i] == val) { + idx = i; + break; + } + i--; + } + if (idx == -1 || i == 0) + return val; + return array[i - 1]; +} + +static uint16_t get_itime_ms(IntegrationTime501 time) { + static const uint16_t ALS_INT_TIME[4] = {100, 50, 200, 400}; + return ALS_INT_TIME[time & 0b11]; +} + +static uint16_t get_meas_time_ms(MeasurementRepeatRate rate) { + static const uint16_t ALS_MEAS_RATE[8] = {50, 100, 200, 500, 1000, 2000, 2000, 2000}; + return ALS_MEAS_RATE[rate & 0b111]; +} + +static float get_gain_coeff(AlsGain501 gain) { return gain == AlsGain501::GAIN_1 ? 1.0f : 150.0f; } + +static float get_ps_gain_coeff(PsGain501 gain) { + static const float PS_GAIN[4] = {1, 4, 8, 16}; + return PS_GAIN[gain & 0b11]; +} + +void LTRAlsPs501Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up LTR-501/301/558"); + // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive + this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; }); +} + +void LTRAlsPs501Component::dump_config() { + auto get_device_type = [](LtrType typ) { + switch (typ) { + case LtrType::LTR_TYPE_ALS_ONLY: + return "ALS only"; + case LtrType::LTR_TYPE_PS_ONLY: + return "PS only"; + case LtrType::LTR_TYPE_ALS_AND_PS: + return "Als + PS"; + default: + return "Unknown"; + } + }; + + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Device type: %s", get_device_type(this->ltr_type_)); + ESP_LOGCONFIG(TAG, " Automatic mode: %s", ONOFF(this->automatic_mode_enabled_)); + ESP_LOGCONFIG(TAG, " Gain: %.0fx", get_gain_coeff(this->gain_)); + ESP_LOGCONFIG(TAG, " Integration time: %d ms", get_itime_ms(this->integration_time_)); + ESP_LOGCONFIG(TAG, " Measurement repeat rate: %d ms", get_meas_time_ms(this->repeat_rate_)); + ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_); + ESP_LOGCONFIG(TAG, " Proximity gain: %.0fx", get_ps_gain_coeff(this->ps_gain_)); + ESP_LOGCONFIG(TAG, " Proximity cooldown time: %d s", this->ps_cooldown_time_s_); + ESP_LOGCONFIG(TAG, " Proximity high threshold: %d", this->ps_threshold_high_); + ESP_LOGCONFIG(TAG, " Proximity low threshold: %d", this->ps_threshold_low_); + + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "ALS calculated lux", this->ambient_light_sensor_); + LOG_SENSOR(" ", "CH1 Infrared counts", this->infrared_counts_sensor_); + LOG_SENSOR(" ", "CH0 Visible+IR counts", this->full_spectrum_counts_sensor_); + LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with I2C LTR-501/301/558 failed!"); + } +} + +void LTRAlsPs501Component::update() { + if (!this->is_als_()) { + ESP_LOGW(TAG, "Update. ALS data not available. Change configuration to ALS or ALS_PS."); + return; + } + if (this->is_ready() && this->is_als_() && this->state_ == State::IDLE) { + ESP_LOGV(TAG, "Update. Initiating new ALS data collection."); + + this->state_ = this->automatic_mode_enabled_ ? State::COLLECTING_DATA_AUTO : State::WAITING_FOR_DATA; + + this->als_readings_.ch0 = 0; + this->als_readings_.ch1 = 0; + this->als_readings_.gain = this->gain_; + this->als_readings_.integration_time = this->integration_time_; + this->als_readings_.lux = 0; + this->als_readings_.number_of_adjustments = 0; + + } else { + ESP_LOGV(TAG, "Update. Component not ready yet."); + } +} + +void LTRAlsPs501Component::loop() { + ErrorCode err = i2c::ERROR_OK; + static uint8_t tries{0}; + + switch (this->state_) { + case State::DELAYED_SETUP: + err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "i2c connection failed"); + this->mark_failed(); + } + this->configure_reset_(); + if (this->is_als_()) { + this->configure_als_(); + this->configure_integration_time_(this->integration_time_); + } + if (this->is_ps_()) { + this->configure_ps_(); + } + + this->state_ = State::IDLE; + break; + + case State::IDLE: + if (this->is_ps_()) { + this->check_and_trigger_ps_(); + } + break; + + case State::WAITING_FOR_DATA: + if (this->is_als_data_ready_(this->als_readings_) == DataAvail::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)); + this->read_sensor_data_(this->als_readings_); + this->apply_lux_calculation_(this->als_readings_); + this->state_ = State::DATA_COLLECTED; + } else if (tries >= MAX_TRIES) { + ESP_LOGW(TAG, "Can't get data after several tries. Aborting."); + tries = 0; + this->status_set_warning(); + this->state_ = State::IDLE; + return; + } else { + tries++; + } + break; + + case State::COLLECTING_DATA_AUTO: + case State::DATA_COLLECTED: + // first measurement in auto mode (COLLECTING_DATA_AUTO state) require device reconfiguration + if (this->state_ == State::COLLECTING_DATA_AUTO || this->are_adjustments_required_(this->als_readings_)) { + this->state_ = State::ADJUSTMENT_IN_PROGRESS; + ESP_LOGD(TAG, "Reconfiguring sensitivity: gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), + get_itime_ms(this->als_readings_.integration_time)); + this->configure_integration_time_(this->als_readings_.integration_time); + this->configure_gain_(this->als_readings_.gain); + // if sensitivity adjustment needed - need to wait for first data samples after setting new parameters + this->set_timeout(2 * get_meas_time_ms(this->repeat_rate_), + [this]() { this->state_ = State::WAITING_FOR_DATA; }); + } else { + this->state_ = State::READY_TO_PUBLISH; + } + break; + + case State::ADJUSTMENT_IN_PROGRESS: + // nothing to be done, just waiting for the timeout + break; + + case State::READY_TO_PUBLISH: + this->publish_data_part_1_(this->als_readings_); + this->state_ = State::KEEP_PUBLISHING; + break; + + case State::KEEP_PUBLISHING: + this->publish_data_part_2_(this->als_readings_); + this->status_clear_warning(); + this->state_ = State::IDLE; + break; + + default: + break; + } +} + +void LTRAlsPs501Component::check_and_trigger_ps_() { + static uint32_t last_high_trigger_time{0}; + static uint32_t last_low_trigger_time{0}; + uint16_t ps_data = this->read_ps_data_(); + uint32_t now = millis(); + + if (ps_data != this->ps_readings_) { + this->ps_readings_ = ps_data; + // Higher values - object is closer to sensor + if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { + last_high_trigger_time = now; + ESP_LOGD(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, + this->ps_threshold_high_); + this->on_ps_high_trigger_callback_.call(); + } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { + last_low_trigger_time = now; + ESP_LOGD(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, + this->ps_threshold_low_); + this->on_ps_low_trigger_callback_.call(); + } + } +} + +bool LTRAlsPs501Component::check_part_number_() { + uint8_t manuf_id = this->reg((uint8_t) CommandRegisters::MANUFAC_ID).get(); + if (manuf_id != 0x05) { // 0x05 is Lite-On Semiconductor Corp. ID + ESP_LOGW(TAG, "Unknown manufacturer ID: 0x%02X", manuf_id); + this->mark_failed(); + return false; + } + + // Things getting not really funny here, we can't identify device type by part number ID + // ======================== ========= ===== ================= + // Device Part ID Rev Capabilities + // ======================== ========= ===== ================= + // ltr-558als 0x08 0 als + ps + // ltr-501als 0x08 0 als + ps + // ltr-301als - 0x08 0 als only + + PartIdRegister part_id{0}; + part_id.raw = this->reg((uint8_t) CommandRegisters::PART_ID).get(); + if (part_id.part_number_id != 0x08) { + ESP_LOGW(TAG, "Unknown part number ID: 0x%02X. LTR-501/301 shall have 0x08. It might not work properly.", + part_id.part_number_id); + this->status_set_warning(); + return true; + } + return true; +} + +void LTRAlsPs501Component::configure_reset_() { + ESP_LOGV(TAG, "Resetting"); + + AlsControlRegister501 als_ctrl{0}; + als_ctrl.sw_reset = true; + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + + uint8_t tries = MAX_TRIES; + do { + ESP_LOGV(TAG, "Waiting chip to reset"); + delay(2); + als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + } while (als_ctrl.sw_reset && tries--); // while sw reset bit is on - keep waiting + + if (als_ctrl.sw_reset) { + ESP_LOGW(TAG, "Reset failed"); + } +} + +void LTRAlsPs501Component::configure_als_() { + AlsControlRegister501 als_ctrl{0}; + als_ctrl.sw_reset = false; + als_ctrl.als_mode_active = true; + als_ctrl.gain = this->gain_; + + ESP_LOGV(TAG, "Setting active mode and gain reg 0x%02X", als_ctrl.raw); + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(5); + + uint8_t tries = MAX_TRIES; + do { + ESP_LOGV(TAG, "Waiting for ALS device to become active..."); + delay(2); + als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + } while (!als_ctrl.als_mode_active && tries--); // while active mode is not set - keep waiting + + if (!als_ctrl.als_mode_active) { + ESP_LOGW(TAG, "Failed to activate ALS device"); + } +} + +void LTRAlsPs501Component::configure_ps_() { + PsMeasurementRateRegister ps_meas{0}; + ps_meas.ps_measurement_rate = PsMeasurementRate::PS_MEAS_RATE_50MS; + this->reg((uint8_t) CommandRegisters::PS_MEAS_RATE) = ps_meas.raw; + + PsControlRegister501 ps_ctrl{0}; + ps_ctrl.ps_mode_active = true; + ps_ctrl.ps_mode_xxx = true; + this->reg((uint8_t) CommandRegisters::PS_CONTR) = ps_ctrl.raw; +} + +uint16_t LTRAlsPs501Component::read_ps_data_() { + AlsPsStatusRegister als_status{0}; + als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); + if (!als_status.ps_new_data) { + return this->ps_readings_; + } + + uint8_t ps_low = this->reg((uint8_t) CommandRegisters::PS_DATA_0).get(); + PsData1Register ps_high; + ps_high.raw = this->reg((uint8_t) CommandRegisters::PS_DATA_1).get(); + + uint16_t val = encode_uint16(ps_high.ps_data_high, ps_low); + return val; +} + +void LTRAlsPs501Component::configure_gain_(AlsGain501 gain) { + AlsControlRegister501 als_ctrl{0}; + als_ctrl.als_mode_active = true; + als_ctrl.gain = gain; + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + + AlsControlRegister501 read_als_ctrl{0}; + read_als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + if (read_als_ctrl.gain != gain) { + ESP_LOGW(TAG, "Failed to set gain. We will try one more time."); + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + } +} + +void LTRAlsPs501Component::configure_integration_time_(IntegrationTime501 time) { + MeasurementRateRegister501 meas{0}; + meas.measurement_repeat_rate = this->repeat_rate_; + meas.integration_time = time; + this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw; + delay(2); + + MeasurementRateRegister501 read_meas{0}; + read_meas.raw = this->reg((uint8_t) CommandRegisters::MEAS_RATE).get(); + if (read_meas.integration_time != time) { + ESP_LOGW(TAG, "Failed to set integration time. We will try one more time."); + this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw; + delay(2); + } +} + +DataAvail 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; + 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; + } + data.gain = als_status.gain; + return DataAvail::DATA_OK; +} + +void LTRAlsPs501Component::read_sensor_data_(AlsReadings &data) { + data.ch1 = 0; + data.ch0 = 0; + uint8_t ch1_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_0).get(); + uint8_t ch1_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_1).get(); + uint8_t ch0_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_0).get(); + uint8_t ch0_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_1).get(); + data.ch1 = encode_uint16(ch1_1, ch1_0); + data.ch0 = encode_uint16(ch0_1, ch0_0); + + ESP_LOGD(TAG, "Got sensor data: CH1 = %d, CH0 = %d", data.ch1, data.ch0); +} + +bool LTRAlsPs501Component::are_adjustments_required_(AlsReadings &data) { + if (!this->automatic_mode_enabled_) + return false; + + // sometimes sensors fail to change sensitivity. this prevents us from infinite loop + if (data.number_of_adjustments++ > MAX_SENSITIVITY_ADJUSTMENTS) { + ESP_LOGW(TAG, "Too many sensitivity adjustments done. Something wrong with the sensor. Stopping."); + return false; + } + + ESP_LOGV(TAG, "Adjusting sensitivity, run #%d", data.number_of_adjustments); + + // available combinations of gain and integration times: + static const GainTimePair GAIN_TIME_PAIRS[] = { + {AlsGain501::GAIN_1, INTEGRATION_TIME_50MS}, {AlsGain501::GAIN_1, INTEGRATION_TIME_100MS}, + {AlsGain501::GAIN_150, INTEGRATION_TIME_100MS}, {AlsGain501::GAIN_150, INTEGRATION_TIME_200MS}, + {AlsGain501::GAIN_150, INTEGRATION_TIME_400MS}, + }; + + GainTimePair current_pair = {data.gain, data.integration_time}; + + // Here comes funky business with this sensor. it has no internal error checking mechanism + // as in later versions (LTR-303/329/559/..) and sensor gets overwhelmed when saturated + // and readings are strange. We only check high sensitivity mode for now. + // Nothing is documented and it is a result of real-world testing. + if (data.gain == AlsGain501::GAIN_150) { + // when sensor is saturated it returns various crazy numbers + // CH1 = 1, CH0 = 0 + if (data.ch1 == 1 && data.ch0 == 0) { + ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = 1, CH0 = 0, Gain 150x"); + // fake saturation + data.ch0 = 0xffff; + data.ch1 = 0xffff; + } else if (data.ch1 == 65535 && data.ch0 == 0) { + ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = 65535, CH0 = 0, Gain 150x"); + data.ch0 = 0xffff; + } else if (data.ch1 > 1000 && data.ch0 == 0) { + ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = %d, CH0 = 0, Gain 150x", data.ch1); + data.ch0 = 0xffff; + } + } + + static const uint16_t LOW_INTENSITY_THRESHOLD_1 = 100; + static const uint16_t LOW_INTENSITY_THRESHOLD_200 = 2000; + static const uint16_t HIGH_INTENSITY_THRESHOLD = 25000; + + if (data.ch0 <= (data.gain == AlsGain501::GAIN_1 ? LOW_INTENSITY_THRESHOLD_1 : LOW_INTENSITY_THRESHOLD_200) || + (data.gain == AlsGain501::GAIN_1 && data.lux < 320)) { + GainTimePair next_pair = get_next(GAIN_TIME_PAIRS, current_pair); + if (next_pair != current_pair) { + data.gain = next_pair.gain; + data.integration_time = next_pair.time; + ESP_LOGV(TAG, "Low illuminance. Increasing sensitivity."); + return true; + } + + } else if (data.ch0 >= HIGH_INTENSITY_THRESHOLD || data.ch1 >= HIGH_INTENSITY_THRESHOLD) { + GainTimePair prev_pair = get_prev(GAIN_TIME_PAIRS, current_pair); + if (prev_pair != current_pair) { + data.gain = prev_pair.gain; + data.integration_time = prev_pair.time; + ESP_LOGV(TAG, "High illuminance. Decreasing sensitivity."); + return true; + } + } else { + ESP_LOGD(TAG, "Illuminance is good enough."); + return false; + } + ESP_LOGD(TAG, "Can't adjust sensitivity anymore."); + return false; +} + +void LTRAlsPs501Component::apply_lux_calculation_(AlsReadings &data) { + if ((data.ch0 == 0xFFFF) || (data.ch1 == 0xFFFF)) { + ESP_LOGW(TAG, "Sensors got saturated"); + data.lux = 0.0f; + return; + } + + if ((data.ch0 == 0x0000) && (data.ch1 == 0x0000)) { + ESP_LOGW(TAG, "Sensors blacked out"); + data.lux = 0.0f; + return; + } + + float ch0 = data.ch0; + float ch1 = data.ch1; + float ratio = ch1 / (ch0 + ch1); + float als_gain = get_gain_coeff(data.gain); + float als_time = ((float) get_itime_ms(data.integration_time)) / 100.0f; + float inv_pfactor = this->glass_attenuation_factor_; + float lux = 0.0f; + + // method from + // https://github.com/fards/Ainol_fire_kernel/blob/83832cf8a3082fd8e963230f4b1984479d1f1a84/customer/drivers/lightsensor/ltr501als.c#L295 + + if (ratio < 0.45) { + lux = 1.7743 * ch0 + 1.1059 * ch1; + } else if (ratio < 0.64) { + lux = 3.7725 * ch0 - 1.3363 * ch1; + } else if (ratio < 0.85) { + lux = 1.6903 * ch0 - 0.1693 * ch1; + } else { + ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio"); + lux = 0.0f; + } + + lux = inv_pfactor * lux / als_gain / als_time; + data.lux = lux; + + ESP_LOGD(TAG, "Lux calculation: ratio %.3f, gain %.0fx, int time %.1f, inv_pfactor %.3f, lux %.3f", ratio, als_gain, + als_time, inv_pfactor, lux); +} + +void LTRAlsPs501Component::publish_data_part_1_(AlsReadings &data) { + if (this->proximity_counts_sensor_ != nullptr) { + this->proximity_counts_sensor_->publish_state(this->ps_readings_); + } + if (this->ambient_light_sensor_ != nullptr) { + this->ambient_light_sensor_->publish_state(data.lux); + } + if (this->infrared_counts_sensor_ != nullptr) { + this->infrared_counts_sensor_->publish_state(data.ch1); + } + if (this->full_spectrum_counts_sensor_ != nullptr) { + this->full_spectrum_counts_sensor_->publish_state(data.ch0); + } +} + +void LTRAlsPs501Component::publish_data_part_2_(AlsReadings &data) { + if (this->actual_gain_sensor_ != nullptr) { + this->actual_gain_sensor_->publish_state(get_gain_coeff(data.gain)); + } + if (this->actual_integration_time_sensor_ != nullptr) { + this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.integration_time)); + } +} +} // namespace ltr501 +} // namespace esphome diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h new file mode 100644 index 0000000000..07b69fa0d0 --- /dev/null +++ b/esphome/components/ltr501/ltr501.h @@ -0,0 +1,184 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "esphome/core/automation.h" + +#include "ltr_definitions_501.h" + +namespace esphome { +namespace ltr501 { + +enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; + +enum LtrType : uint8_t { + LTR_TYPE_UNKNOWN = 0, + LTR_TYPE_ALS_ONLY = 1, + LTR_TYPE_PS_ONLY = 2, + LTR_TYPE_ALS_AND_PS = 3, +}; + +class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { + public: + // + // EspHome framework functions + // + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + void loop() override; + + // Configuration setters : General + // + void set_ltr_type(LtrType type) { this->ltr_type_ = type; } + + // Configuration setters : ALS + // + void set_als_auto_mode(bool enable) { this->automatic_mode_enabled_ = enable; } + void set_als_gain(AlsGain501 gain) { this->gain_ = gain; } + void set_als_integration_time(IntegrationTime501 time) { this->integration_time_ = time; } + void set_als_meas_repeat_rate(MeasurementRepeatRate rate) { this->repeat_rate_ = rate; } + void set_als_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; } + + // Configuration setters : PS + // + void set_ps_high_threshold(uint16_t threshold) { this->ps_threshold_high_ = threshold; } + void set_ps_low_threshold(uint16_t threshold) { this->ps_threshold_low_ = threshold; } + void set_ps_cooldown_time_s(uint16_t time) { this->ps_cooldown_time_s_ = time; } + void set_ps_gain(PsGain501 gain) { this->ps_gain_ = gain; } + + // Sensors setters + // + void set_ambient_light_sensor(sensor::Sensor *sensor) { this->ambient_light_sensor_ = sensor; } + void set_full_spectrum_counts_sensor(sensor::Sensor *sensor) { this->full_spectrum_counts_sensor_ = sensor; } + void set_infrared_counts_sensor(sensor::Sensor *sensor) { this->infrared_counts_sensor_ = sensor; } + void set_actual_gain_sensor(sensor::Sensor *sensor) { this->actual_gain_sensor_ = sensor; } + void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } + void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + + protected: + // + // Internal state machine, used to split all the actions into + // small steps in loop() to make sure we are not blocking execution + // + enum class State : uint8_t { + NOT_INITIALIZED, + DELAYED_SETUP, + IDLE, + WAITING_FOR_DATA, + COLLECTING_DATA_AUTO, + DATA_COLLECTED, + ADJUSTMENT_IN_PROGRESS, + READY_TO_PUBLISH, + KEEP_PUBLISHING + } state_{State::NOT_INITIALIZED}; + + LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY}; + + // + // Current measurements data + // + struct AlsReadings { + uint16_t ch0{0}; + uint16_t ch1{0}; + AlsGain501 gain{AlsGain501::GAIN_1}; + IntegrationTime501 integration_time{IntegrationTime501::INTEGRATION_TIME_100MS}; + float lux{0.0f}; + uint8_t number_of_adjustments{0}; + } als_readings_; + uint16_t ps_readings_{0xfffe}; + + inline bool is_als_() const { + return this->ltr_type_ == LtrType::LTR_TYPE_ALS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS; + } + inline bool is_ps_() const { + return this->ltr_type_ == LtrType::LTR_TYPE_PS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS; + } + + // + // Device interaction and data manipulation + // + bool check_part_number_(); + + void configure_reset_(); + void configure_als_(); + void configure_integration_time_(IntegrationTime501 time); + void configure_gain_(AlsGain501 gain); + DataAvail is_als_data_ready_(AlsReadings &data); + void read_sensor_data_(AlsReadings &data); + bool are_adjustments_required_(AlsReadings &data); + void apply_lux_calculation_(AlsReadings &data); + void publish_data_part_1_(AlsReadings &data); + void publish_data_part_2_(AlsReadings &data); + + void configure_ps_(); + uint16_t read_ps_data_(); + void check_and_trigger_ps_(); + + // + // Component configuration + // + bool automatic_mode_enabled_{false}; + AlsGain501 gain_{AlsGain501::GAIN_1}; + IntegrationTime501 integration_time_{IntegrationTime501::INTEGRATION_TIME_100MS}; + MeasurementRepeatRate repeat_rate_{MeasurementRepeatRate::REPEAT_RATE_500MS}; + float glass_attenuation_factor_{1.0}; + + uint16_t ps_cooldown_time_s_{5}; + PsGain501 ps_gain_{PsGain501::PS_GAIN_1}; + uint16_t ps_threshold_high_{0xffff}; + uint16_t ps_threshold_low_{0x0000}; + + // + // Sensors for publishing data + // + sensor::Sensor *infrared_counts_sensor_{nullptr}; // direct reading CH1, infrared only + sensor::Sensor *full_spectrum_counts_sensor_{nullptr}; // direct reading CH0, infrared + visible light + sensor::Sensor *ambient_light_sensor_{nullptr}; // calculated lux + sensor::Sensor *actual_gain_sensor_{nullptr}; // actual gain of reading + sensor::Sensor *actual_integration_time_sensor_{nullptr}; // actual integration time + sensor::Sensor *proximity_counts_sensor_{nullptr}; // proximity sensor + + bool is_any_als_sensor_enabled_() const { + return this->ambient_light_sensor_ != nullptr || this->full_spectrum_counts_sensor_ != nullptr || + this->infrared_counts_sensor_ != nullptr || this->actual_gain_sensor_ != nullptr || + this->actual_integration_time_sensor_ != nullptr; + } + bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } + + // + // Trigger section for the automations + // + friend class LTRPsHighTrigger; + friend class LTRPsLowTrigger; + + CallbackManager on_ps_high_trigger_callback_; + CallbackManager on_ps_low_trigger_callback_; + + void add_on_ps_high_trigger_callback_(std::function callback) { + this->on_ps_high_trigger_callback_.add(std::move(callback)); + } + + void add_on_ps_low_trigger_callback_(std::function callback) { + this->on_ps_low_trigger_callback_.add(std::move(callback)); + } +}; + +class LTRPsHighTrigger : public Trigger<> { + public: + explicit LTRPsHighTrigger(LTRAlsPs501Component *parent) { + parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); + } +}; + +class LTRPsLowTrigger : public Trigger<> { + public: + explicit LTRPsLowTrigger(LTRAlsPs501Component *parent) { + parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); + } +}; +} // namespace ltr501 +} // namespace esphome diff --git a/esphome/components/ltr501/ltr_definitions_501.h b/esphome/components/ltr501/ltr_definitions_501.h new file mode 100644 index 0000000000..604bd92b68 --- /dev/null +++ b/esphome/components/ltr501/ltr_definitions_501.h @@ -0,0 +1,260 @@ +#pragma once + +#include + +namespace esphome { +namespace ltr501 { + +enum class CommandRegisters : uint8_t { + ALS_CONTR = 0x80, // ALS operation mode control and SW reset + PS_CONTR = 0x81, // PS operation mode control + PS_LED = 0x82, // PS LED pulse frequency control + PS_N_PULSES = 0x83, // PS number of pulses control + PS_MEAS_RATE = 0x84, // PS measurement rate in active mode + MEAS_RATE = 0x85, // ALS measurement rate in active mode + PART_ID = 0x86, // Part Number ID and Revision ID + MANUFAC_ID = 0x87, // Manufacturer ID + ALS_DATA_CH1_0 = 0x88, // ALS measurement CH1 data, lower byte - infrared only + ALS_DATA_CH1_1 = 0x89, // ALS measurement CH1 data, upper byte - infrared only + ALS_DATA_CH0_0 = 0x8A, // ALS measurement CH0 data, lower byte - visible + infrared + ALS_DATA_CH0_1 = 0x8B, // ALS measurement CH0 data, upper byte - visible + infrared + ALS_PS_STATUS = 0x8C, // ALS PS new data status + PS_DATA_0 = 0x8D, // PS measurement data, lower byte + PS_DATA_1 = 0x8E, // PS measurement data, upper byte + ALS_PS_INTERRUPT = 0x8F, // Interrupt status + PS_THRES_UP_0 = 0x90, // PS interrupt upper threshold, lower byte + PS_THRES_UP_1 = 0x91, // PS interrupt upper threshold, upper byte + PS_THRES_LOW_0 = 0x92, // PS interrupt lower threshold, lower byte + PS_THRES_LOW_1 = 0x93, // PS interrupt lower threshold, upper byte + PS_OFFSET_1 = 0x94, // PS offset, upper byte + PS_OFFSET_0 = 0x95, // PS offset, lower byte + // 0x96 - reserved + ALS_THRES_UP_0 = 0x97, // ALS interrupt upper threshold, lower byte + ALS_THRES_UP_1 = 0x98, // ALS interrupt upper threshold, upper byte + ALS_THRES_LOW_0 = 0x99, // ALS interrupt lower threshold, lower byte + ALS_THRES_LOW_1 = 0x9A, // ALS interrupt lower threshold, upper byte + // 0x9B - reserved + // 0x9C - reserved + // 0x9D - reserved + INTERRUPT_PERSIST = 0x9E // Interrupt persistence filter +}; + +// ALS Sensor gain levels +enum AlsGain501 : uint8_t { + GAIN_1 = 0, // GAIN_RANGE_2 // default + GAIN_150 = 1, // GAIN_RANGE_1 +}; +static const uint8_t GAINS_COUNT = 2; + +// ALS Sensor integration times +enum IntegrationTime501 : uint8_t { + INTEGRATION_TIME_100MS = 0, // default + INTEGRATION_TIME_50MS = 1, // only in Dynamic GAIN_RANGE_2 + INTEGRATION_TIME_200MS = 2, // only in Dynamic GAIN_RANGE_1 + INTEGRATION_TIME_400MS = 3, // only in Dynamic GAIN_RANGE_1 +}; +static const uint8_t TIMES_COUNT = 4; + +// ALS Sensor measurement repeat rate +enum MeasurementRepeatRate { + REPEAT_RATE_50MS = 0, + REPEAT_RATE_100MS = 1, + REPEAT_RATE_200MS = 2, + REPEAT_RATE_500MS = 3, // default + REPEAT_RATE_1000MS = 4, + REPEAT_RATE_2000MS = 5 +}; + +// PS Sensor gain levels +enum PsGain501 : uint8_t { + PS_GAIN_1 = 0, // default + PS_GAIN_4 = 1, + PS_GAIN_8 = 2, + PS_GAIN_16 = 3, +}; + +// LED Pulse Modulation Frequency +enum PsLedFreq : uint8_t { + PS_LED_FREQ_30KHZ = 0, + PS_LED_FREQ_40KHZ = 1, + PS_LED_FREQ_50KHZ = 2, + PS_LED_FREQ_60KHZ = 3, // default + PS_LED_FREQ_70KHZ = 4, + PS_LED_FREQ_80KHZ = 5, + PS_LED_FREQ_90KHZ = 6, + PS_LED_FREQ_100KHZ = 7, +}; + +// LED current duty +enum PsLedDuty : uint8_t { + PS_LED_DUTY_25 = 0, + PS_LED_DUTY_50 = 1, // default + PS_LED_DUTY_75 = 2, + PS_LED_DUTY_100 = 3, +}; + +// LED pulsed current level +enum PsLedCurrent : uint8_t { + PS_LED_CURRENT_5MA = 0, + PS_LED_CURRENT_10MA = 1, + PS_LED_CURRENT_20MA = 2, + PS_LED_CURRENT_50MA = 3, // default + PS_LED_CURRENT_100MA = 4, + PS_LED_CURRENT_100MA1 = 5, + PS_LED_CURRENT_100MA2 = 6, + PS_LED_CURRENT_100MA3 = 7, +}; + +// PS measurement rate +enum PsMeasurementRate : uint8_t { + PS_MEAS_RATE_50MS = 0, + PS_MEAS_RATE_70MS = 1, + PS_MEAS_RATE_100MS = 2, // default + PS_MEAS_RATE_200MS = 3, + PS_MEAS_RATE_500MS = 4, + PS_MEAS_RATE_1000MS = 5, + PS_MEAS_RATE_2000MS = 6, + PS_MEAS_RATE_2000MS1 = 7, +}; + +// +// ALS_CONTR Register (0x80) +// +union AlsControlRegister501 { + uint8_t raw; + struct { + bool asl_mode_xxx : 1; + bool als_mode_active : 1; + bool sw_reset : 1; + AlsGain501 gain : 1; + uint8_t reserved : 4; + } __attribute__((packed)); +}; + +// +// PS_CONTR Register (0x81) +// +union PsControlRegister501 { + uint8_t raw; + struct { + bool ps_mode_xxx : 1; + bool ps_mode_active : 1; + PsGain501 ps_gain : 2; + bool reserved_4 : 1; + bool reserved_5 : 1; + bool reserved_6 : 1; + bool reserved_7 : 1; + } __attribute__((packed)); +}; + +// +// PS_LED Register (0x82) +// +union PsLedRegister { + uint8_t raw; + struct { + PsLedCurrent ps_led_current : 3; + PsLedDuty ps_led_duty : 2; + PsLedFreq ps_led_freq : 3; + } __attribute__((packed)); +}; + +// +// PS_N_PULSES Register (0x83) +// +union PsNPulsesRegister501 { + uint8_t raw; + uint8_t number_of_pulses; +}; + +// +// PS_MEAS_RATE Register (0x84) +// +union PsMeasurementRateRegister { + uint8_t raw; + struct { + PsMeasurementRate ps_measurement_rate : 4; + uint8_t reserved : 4; + } __attribute__((packed)); +}; + +// +// ALS_MEAS_RATE Register (0x85) +// +union MeasurementRateRegister501 { + uint8_t raw; + struct { + MeasurementRepeatRate measurement_repeat_rate : 3; + IntegrationTime501 integration_time : 2; + bool reserved_5 : 1; + bool reserved_6 : 1; + bool reserved_7 : 1; + } __attribute__((packed)); +}; + +// +// PART_ID Register (0x86) (Read Only) +// +union PartIdRegister { + uint8_t raw; + struct { + uint8_t part_number_id : 4; + uint8_t revision_id : 4; + } __attribute__((packed)); +}; + +// +// ALS_PS_STATUS Register (0x8C) (Read Only) +// +union AlsPsStatusRegister { + uint8_t raw; + struct { + bool ps_new_data : 1; // 0 - old data, 1 - new data + bool ps_interrupt : 1; // 0 - interrupt signal not active, 1 - interrupt signal active + bool als_new_data : 1; // 0 - old data, 1 - new data + bool als_interrupt : 1; // 0 - interrupt signal not active, 1 - interrupt signal active + AlsGain501 gain : 1; // current ALS gain + bool reserved_5 : 1; + bool reserved_6 : 1; + bool reserved_7 : 1; + } __attribute__((packed)); +}; + +// +// PS_DATA_1 Register (0x8E) (Read Only) +// +union PsData1Register { + uint8_t raw; + struct { + uint8_t ps_data_high : 3; + uint8_t reserved : 4; + bool ps_saturation_flag : 1; + } __attribute__((packed)); +}; + +// +// INTERRUPT Register (0x8F) (Read Only) +// +union InterruptRegister { + uint8_t raw; + struct { + bool ps_interrupt : 1; + bool als_interrupt : 1; + bool interrupt_polarity : 1; // 0 - active low (default), 1 - active high + uint8_t reserved : 5; + } __attribute__((packed)); +}; + +// +// INTERRUPT_PERSIST Register (0x9E) +// +union InterruptPersistRegister { + uint8_t raw; + struct { + uint8_t als_persist : 4; // 0 - every ALS cycle, 1 - every 2 ALS cycles, ... 15 - every 16 ALS cycles + uint8_t ps_persist : 4; // 0 - every PS cycle, 1 - every 2 PS cycles, ... 15 - every 16 PS cycles + } __attribute__((packed)); +}; + +} // namespace ltr501 +} // namespace esphome diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py new file mode 100644 index 0000000000..153d1b3ad1 --- /dev/null +++ b/esphome/components/ltr501/sensor.py @@ -0,0 +1,274 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ACTUAL_GAIN, + CONF_ACTUAL_INTEGRATION_TIME, + CONF_AMBIENT_LIGHT, + CONF_AUTO_MODE, + CONF_FULL_SPECTRUM_COUNTS, + CONF_GAIN, + CONF_GLASS_ATTENUATION_FACTOR, + CONF_ID, + CONF_INTEGRATION_TIME, + CONF_NAME, + CONF_REPEAT, + CONF_TRIGGER_ID, + CONF_TYPE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + ICON_BRIGHTNESS_5, + ICON_BRIGHTNESS_6, + ICON_TIMER, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, + UNIT_MILLISECOND, +) + +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +CONF_INFRARED_COUNTS = "infrared_counts" +CONF_ON_PS_HIGH_THRESHOLD = "on_ps_high_threshold" +CONF_ON_PS_LOW_THRESHOLD = "on_ps_low_threshold" +CONF_PS_COOLDOWN = "ps_cooldown" +CONF_PS_COUNTS = "ps_counts" +CONF_PS_GAIN = "ps_gain" +CONF_PS_HIGH_THRESHOLD = "ps_high_threshold" +CONF_PS_LOW_THRESHOLD = "ps_low_threshold" +ICON_BRIGHTNESS_7 = "mdi:brightness-7" +ICON_GAIN = "mdi:multiplication" +ICON_PROXIMITY = "mdi:hand-wave-outline" +UNIT_COUNTS = "#" + +ltr501_ns = cg.esphome_ns.namespace("ltr501") + +LTRAlsPsComponent = ltr501_ns.class_( + "LTRAlsPs501Component", cg.PollingComponent, i2c.I2CDevice +) + +LtrType = ltr501_ns.enum("LtrType") +LTR_TYPES = { + "ALS": LtrType.LTR_TYPE_ALS_ONLY, + "PS": LtrType.LTR_TYPE_PS_ONLY, + "ALS_PS": LtrType.LTR_TYPE_ALS_AND_PS, +} + +AlsGain = ltr501_ns.enum("AlsGain501") +ALS_GAINS = { + "1X": AlsGain.GAIN_1, + "150X": AlsGain.GAIN_150, +} + +IntegrationTime = ltr501_ns.enum("IntegrationTime501") +INTEGRATION_TIMES = { + 50: IntegrationTime.INTEGRATION_TIME_50MS, + 100: IntegrationTime.INTEGRATION_TIME_100MS, + 200: IntegrationTime.INTEGRATION_TIME_200MS, + 400: IntegrationTime.INTEGRATION_TIME_400MS, +} + +MeasurementRepeatRate = ltr501_ns.enum("MeasurementRepeatRate") +MEASUREMENT_REPEAT_RATES = { + 50: MeasurementRepeatRate.REPEAT_RATE_50MS, + 100: MeasurementRepeatRate.REPEAT_RATE_100MS, + 200: MeasurementRepeatRate.REPEAT_RATE_200MS, + 500: MeasurementRepeatRate.REPEAT_RATE_500MS, + 1000: MeasurementRepeatRate.REPEAT_RATE_1000MS, + 2000: MeasurementRepeatRate.REPEAT_RATE_2000MS, +} + +PsGain = ltr501_ns.enum("PsGain501") +PS_GAINS = { + "1X": PsGain.PS_GAIN_1, + "4X": PsGain.PS_GAIN_4, + "8X": PsGain.PS_GAIN_8, + "16X": PsGain.PS_GAIN_16, +} + +LTRPsHighTrigger = ltr501_ns.class_("LTRPsHighTrigger", automation.Trigger.template()) +LTRPsLowTrigger = ltr501_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) + + +def validate_integration_time(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(INTEGRATION_TIMES, int=True)(value) + + +def validate_repeat_rate(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value) + + +def validate_time_and_repeat_rate(config): + integraton_time = config[CONF_INTEGRATION_TIME] + repeat_rate = config[CONF_REPEAT] + if integraton_time > repeat_rate: + raise cv.Invalid( + f"Measurement repeat rate ({repeat_rate}ms) shall be greater or equal to integration time ({integraton_time}ms)" + ) + return config + + +def validate_als_gain_and_integration_time(config): + integraton_time = config[CONF_INTEGRATION_TIME] + if config[CONF_GAIN] == "1X" and integraton_time > 100: + raise cv.Invalid( + "ALS gain 1X can only be used with integration time 50ms or 100ms" + ) + if config[CONF_GAIN] == "200X" and integraton_time == 50: + raise cv.Invalid("ALS gain 200X can not be used with integration time 50ms") + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LTRAlsPsComponent), + cv.Optional(CONF_TYPE, default="ALS_PS"): cv.enum(LTR_TYPES, upper=True), + cv.Optional(CONF_AUTO_MODE, default=True): cv.boolean, + cv.Optional(CONF_GAIN, default="1X"): cv.enum(ALS_GAINS, upper=True), + cv.Optional( + CONF_INTEGRATION_TIME, default="100ms" + ): validate_integration_time, + cv.Optional(CONF_REPEAT, default="500ms"): validate_repeat_rate, + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range( + min=1.0 + ), + cv.Optional( + CONF_PS_COOLDOWN, default="5s" + ): cv.positive_time_period_seconds, + cv.Optional(CONF_PS_GAIN, default="1X"): cv.enum(PS_GAINS, upper=True), + cv.Optional(CONF_PS_HIGH_THRESHOLD, default=65535): cv.int_range( + min=0, max=65535 + ), + cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( + min=0, max=65535 + ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), + } + ), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), + } + ), + cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_INFRARED_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_FULL_SPECTRUM_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_7, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_PS_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_PROXIMITY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTUAL_GAIN): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_GAIN, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTUAL_INTEGRATION_TIME): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x23)), + validate_time_and_repeat_rate, + validate_als_gain_and_integration_time, +) + + +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) + + if als_config := config.get(CONF_AMBIENT_LIGHT): + sens = await sensor.new_sensor(als_config) + cg.add(var.set_ambient_light_sensor(sens)) + + if infrared_cnt_config := config.get(CONF_INFRARED_COUNTS): + sens = await sensor.new_sensor(infrared_cnt_config) + cg.add(var.set_infrared_counts_sensor(sens)) + + if full_spect_cnt_config := config.get(CONF_FULL_SPECTRUM_COUNTS): + sens = await sensor.new_sensor(full_spect_cnt_config) + cg.add(var.set_full_spectrum_counts_sensor(sens)) + + if act_gain_config := config.get(CONF_ACTUAL_GAIN): + sens = await sensor.new_sensor(act_gain_config) + cg.add(var.set_actual_gain_sensor(sens)) + + if act_itime_config := config.get(CONF_ACTUAL_INTEGRATION_TIME): + sens = await sensor.new_sensor(act_itime_config) + cg.add(var.set_actual_integration_time_sensor(sens)) + + if prox_cnt_config := config.get(CONF_PS_COUNTS): + sens = await sensor.new_sensor(prox_cnt_config) + cg.add(var.set_proximity_counts_sensor(sens)) + + for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], prox_high_tr) + + for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], prox_low_tr) + + cg.add(var.set_ltr_type(config[CONF_TYPE])) + + cg.add(var.set_als_auto_mode(config[CONF_AUTO_MODE])) + cg.add(var.set_als_gain(config[CONF_GAIN])) + cg.add(var.set_als_integration_time(config[CONF_INTEGRATION_TIME])) + cg.add(var.set_als_meas_repeat_rate(config[CONF_REPEAT])) + cg.add(var.set_als_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR])) + + cg.add(var.set_ps_cooldown_time_s(config[CONF_PS_COOLDOWN])) + cg.add(var.set_ps_gain(config[CONF_PS_GAIN])) + cg.add(var.set_ps_high_threshold(config[CONF_PS_HIGH_THRESHOLD])) + cg.add(var.set_ps_low_threshold(config[CONF_PS_LOW_THRESHOLD])) diff --git a/esphome/components/ltr_als_ps/__init__.py b/esphome/components/ltr_als_ps/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/ltr_als_ps/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp new file mode 100644 index 0000000000..ae299c9b66 --- /dev/null +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -0,0 +1,519 @@ +#include "ltr_als_ps.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +using esphome::i2c::ErrorCode; + +namespace esphome { +namespace ltr_als_ps { + +static const char *const TAG = "ltr_als_ps"; + +static const uint8_t MAX_TRIES = 5; + +template T get_next(const T (&array)[size], const T val) { + size_t i = 0; + size_t idx = -1; + while (idx == -1 && i < size) { + if (array[i] == val) { + idx = i; + break; + } + i++; + } + if (idx == -1 || i + 1 >= size) + return val; + return array[i + 1]; +} + +template T get_prev(const T (&array)[size], const T val) { + size_t i = size - 1; + size_t idx = -1; + while (idx == -1 && i > 0) { + if (array[i] == val) { + idx = i; + break; + } + i--; + } + if (idx == -1 || i == 0) + return val; + return array[i - 1]; +} + +static uint16_t get_itime_ms(IntegrationTime time) { + static const uint16_t ALS_INT_TIME[8] = {100, 50, 200, 400, 150, 250, 300, 350}; + return ALS_INT_TIME[time & 0b111]; +} + +static uint16_t get_meas_time_ms(MeasurementRepeatRate rate) { + static const uint16_t ALS_MEAS_RATE[8] = {50, 100, 200, 500, 1000, 2000, 2000, 2000}; + return ALS_MEAS_RATE[rate & 0b111]; +} + +static float get_gain_coeff(AlsGain gain) { + static const float ALS_GAIN[8] = {1, 2, 4, 8, 0, 0, 48, 96}; + return ALS_GAIN[gain & 0b111]; +} + +static float get_ps_gain_coeff(PsGain gain) { + static const float PS_GAIN[4] = {16, 0, 32, 64}; + return PS_GAIN[gain & 0b11]; +} + +void LTRAlsPsComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up LTR-303/329/55x/659"); + // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive + this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; }); +} + +void LTRAlsPsComponent::dump_config() { + auto get_device_type = [](LtrType typ) { + switch (typ) { + case LtrType::LTR_TYPE_ALS_ONLY: + return "ALS only"; + case LtrType::LTR_TYPE_PS_ONLY: + return "PS only"; + case LtrType::LTR_TYPE_ALS_AND_PS: + return "ALS + PS"; + default: + return "Unknown"; + } + }; + + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Device type: %s", get_device_type(this->ltr_type_)); + if (this->is_als_()) { + ESP_LOGCONFIG(TAG, " Automatic mode: %s", ONOFF(this->automatic_mode_enabled_)); + ESP_LOGCONFIG(TAG, " Gain: %.0fx", get_gain_coeff(this->gain_)); + ESP_LOGCONFIG(TAG, " Integration time: %d ms", get_itime_ms(this->integration_time_)); + ESP_LOGCONFIG(TAG, " Measurement repeat rate: %d ms", get_meas_time_ms(this->repeat_rate_)); + ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_); + LOG_SENSOR(" ", "ALS calculated lux", this->ambient_light_sensor_); + LOG_SENSOR(" ", "CH1 Infrared counts", this->infrared_counts_sensor_); + LOG_SENSOR(" ", "CH0 Visible+IR counts", this->full_spectrum_counts_sensor_); + LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_); + } + if (this->is_ps_()) { + ESP_LOGCONFIG(TAG, " Proximity gain: %.0fx", get_ps_gain_coeff(this->ps_gain_)); + ESP_LOGCONFIG(TAG, " Proximity cooldown time: %d s", this->ps_cooldown_time_s_); + ESP_LOGCONFIG(TAG, " Proximity high threshold: %d", this->ps_threshold_high_); + ESP_LOGCONFIG(TAG, " Proximity low threshold: %d", this->ps_threshold_low_); + LOG_SENSOR(" ", "Proximity counts", this->proximity_counts_sensor_); + } + LOG_UPDATE_INTERVAL(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with I2C LTR-303/329/55x/659 failed!"); + } +} + +void LTRAlsPsComponent::update() { + ESP_LOGV(TAG, "Updating"); + if (this->is_ready() && this->state_ == State::IDLE) { + ESP_LOGV(TAG, "Initiating new data collection"); + + this->state_ = this->automatic_mode_enabled_ ? State::COLLECTING_DATA_AUTO : State::WAITING_FOR_DATA; + + this->als_readings_.ch0 = 0; + this->als_readings_.ch1 = 0; + this->als_readings_.gain = this->gain_; + this->als_readings_.integration_time = this->integration_time_; + this->als_readings_.lux = 0; + this->als_readings_.number_of_adjustments = 0; + + } else { + ESP_LOGV(TAG, "Component not ready yet"); + } +} + +void LTRAlsPsComponent::loop() { + ErrorCode err = i2c::ERROR_OK; + static uint8_t tries{0}; + + switch (this->state_) { + case State::DELAYED_SETUP: + err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + ESP_LOGV(TAG, "i2c connection failed"); + this->mark_failed(); + } + this->configure_reset_(); + if (this->is_als_()) { + this->configure_als_(); + this->configure_integration_time_(this->integration_time_); + } + if (this->is_ps_()) { + this->configure_ps_(); + } + + this->state_ = State::IDLE; + break; + + case State::IDLE: + if (this->is_ps_()) { + check_and_trigger_ps_(); + } + break; + + case State::WAITING_FOR_DATA: + if (this->is_als_data_ready_(this->als_readings_) == DataAvail::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)); + this->read_sensor_data_(this->als_readings_); + this->state_ = State::DATA_COLLECTED; + this->apply_lux_calculation_(this->als_readings_); + } else if (tries >= MAX_TRIES) { + ESP_LOGW(TAG, "Can't get data after several tries."); + tries = 0; + this->status_set_warning(); + this->state_ = State::IDLE; + return; + } else { + tries++; + } + break; + + case State::COLLECTING_DATA_AUTO: + case State::DATA_COLLECTED: + // first measurement in auto mode (COLLECTING_DATA_AUTO state) require device reconfiguration + if (this->state_ == State::COLLECTING_DATA_AUTO || this->are_adjustments_required_(this->als_readings_)) { + this->state_ = State::ADJUSTMENT_IN_PROGRESS; + ESP_LOGD(TAG, "Reconfiguring sensitivity: gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), + get_itime_ms(this->als_readings_.integration_time)); + this->configure_integration_time_(this->als_readings_.integration_time); + this->configure_gain_(this->als_readings_.gain); + // if sensitivity adjustment needed - need to wait for first data samples after setting new parameters + this->set_timeout(2 * get_meas_time_ms(this->repeat_rate_), + [this]() { this->state_ = State::WAITING_FOR_DATA; }); + } else { + this->state_ = State::READY_TO_PUBLISH; + } + break; + + case State::ADJUSTMENT_IN_PROGRESS: + // nothing to be done, just waiting for the timeout + break; + + case State::READY_TO_PUBLISH: + this->publish_data_part_1_(this->als_readings_); + this->state_ = State::KEEP_PUBLISHING; + break; + + case State::KEEP_PUBLISHING: + this->publish_data_part_2_(this->als_readings_); + this->status_clear_warning(); + this->state_ = State::IDLE; + break; + + default: + break; + } +} + +void LTRAlsPsComponent::check_and_trigger_ps_() { + static uint32_t last_high_trigger_time{0}; + static uint32_t last_low_trigger_time{0}; + uint16_t ps_data = this->read_ps_data_(); + uint32_t now = millis(); + + if (ps_data != this->ps_readings_) { + this->ps_readings_ = ps_data; + // Higher values - object is closer to sensor + if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { + last_high_trigger_time = now; + ESP_LOGV(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, + this->ps_threshold_high_); + this->on_ps_high_trigger_callback_.call(); + } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { + last_low_trigger_time = now; + ESP_LOGV(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, + this->ps_threshold_low_); + this->on_ps_low_trigger_callback_.call(); + } + } +} + +bool LTRAlsPsComponent::check_part_number_() { + uint8_t manuf_id = this->reg((uint8_t) CommandRegisters::MANUFAC_ID).get(); + if (manuf_id != 0x05) { // 0x05 is Lite-On Semiconductor Corp. ID + ESP_LOGW(TAG, "Unknown manufacturer ID: 0x%02X", manuf_id); + this->mark_failed(); + return false; + } + + // Things getting not really funny here, we can't identify device type by part number ID + // ======================== ========= ===== ================= + // Device Part ID Rev Capabilities + // ======================== ========= ===== ================= + // Ltr-329/ltr-303 0x0a 0x00 Als 16b + // Ltr-553/ltr-556/ltr-556 0x09 0x02 Als 16b + Ps 11b diff nm sens + // Ltr-659 0x09 0x02 Ps 11b and ps gain + // + // There are other devices which might potentially work with default settings, + // but registers layout is different and we can't use them properly. For ex. ltr-558 + + PartIdRegister part_id{0}; + part_id.raw = this->reg((uint8_t) CommandRegisters::PART_ID).get(); + if (part_id.part_number_id != 0x0a && part_id.part_number_id != 0x09) { + ESP_LOGW(TAG, "Unknown part number ID: 0x%02X. It might not work properly.", part_id.part_number_id); + this->status_set_warning(); + return true; + } + return true; +} + +void LTRAlsPsComponent::configure_reset_() { + ESP_LOGV(TAG, "Resetting"); + + AlsControlRegister als_ctrl{0}; + als_ctrl.sw_reset = true; + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + + uint8_t tries = MAX_TRIES; + do { + ESP_LOGV(TAG, "Waiting for chip to reset"); + delay(2); + als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + } while (als_ctrl.sw_reset && tries--); // while sw reset bit is on - keep waiting + + if (als_ctrl.sw_reset) { + ESP_LOGW(TAG, "Reset timed out"); + } +} + +void LTRAlsPsComponent::configure_als_() { + AlsControlRegister als_ctrl{0}; + + als_ctrl.sw_reset = false; + als_ctrl.active_mode = true; + als_ctrl.gain = this->gain_; + + ESP_LOGV(TAG, "Setting active mode and gain reg 0x%02X", als_ctrl.raw); + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(5); + + uint8_t tries = MAX_TRIES; + do { + ESP_LOGV(TAG, "Waiting for device to become active..."); + delay(2); + als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + } while (!als_ctrl.active_mode && tries--); // while active mode is not set - keep waiting + + if (!als_ctrl.active_mode) { + ESP_LOGW(TAG, "Failed to activate device"); + } +} + +void LTRAlsPsComponent::configure_ps_() { + PsMeasurementRateRegister ps_meas{0}; + ps_meas.ps_measurement_rate = PsMeasurementRate::PS_MEAS_RATE_50MS; + this->reg((uint8_t) CommandRegisters::PS_MEAS_RATE) = ps_meas.raw; + + PsControlRegister ps_ctrl{0}; + ps_ctrl.ps_mode_active = true; + ps_ctrl.ps_mode_xxx = true; + this->reg((uint8_t) CommandRegisters::PS_CONTR) = ps_ctrl.raw; +} + +uint16_t LTRAlsPsComponent::read_ps_data_() { + AlsPsStatusRegister als_status{0}; + als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); + if (!als_status.ps_new_data || als_status.data_invalid) { + return this->ps_readings_; + } + + uint8_t ps_low = this->reg((uint8_t) CommandRegisters::PS_DATA_0).get(); + PsData1Register ps_high; + ps_high.raw = this->reg((uint8_t) CommandRegisters::PS_DATA_1).get(); + + uint16_t val = encode_uint16(ps_high.ps_data_high, ps_low); + if (ps_high.ps_saturation_flag) { + return 0x7ff; // full 11 bit range + } + return val; +} + +void LTRAlsPsComponent::configure_gain_(AlsGain gain) { + AlsControlRegister als_ctrl{0}; + als_ctrl.active_mode = true; + als_ctrl.gain = gain; + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + + AlsControlRegister read_als_ctrl{0}; + read_als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + if (read_als_ctrl.gain != gain) { + ESP_LOGW(TAG, "Failed to set gain. We will try one more time."); + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + } +} + +void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) { + MeasurementRateRegister meas{0}; + meas.measurement_repeat_rate = this->repeat_rate_; + meas.integration_time = time; + this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw; + delay(2); + + MeasurementRateRegister read_meas{0}; + read_meas.raw = this->reg((uint8_t) CommandRegisters::MEAS_RATE).get(); + if (read_meas.integration_time != time) { + ESP_LOGW(TAG, "Failed to set integration time. We will try one more time."); + this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw; + delay(2); + } +} + +DataAvail 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; + + if (als_status.data_invalid) { + ESP_LOGW(TAG, "Data available but not valid"); + return DataAvail::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 DataAvail::DATA_OK; +} + +void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) { + data.ch1 = 0; + data.ch0 = 0; + uint8_t ch1_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_0).get(); + uint8_t ch1_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_1).get(); + uint8_t ch0_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_0).get(); + uint8_t ch0_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_1).get(); + data.ch1 = encode_uint16(ch1_1, ch1_0); + data.ch0 = encode_uint16(ch0_1, ch0_0); + + ESP_LOGV(TAG, "Got sensor data: CH1 = %d, CH0 = %d", data.ch1, data.ch0); +} + +bool LTRAlsPsComponent::are_adjustments_required_(AlsReadings &data) { + if (!this->automatic_mode_enabled_) + return false; + + if (data.number_of_adjustments > 15) { + // sometimes sensors fail to change sensitivity. this prevents us from infinite loop + ESP_LOGW(TAG, "Too many sensitivity adjustments done. Apparently, sensor reconfiguration fails. Stopping."); + return false; + } + data.number_of_adjustments++; + + // Recommended thresholds as per datasheet + static const uint16_t LOW_INTENSITY_THRESHOLD = 1000; + static const uint16_t HIGH_INTENSITY_THRESHOLD = 30000; + static const AlsGain GAINS[GAINS_COUNT] = {GAIN_1, GAIN_2, GAIN_4, GAIN_8, GAIN_48, GAIN_96}; + static const IntegrationTime INT_TIMES[TIMES_COUNT] = { + INTEGRATION_TIME_50MS, INTEGRATION_TIME_100MS, INTEGRATION_TIME_150MS, INTEGRATION_TIME_200MS, + INTEGRATION_TIME_250MS, INTEGRATION_TIME_300MS, INTEGRATION_TIME_350MS, INTEGRATION_TIME_400MS}; + + if (data.ch0 <= LOW_INTENSITY_THRESHOLD) { + AlsGain next_gain = get_next(GAINS, data.gain); + if (next_gain != data.gain) { + data.gain = next_gain; + ESP_LOGV(TAG, "Low illuminance. Increasing gain."); + return true; + } + IntegrationTime next_time = get_next(INT_TIMES, data.integration_time); + if (next_time != data.integration_time) { + data.integration_time = next_time; + ESP_LOGV(TAG, "Low illuminance. Increasing integration time."); + return true; + } + } else if (data.ch0 >= HIGH_INTENSITY_THRESHOLD) { + AlsGain prev_gain = get_prev(GAINS, data.gain); + if (prev_gain != data.gain) { + data.gain = prev_gain; + ESP_LOGV(TAG, "High illuminance. Decreasing gain."); + return true; + } + IntegrationTime prev_time = get_prev(INT_TIMES, data.integration_time); + if (prev_time != data.integration_time) { + data.integration_time = prev_time; + ESP_LOGV(TAG, "High illuminance. Decreasing integration time."); + return true; + } + } else { + ESP_LOGD(TAG, "Illuminance is sufficient."); + return false; + } + ESP_LOGD(TAG, "Can't adjust sensitivity anymore."); + return false; +} + +void LTRAlsPsComponent::apply_lux_calculation_(AlsReadings &data) { + if ((data.ch0 == 0xFFFF) || (data.ch1 == 0xFFFF)) { + ESP_LOGW(TAG, "Sensors got saturated"); + data.lux = 0.0f; + return; + } + + if ((data.ch0 == 0x0000) && (data.ch1 == 0x0000)) { + ESP_LOGW(TAG, "Sensors blacked out"); + data.lux = 0.0f; + return; + } + + float ch0 = data.ch0; + float ch1 = data.ch1; + float ratio = ch1 / (ch0 + ch1); + float als_gain = get_gain_coeff(data.gain); + float als_time = ((float) get_itime_ms(data.integration_time)) / 100.0f; + float inv_pfactor = this->glass_attenuation_factor_; + float lux = 0.0f; + + if (ratio < 0.45) { + lux = (1.7743 * ch0 + 1.1059 * ch1); + } else if (ratio < 0.64 && ratio >= 0.45) { + lux = (4.2785 * ch0 - 1.9548 * ch1); + } else if (ratio < 0.85 && ratio >= 0.64) { + lux = (0.5926 * ch0 + 0.1185 * ch1); + } else { + ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio"); + lux = 0.0f; + } + lux = inv_pfactor * lux / als_gain / als_time; + data.lux = lux; + + ESP_LOGV(TAG, "Lux calculation: ratio %.3f, gain %.0fx, int time %.1f, inv_pfactor %.3f, lux %.3f", ratio, als_gain, + als_time, inv_pfactor, lux); +} + +void LTRAlsPsComponent::publish_data_part_1_(AlsReadings &data) { + if (this->proximity_counts_sensor_ != nullptr) { + this->proximity_counts_sensor_->publish_state(this->ps_readings_); + } + if (this->ambient_light_sensor_ != nullptr) { + this->ambient_light_sensor_->publish_state(data.lux); + } + if (this->infrared_counts_sensor_ != nullptr) { + this->infrared_counts_sensor_->publish_state(data.ch1); + } + if (this->full_spectrum_counts_sensor_ != nullptr) { + this->full_spectrum_counts_sensor_->publish_state(data.ch0); + } +} + +void LTRAlsPsComponent::publish_data_part_2_(AlsReadings &data) { + if (this->actual_gain_sensor_ != nullptr) { + this->actual_gain_sensor_->publish_state(get_gain_coeff(data.gain)); + } + if (this->actual_integration_time_sensor_ != nullptr) { + this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.integration_time)); + } +} +} // namespace ltr_als_ps +} // namespace esphome diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h new file mode 100644 index 0000000000..4cbbcea54c --- /dev/null +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -0,0 +1,184 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "esphome/core/automation.h" + +#include "ltr_definitions.h" + +namespace esphome { +namespace ltr_als_ps { + +enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; + +enum LtrType : uint8_t { + LTR_TYPE_UNKNOWN = 0, + LTR_TYPE_ALS_ONLY = 1, + LTR_TYPE_PS_ONLY = 2, + LTR_TYPE_ALS_AND_PS = 3, +}; + +class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { + public: + // + // EspHome framework functions + // + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + void loop() override; + + // Configuration setters : General + // + void set_ltr_type(LtrType type) { this->ltr_type_ = type; } + + // Configuration setters : ALS + // + void set_als_auto_mode(bool enable) { this->automatic_mode_enabled_ = enable; } + void set_als_gain(AlsGain gain) { this->gain_ = gain; } + void set_als_integration_time(IntegrationTime time) { this->integration_time_ = time; } + void set_als_meas_repeat_rate(MeasurementRepeatRate rate) { this->repeat_rate_ = rate; } + void set_als_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; } + + // Configuration setters : PS + // + void set_ps_high_threshold(uint16_t threshold) { this->ps_threshold_high_ = threshold; } + void set_ps_low_threshold(uint16_t threshold) { this->ps_threshold_low_ = threshold; } + void set_ps_cooldown_time_s(uint16_t time) { this->ps_cooldown_time_s_ = time; } + void set_ps_gain(PsGain gain) { this->ps_gain_ = gain; } + + // Sensors setters + // + void set_ambient_light_sensor(sensor::Sensor *sensor) { this->ambient_light_sensor_ = sensor; } + void set_full_spectrum_counts_sensor(sensor::Sensor *sensor) { this->full_spectrum_counts_sensor_ = sensor; } + void set_infrared_counts_sensor(sensor::Sensor *sensor) { this->infrared_counts_sensor_ = sensor; } + void set_actual_gain_sensor(sensor::Sensor *sensor) { this->actual_gain_sensor_ = sensor; } + void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } + void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + + protected: + // + // Internal state machine, used to split all the actions into + // small steps in loop() to make sure we are not blocking execution + // + enum class State : uint8_t { + NOT_INITIALIZED, + DELAYED_SETUP, + IDLE, + WAITING_FOR_DATA, + COLLECTING_DATA_AUTO, + DATA_COLLECTED, + ADJUSTMENT_IN_PROGRESS, + READY_TO_PUBLISH, + KEEP_PUBLISHING + } state_{State::NOT_INITIALIZED}; + + LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY}; + + // + // Current measurements data + // + struct AlsReadings { + uint16_t ch0{0}; + uint16_t ch1{0}; + AlsGain gain{AlsGain::GAIN_1}; + IntegrationTime integration_time{IntegrationTime::INTEGRATION_TIME_100MS}; + float lux{0.0f}; + uint8_t number_of_adjustments{0}; + } als_readings_; + uint16_t ps_readings_{0xfffe}; + + inline bool is_als_() const { + return this->ltr_type_ == LtrType::LTR_TYPE_ALS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS; + } + inline bool is_ps_() const { + return this->ltr_type_ == LtrType::LTR_TYPE_PS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS; + } + + // + // Device interaction and data manipulation + // + bool check_part_number_(); + + void configure_reset_(); + void configure_als_(); + void configure_integration_time_(IntegrationTime time); + void configure_gain_(AlsGain gain); + DataAvail is_als_data_ready_(AlsReadings &data); + void read_sensor_data_(AlsReadings &data); + bool are_adjustments_required_(AlsReadings &data); + void apply_lux_calculation_(AlsReadings &data); + void publish_data_part_1_(AlsReadings &data); + void publish_data_part_2_(AlsReadings &data); + + void configure_ps_(); + uint16_t read_ps_data_(); + void check_and_trigger_ps_(); + + // + // Component configuration + // + bool automatic_mode_enabled_{true}; + AlsGain gain_{AlsGain::GAIN_1}; + IntegrationTime integration_time_{IntegrationTime::INTEGRATION_TIME_100MS}; + MeasurementRepeatRate repeat_rate_{MeasurementRepeatRate::REPEAT_RATE_500MS}; + float glass_attenuation_factor_{1.0}; + + uint16_t ps_cooldown_time_s_{5}; + PsGain ps_gain_{PsGain::PS_GAIN_16}; + uint16_t ps_threshold_high_{0xffff}; + uint16_t ps_threshold_low_{0x0000}; + + // + // Sensors for publishing data + // + sensor::Sensor *infrared_counts_sensor_{nullptr}; // direct reading CH1, infrared only + sensor::Sensor *full_spectrum_counts_sensor_{nullptr}; // direct reading CH0, infrared + visible light + sensor::Sensor *ambient_light_sensor_{nullptr}; // calculated lux + sensor::Sensor *actual_gain_sensor_{nullptr}; // actual gain of reading + sensor::Sensor *actual_integration_time_sensor_{nullptr}; // actual integration time + sensor::Sensor *proximity_counts_sensor_{nullptr}; // proximity sensor + + bool is_any_als_sensor_enabled_() const { + return this->ambient_light_sensor_ != nullptr || this->full_spectrum_counts_sensor_ != nullptr || + this->infrared_counts_sensor_ != nullptr || this->actual_gain_sensor_ != nullptr || + this->actual_integration_time_sensor_ != nullptr; + } + bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } + + // + // Trigger section for the automations + // + friend class LTRPsHighTrigger; + friend class LTRPsLowTrigger; + + CallbackManager on_ps_high_trigger_callback_; + CallbackManager on_ps_low_trigger_callback_; + + void add_on_ps_high_trigger_callback_(std::function callback) { + this->on_ps_high_trigger_callback_.add(std::move(callback)); + } + + void add_on_ps_low_trigger_callback_(std::function callback) { + this->on_ps_low_trigger_callback_.add(std::move(callback)); + } +}; + +class LTRPsHighTrigger : public Trigger<> { + public: + explicit LTRPsHighTrigger(LTRAlsPsComponent *parent) { + parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); + } +}; + +class LTRPsLowTrigger : public Trigger<> { + public: + explicit LTRPsLowTrigger(LTRAlsPsComponent *parent) { + parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); + } +}; +} // namespace ltr_als_ps +} // namespace esphome diff --git a/esphome/components/ltr_als_ps/ltr_definitions.h b/esphome/components/ltr_als_ps/ltr_definitions.h new file mode 100644 index 0000000000..739445e9a0 --- /dev/null +++ b/esphome/components/ltr_als_ps/ltr_definitions.h @@ -0,0 +1,275 @@ +#pragma once + +#include + +namespace esphome { +namespace ltr_als_ps { + +enum class CommandRegisters : uint8_t { + ALS_CONTR = 0x80, // ALS operation mode control and SW reset + PS_CONTR = 0x81, // PS operation mode control + PS_LED = 0x82, // PS LED pulse frequency control + PS_N_PULSES = 0x83, // PS number of pulses control + PS_MEAS_RATE = 0x84, // PS measurement rate in active mode + MEAS_RATE = 0x85, // ALS measurement rate in active mode + PART_ID = 0x86, // Part Number ID and Revision ID + MANUFAC_ID = 0x87, // Manufacturer ID + ALS_DATA_CH1_0 = 0x88, // ALS measurement CH1 data, lower byte - infrared only + ALS_DATA_CH1_1 = 0x89, // ALS measurement CH1 data, upper byte - infrared only + ALS_DATA_CH0_0 = 0x8A, // ALS measurement CH0 data, lower byte - visible + infrared + ALS_DATA_CH0_1 = 0x8B, // ALS measurement CH0 data, upper byte - visible + infrared + ALS_PS_STATUS = 0x8C, // ALS PS new data status + PS_DATA_0 = 0x8D, // PS measurement data, lower byte + PS_DATA_1 = 0x8E, // PS measurement data, upper byte + ALS_PS_INTERRUPT = 0x8F, // Interrupt status + PS_THRES_UP_0 = 0x90, // PS interrupt upper threshold, lower byte + PS_THRES_UP_1 = 0x91, // PS interrupt upper threshold, upper byte + PS_THRES_LOW_0 = 0x92, // PS interrupt lower threshold, lower byte + PS_THRES_LOW_1 = 0x93, // PS interrupt lower threshold, upper byte + PS_OFFSET_1 = 0x94, // PS offset, upper byte + PS_OFFSET_0 = 0x95, // PS offset, lower byte + // 0x96 - reserved + ALS_THRES_UP_0 = 0x97, // ALS interrupt upper threshold, lower byte + ALS_THRES_UP_1 = 0x98, // ALS interrupt upper threshold, upper byte + ALS_THRES_LOW_0 = 0x99, // ALS interrupt lower threshold, lower byte + ALS_THRES_LOW_1 = 0x9A, // ALS interrupt lower threshold, upper byte + // 0x9B - reserved + // 0x9C - reserved + // 0x9D - reserved + INTERRUPT_PERSIST = 0x9E // Interrupt persistence filter +}; + +// ALS Sensor gain levels +enum AlsGain : uint8_t { + GAIN_1 = 0, // default + GAIN_2 = 1, + GAIN_4 = 2, + GAIN_8 = 3, + GAIN_48 = 6, + GAIN_96 = 7, +}; +static const uint8_t GAINS_COUNT = 6; + +// ALS Sensor integration times +enum IntegrationTime : uint8_t { + INTEGRATION_TIME_100MS = 0, // default + INTEGRATION_TIME_50MS = 1, + INTEGRATION_TIME_200MS = 2, + INTEGRATION_TIME_400MS = 3, + INTEGRATION_TIME_150MS = 4, + INTEGRATION_TIME_250MS = 5, + INTEGRATION_TIME_300MS = 6, + INTEGRATION_TIME_350MS = 7 +}; +static const uint8_t TIMES_COUNT = 8; + +// ALS Sensor measurement repeat rate +enum MeasurementRepeatRate { + REPEAT_RATE_50MS = 0, + REPEAT_RATE_100MS = 1, + REPEAT_RATE_200MS = 2, + REPEAT_RATE_500MS = 3, // default + REPEAT_RATE_1000MS = 4, + REPEAT_RATE_2000MS = 5 +}; + +// PS Sensor gain levels +enum PsGain : uint8_t { + PS_GAIN_16 = 0, // default + PS_GAIN_32 = 2, + PS_GAIN_64 = 3, +}; + +// PS Mode +enum PsMode : uint8_t { + PS_MODE_STANDBY_00 = 0, // default + PS_MODE_STANDBY_01 = 1, + PS_MODE_ACTIVE_10 = 2, + PS_MODE_ACTIVE_11 = 3, +}; + +// LED Pulse Modulation Frequency +enum PsLedFreq : uint8_t { + PS_LED_FREQ_30KHZ = 0, + PS_LED_FREQ_40KHZ = 1, + PS_LED_FREQ_50KHZ = 2, + PS_LED_FREQ_60KHZ = 3, // default + PS_LED_FREQ_70KHZ = 4, + PS_LED_FREQ_80KHZ = 5, + PS_LED_FREQ_90KHZ = 6, + PS_LED_FREQ_100KHZ = 7, +}; + +// LED current duty +enum PsLedDuty : uint8_t { + PS_LED_DUTY_25 = 0, + PS_LED_DUTY_50 = 1, + PS_LED_DUTY_75 = 2, + PS_LED_DUTY_100 = 3, // default +}; + +// LED pulsed current level +enum PsLedCurrent : uint8_t { + PS_LED_CURRENT_5MA = 0, + PS_LED_CURRENT_10MA = 1, + PS_LED_CURRENT_20MA = 2, + PS_LED_CURRENT_50MA = 3, + PS_LED_CURRENT_100MA = 4, // default + PS_LED_CURRENT_100MA1 = 5, + PS_LED_CURRENT_100MA2 = 6, + PS_LED_CURRENT_100MA3 = 7, +}; + +// PS measurement rate +enum PsMeasurementRate : uint8_t { + PS_MEAS_RATE_50MS = 0, + PS_MEAS_RATE_70MS = 1, + PS_MEAS_RATE_100MS = 2, + PS_MEAS_RATE_200MS = 3, + PS_MEAS_RATE_500MS = 4, // default + PS_MEAS_RATE_1000MS = 5, + PS_MEAS_RATE_2000MS = 6, + PS_MEAS_RATE_2000MS1 = 7, + PS_MEAS_RATE_10MS = 8, +}; + +// +// ALS_CONTR Register (0x80) +// +union AlsControlRegister { + uint8_t raw; + struct { + bool active_mode : 1; + bool sw_reset : 1; + AlsGain gain : 3; + uint8_t reserved : 3; + } __attribute__((packed)); +}; + +// +// PS_CONTR Register (0x81) +// +union PsControlRegister { + uint8_t raw; + struct { + bool ps_mode_xxx : 1; + bool ps_mode_active : 1; + PsGain ps_gain : 2; // only LTR-659/558 + bool reserved_4 : 1; + bool ps_saturation_indicator_enable : 1; + bool reserved_6 : 1; + bool reserved_7 : 1; + } __attribute__((packed)); +}; + +// +// PS_LED Register (0x82) +// +union PsLedRegister { + uint8_t raw; + struct { + PsLedCurrent ps_led_current : 3; + PsLedDuty ps_led_duty : 2; + PsLedFreq ps_led_freq : 3; + } __attribute__((packed)); +}; + +// +// PS_N_PULSES Register (0x83) +// +union PsNPulsesRegister { + uint8_t raw; + struct { + uint8_t number_of_pulses : 4; + uint8_t reserved : 4; + } __attribute__((packed)); +}; + +// +// PS_MEAS_RATE Register (0x84) +// +union PsMeasurementRateRegister { + uint8_t raw; + struct { + PsMeasurementRate ps_measurement_rate : 4; + uint8_t reserved : 4; + } __attribute__((packed)); +}; + +// +// ALS_MEAS_RATE Register (0x85) +// +union MeasurementRateRegister { + uint8_t raw; + struct { + MeasurementRepeatRate measurement_repeat_rate : 3; + IntegrationTime integration_time : 3; + bool reserved_6 : 1; + bool reserved_7 : 1; + } __attribute__((packed)); +}; + +// +// PART_ID Register (0x86) (Read Only) +// +union PartIdRegister { + uint8_t raw; + struct { + uint8_t part_number_id : 4; + uint8_t revision_id : 4; + } __attribute__((packed)); +}; + +// +// ALS_PS_STATUS Register (0x8C) (Read Only) +// +union AlsPsStatusRegister { + uint8_t raw; + struct { + bool ps_new_data : 1; // 0 - old data, 1 - new data + bool ps_interrupt : 1; // 0 - interrupt signal not active, 1 - interrupt signal active + bool als_new_data : 1; // 0 - old data, 1 - new data + bool als_interrupt : 1; // 0 - interrupt signal not active, 1 - interrupt signal active + AlsGain gain : 3; // current ALS gain + bool data_invalid : 1; + } __attribute__((packed)); +}; + +// +// PS_DATA_1 Register (0x8E) (Read Only) +// +union PsData1Register { + uint8_t raw; + struct { + uint8_t ps_data_high : 3; + uint8_t reserved : 4; + bool ps_saturation_flag : 1; + } __attribute__((packed)); +}; + +// +// INTERRUPT Register (0x8F) (Read Only) +// +union InterruptRegister { + uint8_t raw; + struct { + bool ps_interrupt : 1; + bool als_interrupt : 1; + bool interrupt_polarity : 1; // 0 - active low (default), 1 - active high + uint8_t reserved : 5; + } __attribute__((packed)); +}; + +// +// INTERRUPT_PERSIST Register (0x9E) +// +union InterruptPersistRegister { + uint8_t raw; + struct { + uint8_t als_persist : 4; // 0 - every ALS cycle, 1 - every 2 ALS cycles, ... 15 - every 16 ALS cycles + uint8_t ps_persist : 4; // 0 - every PS cycle, 1 - every 2 PS cycles, ... 15 - every 16 PS cycles + } __attribute__((packed)); +}; + +} // namespace ltr_als_ps +} // namespace esphome diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py new file mode 100644 index 0000000000..e9a5264941 --- /dev/null +++ b/esphome/components/ltr_als_ps/sensor.py @@ -0,0 +1,271 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ACTUAL_GAIN, + CONF_ACTUAL_INTEGRATION_TIME, + CONF_AMBIENT_LIGHT, + CONF_AUTO_MODE, + CONF_FULL_SPECTRUM_COUNTS, + CONF_GAIN, + CONF_GLASS_ATTENUATION_FACTOR, + CONF_ID, + CONF_INTEGRATION_TIME, + CONF_NAME, + CONF_REPEAT, + CONF_TRIGGER_ID, + CONF_TYPE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + ICON_BRIGHTNESS_5, + ICON_BRIGHTNESS_6, + ICON_TIMER, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, + UNIT_MILLISECOND, +) + +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +CONF_INFRARED_COUNTS = "infrared_counts" +CONF_ON_PS_HIGH_THRESHOLD = "on_ps_high_threshold" +CONF_ON_PS_LOW_THRESHOLD = "on_ps_low_threshold" +CONF_PS_COOLDOWN = "ps_cooldown" +CONF_PS_COUNTS = "ps_counts" +CONF_PS_GAIN = "ps_gain" +CONF_PS_HIGH_THRESHOLD = "ps_high_threshold" +CONF_PS_LOW_THRESHOLD = "ps_low_threshold" +ICON_BRIGHTNESS_7 = "mdi:brightness-7" +ICON_GAIN = "mdi:multiplication" +ICON_PROXIMITY = "mdi:hand-wave-outline" +UNIT_COUNTS = "#" + +ltr_als_ps_ns = cg.esphome_ns.namespace("ltr_als_ps") + +LTRAlsPsComponent = ltr_als_ps_ns.class_( + "LTRAlsPsComponent", cg.PollingComponent, i2c.I2CDevice +) + +LtrType = ltr_als_ps_ns.enum("LtrType") +LTR_TYPES = { + "ALS": LtrType.LTR_TYPE_ALS_ONLY, + "PS": LtrType.LTR_TYPE_PS_ONLY, + "ALS_PS": LtrType.LTR_TYPE_ALS_AND_PS, +} + +AlsGain = ltr_als_ps_ns.enum("AlsGain") +ALS_GAINS = { + "1X": AlsGain.GAIN_1, + "2X": AlsGain.GAIN_2, + "4X": AlsGain.GAIN_4, + "8X": AlsGain.GAIN_8, + "48X": AlsGain.GAIN_48, + "96X": AlsGain.GAIN_96, +} + +IntegrationTime = ltr_als_ps_ns.enum("IntegrationTime") +INTEGRATION_TIMES = { + 50: IntegrationTime.INTEGRATION_TIME_50MS, + 100: IntegrationTime.INTEGRATION_TIME_100MS, + 150: IntegrationTime.INTEGRATION_TIME_150MS, + 200: IntegrationTime.INTEGRATION_TIME_200MS, + 250: IntegrationTime.INTEGRATION_TIME_250MS, + 300: IntegrationTime.INTEGRATION_TIME_300MS, + 350: IntegrationTime.INTEGRATION_TIME_350MS, + 400: IntegrationTime.INTEGRATION_TIME_400MS, +} + +MeasurementRepeatRate = ltr_als_ps_ns.enum("MeasurementRepeatRate") +MEASUREMENT_REPEAT_RATES = { + 50: MeasurementRepeatRate.REPEAT_RATE_50MS, + 100: MeasurementRepeatRate.REPEAT_RATE_100MS, + 200: MeasurementRepeatRate.REPEAT_RATE_200MS, + 500: MeasurementRepeatRate.REPEAT_RATE_500MS, + 1000: MeasurementRepeatRate.REPEAT_RATE_1000MS, + 2000: MeasurementRepeatRate.REPEAT_RATE_2000MS, +} + +PsGain = ltr_als_ps_ns.enum("PsGain") +PS_GAINS = { + "16X": PsGain.PS_GAIN_16, + "32X": PsGain.PS_GAIN_32, + "64X": PsGain.PS_GAIN_64, +} + +LTRPsHighTrigger = ltr_als_ps_ns.class_( + "LTRPsHighTrigger", automation.Trigger.template() +) +LTRPsLowTrigger = ltr_als_ps_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) + + +def validate_integration_time(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(INTEGRATION_TIMES, int=True)(value) + + +def validate_repeat_rate(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value) + + +def validate_time_and_repeat_rate(config): + integraton_time = config[CONF_INTEGRATION_TIME] + repeat_rate = config[CONF_REPEAT] + if integraton_time > repeat_rate: + raise cv.Invalid( + f"Measurement repeat rate ({repeat_rate}ms) shall be greater or equal to integration time ({integraton_time}ms)" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LTRAlsPsComponent), + cv.Optional(CONF_TYPE, default="ALS_PS"): cv.enum(LTR_TYPES, upper=True), + cv.Optional(CONF_AUTO_MODE, default=True): cv.boolean, + cv.Optional(CONF_GAIN, default="1X"): cv.enum(ALS_GAINS, upper=True), + cv.Optional( + CONF_INTEGRATION_TIME, default="100ms" + ): validate_integration_time, + cv.Optional(CONF_REPEAT, default="500ms"): validate_repeat_rate, + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range( + min=1.0 + ), + cv.Optional( + CONF_PS_COOLDOWN, default="5s" + ): cv.positive_time_period_seconds, + cv.Optional(CONF_PS_GAIN, default="16X"): cv.enum(PS_GAINS, upper=True), + cv.Optional(CONF_PS_HIGH_THRESHOLD, default=65535): cv.int_range( + min=0, max=65535 + ), + cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( + min=0, max=65535 + ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), + } + ), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), + } + ), + cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_INFRARED_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_FULL_SPECTRUM_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_7, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_PS_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_PROXIMITY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTUAL_GAIN): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_GAIN, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTUAL_INTEGRATION_TIME): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x29)), + validate_time_and_repeat_rate, +) + + +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) + + if als_config := config.get(CONF_AMBIENT_LIGHT): + sens = await sensor.new_sensor(als_config) + cg.add(var.set_ambient_light_sensor(sens)) + + if infrared_cnt_config := config.get(CONF_INFRARED_COUNTS): + sens = await sensor.new_sensor(infrared_cnt_config) + cg.add(var.set_infrared_counts_sensor(sens)) + + if full_spect_cnt_config := config.get(CONF_FULL_SPECTRUM_COUNTS): + sens = await sensor.new_sensor(full_spect_cnt_config) + cg.add(var.set_full_spectrum_counts_sensor(sens)) + + if act_gain_config := config.get(CONF_ACTUAL_GAIN): + sens = await sensor.new_sensor(act_gain_config) + cg.add(var.set_actual_gain_sensor(sens)) + + if act_itime_config := config.get(CONF_ACTUAL_INTEGRATION_TIME): + sens = await sensor.new_sensor(act_itime_config) + cg.add(var.set_actual_integration_time_sensor(sens)) + + if prox_cnt_config := config.get(CONF_PS_COUNTS): + sens = await sensor.new_sensor(prox_cnt_config) + cg.add(var.set_proximity_counts_sensor(sens)) + + for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], prox_high_tr) + + for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], prox_low_tr) + + cg.add(var.set_ltr_type(config[CONF_TYPE])) + + cg.add(var.set_als_auto_mode(config[CONF_AUTO_MODE])) + cg.add(var.set_als_gain(config[CONF_GAIN])) + cg.add(var.set_als_integration_time(config[CONF_INTEGRATION_TIME])) + cg.add(var.set_als_meas_repeat_rate(config[CONF_REPEAT])) + cg.add(var.set_als_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR])) + + cg.add(var.set_ps_cooldown_time_s(config[CONF_PS_COOLDOWN])) + cg.add(var.set_ps_gain(config[CONF_PS_GAIN])) + cg.add(var.set_ps_high_threshold(config[CONF_PS_HIGH_THRESHOLD])) + cg.add(var.set_ps_low_threshold(config[CONF_PS_LOW_THRESHOLD])) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py new file mode 100644 index 0000000000..4a1a26cc0b --- /dev/null +++ b/esphome/components/lvgl/__init__.py @@ -0,0 +1,395 @@ +import logging + +from esphome.automation import build_automation, register_action, validate_automation +import esphome.codegen as cg +from esphome.components.display import Display +import esphome.config_validation as cv +from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, + CONF_BUFFER_SIZE, + CONF_ID, + CONF_LAMBDA, + CONF_ON_IDLE, + CONF_PAGES, + CONF_TIMEOUT, + CONF_TRIGGER_ID, + CONF_TYPE, +) +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj +from esphome.final_validate import full_config +from esphome.helpers import write_file_if_changed + +from . import defines as df, helpers, lv_validation as lvalid +from .automation import disp_update, focused_widgets, update_to_code +from .defines import add_define +from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code +from .gradient import GRADIENT_SCHEMA, gradients_to_code +from .hello_world import get_hello_world +from .lv_validation import lv_bool, lv_images_used +from .lvcode import LvContext, LvglComponent +from .schemas import ( + DISP_BG_SCHEMA, + FLEX_OBJ_SCHEMA, + GRID_CELL_SCHEMA, + LAYOUT_SCHEMAS, + STYLE_SCHEMA, + WIDGET_TYPES, + any_widget_schema, + container_schema, + create_modify_schema, + grid_alignments, + obj_schema, +) +from .styles import add_top_layer, styles_to_code, theme_to_code +from .touchscreens import touchscreen_schema, touchscreens_to_code +from .trigger import generate_triggers +from .types import ( + FontEngine, + IdleTrigger, + ObjUpdateAction, + PauseTrigger, + lv_font_t, + lv_group_t, + lv_style_t, + lvgl_ns, +) +from .widgets import Widget, add_widgets, get_scr_act, set_obj_properties, styles_used +from .widgets.animimg import animimg_spec +from .widgets.arc import arc_spec +from .widgets.button import button_spec +from .widgets.buttonmatrix import buttonmatrix_spec +from .widgets.checkbox import checkbox_spec +from .widgets.dropdown import dropdown_spec +from .widgets.img import img_spec +from .widgets.keyboard import keyboard_spec +from .widgets.label import label_spec +from .widgets.led import led_spec +from .widgets.line import line_spec +from .widgets.lv_bar import bar_spec +from .widgets.meter import meter_spec +from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code +from .widgets.obj import obj_spec +from .widgets.page import add_pages, generate_page_triggers, page_spec +from .widgets.qrcode import qr_code_spec +from .widgets.roller import roller_spec +from .widgets.slider import slider_spec +from .widgets.spinbox import spinbox_spec +from .widgets.spinner import spinner_spec +from .widgets.switch import switch_spec +from .widgets.tabview import tabview_spec +from .widgets.textarea import textarea_spec +from .widgets.tileview import tileview_spec + +DOMAIN = "lvgl" +DEPENDENCIES = ["display"] +AUTO_LOAD = ["key_provider"] +CODEOWNERS = ["@clydebarrow"] +LOGGER = logging.getLogger(__name__) + +for w_type in ( + label_spec, + obj_spec, + button_spec, + bar_spec, + slider_spec, + arc_spec, + line_spec, + spinner_spec, + led_spec, + animimg_spec, + checkbox_spec, + img_spec, + switch_spec, + tabview_spec, + buttonmatrix_spec, + meter_spec, + dropdown_spec, + roller_spec, + textarea_spec, + spinbox_spec, + keyboard_spec, + tileview_spec, + qr_code_spec, +): + WIDGET_TYPES[w_type.name] = w_type + +WIDGET_SCHEMA = any_widget_schema() + +LAYOUT_SCHEMAS[df.TYPE_GRID] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA)) +} +LAYOUT_SCHEMAS[df.TYPE_FLEX] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA)) +} +LAYOUT_SCHEMAS[df.TYPE_NONE] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema()) +} +for w_type in WIDGET_TYPES.values(): + register_action( + f"lvgl.{w_type.name}.update", + ObjUpdateAction, + create_modify_schema(w_type), + )(update_to_code) + + +def as_macro(macro, value): + if value is None: + return f"#define {macro}" + return f"#define {macro} {value}" + + +LV_CONF_FILENAME = "lv_conf.h" +LV_CONF_H_FORMAT = """\ +#pragma once +{} +""" + + +def generate_lv_conf_h(): + definitions = [as_macro(m, v) for m, v in df.lv_defines.items()] + definitions.sort() + return LV_CONF_H_FORMAT.format("\n".join(definitions)) + + +def final_validation(config): + if pages := config.get(CONF_PAGES): + if all(p[df.CONF_SKIP] for p in pages): + raise cv.Invalid("At least one page must not be skipped") + global_config = full_config.get() + for display_id in config[df.CONF_DISPLAYS]: + path = global_config.get_path_for_id(display_id)[:-1] + display = global_config.get_config_for_path(path) + if CONF_LAMBDA in display: + raise cv.Invalid("Using lambda: in display config not compatible with LVGL") + if display[CONF_AUTO_CLEAR_ENABLED]: + raise cv.Invalid( + "Using auto_clear_enabled: true in display config not compatible with LVGL" + ) + buffer_frac = config[CONF_BUFFER_SIZE] + if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: + LOGGER.warning("buffer_size: may need to be reduced without PSRAM") + for image_id in lv_images_used: + path = global_config.get_path_for_id(image_id)[:-1] + image_conf = global_config.get_config_for_path(path) + if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): + raise cv.Invalid( + "Using RGBA or RGB24 in image config not compatible with LVGL", path + ) + for w in focused_widgets: + path = global_config.get_path_for_id(w) + widget_conf = global_config.get_config_for_path(path[:-1]) + if df.CONF_ADJUSTABLE in widget_conf and not widget_conf[df.CONF_ADJUSTABLE]: + raise cv.Invalid( + "A non adjustable arc may not be focused", + path, + ) + + +async def to_code(config): + cg.add_library("lvgl/lvgl", "8.4.0") + cg.add_define("USE_LVGL") + # suppress default enabling of extra widgets + add_define("_LV_KCONFIG_PRESENT") + # Always enable - lots of things use it. + add_define("LV_DRAW_COMPLEX", "1") + add_define("LV_TICK_CUSTOM", "1") + add_define("LV_TICK_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') + add_define("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(lv_millis())") + add_define("LV_MEM_CUSTOM", "1") + add_define("LV_MEM_CUSTOM_ALLOC", "lv_custom_mem_alloc") + add_define("LV_MEM_CUSTOM_FREE", "lv_custom_mem_free") + add_define("LV_MEM_CUSTOM_REALLOC", "lv_custom_mem_realloc") + add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') + + add_define( + "LV_LOG_LEVEL", f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config[df.CONF_LOG_LEVEL]]}" + ) + cg.add_define( + "LVGL_LOG_LEVEL", + cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config[df.CONF_LOG_LEVEL]}"), + ) + add_define("LV_COLOR_DEPTH", config[df.CONF_COLOR_DEPTH]) + for font in helpers.lv_fonts_used: + add_define(f"LV_FONT_{font.upper()}") + + if config[df.CONF_COLOR_DEPTH] == 16: + add_define( + "LV_COLOR_16_SWAP", + "1" if config[df.CONF_BYTE_ORDER] == "big_endian" else "0", + ) + add_define( + "LV_COLOR_CHROMA_KEY", + await lvalid.lv_color.process(config[df.CONF_TRANSPARENCY_KEY]), + ) + cg.add_build_flag("-Isrc") + + cg.add_global(lvgl_ns.using) + frac = config[CONF_BUFFER_SIZE] + if frac >= 0.75: + frac = 1 + elif frac >= 0.375: + frac = 2 + elif frac > 0.19: + frac = 4 + else: + frac = 8 + displays = [await cg.get_variable(display) for display in config[df.CONF_DISPLAYS]] + lv_component = cg.new_Pvariable( + config[CONF_ID], + displays, + frac, + config[df.CONF_FULL_REFRESH], + config[df.CONF_DRAW_ROUNDING], + config[df.CONF_RESUME_ON_INPUT], + ) + await cg.register_component(lv_component, config) + Widget.create(config[CONF_ID], lv_component, obj_spec, config) + + for font in helpers.esphome_fonts_used: + await cg.get_variable(font) + cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) + default_font = config[df.CONF_DEFAULT_FONT] + if not lvalid.is_lv_font(default_font): + add_define( + "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})" + ) + globfont_id = ID( + df.DEFAULT_ESPHOME_FONT, + True, + type=lv_font_t.operator("ptr").operator("const"), + ) + cg.new_variable( + globfont_id, MockObj(await lvalid.lv_font.process(default_font)) + ) + add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) + else: + add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) + + lv_scr_act = get_scr_act(lv_component) + async with LvContext(lv_component): + await touchscreens_to_code(lv_component, config) + await encoders_to_code(lv_component, config) + await theme_to_code(config) + await styles_to_code(config) + await gradients_to_code(config) + await set_obj_properties(lv_scr_act, config) + await add_widgets(lv_scr_act, config) + await add_pages(lv_component, config) + await add_top_layer(lv_component, config) + await msgboxes_to_code(lv_component, config) + await disp_update(lv_component.get_disp(), config) + # Set this directly since we are limited in how many methods can be added to the Widget class. + Widget.widgets_completed = True + async with LvContext(lv_component): + await generate_triggers(lv_component) + await generate_page_triggers(lv_component, config) + await initial_focus_to_code(config) + for conf in config.get(CONF_ON_IDLE, ()): + templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) + idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) + await build_automation(idle_trigger, [], conf) + for conf in config.get(df.CONF_ON_PAUSE, ()): + pause_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, True) + await build_automation(pause_trigger, [], conf) + for conf in config.get(df.CONF_ON_RESUME, ()): + resume_trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], lv_component, False + ) + await build_automation(resume_trigger, [], conf) + + for comp in helpers.lvgl_components_required: + cg.add_define(f"USE_LVGL_{comp.upper()}") + if "transform_angle" in styles_used: + add_define("LV_COLOR_SCREEN_TRANSP", "1") + for use in helpers.lv_uses: + add_define(f"LV_USE_{use.upper()}") + lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) + write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) + cg.add_build_flag("-DLV_CONF_H=1") + cg.add_build_flag(f'-DLV_CONF_PATH="{LV_CONF_FILENAME}"') + + +def display_schema(config): + value = cv.ensure_list(cv.use_id(Display))(config) + return value or [cv.use_id(Display)(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] = cv.ensure_list(WIDGET_SCHEMA)(get_hello_world()) + return config + + +FINAL_VALIDATE_SCHEMA = final_validation + +CONFIG_SCHEMA = ( + cv.polling_component_schema("1s") + .extend(obj_schema(obj_spec)) + .extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), + cv.GenerateID(df.CONF_DISPLAYS): display_schema, + cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16), + cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font, + cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, + cv.Optional(df.CONF_DRAW_ROUNDING, default=2): cv.positive_int, + cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage, + cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of( + *df.LV_LOG_LEVELS, upper=True + ), + cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( + "big_endian", "little_endian" + ), + cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}) + .extend(STYLE_SCHEMA) + .extend( + { + cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + } + ) + ), + cv.Optional(CONF_ON_IDLE): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), + cv.Optional(df.CONF_ON_PAUSE): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), + } + ), + cv.Optional(df.CONF_ON_RESUME): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), + } + ), + cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA), + cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( + container_schema(page_spec) + ), + cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), + cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, + cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), + cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, + cv.Optional(df.CONF_THEME): cv.Schema( + {cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()} + ), + cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA, + cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, + cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, + cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t), + cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean, + } + ) + .extend(DISP_BG_SCHEMA) + .add_extra(add_hello_world) +) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py new file mode 100644 index 0000000000..48472354f8 --- /dev/null +++ b/esphome/components/lvgl/automation.py @@ -0,0 +1,323 @@ +from collections.abc import Awaitable +from typing import Callable + +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT +from esphome.cpp_generator import get_variable +from esphome.cpp_types import nullptr + +from .defines import ( + CONF_DISP_BG_COLOR, + CONF_DISP_BG_IMAGE, + CONF_EDITING, + CONF_FREEZE, + CONF_LVGL_ID, + CONF_SHOW_SNOW, + literal, +) +from .lv_validation import lv_bool, lv_color, lv_image +from .lvcode import ( + LVGL_COMP_ARG, + UPDATE_EVENT, + LambdaContext, + LocalVariable, + LvglComponent, + ReturnStatement, + add_line_marks, + lv, + lv_add, + lv_expr, + lv_obj, + lvgl_comp, + static_cast, +) +from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA +from .types import ( + LV_STATE, + LvglAction, + LvglCondition, + ObjUpdateAction, + lv_disp_t, + lv_group_t, + lv_obj_t, + lv_pseudo_button_t, +) +from .widgets import ( + Widget, + get_scr_act, + get_widgets, + set_obj_properties, + wait_for_widgets, +) + +# Record widgets that are used in a focused action here +focused_widgets = set() + + +async def action_to_code( + widgets: list[Widget], + action: Callable[[Widget], Awaitable[None]], + action_id, + template_arg, + args, +): + await wait_for_widgets() + async with LambdaContext(parameters=args, where=action_id) as context: + for widget in widgets: + await action(widget) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + return var + + +async def update_to_code(config, action_id, template_arg, args): + async def do_update(widget: Widget): + await set_obj_properties(widget, config) + await widget.type.to_code(widget, config) + if ( + widget.type.w_type.value_property is not None + and widget.type.w_type.value_property in config + ): + lv.event_send(widget.obj, UPDATE_EVENT, nullptr) + + widgets = await get_widgets(config[CONF_ID]) + return await action_to_code(widgets, do_update, action_id, template_arg, args) + + +@automation.register_condition( + "lvgl.is_paused", + LvglCondition, + LVGL_SCHEMA, +) +async def lvgl_is_paused(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: + lv_add(ReturnStatement(lvgl_comp.is_paused())) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +@automation.register_condition( + "lvgl.is_idle", + LvglCondition, + LVGL_SCHEMA.extend( + { + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ) + } + ), +) +async def lvgl_is_idle(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) + async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: + lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +async def disp_update(disp, config: dict): + if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: + return + with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp: + if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: + lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) + if bg_image := config.get(CONF_DISP_BG_IMAGE): + lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) + + +@automation.register_action( + "lvgl.widget.redraw", + ObjUpdateAction, + cv.Any( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_obj_t), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + }, + key=CONF_ID, + ), + cv.Schema( + { + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + } + ), + ), +) +async def obj_invalidate_to_code(config, action_id, template_arg, args): + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) + widgets = await get_widgets(config) or [get_scr_act(lv_comp)] + + async def do_invalidate(widget: Widget): + lv_obj.invalidate(widget.obj) + + return await action_to_code(widgets, do_invalidate, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.update", + LvglAction, + DISP_BG_SCHEMA.extend( + { + cv.GenerateID(): cv.use_id(LvglComponent), + } + ).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)), +) +async def lvgl_update_to_code(config, action_id, template_arg, args): + widgets = await get_widgets(config) + w = widgets[0] + disp = literal(f"{w.obj}->get_disp()") + async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: + await disp_update(disp, config) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, w.var) + return var + + +@automation.register_action( + "lvgl.pause", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, + }, +) +async def pause_action_to_code(config, action_id, template_arg, args): + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(where=action_id) + lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "lvgl.resume", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + }, +) +async def resume_action_to_code(config, action_id, template_arg, args): + async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: + lv_add(lvgl_comp.set_paused(False, False)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action("lvgl.widget.disable", ObjUpdateAction, LIST_ACTION_SCHEMA) +async def obj_disable_to_code(config, action_id, template_arg, args): + async def do_disable(widget: Widget): + widget.add_state(LV_STATE.DISABLED) + + return await action_to_code( + await get_widgets(config), do_disable, action_id, template_arg, args + ) + + +@automation.register_action("lvgl.widget.enable", ObjUpdateAction, LIST_ACTION_SCHEMA) +async def obj_enable_to_code(config, action_id, template_arg, args): + async def do_enable(widget: Widget): + widget.clear_state(LV_STATE.DISABLED) + + return await action_to_code( + await get_widgets(config), do_enable, action_id, template_arg, args + ) + + +@automation.register_action("lvgl.widget.hide", ObjUpdateAction, LIST_ACTION_SCHEMA) +async def obj_hide_to_code(config, action_id, template_arg, args): + async def do_hide(widget: Widget): + widget.add_flag("LV_OBJ_FLAG_HIDDEN") + + widgets = [ + widget.outer if widget.outer else widget for widget in await get_widgets(config) + ] + return await action_to_code(widgets, do_hide, action_id, template_arg, args) + + +@automation.register_action("lvgl.widget.show", ObjUpdateAction, LIST_ACTION_SCHEMA) +async def obj_show_to_code(config, action_id, template_arg, args): + async def do_show(widget: Widget): + widget.clear_flag("LV_OBJ_FLAG_HIDDEN") + if widget.move_to_foreground: + lv_obj.move_foreground(widget.obj) + + widgets = [ + widget.outer if widget.outer else widget for widget in await get_widgets(config) + ] + return await action_to_code(widgets, do_show, action_id, template_arg, args) + + +def focused_id(value): + value = cv.use_id(lv_pseudo_button_t)(value) + focused_widgets.add(value) + return value + + +@automation.register_action( + "lvgl.widget.focus", + ObjUpdateAction, + cv.Any( + cv.maybe_simple_value( + { + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), + cv.Required(CONF_ACTION): cv.one_of( + "MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True + ), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + cv.Optional(CONF_FREEZE, default=False): cv.boolean, + }, + key=CONF_ACTION, + ), + cv.maybe_simple_value( + { + cv.Required(CONF_ID): focused_id, + cv.Optional(CONF_FREEZE, default=False): cv.boolean, + cv.Optional(CONF_EDITING, default=False): cv.boolean, + }, + key=CONF_ID, + ), + ), +) +async def widget_focus(config, action_id, template_arg, args): + widget = await get_widgets(config) + if widget: + widget = widget[0] + group = static_cast( + lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj) + ) + elif group := config.get(CONF_GROUP): + group = await get_variable(group) + else: + group = lv_expr.group_get_default() + + async with LambdaContext(parameters=args, where=action_id) as context: + if widget: + lv.group_focus_freeze(group, False) + lv.group_focus_obj(widget.obj) + if config[CONF_EDITING]: + lv.group_set_editing(group, True) + else: + action = config[CONF_ACTION] + lv_comp = await get_variable(config[CONF_LVGL_ID]) + if action == "MARK": + context.add(lv_comp.set_focus_mark(group)) + else: + lv.group_focus_freeze(group, False) + if action == "RESTORE": + context.add(lv_comp.restore_focus_mark(group)) + elif action == "NEXT": + lv.group_focus_next(group) + else: + lv.group_focus_prev(group) + + if config[CONF_FREEZE]: + lv.group_focus_freeze(group, True) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + return var diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py new file mode 100644 index 0000000000..56984405aa --- /dev/null +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import ( + BinarySensor, + binary_sensor_schema, + new_binary_sensor, +) +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import EVENT_ARG, LambdaContext, LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, lv_pseudo_button_t +from ..widgets import Widget, get_widgets, wait_for_widgets + +CONFIG_SCHEMA = ( + binary_sensor_schema(BinarySensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } + ) +) + + +async def to_code(config): + sensor = await new_binary_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + assert isinstance(widget, Widget) + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as pressed_ctx: + pressed_ctx.add(sensor.publish_state(widget.is_pressed())) + async with LvContext(paren) as ctx: + ctx.add(sensor.publish_initial_state(widget.is_pressed())) + ctx.add( + paren.add_event_cb( + widget.obj, + await pressed_ctx.get_lambda(), + LV_EVENT.PRESSING, + LV_EVENT.RELEASED, + ) + ) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py new file mode 100644 index 0000000000..4d48028611 --- /dev/null +++ b/esphome/components/lvgl/defines.py @@ -0,0 +1,541 @@ +""" +This is the base of the import tree for LVGL. It contains constant definitions used elsewhere. +Constants already defined in esphome.const are not duplicated here and must be imported where used. + +""" + +import logging + +from esphome import codegen as cg, config_validation as cv +from esphome.const import CONF_ITEMS +from esphome.core import Lambda +from esphome.cpp_generator import LambdaExpression, MockObj +from esphome.cpp_types import uint32 +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor + +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 + + +def add_define(macro, value="1"): + if macro in lv_defines and lv_defines[macro] != value: + LOGGER.error( + "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value + ) + lv_defines[macro] = value + + +def literal(arg): + if isinstance(arg, str): + return MockObj(arg) + return arg + + +def call_lambda(lamb: LambdaExpression): + expr = lamb.content.strip() + if expr.startswith("return") and expr.endswith(";"): + return expr[7:][:-1] + return f"{lamb}()" + + +class LValidator: + """ + A validator for a particular type used in LVGL. Usable in configs as a validator, also + has `process()` to convert a value during code generation + """ + + def __init__(self, validator, rtype, retmapper=None, requires=None): + self.validator = validator + self.rtype = rtype + self.retmapper = retmapper + self.requires = requires + + def __call__(self, value): + if self.requires: + value = requires_component(self.requires)(value) + if isinstance(value, cv.Lambda): + return cv.returning_lambda(value) + return self.validator(value) + + async def process(self, value, args=()): + if value is None: + return None + if isinstance(value, Lambda): + return cg.RawExpression( + call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) + ) + if self.retmapper is not None: + return self.retmapper(value) + return cg.safe_exp(value) + + +class LvConstant(LValidator): + """ + Allow one of a list of choices, mapped to upper case, and prepend the choice with the prefix. + It's also permitted to include the prefix in the value + The property `one_of` has the single case validator, and `several_of` allows a list of constants. + """ + + def __init__(self, prefix: str, *choices): + self.prefix = prefix + self.choices = choices + prefixed_choices = [prefix + v for v in choices] + prefixed_validator = cv.one_of(*prefixed_choices, upper=True) + + @schema_extractor("one_of") + def validator(value): + if value == SCHEMA_EXTRACT: + return self.choices + if isinstance(value, str) and value.startswith(self.prefix): + return prefixed_validator(value) + return self.prefix + cv.one_of(*choices, upper=True)(value) + + super().__init__(validator, rtype=uint32) + self.retmapper = self.mapper + self.one_of = LValidator(validator, uint32, retmapper=self.mapper) + self.several_of = LValidator( + cv.ensure_list(self.one_of), uint32, retmapper=self.mapper + ) + + def mapper(self, value): + if not isinstance(value, list): + value = [value] + return literal( + "|".join( + [ + str(v) if str(v).startswith(self.prefix) else self.prefix + str(v) + for v in value + ] + ).upper() + ) + + def extend(self, *choices): + """ + Extend an LVconstant with additional choices. + :param choices: The extra choices + :return: A new LVConstant instance + """ + return LvConstant(self.prefix, *(self.choices + choices)) + + +# Parts +CONF_MAIN = "main" +CONF_SCROLLBAR = "scrollbar" +CONF_INDICATOR = "indicator" +CONF_KNOB = "knob" +CONF_SELECTED = "selected" +CONF_TICKS = "ticks" +CONF_CURSOR = "cursor" +CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder" + +# Layout types + +TYPE_FLEX = "flex" +TYPE_GRID = "grid" +TYPE_NONE = "none" + +LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ + "dejavu_16_persian_hebrew", + "simsun_16_cjk", + "unscii_8", + "unscii_16", +] + +LV_EVENT_MAP = { + "PRESS": "PRESSED", + "SHORT_CLICK": "SHORT_CLICKED", + "LONG_PRESS": "LONG_PRESSED", + "LONG_PRESS_REPEAT": "LONG_PRESSED_REPEAT", + "CLICK": "CLICKED", + "RELEASE": "RELEASED", + "SCROLL_BEGIN": "SCROLL_BEGIN", + "SCROLL_END": "SCROLL_END", + "SCROLL": "SCROLL", + "FOCUS": "FOCUSED", + "DEFOCUS": "DEFOCUSED", + "READY": "READY", + "CANCEL": "CANCEL", + "ALL_EVENTS": "ALL", +} + +LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) + + +LV_ANIM = LvConstant( + "LV_SCR_LOAD_ANIM_", + "NONE", + "OVER_LEFT", + "OVER_RIGHT", + "OVER_TOP", + "OVER_BOTTOM", + "MOVE_LEFT", + "MOVE_RIGHT", + "MOVE_TOP", + "MOVE_BOTTOM", + "FADE_IN", + "FADE_OUT", + "OUT_LEFT", + "OUT_RIGHT", + "OUT_TOP", + "OUT_BOTTOM", +) + +LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER") +LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF") + +LV_LOG_LEVELS = { + "VERBOSE": "TRACE", + "DEBUG": "TRACE", + "INFO": "INFO", + "WARN": "WARN", + "ERROR": "ERROR", + "NONE": "NONE", +} + +LV_LONG_MODES = LvConstant( + "LV_LABEL_LONG_", + "WRAP", + "DOT", + "SCROLL", + "SCROLL_CIRCULAR", + "CLIP", +) + +STATES = ( + "default", + "checked", + "focused", + "focus_key", + "edited", + "hovered", + "pressed", + "scrolled", + "disabled", + "user_1", + "user_2", + "user_3", + "user_4", +) + +PARTS = ( + CONF_MAIN, + CONF_SCROLLBAR, + CONF_INDICATOR, + CONF_KNOB, + CONF_SELECTED, + CONF_ITEMS, + CONF_TICKS, + CONF_CURSOR, + CONF_TEXTAREA_PLACEHOLDER, +) + +KEYBOARD_MODES = LvConstant( + "LV_KEYBOARD_MODE_", + "TEXT_LOWER", + "TEXT_UPPER", + "SPECIAL", + "NUMBER", +) +ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE") +DIRECTIONS = LvConstant("LV_DIR_", "LEFT", "RIGHT", "BOTTOM", "TOP") +TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL") +CHILD_ALIGNMENTS = LvConstant( + "LV_ALIGN_", + "TOP_LEFT", + "TOP_MID", + "TOP_RIGHT", + "LEFT_MID", + "CENTER", + "RIGHT_MID", + "BOTTOM_LEFT", + "BOTTOM_MID", + "BOTTOM_RIGHT", +) + +SIBLING_ALIGNMENTS = LvConstant( + "LV_ALIGN_", + "OUT_LEFT_TOP", + "OUT_TOP_LEFT", + "OUT_TOP_MID", + "OUT_TOP_RIGHT", + "OUT_RIGHT_TOP", + "OUT_LEFT_MID", + "OUT_RIGHT_MID", + "OUT_LEFT_BOTTOM", + "OUT_BOTTOM_LEFT", + "OUT_BOTTOM_MID", + "OUT_BOTTOM_RIGHT", + "OUT_RIGHT_BOTTOM", +) +ALIGN_ALIGNMENTS = CHILD_ALIGNMENTS.extend(*SIBLING_ALIGNMENTS.choices) + +FLEX_FLOWS = LvConstant( + "LV_FLEX_FLOW_", + "ROW", + "COLUMN", + "ROW_WRAP", + "COLUMN_WRAP", + "ROW_REVERSE", + "COLUMN_REVERSE", + "ROW_WRAP_REVERSE", + "COLUMN_WRAP_REVERSE", +) + +OBJ_FLAGS = ( + "hidden", + "clickable", + "click_focusable", + "checkable", + "scrollable", + "scroll_elastic", + "scroll_momentum", + "scroll_one", + "scroll_chain_hor", + "scroll_chain_ver", + "scroll_chain", + "scroll_on_focus", + "scroll_with_arrow", + "snappable", + "press_lock", + "event_bubble", + "gesture_bubble", + "adv_hittest", + "ignore_layout", + "floating", + "overflow_visible", + "layout_1", + "layout_2", + "widget_1", + "widget_2", + "user_1", + "user_2", + "user_3", + "user_4", +) + +ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") +BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") + +BUTTONMATRIX_CTRLS = LvConstant( + "LV_BTNMATRIX_CTRL_", + "HIDDEN", + "NO_REPEAT", + "DISABLED", + "CHECKABLE", + "CHECKED", + "CLICK_TRIG", + "POPOVER", + "RECOLOR", + "CUSTOM_1", + "CUSTOM_2", +) + +LV_BASE_ALIGNMENTS = ( + "START", + "CENTER", + "END", +) +LV_CELL_ALIGNMENTS = LvConstant( + "LV_GRID_ALIGN_", + *LV_BASE_ALIGNMENTS, +) +LV_GRID_ALIGNMENTS = LV_CELL_ALIGNMENTS.extend( + "STRETCH", + "SPACE_EVENLY", + "SPACE_AROUND", + "SPACE_BETWEEN", +) + +LV_FLEX_ALIGNMENTS = LvConstant( + "LV_FLEX_ALIGN_", + *LV_BASE_ALIGNMENTS, + "SPACE_EVENLY", + "SPACE_AROUND", + "SPACE_BETWEEN", +) + +LV_MENU_MODES = LvConstant( + "LV_MENU_HEADER_", + "TOP_FIXED", + "TOP_UNFIXED", + "BOTTOM_FIXED", +) + +LV_CHART_TYPES = ( + "NONE", + "LINE", + "BAR", + "SCATTER", +) +LV_CHART_AXES = ( + "PRIMARY_Y", + "SECONDARY_Y", + "PRIMARY_X", + "SECONDARY_X", +) + +CONF_ACCEPTED_CHARS = "accepted_chars" +CONF_ADJUSTABLE = "adjustable" +CONF_ALIGN = "align" +CONF_ALIGN_TO = "align_to" +CONF_ANIMATED = "animated" +CONF_ANIMATION = "animation" +CONF_ANTIALIAS = "antialias" +CONF_ARC_LENGTH = "arc_length" +CONF_AUTO_START = "auto_start" +CONF_BACKGROUND_STYLE = "background_style" +CONF_BUTTON_STYLE = "button_style" +CONF_DECIMAL_PLACES = "decimal_places" +CONF_COLUMN = "column" +CONF_DIGITS = "digits" +CONF_DISP_BG_COLOR = "disp_bg_color" +CONF_DISP_BG_IMAGE = "disp_bg_image" +CONF_BODY = "body" +CONF_BUTTONS = "buttons" +CONF_BYTE_ORDER = "byte_order" +CONF_CHANGE_RATE = "change_rate" +CONF_CLOSE_BUTTON = "close_button" +CONF_COLOR_DEPTH = "color_depth" +CONF_CONTROL = "control" +CONF_DEFAULT = "default" +CONF_DEFAULT_FONT = "default_font" +CONF_DEFAULT_GROUP = "default_group" +CONF_DIR = "dir" +CONF_DISPLAYS = "displays" +CONF_DRAW_ROUNDING = "draw_rounding" +CONF_EDITING = "editing" +CONF_ENCODERS = "encoders" +CONF_END_ANGLE = "end_angle" +CONF_END_VALUE = "end_value" +CONF_ENTER_BUTTON = "enter_button" +CONF_ENTRIES = "entries" +CONF_FLAGS = "flags" +CONF_FLEX_FLOW = "flex_flow" +CONF_FLEX_ALIGN_MAIN = "flex_align_main" +CONF_FLEX_ALIGN_CROSS = "flex_align_cross" +CONF_FLEX_ALIGN_TRACK = "flex_align_track" +CONF_FLEX_GROW = "flex_grow" +CONF_FREEZE = "freeze" +CONF_FULL_REFRESH = "full_refresh" +CONF_GRADIENTS = "gradients" +CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos" +CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos" +CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span" +CONF_GRID_CELL_COLUMN_SPAN = "grid_cell_column_span" +CONF_GRID_CELL_X_ALIGN = "grid_cell_x_align" +CONF_GRID_CELL_Y_ALIGN = "grid_cell_y_align" +CONF_GRID_COLUMN_ALIGN = "grid_column_align" +CONF_GRID_COLUMNS = "grid_columns" +CONF_GRID_ROW_ALIGN = "grid_row_align" +CONF_GRID_ROWS = "grid_rows" +CONF_HEADER_MODE = "header_mode" +CONF_HOME = "home" +CONF_INITIAL_FOCUS = "initial_focus" +CONF_KEY_CODE = "key_code" +CONF_LAYOUT = "layout" +CONF_LEFT_BUTTON = "left_button" +CONF_LINE_WIDTH = "line_width" +CONF_LOG_LEVEL = "log_level" +CONF_LONG_PRESS_TIME = "long_press_time" +CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" +CONF_LVGL_ID = "lvgl_id" +CONF_LONG_MODE = "long_mode" +CONF_MSGBOXES = "msgboxes" +CONF_OBJ = "obj" +CONF_OFFSET_X = "offset_x" +CONF_OFFSET_Y = "offset_y" +CONF_ONE_CHECKED = "one_checked" +CONF_ONE_LINE = "one_line" +CONF_ON_PAUSE = "on_pause" +CONF_ON_RESUME = "on_resume" +CONF_ON_SELECT = "on_select" +CONF_OPA = "opa" +CONF_NEXT = "next" +CONF_PAD_ROW = "pad_row" +CONF_PAD_COLUMN = "pad_column" +CONF_PAGE = "page" +CONF_PAGE_WRAP = "page_wrap" +CONF_PASSWORD_MODE = "password_mode" +CONF_PIVOT_X = "pivot_x" +CONF_PIVOT_Y = "pivot_y" +CONF_PLACEHOLDER_TEXT = "placeholder_text" +CONF_POINTS = "points" +CONF_PREVIOUS = "previous" +CONF_REPEAT_COUNT = "repeat_count" +CONF_RECOLOR = "recolor" +CONF_RESUME_ON_INPUT = "resume_on_input" +CONF_RIGHT_BUTTON = "right_button" +CONF_ROLLOVER = "rollover" +CONF_ROOT_BACK_BTN = "root_back_btn" +CONF_ROWS = "rows" +CONF_SCALE_LINES = "scale_lines" +CONF_SCROLLBAR_MODE = "scrollbar_mode" +CONF_SELECTED_INDEX = "selected_index" +CONF_SELECTED_TEXT = "selected_text" +CONF_SHOW_SNOW = "show_snow" +CONF_SPIN_TIME = "spin_time" +CONF_SRC = "src" +CONF_START_ANGLE = "start_angle" +CONF_START_VALUE = "start_value" +CONF_STATES = "states" +CONF_STYLE = "style" +CONF_STYLES = "styles" +CONF_STYLE_DEFINITIONS = "style_definitions" +CONF_STYLE_ID = "style_id" +CONF_SKIP = "skip" +CONF_SYMBOL = "symbol" +CONF_TAB_ID = "tab_id" +CONF_TABS = "tabs" +CONF_TIME_FORMAT = "time_format" +CONF_TILE = "tile" +CONF_TILE_ID = "tile_id" +CONF_TILES = "tiles" +CONF_TITLE = "title" +CONF_TOP_LAYER = "top_layer" +CONF_TOUCHSCREENS = "touchscreens" +CONF_TRANSPARENCY_KEY = "transparency_key" +CONF_THEME = "theme" +CONF_UPDATE_ON_RELEASE = "update_on_release" +CONF_VISIBLE_ROW_COUNT = "visible_row_count" +CONF_WIDGET = "widget" +CONF_WIDGETS = "widgets" +CONF_X = "x" +CONF_Y = "y" +CONF_ZOOM = "zoom" + +# Keypad keys + +LV_KEYS = LvConstant( + "LV_KEY_", + "UP", + "DOWN", + "RIGHT", + "LEFT", + "ESC", + "DEL", + "BACKSPACE", + "ENTER", + "NEXT", + "PREV", + "HOME", + "END", +) + + +DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" + + +def join_enums(enums, prefix=""): + enums = list(enums) + enums.sort() + # If a prefix is provided, prepend each constant with the prefix, and assume that all the constants are within the + # same namespace, otherwise cast to int to avoid triggering warnings about mixing enum types. + if prefix: + return literal("|".join(f"{prefix}{e.upper()}" for e in enums)) + return literal("|".join(f"(int){e.upper()}" for e in enums)) diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py new file mode 100644 index 0000000000..81bcda95b4 --- /dev/null +++ b/esphome/components/lvgl/encoders.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import BinarySensor +from esphome.components.rotary_encoder.sensor import RotaryEncoderSensor +import esphome.config_validation as cv +from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR + +from .defines import ( + CONF_DEFAULT_GROUP, + CONF_ENCODERS, + CONF_ENTER_BUTTON, + CONF_INITIAL_FOCUS, + CONF_LEFT_BUTTON, + CONF_LONG_PRESS_REPEAT_TIME, + CONF_LONG_PRESS_TIME, + CONF_RIGHT_BUTTON, +) +from .helpers import lvgl_components_required, requires_component +from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable +from .schemas import ENCODER_SCHEMA +from .types import lv_group_t, lv_indev_type_t + +ENCODERS_CONFIG = cv.ensure_list( + ENCODER_SCHEMA.extend( + { + cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_SENSOR): cv.Any( + cv.All( + cv.use_id(RotaryEncoderSensor), requires_component("rotary_encoder") + ), + cv.Schema( + { + cv.Required(CONF_LEFT_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_RIGHT_BUTTON): cv.use_id(BinarySensor), + } + ), + ), + } + ) +) + + +async def encoders_to_code(var, config): + default_group = lv_Pvariable(lv_group_t, config[CONF_DEFAULT_GROUP]) + lv_assign(default_group, lv_expr.group_create()) + lv.group_set_default(default_group) + for enc_conf in config[CONF_ENCODERS]: + lvgl_components_required.add("KEY_LISTENER") + lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds + lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds + listener = cg.new_Pvariable( + enc_conf[CONF_ID], lv_indev_type_t.LV_INDEV_TYPE_ENCODER, lpt, lprt + ) + await cg.register_parented(listener, var) + if sensor_config := enc_conf.get(CONF_SENSOR): + if isinstance(sensor_config, dict): + b_sensor = await cg.get_variable(sensor_config[CONF_LEFT_BUTTON]) + cg.add(listener.set_left_button(b_sensor)) + b_sensor = await cg.get_variable(sensor_config[CONF_RIGHT_BUTTON]) + cg.add(listener.set_right_button(b_sensor)) + else: + sensor_config = await cg.get_variable(sensor_config) + lv_add(listener.set_sensor(sensor_config)) + b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON]) + cg.add(listener.set_enter_button(b_sensor)) + if group := enc_conf.get(CONF_GROUP): + group = lv_Pvariable(lv_group_t, group) + lv_assign(group, lv_expr.group_create()) + else: + group = default_group + lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + + +async def initial_focus_to_code(config): + for enc_conf in config[CONF_ENCODERS]: + if default_focus := enc_conf.get(CONF_INITIAL_FOCUS): + obj = await cg.get_variable(default_focus) + lv.group_focus_obj(obj) diff --git a/esphome/components/lvgl/font.cpp b/esphome/components/lvgl/font.cpp new file mode 100644 index 0000000000..9c172f07f5 --- /dev/null +++ b/esphome/components/lvgl/font.cpp @@ -0,0 +1,76 @@ +#include "lvgl_esphome.h" + +#ifdef USE_LVGL_FONT +namespace esphome { +namespace lvgl { + +static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { + auto *fe = (FontEngine *) font->dsc; + const auto *gd = fe->get_glyph_data(unicode_letter); + if (gd == nullptr) + return nullptr; + // esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data); + + return gd->data; +} + +static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { + auto *fe = (FontEngine *) font->dsc; + const auto *gd = fe->get_glyph_data(unicode_letter); + if (gd == nullptr) + return false; + dsc->adv_w = gd->offset_x + gd->width; + dsc->ofs_x = gd->offset_x; + dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; + dsc->box_w = gd->width; + dsc->box_h = gd->height; + dsc->is_placeholder = 0; + dsc->bpp = fe->bpp; + return true; +} + +FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) { + this->bpp = esp_font->get_bpp(); + this->lv_font_.dsc = this; + this->lv_font_.line_height = this->height = esp_font->get_height(); + this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline(); + this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; + this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; + this->lv_font_.subpx = LV_FONT_SUBPX_NONE; + this->lv_font_.underline_position = -1; + this->lv_font_.underline_thickness = 1; +} + +const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; } + +const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) { + if (unicode_letter == last_letter_) + return this->last_data_; + uint8_t unicode[5]; + memset(unicode, 0, sizeof unicode); + if (unicode_letter > 0xFFFF) { + unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7); + unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F); + unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F); + unicode[3] = 0x80 + (unicode_letter & 0x3F); + } else if (unicode_letter > 0x7FF) { + unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF); + unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F); + unicode[2] = 0x80 + (unicode_letter & 0x3F); + } else if (unicode_letter > 0x7F) { + unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F); + unicode[1] = 0x80 + (unicode_letter & 0x3F); + } else { + unicode[0] = unicode_letter; + } + int match_length; + int glyph_n = this->font_->match_next_glyph(unicode, &match_length); + if (glyph_n < 0) + return nullptr; + this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data(); + this->last_letter_ = unicode_letter; + return this->last_data_; +} +} // namespace lvgl +} // namespace esphome +#endif // USES_LVGL_FONT diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py new file mode 100644 index 0000000000..bc89470d47 --- /dev/null +++ b/esphome/components/lvgl/gradient.py @@ -0,0 +1,61 @@ +from esphome import config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_COLOR, + CONF_DIRECTION, + CONF_DITHER, + CONF_ID, + CONF_POSITION, +) +from esphome.cpp_generator import MockObj + +from .defines import CONF_GRADIENTS, LV_DITHER, LV_GRAD_DIR, add_define +from .lv_validation import lv_color, lv_fraction +from .lvcode import lv_assign +from .types import lv_gradient_t + +CONF_STOPS = "stops" + + +def min_stops(value): + if len(value) < 2: + raise cv.Invalid("Must have at least 2 stops") + return value + + +GRADIENT_SCHEMA = cv.ensure_list( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t), + cv.Optional(CONF_DIRECTION, default="NONE"): LV_GRAD_DIR.one_of, + cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of, + cv.Required(CONF_STOPS): cv.All( + [ + cv.Schema( + { + cv.Required(CONF_COLOR): lv_color, + cv.Required(CONF_POSITION): lv_fraction, + } + ) + ], + min_stops, + ), + } + ) +) + + +async def gradients_to_code(config): + max_stops = 2 + for gradient in config.get(CONF_GRADIENTS, ()): + var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->") + max_stops = max(max_stops, len(gradient[CONF_STOPS])) + lv_assign(var.dir, await LV_GRAD_DIR.process(gradient[CONF_DIRECTION])) + lv_assign(var.dither, await LV_DITHER.process(gradient[CONF_DITHER])) + lv_assign(var.stops_count, len(gradient[CONF_STOPS])) + for index, stop in enumerate(gradient[CONF_STOPS]): + lv_assign(var.stops[index].color, await lv_color.process(stop[CONF_COLOR])) + lv_assign( + var.stops[index].frac, await lv_fraction.process(stop[CONF_POSITION]) + ) + add_define("LV_GRADIENT_MAX_STOPS", max_stops) diff --git a/esphome/components/lvgl/hello_world.py b/esphome/components/lvgl/hello_world.py new file mode 100644 index 0000000000..2c2ec6732c --- /dev/null +++ b/esphome/components/lvgl/hello_world.py @@ -0,0 +1,64 @@ +from io import StringIO + +from esphome.yaml_util import parse_yaml + +CONFIG = """ +- obj: + radius: 0 + pad_all: 12 + bg_color: 0xFFFFFF + height: 100% + width: 100% + widgets: + - spinner: + id: hello_world_spinner_ + align: center + indicator: + arc_color: tomato + height: 100 + width: 100 + spin_time: 2s + arc_length: 60deg + - label: + id: hello_world_label_ + text: "Hello World!" + align: center + on_click: + lvgl.spinner.update: + id: hello_world_spinner_ + arc_color: springgreen + - checkbox: + pad_all: 8 + text: Checkbox + align: top_right + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Checked!" + - button: + pad_all: 8 + checkable: true + align: top_left + text_font: montserrat_20 + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Clicked!" + widgets: + - label: + text: "Button" + - 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/helpers.py b/esphome/components/lvgl/helpers.py new file mode 100644 index 0000000000..e04a0105d5 --- /dev/null +++ b/esphome/components/lvgl/helpers.py @@ -0,0 +1,49 @@ +import re + +from esphome import config_validation as cv +from esphome.const import CONF_ARGS, CONF_FORMAT + +lv_uses = { + "USER_DATA", + "LOG", + "STYLE", + "FONT_PLACEHOLDER", + "THEME_DEFAULT", +} + + +def add_lv_use(*names): + for name in names: + lv_uses.add(name) + + +lv_fonts_used = set() +esphome_fonts_used = set() +lvgl_components_required = set() + + +def validate_printf(value): + cfmt = r""" + ( # start of capture group 1 + % # literal "%" + (?:[-+0 #]{0,5}) # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:h|l|ll|w|I|I32|I64)? # size + [cCdiouxXeEfgGaAnpsSZ] # type + ) + """ # noqa + matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X) + if len(matches) != len(value[CONF_ARGS]): + raise cv.Invalid( + f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" + ) + return value + + +def requires_component(comp): + def validator(value): + lvgl_components_required.add(comp) + return cv.requires_component(comp)(value) + + return validator diff --git a/esphome/components/lvgl/light/__init__.py b/esphome/components/lvgl/light/__init__.py new file mode 100644 index 0000000000..8031ae8221 --- /dev/null +++ b/esphome/components/lvgl/light/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import light +from esphome.components.light import LightOutput +import esphome.config_validation as cv +from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LvType, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +lv_led_t = LvType("lv_led_t") +LVLight = lvgl_ns.class_("LVLight", LightOutput) +CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( + { + cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float, + cv.Required(CONF_WIDGET): cv.use_id(lv_led_t), + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight), + } +).extend(LVGL_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + await wait_for_widgets() + async with LvContext(paren) as ctx: + ctx.add(var.set_obj(widget.obj)) diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h new file mode 100644 index 0000000000..50ae4c5327 --- /dev/null +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/light_output.h" +#include "../lvgl_esphome.h" + +namespace esphome { +namespace lvgl { + +class LVLight : public light::LightOutput { + public: + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + void write_state(light::LightState *state) override { + float red, green, blue; + state->current_values_as_rgb(&red, &green, &blue, false); + auto color = lv_color_make(red * 255, green * 255, blue * 255); + if (this->obj_ != nullptr) { + this->set_value_(color); + } else { + this->initial_value_ = color; + } + } + + void set_obj(lv_obj_t *obj) { + this->obj_ = obj; + if (this->initial_value_) { + lv_led_set_color(obj, this->initial_value_.value()); + lv_led_on(obj); + this->initial_value_.reset(); + } + } + + protected: + void set_value_(lv_color_t value) { + lv_led_set_color(this->obj_, value); + lv_led_on(this->obj_); + lv_event_send(this->obj_, lv_api_event, nullptr); + } + lv_obj_t *obj_{}; + optional initial_value_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py new file mode 100644 index 0000000000..b91b0905df --- /dev/null +++ b/esphome/components/lvgl/lv_validation.py @@ -0,0 +1,479 @@ +from typing import Union + +import esphome.codegen as cg +from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw +from esphome.components.font import Font +from esphome.components.image import Image_ +import esphome.config_validation as cv +from esphome.const import ( + CONF_ARGS, + CONF_COLOR, + CONF_FORMAT, + CONF_ID, + CONF_TIME, + CONF_VALUE, +) +from esphome.core import CORE, ID, Lambda +from esphome.cpp_generator import MockObj +from esphome.cpp_types import ESPTime, uint32 +from esphome.helpers import cpp_string_escape +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor + +from . import types as ty +from .defines import ( + CONF_END_VALUE, + CONF_START_VALUE, + CONF_TIME_FORMAT, + LV_FONTS, + LValidator, + LvConstant, + call_lambda, + literal, +) +from .helpers import esphome_fonts_used, lv_fonts_used, requires_component +from .types import lv_font_t, lv_gradient_t, lv_img_t + +opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") + + +@schema_extractor("one_of") +def opacity_validator(value): + if value == SCHEMA_EXTRACT: + return opacity_consts.choices + value = cv.Any(cv.percentage, opacity_consts.one_of)(value) + if isinstance(value, float): + return int(value * 255) + return value + + +opacity = LValidator(opacity_validator, uint32, retmapper=literal) + +COLOR_NAMES = { + "aliceblue": 0xF0F8FF, + "antiquewhite": 0xFAEBD7, + "aqua": 0x00FFFF, + "aquamarine": 0x7FFFD4, + "azure": 0xF0FFFF, + "beige": 0xF5F5DC, + "bisque": 0xFFE4C4, + "black": 0x000000, + "blanchedalmond": 0xFFEBCD, + "blue": 0x0000FF, + "blueviolet": 0x8A2BE2, + "brown": 0xA52A2A, + "burlywood": 0xDEB887, + "cadetblue": 0x5F9EA0, + "chartreuse": 0x7FFF00, + "chocolate": 0xD2691E, + "coral": 0xFF7F50, + "cornflowerblue": 0x6495ED, + "cornsilk": 0xFFF8DC, + "crimson": 0xDC143C, + "cyan": 0x00FFFF, + "darkblue": 0x00008B, + "darkcyan": 0x008B8B, + "darkgoldenrod": 0xB8860B, + "darkgray": 0xA9A9A9, + "darkgreen": 0x006400, + "darkgrey": 0xA9A9A9, + "darkkhaki": 0xBDB76B, + "darkmagenta": 0x8B008B, + "darkolivegreen": 0x556B2F, + "darkorange": 0xFF8C00, + "darkorchid": 0x9932CC, + "darkred": 0x8B0000, + "darksalmon": 0xE9967A, + "darkseagreen": 0x8FBC8F, + "darkslateblue": 0x483D8B, + "darkslategray": 0x2F4F4F, + "darkslategrey": 0x2F4F4F, + "darkturquoise": 0x00CED1, + "darkviolet": 0x9400D3, + "deeppink": 0xFF1493, + "deepskyblue": 0x00BFFF, + "dimgray": 0x696969, + "dimgrey": 0x696969, + "dodgerblue": 0x1E90FF, + "firebrick": 0xB22222, + "floralwhite": 0xFFFAF0, + "forestgreen": 0x228B22, + "fuchsia": 0xFF00FF, + "gainsboro": 0xDCDCDC, + "ghostwhite": 0xF8F8FF, + "goldenrod": 0xDAA520, + "gold": 0xFFD700, + "gray": 0x808080, + "green": 0x008000, + "greenyellow": 0xADFF2F, + "grey": 0x808080, + "honeydew": 0xF0FFF0, + "hotpink": 0xFF69B4, + "indianred": 0xCD5C5C, + "indigo": 0x4B0082, + "ivory": 0xFFFFF0, + "khaki": 0xF0E68C, + "lavenderblush": 0xFFF0F5, + "lavender": 0xE6E6FA, + "lawngreen": 0x7CFC00, + "lemonchiffon": 0xFFFACD, + "lightblue": 0xADD8E6, + "lightcoral": 0xF08080, + "lightcyan": 0xE0FFFF, + "lightgoldenrodyellow": 0xFAFAD2, + "lightgray": 0xD3D3D3, + "lightgreen": 0x90EE90, + "lightgrey": 0xD3D3D3, + "lightpink": 0xFFB6C1, + "lightsalmon": 0xFFA07A, + "lightseagreen": 0x20B2AA, + "lightskyblue": 0x87CEFA, + "lightslategray": 0x778899, + "lightslategrey": 0x778899, + "lightsteelblue": 0xB0C4DE, + "lightyellow": 0xFFFFE0, + "lime": 0x00FF00, + "limegreen": 0x32CD32, + "linen": 0xFAF0E6, + "magenta": 0xFF00FF, + "maroon": 0x800000, + "mediumaquamarine": 0x66CDAA, + "mediumblue": 0x0000CD, + "mediumorchid": 0xBA55D3, + "mediumpurple": 0x9370DB, + "mediumseagreen": 0x3CB371, + "mediumslateblue": 0x7B68EE, + "mediumspringgreen": 0x00FA9A, + "mediumturquoise": 0x48D1CC, + "mediumvioletred": 0xC71585, + "midnightblue": 0x191970, + "mintcream": 0xF5FFFA, + "mistyrose": 0xFFE4E1, + "moccasin": 0xFFE4B5, + "navajowhite": 0xFFDEAD, + "navy": 0x000080, + "oldlace": 0xFDF5E6, + "olive": 0x808000, + "olivedrab": 0x6B8E23, + "orange": 0xFFA500, + "orangered": 0xFF4500, + "orchid": 0xDA70D6, + "palegoldenrod": 0xEEE8AA, + "palegreen": 0x98FB98, + "paleturquoise": 0xAFEEEE, + "palevioletred": 0xDB7093, + "papayawhip": 0xFFEFD5, + "peachpuff": 0xFFDAB9, + "peru": 0xCD853F, + "pink": 0xFFC0CB, + "plum": 0xDDA0DD, + "powderblue": 0xB0E0E6, + "purple": 0x800080, + "rebeccapurple": 0x663399, + "red": 0xFF0000, + "rosybrown": 0xBC8F8F, + "royalblue": 0x4169E1, + "saddlebrown": 0x8B4513, + "salmon": 0xFA8072, + "sandybrown": 0xF4A460, + "seagreen": 0x2E8B57, + "seashell": 0xFFF5EE, + "sienna": 0xA0522D, + "silver": 0xC0C0C0, + "skyblue": 0x87CEEB, + "slateblue": 0x6A5ACD, + "slategray": 0x708090, + "slategrey": 0x708090, + "snow": 0xFFFAFA, + "springgreen": 0x00FF7F, + "steelblue": 0x4682B4, + "tan": 0xD2B48C, + "teal": 0x008080, + "thistle": 0xD8BFD8, + "tomato": 0xFF6347, + "turquoise": 0x40E0D0, + "violet": 0xEE82EE, + "wheat": 0xF5DEB3, + "white": 0xFFFFFF, + "whitesmoke": 0xF5F5F5, + "yellow": 0xFFFF00, + "yellowgreen": 0x9ACD32, +} + + +@schema_extractor("one_of") +def color(value): + if value == SCHEMA_EXTRACT: + return ["hex color value", "color ID"] + return cv.Any(cv.int_, cv.one_of(*COLOR_NAMES, lower=True), cv.use_id(ColorStruct))( + value + ) + + +def color_retmapper(value): + if isinstance(value, cv.Lambda): + return cv.returning_lambda(value) + if isinstance(value, str) and value in COLOR_NAMES: + value = COLOR_NAMES[value] + if isinstance(value, int): + return literal( + f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})" + ) + if isinstance(value, ID): + cval = [x for x in CORE.config[CONF_COLOR] if x[CONF_ID] == value][0] + if CONF_HEX in cval: + r, g, b = cval[CONF_HEX] + else: + r, g, b, _ = from_rgbw(cval) + return literal(f"lv_color_make({r}, {g}, {b})") + assert False + + +def option_string(value): + value = cv.string(value).strip() + if value.find("\n") != -1: + raise cv.Invalid("Options strings must not contain newlines") + return value + + +lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) + + +def pixels_or_percent_validator(value): + """A length in one axis - either a number (pixels) or a percentage""" + if value == SCHEMA_EXTRACT: + return ["pixels", "..%"] + if isinstance(value, str) and value.lower().endswith("px"): + value = cv.int_(value[:-2]) + value = cv.Any(cv.int_, cv.percentage)(value) + if isinstance(value, int): + return value + return f"lv_pct({int(value * 100)})" + + +pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal) + + +def zoom(value): + value = cv.float_range(0.1, 10.0)(value) + return int(value * 256) + + +def angle(value): + """ + Validation for an angle in degrees, converted to an integer representing 0.1deg units + :param value: The input in the range 0..360 + :return: An angle in 1/10 degree units. + """ + return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) + + +lv_angle = LValidator(angle, uint32) + + +@schema_extractor("one_of") +def size_validator(value): + """A size in one axis - one of "size_content", a number (pixels) or a percentage""" + if value == SCHEMA_EXTRACT: + return ["SIZE_CONTENT", "number of pixels", "percentage"] + if isinstance(value, str) and value.lower().endswith("px"): + value = cv.int_(value[:-2]) + if isinstance(value, str) and value.upper() == "SIZE_CONTENT": + return "LV_SIZE_CONTENT" + return pixels_or_percent_validator(value) + + +size = LValidator(size_validator, uint32, retmapper=literal) + + +def pixels_validator(value): + if isinstance(value, str) and value.lower().endswith("px"): + return cv.int_(value[:-2]) + return cv.int_(value) + + +pixels = LValidator(pixels_validator, uint32, retmapper=literal) + +radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") + + +@schema_extractor("one_of") +def fraction_validator(value): + if value == SCHEMA_EXTRACT: + return radius_consts.choices + value = cv.Any(size, cv.percentage, radius_consts.one_of)(value) + if isinstance(value, float): + return int(value * 255) + return value + + +lv_fraction = LValidator(fraction_validator, uint32, retmapper=literal) + + +def id_name(value): + if value == SCHEMA_EXTRACT: + return "id" + return cv.validate_id_name(value) + + +def stop_value(value): + return cv.int_range(0, 255)(value) + + +lv_images_used = set() + + +def image_validator(value): + value = requires_component("image")(value) + value = cv.use_id(Image_)(value) + lv_images_used.add(value) + return value + + +lv_image = LValidator( + image_validator, + lv_img_t, + retmapper=lambda x: MockObj(x, "->").get_lv_img_dsc(), + requires="image", +) +lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) + + +def lv_pct(value: Union[int, float]): + if isinstance(value, float): + value = int(value * 100) + return literal(f"lv_pct({value})") + + +def lvms_validator_(value): + if value == "never": + value = "2147483647ms" + return cv.positive_time_period_milliseconds(value) + + +lv_milliseconds = LValidator( + lvms_validator_, cg.int32, retmapper=lambda x: x.total_milliseconds +) + + +class TextValidator(LValidator): + def __init__(self): + super().__init__(cv.string, cg.std_string, lambda s: cg.safe_exp(f"{s}")) + + def __call__(self, value): + if isinstance(value, dict) and CONF_FORMAT in value: + return value + return super().__call__(value) + + async def process(self, value, args=()): + if isinstance(value, dict): + if format_str := value.get(CONF_FORMAT): + args = [str(x) for x in value[CONF_ARGS]] + arg_expr = cg.RawExpression(",".join(args)) + format_str = cpp_string_escape(format_str) + return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + if time_format := value.get(CONF_TIME_FORMAT): + source = value[CONF_TIME] + if isinstance(source, Lambda): + time_format = cpp_string_escape(time_format) + return cg.RawExpression( + call_lambda( + await cg.process_lambda(source, args, return_type=ESPTime) + ) + + f".strftime({time_format}).c_str()" + ) + # must be an ID + source = await cg.get_variable(source) + return source.now().strftime(time_format).c_str() + if isinstance(value, Lambda): + value = call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) + + # Was the lambda call reduced to a string? + if value.endswith("c_str()") or ( + value.endswith('"') and value.startswith('"') + ): + pass + else: + # Either a std::string or a lambda call returning that. We need const char* + value = f"({value}).c_str()" + return cg.RawExpression(value) + return await super().process(value, args) + + +lv_text = TextValidator() +lv_float = LValidator(cv.float_, cg.float_) +lv_int = LValidator(cv.int_, cg.int_) +lv_positive_int = LValidator(cv.positive_int, cg.int_) +lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) + + +def gradient_mapper(value): + return MockObj(value) + + +def gradient_validator(value): + return cv.use_id(lv_gradient_t)(value) + + +lv_gradient = LValidator( + validator=gradient_validator, + rtype=lv_gradient_t, + retmapper=gradient_mapper, +) + + +def is_lv_font(font): + return isinstance(font, str) and font.lower() in LV_FONTS + + +class LvFont(LValidator): + def __init__(self): + def lv_builtin_font(value): + fontval = cv.one_of(*LV_FONTS, lower=True)(value) + lv_fonts_used.add(fontval) + return fontval + + def validator(value): + if value == SCHEMA_EXTRACT: + return LV_FONTS + if is_lv_font(value): + return lv_builtin_font(value) + fontval = cv.use_id(Font)(value) + esphome_fonts_used.add(fontval) + return requires_component("font")(fontval) + + super().__init__(validator, lv_font_t) + + async def process(self, value, args=()): + if is_lv_font(value): + return literal(f"&lv_font_{value}") + return literal(f"{value}_engine->get_lv_font()") + + +lv_font = LvFont() + + +def animated(value): + if isinstance(value, bool): + value = "ON" if value else "OFF" + return LvConstant("LV_ANIM_", "OFF", "ON").one_of(value) + + +def key_code(value): + value = cv.Any(cv.All(cv.string_strict, cv.Length(min=1, max=1)), cv.uint8_t)(value) + if isinstance(value, str): + return ord(value[0]) + return value + + +async def get_end_value(config): + return await lv_int.process(config.get(CONF_END_VALUE)) + + +async def get_start_value(config): + if CONF_START_VALUE in config: + value = config[CONF_START_VALUE] + else: + value = config.get(CONF_VALUE) + return await lv_int.process(value) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py new file mode 100644 index 0000000000..37d6670b84 --- /dev/null +++ b/esphome/components/lvgl/lvcode.py @@ -0,0 +1,351 @@ +import abc +from typing import Union + +from esphome import codegen as cg +from esphome.config import Config +from esphome.core import CORE, ID, Lambda +from esphome.cpp_generator import ( + AssignmentExpression, + CallExpression, + Expression, + ExpressionStatement, + LambdaExpression, + MockObj, + RawExpression, + RawStatement, + SafeExpType, + Statement, + VariableDeclarationExpression, + statement, +) +from esphome.yaml_util import ESPHomeDataBase + +from .defines import literal, lvgl_ns + +LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp() + +# Argument tuple for use in lambdas +LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) +LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)] +lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") +EVENT_ARG = [(lv_event_t_ptr, "event")] +# Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction; +# UPDATE_EVENT is fired when an entity is programmatically updated locally. +# VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction. +API_EVENT = literal("lvgl::lv_api_event") +UPDATE_EVENT = literal("lvgl::lv_update_event") + + +def get_line_marks(value) -> list: + """ + If possible, return a preprocessor directive to identify the line number where the given id was defined. + :param value: The id or other token to get the line number for + :return: A list containing zero or more line directives + """ + path = None + if isinstance(value, ESPHomeDataBase): + path = value.esp_range + elif isinstance(value, ID) and isinstance(CORE.config, Config): + path = CORE.config.get_path_for_id(value)[:-1] + path = CORE.config.get_deepest_document_range_for_path(path) + if path is None: + return [] + return [path.start_mark.as_line_directive] + + +class IndentedStatement(Statement): + def __init__(self, stmt: Statement, indent: int): + self.statement = stmt + self.indent = indent + + def __str__(self): + result = " " * self.indent * 4 + str(self.statement).strip() + if not isinstance(self.statement, RawStatement): + result += ";" + return result + + +class CodeContext(abc.ABC): + """ + A class providing a context for code generation. Generated code will be added to the + current context. A new context will stack on the current context, and restore it + when done. Used with the `with` statement. + """ + + code_context = None + + @abc.abstractmethod + def add(self, expression: Union[Expression, Statement]): + pass + + @staticmethod + def start_block(): + CodeContext.append(RawStatement("{")) + CodeContext.code_context.indent() + + @staticmethod + def end_block(): + CodeContext.code_context.detent() + CodeContext.append(RawStatement("}")) + + @staticmethod + def append(expression: Union[Expression, Statement]): + if CodeContext.code_context is not None: + CodeContext.code_context.add(expression) + return expression + + def __init__(self): + self.previous: Union[CodeContext | None] = None + self.indent_level = 0 + + async def __aenter__(self): + self.previous = CodeContext.code_context + CodeContext.code_context = self + return self + + async def __aexit__(self, *args): + CodeContext.code_context = self.previous + + def indent(self): + self.indent_level += 1 + + def detent(self): + self.indent_level -= 1 + + def indented_statement(self, stmt): + return IndentedStatement(stmt, self.indent_level) + + +class MainContext(CodeContext): + """ + Code generation into the main() function + """ + + def add(self, expression: Union[Expression, Statement]): + return cg.add(self.indented_statement(expression)) + + +class LambdaContext(CodeContext): + """ + A context that will accumlate code for use in a lambda. + """ + + def __init__( + self, + parameters: list[tuple[SafeExpType, str]] = None, + return_type: SafeExpType = cg.void, + capture: str = "", + where=None, + ): + super().__init__() + self.code_list: list[Statement] = [] + self.parameters = parameters or [] + self.return_type = return_type + self.capture = capture + self.where = where + + def add(self, expression: Union[Expression, Statement]): + self.code_list.append(self.indented_statement(expression)) + return expression + + async def get_lambda(self) -> LambdaExpression: + code_text = self.get_code() + return await cg.process_lambda( + Lambda("\n".join(code_text) + "\n"), + self.parameters, + capture=self.capture, + return_type=self.return_type, + ) + + def get_code(self): + code_text = [] + for exp in self.code_list: + text = str(statement(exp)) + text = text.rstrip() + code_text.append(text) + return code_text + + async def __aenter__(self): + await super().__aenter__() + add_line_marks(self.where) + return self + + +class LvContext(LambdaContext): + """ + Code generation into the LVGL initialisation code (called in `setup()`) + """ + + added_lambda_count = 0 + + def __init__(self, lv_component, args=None): + self.args = args or LVGL_COMP_ARG + super().__init__(parameters=self.args) + self.lv_component = lv_component + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await super().__aexit__(exc_type, exc_val, exc_tb) + + def add(self, expression: Union[Expression, Statement]): + cg.add(expression) + return expression + + def __call__(self, *args): + return self.add(*args) + + +class LocalVariable(MockObj): + """ + Create a local variable and enclose the code using it within a block. + """ + + def __init__(self, name, type, rhs=None, modifier="*"): + base = ID(name + "_VAR_", True, type) + super().__init__(base, "") + self.modifier = modifier + self.rhs = rhs + + def __enter__(self): + CodeContext.start_block() + CodeContext.append( + VariableDeclarationExpression(self.base.type, self.modifier, self.base.id) + ) + if self.rhs is not None: + CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs)) + return MockObj(self.base) + + def __exit__(self, *args): + CodeContext.end_block() + + +class MockLv: + """ + A mock object that can be used to generate LVGL calls. + """ + + def __init__(self, base): + self.base = base + + def __getattr__(self, attr: str) -> "MockLv": + return MockLv(f"{self.base}{attr}") + + def append(self, expression): + CodeContext.append(expression) + + def __call__(self, *args: SafeExpType) -> "MockObj": + call = CallExpression(self.base, *args) + result = MockObj(call, "") + self.append(result) + return result + + def __str__(self): + return str(self.base) + + def __repr__(self): + return f"MockLv<{str(self.base)}>" + + def call(self, prop, *args): + call = CallExpression(RawExpression(f"{self.base}{prop}"), *args) + result = MockObj(call, "") + self.append(result) + return result + + +class LvConditional: + def __init__(self, condition): + self.condition = condition + + def __enter__(self): + if self.condition is not None: + CodeContext.append(RawStatement(f"if ({self.condition}) {{")) + CodeContext.code_context.indent() + return self + + def __exit__(self, *args): + if self.condition is not None: + CodeContext.code_context.detent() + CodeContext.append(RawStatement("}")) + + def else_(self): + assert self.condition is not None + CodeContext.code_context.detent() + CodeContext.append(RawStatement("} else {")) + CodeContext.code_context.indent() + + +class ReturnStatement(ExpressionStatement): + def __str__(self): + return f"return {self.expression};" + + +class LvExpr(MockLv): + def __getattr__(self, attr: str) -> "MockLv": + return LvExpr(f"{self.base}{attr}") + + def append(self, expression): + pass + + +def static_cast(type, value): + return literal(f"static_cast<{type}>({value})") + + +# Top level mock for generic lv_ calls to be recorded +lv = MockLv("lv_") +# Just generate an expression +lv_expr = LvExpr("lv_") +# Mock for lv_obj_ calls +lv_obj = MockLv("lv_obj_") +# Operations on the LVGL component +lvgl_comp = MockObj(LVGL_COMP, "->") + + +# equivalent to cg.add() for the current code context +def lv_add(expression: Union[Expression, Statement]): + return CodeContext.append(expression) + + +def add_line_marks(where): + """ + Add line marks for the current code context + :param where: An object to identify the source of the line marks + :return: + """ + for mark in get_line_marks(where): + lv_add(cg.RawStatement(mark)) + + +def lv_assign(target, expression): + lv_add(AssignmentExpression("", "", target, expression)) + + +def lv_Pvariable(type, name): + """ + Create but do not initialise a pointer variable + :param type: Type of the variable target + :param name: name of the variable, or an ID + :return: A MockObj of the variable + """ + if isinstance(name, str): + name = ID(name, True, type) + decl = VariableDeclarationExpression(type, "*", name) + CORE.add_global(decl) + var = MockObj(name, "->") + CORE.register_variable(name, var) + return var + + +def lv_variable(type, name): + """ + Create but do not initialise a variable + :param type: Type of the variable target + :param name: name of the variable, or an ID + :return: A MockObj of the variable + """ + if isinstance(name, str): + name = ID(name, True, type) + decl = VariableDeclarationExpression(type, "", name) + CORE.add_global(decl) + var = MockObj(name, ".") + CORE.register_variable(name, var) + return var diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp new file mode 100644 index 0000000000..70cfb859de --- /dev/null +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -0,0 +1,541 @@ +#include "esphome/core/defines.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" +#include "lvgl_hal.h" +#include "lvgl_esphome.h" + +#include + +namespace esphome { +namespace lvgl { +static const char *const TAG = "lvgl"; + +static const char *const EVENT_NAMES[] = { + "NONE", + "PRESSED", + "PRESSING", + "PRESS_LOST", + "SHORT_CLICKED", + "LONG_PRESSED", + "LONG_PRESSED_REPEAT", + "CLICKED", + "RELEASED", + "SCROLL_BEGIN", + "SCROLL_END", + "SCROLL", + "GESTURE", + "KEY", + "FOCUSED", + "DEFOCUSED", + "LEAVE", + "HIT_TEST", + "COVER_CHECK", + "REFR_EXT_DRAW_SIZE", + "DRAW_MAIN_BEGIN", + "DRAW_MAIN", + "DRAW_MAIN_END", + "DRAW_POST_BEGIN", + "DRAW_POST", + "DRAW_POST_END", + "DRAW_PART_BEGIN", + "DRAW_PART_END", + "VALUE_CHANGED", + "INSERT", + "REFRESH", + "READY", + "CANCEL", + "DELETE", + "CHILD_CHANGED", + "CHILD_CREATED", + "CHILD_DELETED", + "SCREEN_UNLOAD_START", + "SCREEN_LOAD_START", + "SCREEN_LOADED", + "SCREEN_UNLOADED", + "SIZE_CHANGED", + "STYLE_CHANGED", + "LAYOUT_CHANGED", + "GET_SELF_SIZE", +}; + +std::string lv_event_code_name_for(uint8_t event_code) { + if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) { + return EVENT_NAMES[event_code]; + } + return str_sprintf("%2d", event_code); +} + +static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { + // cater for display driver chips with special requirements for bounds of partial + // draw areas. Extend the draw area to satisfy: + // * Coordinates must be a multiple of draw_rounding + auto *comp = static_cast(disp_drv->user_data); + auto draw_rounding = comp->draw_rounding; + // round down the start coordinates + area->x1 = area->x1 / draw_rounding * draw_rounding; + area->y1 = area->y1 / draw_rounding * draw_rounding; + // round up the end coordinates + area->x2 = (area->x2 + draw_rounding) / draw_rounding * draw_rounding - 1; + area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1; +} + +lv_event_code_t lv_api_event; // NOLINT +lv_event_code_t lv_update_event; // NOLINT +void LvglComponent::dump_config() { + ESP_LOGCONFIG(TAG, "LVGL:"); + ESP_LOGCONFIG(TAG, " Display width/height: %d x %d", this->disp_drv_.hor_res, this->disp_drv_.ver_res); + ESP_LOGCONFIG(TAG, " Rotation: %d", this->rotation); + ESP_LOGCONFIG(TAG, " Draw rounding: %d", (int) this->draw_rounding); +} +void LvglComponent::set_paused(bool paused, bool show_snow) { + this->paused_ = paused; + this->show_snow_ = show_snow; + if (!paused && lv_scr_act() != nullptr) { + lv_disp_trig_activity(this->disp_); // resets the inactivity time + lv_obj_invalidate(lv_scr_act()); + } + this->pause_callbacks_.call(paused); +} + +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, this); +} +void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, + lv_event_code_t event2) { + this->add_event_cb(obj, callback, event1); + this->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) { + this->add_event_cb(obj, callback, event1); + this->add_event_cb(obj, callback, event2); + this->add_event_cb(obj, callback, event3); +} +void LvglComponent::add_page(LvPageType *page) { + this->pages_.push_back(page); + 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; + do { + this->current_page_ = (this->current_page_ + 1) % this->pages_.size(); + } 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; + do { + this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size(); + } while (this->pages_[this->current_page_]->skip); // skip empty pages() + this->show_page(this->current_page_, anim, time); +} +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); + auto x1 = area->x1; + auto y1 = area->y1; + lv_color_t *dst = this->rotate_buf_; + switch (this->rotation) { + case display::DISPLAY_ROTATION_90_DEGREES: + for (lv_coord_t x = height; x-- != 0;) { + for (lv_coord_t y = 0; y != width; y++) { + dst[y * height + x] = *ptr++; + } + } + y1 = x1; + x1 = this->disp_drv_.ver_res - area->y1 - height; + width = height; + height = lv_area_get_width(area); + break; + + case display::DISPLAY_ROTATION_180_DEGREES: + for (lv_coord_t y = height; y-- != 0;) { + for (lv_coord_t x = width; x-- != 0;) { + dst[y * width + x] = *ptr++; + } + } + x1 = this->disp_drv_.hor_res - x1 - width; + y1 = this->disp_drv_.ver_res - y1 - height; + break; + + case display::DISPLAY_ROTATION_270_DEGREES: + for (lv_coord_t x = 0; x != height; x++) { + for (lv_coord_t y = width; y-- != 0;) { + dst[y * height + x] = *ptr++; + } + } + x1 = y1; + y1 = this->disp_drv_.hor_res - area->x1 - width; + width = height; + height = lv_area_get_width(area); + break; + + default: + dst = ptr; + break; + } + for (auto *display : this->displays_) { + ESP_LOGV(TAG, "draw buffer x1=%d, y1=%d, width=%d, height=%d", x1, y1, width, height); + display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) dst, display::COLOR_ORDER_RGB, LV_BITNESS, + LV_COLOR_16_SWAP); + } +} + +void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { + if (!this->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), + lv_area_get_height(area), (int) (millis() - now)); + } + 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()) { + this->is_idle_ = true; + this->trigger(); + } else if (this->is_idle_ && idle_time < this->timeout_.value()) { + this->is_idle_ = false; + } + }); +} + +PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue paused) : paused_(std::move(paused)) { + parent->add_on_pause_callback([this](bool pausing) { + if (this->paused_.value() == pausing) + this->trigger(); + }); +} + +#ifdef USE_LVGL_TOUCHSCREEN +LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { + lv_indev_drv_init(&this->drv_); + this->drv_.long_press_repeat_time = long_press_repeat_time; + this->drv_.long_press_time = long_press_time; + this->drv_.type = LV_INDEV_TYPE_POINTER; + this->drv_.user_data = this; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + if (l->touch_pressed_) { + data->point.x = l->touch_point_.x; + data->point.y = l->touch_point_.y; + data->state = LV_INDEV_STATE_PRESSED; + } else { + data->state = LV_INDEV_STATE_RELEASED; + } + }; +} +void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { + this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); + if (this->touch_pressed_) + this->touch_point_ = tpoints[0]; +} +#endif // USE_LVGL_TOUCHSCREEN + +#ifdef USE_LVGL_KEY_LISTENER +LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { + lv_indev_drv_init(&this->drv_); + this->drv_.type = type; + this->drv_.user_data = this; + this->drv_.long_press_time = lpt; + this->drv_.long_press_repeat_time = lprt; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + data->key = l->key_; + data->enc_diff = (int16_t) (l->count_ - l->last_count_); + l->last_count_ = l->count_; + data->continue_reading = false; + }; +} +#endif // USE_LVGL_KEY_LISTENER + +#if defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER) +std::string LvSelectable::get_selected_text() { + auto selected = this->get_selected_index(); + if (selected >= this->options_.size()) + return ""; + return this->options_[selected]; +} + +static std::string join_string(std::vector options) { + return std::accumulate( + options.begin(), options.end(), std::string(), + [](const std::string &a, const std::string &b) -> std::string { return a + (a.length() > 0 ? "\n" : "") + b; }); +} + +void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t anim) { + auto index = std::find(this->options_.begin(), this->options_.end(), text); + if (index != this->options_.end()) { + this->set_selected_index(index - this->options_.begin(), anim); + lv_event_send(this->obj, lv_api_event, nullptr); + } +} + +void LvSelectable::set_options(std::vector options) { + auto index = this->get_selected_index(); + if (index >= options.size()) + index = options.size() - 1; + this->options_ = std::move(options); + this->set_option_string(join_string(this->options_).c_str()); + lv_event_send(this->obj, LV_EVENT_REFRESH, nullptr); + this->set_selected_index(index, LV_ANIM_OFF); +} +#endif // USE_LVGL_DROPDOWN || LV_USE_ROLLER + +#ifdef USE_LVGL_BUTTONMATRIX +void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) { + LvCompound::set_obj(lv_obj); + lv_obj_add_event_cb( + lv_obj, + [](lv_event_t *event) { + auto *self = static_cast(event->user_data); + if (self->key_callback_.size() == 0) + return; + auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); + if (key_idx == LV_BTNMATRIX_BTN_NONE) + return; + if (self->key_map_.count(key_idx) != 0) { + self->send_key_(self->key_map_[key_idx]); + return; + } + const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx); + auto len = strlen(str); + while (len--) + self->send_key_(*str++); + }, + LV_EVENT_PRESSED, this); +} +#endif // USE_LVGL_BUTTONMATRIX + +#ifdef USE_LVGL_KEYBOARD +static const char *const KB_SPECIAL_KEYS[] = { + "abc", "ABC", "1#", + // maybe add other special keys here +}; + +void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { + LvCompound::set_obj(lv_obj); + lv_obj_add_event_cb( + lv_obj, + [](lv_event_t *event) { + auto *self = static_cast(event->user_data); + if (self->key_callback_.size() == 0) + return; + + auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); + if (key_idx == LV_BTNMATRIX_BTN_NONE) + return; + const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx); + if (txt == nullptr) + return; + for (const auto *kb_special_key : KB_SPECIAL_KEYS) { + if (strcmp(txt, kb_special_key) == 0) + return; + } + while (*txt != 0) + self->send_key_(*txt++); + }, + LV_EVENT_PRESSED, this); +} +#endif // USE_LVGL_KEYBOARD + +void LvglComponent::write_random_() { + int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000; + if (iterations <= 0) + iterations = 1; + while (iterations-- != 0) { + auto col = random_uint32() % this->disp_drv_.hor_res; + col = col / this->draw_rounding * this->draw_rounding; + auto row = random_uint32() % this->disp_drv_.ver_res; + row = row / this->draw_rounding * this->draw_rounding; + auto size = (random_uint32() % 32) / this->draw_rounding * this->draw_rounding - 1; + lv_area_t area; + area.x1 = col; + area.y1 = row; + area.x2 = col + size; + area.y2 = row + size; + if (area.x2 >= this->disp_drv_.hor_res) + area.x2 = this->disp_drv_.hor_res - 1; + if (area.y2 >= this->disp_drv_.ver_res) + area.y2 = this->disp_drv_.ver_res - 1; + + size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2; + for (size_t i = 0; i != line_len; i++) { + ((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32(); + } + this->draw_buffer_(&area, (lv_color_t *) this->draw_buf_.buf1); + } +} + +/** + * @class LvglComponent + * @brief Component for rendering LVGL. + * + * This component renders LVGL widgets on a display. Some initialisation must be done in the constructor + * since LVGL needs to be initialised before any widgets can be created. + * + * @param displays a list of displays to render onto. All displays must have the same + * resolution. + * @param buffer_frac the fraction of the display resolution to use for the LVGL + * draw buffer. A higher value will make animations smoother but + * also increase memory usage. + * @param full_refresh if true, the display will be fully refreshed on every frame. + * If false, only changed areas will be updated. + * @param draw_rounding the rounding to use when drawing. A value of 1 will draw + * without any rounding, a value of 2 will round to the nearest + * multiple of 2, and so on. + * @param resume_on_input if true, this component will resume rendering when the user + * 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) + : draw_rounding(draw_rounding), + displays_(std::move(displays)), + buffer_frac_(buffer_frac), + full_refresh_(full_refresh), + resume_on_input_(resume_on_input) { + lv_init(); + lv_update_event = static_cast(lv_event_register_id()); + lv_api_event = static_cast(lv_event_register_id()); + auto *display = this->displays_[0]; + size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_; + auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; + this->rotation = display->get_rotation(); + 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) + return; + } + auto *buf = lv_custom_mem_alloc(buf_bytes); // NOLINT + if (buf == nullptr) + return; + lv_disp_draw_buf_init(&this->draw_buf_, buf, nullptr, buffer_pixels); + lv_disp_drv_init(&this->disp_drv_); + this->disp_drv_.draw_buf = &this->draw_buf_; + this->disp_drv_.user_data = this; + this->disp_drv_.full_refresh = this->full_refresh_; + this->disp_drv_.flush_cb = static_flush_cb; + this->disp_drv_.rounder_cb = rounder_cb; + this->disp_drv_.hor_res = (lv_coord_t) display->get_width(); + this->disp_drv_.ver_res = (lv_coord_t) display->get_height(); + this->disp_ = lv_disp_drv_register(&this->disp_drv_); +} + +void LvglComponent::setup() { + if (this->draw_buf_.buf1 == nullptr) { + this->mark_failed(); + this->status_set_error("Memory allocation failure"); + return; + } + ESP_LOGCONFIG(TAG, "LVGL Setup starts"); +#if LV_USE_LOG + lv_log_register_print_cb([](const char *buf) { + auto next = strchr(buf, ')'); + if (next != nullptr) + buf = next + 1; + while (isspace(*buf)) + buf++; + esp_log_printf_(LVGL_LOG_LEVEL, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); + }); +#endif + // Rotation will be handled by our drawing function, so reset the display rotation. + for (auto *display : this->displays_) + display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); + this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); + lv_disp_trig_activity(this->disp_); + ESP_LOGCONFIG(TAG, "LVGL Setup complete"); +} + +void LvglComponent::update() { + // update indicators + if (this->paused_) { + return; + } + this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); +} +void LvglComponent::loop() { + if (this->paused_) { + if (this->show_snow_) + this->write_random_(); + } + lv_timer_handler_run_in_period(5); +} + +#ifdef USE_LVGL_ANIMIMG +void lv_animimg_stop(lv_obj_t *obj) { + auto *animg = (lv_animimg_t *) obj; + int32_t duration = animg->anim.time; + lv_animimg_set_duration(obj, 0); + lv_animimg_start(obj); + lv_animimg_set_duration(obj, duration); +} +#endif +void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { + reinterpret_cast(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); +} +} // namespace lvgl +} // namespace esphome + +size_t lv_millis(void) { return esphome::millis(); } + +#if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266) +void *lv_custom_mem_alloc(size_t size) { + auto *ptr = malloc(size); // NOLINT + if (ptr == nullptr) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR + esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); +#endif + } + return ptr; +} +void lv_custom_mem_free(void *ptr) { return free(ptr); } // NOLINT +void *lv_custom_mem_realloc(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT +#else +static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT + +void *lv_custom_mem_alloc(size_t size) { + void *ptr; + ptr = heap_caps_malloc(size, cap_bits); + if (ptr == nullptr) { + cap_bits = MALLOC_CAP_8BIT; + ptr = heap_caps_malloc(size, cap_bits); + } + if (ptr == nullptr) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR + esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); +#endif + return nullptr; + } +#ifdef ESPHOME_LOG_HAS_VERBOSE + esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); +#endif + return ptr; +} + +void lv_custom_mem_free(void *ptr) { +#ifdef ESPHOME_LOG_HAS_VERBOSE + esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); +#endif + if (ptr == nullptr) + return; + heap_caps_free(ptr); +} + +void *lv_custom_mem_realloc(void *ptr, size_t size) { +#ifdef ESPHOME_LOG_HAS_VERBOSE + esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); +#endif + return heap_caps_realloc(ptr, size, cap_bits); +} +#endif diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h new file mode 100644 index 0000000000..f357c4950c --- /dev/null +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -0,0 +1,363 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif // USE_BINARY_SENSOR +#ifdef USE_LVGL_IMAGE +#include "esphome/components/image/image.h" +#endif // USE_LVGL_IMAGE +#ifdef USE_LVGL_ROTARY_ENCODER +#include "esphome/components/rotary_encoder/rotary_encoder.h" +#endif // USE_LVGL_ROTARY_ENCODER + +// required for clang-tidy +#ifndef LV_CONF_H +#define LV_CONF_SKIP 1 // NOLINT +#endif // LV_CONF_H + +#include "esphome/components/display/display.h" +#include "esphome/components/display/display_color_utils.h" +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include + +#ifdef USE_LVGL_FONT +#include "esphome/components/font/font.h" +#endif // USE_LVGL_FONT +#ifdef USE_LVGL_TOUCHSCREEN +#include "esphome/components/touchscreen/touchscreen.h" +#endif // USE_LVGL_TOUCHSCREEN + +#if defined(USE_LVGL_BUTTONMATRIX) || defined(USE_LVGL_KEYBOARD) +#include "esphome/components/key_provider/key_provider.h" +#endif // USE_LVGL_BUTTONMATRIX + +namespace esphome { +namespace lvgl { + +extern lv_event_code_t lv_api_event; // NOLINT +extern lv_event_code_t lv_update_event; // NOLINT +extern std::string lv_event_code_name_for(uint8_t event_code); +#if LV_COLOR_DEPTH == 16 +static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; +#elif LV_COLOR_DEPTH == 32 +static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_888; +#else // LV_COLOR_DEPTH +static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; +#endif // LV_COLOR_DEPTH + +#ifdef USE_LVGL_IMAGE +// Shortcut / overload, so that the source of an image can easily be updated +// from within a lambda. +inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) { + lv_img_set_src(obj, image->get_lv_img_dsc()); +} +#endif // USE_LVGL_IMAGE + +// Parent class for things that wrap an LVGL object +class LvCompound { + public: + virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } + lv_obj_t *obj{}; +}; + +class LvPageType { + public: + LvPageType(bool skip) : skip(skip) {} + + void setup(size_t index) { + this->index = index; + this->obj = lv_obj_create(nullptr); + } + lv_obj_t *obj{}; + size_t index{}; + bool skip; +}; + +using LvLambdaType = std::function; +using set_value_lambda_t = std::function; +using event_callback_t = void(_lv_event_t *); +using text_lambda_t = std::function; + +template class ObjUpdateAction : public Action { + public: + explicit ObjUpdateAction(std::function &&lamb) : lamb_(std::move(lamb)) {} + + void play(Ts... x) override { this->lamb_(x...); } + + protected: + std::function lamb_; +}; +#ifdef USE_LVGL_FONT +class FontEngine { + public: + FontEngine(font::Font *esp_font); + const lv_font_t *get_lv_font(); + + const font::GlyphData *get_glyph_data(uint32_t unicode_letter); + uint16_t baseline{}; + uint16_t height{}; + uint8_t bpp{}; + + protected: + font::Font *font_{}; + uint32_t last_letter_{}; + const font::GlyphData *last_data_{}; + lv_font_t lv_font_{}; +}; +#endif // USE_LVGL_FONT +#ifdef USE_LVGL_ANIMIMG +void lv_animimg_stop(lv_obj_t *obj); +#endif // USE_LVGL_ANIMIMG + +class LvglComponent : public PollingComponent { + constexpr static const char *const TAG = "lvgl"; + + public: + LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, int draw_rounding, + bool resume_on_input); + 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; } + void setup() override; + void update() override; + void loop() override; + void add_on_idle_callback(std::function &&callback) { + this->idle_callbacks_.add(std::move(callback)); + } + void add_on_pause_callback(std::function &&callback) { this->pause_callbacks_.add(std::move(callback)); } + void dump_config() override; + bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } + lv_disp_t *get_disp() { return this->disp_; } + lv_obj_t *get_scr_act() { return lv_disp_get_scr_act(this->disp_); } + // Pause or resume the display. + // @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_; } + // 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_) { + this->set_paused(false, false); + } + } + + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); + void 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); + void add_page(LvPageType *page); + void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); + void show_next_page(lv_scr_load_anim_t anim, uint32_t time); + void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); + void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } + void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); } + void restore_focus_mark(lv_group_t *group) { + auto *mark = this->focus_marks_[group]; + if (mark != nullptr) { + lv_group_focus_obj(mark); + } + } + // rounding factor to align bounds of update area when drawing + size_t draw_rounding{2}; + + display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES}; + + protected: + void write_random_(); + void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); + void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); + + std::vector displays_{}; + size_t buffer_frac_{1}; + bool full_refresh_{}; + bool resume_on_input_{}; + + lv_disp_draw_buf_t draw_buf_{}; + lv_disp_drv_t disp_drv_{}; + lv_disp_t *disp_{}; + bool paused_{}; + std::vector pages_{}; + size_t current_page_{0}; + bool show_snow_{}; + bool page_wrap_{true}; + std::map focus_marks_{}; + + CallbackManager idle_callbacks_{}; + CallbackManager pause_callbacks_{}; + lv_color_t *rotate_buf_{}; +}; + +class IdleTrigger : public Trigger<> { + public: + explicit IdleTrigger(LvglComponent *parent, TemplatableValue timeout); + + protected: + TemplatableValue timeout_; + bool is_idle_{}; +}; + +class PauseTrigger : public Trigger<> { + public: + explicit PauseTrigger(LvglComponent *parent, TemplatableValue paused); + + protected: + TemplatableValue paused_; +}; + +template class LvglAction : public Action, public Parented { + public: + explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} + void play(Ts... x) override { this->action_(this->parent_); } + + protected: + std::function action_{}; +}; + +template class LvglCondition : public Condition, public Parented { + public: + LvglCondition(std::function &&condition_lambda) + : condition_lambda_(std::move(condition_lambda)) {} + bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } + + protected: + std::function condition_lambda_{}; +}; + +#ifdef USE_LVGL_TOUCHSCREEN +class LVTouchListener : public touchscreen::TouchListener, public Parented { + public: + LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time); + void update(const touchscreen::TouchPoints_t &tpoints) override; + void release() override { + touch_pressed_ = false; + this->parent_->maybe_wakeup(); + } + lv_indev_drv_t *get_drv() { return &this->drv_; } + + protected: + lv_indev_drv_t drv_{}; + touchscreen::TouchPoint touch_point_{}; + bool touch_pressed_{}; +}; +#endif // USE_LVGL_TOUCHSCREEN + +#ifdef USE_LVGL_KEY_LISTENER +class LVEncoderListener : public Parented { + public: + LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt); + +#ifdef USE_BINARY_SENSOR + void set_left_button(binary_sensor::BinarySensor *left_button) { + left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); + } + void set_right_button(binary_sensor::BinarySensor *right_button) { + right_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_RIGHT, state); }); + } + + void set_enter_button(binary_sensor::BinarySensor *enter_button) { + enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); }); + } +#endif + +#ifdef USE_LVGL_ROTARY_ENCODER + void set_sensor(rotary_encoder::RotaryEncoderSensor *sensor) { + sensor->register_listener([this](int32_t count) { this->set_count(count); }); + } +#endif // USE_LVGL_ROTARY_ENCODER + + void event(int key, bool pressed) { + if (!this->parent_->is_paused()) { + this->pressed_ = pressed; + this->key_ = key; + } else if (!pressed) { + // maybe wakeup on release if paused + this->parent_->maybe_wakeup(); + } + } + + void set_count(int32_t count) { + if (!this->parent_->is_paused()) { + this->count_ = count; + } else { + this->parent_->maybe_wakeup(); + } + } + + lv_indev_drv_t *get_drv() { return &this->drv_; } + + protected: + lv_indev_drv_t drv_{}; + bool pressed_{}; + int32_t count_{}; + int32_t last_count_{}; + int key_{}; +}; +#endif // USE_LVGL_KEY_LISTENER + +#if defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER) +class LvSelectable : public LvCompound { + public: + virtual size_t get_selected_index() = 0; + virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; + void set_selected_text(const std::string &text, lv_anim_enable_t anim); + std::string get_selected_text(); + std::vector get_options() { return this->options_; } + void set_options(std::vector options); + + protected: + virtual void set_option_string(const char *options) = 0; + std::vector options_{}; +}; + +#ifdef USE_LVGL_DROPDOWN +class LvDropdownType : public LvSelectable { + public: + size_t get_selected_index() override { return lv_dropdown_get_selected(this->obj); } + void set_selected_index(size_t index, lv_anim_enable_t anim) override { lv_dropdown_set_selected(this->obj, index); } + + protected: + void set_option_string(const char *options) override { lv_dropdown_set_options(this->obj, options); } +}; +#endif // USE_LVGL_DROPDOWN + +#ifdef USE_LVGL_ROLLER +class LvRollerType : public LvSelectable { + public: + size_t get_selected_index() override { return lv_roller_get_selected(this->obj); } + void set_selected_index(size_t index, lv_anim_enable_t anim) override { + lv_roller_set_selected(this->obj, index, anim); + } + void set_mode(lv_roller_mode_t mode) { this->mode_ = mode; } + + protected: + void set_option_string(const char *options) override { lv_roller_set_options(this->obj, options, this->mode_); } + lv_roller_mode_t mode_{LV_ROLLER_MODE_NORMAL}; +}; +#endif +#endif // defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER) + +#ifdef USE_LVGL_BUTTONMATRIX +class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound { + public: + void set_obj(lv_obj_t *lv_obj) override; + uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } + void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; } + + protected: + std::map key_map_{}; +}; +#endif // USE_LVGL_BUTTONMATRIX + +#ifdef USE_LVGL_KEYBOARD +class LvKeyboardType : public key_provider::KeyProvider, public LvCompound { + public: + void set_obj(lv_obj_t *lv_obj) override; +}; +#endif // USE_LVGL_KEYBOARD +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/lvgl_hal.h b/esphome/components/lvgl/lvgl_hal.h new file mode 100644 index 0000000000..754cc70391 --- /dev/null +++ b/esphome/components/lvgl/lvgl_hal.h @@ -0,0 +1,21 @@ +// +// Created by Clyde Stubbs on 20/9/2023. +// + +#pragma once + +#ifdef __cplusplus +#define EXTERNC extern "C" +#include +namespace esphome { +namespace lvgl {} +} // namespace esphome +#else +#define EXTERNC extern +#include +#endif + +EXTERNC size_t lv_millis(void); +EXTERNC void *lv_custom_mem_alloc(size_t size); +EXTERNC void lv_custom_mem_free(void *ptr); +EXTERNC void *lv_custom_mem_realloc(void *ptr, size_t size); diff --git a/esphome/components/lvgl/lvgl_proxy.h b/esphome/components/lvgl/lvgl_proxy.h new file mode 100644 index 0000000000..0ccd80e541 --- /dev/null +++ b/esphome/components/lvgl/lvgl_proxy.h @@ -0,0 +1,17 @@ +#pragma once +/** +* This header is for use in components that might or might not use LVGL. There is a platformio bug where +the mere mention of a header file, even if ifdefed, causes the build to fail. This is a workaround, since if this +file is included in the build, LVGL is always included. +*/ +#ifdef USE_LVGL +// required for clang-tidy +#ifndef LV_CONF_H +#define LV_CONF_SKIP 1 // NOLINT +#endif // LV_CONF_H + +#include +namespace esphome { +namespace lvgl {} // namespace lvgl +} // namespace esphome +#endif // USE_LVGL diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py new file mode 100644 index 0000000000..07f92635b5 --- /dev/null +++ b/esphome/components/lvgl/number/__init__.py @@ -0,0 +1,68 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.cpp_generator import MockObj + +from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_UPDATE_ON_RELEASE, CONF_WIDGET +from ..lv_validation import animated +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvNumber, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number) + +CONFIG_SCHEMA = ( + number.number_schema(LVGLNumber) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + cv.Optional(CONF_ANIMATED, default=True): animated, + cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, + } + ) +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + var = await number.new_number( + config, + max_value=widget.get_max(), + min_value=widget.get_min(), + step=widget.get_step(), + ) + + await wait_for_widgets() + async with LambdaContext([(cg.float_, "v")]) as control: + await widget.set_property( + "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] + ) + lv.event_send(widget.obj, API_EVENT, cg.nullptr) + control.add(var.publish_state(widget.get_value())) + async with LambdaContext(EVENT_ARG) as event: + event.add(var.publish_state(widget.get_value())) + event_code = ( + LV_EVENT.VALUE_CHANGED + if not config[CONF_UPDATE_ON_RELEASE] + else LV_EVENT.RELEASED + ) + async with LvContext(paren): + lv_add(var.set_control_lambda(await control.get_lambda())) + lv_add( + paren.add_event_cb( + widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code + ) + ) + lv_add(var.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h new file mode 100644 index 0000000000..77fadd2a29 --- /dev/null +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLNumber : public number::Number { + public: + void set_control_lambda(std::function control_lambda) { + this->control_lambda_ = std::move(control_lambda); + if (this->initial_state_.has_value()) { + this->control_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void control(float value) override { + if (this->control_lambda_ != nullptr) { + this->control_lambda_(value); + } else { + this->initial_state_ = value; + } + } + std::function control_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py new file mode 100644 index 0000000000..516627708e --- /dev/null +++ b/esphome/components/lvgl/schemas.py @@ -0,0 +1,443 @@ +from esphome import config_validation as cv +from esphome.automation import Trigger, validate_automation +from esphome.components.time import RealTimeClock +from esphome.const import ( + CONF_ARGS, + CONF_FORMAT, + CONF_GROUP, + CONF_ID, + CONF_ON_VALUE, + CONF_STATE, + CONF_TEXT, + CONF_TIME, + CONF_TRIGGER_ID, + CONF_TYPE, +) +from esphome.core import TimePeriod +from esphome.schema_extractors import SCHEMA_EXTRACT + +from . import defines as df, lv_validation as lvalid +from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR +from .helpers import add_lv_use, requires_component, validate_printf +from .lv_validation import lv_color, lv_font, lv_gradient, lv_image +from .lvcode import LvglComponent, lv_event_t_ptr +from .types import ( + LVEncoderListener, + LvType, + WidgetType, + lv_group_t, + lv_obj_t, + lv_pseudo_button_t, + lv_style_t, +) + +# this will be populated later, in __init__.py to avoid circular imports. +WIDGET_TYPES: dict = {} + +# A schema for text properties +TEXT_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TEXT): cv.Any( + cv.All( + cv.Schema( + { + cv.Required(CONF_FORMAT): cv.string, + cv.Optional(CONF_ARGS, default=list): cv.ensure_list( + cv.lambda_ + ), + }, + ), + validate_printf, + ), + cv.Schema( + { + cv.Required(CONF_TIME_FORMAT): cv.string, + cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)), + } + ), + cv.templatable(cv.string), + ) + } +) + +LIST_ACTION_SCHEMA = cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_pseudo_button_t), + }, + key=CONF_ID, + ) +) + +PRESS_TIME = cv.All( + lvalid.lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535)) +) + +ENCODER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.All( + cv.declare_id(LVEncoderListener), requires_component("binary_sensor") + ), + cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t), + cv.Optional(df.CONF_INITIAL_FOCUS): cv.use_id(lv_obj_t), + cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, + cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, + } +) + +# All LVGL styles and their validators +STYLE_PROPS = { + "align": df.CHILD_ALIGNMENTS.one_of, + "arc_opa": lvalid.opacity, + "arc_color": lvalid.lv_color, + "arc_rounded": lvalid.lv_bool, + "arc_width": lvalid.lv_positive_int, + "anim_time": lvalid.lv_milliseconds, + "bg_color": lvalid.lv_color, + "bg_grad": lv_gradient, + "bg_grad_color": lvalid.lv_color, + "bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of, + "bg_grad_dir": LV_GRAD_DIR.one_of, + "bg_grad_stop": lvalid.stop_value, + "bg_image_opa": lvalid.opacity, + "bg_image_recolor": lvalid.lv_color, + "bg_image_recolor_opa": lvalid.opacity, + "bg_image_src": lvalid.lv_image, + "bg_main_stop": lvalid.stop_value, + "bg_opa": lvalid.opacity, + "border_color": lvalid.lv_color, + "border_opa": lvalid.opacity, + "border_post": lvalid.lv_bool, + "border_side": df.LvConstant( + "LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL" + ).several_of, + "border_width": lvalid.lv_positive_int, + "clip_corner": lvalid.lv_bool, + "color_filter_opa": lvalid.opacity, + "height": lvalid.size, + "image_recolor": lvalid.lv_color, + "image_recolor_opa": lvalid.opacity, + "line_width": cv.positive_int, + "line_dash_width": cv.positive_int, + "line_dash_gap": cv.positive_int, + "line_rounded": lvalid.lv_bool, + "line_color": lvalid.lv_color, + "opa": lvalid.opacity, + "opa_layered": lvalid.opacity, + "outline_color": lvalid.lv_color, + "outline_opa": lvalid.opacity, + "outline_pad": lvalid.pixels, + "outline_width": lvalid.pixels, + "pad_all": lvalid.pixels, + "pad_bottom": lvalid.pixels, + "pad_left": lvalid.pixels, + "pad_right": lvalid.pixels, + "pad_top": lvalid.pixels, + "shadow_color": lvalid.lv_color, + "shadow_ofs_x": lvalid.lv_int, + "shadow_ofs_y": lvalid.lv_int, + "shadow_opa": lvalid.opacity, + "shadow_spread": lvalid.lv_int, + "shadow_width": lvalid.lv_positive_int, + "text_align": df.LvConstant( + "LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO" + ).one_of, + "text_color": lvalid.lv_color, + "text_decor": df.LvConstant( + "LV_TEXT_DECOR_", "NONE", "UNDERLINE", "STRIKETHROUGH" + ).several_of, + "text_font": lv_font, + "text_letter_space": cv.positive_int, + "text_line_space": cv.positive_int, + "text_opa": lvalid.opacity, + "transform_angle": lvalid.lv_angle, + "transform_height": lvalid.pixels_or_percent, + "transform_pivot_x": lvalid.pixels_or_percent, + "transform_pivot_y": lvalid.pixels_or_percent, + "transform_zoom": lvalid.zoom, + "translate_x": lvalid.pixels_or_percent, + "translate_y": lvalid.pixels_or_percent, + "max_height": lvalid.pixels_or_percent, + "max_width": lvalid.pixels_or_percent, + "min_height": lvalid.pixels_or_percent, + "min_width": lvalid.pixels_or_percent, + "radius": lvalid.lv_fraction, + "width": lvalid.size, + "x": lvalid.pixels_or_percent, + "y": lvalid.pixels_or_percent, +} + +STYLE_REMAP = { + "bg_image_opa": "bg_img_opa", + "bg_image_recolor": "bg_img_recolor", + "bg_image_recolor_opa": "bg_img_recolor_opa", + "bg_image_src": "bg_img_src", + "image_recolor": "img_recolor", + "image_recolor_opa": "img_recolor_opa", +} + +# Complete object style schema +STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( + { + cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(lv_style_t)), + cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( + "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" + ).one_of, + } +) + +# Object states. Top level properties apply to MAIN +STATE_SCHEMA = cv.Schema( + {cv.Optional(state): STYLE_SCHEMA for state in df.STATES} +).extend(STYLE_SCHEMA) +# Setting object states +SET_STATE_SCHEMA = cv.Schema( + {cv.Optional(state): lvalid.lv_bool for state in df.STATES} +) +# Setting object flags +FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FLAGS}) +FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of) + + +def part_schema(widget_type: WidgetType): + """ + Generate a schema for the various parts (e.g. main:, indicator:) of a widget type + :param widget_type: The type of widget to generate for + :return: + """ + parts = widget_type.parts + return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( + STATE_SCHEMA + ) + + +def automation_schema(typ: LvType): + if typ.has_on_value: + events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,) + else: + events = df.LV_EVENT_TRIGGERS + args = typ.get_arg_type() if isinstance(typ, LvType) else [] + args.append(lv_event_t_ptr) + return { + cv.Optional(event): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template(*args)), + } + ) + for event in events + } + + +def create_modify_schema(widget_type): + return ( + part_schema(widget_type) + .extend( + { + cv.Required(CONF_ID): cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(widget_type), + }, + key=CONF_ID, + ) + ), + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + } + ) + .extend(FLAG_SCHEMA) + .extend(widget_type.modify_schema) + ) + + +def obj_schema(widget_type: WidgetType): + """ + Create a schema for a widget type itself i.e. no allowance for children + :param widget_type: + :return: + """ + return ( + part_schema(widget_type) + .extend(FLAG_SCHEMA) + .extend(LAYOUT_SCHEMA) + .extend(ALIGN_TO_SCHEMA) + .extend(automation_schema(widget_type.w_type)) + .extend( + cv.Schema( + { + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), + } + ) + ) + ) + + +LAYOUT_SCHEMAS = {} + +ALIGN_TO_SCHEMA = { + cv.Optional(df.CONF_ALIGN_TO): cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_obj_t), + cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of, + cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent, + cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent, + } + ) +} + + +def grid_free_space(value): + value = cv.Upper(value) + if value.startswith("FR(") and value.endswith(")"): + value = value.removesuffix(")").removeprefix("FR(") + return f"LV_GRID_FR({cv.positive_int(value)})" + raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") + + +grid_spec = cv.Any( + lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space +) + +cell_alignments = df.LV_CELL_ALIGNMENTS.one_of +grid_alignments = df.LV_GRID_ALIGNMENTS.one_of +flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of + +LAYOUT_SCHEMA = { + cv.Optional(df.CONF_LAYOUT): cv.typed_schema( + { + df.TYPE_GRID: { + cv.Required(df.CONF_GRID_ROWS): [grid_spec], + cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], + cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + }, + df.TYPE_FLEX: { + cv.Optional( + df.CONF_FLEX_FLOW, default="row_wrap" + ): df.FLEX_FLOWS.one_of, + cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, + cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, + cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + }, + }, + lower=True, + ) +} + +GRID_CELL_SCHEMA = { + cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, +} + +FLEX_OBJ_SCHEMA = { + cv.Optional(df.CONF_FLEX_GROW): cv.int_, +} + +DISP_BG_SCHEMA = cv.Schema( + { + cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image, + cv.Optional(df.CONF_DISP_BG_COLOR): lv_color, + } +) + +# A style schema that can include text +STYLED_TEXT_SCHEMA = cv.maybe_simple_value( + STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT +) + +# For use by platform components +LVGL_SCHEMA = cv.Schema( + { + cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(LvglComponent), + } +) + +ALL_STYLES = { + **STYLE_PROPS, + **GRID_CELL_SCHEMA, + **FLEX_OBJ_SCHEMA, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, +} + + +def container_validator(schema, widget_type: WidgetType): + """ + Create a validator for a container given the widget type + :param schema: Base schema to extend + :param widget_type: + :return: + """ + + def validator(value): + result = schema + if w_sch := widget_type.schema: + result = result.extend(w_sch) + ltype = df.TYPE_NONE + if value and (layout := value.get(df.CONF_LAYOUT)): + if not isinstance(layout, dict): + raise cv.Invalid("Layout value must be a dict") + ltype = layout.get(CONF_TYPE) + if not ltype: + raise (cv.Invalid("Layout schema requires type:")) + add_lv_use(ltype) + if value == SCHEMA_EXTRACT: + return result + result = result.extend( + LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) + ) + return result(value) + + return validator + + +def container_schema(widget_type: WidgetType, extras=None): + """ + Create a schema for a container widget of a given type. All obj properties are available, plus + the extras passed in, plus any defined for the specific widget being specified. + :param widget_type: The widget type, e.g. "img" + :param extras: Additional options to be made available, e.g. layout properties for children + :return: The schema for this type of widget. + """ + schema = obj_schema(widget_type).extend( + {cv.GenerateID(): cv.declare_id(widget_type.w_type)} + ) + if extras: + schema = schema.extend(extras) + # Delayed evaluation for recursion + return container_validator(schema, widget_type) + + +def widget_schema(widget_type: WidgetType, extras=None): + """ + Create a schema for a given widget type + :param widget_type: The name of the widget + :param extras: + :return: + """ + validator = container_schema(widget_type, extras=extras) + if required := widget_type.required_component: + validator = cv.All(validator, requires_component(required)) + return cv.Exclusive(widget_type.name, df.CONF_WIDGETS), validator + + +# All widget schemas must be defined before this is called. + + +def any_widget_schema(extras=None): + """ + Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of + widget under the widgets: key. + + :param extras: Additional schema to be applied to each generated one + :return: + """ + return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_TYPES.values())) diff --git a/esphome/components/lvgl/select/__init__.py b/esphome/components/lvgl/select/__init__.py new file mode 100644 index 0000000000..5e50b6b385 --- /dev/null +++ b/esphome/components/lvgl/select/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import CONF_OPTIONS + +from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET, literal +from ..lvcode import LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LvSelect, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select) + +CONFIG_SCHEMA = ( + select.select_schema(LVGLSelect) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvSelect), + cv.Optional(CONF_ANIMATED, default=False): cv.boolean, + } + ) +) + + +async def to_code(config): + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + options = widget.config.get(CONF_OPTIONS, []) + selector = await select.new_select(config, options=options) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + await wait_for_widgets() + async with LvContext(paren) as ctx: + ctx.add( + selector.set_widget( + widget.var, + literal("LV_ANIM_ON" if config[CONF_ANIMATED] else "LV_ANIM_OFF"), + ) + ) diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h new file mode 100644 index 0000000000..4538e339c3 --- /dev/null +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include "esphome/components/select/select.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "../lvgl.h" + +namespace esphome { +namespace lvgl { + +class LVGLSelect : public select::Select { + public: + void set_widget(LvSelectable *widget, lv_anim_enable_t anim = LV_ANIM_OFF) { + this->widget_ = widget; + this->anim_ = anim; + this->set_options_(); + lv_obj_add_event_cb( + this->widget_->obj, + [](lv_event_t *e) { + auto *it = static_cast(e->user_data); + it->set_options_(); + }, + LV_EVENT_REFRESH, this); + if (this->initial_state_.has_value()) { + this->control(this->initial_state_.value()); + this->initial_state_.reset(); + } + this->publish(); + auto lamb = [](lv_event_t *e) { + auto *self = static_cast(e->user_data); + self->publish(); + }; + lv_obj_add_event_cb(this->widget_->obj, lamb, LV_EVENT_VALUE_CHANGED, this); + lv_obj_add_event_cb(this->widget_->obj, lamb, lv_update_event, this); + } + + void publish() { this->publish_state(this->widget_->get_selected_text()); } + + protected: + void control(const std::string &value) override { + if (this->widget_ != nullptr) { + this->widget_->set_selected_text(value, this->anim_); + } else { + this->initial_state_ = value; + } + } + void set_options_() { this->traits.set_options(this->widget_->get_options()); } + + LvSelectable *widget_{}; + optional initial_state_{}; + lv_anim_enable_t anim_{LV_ANIM_OFF}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py new file mode 100644 index 0000000000..a2a2298c27 --- /dev/null +++ b/esphome/components/lvgl/sensor/__init__.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +from esphome.components.sensor import Sensor, new_sensor, sensor_schema +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + LVGL_COMP_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvNumber +from ..widgets import Widget, get_widgets, wait_for_widgets + +CONFIG_SCHEMA = ( + sensor_schema(Sensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + } + ) +) + + +async def to_code(config): + sensor = await new_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + assert isinstance(widget, Widget) + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as lamb: + lv_add(sensor.publish_state(widget.get_value())) + async with LvContext(paren, LVGL_COMP_ARG): + lv_add( + paren.add_event_cb( + widget.obj, + await lamb.get_lambda(), + LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, + ) + ) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py new file mode 100644 index 0000000000..6332e0976f --- /dev/null +++ b/esphome/components/lvgl/styles.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +from esphome.const import CONF_ID +from esphome.core import ID +from esphome.cpp_generator import MockObj + +from .defines import ( + CONF_STYLE_DEFINITIONS, + CONF_THEME, + CONF_TOP_LAYER, + LValidator, + literal, +) +from .helpers import add_lv_use +from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable +from .schemas import ALL_STYLES, STYLE_REMAP +from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr +from .widgets import Widget, add_widgets, set_obj_properties, theme_widget_map +from .widgets.obj import obj_spec + + +async def styles_to_code(config): + """Convert styles to C__ code.""" + for style in config.get(CONF_STYLE_DEFINITIONS, ()): + svar = cg.new_Pvariable(style[CONF_ID]) + lv.style_init(svar) + for prop, validator in ALL_STYLES.items(): + if (value := style.get(prop)) is not None: + if isinstance(validator, LValidator): + value = await validator.process(value) + if isinstance(value, list): + value = "|".join(value) + remapped_prop = STYLE_REMAP.get(prop, prop) + lv.call(f"style_set_{remapped_prop}", svar, literal(value)) + + +async def theme_to_code(config): + if theme := config.get(CONF_THEME): + add_lv_use(CONF_THEME) + for w_name, style in theme.items(): + if not isinstance(style, dict): + continue + + lname = "lv_theme_apply_" + w_name + apply = lv_variable(lv_lambda_t, lname) + theme_widget_map[w_name] = apply + ow = Widget.create("obj", MockObj(ID("obj")), obj_spec) + async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context: + await set_obj_properties(ow, style) + lv_assign(apply, await context.get_lambda()) + + +async def add_top_layer(lv_component, config): + top_layer = lv.disp_get_layer_top(lv_component.get_disp()) + if top_conf := config.get(CONF_TOP_LAYER): + with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj: + top_w = Widget(top_layer_obj, obj_spec, top_conf) + await set_obj_properties(top_w, top_conf) + await add_widgets(top_w, top_conf) diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py new file mode 100644 index 0000000000..8c090543f9 --- /dev/null +++ b/esphome/components/lvgl/switch/__init__.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +from esphome.components.switch import Switch, new_switch, switch_schema +import esphome.config_validation as cv +from esphome.cpp_generator import MockObj + +from ..defines import CONF_LVGL_ID, CONF_WIDGET, literal +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvConditional, + LvContext, + lv, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch) +CONFIG_SCHEMA = ( + switch_schema(LVGLSwitch) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } + ) +) + + +async def to_code(config): + switch = await new_switch(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as checked_ctx: + checked_ctx.add(switch.publish_state(widget.get_value())) + async with LambdaContext([(cg.bool_, "v")]) as control: + with LvConditional(MockObj("v")) as cond: + widget.add_state(LV_STATE.CHECKED) + cond.else_() + widget.clear_state(LV_STATE.CHECKED) + lv.event_send(widget.obj, API_EVENT, cg.nullptr) + control.add(switch.publish_state(literal("v"))) + async with LvContext(paren) as ctx: + lv_add(switch.set_control_lambda(await control.get_lambda())) + ctx.add( + paren.add_event_cb( + widget.obj, + await checked_ctx.get_lambda(), + LV_EVENT.VALUE_CHANGED, + UPDATE_EVENT, + ) + ) + lv_add(switch.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/switch/lvgl_switch.h b/esphome/components/lvgl/switch/lvgl_switch.h new file mode 100644 index 0000000000..af839b8892 --- /dev/null +++ b/esphome/components/lvgl/switch/lvgl_switch.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "esphome/components/switch/switch.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLSwitch : public switch_::Switch { + public: + void set_control_lambda(std::function state_lambda) { + this->state_lambda_ = std::move(state_lambda); + if (this->initial_state_.has_value()) { + this->state_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void write_state(bool value) override { + if (this->state_lambda_ != nullptr) { + this->state_lambda_(value); + } else { + this->initial_state_ = value; + } + } + std::function state_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py new file mode 100644 index 0000000000..a59e703591 --- /dev/null +++ b/esphome/components/lvgl/text/__init__.py @@ -0,0 +1,52 @@ +import esphome.codegen as cg +from esphome.components import text +from esphome.components.text import new_text +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvText, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +LVGLText = lvgl_ns.class_("LVGLText", text.Text) + +CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(LVGLText), + cv.Required(CONF_WIDGET): cv.use_id(LvText), + } +) + + +async def to_code(config): + textvar = await new_text(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + await wait_for_widgets() + async with LambdaContext([(cg.std_string, "text_value")]) as control: + await widget.set_property("text", "text_value.c_str()") + lv.event_send(widget.obj, API_EVENT, cg.nullptr) + control.add(textvar.publish_state(widget.get_value())) + async with LambdaContext(EVENT_ARG) as lamb: + lv_add(textvar.publish_state(widget.get_value())) + async with LvContext(paren): + lv_add(textvar.set_control_lambda(await control.get_lambda())) + lv_add( + paren.add_event_cb( + widget.obj, + await lamb.get_lambda(), + LV_EVENT.VALUE_CHANGED, + UPDATE_EVENT, + ) + ) + lv_add(textvar.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h new file mode 100644 index 0000000000..4c380d69a2 --- /dev/null +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "esphome/components/text/text.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLText : public text::Text { + public: + void set_control_lambda(std::function control_lambda) { + this->control_lambda_ = std::move(control_lambda); + if (this->initial_state_.has_value()) { + this->control_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void control(const std::string &value) override { + if (this->control_lambda_ != nullptr) { + this->control_lambda_(value); + } else { + this->initial_state_ = value; + } + } + std::function control_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/text_sensor/__init__.py b/esphome/components/lvgl/text_sensor/__init__.py new file mode 100644 index 0000000000..ae39eec291 --- /dev/null +++ b/esphome/components/lvgl/text_sensor/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components.text_sensor import ( + TextSensor, + new_text_sensor, + text_sensor_schema, +) +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvText +from ..widgets import get_widgets, wait_for_widgets + +CONFIG_SCHEMA = ( + text_sensor_schema(TextSensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvText), + } + ) +) + + +async def to_code(config): + sensor = await new_text_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as pressed_ctx: + pressed_ctx.add(sensor.publish_state(widget.get_value())) + async with LvContext(paren) as ctx: + ctx.add( + paren.add_event_cb( + widget.obj, + await pressed_ctx.get_lambda(), + LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, + ) + ) diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py new file mode 100644 index 0000000000..4d430a428e --- /dev/null +++ b/esphome/components/lvgl/touchscreens.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE + +from .defines import ( + CONF_LONG_PRESS_REPEAT_TIME, + CONF_LONG_PRESS_TIME, + CONF_TOUCHSCREENS, +) +from .helpers import lvgl_components_required +from .lvcode import lv +from .schemas import PRESS_TIME +from .types import LVTouchListener + +CONF_TOUCHSCREEN = "touchscreen" +TOUCHSCREENS_CONFIG = cv.maybe_simple_value( + { + cv.Required(CONF_TOUCHSCREEN_ID): cv.use_id(Touchscreen), + cv.Optional(CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, + cv.Optional(CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, + cv.GenerateID(): cv.declare_id(LVTouchListener), + }, + key=CONF_TOUCHSCREEN_ID, +) + + +def touchscreen_schema(config): + value = cv.ensure_list(TOUCHSCREENS_CONFIG)(config) + if value or CONF_TOUCHSCREEN not in CORE.loaded_integrations: + return value + return [TOUCHSCREENS_CONFIG(config)] + + +async def touchscreens_to_code(var, config): + for tconf in config[CONF_TOUCHSCREENS]: + lvgl_components_required.add(CONF_TOUCHSCREEN) + touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) + lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds + lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds + listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt) + await cg.register_parented(listener, var) + lv.indev_drv_register(listener.get_drv()) + cg.add(touchscreen.register_listener(listener)) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py new file mode 100644 index 0000000000..eb6e483203 --- /dev/null +++ b/esphome/components/lvgl/trigger.py @@ -0,0 +1,75 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_TRIGGER_ID + +from .defines import ( + CONF_ALIGN, + CONF_ALIGN_TO, + CONF_X, + CONF_Y, + LV_EVENT_MAP, + LV_EVENT_TRIGGERS, + literal, +) +from .lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvConditional, + lv, + lv_add, + lv_event_t_ptr, +) +from .types import LV_EVENT +from .widgets import widget_map + + +async def generate_triggers(lv_component): + """ + Generate LVGL triggers for all defined widgets + Must be done after all widgets completed + :param lv_component: The parent component + :return: + """ + + for w in widget_map.values(): + if w.config: + for event, conf in { + event: conf + for event, conf in w.config.items() + if event in LV_EVENT_TRIGGERS + }.items(): + conf = conf[0] + w.add_flag("LV_OBJ_FLAG_CLICKABLE") + event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) + await add_trigger(conf, lv_component, w, event) + for conf in w.config.get(CONF_ON_VALUE, ()): + await add_trigger( + conf, + lv_component, + w, + LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, + ) + + # Generate align to directives while we're here + if align_to := w.config.get(CONF_ALIGN_TO): + target = widget_map[align_to[CONF_ID]].obj + align = literal(align_to[CONF_ALIGN]) + x = align_to[CONF_X] + y = align_to[CONF_Y] + lv.obj_align_to(w.obj, target, align, x, y) + + +async def add_trigger(conf, lv_component, w, *events): + tid = conf[CONF_TRIGGER_ID] + trigger = cg.new_Pvariable(tid) + args = w.get_args() + [(lv_event_t_ptr, "event")] + value = w.get_values() + await automation.build_automation(trigger, args, conf) + async with LambdaContext(EVENT_ARG, where=tid) as context: + with LvConditional(w.is_selected()): + lv_add(trigger.trigger(*value, literal("event"))) + lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events)) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py new file mode 100644 index 0000000000..b504f24674 --- /dev/null +++ b/esphome/components/lvgl/types.py @@ -0,0 +1,197 @@ +import sys + +from esphome import automation, codegen as cg +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE +from esphome.cpp_generator import MockObj, MockObjClass + +from .defines import lvgl_ns +from .lvcode import lv_expr + + +class LvType(cg.MockObjClass): + def __init__(self, *args, **kwargs): + parens = kwargs.pop("parents", ()) + super().__init__(*args, parents=parens + (lv_obj_base_t,)) + self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")]) + self.value = kwargs.pop("lvalue", lambda w: w.obj) + self.has_on_value = kwargs.pop("has_on_value", False) + self.value_property = None + + def get_arg_type(self): + if len(self.args) == 0: + return None + return [arg[0] for arg in self.args] + + +class LvNumber(LvType): + def __init__(self, *args): + super().__init__( + *args, + largs=[(cg.float_, "x")], + lvalue=lambda w: w.get_number_value(), + has_on_value=True, + ) + self.value_property = CONF_VALUE + + +uint16_t_ptr = cg.uint16.operator("ptr") +char_ptr = cg.global_ns.namespace("char").operator("ptr") +void_ptr = cg.void.operator("ptr") +lv_coord_t = cg.global_ns.namespace("lv_coord_t") +lv_event_code_t = cg.global_ns.enum("lv_event_code_t") +lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") +FontEngine = lvgl_ns.class_("FontEngine") +IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) +PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template()) +ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) +LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) +LvglAction = lvgl_ns.class_("LvglAction", automation.Action) +lv_lambda_t = lvgl_ns.class_("LvLambdaType") +LvCompound = lvgl_ns.class_("LvCompound") +lv_font_t = cg.global_ns.class_("lv_font_t") +lv_style_t = cg.global_ns.struct("lv_style_t") +# fake parent class for first class widgets and matrix buttons +lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") +lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) +lv_obj_t_ptr = lv_obj_base_t.operator("ptr") +lv_disp_t = cg.global_ns.struct("lv_disp_t") +lv_color_t = cg.global_ns.struct("lv_color_t") +lv_group_t = cg.global_ns.struct("lv_group_t") +LVTouchListener = lvgl_ns.class_("LVTouchListener") +LVEncoderListener = lvgl_ns.class_("LVEncoderListener") +lv_obj_t = LvType("lv_obj_t") +lv_page_t = LvType("LvPageType", parents=(LvCompound,)) +lv_img_t = LvType("lv_img_t") +lv_gradient_t = LvType("lv_grad_dsc_t") + +LV_EVENT = MockObj(base="LV_EVENT_", op="") +LV_STATE = MockObj(base="LV_STATE_", op="") +LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="") + + +class LvText(LvType): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + largs=[(cg.std_string, "text")], + lvalue=lambda w: w.get_property("text"), + has_on_value=True, + **kwargs, + ) + self.value_property = CONF_TEXT + + +class LvBoolean(LvType): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + largs=[(cg.bool_, "x")], + lvalue=lambda w: w.is_checked(), + has_on_value=True, + **kwargs, + ) + + +class LvSelect(LvType): + def __init__(self, *args, **kwargs): + parens = kwargs.pop("parents", ()) + (LvCompound,) + super().__init__( + *args, + largs=[(cg.int_, "x"), (cg.std_string, "text")], + lvalue=lambda w: [w.var.get_selected_index(), w.var.get_selected_text()], + has_on_value=True, + parents=parens, + **kwargs, + ) + + +class WidgetType: + """ + Describes a type of Widget, e.g. "bar" or "line" + """ + + def __init__( + self, + name: str, + w_type: LvType, + parts: tuple, + schema=None, + modify_schema=None, + lv_name=None, + ): + """ + :param name: The widget name, e.g. "bar" + :param w_type: The C type of the widget + :param parts: What parts this widget supports + :param schema: The config schema for defining a widget + :param modify_schema: A schema to update the widget + """ + self.name = name + self.lv_name = lv_name or name + self.w_type = w_type + self.parts = parts + if schema is None: + self.schema = {} + else: + self.schema = schema + if modify_schema is None: + self.modify_schema = self.schema + else: + self.modify_schema = modify_schema + self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") + + @property + def animated(self): + return False + + @property + def required_component(self): + return None + + def is_compound(self): + return self.w_type.inherits_from(LvCompound) + + async def to_code(self, w, config: dict): + """ + Generate code for a given widget + :param w: The widget + :param config: Its configuration + :return: Generated code as a list of text lines + """ + return [] + + def obj_creator(self, parent: MockObjClass, config: dict): + """ + Create an instance of the widget type + :param parent: The parent to which it should be attached + :param config: Its configuration + :return: Generated code as a single text line + """ + return lv_expr.call(f"{self.lv_name}_create", parent) + + def get_uses(self): + """ + Get a list of other widgets used by this one + :return: + """ + return () + + def get_max(self, config: dict): + return sys.maxsize + + def get_min(self, config: dict): + return -sys.maxsize + + def get_step(self, config: dict): + return 1 + + def get_scale(self, config: dict): + return 1.0 + + +class NumberType(WidgetType): + def get_max(self, config: dict): + return int(config[CONF_MAX_VALUE] or 100) + + def get_min(self, config: dict): + return int(config[CONF_MIN_VALUE] or 0) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py new file mode 100644 index 0000000000..e946a96000 --- /dev/null +++ b/esphome/components/lvgl/widgets/__init__.py @@ -0,0 +1,460 @@ +import sys +from typing import Any, Union + +from esphome import codegen as cg, config_validation as cv +from esphome.config_validation import Invalid +from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE +from esphome.core import ID, TimePeriod +from esphome.coroutine import FakeAwaitable +from esphome.cpp_generator import CallExpression, MockObj + +from ..defines import ( + CONF_DEFAULT, + CONF_FLEX_ALIGN_CROSS, + CONF_FLEX_ALIGN_MAIN, + CONF_FLEX_ALIGN_TRACK, + CONF_FLEX_FLOW, + CONF_GRID_COLUMN_ALIGN, + CONF_GRID_COLUMNS, + CONF_GRID_ROW_ALIGN, + CONF_GRID_ROWS, + CONF_LAYOUT, + CONF_MAIN, + CONF_PAD_COLUMN, + CONF_PAD_ROW, + CONF_SCROLLBAR_MODE, + CONF_STYLES, + CONF_WIDGETS, + OBJ_FLAGS, + PARTS, + STATES, + TYPE_FLEX, + TYPE_GRID, + LValidator, + call_lambda, + join_enums, + literal, +) +from ..helpers import add_lv_use +from ..lvcode import ( + LvConditional, + add_line_marks, + lv, + lv_add, + lv_assign, + lv_expr, + lv_obj, + lv_Pvariable, +) +from ..schemas import ALL_STYLES, 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__" + +theme_widget_map = {} +styles_used = set() + + +class Widget: + """ + Represents a Widget. + This class has a lot of methods. Adding any more runs foul of lint checks ("too many public methods"). + """ + + widgets_completed = False + + def __init__(self, var, wtype: WidgetType, config: dict = None): + self.var = var + self.type = wtype + self.config = config + self.scale = 1.0 + self.step = 1.0 + self.range_from = -sys.maxsize + self.range_to = sys.maxsize + if wtype.is_compound(): + self.obj = MockObj(f"{self.var}->obj") + else: + self.obj = var + self.outer = None + self.move_to_foreground = False + + @staticmethod + def create(name, var, wtype: WidgetType, config: dict = None): + w = Widget(var, wtype, config) + if name is not None: + widget_map[name] = w + return w + + def add_state(self, state): + return lv_obj.add_state(self.obj, literal(state)) + + def clear_state(self, state): + return lv_obj.clear_state(self.obj, literal(state)) + + def has_state(self, state): + return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0 + + def is_pressed(self): + return self.has_state(LV_STATE.PRESSED) + + def is_checked(self): + return self.has_state(LV_STATE.CHECKED) + + def add_flag(self, flag): + return lv_obj.add_flag(self.obj, literal(flag)) + + def clear_flag(self, flag): + return lv_obj.clear_flag(self.obj, literal(flag)) + + async def set_property(self, prop, value, animated: bool = None, lv_name=None): + """ + Set a property of the widget. + :param prop: The property name + :param value: The value + :param animated: If the change should be animated + :param lv_name: The base type of the widget e.g. "obj" + """ + if isinstance(value, dict): + value = value.get(prop) + if isinstance(ALL_STYLES.get(prop), LValidator): + value = await ALL_STYLES[prop].process(value) + else: + value = literal(value) + if value is None: + return + if isinstance(value, TimePeriod): + value = value.total_milliseconds + if isinstance(value, str): + value = literal(value) + lv_name = lv_name or self.type.lv_name + if animated is None or self.type.animated is not True: + lv.call(f"{lv_name}_set_{prop}", self.obj, value) + else: + lv.call( + f"{lv_name}_set_{prop}", + self.obj, + value, + literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"), + ) + + def get_property(self, prop, ltype=None): + ltype = ltype or self.__type_base() + return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})") + + def set_style(self, prop, value, state): + if value is None: + return + styles_used.add(prop) + lv.call(f"obj_set_style_{prop}", self.obj, value, state) + + def __type_base(self): + wtype = self.type.w_type + base = str(wtype) + if base.startswith("Lv"): + return f"{wtype}".removeprefix("Lv").removesuffix("Type").lower() + return f"{wtype}".removeprefix("lv_").removesuffix("_t") + + def __str__(self): + return f"({self.var}, {self.type})" + + def get_args(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.args + return [(lv_obj_t_ptr, "obj")] + + def get_value(self): + if isinstance(self.type.w_type, LvType): + result = self.type.w_type.value(self) + if isinstance(result, list): + return result[0] + return result + return self.obj + + def get_values(self): + if isinstance(self.type.w_type, LvType): + result = self.type.w_type.value(self) + if isinstance(result, list): + return result + return [result] + return [self.obj] + + def get_number_value(self): + value = self.type.mock_obj.get_value(self.obj) + if self.scale == 1.0: + return value + return value / float(self.scale) + + def is_selected(self): + """ + Overridable property to determine if the widget is selected. Will be None except + for matrix buttons + :return: + """ + return None + + def get_max(self): + return self.type.get_max(self.config) + + def get_min(self): + return self.type.get_min(self.config) + + def get_step(self): + return self.type.get_step(self.config) + + def get_scale(self): + return self.type.get_scale(self.config) + + +# Map of widgets to their config, used for trigger generation +widget_map: dict[Any, Widget] = {} + + +class LvScrActType(WidgetType): + """ + A "widget" representing the active screen. + """ + + def __init__(self): + super().__init__("lv_scr_act()", lv_obj_t, ()) + + async def to_code(self, w, config: dict): + return [] + + +lv_scr_act_spec = LvScrActType() + + +def get_scr_act(lv_comp: MockObj) -> Widget: + return Widget.create(None, lv_comp.get_scr_act(), lv_scr_act_spec, {}) + + +def get_widget_generator(wid): + """ + Used to wait for a widget during code generation. + :param wid: + :return: + """ + while True: + if obj := widget_map.get(wid): + return obj + if Widget.widgets_completed: + raise Invalid( + f"Widget {wid} not found, yet all widgets should be defined by now" + ) + yield + + +async def get_widget_(wid): + if obj := widget_map.get(wid): + return obj + return await FakeAwaitable(get_widget_generator(wid)) + + +def widgets_wait_generator(): + while True: + if Widget.widgets_completed: + return + yield + + +async def wait_for_widgets(): + if Widget.widgets_completed: + return + await FakeAwaitable(widgets_wait_generator()) + + +async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]: + if not config: + return [] + if not isinstance(config, list): + config = [config] + return [await get_widget_(c[id]) for c in config if id in c] + + +def collect_props(config): + """ + Collect all properties from a configuration + :param config: + :return: + """ + props = {} + for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]: + if prop in config: + props[prop] = config[prop] + return props + + +def collect_states(config): + """ + Collect prperties for each state of a widget + :param config: + :return: + """ + states = {CONF_DEFAULT: collect_props(config)} + for state in STATES: + if state in config: + states[state] = collect_props(config[state]) + return states + + +def collect_parts(config): + """ + Collect properties and states for all widget parts + :param config: + :return: + """ + parts = {CONF_MAIN: collect_states(config)} + for part in PARTS: + if part in config: + parts[part] = collect_states(config[part]) + return parts + + +async def set_obj_properties(w: Widget, config): + """Generate a list of C++ statements to apply properties to an lv_obj_t""" + if layout := config.get(CONF_LAYOUT): + layout_type: str = layout[CONF_TYPE] + add_lv_use(layout_type) + lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) + if (pad_row := layout.get(CONF_PAD_ROW)) is not None: + w.set_style(CONF_PAD_ROW, pad_row, 0) + if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: + w.set_style(CONF_PAD_COLUMN, pad_column, 0) + if layout_type == TYPE_GRID: + wid = config[CONF_ID] + rows = [str(x) for x in layout[CONF_GRID_ROWS]] + rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}" + row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) + row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) + w.set_style("grid_row_dsc_array", row_array, 0) + columns = [str(x) for x in layout[CONF_GRID_COLUMNS]] + columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}" + column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) + column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) + w.set_style("grid_column_dsc_array", column_array, 0) + w.set_style( + CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0 + ) + w.set_style( + CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0 + ) + if layout_type == TYPE_FLEX: + lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) + main = literal(layout[CONF_FLEX_ALIGN_MAIN]) + cross = literal(layout[CONF_FLEX_ALIGN_CROSS]) + track = literal(layout[CONF_FLEX_ALIGN_TRACK]) + lv_obj.set_flex_align(w.obj, main, cross, track) + parts = collect_parts(config) + for part, states in parts.items(): + part = "LV_PART_" + part.upper() + for state, props in states.items(): + state = "LV_STATE_" + state.upper() + if state == "LV_STATE_DEFAULT": + lv_state = literal(part) + elif part == "LV_PART_MAIN": + lv_state = literal(state) + else: + lv_state = join_enums((state, part)) + for style_id in props.get(CONF_STYLES, ()): + lv_obj.add_style(w.obj, MockObj(style_id), lv_state) + for prop, value in { + k: v for k, v in props.items() if k in ALL_STYLES + }.items(): + if isinstance(ALL_STYLES[prop], LValidator): + value = await ALL_STYLES[prop].process(value) + prop_r = STYLE_REMAP.get(prop, prop) + w.set_style(prop_r, value, lv_state) + if group := config.get(CONF_GROUP): + group = await cg.get_variable(group) + lv.group_add_obj(group, w.obj) + props = parts[CONF_MAIN][CONF_DEFAULT] + lambs = {} + flag_set = set() + flag_clr = set() + for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): + if isinstance(value, cv.Lambda): + lambs[prop] = value + elif value: + flag_set.add(prop) + else: + flag_clr.add(prop) + if flag_set: + adds = join_enums(flag_set, "LV_OBJ_FLAG_") + w.add_flag(adds) + if flag_clr: + 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_) + flag = f"LV_OBJ_FLAG_{key.upper()}" + with LvConditional(call_lambda(lamb)) as cond: + w.add_flag(flag) + cond.else_() + w.clear_flag(flag) + + if states := config.get(CONF_STATE): + adds = set() + clears = set() + lambs = {} + for key, value in states.items(): + if isinstance(value, cv.Lambda): + lambs[key] = value + elif value: + adds.add(key) + else: + clears.add(key) + if adds: + adds = join_enums(adds, "LV_STATE_") + w.add_state(adds) + if clears: + 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_) + 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") + + +async def add_widgets(parent: Widget, config: dict): + """ + Add all widgets to an object + :param parent: The enclosing obj + :param config: The configuration + :return: + """ + for w in config.get(CONF_WIDGETS, ()): + w_type, w_cnfig = next(iter(w.items())) + await widget_to_code(w_cnfig, w_type, parent.obj) + + +async def widget_to_code(w_cnfig, w_type: WidgetType, parent): + """ + Converts a Widget definition to C code. + :param w_cnfig: The widget configuration + :param w_type: The Widget type + :param parent: The parent to which the widget should be added + :return: + """ + spec: WidgetType = WIDGET_TYPES[w_type] + creator = spec.obj_creator(parent, w_cnfig) + add_lv_use(spec.name) + add_lv_use(*spec.get_uses()) + wid = w_cnfig[CONF_ID] + add_line_marks(wid) + if spec.is_compound(): + var = cg.new_Pvariable(wid) + lv_add(var.set_obj(creator)) + else: + var = lv_Pvariable(lv_obj_t, wid) + lv_assign(var, creator) + + w = Widget.create(wid, var, spec, w_cnfig) + if theme := theme_widget_map.get(w_type): + lv_add(CallExpression(theme, w.obj)) + await set_obj_properties(w, w_cnfig) + await add_widgets(w, w_cnfig) + await spec.to_code(w, w_cnfig) diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py new file mode 100644 index 0000000000..8adea72ad3 --- /dev/null +++ b/esphome/components/lvgl/widgets/animimg.py @@ -0,0 +1,117 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_DURATION, CONF_ID + +from ..automation import action_to_code +from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC +from ..helpers import lvgl_components_required +from ..lv_validation import lv_image, lv_milliseconds +from ..lvcode import lv +from ..types import LvType, ObjUpdateAction, void_ptr +from . import Widget, WidgetType, get_widgets +from .img import CONF_IMAGE +from .label import CONF_LABEL + +CONF_ANIMIMG = "animimg" +CONF_SRC_LIST_ID = "src_list_id" + + +def lv_repeat_count(value): + if isinstance(value, str) and value.lower() in ("forever", "infinite"): + value = 0xFFFF + return cv.int_range(min=0, max=0xFFFF)(value) + + +ANIMIMG_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_REPEAT_COUNT, default="forever"): lv_repeat_count, + cv.Optional(CONF_AUTO_START, default=True): cv.boolean, + } +) +ANIMIMG_SCHEMA = ANIMIMG_BASE_SCHEMA.extend( + { + cv.Required(CONF_DURATION): lv_milliseconds, + cv.Required(CONF_SRC): cv.ensure_list(lv_image), + cv.GenerateID(CONF_SRC_LIST_ID): cv.declare_id(void_ptr), + } +) + +ANIMIMG_MODIFY_SCHEMA = ANIMIMG_BASE_SCHEMA.extend( + { + cv.Optional(CONF_DURATION): lv_milliseconds, + } +) + +lv_animimg_t = LvType("lv_animimg_t") + + +class AnimimgType(WidgetType): + def __init__(self): + super().__init__( + CONF_ANIMIMG, + lv_animimg_t, + (CONF_MAIN,), + ANIMIMG_SCHEMA, + ANIMIMG_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + lvgl_components_required.add(CONF_IMAGE) + lvgl_components_required.add(CONF_ANIMIMG) + if CONF_SRC in config: + srcs = [ + await lv_image.process(await cg.get_variable(x)) + for x in config[CONF_SRC] + ] + src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs) + count = len(config[CONF_SRC]) + lv.animimg_set_src(w.obj, src_id, count) + lv.animimg_set_repeat_count(w.obj, config[CONF_REPEAT_COUNT]) + lv.animimg_set_duration(w.obj, config[CONF_DURATION]) + if config.get(CONF_AUTO_START): + lv.animimg_start(w.obj) + + def get_uses(self): + return "img", CONF_IMAGE, CONF_LABEL + + +animimg_spec = AnimimgType() + + +@automation.register_action( + "lvgl.animimg.start", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_animimg_t), + }, + key=CONF_ID, + ), +) +async def animimg_start(config, action_id, template_arg, args): + widget = await get_widgets(config) + + async def do_start(w: Widget): + lv.animimg_start(w.obj) + + return await action_to_code(widget, do_start, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.animimg.stop", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_animimg_t), + }, + key=CONF_ID, + ), +) +async def animimg_stop(config, action_id, template_arg, args): + widget = await get_widgets(config) + + async def do_stop(w: Widget): + lv.animimg_stop(w.obj) + + return await action_to_code(widget, do_stop, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py new file mode 100644 index 0000000000..dc120e4cbb --- /dev/null +++ b/esphome/components/lvgl/widgets/arc.py @@ -0,0 +1,82 @@ +import esphome.config_validation as cv +from esphome.const import ( + CONF_GROUP, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MODE, + CONF_ROTATION, + CONF_VALUE, +) +from esphome.cpp_types import nullptr + +from ..defines import ( + ARC_MODES, + CONF_ADJUSTABLE, + CONF_CHANGE_RATE, + CONF_END_ANGLE, + CONF_INDICATOR, + CONF_KNOB, + CONF_MAIN, + CONF_START_ANGLE, + literal, +) +from ..lv_validation import angle, get_start_value, lv_float +from ..lvcode import lv, lv_expr, lv_obj +from ..types import LvNumber, NumberType +from . import Widget + +CONF_ARC = "arc" +ARC_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_START_ANGLE, default=135): angle, + cv.Optional(CONF_END_ANGLE, default=45): angle, + cv.Optional(CONF_ROTATION, default=0.0): angle, + 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, + } +) + +ARC_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + } +) + + +class ArcType(NumberType): + def __init__(self): + super().__init__( + CONF_ARC, + LvNumber("lv_arc_t"), + parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + schema=ARC_SCHEMA, + modify_schema=ARC_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if CONF_MIN_VALUE in config: + lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.arc_set_bg_angles( + w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10 + ) + lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10) + lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) + lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) + + if config.get(CONF_ADJUSTABLE) is False: + lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) + w.clear_flag("LV_OBJ_FLAG_CLICKABLE") + elif CONF_GROUP not in config: + # 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) + + +arc_spec = ArcType() diff --git a/esphome/components/lvgl/widgets/button.py b/esphome/components/lvgl/widgets/button.py new file mode 100644 index 0000000000..b59884ee67 --- /dev/null +++ b/esphome/components/lvgl/widgets/button.py @@ -0,0 +1,20 @@ +from esphome.const import CONF_BUTTON + +from ..defines import CONF_MAIN +from ..types import LvBoolean, WidgetType + +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") + + def get_uses(self): + return ("btn",) + + async def to_code(self, w, config): + return [] + + +button_spec = ButtonType() diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py new file mode 100644 index 0000000000..c65bb4b354 --- /dev/null +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -0,0 +1,279 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components.key_provider import KeyProvider +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH +from esphome.cpp_generator import MockObj + +from ..automation import action_to_code +from ..defines import ( + BUTTONMATRIX_CTRLS, + CONF_BUTTONS, + CONF_CONTROL, + CONF_KEY_CODE, + CONF_MAIN, + CONF_ONE_CHECKED, + CONF_PAD_COLUMN, + CONF_PAD_ROW, + CONF_ROWS, + CONF_SELECTED, +) +from ..helpers import lvgl_components_required +from ..lv_validation import key_code, lv_bool, pixels +from ..lvcode import lv, lv_add, lv_expr +from ..schemas import automation_schema +from ..types import ( + LV_BTNMATRIX_CTRL, + LV_STATE, + LvBoolean, + LvCompound, + LvType, + ObjUpdateAction, + char_ptr, + lv_pseudo_button_t, +) +from . import Widget, WidgetType, get_widgets, widget_map +from .button import lv_button_t + +CONF_BUTTONMATRIX = "buttonmatrix" +CONF_BUTTON_TEXT_LIST_ID = "button_text_list_id" + +LvButtonMatrixButton = LvBoolean( + str(cg.uint16), + parents=(lv_pseudo_button_t,), +) +BUTTONMATRIX_BUTTON_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TEXT): cv.string, + cv.Optional(CONF_KEY_CODE): key_code, + cv.GenerateID(): cv.declare_id(LvButtonMatrixButton), + cv.Optional(CONF_WIDTH, default=1): cv.positive_int, + cv.Optional(CONF_CONTROL): cv.ensure_list( + cv.Schema( + {cv.Optional(k.lower()): cv.boolean for k in BUTTONMATRIX_CTRLS.choices} + ) + ), + } +).extend(automation_schema(lv_button_t)) + +BUTTONMATRIX_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ONE_CHECKED, default=False): lv_bool, + cv.Optional(CONF_PAD_ROW): pixels, + cv.Optional(CONF_PAD_COLUMN): pixels, + cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), + cv.Required(CONF_ROWS): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_BUTTONS): cv.ensure_list( + BUTTONMATRIX_BUTTON_SCHEMA + ), + } + ) + ), + } +) + + +class ButtonmatrixButtonType(WidgetType): + """ + A pseudo-widget for the matrix buttons + """ + + def __init__(self): + super().__init__("btnmatrix_btn", LvButtonMatrixButton, (), {}, {}) + + async def to_code(self, w, config: dict): + return [] + + +btn_btn_spec = ButtonmatrixButtonType() + + +class MatrixButton(Widget): + """ + Describes a button within a button matrix. + """ + + @staticmethod + def create_button(id, parent, config: dict, index): + w = MatrixButton(id, parent, config, index) + widget_map[id] = w + return w + + def __init__(self, id, parent: Widget, config, index): + super().__init__(id, btn_btn_spec, config) + self.parent = parent + self.index = index + self.obj = parent.obj + + def is_selected(self): + return self.parent.var.get_selected() == MockObj(self.var) + + @staticmethod + def map_ctrls(state): + state = str(state).upper().removeprefix("LV_STATE_") + assert state in BUTTONMATRIX_CTRLS.choices + return getattr(LV_BTNMATRIX_CTRL, state) + + def has_state(self, state): + state = self.map_ctrls(state) + return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state) + + def add_state(self, state): + state = self.map_ctrls(state) + return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state) + + def clear_state(self, state): + state = self.map_ctrls(state) + return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state) + + def is_pressed(self): + return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED) + + def is_checked(self): + return self.has_state(LV_STATE.CHECKED) + + def get_value(self): + return self.is_checked() + + def check_null(self): + return None + + +async def get_button_data(config, buttonmatrix: Widget): + """ + Process a button matrix button list + :param config: The row list + :param buttonmatrix: The parent variable + :return: text array id, control list, width list + """ + text_list = [] + ctrl_list = [] + width_list = [] + key_list = [] + for row in config: + for button_conf in row.get(CONF_BUTTONS, ()): + bid = button_conf[CONF_ID] + index = len(width_list) + MatrixButton.create_button(bid, buttonmatrix, button_conf, index) + cg.new_variable(bid, index) + text_list.append(button_conf.get(CONF_TEXT) or "") + key_list.append(button_conf.get(CONF_KEY_CODE) or 0) + width_list.append(button_conf[CONF_WIDTH]) + ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"] + for item in button_conf.get(CONF_CONTROL, ()): + ctrl.extend([k for k, v in item.items() if v]) + ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl)) + text_list.append("\n") + text_list = text_list[:-1] + text_list.append(cg.nullptr) + return text_list, ctrl_list, width_list, key_list + + +lv_buttonmatrix_t = LvType( + "LvButtonMatrixType", + parents=(KeyProvider, LvCompound), + largs=[(cg.uint16, "x")], + lvalue=lambda w: w.var.get_selected(), +) + + +class ButtonMatrixType(WidgetType): + def __init__(self): + super().__init__( + CONF_BUTTONMATRIX, + lv_buttonmatrix_t, + (CONF_MAIN, CONF_ITEMS), + BUTTONMATRIX_SCHEMA, + {}, + lv_name="btnmatrix", + ) + + async def to_code(self, w: Widget, config): + lvgl_components_required.add("BUTTONMATRIX") + if CONF_ROWS not in config: + return [] + text_list, ctrl_list, width_list, key_list = await get_button_data( + config[CONF_ROWS], w + ) + text_id = config[CONF_BUTTON_TEXT_LIST_ID] + text_id = cg.static_const_array(text_id, text_list) + lv.btnmatrix_set_map(w.obj, text_id) + set_btn_data(w.obj, ctrl_list, width_list) + lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED]) + for index, key in enumerate(key_list): + if key != 0: + lv_add(w.var.set_key(index, key)) + + def get_uses(self): + return ("btnmatrix",) + + +def set_btn_data(obj, ctrl_list, width_list): + for index, ctrl in enumerate(ctrl_list): + lv.btnmatrix_set_btn_ctrl(obj, index, ctrl) + for index, width in enumerate(width_list): + lv.btnmatrix_set_btn_width(obj, index, width) + + +buttonmatrix_spec = ButtonMatrixType() + + +@automation.register_action( + "lvgl.matrix.button.update", + ObjUpdateAction, + cv.Schema( + { + cv.Optional(CONF_WIDTH): cv.positive_int, + cv.Optional(CONF_CONTROL): cv.ensure_list( + cv.Schema( + { + cv.Optional(k.lower()): cv.boolean + for k in BUTTONMATRIX_CTRLS.choices + } + ), + ), + cv.Required(CONF_ID): cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(LvButtonMatrixButton), + }, + key=CONF_ID, + ) + ), + cv.Optional(CONF_SELECTED): lv_bool, + } + ), +) +async def button_update_to_code(config, action_id, template_arg, args): + widgets = await get_widgets(config[CONF_ID]) + assert all(isinstance(w, MatrixButton) for w in widgets) + + async def do_button_update(w: MatrixButton): + if (width := config.get(CONF_WIDTH)) is not None: + lv.btnmatrix_set_btn_width(w.obj, w.index, width) + if config.get(CONF_SELECTED): + lv.btnmatrix_set_selected_btn(w.obj, w.index) + if controls := config.get(CONF_CONTROL): + adds = [] + clrs = [] + for item in controls: + adds.extend( + [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v] + ) + clrs.extend( + [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v] + ) + if adds: + lv.btnmatrix_set_btn_ctrl( + w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds) + ) + if clrs: + lv.btnmatrix_clear_btn_ctrl( + w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs) + ) + + return await action_to_code( + widgets, do_button_update, action_id, template_arg, args + ) diff --git a/esphome/components/lvgl/widgets/checkbox.py b/esphome/components/lvgl/widgets/checkbox.py new file mode 100644 index 0000000000..75f4142eb1 --- /dev/null +++ b/esphome/components/lvgl/widgets/checkbox.py @@ -0,0 +1,32 @@ +from esphome.config_validation import Optional +from esphome.const import CONF_TEXT + +from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_PAD_COLUMN +from ..lv_validation import lv_text, pixels +from ..lvcode import lv +from ..schemas import TEXT_SCHEMA +from ..types import LvBoolean +from . import Widget, WidgetType + +CONF_CHECKBOX = "checkbox" + + +class CheckboxType(WidgetType): + def __init__(self): + super().__init__( + CONF_CHECKBOX, + LvBoolean("lv_checkbox_t"), + (CONF_MAIN, CONF_INDICATOR), + TEXT_SCHEMA.extend( + { + Optional(CONF_PAD_COLUMN): pixels, + } + ), + ) + + async def to_code(self, w: Widget, config): + if (value := config.get(CONF_TEXT)) is not None: + lv.checkbox_set_text(w.obj, await lv_text.process(value)) + + +checkbox_spec = CheckboxType() diff --git a/esphome/components/lvgl/widgets/dropdown.py b/esphome/components/lvgl/widgets/dropdown.py new file mode 100644 index 0000000000..a6bfc6bb88 --- /dev/null +++ b/esphome/components/lvgl/widgets/dropdown.py @@ -0,0 +1,92 @@ +import esphome.config_validation as cv +from esphome.const import CONF_OPTIONS + +from ..defines import ( + CONF_DIR, + CONF_INDICATOR, + CONF_MAIN, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_SELECTED_INDEX, + CONF_SELECTED_TEXT, + CONF_SYMBOL, + DIRECTIONS, + literal, +) +from ..helpers import lvgl_components_required +from ..lv_validation import lv_int, lv_text, option_string +from ..lvcode import LocalVariable, lv, lv_add, lv_expr +from ..schemas import part_schema +from ..types import LvCompound, LvSelect, LvType, lv_obj_t +from . import Widget, WidgetType, set_obj_properties +from .label import CONF_LABEL + +CONF_DROPDOWN = "dropdown" +CONF_DROPDOWN_LIST = "dropdown_list" + +lv_dropdown_t = LvSelect("LvDropdownType", parents=(LvCompound,)) + +lv_dropdown_list_t = LvType("lv_dropdown_list_t") +dropdown_list_spec = WidgetType( + CONF_DROPDOWN_LIST, lv_dropdown_list_t, (CONF_MAIN, CONF_SELECTED, CONF_SCROLLBAR) +) + +DROPDOWN_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_SYMBOL): lv_text, + cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int, + cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text, + cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, + cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec), + } +) + +DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), + } +) + +DROPDOWN_UPDATE_SCHEMA = DROPDOWN_BASE_SCHEMA.extend( + { + cv.Optional(CONF_OPTIONS): cv.ensure_list(option_string), + } +) + + +class DropdownType(WidgetType): + def __init__(self): + super().__init__( + CONF_DROPDOWN, + lv_dropdown_t, + (CONF_MAIN, CONF_INDICATOR), + DROPDOWN_SCHEMA, + modify_schema=DROPDOWN_UPDATE_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + lvgl_components_required.add(CONF_DROPDOWN) + if options := config.get(CONF_OPTIONS): + lv_add(w.var.set_options(options)) + if symbol := config.get(CONF_SYMBOL): + lv.dropdown_set_symbol(w.var.obj, await lv_text.process(symbol)) + if (selected := config.get(CONF_SELECTED_INDEX)) is not None: + value = await lv_int.process(selected) + lv_add(w.var.set_selected_index(value, literal("LV_ANIM_OFF"))) + if (selected := config.get(CONF_SELECTED_TEXT)) is not None: + value = await lv_text.process(selected) + lv_add(w.var.set_selected_text(value, literal("LV_ANIM_OFF"))) + if dirn := config.get(CONF_DIR): + lv.dropdown_set_dir(w.obj, literal(dirn)) + if dlist := config.get(CONF_DROPDOWN_LIST): + with LocalVariable( + "dropdown_list", lv_obj_t, lv_expr.dropdown_get_list(w.obj) + ) as dlist_obj: + dwid = Widget(dlist_obj, dropdown_list_spec, dlist) + await set_obj_properties(dwid, dlist) + + def get_uses(self): + return (CONF_LABEL,) + + +dropdown_spec = DropdownType() diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py new file mode 100644 index 0000000000..931d0c0b5b --- /dev/null +++ b/esphome/components/lvgl/widgets/img.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ANGLE, CONF_MODE + +from ..defines import ( + CONF_ANTIALIAS, + CONF_MAIN, + CONF_OFFSET_X, + CONF_OFFSET_Y, + CONF_PIVOT_X, + CONF_PIVOT_Y, + CONF_SRC, + CONF_ZOOM, + LvConstant, +) +from ..lv_validation import angle, lv_bool, lv_image, size, zoom +from ..lvcode import lv +from ..types import lv_img_t +from . import Widget, WidgetType +from .label import CONF_LABEL + +CONF_IMAGE = "image" + +BASE_IMG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PIVOT_X, default="50%"): size, + cv.Optional(CONF_PIVOT_Y, default="50%"): size, + cv.Optional(CONF_ANGLE): angle, + cv.Optional(CONF_ZOOM): zoom, + cv.Optional(CONF_OFFSET_X): size, + cv.Optional(CONF_OFFSET_Y): size, + cv.Optional(CONF_ANTIALIAS): lv_bool, + cv.Optional(CONF_MODE): LvConstant( + "LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL" + ).one_of, + } +) + +IMG_SCHEMA = BASE_IMG_SCHEMA.extend( + { + cv.Required(CONF_SRC): lv_image, + } +) + +IMG_MODIFY_SCHEMA = BASE_IMG_SCHEMA.extend( + { + cv.Optional(CONF_SRC): lv_image, + } +) + + +class ImgType(WidgetType): + def __init__(self): + super().__init__( + CONF_IMAGE, + lv_img_t, + (CONF_MAIN,), + IMG_SCHEMA, + IMG_MODIFY_SCHEMA, + lv_name="img", + ) + + def get_uses(self): + return "img", CONF_LABEL + + async def to_code(self, w: Widget, config): + if src := config.get(CONF_SRC): + src = await cg.get_variable(src) + lv.img_set_src(w.obj, await lv_image.process(src)) + if (cf_angle := config.get(CONF_ANGLE)) is not None: + pivot_x = config[CONF_PIVOT_X] + pivot_y = config[CONF_PIVOT_Y] + lv.img_set_pivot(w.obj, pivot_x, pivot_y) + lv.img_set_angle(w.obj, cf_angle) + if (img_zoom := config.get(CONF_ZOOM)) is not None: + lv.img_set_zoom(w.obj, img_zoom) + if (offset := config.get(CONF_OFFSET_X)) is not None: + lv.img_set_offset_x(w.obj, offset) + if (offset := config.get(CONF_OFFSET_Y)) is not None: + lv.img_set_offset_y(w.obj, offset) + if CONF_ANTIALIAS in config: + lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) + if mode := config.get(CONF_MODE): + lv.img_set_mode(w.obj, mode) + + +img_spec = ImgType() diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py new file mode 100644 index 0000000000..ba7edb302e --- /dev/null +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -0,0 +1,49 @@ +from esphome.components.key_provider import KeyProvider +import esphome.config_validation as cv +from esphome.const import CONF_ITEMS, CONF_MODE +from esphome.cpp_types import std_string + +from ..defines import CONF_MAIN, KEYBOARD_MODES, literal +from ..helpers import add_lv_use, lvgl_components_required +from ..types import LvCompound, LvType +from . import Widget, WidgetType, get_widgets +from .textarea import CONF_TEXTAREA, lv_textarea_t + +CONF_KEYBOARD = "keyboard" + +KEYBOARD_SCHEMA = { + cv.Optional(CONF_MODE, default="TEXT_UPPER"): KEYBOARD_MODES.one_of, + cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t), +} + +lv_keyboard_t = LvType( + "LvKeyboardType", + parents=(KeyProvider, LvCompound), + largs=[(std_string, "text")], + has_on_value=True, + lvalue=lambda w: literal(f"lv_textarea_get_text({w.obj})"), +) + + +class KeyboardType(WidgetType): + def __init__(self): + super().__init__( + CONF_KEYBOARD, + lv_keyboard_t, + (CONF_MAIN, CONF_ITEMS), + KEYBOARD_SCHEMA, + ) + + def get_uses(self): + return CONF_KEYBOARD, CONF_TEXTAREA + + async def to_code(self, w: Widget, config: dict): + lvgl_components_required.add("KEY_LISTENER") + lvgl_components_required.add(CONF_KEYBOARD) + add_lv_use("btnmatrix") + await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE])) + if ta := await get_widgets(config, CONF_TEXTAREA): + await w.set_property(CONF_TEXTAREA, ta[0].obj) + + +keyboard_spec = KeyboardType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py new file mode 100644 index 0000000000..6b04235674 --- /dev/null +++ b/esphome/components/lvgl/widgets/label.py @@ -0,0 +1,42 @@ +import esphome.config_validation as cv +from esphome.const import CONF_TEXT + +from ..defines import ( + CONF_LONG_MODE, + CONF_MAIN, + CONF_RECOLOR, + CONF_SCROLLBAR, + CONF_SELECTED, + LV_LONG_MODES, +) +from ..lv_validation import lv_bool, lv_text +from ..schemas import TEXT_SCHEMA +from ..types import LvText, WidgetType +from . import Widget + +CONF_LABEL = "label" + + +class LabelType(WidgetType): + def __init__(self): + super().__init__( + CONF_LABEL, + LvText("lv_label_t"), + (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), + TEXT_SCHEMA.extend( + { + cv.Optional(CONF_RECOLOR): lv_bool, + cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, + } + ), + ) + + async def to_code(self, w: Widget, config): + """For a text object, create and set text""" + if value := config.get(CONF_TEXT): + await w.set_property(CONF_TEXT, await lv_text.process(value)) + await w.set_property(CONF_LONG_MODE, config) + await w.set_property(CONF_RECOLOR, config) + + +label_spec = LabelType() diff --git a/esphome/components/lvgl/widgets/led.py b/esphome/components/lvgl/widgets/led.py new file mode 100644 index 0000000000..647973c9b7 --- /dev/null +++ b/esphome/components/lvgl/widgets/led.py @@ -0,0 +1,29 @@ +import esphome.config_validation as cv +from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED + +from ..defines import CONF_MAIN +from ..lv_validation import lv_brightness, lv_color +from ..lvcode import lv +from ..types import LvType +from . import Widget, WidgetType + +LED_SCHEMA = cv.Schema( + { + cv.Optional(CONF_COLOR): lv_color, + cv.Optional(CONF_BRIGHTNESS): lv_brightness, + } +) + + +class LedType(WidgetType): + def __init__(self): + super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA) + + async def to_code(self, w: Widget, config): + if (color := config.get(CONF_COLOR)) is not None: + lv.led_set_color(w.obj, await lv_color.process(color)) + if (brightness := config.get(CONF_BRIGHTNESS)) is not None: + lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) + + +led_spec = LedType() diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py new file mode 100644 index 0000000000..4c6439fde4 --- /dev/null +++ b/esphome/components/lvgl/widgets/line.py @@ -0,0 +1,52 @@ +import functools + +import esphome.codegen as cg +import esphome.config_validation as cv + +from ..defines import CONF_MAIN +from ..lvcode import lv +from ..types import LvType +from . import Widget, WidgetType + +CONF_LINE = "line" +CONF_POINTS = "points" +CONF_POINT_LIST_ID = "point_list_id" + +lv_point_t = cg.global_ns.struct("lv_point_t") + + +def point_list(il): + il = cv.string(il) + nl = il.replace(" ", "").split(",") + return [int(n) for n in nl] + + +def cv_point_list(value): + if not isinstance(value, list): + raise cv.Invalid("List of points required") + values = [point_list(v) for v in value] + if not functools.reduce(lambda f, v: f and len(v) == 2, values, True): + raise cv.Invalid("Points must be a list of x,y integer pairs") + return values + + +LINE_SCHEMA = { + cv.Required(CONF_POINTS): cv_point_list, + cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t), +} + + +class LineType(WidgetType): + def __init__(self): + super().__init__( + CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA, modify_schema={} + ) + + async def to_code(self, w: Widget, config): + """For a line object, create and add the points""" + if data := config.get(CONF_POINTS): + points = cg.static_const_array(config[CONF_POINT_LIST_ID], data) + lv.line_set_points(w.obj, points, len(data)) + + +line_spec = LineType() diff --git a/esphome/components/lvgl/widgets/lv_bar.py b/esphome/components/lvgl/widgets/lv_bar.py new file mode 100644 index 0000000000..57209370c0 --- /dev/null +++ b/esphome/components/lvgl/widgets/lv_bar.py @@ -0,0 +1,55 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE + +from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal +from ..lv_validation import animated, get_start_value, lv_float +from ..lvcode import lv +from ..types import LvNumber, NumberType +from . import Widget + +# Note this file cannot be called "bar.py" because that name is disallowed. + +CONF_BAR = "bar" +BAR_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + +BAR_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + + +class BarType(NumberType): + def __init__(self): + super().__init__( + CONF_BAR, + LvNumber("lv_bar_t"), + parts=(CONF_MAIN, CONF_INDICATOR), + schema=BAR_SCHEMA, + modify_schema=BAR_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + var = w.obj + if CONF_MIN_VALUE in config: + lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.bar_set_mode(var, literal(config[CONF_MODE])) + value = await get_start_value(config) + if value is not None: + lv.bar_set_value(var, value, literal(config[CONF_ANIMATED])) + + @property + def animated(self): + return True + + +bar_spec = BarType() diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py new file mode 100644 index 0000000000..cd61d1c775 --- /dev/null +++ b/esphome/components/lvgl/widgets/meter.py @@ -0,0 +1,314 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_COLOR, + CONF_COUNT, + CONF_ID, + CONF_ITEMS, + CONF_LENGTH, + CONF_LOCAL, + CONF_RANGE_FROM, + CONF_RANGE_TO, + CONF_ROTATION, + CONF_VALUE, + CONF_WIDTH, +) + +from ..automation import action_to_code +from ..defines import ( + CONF_END_VALUE, + CONF_INDICATOR, + CONF_MAIN, + CONF_OPA, + CONF_PIVOT_X, + CONF_PIVOT_Y, + CONF_SRC, + CONF_START_VALUE, + CONF_TICKS, +) +from ..helpers import add_lv_use +from ..lv_validation import ( + angle, + get_end_value, + get_start_value, + lv_bool, + lv_color, + lv_float, + lv_image, + opacity, + requires_component, + size, +) +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj +from ..types import LvType, ObjUpdateAction +from . import Widget, WidgetType, get_widgets +from .arc import CONF_ARC +from .img import CONF_IMAGE +from .line import CONF_LINE +from .obj import obj_spec + +CONF_ANGLE_RANGE = "angle_range" +CONF_COLOR_END = "color_end" +CONF_COLOR_START = "color_start" +CONF_INDICATORS = "indicators" +CONF_LABEL_GAP = "label_gap" +CONF_MAJOR = "major" +CONF_METER = "meter" +CONF_R_MOD = "r_mod" +CONF_SCALES = "scales" +CONF_STRIDE = "stride" +CONF_TICK_STYLE = "tick_style" + +lv_meter_t = LvType("lv_meter_t") +lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t") +lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr") + + +def pixels(value): + """A size in one axis in pixels""" + if isinstance(value, str) and value.lower().endswith("px"): + return cv.int_(value[:-2]) + return cv.int_(value) + + +INDICATOR_LINE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_R_MOD, default=0): size, + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_OPA): opacity, + } +) +INDICATOR_IMG_SCHEMA = cv.Schema( + { + cv.Required(CONF_SRC): lv_image, + cv.Required(CONF_PIVOT_X): pixels, + cv.Required(CONF_PIVOT_Y): pixels, + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_OPA): opacity, + } +) +INDICATOR_ARC_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_R_MOD, default=0): size, + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + cv.Optional(CONF_OPA): opacity, + } +) +INDICATOR_TICKS_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR_START, default=0): lv_color, + cv.Optional(CONF_COLOR_END): lv_color, + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + cv.Optional(CONF_LOCAL, default=False): lv_bool, + } +) +INDICATOR_SCHEMA = cv.Schema( + { + cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All( + INDICATOR_IMG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + requires_component("image"), + ), + cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + } +) + +SCALE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TICKS): cv.Schema( + { + cv.Optional(CONF_COUNT, default=12): cv.positive_int, + cv.Optional(CONF_WIDTH, default=2): size, + cv.Optional(CONF_LENGTH, default=10): size, + cv.Optional(CONF_COLOR, default=0x808080): lv_color, + cv.Optional(CONF_MAJOR): cv.Schema( + { + cv.Optional(CONF_STRIDE, default=3): cv.positive_int, + cv.Optional(CONF_WIDTH, default=5): size, + cv.Optional(CONF_LENGTH, default="15%"): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_LABEL_GAP, default=4): size, + } + ), + } + ), + cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, + cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, + cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), + cv.Optional(CONF_ROTATION): angle, + cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), + } +) + +METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)} + + +class MeterType(WidgetType): + def __init__(self): + super().__init__( + CONF_METER, + lv_meter_t, + (CONF_MAIN, CONF_INDICATOR, CONF_TICKS, CONF_ITEMS), + METER_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + """For a meter object, create and set parameters""" + + var = w.obj + for scale_conf in config.get(CONF_SCALES, ()): + rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 + if CONF_ROTATION in scale_conf: + rotation = scale_conf[CONF_ROTATION] // 10 + with LocalVariable( + "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) + ) as meter_var: + lv.meter_set_scale_range( + var, + meter_var, + scale_conf[CONF_RANGE_FROM], + scale_conf[CONF_RANGE_TO], + scale_conf[CONF_ANGLE_RANGE], + rotation, + ) + if ticks := scale_conf.get(CONF_TICKS): + color = await lv_color.process(ticks[CONF_COLOR]) + lv.meter_set_scale_ticks( + var, + meter_var, + ticks[CONF_COUNT], + ticks[CONF_WIDTH], + ticks[CONF_LENGTH], + color, + ) + if CONF_MAJOR in ticks: + major = ticks[CONF_MAJOR] + color = await lv_color.process(major[CONF_COLOR]) + lv.meter_set_scale_major_ticks( + var, + meter_var, + major[CONF_STRIDE], + major[CONF_WIDTH], + major[CONF_LENGTH], + color, + major[CONF_LABEL_GAP], + ) + for indicator in scale_conf.get(CONF_INDICATORS, ()): + (t, v) = next(iter(indicator.items())) + iid = v[CONF_ID] + ivar = cg.Pvariable(iid, cg.nullptr, type_=lv_meter_indicator_t) + # Enable getting the meter to which this belongs. + wid = Widget.create(iid, var, obj_spec, v) + wid.obj = ivar + if t == CONF_LINE: + color = await lv_color.process(v[CONF_COLOR]) + lv_assign( + ivar, + lv_expr.meter_add_needle_line( + var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + ), + ) + if t == CONF_ARC: + color = await lv_color.process(v[CONF_COLOR]) + lv_assign( + ivar, + lv_expr.meter_add_arc( + var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + ), + ) + if t == CONF_TICK_STYLE: + color_start = await lv_color.process(v[CONF_COLOR_START]) + color_end = await lv_color.process( + v.get(CONF_COLOR_END) or color_start + ) + lv_assign( + ivar, + lv_expr.meter_add_scale_lines( + var, + meter_var, + color_start, + color_end, + v[CONF_LOCAL], + v[CONF_WIDTH], + ), + ) + if t == CONF_IMAGE: + add_lv_use("img") + lv_assign( + ivar, + lv_expr.meter_add_needle_img( + var, + meter_var, + await lv_image.process(v[CONF_SRC]), + v[CONF_PIVOT_X], + v[CONF_PIVOT_Y], + ), + ) + await set_indicator_values(var, ivar, v) + + +meter_spec = MeterType() + + +@automation.register_action( + "lvgl.indicator.update", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_meter_indicator_t), + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + cv.Optional(CONF_OPA): opacity, + } + ), +) +async def indicator_update_to_code(config, action_id, template_arg, args): + widget = await get_widgets(config) + + async def set_value(w: Widget): + await set_indicator_values(w.var, w.obj, config) + + return await action_to_code(widget, set_value, action_id, template_arg, args) + + +async def set_indicator_values(meter, indicator, config): + start_value = await get_start_value(config) + end_value = await get_end_value(config) + if start_value is not None: + if end_value is None: + lv.meter_set_indicator_value(meter, indicator, start_value) + else: + lv.meter_set_indicator_start_value(meter, indicator, start_value) + if end_value is not None: + lv.meter_set_indicator_end_value(meter, indicator, end_value) + if (opa := config.get(CONF_OPA)) is not None: + lv_assign(indicator.opa, await opacity.process(opa)) + lv_obj.invalidate(meter) diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py new file mode 100644 index 0000000000..be0f2100d7 --- /dev/null +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -0,0 +1,147 @@ +from esphome import config_validation as cv +from esphome.const import CONF_BUTTON, CONF_ID, CONF_ITEMS, CONF_TEXT +from esphome.core import ID +from esphome.cpp_generator import new_Pvariable, static_const_array +from esphome.cpp_types import nullptr + +from ..defines import ( + CONF_BODY, + CONF_BUTTON_STYLE, + CONF_BUTTONS, + CONF_CLOSE_BUTTON, + CONF_MSGBOXES, + CONF_TITLE, + TYPE_FLEX, + literal, +) +from ..helpers import add_lv_use, lvgl_components_required +from ..lv_validation import lv_bool, lv_pct, lv_text +from ..lvcode import ( + EVENT_ARG, + LambdaContext, + LocalVariable, + lv, + lv_add, + lv_assign, + lv_expr, + lv_obj, + lv_Pvariable, +) +from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema, part_schema +from ..types import LV_EVENT, char_ptr, lv_obj_t +from . import Widget, set_obj_properties +from .button import button_spec +from .buttonmatrix import ( + BUTTONMATRIX_BUTTON_SCHEMA, + CONF_BUTTON_TEXT_LIST_ID, + buttonmatrix_spec, + get_button_data, + lv_buttonmatrix_t, + set_btn_data, +) +from .label import CONF_LABEL +from .obj import obj_spec + +CONF_MSGBOX = "msgbox" +MSGBOX_SCHEMA = container_schema( + obj_spec, + STYLE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t), + cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, + cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA, + cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA), + cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool, + cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), + } + ), +) + + +async def msgbox_to_code(top_layer, conf): + """ + Construct a message box. This consists of a full-screen translucent background enclosing a centered container + with an optional title, body, close button and a button matrix. And any other widgets the user cares to add + :param conf: The config data + :return: code to add to the init lambda + """ + add_lv_use( + TYPE_FLEX, + CONF_BUTTON, + CONF_LABEL, + CONF_MSGBOX, + *buttonmatrix_spec.get_uses(), + *button_spec.get_uses(), + ) + lvgl_components_required.add("BUTTONMATRIX") + messagebox_id = conf[CONF_ID] + outer_id = f"{messagebox_id.id}_outer" + outer = lv_Pvariable(lv_obj_t, messagebox_id.id + "_outer") + buttonmatrix = new_Pvariable( + ID( + f"{messagebox_id.id}_buttonmatrix_", + is_declaration=True, + type=lv_buttonmatrix_t, + ) + ) + msgbox = lv_Pvariable(lv_obj_t, messagebox_id.id) + outer_widget = Widget.create(outer_id, outer, obj_spec, conf) + outer_widget.move_to_foreground = True + msgbox_widget = Widget.create(messagebox_id, msgbox, obj_spec, conf) + msgbox_widget.outer = outer_widget + buttonmatrix_widget = Widget.create( + str(buttonmatrix), buttonmatrix, buttonmatrix_spec, conf + ) + text_list, ctrl_list, width_list, _ = await get_button_data( + (conf,), buttonmatrix_widget + ) + text_id = conf[CONF_BUTTON_TEXT_LIST_ID] + text_list = static_const_array(text_id, text_list) + text = await lv_text.process(conf[CONF_BODY].get(CONF_TEXT, "")) + title = await lv_text.process(conf[CONF_TITLE].get(CONF_TEXT, "")) + close_button = conf[CONF_CLOSE_BUTTON] + lv_assign(outer, lv_expr.obj_create(top_layer)) + lv_obj.set_width(outer, lv_pct(100)) + lv_obj.set_height(outer, lv_pct(100)) + lv_obj.set_style_bg_opa(outer, 128, 0) + lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0) + lv_obj.set_style_border_width(outer, 0, 0) + lv_obj.set_style_pad_all(outer, 0, 0) + lv_obj.set_style_radius(outer, 0, 0) + outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") + lv_assign( + msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button) + ) + lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0) + lv_add(buttonmatrix.set_obj(lv_expr.msgbox_get_btns(msgbox))) + if button_style := conf.get(CONF_BUTTON_STYLE): + button_style = {CONF_ITEMS: button_style} + await set_obj_properties(buttonmatrix_widget, button_style) + await set_obj_properties(msgbox_widget, conf) + async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action: + outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") + if close_button: + with LocalVariable( + "close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox) + ) as close_btn: + lv_obj.remove_event_cb(close_btn, nullptr) + lv_obj.add_event_cb( + close_btn, + await close_action.get_lambda(), + LV_EVENT.CLICKED, + nullptr, + ) + else: + lv_obj.add_event_cb( + outer, await close_action.get_lambda(), LV_EVENT.CLICKED, nullptr + ) + + if len(ctrl_list) != 0 or len(width_list) != 0: + set_btn_data(buttonmatrix.obj, ctrl_list, width_list) + + +async def msgboxes_to_code(lv_component, config): + top_layer = lv.disp_get_layer_top(lv_component.get_disp()) + for conf in config.get(CONF_MSGBOXES, ()): + await msgbox_to_code(top_layer, conf) diff --git a/esphome/components/lvgl/widgets/obj.py b/esphome/components/lvgl/widgets/obj.py new file mode 100644 index 0000000000..afb4c97f33 --- /dev/null +++ b/esphome/components/lvgl/widgets/obj.py @@ -0,0 +1,30 @@ +from esphome import automation + +from ..automation import update_to_code +from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR +from ..schemas import create_modify_schema +from ..types import ObjUpdateAction, WidgetType, lv_obj_t + + +class ObjType(WidgetType): + """ + The base LVGL object. All other widgets inherit from this. + """ + + def __init__(self): + super().__init__( + CONF_OBJ, lv_obj_t, (CONF_MAIN, CONF_SCROLLBAR), schema={}, modify_schema={} + ) + + async def to_code(self, w, config): + return [] + + +obj_spec = ObjType() + + +@automation.register_action( + "lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec) +) +async def obj_update_to_code(config, action_id, template_arg, args): + return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/page.py b/esphome/components/lvgl/widgets/page.py new file mode 100644 index 0000000000..0e84ab6791 --- /dev/null +++ b/esphome/components/lvgl/widgets/page.py @@ -0,0 +1,157 @@ +from esphome import automation, codegen as cg +from esphome.automation import Trigger +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID + +from ..defines import ( + CONF_ANIMATION, + CONF_LVGL_ID, + CONF_PAGE, + CONF_PAGE_WRAP, + CONF_SKIP, + LV_ANIM, + literal, +) +from ..lv_validation import lv_bool, lv_milliseconds +from ..lvcode import ( + EVENT_ARG, + LVGL_COMP_ARG, + LambdaContext, + add_line_marks, + lv_add, + lvgl_comp, +) +from ..schemas import LVGL_SCHEMA +from ..types import LvglAction, lv_page_t +from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties + +CONF_ON_LOAD = "on_load" +CONF_ON_UNLOAD = "on_unload" + +PAGE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_SKIP, default=False): lv_bool, + cv.Optional(CONF_ON_LOAD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()), + } + ), + cv.Optional(CONF_ON_UNLOAD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()), + } + ), + } +) + + +class PageType(WidgetType): + def __init__(self): + super().__init__( + CONF_PAGE, + lv_page_t, + (), + PAGE_SCHEMA, + modify_schema={}, + ) + + async def to_code(self, w: Widget, config: dict): + return [] + + +SHOW_SCHEMA = LVGL_SCHEMA.extend( + { + cv.Optional(CONF_ANIMATION, default="NONE"): LV_ANIM.one_of, + cv.Optional(CONF_TIME, default="50ms"): lv_milliseconds, + } +) + +page_spec = PageType() + + +@automation.register_action( + "lvgl.page.next", + LvglAction, + SHOW_SCHEMA, +) +async def page_next_to_code(config, action_id, template_arg, args): + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_next_page(animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +@automation.register_action( + "lvgl.page.previous", + LvglAction, + SHOW_SCHEMA, +) +async def page_previous_to_code(config, action_id, template_arg, args): + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_prev_page(animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +@automation.register_action( + "lvgl.page.show", + LvglAction, + cv.maybe_simple_value( + SHOW_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.use_id(lv_page_t), + } + ), + key=CONF_ID, + ), +) +async def page_show_to_code(config, action_id, template_arg, args): + widget = await cg.get_variable(config[CONF_ID]) + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_page(widget.index, animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +async def add_pages(lv_component, config): + lv_add(lv_component.set_page_wrap(config[CONF_PAGE_WRAP])) + for pconf in config.get(CONF_PAGES, ()): + id = pconf[CONF_ID] + skip = pconf[CONF_SKIP] + var = cg.new_Pvariable(id, skip) + page = Widget.create(id, var, page_spec, pconf) + lv_add(lv_component.add_page(var)) + # Set outer config first + await set_obj_properties(page, config) + await set_obj_properties(page, pconf) + await add_widgets(page, pconf) + + +async def generate_page_triggers(lv_component, config): + for pconf in config.get(CONF_PAGES, ()): + page = (await get_widgets(pconf))[0] + for ev in (CONF_ON_LOAD, CONF_ON_UNLOAD): + for loaded in pconf.get(ev, ()): + trigger = cg.new_Pvariable(loaded[CONF_TRIGGER_ID]) + await automation.build_automation(trigger, [], loaded) + async with LambdaContext(EVENT_ARG, where=id) as context: + lv_add(trigger.trigger()) + lv_add( + lv_component.add_event_cb( + page.obj, + await context.get_lambda(), + literal(f"LV_EVENT_SCREEN_{ev[3:].upper()}_START"), + ) + ) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py new file mode 100644 index 0000000000..742b538938 --- /dev/null +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_SIZE, CONF_TEXT +from esphome.cpp_generator import MockObjClass + +from ..defines import CONF_MAIN, literal +from ..lv_validation import color, color_retmapper, lv_text +from ..lvcode import LocalVariable, lv, lv_expr +from ..schemas import TEXT_SCHEMA +from ..types import WidgetType, lv_obj_t +from . import Widget + +CONF_QRCODE = "qrcode" +CONF_DARK_COLOR = "dark_color" +CONF_LIGHT_COLOR = "light_color" + +QRCODE_SCHEMA = TEXT_SCHEMA.extend( + { + cv.Optional(CONF_DARK_COLOR, default="black"): color, + cv.Optional(CONF_LIGHT_COLOR, default="white"): color, + cv.Required(CONF_SIZE): cv.int_, + } +) + + +class QrCodeType(WidgetType): + def __init__(self): + super().__init__( + CONF_QRCODE, + lv_obj_t, + (CONF_MAIN,), + QRCODE_SCHEMA, + modify_schema=TEXT_SCHEMA, + ) + + def get_uses(self): + return ("canvas", "img") + + def obj_creator(self, parent: MockObjClass, config: dict): + dark_color = color_retmapper(config[CONF_DARK_COLOR]) + light_color = color_retmapper(config[CONF_LIGHT_COLOR]) + size = config[CONF_SIZE] + return lv_expr.call("qrcode_create", parent, size, dark_color, light_color) + + async def to_code(self, w: Widget, config): + if (value := config.get(CONF_TEXT)) is not None: + value = await lv_text.process(value) + with LocalVariable( + "qr_text", cg.const_char_ptr, value, modifier="" + ) as str_obj: + lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})")) + + +qr_code_spec = QrCodeType() diff --git a/esphome/components/lvgl/widgets/roller.py b/esphome/components/lvgl/widgets/roller.py new file mode 100644 index 0000000000..6f9fee47d4 --- /dev/null +++ b/esphome/components/lvgl/widgets/roller.py @@ -0,0 +1,84 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MODE, CONF_OPTIONS + +from ..defines import ( + CONF_ANIMATED, + CONF_MAIN, + CONF_SELECTED, + CONF_SELECTED_INDEX, + CONF_SELECTED_TEXT, + CONF_VISIBLE_ROW_COUNT, + ROLLER_MODES, + literal, +) +from ..helpers import lvgl_components_required +from ..lv_validation import animated, lv_int, lv_text, option_string +from ..lvcode import lv_add +from ..types import LvSelect +from . import WidgetType +from .label import CONF_LABEL + +CONF_ROLLER = "roller" +lv_roller_t = LvSelect("LvRollerType") + +ROLLER_BASE_SCHEMA = cv.Schema( + { + cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int, + cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text, + cv.Optional(CONF_VISIBLE_ROW_COUNT): lv_int, + cv.Optional(CONF_MODE): ROLLER_MODES.one_of, + } +) + +ROLLER_SCHEMA = ROLLER_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), + } +) + +ROLLER_MODIFY_SCHEMA = ROLLER_BASE_SCHEMA.extend( + { + cv.Optional(CONF_ANIMATED, default=True): animated, + cv.Optional(CONF_OPTIONS): cv.ensure_list(option_string), + } +) + + +class RollerType(WidgetType): + def __init__(self): + super().__init__( + CONF_ROLLER, + lv_roller_t, + (CONF_MAIN, CONF_SELECTED), + ROLLER_SCHEMA, + ROLLER_MODIFY_SCHEMA, + ) + + async def to_code(self, w, config): + lvgl_components_required.add(CONF_ROLLER) + if mode := config.get(CONF_MODE): + mode = await ROLLER_MODES.process(mode) + lv_add(w.var.set_mode(mode)) + if options := config.get(CONF_OPTIONS): + lv_add(w.var.set_options(options)) + animopt = literal(config.get(CONF_ANIMATED, "LV_ANIM_OFF")) + if (selected := config.get(CONF_SELECTED_INDEX)) is not None: + value = await lv_int.process(selected) + lv_add(w.var.set_selected_index(value, animopt)) + if (selected := config.get(CONF_SELECTED_TEXT)) is not None: + value = await lv_text.process(selected) + lv_add(w.var.set_selected_text(value, animopt)) + await w.set_property( + CONF_VISIBLE_ROW_COUNT, + await lv_int.process(config.get(CONF_VISIBLE_ROW_COUNT)), + ) + + @property + def animated(self): + return True + + def get_uses(self): + return (CONF_LABEL,) + + +roller_spec = RollerType() diff --git a/esphome/components/lvgl/widgets/slider.py b/esphome/components/lvgl/widgets/slider.py new file mode 100644 index 0000000000..d5017668e4 --- /dev/null +++ b/esphome/components/lvgl/widgets/slider.py @@ -0,0 +1,63 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE + +from ..defines import ( + BAR_MODES, + CONF_ANIMATED, + CONF_INDICATOR, + CONF_KNOB, + CONF_MAIN, + literal, +) +from ..helpers import add_lv_use +from ..lv_validation import animated, get_start_value, lv_float +from ..lvcode import lv +from ..types import LvNumber, NumberType +from . import Widget +from .lv_bar import CONF_BAR + +CONF_SLIDER = "slider" +SLIDER_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + +SLIDER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + + +class SliderType(NumberType): + def __init__(self): + super().__init__( + CONF_SLIDER, + LvNumber("lv_slider_t"), + parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + schema=SLIDER_SCHEMA, + modify_schema=SLIDER_MODIFY_SCHEMA, + ) + + @property + def animated(self): + return True + + async def to_code(self, w: Widget, config): + add_lv_use(CONF_BAR) + if CONF_MIN_VALUE in config: + # not modify case + lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.slider_set_mode(w.obj, literal(config[CONF_MODE])) + value = await get_start_value(config) + if value is not None: + lv.slider_set_value(w.obj, value, literal(config[CONF_ANIMATED])) + + +slider_spec = SliderType() diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py new file mode 100644 index 0000000000..b84dc7cd23 --- /dev/null +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -0,0 +1,178 @@ +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 ..automation import action_to_code, update_to_code +from ..defines import ( + CONF_CURSOR, + CONF_DECIMAL_PLACES, + CONF_DIGITS, + CONF_MAIN, + CONF_ROLLOVER, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_TEXTAREA_PLACEHOLDER, +) +from ..lv_validation import lv_bool, lv_float +from ..lvcode import lv +from ..types import LvNumber, ObjUpdateAction +from . import Widget, WidgetType, get_widgets +from .label import CONF_LABEL +from .textarea import CONF_TEXTAREA + +CONF_SPINBOX = "spinbox" + +lv_spinbox_t = LvNumber("lv_spinbox_t") + +SPIN_ACTIONS = ( + "INCREMENT", + "DECREMENT", + "STEP_NEXT", + "STEP_PREV", + "CLEAR", +) + + +def validate_spinbox(config): + max_val = 2**31 - 1 + min_val = -1 - max_val + range_from = int(config[CONF_RANGE_FROM]) + range_to = int(config[CONF_RANGE_TO]) + step = int(config[CONF_STEP]) + if ( + range_from > max_val + or range_from < min_val + or range_to > max_val + or range_to < min_val + ): + raise cv.Invalid("Range outside allowed limits") + if step <= 0 or step >= (range_to - range_from) / 2: + raise cv.Invalid("Invalid step value") + if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]: + raise cv.Invalid("Number of digits must exceed number of decimal places") + return config + + +SPINBOX_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_RANGE_FROM, default=0): cv.float_, + cv.Optional(CONF_RANGE_TO, default=100): cv.float_, + cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10), + cv.Optional(CONF_STEP, default=1.0): cv.positive_float, + cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6), + cv.Optional(CONF_ROLLOVER, default=False): lv_bool, + } +).add_extra(validate_spinbox) + + +SPINBOX_MODIFY_SCHEMA = { + cv.Required(CONF_VALUE): lv_float, +} + + +class SpinboxType(WidgetType): + def __init__(self): + super().__init__( + CONF_SPINBOX, + lv_spinbox_t, + ( + CONF_MAIN, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_CURSOR, + CONF_TEXTAREA_PLACEHOLDER, + ), + SPINBOX_SCHEMA, + SPINBOX_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if CONF_DIGITS in config: + digits = config[CONF_DIGITS] + scale = 10 ** config[CONF_DECIMAL_PLACES] + range_from = int(config[CONF_RANGE_FROM]) * scale + range_to = int(config[CONF_RANGE_TO]) * scale + step = int(config[CONF_STEP]) * scale + w.scale = scale + w.step = step + w.range_to = range_to + w.range_from = range_from + lv.spinbox_set_range(w.obj, range_from, range_to) + await w.set_property(CONF_STEP, step) + await w.set_property(CONF_ROLLOVER, config) + lv.spinbox_set_digit_format( + 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)) + + def get_scale(self, config): + return 10 ** config[CONF_DECIMAL_PLACES] + + def get_uses(self): + return CONF_TEXTAREA, CONF_LABEL + + def get_max(self, config: dict): + return config[CONF_RANGE_TO] + + def get_min(self, config: dict): + return config[CONF_RANGE_FROM] + + def get_step(self, config: dict): + return config[CONF_STEP] + + +spinbox_spec = SpinboxType() + + +@automation.register_action( + "lvgl.spinbox.increment", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + }, + key=CONF_ID, + ), +) +async def spinbox_increment(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_increment(w: Widget): + lv.spinbox_increment(w.obj) + + return await action_to_code(widgets, do_increment, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.spinbox.decrement", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + }, + key=CONF_ID, + ), +) +async def spinbox_decrement(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_increment(w: Widget): + lv.spinbox_decrement(w.obj) + + return await action_to_code(widgets, do_increment, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.spinbox.update", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + cv.Required(CONF_VALUE): lv_float, + } + ), +) +async def spinbox_update_to_code(config, action_id, template_arg, args): + return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/spinner.py b/esphome/components/lvgl/widgets/spinner.py new file mode 100644 index 0000000000..2940feb594 --- /dev/null +++ b/esphome/components/lvgl/widgets/spinner.py @@ -0,0 +1,43 @@ +import esphome.config_validation as cv +from esphome.cpp_generator import MockObjClass + +from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME +from ..lv_validation import angle +from ..lvcode import lv_expr +from ..types import LvType +from . import Widget, WidgetType +from .arc import CONF_ARC + +CONF_SPINNER = "spinner" + +SPINNER_SCHEMA = cv.Schema( + { + cv.Required(CONF_ARC_LENGTH): angle, + cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds, + } +) + + +class SpinnerType(WidgetType): + def __init__(self): + super().__init__( + CONF_SPINNER, + LvType("lv_spinner_t"), + (CONF_MAIN, CONF_INDICATOR), + SPINNER_SCHEMA, + {}, + ) + + async def to_code(self, w: Widget, config): + return [] + + def get_uses(self): + return (CONF_ARC,) + + def obj_creator(self, parent: MockObjClass, config: dict): + spin_time = config[CONF_SPIN_TIME].total_milliseconds + arc_length = config[CONF_ARC_LENGTH] // 10 + return lv_expr.call("spinner_create", parent, spin_time, arc_length) + + +spinner_spec = SpinnerType() diff --git a/esphome/components/lvgl/widgets/switch.py b/esphome/components/lvgl/widgets/switch.py new file mode 100644 index 0000000000..a7c1356bf2 --- /dev/null +++ b/esphome/components/lvgl/widgets/switch.py @@ -0,0 +1,20 @@ +from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN +from ..types import LvBoolean +from . import WidgetType + +CONF_SWITCH = "switch" + + +class SwitchType(WidgetType): + def __init__(self): + super().__init__( + CONF_SWITCH, + LvBoolean("lv_switch_t"), + (CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + ) + + async def to_code(self, w, config): + return [] + + +switch_spec = SwitchType() diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py new file mode 100644 index 0000000000..226fc3f286 --- /dev/null +++ b/esphome/components/lvgl/widgets/tabview.py @@ -0,0 +1,114 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE +from esphome.cpp_generator import MockObjClass + +from ..automation import action_to_code +from ..defines import ( + CONF_ANIMATED, + CONF_MAIN, + CONF_TAB_ID, + CONF_TABS, + DIRECTIONS, + TYPE_FLEX, + literal, +) +from ..lv_validation import animated, lv_int, size +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..schemas import container_schema, part_schema +from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr +from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from .buttonmatrix import buttonmatrix_spec +from .obj import obj_spec + +CONF_TABVIEW = "tabview" +CONF_TAB_STYLE = "tab_style" + +lv_tab_t = LvType("lv_obj_t") + +TABVIEW_SCHEMA = cv.Schema( + { + cv.Required(CONF_TABS): cv.ensure_list( + container_schema( + obj_spec, + { + cv.Required(CONF_NAME): cv.string, + cv.GenerateID(): cv.declare_id(lv_tab_t), + }, + ) + ), + cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of, + cv.Optional(CONF_SIZE, default="10%"): size, + } +) + + +class TabviewType(WidgetType): + def __init__(self): + super().__init__( + CONF_TABVIEW, + LvType( + "lv_tabview_t", + largs=[(lv_obj_t_ptr, "tab")], + lvalue=lambda w: lv_expr.obj_get_child( + lv_expr.tabview_get_content(w.obj), + lv_expr.tabview_get_tab_act(w.obj), + ), + has_on_value=True, + ), + parts=(CONF_MAIN,), + schema=TABVIEW_SCHEMA, + modify_schema={}, + ) + + def get_uses(self): + return "btnmatrix", TYPE_FLEX + + async def to_code(self, w: Widget, config: dict): + for tab_conf in config[CONF_TABS]: + w_id = tab_conf[CONF_ID] + tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t) + tab_widget = Widget.create(w_id, tab_obj, obj_spec) + lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME])) + await set_obj_properties(tab_widget, tab_conf) + await add_widgets(tab_widget, tab_conf) + if button_style := config.get(CONF_TAB_STYLE): + with LocalVariable( + "tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj) + ) as btnmatrix_obj: + await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style) + + def obj_creator(self, parent: MockObjClass, config: dict): + return lv_expr.call( + "tabview_create", + parent, + literal(config[CONF_POSITION]), + literal(config[CONF_SIZE]), + ) + + +tabview_spec = TabviewType() + + +@automation.register_action( + "lvgl.tabview.select", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(tabview_spec.w_type), + cv.Optional(CONF_ANIMATED, default=False): animated, + cv.Required(CONF_INDEX): lv_int, + }, + ).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)), +) +async def tabview_select(config, action_id, template_arg, args): + widget = await get_widgets(config) + index = config[CONF_INDEX] + + async def do_select(w: Widget): + lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED])) + lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + + return await action_to_code(widget, do_select, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/textarea.py b/esphome/components/lvgl/widgets/textarea.py new file mode 100644 index 0000000000..23d50b3894 --- /dev/null +++ b/esphome/components/lvgl/widgets/textarea.py @@ -0,0 +1,66 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_LENGTH, CONF_TEXT + +from ..defines import ( + CONF_ACCEPTED_CHARS, + CONF_CURSOR, + CONF_MAIN, + CONF_ONE_LINE, + CONF_PASSWORD_MODE, + CONF_PLACEHOLDER_TEXT, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_TEXTAREA_PLACEHOLDER, +) +from ..lv_validation import lv_bool, lv_int, lv_text +from ..schemas import TEXT_SCHEMA +from ..types import LvText +from . import Widget, WidgetType + +CONF_TEXTAREA = "textarea" + +lv_textarea_t = LvText("lv_textarea_t") + +TEXTAREA_SCHEMA = TEXT_SCHEMA.extend( + { + cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, + cv.Optional(CONF_ACCEPTED_CHARS): lv_text, + cv.Optional(CONF_ONE_LINE): lv_bool, + cv.Optional(CONF_PASSWORD_MODE): lv_bool, + cv.Optional(CONF_MAX_LENGTH): lv_int, + } +) + + +class TextareaType(WidgetType): + def __init__(self): + super().__init__( + CONF_TEXTAREA, + lv_textarea_t, + ( + CONF_MAIN, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_CURSOR, + CONF_TEXTAREA_PLACEHOLDER, + ), + TEXTAREA_SCHEMA, + ) + + async def to_code(self, w: Widget, config: dict): + for prop in (CONF_TEXT, CONF_PLACEHOLDER_TEXT, CONF_ACCEPTED_CHARS): + if (value := config.get(prop)) is not None: + await w.set_property(prop, await lv_text.process(value)) + await w.set_property( + CONF_MAX_LENGTH, await lv_int.process(config.get(CONF_MAX_LENGTH)) + ) + await w.set_property( + CONF_PASSWORD_MODE, + await lv_bool.process(config.get(CONF_PASSWORD_MODE)), + ) + await w.set_property( + CONF_ONE_LINE, await lv_bool.process(config.get(CONF_ONE_LINE)) + ) + + +textarea_spec = TextareaType() diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py new file mode 100644 index 0000000000..3865d404e2 --- /dev/null +++ b/esphome/components/lvgl/widgets/tileview.py @@ -0,0 +1,123 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_ROW + +from ..automation import action_to_code +from ..defines import ( + CONF_ANIMATED, + CONF_COLUMN, + CONF_DIR, + CONF_MAIN, + CONF_SCROLLBAR, + CONF_TILE_ID, + CONF_TILES, + TILE_DIRECTIONS, + literal, +) +from ..lv_validation import animated, lv_int +from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable +from ..schemas import container_schema +from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr +from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from .obj import obj_spec + +CONF_TILEVIEW = "tileview" + +lv_tile_t = LvType("lv_tileview_tile_t") + +lv_tileview_t = LvType( + "lv_tileview_t", + largs=[(lv_obj_t_ptr, "tile")], + lvalue=lambda w: w.get_property("tile_act"), + has_on_value=True, +) + +tile_spec = WidgetType("lv_tileview_tile_t", lv_tile_t, (CONF_MAIN,), {}) + +TILEVIEW_SCHEMA = cv.Schema( + { + cv.Required(CONF_TILES): cv.ensure_list( + container_schema( + obj_spec, + { + cv.Required(CONF_ROW): lv_int, + cv.Required(CONF_COLUMN): lv_int, + cv.GenerateID(): cv.declare_id(lv_tile_t), + cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of, + }, + ) + ), + } +) + + +class TileviewType(WidgetType): + def __init__(self): + super().__init__( + CONF_TILEVIEW, + lv_tileview_t, + (CONF_MAIN, CONF_SCROLLBAR), + schema=TILEVIEW_SCHEMA, + modify_schema={}, + ) + + async def to_code(self, w: Widget, config: dict): + for tile_conf in config.get(CONF_TILES, ()): + w_id = tile_conf[CONF_ID] + tile_obj = lv_Pvariable(lv_obj_t, w_id) + tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) + dirs = tile_conf[CONF_DIR] + if isinstance(dirs, list): + dirs = "|".join(dirs) + lv_assign( + tile_obj, + lv_expr.tileview_add_tile( + w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs) + ), + ) + await set_obj_properties(tile, tile_conf) + await add_widgets(tile, tile_conf) + + +tileview_spec = TileviewType() + + +def tile_select_validate(config): + row = CONF_ROW in config + column = CONF_COLUMN in config + tile = CONF_TILE_ID in config + if tile and (row or column) or not tile and not (row and column): + raise cv.Invalid("Specify either a tile id, or both a row and a column") + return config + + +@automation.register_action( + "lvgl.tileview.select", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_tileview_t), + cv.Optional(CONF_ANIMATED, default=False): animated, + cv.Optional(CONF_ROW): lv_int, + cv.Optional(CONF_COLUMN): lv_int, + cv.Optional(CONF_TILE_ID): cv.use_id(lv_tile_t), + }, + ).add_extra(tile_select_validate), +) +async def tileview_select(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_select(w: Widget): + if tile := config.get(CONF_TILE_ID): + tile = await cg.get_variable(tile) + lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) + else: + row = await lv_int.process(config[CONF_ROW]) + column = await lv_int.process(config[CONF_COLUMN]) + lv_obj.set_tile_id( + widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) + ) + lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + + return await action_to_code(widgets, do_select, action_id, template_arg, args) diff --git a/esphome/components/m5stack_8angle/__init__.py b/esphome/components/m5stack_8angle/__init__.py new file mode 100644 index 0000000000..1aaa86a6fd --- /dev/null +++ b/esphome/components/m5stack_8angle/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@rnauber"] +MULTI_CONF = True + +CONF_M5STACK_8ANGLE_ID = "m5stack_8angle_id" + +m5stack_8angle_ns = cg.esphome_ns.namespace("m5stack_8angle") +M5Stack8AngleComponent = m5stack_8angle_ns.class_( + "M5Stack8AngleComponent", + i2c.I2CDevice, + cg.Component, +) + +AnalogBits = m5stack_8angle_ns.enum("AnalogBits") + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(M5Stack8AngleComponent), + } +).extend(i2c.i2c_device_schema(0x43)) + + +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) diff --git a/esphome/components/m5stack_8angle/binary_sensor/__init__.py b/esphome/components/m5stack_8angle/binary_sensor/__init__.py new file mode 100644 index 0000000000..a8b2690083 --- /dev/null +++ b/esphome/components/m5stack_8angle/binary_sensor/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor + +from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID + + +M5Stack8AngleSwitchBinarySensor = m5stack_8angle_ns.class_( + "M5Stack8AngleSwitchBinarySensor", + binary_sensor.BinarySensor, + cg.PollingComponent, +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent), + } + ) + .extend(binary_sensor.binary_sensor_schema(M5Stack8AngleSwitchBinarySensor)) + .extend(cv.polling_component_schema("10s")) +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID]) + sens = await binary_sensor.new_binary_sensor(config) + cg.add(sens.set_parent(hub)) + await cg.register_component(sens, config) diff --git a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp new file mode 100644 index 0000000000..2f68d9f254 --- /dev/null +++ b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp @@ -0,0 +1,17 @@ +#include "m5stack_8angle_binary_sensor.h" + +namespace esphome { +namespace m5stack_8angle { + +void M5Stack8AngleSwitchBinarySensor::update() { + int8_t out = this->parent_->read_switch(); + if (out == -1) { + this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle."); + return; + } + this->publish_state(out != 0); + this->status_clear_warning(); +} + +} // namespace m5stack_8angle +} // namespace esphome diff --git a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.h b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.h new file mode 100644 index 0000000000..b8bb601525 --- /dev/null +++ b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/core/component.h" + +#include "../m5stack_8angle.h" + +namespace esphome { +namespace m5stack_8angle { + +class M5Stack8AngleSwitchBinarySensor : public binary_sensor::BinarySensor, + public PollingComponent, + public Parented { + public: + void update() override; +}; + +} // namespace m5stack_8angle +} // namespace esphome diff --git a/esphome/components/m5stack_8angle/light/__init__.py b/esphome/components/m5stack_8angle/light/__init__.py new file mode 100644 index 0000000000..07384ecd61 --- /dev/null +++ b/esphome/components/m5stack_8angle/light/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light + +from esphome.const import CONF_OUTPUT_ID + +from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID + + +M5Stack8AngleLightsComponent = m5stack_8angle_ns.class_( + "M5Stack8AngleLightOutput", + light.AddressableLight, +) + + +CONFIG_SCHEMA = cv.All( + light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent), + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(M5Stack8AngleLightsComponent), + } + ) +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID]) + lights = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(lights, config) + await cg.register_component(lights, config) + cg.add(lights.set_parent(hub)) diff --git a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp new file mode 100644 index 0000000000..95fd8cb98f --- /dev/null +++ b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp @@ -0,0 +1,45 @@ +#include "m5stack_8angle_light.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace m5stack_8angle { + +static const char *const TAG = "m5stack_8angle.light"; + +void M5Stack8AngleLightOutput::setup() { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); + if (this->buf_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); + this->mark_failed(); + return; + }; + memset(this->buf_, 0xFF, M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); + + this->effect_data_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS); + if (this->effect_data_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate effect data of size %u", M5STACK_8ANGLE_NUM_LEDS); + this->mark_failed(); + return; + }; + memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS); +} + +void M5Stack8AngleLightOutput::write_state(light::LightState *state) { + for (int i = 0; i < M5STACK_8ANGLE_NUM_LEDS; + i++) { // write one LED at a time, otherwise the message will be truncated + this->parent_->write_register(M5STACK_8ANGLE_REGISTER_RGB_24B + i * M5STACK_8ANGLE_BYTES_PER_LED, + this->buf_ + i * M5STACK_8ANGLE_BYTES_PER_LED, M5STACK_8ANGLE_BYTES_PER_LED); + } +} + +light::ESPColorView M5Stack8AngleLightOutput::get_view_internal(int32_t index) const { + size_t pos = index * M5STACK_8ANGLE_BYTES_PER_LED; + // red, green, blue, white, effect_data, color_correction + return {this->buf_ + pos, this->buf_ + pos + 1, this->buf_ + pos + 2, + nullptr, this->effect_data_ + index, &this->correction_}; +} + +} // namespace m5stack_8angle +} // namespace esphome diff --git a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.h b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.h new file mode 100644 index 0000000000..204f2c04c7 --- /dev/null +++ b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/light_output.h" + +#include "../m5stack_8angle.h" + +namespace esphome { +namespace m5stack_8angle { + +static const uint8_t M5STACK_8ANGLE_NUM_LEDS = 9; +static const uint8_t M5STACK_8ANGLE_BYTES_PER_LED = 4; + +class M5Stack8AngleLightOutput : public light::AddressableLight, public Parented { + public: + void setup() override; + + void write_state(light::LightState *state) override; + + int32_t size() const override { return M5STACK_8ANGLE_NUM_LEDS; } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + }; + + void clear_effect_data() override { memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS); }; + + protected: + light::ESPColorView get_view_internal(int32_t index) const override; + + uint8_t *buf_{nullptr}; + uint8_t *effect_data_{nullptr}; +}; + +} // namespace m5stack_8angle +} // namespace esphome diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.cpp b/esphome/components/m5stack_8angle/m5stack_8angle.cpp new file mode 100644 index 0000000000..6a584eddbc --- /dev/null +++ b/esphome/components/m5stack_8angle/m5stack_8angle.cpp @@ -0,0 +1,74 @@ +#include "m5stack_8angle.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace m5stack_8angle { + +static const char *const TAG = "m5stack_8angle"; + +void M5Stack8AngleComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up M5STACK_8ANGLE..."); + i2c::ErrorCode err; + + err = this->read(nullptr, 0); + if (err != i2c::NO_ERROR) { + ESP_LOGE(TAG, "I2C error %02X...", err); + this->mark_failed(); + return; + }; + + err = this->read_register(M5STACK_8ANGLE_REGISTER_FW_VERSION, &this->fw_version_, 1); + if (err != i2c::NO_ERROR) { + ESP_LOGE(TAG, "I2C error %02X...", err); + this->mark_failed(); + return; + }; +} + +void M5Stack8AngleComponent::dump_config() { + ESP_LOGCONFIG(TAG, "M5STACK_8ANGLE:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Firmware version: %d ", this->fw_version_); +} + +float M5Stack8AngleComponent::read_knob_pos(uint8_t channel, AnalogBits bits) { + int32_t raw_pos = this->read_knob_pos_raw(channel, bits); + if (raw_pos == -1) { + return NAN; + } + return (float) raw_pos / ((1 << bits) - 1); +} + +int32_t M5Stack8AngleComponent::read_knob_pos_raw(uint8_t channel, AnalogBits bits) { + uint16_t knob_pos = 0; + i2c::ErrorCode err; + if (bits == BITS_8) { + err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B + channel, (uint8_t *) &knob_pos, 1); + } else if (bits == BITS_12) { + err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B + (channel * 2), (uint8_t *) &knob_pos, 2); + } else { + ESP_LOGE(TAG, "Invalid number of bits: %d", bits); + return -1; + } + if (err == i2c::NO_ERROR) { + return knob_pos; + } else { + return -1; + } +} + +int8_t M5Stack8AngleComponent::read_switch() { + uint8_t out; + i2c::ErrorCode err = this->read_register(M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT, (uint8_t *) &out, 1); + if (err == i2c::NO_ERROR) { + return out ? 1 : 0; + } else { + return -1; + } +} + +float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace m5stack_8angle +} // namespace esphome diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.h b/esphome/components/m5stack_8angle/m5stack_8angle.h new file mode 100644 index 0000000000..831b1422fd --- /dev/null +++ b/esphome/components/m5stack_8angle/m5stack_8angle.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace m5stack_8angle { + +static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B = 0x00; +static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B = 0x10; +static const uint8_t M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT = 0x20; +static const uint8_t M5STACK_8ANGLE_REGISTER_RGB_24B = 0x30; +static const uint8_t M5STACK_8ANGLE_REGISTER_FW_VERSION = 0xFE; + +enum AnalogBits : uint8_t { + BITS_8 = 8, + BITS_12 = 12, +}; + +class M5Stack8AngleComponent : public i2c::I2CDevice, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); + int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); + int8_t read_switch(); + + protected: + uint8_t fw_version_; +}; + +} // namespace m5stack_8angle +} // namespace esphome diff --git a/esphome/components/m5stack_8angle/sensor/__init__.py b/esphome/components/m5stack_8angle/sensor/__init__.py new file mode 100644 index 0000000000..70744a59e6 --- /dev/null +++ b/esphome/components/m5stack_8angle/sensor/__init__.py @@ -0,0 +1,66 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor + +from esphome.const import ( + CONF_BIT_DEPTH, + CONF_CHANNEL, + CONF_RAW, + ICON_ROTATE_RIGHT, + STATE_CLASS_MEASUREMENT, +) + +from .. import ( + AnalogBits, + M5Stack8AngleComponent, + m5stack_8angle_ns, + CONF_M5STACK_8ANGLE_ID, +) + + +M5Stack8AngleKnobSensor = m5stack_8angle_ns.class_( + "M5Stack8AngleKnobSensor", + sensor.Sensor, + cg.PollingComponent, +) + + +BIT_DEPTHS = { + 8: AnalogBits.BITS_8, + 12: AnalogBits.BITS_12, +} + +_validate_bits = cv.float_with_unit("bits", "bit") + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(M5Stack8AngleKnobSensor), + cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent), + cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=8), + cv.Optional(CONF_BIT_DEPTH, default="8bit"): cv.All( + _validate_bits, cv.enum(BIT_DEPTHS) + ), + cv.Optional(CONF_RAW, default=False): cv.boolean, + } + ) + .extend( + sensor.sensor_schema( + M5Stack8AngleKnobSensor, + accuracy_decimals=2, + icon=ICON_ROTATE_RIGHT, + state_class=STATE_CLASS_MEASUREMENT, + ) + ) + .extend(cv.polling_component_schema("10s")) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_M5STACK_8ANGLE_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL] - 1)) + cg.add(var.set_bit_depth(BIT_DEPTHS[config[CONF_BIT_DEPTH]])) + cg.add(var.set_raw(config[CONF_RAW])) diff --git a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp new file mode 100644 index 0000000000..5e034f1dd3 --- /dev/null +++ b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp @@ -0,0 +1,24 @@ +#include "m5stack_8angle_sensor.h" + +namespace esphome { +namespace m5stack_8angle { + +void M5Stack8AngleKnobSensor::update() { + if (this->parent_ != nullptr) { + int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_); + if (raw_pos == -1) { + this->status_set_warning("Could not read knob position from M5Stack 8Angle."); + return; + } + if (this->raw_) { + this->publish_state(raw_pos); + } else { + float knob_pos = (float) raw_pos / ((1 << this->bits_) - 1); + this->publish_state(knob_pos); + } + this->status_clear_warning(); + }; +} + +} // namespace m5stack_8angle +} // namespace esphome diff --git a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.h b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.h new file mode 100644 index 0000000000..4848f8f80f --- /dev/null +++ b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +#include "../m5stack_8angle.h" + +namespace esphome { +namespace m5stack_8angle { + +class M5Stack8AngleKnobSensor : public sensor::Sensor, + public PollingComponent, + public Parented { + public: + void update() override; + void set_channel(uint8_t channel) { this->channel_ = channel; }; + void set_bit_depth(AnalogBits bits) { this->bits_ = bits; }; + void set_raw(bool raw) { this->raw_ = raw; }; + + protected: + uint8_t channel_; + AnalogBits bits_; + bool raw_; +}; + +} // namespace m5stack_8angle +} // namespace esphome diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 1c549007b9..5250a45732 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -21,6 +21,7 @@ CONF_COLUMNS = "columns" CONF_KEYS = "keys" CONF_DEBOUNCE_TIME = "debounce_time" CONF_HAS_DIODES = "has_diodes" +CONF_HAS_PULLDOWNS = "has_pulldowns" def check_keys(obj): @@ -45,6 +46,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_KEYS): cv.string, cv.Optional(CONF_DEBOUNCE_TIME, default=1): cv.int_range(min=1, max=100), cv.Optional(CONF_HAS_DIODES): cv.boolean, + cv.Optional(CONF_HAS_PULLDOWNS): cv.boolean, } ), check_keys, @@ -69,3 +71,5 @@ async def to_code(config): cg.add(var.set_debounce_time(config[CONF_DEBOUNCE_TIME])) if CONF_HAS_DIODES in config: cg.add(var.set_has_diodes(config[CONF_HAS_DIODES])) + if CONF_HAS_PULLDOWNS in config: + cg.add(var.set_has_pulldowns(config[CONF_HAS_PULLDOWNS])) diff --git a/esphome/components/matrix_keypad/binary_sensor/__init__.py b/esphome/components/matrix_keypad/binary_sensor/__init__.py index 9ad909f60a..edebf7b772 100644 --- a/esphome/components/matrix_keypad/binary_sensor/__init__.py +++ b/esphome/components/matrix_keypad/binary_sensor/__init__.py @@ -1,12 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ID, CONF_KEY +from esphome.const import CONF_ID, CONF_KEY, CONF_ROW, CONF_COL from .. import MatrixKeypad, matrix_keypad_ns, CONF_KEYPAD_ID -CONF_ROW = "row" -CONF_COL = "col" - DEPENDENCIES = ["matrix_keypad"] MatrixKeypadBinarySensor = matrix_keypad_ns.class_( diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index f4e7bf4d23..f62c75c869 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -8,14 +8,21 @@ static const char *const TAG = "matrix_keypad"; void MatrixKeypad::setup() { for (auto *pin : this->rows_) { + pin->setup(); if (!has_diodes_) { pin->pin_mode(gpio::FLAG_INPUT); } else { - pin->digital_write(true); + pin->digital_write(!has_pulldowns_); + } + } + for (auto *pin : this->columns_) { + pin->setup(); + if (has_pulldowns_) { + pin->pin_mode(gpio::FLAG_INPUT); + } else { + pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); } } - for (auto *pin : this->columns_) - pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); } void MatrixKeypad::loop() { @@ -28,9 +35,9 @@ void MatrixKeypad::loop() { for (auto *row : this->rows_) { if (!has_diodes_) row->pin_mode(gpio::FLAG_OUTPUT); - row->digital_write(false); + row->digital_write(has_pulldowns_); for (auto *col : this->columns_) { - if (!col->digital_read()) { + if (col->digital_read() == has_pulldowns_) { if (key != -1) { error = true; } else { @@ -39,7 +46,7 @@ void MatrixKeypad::loop() { } pos++; } - row->digital_write(true); + row->digital_write(!has_pulldowns_); if (!has_diodes_) row->pin_mode(gpio::FLAG_INPUT); } @@ -89,11 +96,13 @@ void MatrixKeypad::loop() { void MatrixKeypad::dump_config() { ESP_LOGCONFIG(TAG, "Matrix Keypad:"); ESP_LOGCONFIG(TAG, " Rows:"); - for (auto &pin : this->rows_) + for (auto &pin : this->rows_) { LOG_PIN(" Pin: ", pin); + } ESP_LOGCONFIG(TAG, " Cols:"); - for (auto &pin : this->columns_) + for (auto &pin : this->columns_) { LOG_PIN(" Pin: ", pin); + } } void MatrixKeypad::register_listener(MatrixKeypadListener *listener) { this->listeners_.push_back(listener); } diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 9f5942be9a..d506040b7c 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -28,6 +28,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { void set_keys(std::string keys) { keys_ = std::move(keys); }; void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; + void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; }; void register_listener(MatrixKeypadListener *listener); @@ -37,6 +38,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { std::string keys_; int debounce_time_ = 0; bool has_diodes_{false}; + bool has_pulldowns_{false}; int pressed_key_ = -1; std::vector listeners_{}; diff --git a/esphome/components/max17043/__init__.py b/esphome/components/max17043/__init__.py new file mode 100644 index 0000000000..1db25ccdd6 --- /dev/null +++ b/esphome/components/max17043/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@blacknell"] diff --git a/esphome/components/max17043/automation.h b/esphome/components/max17043/automation.h new file mode 100644 index 0000000000..44729d119b --- /dev/null +++ b/esphome/components/max17043/automation.h @@ -0,0 +1,20 @@ + +#pragma once +#include "esphome/core/automation.h" +#include "max17043.h" + +namespace esphome { +namespace max17043 { + +template class SleepAction : public Action { + public: + explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {} + + void play(Ts... x) override { this->max17043_->sleep_mode(); } + + protected: + MAX17043Component *max17043_; +}; + +} // namespace max17043 +} // namespace esphome diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp new file mode 100644 index 0000000000..4a83320455 --- /dev/null +++ b/esphome/components/max17043/max17043.cpp @@ -0,0 +1,98 @@ +#include "max17043.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace max17043 { + +// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert +// Consult the datasheet at https://www.analog.com/en/products/max17043.html + +static const char *const TAG = "max17043"; + +static const uint8_t MAX17043_VCELL = 0x02; +static const uint8_t MAX17043_SOC = 0x04; +static const uint8_t MAX17043_CONFIG = 0x0c; + +static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C; +static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4) +static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080; + +void MAX17043Component::update() { + uint16_t raw_voltage, raw_percent; + + if (this->voltage_sensor_ != nullptr) { + if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) { + this->status_set_warning("Unable to read MAX17043_VCELL"); + } else { + float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0; + this->voltage_sensor_->publish_state(voltage); + this->status_clear_warning(); + } + } + if (this->battery_remaining_sensor_ != nullptr) { + if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) { + this->status_set_warning("Unable to read MAX17043_SOC"); + } else { + float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff)); + this->battery_remaining_sensor_->publish_state(percent); + this->status_clear_warning(); + } + } +} + +void MAX17043Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX17043..."); + + uint16_t config_reg; + if (this->write(&MAX17043_CONFIG, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + if (this->read(reinterpret_cast(&config_reg), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + config_reg = i2c::i2ctohs(config_reg) & MAX17043_CONFIG_SAFE_MASK; + ESP_LOGV(TAG, "MAX17043 CONFIG register reads 0x%X", config_reg); + + 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->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->mark_failed(); + return; + } +} + +void MAX17043Component::dump_config() { + ESP_LOGCONFIG(TAG, "MAX17043:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MAX17043 failed"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Battery Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_); +} + +float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; } + +void MAX17043Component::sleep_mode() { + if (!this->is_failed()) { + if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) { + ESP_LOGW(TAG, "Unable to write the sleep bit to config register"); + this->status_set_warning(); + } + } +} + +} // namespace max17043 +} // namespace esphome diff --git a/esphome/components/max17043/max17043.h b/esphome/components/max17043/max17043.h new file mode 100644 index 0000000000..540b977789 --- /dev/null +++ b/esphome/components/max17043/max17043.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace max17043 { + +class MAX17043Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void sleep_mode(); + + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) { + battery_remaining_sensor_ = battery_remaining_sensor; + } + + protected: + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *battery_remaining_sensor_{nullptr}; +}; + +} // namespace max17043 +} // namespace esphome diff --git a/esphome/components/max17043/sensor.py b/esphome/components/max17043/sensor.py new file mode 100644 index 0000000000..3da0f953b0 --- /dev/null +++ b/esphome/components/max17043/sensor.py @@ -0,0 +1,77 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BATTERY_VOLTAGE, + CONF_ID, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + UNIT_VOLT, +) + +DEPENDENCIES = ["i2c"] + +max17043_ns = cg.esphome_ns.namespace("max17043") +MAX17043Component = max17043_ns.class_( + "MAX17043Component", cg.PollingComponent, i2c.I2CDevice +) + +# Actions +SleepAction = max17043_ns.class_("SleepAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MAX17043Component), + 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, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x36)) +) + + +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) + + if voltage_config := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) + cg.add(var.set_voltage_sensor(sens)) + + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_remaining_sensor(sens)) + + +MAX17043_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(MAX17043Component), + } +) + + +@automation.register_action("max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA) +async def max17043_sleep_mode_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) diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 2578c4742d..445e086ef6 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -47,7 +47,7 @@ void MAX31855Sensor::read_data_() { if (mem != 0xFFFFFFFF) { this->status_clear_error(); } else { - ESP_LOGE(TAG, "No data received from MAX31855 (0x%08X). Check wiring!", mem); + ESP_LOGE(TAG, "No data received from MAX31855 (0x%08" PRIX32 "). Check wiring!", mem); this->publish_state(NAN); if (this->temperature_reference_) { this->temperature_reference_->publish_state(NAN); @@ -69,25 +69,25 @@ void MAX31855Sensor::read_data_() { // Check thermocouple faults if (mem & 0x00000001) { - ESP_LOGW(TAG, "Thermocouple open circuit (not connected) fault from MAX31855 (0x%08X)", mem); + ESP_LOGW(TAG, "Thermocouple open circuit (not connected) fault from MAX31855 (0x%08" PRIX32 ")", mem); this->publish_state(NAN); this->status_set_warning(); return; } if (mem & 0x00000002) { - ESP_LOGW(TAG, "Thermocouple short circuit to ground fault from MAX31855 (0x%08X)", mem); + ESP_LOGW(TAG, "Thermocouple short circuit to ground fault from MAX31855 (0x%08" PRIX32 ")", mem); this->publish_state(NAN); this->status_set_warning(); return; } if (mem & 0x00000004) { - ESP_LOGW(TAG, "Thermocouple short circuit to VCC fault from MAX31855 (0x%08X)", mem); + ESP_LOGW(TAG, "Thermocouple short circuit to VCC fault from MAX31855 (0x%08" PRIX32 ")", mem); this->publish_state(NAN); this->status_set_warning(); return; } if (mem & 0x00010000) { - ESP_LOGW(TAG, "Got faulty reading from MAX31855 (0x%08X)", mem); + ESP_LOGW(TAG, "Got faulty reading from MAX31855 (0x%08" PRIX32 ")", mem); this->publish_state(NAN); this->status_set_warning(); return; diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index c0ed8a467d..822e256587 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" +#include + namespace esphome { namespace max31855 { diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index 9300916fdc..6a4d34b430 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -32,6 +32,12 @@ void MAX31856Sensor::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); ESP_LOGCONFIG(TAG, " Mains Filter: %s", (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); + if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) { + ESP_LOGCONFIG(TAG, " Thermocouple Type: Unknown"); + } else { + ESP_LOGCONFIG(TAG, " Thermocouple Type: %c", "BEJKNRST"[this->thermocouple_type_]); + } + LOG_UPDATE_INTERVAL(this); } @@ -129,7 +135,12 @@ void MAX31856Sensor::clear_fault_() { } void MAX31856Sensor::set_thermocouple_type_() { - MAX31856ThermocoupleType type = MAX31856_TCTYPE_K; + MAX31856ThermocoupleType type; + if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) { + type = MAX31856_TCTYPE_K; + } else { + type = this->thermocouple_type_; + } ESP_LOGCONFIG(TAG, "set_thermocouple_type_: 0x%02X", type); uint8_t t = this->read_register_(MAX31856_CR1_REG); t &= 0xF0; // mask off bottom 4 bits @@ -188,7 +199,7 @@ uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { ESP_LOGVV(TAG, "read_byte lsb=0x%02X", lsb); this->disable(); const uint32_t value((msb << 16) | (mid << 8) | lsb); - ESP_LOGV(TAG, "read_register_24_ reg=0x%02X: value=0x%06X", reg, value); + ESP_LOGV(TAG, "read_register_24_ reg=0x%02X: value=0x%06" PRIX32, reg, value); return value; } diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h index 157aad433c..8d64cfe8bc 100644 --- a/esphome/components/max31856/max31856.h +++ b/esphome/components/max31856/max31856.h @@ -4,6 +4,8 @@ #include "esphome/components/spi/spi.h" #include "esphome/core/component.h" +#include + namespace esphome { namespace max31856 { @@ -48,7 +50,6 @@ enum MAX31856Registers { /** * Multiple types of thermocouples supported by the chip. - * Currently only K type implemented here. */ enum MAX31856ThermocoupleType { MAX31856_TCTYPE_B = 0b0000, // 0x00 @@ -76,11 +77,15 @@ class MAX31856Sensor : public sensor::Sensor, void setup() override; void dump_config() override; float get_setup_priority() const override; - void set_filter(MAX31856ConfigFilter filter) { filter_ = filter; } + void set_filter(MAX31856ConfigFilter filter) { this->filter_ = filter; } + void set_thermocouple_type(MAX31856ThermocoupleType thermocouple_type) { + this->thermocouple_type_ = thermocouple_type; + } void update() override; protected: MAX31856ConfigFilter filter_; + MAX31856ThermocoupleType thermocouple_type_; uint8_t read_register_(uint8_t reg); uint32_t read_register24_(uint8_t reg); diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 71f1f3bfa5..679e02b11d 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, spi +import esphome.config_validation as cv from esphome.const import ( CONF_MAINS_FILTER, + CONF_THERMOCOUPLE_TYPE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -15,8 +16,19 @@ MAX31856Sensor = max31856_ns.class_( MAX31865ConfigFilter = max31856_ns.enum("MAX31856ConfigFilter") FILTER = { - "50HZ": MAX31865ConfigFilter.FILTER_50HZ, - "60HZ": MAX31865ConfigFilter.FILTER_60HZ, + 50: MAX31865ConfigFilter.FILTER_50HZ, + 60: MAX31865ConfigFilter.FILTER_60HZ, +} +MAX31856ThermocoupleType = max31856_ns.enum("MAX31856ThermocoupleType") +THERMOCOUPLE_TYPE = { + "B": MAX31856ThermocoupleType.MAX31856_TCTYPE_B, + "E": MAX31856ThermocoupleType.MAX31856_TCTYPE_E, + "J": MAX31856ThermocoupleType.MAX31856_TCTYPE_J, + "K": MAX31856ThermocoupleType.MAX31856_TCTYPE_K, + "N": MAX31856ThermocoupleType.MAX31856_TCTYPE_N, + "R": MAX31856ThermocoupleType.MAX31856_TCTYPE_R, + "S": MAX31856ThermocoupleType.MAX31856_TCTYPE_S, + "T": MAX31856ThermocoupleType.MAX31856_TCTYPE_T, } CONFIG_SCHEMA = ( @@ -29,8 +41,15 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Optional(CONF_MAINS_FILTER, default="60HZ"): cv.enum( - FILTER, upper=True, space="" + cv.Optional(CONF_MAINS_FILTER, default="60Hz"): cv.All( + cv.frequency, cv.enum(FILTER, int=True) + ), + } + ) + .extend( + { + cv.Optional(CONF_THERMOCOUPLE_TYPE, default="K"): cv.enum( + THERMOCOUPLE_TYPE, upper=True, space="" ), } ) @@ -44,3 +63,4 @@ async def to_code(config): await cg.register_component(var, config) await spi.register_spi_device(var, config) cg.add(var.set_filter(config[CONF_MAINS_FILTER])) + cg.add(var.set_thermocouple_type(config[CONF_THERMOCOUPLE_TYPE])) diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 152d7b340b..b48aa2fdd3 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include +#include namespace esphome { namespace max31865 { @@ -45,14 +46,15 @@ void MAX31865Sensor::update() { config = this->read_register_(CONFIGURATION_REG); fault_detect_time = micros() - start_time; if ((fault_detect_time >= 6000) && (config & 0b00001100)) { - ESP_LOGE(TAG, "Fault detection incomplete (0x%02X) after %uμs (datasheet spec is 600μs max)! Aborting read.", + ESP_LOGE(TAG, + "Fault detection incomplete (0x%02X) after %" PRIu32 "μs (datasheet spec is 600μs max)! Aborting read.", config, fault_detect_time); this->publish_state(NAN); this->status_set_error(); return; } } while (config & 0b00001100); - ESP_LOGV(TAG, "Fault detection completed in %uμs.", fault_detect_time); + ESP_LOGV(TAG, "Fault detection completed in %" PRIu32 "μs.", fault_detect_time); // Start 1-shot conversion this->write_config_(0b11100000, 0b10100000); diff --git a/esphome/components/max6675/max6675.h b/esphome/components/max6675/max6675.h index 09bd9df3b8..ab0f06b041 100644 --- a/esphome/components/max6675/max6675.h +++ b/esphome/components/max6675/max6675.h @@ -10,7 +10,7 @@ namespace max6675 { class MAX6675Sensor : public sensor::Sensor, public PollingComponent, public spi::SPIDevice { + spi::DATA_RATE_1MHZ> { public: void setup() override; void dump_config() override; diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index 77e0d37e76..bb71dba8bf 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -74,20 +74,14 @@ def validate_mode(value): CONF_MAX6956 = "max6956" -MAX6956_PIN_SCHEMA = cv.All( +MAX6956_PIN_SCHEMA = pins.gpio_base_schema( + MAX6956GPIOPin, + cv.int_range(min=4, max=31), + modes=[CONF_INPUT, CONF_PULLUP, CONF_OUTPUT], + mode_validator=validate_mode, +).extend( { - cv.GenerateID(): cv.declare_id(MAX6956GPIOPin), cv.Required(CONF_MAX6956): cv.use_id(MAX6956), - cv.Required(CONF_NUMBER): cv.int_range(min=4, max=31), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h index 141164ab30..759fa45b07 100644 --- a/esphome/components/max6956/max6956.h +++ b/esphome/components/max6956/max6956.h @@ -29,7 +29,7 @@ enum MAX6956GPIORegisters { MAX6956_PORT_CONFIG_START = 0x09, // Port Configuration P7, P6, P5, P4 MAX6956_CURRENT_START = 0x12, // Current054 MAX6956_1PORT_VALUE_START = 0x20, // Port 0 only (virtual port, no action) - MAX6956_8PORTS_VALUE_START = 0x44, // 8 ports 4–11 (data bits D0–D7) + MAX6956_8PORTS_VALUE_START = 0x44, // 8 ports 4-11 (data bits D0-D7) }; enum MAX6956GPIOFlag { FLAG_LED = 0x20 }; diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index 391d033f24..13807b0dbd 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -29,7 +29,6 @@ CONFIG_SCHEMA = ( 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) await display.register_display(var, config) diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index b50a78eb96..d3cf6f5c48 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -164,6 +164,10 @@ void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { this->disable(); } void MAX7219Component::update() { + if (this->intensity_changed_) { + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); + this->intensity_changed_ = false; + } for (uint8_t i = 0; i < this->num_chips_ * 8; i++) this->buffer_[i] = 0; if (this->writer_.has_value()) @@ -218,21 +222,22 @@ uint8_t MAX7219Component::printf(const char *format, ...) { } void MAX7219Component::set_writer(max7219_writer_t &&writer) { this->writer_ = writer; } void MAX7219Component::set_intensity(uint8_t intensity) { - this->intensity_ = intensity; - this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); + intensity &= 0xF; + if (intensity != this->intensity_) { + this->intensity_changed_ = true; + this->intensity_ = intensity; + } } void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } -#ifdef USE_TIME -uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t MAX7219Component::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } } // namespace max7219 } // namespace esphome diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 47b54a4c50..270edf3282 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -1,11 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "esphome/core/time.h" #include "esphome/components/spi/spi.h" @@ -46,19 +42,18 @@ class MAX7219Component : public PollingComponent, /// Print `str` at position 0. uint8_t print(const char *str); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); protected: void send_byte_(uint8_t a_register, uint8_t data); void send_to_all_(uint8_t a_register, uint8_t data); - uint8_t intensity_{15}; /// Intensity of the display from 0 to 15 (most) + uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most) + bool intensity_changed_{}; // True if we need to re-send the intensity uint8_t num_chips_{1}; uint8_t *buffer_; bool reverse_{false}; diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index 8db9123a39..779e385ab1 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -39,7 +39,7 @@ CHIP_MODES = { max7219_ns = cg.esphome_ns.namespace("max7219digit") MAX7219Component = max7219_ns.class_( - "MAX7219Component", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer + "MAX7219Component", spi.SPIDevice, display.DisplayBuffer, cg.PollingComponent ) MAX7219ComponentRef = MAX7219Component.operator("ref") @@ -78,7 +78,6 @@ CONFIG_SCHEMA = ( 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) await display.register_display(var, config) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index c65b8e4823..ec9970d1a0 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -278,7 +278,9 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } } else { - b = pixels[7 - col]; + for (uint8_t i = 0; i < 8; i++) { + b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + } } // send this byte to display at selected chip if (this->invert_) { @@ -325,18 +327,16 @@ uint8_t MAX7219Component::printdigitf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->printdigit(pos, buffer); return 0; } -uint8_t MAX7219Component::strftimedigit(const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftimedigit(const char *format, ESPTime time) { return this->strftimedigit(0, format, time); } -#endif } // namespace max7219digit } // namespace esphome diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index 17e369a9d9..ead8033803 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -1,16 +1,13 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" +#include "esphome/core/time.h" + #include "esphome/components/display/display_buffer.h" #include "esphome/components/spi/spi.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace max7219digit { @@ -28,8 +25,7 @@ class MAX7219Component; using max7219_writer_t = std::function; -class MAX7219Component : public PollingComponent, - public display::DisplayBuffer, +class MAX7219Component : public display::DisplayBuffer, public spi::SPIDevice { public: @@ -88,13 +84,11 @@ class MAX7219Component : public PollingComponent, /// Print `str` at position 0. uint8_t printdigit(const char *str); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftimedigit(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftimedigit(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftimedigit(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index c1209a9627..55722e3ae0 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -45,19 +45,15 @@ def validate_mode(value): CONF_MCP23016 = "mcp23016" -MCP23016_PIN_SCHEMA = cv.All( +MCP23016_PIN_SCHEMA = pins.gpio_base_schema( + MCP23016GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(MCP23016GPIOPin), cv.Required(CONF_MCP23016): cv.use_id(MCP23016), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index 7bcd5c84fc..1e41a8ddff 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -54,20 +54,16 @@ def validate_mode(value): CONF_MCP23XXX = "mcp23xxx" -MCP23XXX_PIN_SCHEMA = cv.All( + +MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( + MCP23XXXGPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(MCP23XXXGPIOPin), cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, cv.Optional(CONF_INTERRUPT, default="NO_INTERRUPT"): cv.enum( MCP23XXX_INTERRUPT_MODES, upper=True ), diff --git a/esphome/components/mcp2515/canbus.py b/esphome/components/mcp2515/canbus.py index c410c1af69..4353cd7bc6 100644 --- a/esphome/components/mcp2515/canbus.py +++ b/esphome/components/mcp2515/canbus.py @@ -16,6 +16,7 @@ McpMode = mcp2515_ns.enum("CANCTRL_REQOP_MODE") CAN_CLOCK = { "8MHZ": CanClock.MCP_8MHZ, + "12MHZ": CanClock.MCP_12MHZ, "16MHZ": CanClock.MCP_16MHZ, "20MHZ": CanClock.MCP_20MHZ, } diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index b90b4de66d..fe4a68b583 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -16,11 +16,14 @@ const struct MCP2515::RxBnRegs MCP2515::RXB[N_RXBUFFERS] = {{MCP_RXB0CTRL, MCP_R bool MCP2515::setup_internal() { this->spi_setup(); - if (this->reset_() == canbus::ERROR_FAIL) + if (this->reset_() != canbus::ERROR_OK) return false; - this->set_bitrate_(this->bit_rate_, this->mcp_clock_); - this->set_mode_(this->mcp_mode_); - ESP_LOGV(TAG, "setup done"); + if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK) + return false; + if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK) + return false; + uint8_t err_flags = this->get_error_flags_(); + ESP_LOGD(TAG, "mcp2515 setup done, error_flags = %02X", err_flags); return true; } @@ -38,7 +41,7 @@ canbus::Error MCP2515::reset_() { set_registers_(MCP_TXB0CTRL, zeros, 14); set_registers_(MCP_TXB1CTRL, zeros, 14); set_registers_(MCP_TXB2CTRL, zeros, 14); - ESP_LOGD(TAG, "reset() CLEARED TXB registers"); + ESP_LOGV(TAG, "reset() CLEARED TXB registers"); set_register_(MCP_RXB0CTRL, 0); set_register_(MCP_RXB1CTRL, 0); @@ -114,16 +117,12 @@ canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode); uint32_t end_time = millis() + 10; - bool mode_match = false; while (millis() < end_time) { - uint8_t new_mode = read_register_(MCP_CANSTAT); - new_mode &= CANSTAT_OPMOD; - mode_match = new_mode == mode; - if (mode_match) { - break; - } + if ((read_register_(MCP_CANSTAT) & CANSTAT_OPMOD) == mode) + return canbus::ERROR_OK; } - return mode_match ? canbus::ERROR_OK : canbus::ERROR_FAIL; + ESP_LOGE(TAG, "Failed to set mode"); + return canbus::ERROR_FAIL; } canbus::Error MCP2515::set_clk_out_(const CanClkOut divisor) { @@ -451,6 +450,78 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo } break; + case (MCP_12MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5Kbps + cfg1 = MCP_12MHZ_5KBPS_CFG1; + cfg2 = MCP_12MHZ_5KBPS_CFG2; + cfg3 = MCP_12MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10Kbps + cfg1 = MCP_12MHZ_10KBPS_CFG1; + cfg2 = MCP_12MHZ_10KBPS_CFG2; + cfg3 = MCP_12MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20Kbps + cfg1 = MCP_12MHZ_20KBPS_CFG1; + cfg2 = MCP_12MHZ_20KBPS_CFG2; + cfg3 = MCP_12MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_12MHZ_33K3BPS_CFG1; + cfg2 = MCP_12MHZ_33K3BPS_CFG2; + cfg3 = MCP_12MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_12MHZ_40KBPS_CFG1; + cfg2 = MCP_12MHZ_40KBPS_CFG2; + cfg3 = MCP_12MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg2 = MCP_12MHZ_50KBPS_CFG2; + cfg3 = MCP_12MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_12MHZ_80KBPS_CFG1; + cfg2 = MCP_12MHZ_80KBPS_CFG2; + cfg3 = MCP_12MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_12MHZ_100KBPS_CFG1; + cfg2 = MCP_12MHZ_100KBPS_CFG2; + cfg3 = MCP_12MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_12MHZ_125KBPS_CFG1; + cfg2 = MCP_12MHZ_125KBPS_CFG2; + cfg3 = MCP_12MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_12MHZ_200KBPS_CFG1; + cfg2 = MCP_12MHZ_200KBPS_CFG2; + cfg3 = MCP_12MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_12MHZ_250KBPS_CFG1; + cfg2 = MCP_12MHZ_250KBPS_CFG2; + cfg3 = MCP_12MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_12MHZ_500KBPS_CFG1; + cfg2 = MCP_12MHZ_500KBPS_CFG2; + cfg3 = MCP_12MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_12MHZ_1000KBPS_CFG1; + cfg2 = MCP_12MHZ_1000KBPS_CFG2; + cfg3 = MCP_12MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + case (MCP_16MHZ): switch (can_speed) { case (canbus::CAN_5KBPS): // 5Kbps @@ -602,6 +673,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo set_register_(MCP_CNF3, cfg3); // NOLINT return canbus::ERROR_OK; } else { + ESP_LOGE(TAG, "Invalid frequency/bitrate combination: %d/%d", can_clock, can_speed); return canbus::ERROR_FAIL; } } diff --git a/esphome/components/mcp2515/mcp2515.h b/esphome/components/mcp2515/mcp2515.h index 3b9797a78a..c77480ce7d 100644 --- a/esphome/components/mcp2515/mcp2515.h +++ b/esphome/components/mcp2515/mcp2515.h @@ -11,7 +11,7 @@ static const uint32_t SPI_CLOCK = 10000000; // 10MHz static const int N_TXBUFFERS = 3; static const int N_RXBUFFERS = 2; -enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_8MHZ }; +enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_12MHZ, MCP_8MHZ }; enum MASK { MASK0, MASK1 }; enum RXF { RXF0 = 0, RXF1 = 1, RXF2 = 2, RXF3 = 3, RXF4 = 4, RXF5 = 5 }; enum RXBn { RXB0 = 0, RXB1 = 1 }; diff --git a/esphome/components/mcp2515/mcp2515_defs.h b/esphome/components/mcp2515/mcp2515_defs.h index 454c760c6d..2f5cf2a238 100644 --- a/esphome/components/mcp2515/mcp2515_defs.h +++ b/esphome/components/mcp2515/mcp2515_defs.h @@ -207,6 +207,62 @@ static const uint8_t MCP_8MHZ_5KBPS_CFG1 = 0x1F; static const uint8_t MCP_8MHZ_5KBPS_CFG2 = 0xBF; static const uint8_t MCP_8MHZ_5KBPS_CFG3 = 0x87; +/* + * Speed 12M + */ + +static const uint8_t MCP_12MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_12MHZ_1000KBPS_CFG2 = 0x88; +static const uint8_t MCP_12MHZ_1000KBPS_CFG3 = 0x81; + +static const uint8_t MCP_12MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_12MHZ_500KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_500KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_250KBPS_CFG1 = 0x01; +static const uint8_t MCP_12MHZ_250KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_250KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_12MHZ_200KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_200KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_12MHZ_125KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_125KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_100KBPS_CFG1 = 0x03; +static const uint8_t MCP_12MHZ_100KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_100KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_80KBPS_CFG1 = 0x04; +static const uint8_t MCP_12MHZ_80KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_80KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_50KBPS_CFG1 = 0x07; +static const uint8_t MCP_12MHZ_50KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_50KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_40KBPS_CFG1 = 0x09; +static const uint8_t MCP_12MHZ_40KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_40KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_33K3BPS_CFG1 = 0x08; +static const uint8_t MCP_12MHZ_33K3BPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_33K3BPS_CFG3 = 0x84; + +static const uint8_t MCP_12MHZ_20KBPS_CFG1 = 0x0E; +static const uint8_t MCP_12MHZ_20KBPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_20KBPS_CFG3 = 0x84; + +static const uint8_t MCP_12MHZ_10KBPS_CFG1 = 0x31; +static const uint8_t MCP_12MHZ_10KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_10KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_5KBPS_CFG1 = 0x3B; +static const uint8_t MCP_12MHZ_5KBPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_5KBPS_CFG3 = 0x84; + /* * speed 16M */ diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp index 81abc4f012..aed48456b2 100644 --- a/esphome/components/mcp3008/mcp3008.cpp +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -1,4 +1,6 @@ #include "mcp3008.h" + +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -32,28 +34,10 @@ float MCP3008::read_data(uint8_t pin) { this->disable(); - int data = data_msb << 8 | data_lsb; + uint16_t data = encode_uint16(data_msb, data_lsb); return data / 1023.0f; } -MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage) - : PollingComponent(1000), parent_(parent), pin_(pin), reference_voltage_(reference_voltage) {} - -float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } - -void MCP3008Sensor::setup() { LOG_SENSOR("", "Setting up MCP3008 Sensor '%s'...", this); } -void MCP3008Sensor::dump_config() { - ESP_LOGCONFIG(TAG, "MCP3008Sensor:"); - ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); - ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); -} -float MCP3008Sensor::sample() { - float value_v = this->parent_->read_data(pin_); - value_v = (value_v * this->reference_voltage_); - return value_v; -} -void MCP3008Sensor::update() { this->publish_state(this->sample()); } - } // namespace mcp3008 } // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h index 5d8b823111..baf8d7c152 100644 --- a/esphome/components/mcp3008/mcp3008.h +++ b/esphome/components/mcp3008/mcp3008.h @@ -1,10 +1,8 @@ #pragma once +#include "esphome/components/spi/spi.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/spi/spi.h" -#include "esphome/components/voltage_sampler/voltage_sampler.h" namespace esphome { namespace mcp3008 { @@ -14,31 +12,10 @@ class MCP3008 : public Component, spi::DATA_RATE_75KHZ> { // Running at the slowest max speed supported by the // mcp3008. 2.7v = 75ksps public: - MCP3008() = default; - void setup() override; void dump_config() override; float get_setup_priority() const override; float read_data(uint8_t pin); - - protected: -}; - -class MCP3008Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { - public: - MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage); - - void set_reference_voltage(float reference_voltage) { reference_voltage_ = reference_voltage; } - void setup() override; - void update() override; - void dump_config() override; - float get_setup_priority() const override; - float sample() override; - - protected: - MCP3008 *parent_; - uint8_t pin_; - float reference_voltage_; }; } // namespace mcp3008 diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py deleted file mode 100644 index dd5141484b..0000000000 --- a/esphome/components/mcp3008/sensor.py +++ /dev/null @@ -1,39 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import sensor, voltage_sampler -from esphome.const import CONF_ID, CONF_NUMBER -from . import mcp3008_ns, MCP3008 - -AUTO_LOAD = ["voltage_sampler"] - -DEPENDENCIES = ["mcp3008"] - -MCP3008Sensor = mcp3008_ns.class_( - "MCP3008Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler -) -CONF_REFERENCE_VOLTAGE = "reference_voltage" -CONF_MCP3008_ID = "mcp3008_id" - -CONFIG_SCHEMA = ( - sensor.sensor_schema(MCP3008Sensor) - .extend( - { - cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, - } - ) - .extend(cv.polling_component_schema("1s")) -) - - -async def to_code(config): - parent = await cg.get_variable(config[CONF_MCP3008_ID]) - var = cg.new_Pvariable( - config[CONF_ID], - parent, - config[CONF_NUMBER], - config[CONF_REFERENCE_VOLTAGE], - ) - await cg.register_component(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/mcp3008/sensor/__init__.py b/esphome/components/mcp3008/sensor/__init__.py new file mode 100644 index 0000000000..8ae00ef29e --- /dev/null +++ b/esphome/components/mcp3008/sensor/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import ( + CONF_ID, + CONF_NUMBER, + CONF_REFERENCE_VOLTAGE, + UNIT_VOLT, + STATE_CLASS_MEASUREMENT, + DEVICE_CLASS_VOLTAGE, +) + +from .. import mcp3008_ns, MCP3008 + +AUTO_LOAD = ["voltage_sampler"] + +DEPENDENCIES = ["mcp3008"] + +MCP3008Sensor = mcp3008_ns.class_( + "MCP3008Sensor", + sensor.Sensor, + cg.PollingComponent, + voltage_sampler.VoltageSampler, + cg.Parented.template(MCP3008), +) +CONF_MCP3008_ID = "mcp3008_id" + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + MCP3008Sensor, + unit_of_measurement=UNIT_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + ) + .extend( + { + cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_MCP3008_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp new file mode 100644 index 0000000000..df2a8735f8 --- /dev/null +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp @@ -0,0 +1,27 @@ +#include "mcp3008_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3008 { + +static const char *const TAG = "mcp3008.sensor"; + +float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MCP3008Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008Sensor:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); +} + +float MCP3008Sensor::sample() { + float value_v = this->parent_->read_data(pin_); + value_v = (value_v * this->reference_voltage_); + return value_v; +} + +void MCP3008Sensor::update() { this->publish_state(this->sample()); } + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.h b/esphome/components/mcp3008/sensor/mcp3008_sensor.h new file mode 100644 index 0000000000..ebaeab966f --- /dev/null +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/core/component.h" + +#include "../mcp3008.h" + +namespace esphome { +namespace mcp3008 { + +class MCP3008Sensor : public PollingComponent, + public sensor::Sensor, + public voltage_sampler::VoltageSampler, + public Parented { + public: + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + void set_pin(uint8_t pin) { this->pin_ = pin; } + + void update() override; + void dump_config() override; + float get_setup_priority() const override; + float sample() override; + + protected: + uint8_t pin_; + float reference_voltage_; +}; + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3204/__init__.py b/esphome/components/mcp3204/__init__.py index 0536166e56..98129fc389 100644 --- a/esphome/components/mcp3204/__init__.py +++ b/esphome/components/mcp3204/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import spi -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_REFERENCE_VOLTAGE DEPENDENCIES = ["spi"] MULTI_CONF = True @@ -10,7 +10,6 @@ CODEOWNERS = ["@rsumner"] mcp3204_ns = cg.esphome_ns.namespace("mcp3204") MCP3204 = mcp3204_ns.class_("MCP3204", cg.Component, spi.SPIDevice) -CONF_REFERENCE_VOLTAGE = "reference_voltage" CONFIG_SCHEMA = cv.Schema( { diff --git a/esphome/components/mcp4728/__init__.py b/esphome/components/mcp4728/__init__.py index d130ceb738..a0702c415c 100644 --- a/esphome/components/mcp4728/__init__.py +++ b/esphome/components/mcp4728/__init__.py @@ -10,6 +10,7 @@ CONF_STORE_IN_EEPROM = "store_in_eeprom" mcp4728_ns = cg.esphome_ns.namespace("mcp4728") MCP4728Component = mcp4728_ns.class_("MCP4728Component", cg.Component, i2c.I2CDevice) +CONF_MCP4728_ID = "mcp4728_id" CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/mcp4728/mcp4728.cpp b/esphome/components/mcp4728/mcp4728.cpp new file mode 100644 index 0000000000..1a8568a21c --- /dev/null +++ b/esphome/components/mcp4728/mcp4728.cpp @@ -0,0 +1,114 @@ +#include "mcp4728.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp4728 { + +static const char *const TAG = "mcp4728"; + +void MCP4728Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP4728 (0x%02X)...", this->address_); + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->mark_failed(); + return; + } +} + +void MCP4728Component::dump_config() { + ESP_LOGCONFIG(TAG, "MCP4728:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MCP4728 failed!"); + } +} + +void MCP4728Component::loop() { + if (this->update_) { + this->update_ = false; + if (this->store_in_eeprom_) { + if (!this->seq_write_()) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + } else { + if (!this->multi_write_()) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + } + } +} + +void MCP4728Component::set_channel_value_(MCP4728ChannelIdx channel, uint16_t value) { + uint8_t cn = 0; + if (channel == MCP4728_CHANNEL_A) { + cn = 'A'; + } else if (channel == MCP4728_CHANNEL_B) { + cn = 'B'; + } else if (channel == MCP4728_CHANNEL_C) { + cn = 'C'; + } else { + cn = 'D'; + } + ESP_LOGV(TAG, "Setting MCP4728 channel %c to %d!", cn, value); + reg_[channel].data = value; + this->update_ = true; +} + +bool MCP4728Component::multi_write_() { + i2c::ErrorCode err[4]; + for (uint8_t i = 0; i < 4; ++i) { + uint8_t wd[3]; + wd[0] = ((uint8_t) CMD::MULTI_WRITE | (i << 1)) & 0xFE; + wd[1] = ((uint8_t) reg_[i].vref << 7) | ((uint8_t) reg_[i].pd << 5) | ((uint8_t) reg_[i].gain << 4) | + (reg_[i].data >> 8); + wd[2] = reg_[i].data & 0xFF; + err[i] = this->write(wd, sizeof(wd)); + } + bool ok = true; + for (auto &e : err) { + if (e != i2c::ERROR_OK) { + ok = false; + break; + } + } + return ok; +} + +bool MCP4728Component::seq_write_() { + uint8_t wd[9]; + wd[0] = (uint8_t) CMD::SEQ_WRITE; + for (uint8_t i = 0; i < 4; i++) { + wd[i * 2 + 1] = ((uint8_t) reg_[i].vref << 7) | ((uint8_t) reg_[i].pd << 5) | ((uint8_t) reg_[i].gain << 4) | + (reg_[i].data >> 8); + wd[i * 2 + 2] = reg_[i].data & 0xFF; + } + auto err = this->write(wd, sizeof(wd)); + return err == i2c::ERROR_OK; +} + +void MCP4728Component::select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref) { + reg_[channel].vref = vref; + + this->update_ = true; +} + +void MCP4728Component::select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd) { + reg_[channel].pd = pd; + + this->update_ = true; +} + +void MCP4728Component::select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain) { + reg_[channel].gain = gain; + + this->update_ = true; +} + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp4728/mcp4728.h b/esphome/components/mcp4728/mcp4728.h new file mode 100644 index 0000000000..f2262f4a35 --- /dev/null +++ b/esphome/components/mcp4728/mcp4728.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp4728 { + +enum class CMD { + FAST_WRITE = 0x00, + MULTI_WRITE = 0x40, + SINGLE_WRITE = 0x58, + SEQ_WRITE = 0x50, + SELECT_VREF = 0x80, + SELECT_GAIN = 0xC0, + SELECT_POWER_DOWN = 0xA0 +}; + +enum MCP4728Vref { MCP4728_VREF_VDD = 0, MCP4728_VREF_INTERNAL_2_8V = 1 }; + +enum MCP4728PwrDown { + MCP4728_PD_NORMAL = 0, + MCP4728_PD_GND_1KOHM = 1, + MCP4728_PD_GND_100KOHM = 2, + MCP4728_PD_GND_500KOHM = 3 +}; + +enum MCP4728Gain { MCP4728_GAIN_X1 = 0, MCP4728_GAIN_X2 = 1 }; + +enum MCP4728ChannelIdx { MCP4728_CHANNEL_A = 0, MCP4728_CHANNEL_B = 1, MCP4728_CHANNEL_C = 2, MCP4728_CHANNEL_D = 3 }; + +struct DACInputData { + MCP4728Vref vref; + MCP4728PwrDown pd; + MCP4728Gain gain; + uint16_t data; +}; + +class MCP4728Channel; + +/// MCP4728 float output component. +class MCP4728Component : public Component, public i2c::I2CDevice { + public: + MCP4728Component(bool store_in_eeprom) : store_in_eeprom_(store_in_eeprom) {} + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + protected: + friend MCP4728Channel; + void set_channel_value_(MCP4728ChannelIdx channel, uint16_t value); + bool multi_write_(); + bool seq_write_(); + void select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref); + void select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd); + void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain); + + private: + DACInputData reg_[4]; + bool store_in_eeprom_ = false; + bool update_ = false; +}; + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp4728/mcp4728_output.cpp b/esphome/components/mcp4728/mcp4728_output.cpp deleted file mode 100644 index d011967624..0000000000 --- a/esphome/components/mcp4728/mcp4728_output.cpp +++ /dev/null @@ -1,121 +0,0 @@ -#include "mcp4728_output.h" - -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace mcp4728 { - -static const char *const TAG = "mcp4728"; - -void MCP4728Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up MCP4728 (0x%02X)...", this->address_); - auto err = this->write(nullptr, 0); - if (err != i2c::ERROR_OK) { - this->mark_failed(); - return; - } -} - -void MCP4728Component::dump_config() { - ESP_LOGCONFIG(TAG, "MCP4728:"); - LOG_I2C_DEVICE(this); - if (this->is_failed()) { - ESP_LOGE(TAG, "Communication with MCP4728 failed!"); - } -} - -void MCP4728Component::loop() { - if (this->update_) { - this->update_ = false; - if (this->store_in_eeprom_) { - if (!this->seq_write_()) { - this->status_set_error(); - } else { - this->status_clear_error(); - } - } else { - if (!this->multi_write_()) { - this->status_set_error(); - } else { - this->status_clear_error(); - } - } - } -} - -void MCP4728Component::set_channel_value_(MCP4728ChannelIdx channel, uint16_t value) { - uint8_t cn = 0; - if (channel == MCP4728_CHANNEL_A) { - cn = 'A'; - } else if (channel == MCP4728_CHANNEL_B) { - cn = 'B'; - } else if (channel == MCP4728_CHANNEL_C) { - cn = 'C'; - } else { - cn = 'D'; - } - ESP_LOGV(TAG, "Setting MCP4728 channel %c to %d!", cn, value); - reg_[channel].data = value; - this->update_ = true; -} - -bool MCP4728Component::multi_write_() { - i2c::ErrorCode err[4]; - for (uint8_t i = 0; i < 4; ++i) { - uint8_t wd[3]; - wd[0] = ((uint8_t) CMD::MULTI_WRITE | (i << 1)) & 0xFE; - wd[1] = ((uint8_t) reg_[i].vref << 7) | ((uint8_t) reg_[i].pd << 5) | ((uint8_t) reg_[i].gain << 4) | - (reg_[i].data >> 8); - wd[2] = reg_[i].data & 0xFF; - err[i] = this->write(wd, sizeof(wd)); - } - bool ok = true; - for (auto &e : err) { - if (e != i2c::ERROR_OK) { - ok = false; - break; - } - } - return ok; -} - -bool MCP4728Component::seq_write_() { - uint8_t wd[9]; - wd[0] = (uint8_t) CMD::SEQ_WRITE; - for (uint8_t i = 0; i < 4; i++) { - wd[i * 2 + 1] = ((uint8_t) reg_[i].vref << 7) | ((uint8_t) reg_[i].pd << 5) | ((uint8_t) reg_[i].gain << 4) | - (reg_[i].data >> 8); - wd[i * 2 + 2] = reg_[i].data & 0xFF; - } - auto err = this->write(wd, sizeof(wd)); - return err == i2c::ERROR_OK; -} - -void MCP4728Component::select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref) { - reg_[channel].vref = vref; - - this->update_ = true; -} - -void MCP4728Component::select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd) { - reg_[channel].pd = pd; - - this->update_ = true; -} - -void MCP4728Component::select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain) { - reg_[channel].gain = gain; - - this->update_ = true; -} - -void MCP4728Channel::write_state(float state) { - const uint16_t max_duty = 4095; - const float duty_rounded = roundf(state * max_duty); - auto duty = static_cast(duty_rounded); - this->parent_->set_channel_value_(this->channel_, duty); -} - -} // namespace mcp4728 -} // namespace esphome diff --git a/esphome/components/mcp4728/mcp4728_output.h b/esphome/components/mcp4728/mcp4728_output.h deleted file mode 100644 index 55bcfdccb6..0000000000 --- a/esphome/components/mcp4728/mcp4728_output.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/output/float_output.h" -#include "esphome/components/i2c/i2c.h" - -namespace esphome { -namespace mcp4728 { - -enum class CMD { - FAST_WRITE = 0x00, - MULTI_WRITE = 0x40, - SINGLE_WRITE = 0x58, - SEQ_WRITE = 0x50, - SELECT_VREF = 0x80, - SELECT_GAIN = 0xC0, - SELECT_POWER_DOWN = 0xA0 -}; - -enum MCP4728Vref { MCP4728_VREF_VDD = 0, MCP4728_VREF_INTERNAL_2_8V = 1 }; - -enum MCP4728PwrDown { - MCP4728_PD_NORMAL = 0, - MCP4728_PD_GND_1KOHM = 1, - MCP4728_PD_GND_100KOHM = 2, - MCP4728_PD_GND_500KOHM = 3 -}; - -enum MCP4728Gain { MCP4728_GAIN_X1 = 0, MCP4728_GAIN_X2 = 1 }; - -enum MCP4728ChannelIdx { MCP4728_CHANNEL_A = 0, MCP4728_CHANNEL_B = 1, MCP4728_CHANNEL_C = 2, MCP4728_CHANNEL_D = 3 }; - -struct DACInputData { - MCP4728Vref vref; - MCP4728PwrDown pd; - MCP4728Gain gain; - uint16_t data; -}; - -class MCP4728Channel; - -/// MCP4728 float output component. -class MCP4728Component : public Component, public i2c::I2CDevice { - public: - MCP4728Component(bool store_in_eeprom) : store_in_eeprom_(store_in_eeprom) {} - - void setup() override; - void dump_config() override; - float get_setup_priority() const override { return setup_priority::HARDWARE; } - void loop() override; - - protected: - friend MCP4728Channel; - void set_channel_value_(MCP4728ChannelIdx channel, uint16_t value); - bool multi_write_(); - bool seq_write_(); - void select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref); - void select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd); - void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain); - - private: - DACInputData reg_[4]; - bool store_in_eeprom_ = false; - bool update_ = false; -}; - -class MCP4728Channel : public output::FloatOutput { - public: - MCP4728Channel(MCP4728Component *parent, MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain, - MCP4728PwrDown pwrdown) - : parent_(parent), channel_(channel), vref_(vref), gain_(gain), pwrdown_(pwrdown) { - // update VREF - parent->select_vref_(channel, vref_); - // update PD - parent->select_power_down_(channel, pwrdown_); - // update GAIN - parent->select_gain_(channel, gain_); - } - - protected: - void write_state(float state) override; - - MCP4728Component *parent_; - MCP4728ChannelIdx channel_; - MCP4728Vref vref_; - MCP4728Gain gain_; - MCP4728PwrDown pwrdown_; -}; - -} // namespace mcp4728 -} // namespace esphome diff --git a/esphome/components/mcp4728/output.py b/esphome/components/mcp4728/output.py deleted file mode 100644 index e0913ab98a..0000000000 --- a/esphome/components/mcp4728/output.py +++ /dev/null @@ -1,63 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import output -from esphome.const import CONF_CHANNEL, CONF_ID, CONF_GAIN -from . import MCP4728Component, mcp4728_ns - -DEPENDENCIES = ["mcp4728"] - -MCP4728Channel = mcp4728_ns.class_("MCP4728Channel", output.FloatOutput) -CONF_MCP4728_ID = "mcp4728_id" -CONF_VREF = "vref" -CONF_POWER_DOWN = "power_down" - -MCP4728Vref = mcp4728_ns.enum("MCP4728Vref") -VREF_OPTIONS = { - "vdd": MCP4728Vref.MCP4728_VREF_VDD, - "internal": MCP4728Vref.MCP4728_VREF_INTERNAL_2_8V, -} - -MCP4728Gain = mcp4728_ns.enum("MCP4728Gain") -GAIN_OPTIONS = {"X1": MCP4728Gain.MCP4728_GAIN_X1, "X2": MCP4728Gain.MCP4728_GAIN_X2} - -MCP4728PwrDown = mcp4728_ns.enum("MCP4728PwrDown") -PWRDOWN_OPTIONS = { - "normal": MCP4728PwrDown.MCP4728_PD_NORMAL, - "gnd_1k": MCP4728PwrDown.MCP4728_PD_GND_1KOHM, - "gnd_100k": MCP4728PwrDown.MCP4728_PD_GND_100KOHM, - "gnd_500k": MCP4728PwrDown.MCP4728_PD_GND_500KOHM, -} - -MCP4728ChannelIdx = mcp4728_ns.enum("MCP4728ChannelIdx") -CHANNEL_OPTIONS = { - "A": MCP4728ChannelIdx.MCP4728_CHANNEL_A, - "B": MCP4728ChannelIdx.MCP4728_CHANNEL_B, - "C": MCP4728ChannelIdx.MCP4728_CHANNEL_C, - "D": MCP4728ChannelIdx.MCP4728_CHANNEL_D, -} - -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( - { - cv.Required(CONF_ID): cv.declare_id(MCP4728Channel), - cv.GenerateID(CONF_MCP4728_ID): cv.use_id(MCP4728Component), - cv.Required(CONF_CHANNEL): cv.enum(CHANNEL_OPTIONS, upper=True), - cv.Optional(CONF_VREF, default="vdd"): cv.enum(VREF_OPTIONS, upper=False), - cv.Optional(CONF_POWER_DOWN, default="normal"): cv.enum( - PWRDOWN_OPTIONS, upper=False - ), - cv.Optional(CONF_GAIN, default="X1"): cv.enum(GAIN_OPTIONS, upper=True), - } -) - - -async def to_code(config): - paren = await cg.get_variable(config[CONF_MCP4728_ID]) - var = cg.new_Pvariable( - config[CONF_ID], - paren, - config[CONF_CHANNEL], - config[CONF_VREF], - config[CONF_GAIN], - config[CONF_POWER_DOWN], - ) - await output.register_output(var, config) diff --git a/esphome/components/mcp4728/output/__init__.py b/esphome/components/mcp4728/output/__init__.py new file mode 100644 index 0000000000..20b196ca2c --- /dev/null +++ b/esphome/components/mcp4728/output/__init__.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_GAIN +from .. import MCP4728Component, CONF_MCP4728_ID, mcp4728_ns + +DEPENDENCIES = ["mcp4728"] + +MCP4728Channel = mcp4728_ns.class_("MCP4728Channel", output.FloatOutput) +CONF_VREF = "vref" +CONF_POWER_DOWN = "power_down" + +MCP4728Vref = mcp4728_ns.enum("MCP4728Vref") +VREF_OPTIONS = { + "vdd": MCP4728Vref.MCP4728_VREF_VDD, + "internal": MCP4728Vref.MCP4728_VREF_INTERNAL_2_8V, +} + +MCP4728Gain = mcp4728_ns.enum("MCP4728Gain") +GAIN_OPTIONS = {"X1": MCP4728Gain.MCP4728_GAIN_X1, "X2": MCP4728Gain.MCP4728_GAIN_X2} + +MCP4728PwrDown = mcp4728_ns.enum("MCP4728PwrDown") +PWRDOWN_OPTIONS = { + "normal": MCP4728PwrDown.MCP4728_PD_NORMAL, + "gnd_1k": MCP4728PwrDown.MCP4728_PD_GND_1KOHM, + "gnd_100k": MCP4728PwrDown.MCP4728_PD_GND_100KOHM, + "gnd_500k": MCP4728PwrDown.MCP4728_PD_GND_500KOHM, +} + +MCP4728ChannelIdx = mcp4728_ns.enum("MCP4728ChannelIdx") +CHANNEL_OPTIONS = { + "A": MCP4728ChannelIdx.MCP4728_CHANNEL_A, + "B": MCP4728ChannelIdx.MCP4728_CHANNEL_B, + "C": MCP4728ChannelIdx.MCP4728_CHANNEL_C, + "D": MCP4728ChannelIdx.MCP4728_CHANNEL_D, +} + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MCP4728Channel), + cv.GenerateID(CONF_MCP4728_ID): cv.use_id(MCP4728Component), + cv.Required(CONF_CHANNEL): cv.enum(CHANNEL_OPTIONS, upper=True), + cv.Optional(CONF_VREF, default="vdd"): cv.enum(VREF_OPTIONS, upper=False), + cv.Optional(CONF_POWER_DOWN, default="normal"): cv.enum( + PWRDOWN_OPTIONS, upper=False + ), + cv.Optional(CONF_GAIN, default="X1"): cv.enum(GAIN_OPTIONS, upper=True), + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_MCP4728_ID]) + var = cg.new_Pvariable( + config[CONF_ID], + paren, + config[CONF_CHANNEL], + config[CONF_VREF], + config[CONF_GAIN], + config[CONF_POWER_DOWN], + ) + await output.register_output(var, config) diff --git a/esphome/components/mcp4728/output/mcp4728_output.cpp b/esphome/components/mcp4728/output/mcp4728_output.cpp new file mode 100644 index 0000000000..b587e8801b --- /dev/null +++ b/esphome/components/mcp4728/output/mcp4728_output.cpp @@ -0,0 +1,17 @@ +#include "mcp4728_output.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp4728 { + +void MCP4728Channel::write_state(float state) { + const uint16_t max_duty = 4095; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast(duty_rounded); + this->parent_->set_channel_value_(this->channel_, duty); +} + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp4728/output/mcp4728_output.h b/esphome/components/mcp4728/output/mcp4728_output.h new file mode 100644 index 0000000000..453d632f4c --- /dev/null +++ b/esphome/components/mcp4728/output/mcp4728_output.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../mcp4728.h" +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp4728 { + +class MCP4728Channel : public output::FloatOutput { + public: + MCP4728Channel(MCP4728Component *parent, MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain, + MCP4728PwrDown pwrdown) + : parent_(parent), channel_(channel) { + // update VREF + parent->select_vref_(channel, vref); + // update PD + parent->select_power_down_(channel, pwrdown); + // update GAIN + parent->select_gain_(channel, gain); + } + + protected: + void write_state(float state) override; + + MCP4728Component *parent_; + MCP4728ChannelIdx channel_; +}; + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp9600/sensor.py b/esphome/components/mcp9600/sensor.py index 392ee4e773..65ae5f2eec 100644 --- a/esphome/components/mcp9600/sensor.py +++ b/esphome/components/mcp9600/sensor.py @@ -1,19 +1,19 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import i2c, sensor +import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_THERMOCOUPLE_TYPE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) -CONF_THERMOCOUPLE_TYPE = "thermocouple_type" CONF_HOT_JUNCTION = "hot_junction" CONF_COLD_JUNCTION = "cold_junction" DEPENDENCIES = ["i2c"] -CODEOWNERS = ["@MrEditor97"] +CODEOWNERS = ["@mreditor97"] mcp9600_ns = cg.esphome_ns.namespace("mcp9600") MCP9600Component = mcp9600_ns.class_( diff --git a/esphome/components/md5/__init__.py b/esphome/components/md5/__init__.py index f70ffa9520..1af9ee0b29 100644 --- a/esphome/components/md5/__init__.py +++ b/esphome/components/md5/__init__.py @@ -1 +1,7 @@ +import esphome.codegen as cg + CODEOWNERS = ["@esphome/core"] + + +async def to_code(config): + cg.add_define("USE_MD5") diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index 620b6749f3..31f52634be 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -1,6 +1,7 @@ #include #include #include "md5.h" +#ifdef USE_MD5 #include "esphome/core/helpers.h" namespace esphome { @@ -65,3 +66,4 @@ bool MD5Digest::equals_hex(const char *expected) { } // namespace md5 } // namespace esphome +#endif diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index 738a312267..cb6accf46f 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/defines.h" +#ifdef USE_MD5 #ifdef USE_ESP_IDF #include "esp_rom_md5.h" @@ -22,6 +23,11 @@ #define MD5_CTX_TYPE br_md5_context #endif +#if defined(USE_LIBRETINY) +#include +#define MD5_CTX_TYPE LT_MD5_CTX_T +#endif + namespace esphome { namespace md5 { @@ -61,3 +67,4 @@ class MD5Digest { } // namespace md5 } // namespace esphome +#endif diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index d9b36c7b09..dd68fbb93c 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,17 +1,17 @@ +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_component +import esphome.config_validation as cv from esphome.const import ( + CONF_DISABLED, CONF_ID, CONF_PORT, CONF_PROTOCOL, - CONF_SERVICES, CONF_SERVICE, + CONF_SERVICES, KEY_CORE, KEY_FRAMEWORK_VERSION, - CONF_DISABLED, ) -import esphome.codegen as cg -import esphome.config_validation as cv from esphome.core import CORE, coroutine_with_priority -from esphome.components.esp32 import add_idf_component CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] @@ -74,6 +74,9 @@ def mdns_service( @coroutine_with_priority(55.0) async def to_code(config): + if config[CONF_DISABLED] is True: + return + if CORE.using_arduino: if CORE.is_esp32: cg.add_library("ESPmDNS", None) @@ -86,15 +89,12 @@ async def to_code(config): 5, 0, 0 ): add_idf_component( - "mdns", - "https://github.com/espressif/esp-protocols.git", - "mdns-v1.0.9", - "components/mdns", + name="mdns", + repo="https://github.com/espressif/esp-protocols.git", + ref="mdns-v1.3.2", + path="components/mdns", ) - if config[CONF_DISABLED]: - return - cg.add_define("USE_MDNS") var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index cdb9aa8e74..2fc09330cd 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -1,5 +1,6 @@ -#include "mdns_component.h" #include "esphome/core/defines.h" +#ifdef USE_MDNS +#include "mdns_component.h" #include "esphome/core/version.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -44,6 +45,9 @@ void MDNSComponent::compile_records_() { #endif #ifdef USE_RP2040 platform = "RP2040"; +#endif +#ifdef USE_LIBRETINY + platform = lt_cpu_get_model_name(); #endif if (platform != nullptr) { service.txt_records.push_back({"platform", platform}); @@ -57,6 +61,10 @@ void MDNSComponent::compile_records_() { service.txt_records.push_back({"network", "ethernet"}); #endif +#ifdef USE_API_NOISE + service.txt_records.push_back({"api_encryption", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); +#endif + #ifdef ESPHOME_PROJECT_NAME service.txt_records.push_back({"project_name", ESPHOME_PROJECT_NAME}); service.txt_records.push_back({"project_version", ESPHOME_PROJECT_VERSION}); @@ -118,3 +126,4 @@ void MDNSComponent::dump_config() { } // namespace mdns } // namespace esphome +#endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index b2cb10db62..dfb5b72292 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_MDNS #include #include #include "esphome/core/component.h" @@ -46,3 +47,4 @@ class MDNSComponent : public Component { } // namespace mdns } // namespace esphome +#endif diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 6081c96637..8006eb27f1 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -1,4 +1,5 @@ -#ifdef USE_ESP32 +#include "esphome/core/defines.h" +#if defined(USE_ESP32) && defined(USE_MDNS) #include #include diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 4ccfe42baa..7b6e7ec448 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -1,4 +1,5 @@ -#if defined(USE_ESP8266) && defined(USE_ARDUINO) +#include "esphome/core/defines.h" +#if defined(USE_ESP8266) && defined(USE_ARDUINO) && defined(USE_MDNS) #include #include "esphome/components/network/ip_address.h" @@ -13,8 +14,7 @@ namespace mdns { void MDNSComponent::setup() { this->compile_records_(); - network::IPAddress addr = network::get_ip_address(); - MDNS.begin(this->hostname_.c_str(), (uint32_t) addr); + MDNS.begin(this->hostname_.c_str()); for (const auto &service : this->services_) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index 3f89146f02..78767ed136 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -1,4 +1,5 @@ -#ifdef USE_HOST +#include "esphome/core/defines.h" +#if defined(USE_HOST) && defined(USE_MDNS) #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp new file mode 100644 index 0000000000..c9a9a289dd --- /dev/null +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -0,0 +1,44 @@ +#include "esphome/core/defines.h" +#if defined(USE_LIBRETINY) && defined(USE_MDNS) + +#include "esphome/components/network/ip_address.h" +#include "esphome/components/network/util.h" +#include "esphome/core/log.h" +#include "mdns_component.h" + +#include + +namespace esphome { +namespace mdns { + +void MDNSComponent::setup() { + this->compile_records_(); + + MDNS.begin(this->hostname_.c_str()); + + for (const auto &service : this->services_) { + // Strip the leading underscore from the proto and service_type. While it is + // part of the wire protocol to have an underscore, and for example ESP-IDF + // expects the underscore to be there, the ESP8266 implementation always adds + // the underscore itself. + auto *proto = service.proto.c_str(); + while (*proto == '_') { + proto++; + } + auto *service_type = service.service_type.c_str(); + while (*service_type == '_') { + service_type++; + } + MDNS.addService(service_type, proto, service.port); + for (const auto &record : service.txt_records) { + MDNS.addServiceTxt(service_type, proto, record.key.c_str(), record.value.c_str()); + } + } +} + +void MDNSComponent::on_shutdown() {} + +} // namespace mdns +} // namespace esphome + +#endif diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index b30a3a4ee7..89e668ee59 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -1,4 +1,5 @@ -#ifdef USE_RP2040 +#include "esphome/core/defines.h" +#if defined(USE_RP2040) && defined(USE_MDNS) #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" @@ -13,8 +14,7 @@ namespace mdns { void MDNSComponent::setup() { this->compile_records_(); - network::IPAddress addr = network::get_ip_address(); - MDNS.begin(this->hostname_.c_str(), (uint32_t) addr); + MDNS.begin(this->hostname_.c_str()); for (const auto &service : this->services_) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 80f5fc558a..a46b30db29 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -1,14 +1,18 @@ from esphome import automation -import esphome.config_validation as cv -import esphome.codegen as cg - from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_ON_IDLE, + CONF_ON_STATE, + CONF_TRIGGER_ID, + CONF_VOLUME, +) from esphome.core import CORE from esphome.coroutine import coroutine_with_priority from esphome.cpp_helpers import setup_entity - CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True @@ -17,6 +21,7 @@ media_player_ns = cg.esphome_ns.namespace("media_player") MediaPlayer = media_player_ns.class_("MediaPlayer") + PlayAction = media_player_ns.class_( "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) ) @@ -43,18 +48,24 @@ VolumeSetAction = media_player_ns.class_( ) -CONF_VOLUME = "volume" -CONF_ON_IDLE = "on_idle" CONF_ON_PLAY = "on_play" CONF_ON_PAUSE = "on_pause" +CONF_ON_ANNOUNCEMENT = "on_announcement" CONF_MEDIA_URL = "media_url" StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template()) IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template()) PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template()) PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template()) +AnnoucementTrigger = media_player_ns.class_( + "AnnouncementTrigger", automation.Trigger.template() +) IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) +IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition) IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) +IsAnnouncingCondition = media_player_ns.class_( + "IsAnnouncingCondition", automation.Condition +) async def setup_media_player_core_(var, config): @@ -71,6 +82,9 @@ async def setup_media_player_core_(var, config): for conf in config.get(CONF_ON_PAUSE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ANNOUNCEMENT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_media_player(var, config): @@ -102,6 +116,11 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), } ), + cv.Optional(CONF_ON_ANNOUNCEMENT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AnnoucementTrigger), + } + ), } ) @@ -145,9 +164,15 @@ async def media_player_play_media_action(config, action_id, template_arg, args): @automation.register_condition( "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA ) +@automation.register_condition( + "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_ACTION_SCHEMA +) @automation.register_condition( "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA ) +@automation.register_condition( + "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_ACTION_SCHEMA +) async def media_player_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 261e93775c..7b9220c4a5 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -7,30 +7,24 @@ namespace esphome { namespace media_player { -#define MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ACTION_CLASS, ACTION_COMMAND) \ - template class ACTION_CLASS : public Action, public Parented { \ - void play(Ts... x) override { \ - this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_##ACTION_COMMAND).perform(); \ - } \ - }; +template +class MediaPlayerCommandAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->make_call().set_command(Command).perform(); } +}; -#define MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(TRIGGER_CLASS, TRIGGER_STATE) \ - class TRIGGER_CLASS : public Trigger<> { \ - public: \ - explicit TRIGGER_CLASS(MediaPlayer *player) { \ - player->add_on_state_callback([this, player]() { \ - if (player->state == MediaPlayerState::MEDIA_PLAYER_STATE_##TRIGGER_STATE) \ - this->trigger(); \ - }); \ - } \ - }; - -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PlayAction, PLAY) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PauseAction, PAUSE) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(StopAction, STOP) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN) +template +using PlayAction = MediaPlayerCommandAction; +template +using PauseAction = MediaPlayerCommandAction; +template +using StopAction = MediaPlayerCommandAction; +template +using ToggleAction = MediaPlayerCommandAction; +template +using VolumeUpAction = MediaPlayerCommandAction; +template +using VolumeDownAction = MediaPlayerCommandAction; template class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) @@ -49,9 +43,20 @@ class StateTrigger : public Trigger<> { } }; -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(IdleTrigger, IDLE) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PlayTrigger, PLAYING) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PauseTrigger, PAUSED) +template class MediaPlayerStateTrigger : public Trigger<> { + public: + explicit MediaPlayerStateTrigger(MediaPlayer *player) { + player->add_on_state_callback([this, player]() { + if (player->state == State) + this->trigger(); + }); + } +}; + +using IdleTrigger = MediaPlayerStateTrigger; +using PlayTrigger = MediaPlayerStateTrigger; +using PauseTrigger = MediaPlayerStateTrigger; +using AnnouncementTrigger = MediaPlayerStateTrigger; template class IsIdleCondition : public Condition, public Parented { public: @@ -63,5 +68,15 @@ template class IsPlayingCondition : public Condition, pub bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } }; +template class IsPausedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; } +}; + +template class IsAnnouncingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; } +}; + } // namespace media_player } // namespace esphome diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 81cb6ca751..b5190d8573 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -15,6 +15,8 @@ const char *media_player_state_to_string(MediaPlayerState state) { return "PLAYING"; case MEDIA_PLAYER_STATE_PAUSED: return "PAUSED"; + case MEDIA_PLAYER_STATE_ANNOUNCING: + return "ANNOUNCING"; case MEDIA_PLAYER_STATE_NONE: default: return "UNKNOWN"; @@ -35,6 +37,10 @@ const char *media_player_command_to_string(MediaPlayerCommand command) { return "UNMUTE"; case MEDIA_PLAYER_COMMAND_TOGGLE: return "TOGGLE"; + case MEDIA_PLAYER_COMMAND_VOLUME_UP: + return "VOLUME_UP"; + case MEDIA_PLAYER_COMMAND_VOLUME_DOWN: + return "VOLUME_DOWN"; default: return "UNKNOWN"; } @@ -68,6 +74,9 @@ void MediaPlayerCall::perform() { if (this->volume_.has_value()) { ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value()); } + if (this->announcement_.has_value()) { + ESP_LOGD(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); + } this->parent_->control(*this); } @@ -108,6 +117,11 @@ MediaPlayerCall &MediaPlayerCall::set_volume(float volume) { return *this; } +MediaPlayerCall &MediaPlayerCall::set_announcement(bool announce) { + this->announcement_ = announce; + return *this; +} + void MediaPlayer::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 88114d5337..78b3ed6216 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -10,7 +10,8 @@ enum MediaPlayerState : uint8_t { MEDIA_PLAYER_STATE_NONE = 0, MEDIA_PLAYER_STATE_IDLE = 1, MEDIA_PLAYER_STATE_PLAYING = 2, - MEDIA_PLAYER_STATE_PAUSED = 3 + MEDIA_PLAYER_STATE_PAUSED = 3, + MEDIA_PLAYER_STATE_ANNOUNCING = 4 }; const char *media_player_state_to_string(MediaPlayerState state); @@ -26,6 +27,19 @@ enum MediaPlayerCommand : uint8_t { }; const char *media_player_command_to_string(MediaPlayerCommand command); +enum class MediaPlayerFormatPurpose : uint8_t { + PURPOSE_DEFAULT = 0, + PURPOSE_ANNOUNCEMENT = 1, +}; + +struct MediaPlayerSupportedFormat { + std::string format; + uint32_t sample_rate; + uint32_t num_channels; + MediaPlayerFormatPurpose purpose; + uint32_t sample_bytes; +}; + class MediaPlayer; class MediaPlayerTraits { @@ -36,8 +50,11 @@ class MediaPlayerTraits { bool get_supports_pause() const { return this->supports_pause_; } + std::vector &get_supported_formats() { return this->supported_formats_; } + protected: bool supports_pause_{false}; + std::vector supported_formats_{}; }; class MediaPlayerCall { @@ -51,12 +68,14 @@ class MediaPlayerCall { MediaPlayerCall &set_media_url(const std::string &url); MediaPlayerCall &set_volume(float volume); + MediaPlayerCall &set_announcement(bool announce); void perform(); const optional &get_command() const { return command_; } const optional &get_media_url() const { return media_url_; } const optional &get_volume() const { return volume_; } + const optional &get_announcement() const { return announcement_; } protected: void validate_(); @@ -64,6 +83,7 @@ class MediaPlayerCall { optional command_; optional media_url_; optional volume_; + optional announcement_; }; class MediaPlayer : public EntityBase { diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index db3ad50851..c3c8120362 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -1,6 +1,8 @@ #include "mhz19.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace mhz19 { @@ -29,6 +31,14 @@ void MHZ19Component::setup() { } void MHZ19Component::update() { + uint32_t now_ms = millis(); + uint32_t warmup_ms = this->warmup_seconds_ * 1000; + if (now_ms < warmup_ms) { + ESP_LOGW(TAG, "MHZ19 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); + this->status_set_warning(); + return; + } + uint8_t response[MHZ19_RESPONSE_LENGTH]; if (!this->mhz19_write_command_(MHZ19_COMMAND_GET_PPM, response)) { ESP_LOGW(TAG, "Reading data from MHZ19 failed!"); @@ -101,6 +111,8 @@ void MHZ19Component::dump_config() { } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { ESP_LOGCONFIG(TAG, " Automatic baseline calibration disabled on boot"); } + + ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_); } } // namespace mhz19 diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 151351be4c..ec38f2cd2f 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -25,6 +25,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { 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; } protected: bool mhz19_write_command_(const uint8_t *command, uint8_t *response); @@ -32,6 +33,7 @@ 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_; }; template class MHZ19CalibrateZeroAction : public Action { diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 0081f42952..3956727981 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -18,6 +18,7 @@ from esphome.const import ( DEPENDENCIES = ["uart"] CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration" +CONF_WARMUP_TIME = "warmup_time" mhz19_ns = cg.esphome_ns.namespace("mhz19") MHZ19Component = mhz19_ns.class_("MHZ19Component", cg.PollingComponent, uart.UARTDevice) @@ -45,6 +46,9 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, + cv.Optional( + CONF_WARMUP_TIME, default="75s" + ): cv.positive_time_period_seconds, } ) .extend(cv.polling_component_schema("60s")) @@ -68,6 +72,8 @@ async def to_code(config): if CONF_AUTOMATIC_BASELINE_CALIBRATION in config: cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION])) + cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) + CALIBRATION_ACTION_SCHEMA = maybe_simple_id( { diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py new file mode 100644 index 0000000000..0862406e46 --- /dev/null +++ b/esphome/components/micro_wake_word/__init__.py @@ -0,0 +1,498 @@ +import hashlib +import json +import logging +from pathlib import Path +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 +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_ID, + CONF_MICROPHONE, + CONF_MODEL, + CONF_PASSWORD, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_REF, + CONF_REFRESH, + CONF_TYPE, + CONF_URL, + CONF_USERNAME, + TYPE_GIT, + TYPE_LOCAL, +) +from esphome.core import CORE, HexInt + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@kahrendt", "@jesserockz"] +DEPENDENCIES = ["microphone"] +DOMAIN = "micro_wake_word" + + +CONF_FEATURE_STEP_SIZE = "feature_step_size" +CONF_MODELS = "models" +CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" +CONF_PROBABILITY_CUTOFF = "probability_cutoff" +CONF_SLIDING_WINDOW_AVERAGE_SIZE = "sliding_window_average_size" +CONF_SLIDING_WINDOW_SIZE = "sliding_window_size" +CONF_TENSOR_ARENA_SIZE = "tensor_arena_size" +CONF_VAD = "vad" + +TYPE_HTTP = "http" + +micro_wake_word_ns = cg.esphome_ns.namespace("micro_wake_word") + +MicroWakeWord = micro_wake_word_ns.class_("MicroWakeWord", cg.Component) + +StartAction = micro_wake_word_ns.class_("StartAction", automation.Action) +StopAction = micro_wake_word_ns.class_("StopAction", automation.Action) + +IsRunningCondition = micro_wake_word_ns.class_( + "IsRunningCondition", automation.Condition +) + + +def _validate_json_filename(value): + value = cv.string(value) + if not value.endswith(".json"): + raise cv.Invalid("Manifest filename must end with .json") + return value + + +def _process_git_source(config): + repo_dir, _ = git.clone_or_update( + url=config[CONF_URL], + ref=config.get(CONF_REF), + refresh=config[CONF_REFRESH], + domain=DOMAIN, + username=config.get(CONF_USERNAME), + password=config.get(CONF_PASSWORD), + ) + + if not (repo_dir / config[CONF_FILE]).exists(): + raise cv.Invalid("File does not exist in repository") + + return config + + +CV_GIT_SCHEMA = cv.GIT_SCHEMA +if isinstance(CV_GIT_SCHEMA, dict): + CV_GIT_SCHEMA = cv.Schema(CV_GIT_SCHEMA) + +GIT_SCHEMA = cv.All( + CV_GIT_SCHEMA.extend( + { + cv.Required(CONF_FILE): _validate_json_filename, + cv.Optional(CONF_REFRESH, default="1d"): cv.All( + cv.string, cv.source_refresh + ), + } + ), + _process_git_source, +) + + +KEY_AUTHOR = "author" +KEY_MICRO = "micro" +KEY_MINIMUM_ESPHOME_VERSION = "minimum_esphome_version" +KEY_TRAINED_LANGUAGES = "trained_languages" +KEY_VERSION = "version" +KEY_WAKE_WORD = "wake_word" +KEY_WEBSITE = "website" + +MANIFEST_SCHEMA_V1 = cv.Schema( + { + cv.Required(CONF_TYPE): "micro", + cv.Required(KEY_WAKE_WORD): cv.string, + cv.Required(KEY_AUTHOR): cv.string, + cv.Required(KEY_WEBSITE): cv.url, + cv.Required(KEY_VERSION): cv.All(cv.int_, 1), + cv.Required(CONF_MODEL): cv.string, + cv.Required(KEY_MICRO): cv.Schema( + { + cv.Required(CONF_PROBABILITY_CUTOFF): cv.float_, + cv.Required(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int, + cv.Optional(KEY_MINIMUM_ESPHOME_VERSION): cv.All( + cv.version_number, cv.validate_esphome_version + ), + } + ), + } +) + +MANIFEST_SCHEMA_V2 = cv.Schema( + { + cv.Required(CONF_TYPE): "micro", + cv.Required(CONF_MODEL): cv.string, + cv.Required(KEY_AUTHOR): cv.string, + cv.Required(KEY_VERSION): cv.All(cv.int_, 2), + cv.Required(KEY_WAKE_WORD): cv.string, + cv.Required(KEY_TRAINED_LANGUAGES): cv.ensure_list(cv.string), + cv.Optional(KEY_WEBSITE): cv.url, + cv.Required(KEY_MICRO): cv.Schema( + { + cv.Required(CONF_FEATURE_STEP_SIZE): cv.int_range(min=0, max=30), + cv.Required(CONF_TENSOR_ARENA_SIZE): cv.int_, + cv.Required(CONF_PROBABILITY_CUTOFF): cv.float_, + cv.Required(CONF_SLIDING_WINDOW_SIZE): cv.positive_int, + cv.Required(KEY_MINIMUM_ESPHOME_VERSION): cv.All( + cv.version_number, cv.validate_esphome_version + ), + } + ), + } +) + + +def _compute_local_file_path(config: dict) -> Path: + url = config[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + return base_dir / key + + +def _convert_manifest_v1_to_v2(v1_manifest): + v2_manifest = v1_manifest.copy() + + v2_manifest[KEY_VERSION] = 2 + v2_manifest[KEY_MICRO][CONF_SLIDING_WINDOW_SIZE] = v1_manifest[KEY_MICRO][ + CONF_SLIDING_WINDOW_AVERAGE_SIZE + ] + del v2_manifest[KEY_MICRO][CONF_SLIDING_WINDOW_AVERAGE_SIZE] + + # Original Inception-based V1 manifest models require a minimum of 45672 bytes + v2_manifest[KEY_MICRO][CONF_TENSOR_ARENA_SIZE] = 45672 + + # Original Inception-based V1 manifest models use a 20 ms feature step size + v2_manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE] = 20 + + return v2_manifest + + +def _validate_manifest_version(manifest_data): + if manifest_version := manifest_data.get(KEY_VERSION): + if manifest_version == 1: + try: + MANIFEST_SCHEMA_V1(manifest_data) + except cv.Invalid as e: + raise cv.Invalid(f"Invalid manifest file: {e}") from e + elif manifest_version == 2: + try: + MANIFEST_SCHEMA_V2(manifest_data) + except cv.Invalid as e: + raise cv.Invalid(f"Invalid manifest file: {e}") from e + else: + raise cv.Invalid("Invalid manifest version") + else: + raise cv.Invalid("Invalid manifest file, missing 'version' key.") + + +def _process_http_source(config): + url = config[CONF_URL] + path = _compute_local_file_path(config) + + json_path = path / "manifest.json" + + json_contents = external_files.download_content(url, json_path) + + manifest_data = json.loads(json_contents) + if not isinstance(manifest_data, dict): + raise cv.Invalid("Manifest file must contain a JSON object") + + model = manifest_data[CONF_MODEL] + model_url = urljoin(url, model) + + model_path = path / model + + external_files.download_content(str(model_url), model_path) + + return config + + +HTTP_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.url, + }, + _process_http_source, +) + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.All(_validate_json_filename, cv.file_), + } +) + + +def _validate_source_model_name(value): + if not isinstance(value, str): + raise cv.Invalid("Model name must be a string") + + if value.endswith(".json"): + raise cv.Invalid("Model name must not end with .json") + + return MODEL_SOURCE_SCHEMA( + { + CONF_TYPE: TYPE_HTTP, + CONF_URL: f"https://github.com/esphome/micro-wake-word-models/raw/main/models/v2/{value}.json", + } + ) + + +def _validate_source_shorthand(value): + if not isinstance(value, str): + raise cv.Invalid("Shorthand only for strings") + + try: # Test for model name + return _validate_source_model_name(value) + except cv.Invalid: + pass + + try: # Test for local path + return MODEL_SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) + except cv.Invalid: + pass + + try: # Test for http url + return MODEL_SOURCE_SCHEMA({CONF_TYPE: TYPE_HTTP, CONF_URL: value}) + except cv.Invalid: + pass + + git_file = git.GitFile.from_shorthand(value) + + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: git_file.git_url, + CONF_FILE: git_file.filename, + } + if git_file.ref: + conf[CONF_REF] = git_file.ref + + try: + return MODEL_SOURCE_SCHEMA(conf) + except cv.Invalid as e: + raise cv.Invalid( + f"Could not find file '{git_file.filename}' in the repository. Please make sure it exists." + ) from e + + +MODEL_SOURCE_SCHEMA = cv.Any( + _validate_source_shorthand, + cv.typed_schema( + { + TYPE_GIT: GIT_SCHEMA, + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_HTTP: HTTP_SCHEMA, + } + ), + msg="Not a valid model name, local path, http(s) url, or github shorthand", +) + +MODEL_SCHEMA = cv.Schema( + { + cv.Optional(CONF_MODEL): MODEL_SOURCE_SCHEMA, + cv.Optional(CONF_PROBABILITY_CUTOFF): cv.percentage, + cv.Optional(CONF_SLIDING_WINDOW_SIZE): cv.positive_int, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) + +# Provide a default VAD model that could be overridden +VAD_MODEL_SCHEMA = MODEL_SCHEMA.extend( + cv.Schema( + { + cv.Optional( + CONF_MODEL, + default="vad", + ): MODEL_SOURCE_SCHEMA, + } + ) +) + + +def _maybe_empty_vad_schema(value): + # Idea borrowed from uart/__init__.py's ``maybe_empty_debug`` function. Accessed 2 July 2024. + # Loads a default VAD model without any parameters overridden. + if value is None: + value = {} + return VAD_MODEL_SCHEMA(value) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MicroWakeWord), + cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), + cv.Required(CONF_MODELS): cv.ensure_list( + cv.maybe_simple_value(MODEL_SCHEMA, key=CONF_MODEL) + ), + cv.Optional(CONF_ON_WAKE_WORD_DETECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_VAD): _maybe_empty_vad_schema, + cv.Optional(CONF_MODEL): cv.invalid( + f"The {CONF_MODEL} parameter has moved to be a list element under the {CONF_MODELS} parameter." + ), + cv.Optional(CONF_PROBABILITY_CUTOFF): cv.invalid( + f"The {CONF_PROBABILITY_CUTOFF} parameter has moved to be a list element under the {CONF_MODELS} parameter." + ), + cv.Optional(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.invalid( + f"The {CONF_SLIDING_WINDOW_AVERAGE_SIZE} parameter has been renamed to {CONF_SLIDING_WINDOW_SIZE} and moved to be a list element under the {CONF_MODELS} parameter." + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_with_esp_idf, +) + + +def _load_model_data(manifest_path: Path): + with open(manifest_path, encoding="utf-8") as f: + manifest = json.load(f) + + _validate_manifest_version(manifest) + + model_path = manifest_path.parent / manifest[CONF_MODEL] + + with open(model_path, "rb") as f: + model = f.read() + + if manifest.get(KEY_VERSION) == 1: + manifest = _convert_manifest_v1_to_v2(manifest) + + return manifest, model + + +def _model_config_to_manifest_data(model_config): + if model_config[CONF_TYPE] == TYPE_GIT: + # compute path to model file + key = f"{model_config[CONF_URL]}@{model_config.get(CONF_REF)}" + base_dir = Path(CORE.data_dir) / DOMAIN + h = hashlib.new("sha256") + h.update(key.encode()) + file: Path = base_dir / h.hexdigest()[:8] / model_config[CONF_FILE] + + elif model_config[CONF_TYPE] == TYPE_LOCAL: + file = Path(model_config[CONF_PATH]) + + elif model_config[CONF_TYPE] == TYPE_HTTP: + file = _compute_local_file_path(model_config) / "manifest.json" + + else: + raise ValueError("Unsupported config type: {model_config[CONF_TYPE]}") + + return _load_model_data(file) + + +def _feature_step_size_validate(config): + features_step_size = None + + for model_parameters in config[CONF_MODELS]: + model_config = model_parameters.get(CONF_MODEL) + manifest, _ = _model_config_to_manifest_data(model_config) + + model_step_size = manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE] + + if features_step_size is None: + features_step_size = model_step_size + elif features_step_size != model_step_size: + raise cv.Invalid("Cannot load models with different features step sizes.") + + +FINAL_VALIDATE_SCHEMA = _feature_step_size_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + mic = await cg.get_variable(config[CONF_MICROPHONE]) + cg.add(var.set_microphone(mic)) + + esp32.add_idf_component( + name="esp-tflite-micro", + repo="https://github.com/espressif/esp-tflite-micro", + ref="v1.3.1", + ) + # add esp-nn dependency for tflite-micro to work around https://github.com/espressif/esp-nn/issues/17 + # ...remove after switching to IDF 5.1.4+ + esp32.add_idf_component( + name="esp-nn", + repo="https://github.com/espressif/esp-nn", + ref="v1.1.0", + ) + + cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") + cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") + cg.add_build_flag("-DESP_NN") + + if on_wake_word_detection_config := config.get(CONF_ON_WAKE_WORD_DETECTED): + await automation.build_automation( + var.get_wake_word_detected_trigger(), + [(cg.std_string, "wake_word")], + on_wake_word_detection_config, + ) + + if vad_model := config.get(CONF_VAD): + cg.add_define("USE_MICRO_WAKE_WORD_VAD") + + # Use the general model loading code for the VAD codegen + config[CONF_MODELS].append(vad_model) + + for model_parameters in config[CONF_MODELS]: + model_config = model_parameters.get(CONF_MODEL) + data = [] + manifest, data = _model_config_to_manifest_data(model_config) + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(model_parameters[CONF_RAW_DATA_ID], rhs) + + probability_cutoff = model_parameters.get( + CONF_PROBABILITY_CUTOFF, manifest[KEY_MICRO][CONF_PROBABILITY_CUTOFF] + ) + sliding_window_size = model_parameters.get( + CONF_SLIDING_WINDOW_SIZE, + manifest[KEY_MICRO][CONF_SLIDING_WINDOW_SIZE], + ) + + if manifest[KEY_WAKE_WORD] == "vad": + cg.add( + var.add_vad_model( + prog_arr, + probability_cutoff, + sliding_window_size, + manifest[KEY_MICRO][CONF_TENSOR_ARENA_SIZE], + ) + ) + else: + cg.add( + var.add_wake_word_model( + prog_arr, + probability_cutoff, + sliding_window_size, + manifest[KEY_WAKE_WORD], + manifest[KEY_MICRO][CONF_TENSOR_ARENA_SIZE], + ) + ) + + cg.add(var.set_features_step_size(manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE])) + cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.1.0") + + +MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)}) + + +@register_action("micro_wake_word.start", StartAction, MICRO_WAKE_WORD_ACTION_SCHEMA) +@register_action("micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA) +@register_condition( + "micro_wake_word.is_running", IsRunningCondition, MICRO_WAKE_WORD_ACTION_SCHEMA +) +async def micro_wake_word_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/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp new file mode 100644 index 0000000000..b58c7ec434 --- /dev/null +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -0,0 +1,458 @@ +#include "micro_wake_word.h" +#include "streaming_model.h" + +#ifdef USE_ESP_IDF + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include + +#include +#include +#include + +#include + +namespace esphome { +namespace micro_wake_word { + +static const char *const TAG = "micro_wake_word"; + +static const size_t SAMPLE_RATE_HZ = 16000; // 16 kHz +static const size_t BUFFER_LENGTH = 64; // 0.064 seconds +static const size_t BUFFER_SIZE = SAMPLE_RATE_HZ / 1000 * BUFFER_LENGTH; +static const size_t INPUT_BUFFER_SIZE = 16 * SAMPLE_RATE_HZ / 1000; // 16ms * 16kHz / 1000ms + +float MicroWakeWord::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +static const LogString *micro_wake_word_state_to_string(State state) { + switch (state) { + case State::IDLE: + return LOG_STR("IDLE"); + case State::START_MICROPHONE: + return LOG_STR("START_MICROPHONE"); + case State::STARTING_MICROPHONE: + return LOG_STR("STARTING_MICROPHONE"); + case State::DETECTING_WAKE_WORD: + return LOG_STR("DETECTING_WAKE_WORD"); + case State::STOP_MICROPHONE: + return LOG_STR("STOP_MICROPHONE"); + case State::STOPPING_MICROPHONE: + return LOG_STR("STOPPING_MICROPHONE"); + default: + return LOG_STR("UNKNOWN"); + } +} + +void MicroWakeWord::dump_config() { + ESP_LOGCONFIG(TAG, "microWakeWord:"); + ESP_LOGCONFIG(TAG, " models:"); + for (auto &model : this->wake_word_models_) { + model.log_model_config(); + } +#ifdef USE_MICRO_WAKE_WORD_VAD + this->vad_model_->log_model_config(); +#endif +} + +void MicroWakeWord::setup() { + ESP_LOGCONFIG(TAG, "Setting up microWakeWord..."); + + if (!this->register_streaming_ops_(this->streaming_op_resolver_)) { + this->mark_failed(); + return; + } + + ESP_LOGCONFIG(TAG, "Micro Wake Word initialized"); + + this->frontend_config_.window.size_ms = FEATURE_DURATION_MS; + this->frontend_config_.window.step_size_ms = this->features_step_size_; + this->frontend_config_.filterbank.num_channels = PREPROCESSOR_FEATURE_SIZE; + this->frontend_config_.filterbank.lower_band_limit = 125.0; + this->frontend_config_.filterbank.upper_band_limit = 7500.0; + this->frontend_config_.noise_reduction.smoothing_bits = 10; + this->frontend_config_.noise_reduction.even_smoothing = 0.025; + this->frontend_config_.noise_reduction.odd_smoothing = 0.06; + this->frontend_config_.noise_reduction.min_signal_remaining = 0.05; + this->frontend_config_.pcan_gain_control.enable_pcan = 1; + this->frontend_config_.pcan_gain_control.strength = 0.95; + this->frontend_config_.pcan_gain_control.offset = 80.0; + this->frontend_config_.pcan_gain_control.gain_bits = 21; + this->frontend_config_.log_scale.enable_log = 1; + this->frontend_config_.log_scale.scale_shift = 6; +} + +void MicroWakeWord::add_wake_word_model(const uint8_t *model_start, float probability_cutoff, + size_t sliding_window_average_size, const std::string &wake_word, + size_t tensor_arena_size) { + this->wake_word_models_.emplace_back(model_start, probability_cutoff, sliding_window_average_size, wake_word, + tensor_arena_size); +} + +#ifdef USE_MICRO_WAKE_WORD_VAD +void MicroWakeWord::add_vad_model(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_size, + size_t tensor_arena_size) { + this->vad_model_ = make_unique(model_start, probability_cutoff, sliding_window_size, tensor_arena_size); +} +#endif + +void MicroWakeWord::loop() { + switch (this->state_) { + case State::IDLE: + break; + case State::START_MICROPHONE: + ESP_LOGD(TAG, "Starting Microphone"); + this->microphone_->start(); + this->set_state_(State::STARTING_MICROPHONE); + this->high_freq_.start(); + break; + case State::STARTING_MICROPHONE: + if (this->microphone_->is_running()) { + this->set_state_(State::DETECTING_WAKE_WORD); + } + break; + case State::DETECTING_WAKE_WORD: + while (!this->has_enough_samples_()) { + this->read_microphone_(); + } + this->update_model_probabilities_(); + if (this->detect_wake_words_()) { + ESP_LOGD(TAG, "Wake Word '%s' Detected", (this->detected_wake_word_).c_str()); + this->detected_ = true; + this->set_state_(State::STOP_MICROPHONE); + } + break; + case State::STOP_MICROPHONE: + ESP_LOGD(TAG, "Stopping Microphone"); + this->microphone_->stop(); + this->set_state_(State::STOPPING_MICROPHONE); + this->high_freq_.stop(); + this->unload_models_(); + this->deallocate_buffers_(); + break; + case State::STOPPING_MICROPHONE: + if (this->microphone_->is_stopped()) { + this->set_state_(State::IDLE); + if (this->detected_) { + this->wake_word_detected_trigger_->trigger(this->detected_wake_word_); + this->detected_ = false; + this->detected_wake_word_ = ""; + } + } + break; + } +} + +void MicroWakeWord::start() { + if (!this->is_ready()) { + ESP_LOGW(TAG, "Wake word detection can't start as the component hasn't been setup yet"); + return; + } + + if (this->is_failed()) { + ESP_LOGW(TAG, "Wake word component is marked as failed. Please check setup logs"); + return; + } + + if (!this->load_models_() || !this->allocate_buffers_()) { + ESP_LOGE(TAG, "Failed to load the wake word model(s) or allocate buffers"); + this->status_set_error(); + } else { + this->status_clear_error(); + } + + if (this->status_has_error()) { + ESP_LOGW(TAG, "Wake word component has an error. Please check logs"); + return; + } + + if (this->state_ != State::IDLE) { + ESP_LOGW(TAG, "Wake word is already running"); + return; + } + + this->reset_states_(); + this->set_state_(State::START_MICROPHONE); +} + +void MicroWakeWord::stop() { + if (this->state_ == State::IDLE) { + ESP_LOGW(TAG, "Wake word is already stopped"); + return; + } + if (this->state_ == State::STOPPING_MICROPHONE) { + ESP_LOGW(TAG, "Wake word is already stopping"); + return; + } + this->set_state_(State::STOP_MICROPHONE); +} + +void MicroWakeWord::set_state_(State state) { + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(micro_wake_word_state_to_string(this->state_)), + LOG_STR_ARG(micro_wake_word_state_to_string(state))); + this->state_ = state; +} + +size_t MicroWakeWord::read_microphone_() { + size_t bytes_read = this->microphone_->read(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t)); + if (bytes_read == 0) { + return 0; + } + + size_t bytes_free = this->ring_buffer_->free(); + + if (bytes_free < bytes_read) { + ESP_LOGW(TAG, + "Not enough free bytes in ring buffer to store incoming audio data (free bytes=%d, incoming bytes=%d). " + "Resetting the ring buffer. Wake word detection accuracy will be reduced.", + bytes_free, bytes_read); + + this->ring_buffer_->reset(); + } + + return this->ring_buffer_->write((void *) this->input_buffer_, bytes_read); +} + +bool MicroWakeWord::allocate_buffers_() { + ExternalRAMAllocator audio_samples_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + + if (this->input_buffer_ == nullptr) { + this->input_buffer_ = audio_samples_allocator.allocate(INPUT_BUFFER_SIZE * sizeof(int16_t)); + if (this->input_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate input buffer"); + return false; + } + } + + if (this->preprocessor_audio_buffer_ == nullptr) { + this->preprocessor_audio_buffer_ = audio_samples_allocator.allocate(this->new_samples_to_get_()); + if (this->preprocessor_audio_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the audio preprocessor's buffer."); + return false; + } + } + + if (this->ring_buffer_ == nullptr) { + this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t)); + if (this->ring_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate ring buffer"); + return false; + } + } + + return true; +} + +void MicroWakeWord::deallocate_buffers_() { + ExternalRAMAllocator audio_samples_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + audio_samples_allocator.deallocate(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t)); + this->input_buffer_ = nullptr; + audio_samples_allocator.deallocate(this->preprocessor_audio_buffer_, this->new_samples_to_get_()); + this->preprocessor_audio_buffer_ = nullptr; +} + +bool MicroWakeWord::load_models_() { + // Setup preprocesor feature generator + if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_, AUDIO_SAMPLE_FREQUENCY)) { + ESP_LOGD(TAG, "Failed to populate frontend state"); + FrontendFreeStateContents(&this->frontend_state_); + return false; + } + + // Setup streaming models + for (auto &model : this->wake_word_models_) { + if (!model.load_model(this->streaming_op_resolver_)) { + ESP_LOGE(TAG, "Failed to initialize a wake word model."); + return false; + } + } +#ifdef USE_MICRO_WAKE_WORD_VAD + if (!this->vad_model_->load_model(this->streaming_op_resolver_)) { + ESP_LOGE(TAG, "Failed to initialize VAD model."); + return false; + } +#endif + + return true; +} + +void MicroWakeWord::unload_models_() { + FrontendFreeStateContents(&this->frontend_state_); + + for (auto &model : this->wake_word_models_) { + model.unload_model(); + } +#ifdef USE_MICRO_WAKE_WORD_VAD + this->vad_model_->unload_model(); +#endif +} + +void MicroWakeWord::update_model_probabilities_() { + int8_t audio_features[PREPROCESSOR_FEATURE_SIZE]; + + if (!this->generate_features_for_window_(audio_features)) { + return; + } + + // Increase the counter since the last positive detection + this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0); + + for (auto &model : this->wake_word_models_) { + // Perform inference + model.perform_streaming_inference(audio_features); + } +#ifdef USE_MICRO_WAKE_WORD_VAD + this->vad_model_->perform_streaming_inference(audio_features); +#endif +} + +bool MicroWakeWord::detect_wake_words_() { + // Verify we have processed samples since the last positive detection + if (this->ignore_windows_ < 0) { + return false; + } + +#ifdef USE_MICRO_WAKE_WORD_VAD + bool vad_state = this->vad_model_->determine_detected(); +#endif + + for (auto &model : this->wake_word_models_) { + if (model.determine_detected()) { +#ifdef USE_MICRO_WAKE_WORD_VAD + if (vad_state) { +#endif + this->detected_wake_word_ = model.get_wake_word(); + return true; +#ifdef USE_MICRO_WAKE_WORD_VAD + } else { + ESP_LOGD(TAG, "Wake word model predicts %s, but VAD model doesn't.", model.get_wake_word().c_str()); + } +#endif + } + } + + return false; +} + +bool MicroWakeWord::has_enough_samples_() { + return this->ring_buffer_->available() >= + (this->features_step_size_ * (AUDIO_SAMPLE_FREQUENCY / 1000)) * sizeof(int16_t); +} + +bool MicroWakeWord::generate_features_for_window_(int8_t features[PREPROCESSOR_FEATURE_SIZE]) { + // Ensure we have enough new audio samples in the ring buffer for a full window + if (!this->has_enough_samples_()) { + return false; + } + + size_t bytes_read = this->ring_buffer_->read((void *) (this->preprocessor_audio_buffer_), + this->new_samples_to_get_() * sizeof(int16_t), pdMS_TO_TICKS(200)); + + if (bytes_read == 0) { + ESP_LOGE(TAG, "Could not read data from Ring Buffer"); + } else if (bytes_read < this->new_samples_to_get_() * sizeof(int16_t)) { + ESP_LOGD(TAG, "Partial Read of Data by Model"); + ESP_LOGD(TAG, "Could only read %d bytes when required %d bytes ", bytes_read, + (int) (this->new_samples_to_get_() * sizeof(int16_t))); + return false; + } + + size_t num_samples_read; + struct FrontendOutput frontend_output = FrontendProcessSamples( + &this->frontend_state_, this->preprocessor_audio_buffer_, this->new_samples_to_get_(), &num_samples_read); + + for (size_t i = 0; i < frontend_output.size; ++i) { + // These scaling values are set to match the TFLite audio frontend int8 output. + // The feature pipeline outputs 16-bit signed integers in roughly a 0 to 670 + // range. In training, these are then arbitrarily divided by 25.6 to get + // float values in the rough range of 0.0 to 26.0. This scaling is performed + // for historical reasons, to match up with the output of other feature + // generators. + // The process is then further complicated when we quantize the model. This + // means we have to scale the 0.0 to 26.0 real values to the -128 to 127 + // signed integer numbers. + // All this means that to get matching values from our integer feature + // output into the tensor input, we have to perform: + // input = (((feature / 25.6) / 26.0) * 256) - 128 + // To simplify this and perform it in 32-bit integer math, we rearrange to: + // input = (feature * 256) / (25.6 * 26.0) - 128 + constexpr int32_t value_scale = 256; + constexpr int32_t value_div = 666; // 666 = 25.6 * 26.0 after rounding + int32_t value = ((frontend_output.values[i] * value_scale) + (value_div / 2)) / value_div; + value -= 128; + if (value < -128) { + value = -128; + } + if (value > 127) { + value = 127; + } + features[i] = value; + } + + return true; +} + +void MicroWakeWord::reset_states_() { + ESP_LOGD(TAG, "Resetting buffers and probabilities"); + this->ring_buffer_->reset(); + this->ignore_windows_ = -MIN_SLICES_BEFORE_DETECTION; + for (auto &model : this->wake_word_models_) { + model.reset_probabilities(); + } +#ifdef USE_MICRO_WAKE_WORD_VAD + this->vad_model_->reset_probabilities(); +#endif +} + +bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver) { + if (op_resolver.AddCallOnce() != kTfLiteOk) + return false; + if (op_resolver.AddVarHandle() != kTfLiteOk) + return false; + if (op_resolver.AddReshape() != kTfLiteOk) + return false; + if (op_resolver.AddReadVariable() != kTfLiteOk) + return false; + if (op_resolver.AddStridedSlice() != kTfLiteOk) + return false; + if (op_resolver.AddConcatenation() != kTfLiteOk) + return false; + if (op_resolver.AddAssignVariable() != kTfLiteOk) + return false; + if (op_resolver.AddConv2D() != kTfLiteOk) + return false; + if (op_resolver.AddMul() != kTfLiteOk) + return false; + if (op_resolver.AddAdd() != kTfLiteOk) + return false; + if (op_resolver.AddMean() != kTfLiteOk) + return false; + if (op_resolver.AddFullyConnected() != kTfLiteOk) + return false; + if (op_resolver.AddLogistic() != kTfLiteOk) + return false; + if (op_resolver.AddQuantize() != kTfLiteOk) + return false; + if (op_resolver.AddDepthwiseConv2D() != kTfLiteOk) + return false; + if (op_resolver.AddAveragePool2D() != kTfLiteOk) + return false; + if (op_resolver.AddMaxPool2D() != kTfLiteOk) + return false; + if (op_resolver.AddPad() != kTfLiteOk) + return false; + if (op_resolver.AddPack() != kTfLiteOk) + return false; + if (op_resolver.AddSplitV() != kTfLiteOk) + return false; + + return true; +} + +} // namespace micro_wake_word +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h new file mode 100644 index 0000000000..0c805b75fc --- /dev/null +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -0,0 +1,176 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "preprocessor_settings.h" +#include "streaming_model.h" + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/ring_buffer.h" + +#include "esphome/components/microphone/microphone.h" + +#include + +#include +#include +#include + +namespace esphome { +namespace micro_wake_word { + +enum State { + IDLE, + START_MICROPHONE, + STARTING_MICROPHONE, + DETECTING_WAKE_WORD, + STOP_MICROPHONE, + STOPPING_MICROPHONE, +}; + +// The number of audio slices to process before accepting a positive detection +static const uint8_t MIN_SLICES_BEFORE_DETECTION = 74; + +class MicroWakeWord : public Component { + public: + void setup() override; + void loop() override; + float get_setup_priority() const override; + void dump_config() override; + + void start(); + void stop(); + + bool is_running() const { return this->state_ != State::IDLE; } + + void set_features_step_size(uint8_t step_size) { this->features_step_size_ = step_size; } + + void set_microphone(microphone::Microphone *microphone) { this->microphone_ = microphone; } + + Trigger *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } + + void add_wake_word_model(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_average_size, + const std::string &wake_word, size_t tensor_arena_size); + +#ifdef USE_MICRO_WAKE_WORD_VAD + void add_vad_model(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_size, + size_t tensor_arena_size); +#endif + + protected: + microphone::Microphone *microphone_{nullptr}; + Trigger *wake_word_detected_trigger_ = new Trigger(); + State state_{State::IDLE}; + HighFrequencyLoopRequester high_freq_; + + std::unique_ptr ring_buffer_; + + std::vector wake_word_models_; + +#ifdef USE_MICRO_WAKE_WORD_VAD + std::unique_ptr vad_model_; +#endif + + tflite::MicroMutableOpResolver<20> streaming_op_resolver_; + + // Audio frontend handles generating spectrogram features + struct FrontendConfig frontend_config_; + struct FrontendState frontend_state_; + + // When the wake word detection first starts, we ignore this many audio + // feature slices before accepting a positive detection + int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION}; + + uint8_t features_step_size_; + + // Stores audio read from the microphone before being added to the ring buffer. + int16_t *input_buffer_{nullptr}; + // Stores audio to be fed into the audio frontend for generating features. + int16_t *preprocessor_audio_buffer_{nullptr}; + + bool detected_{false}; + std::string detected_wake_word_{""}; + + void set_state_(State state); + + /// @brief Tests if there are enough samples in the ring buffer to generate new features. + /// @return True if enough samples, false otherwise. + bool has_enough_samples_(); + + /** Reads audio from microphone into the ring buffer + * + * Audio data (16000 kHz with int16 samples) is read into the input_buffer_. + * Verifies the ring buffer has enough space for all audio data. If not, it logs + * a warning and resets the ring buffer entirely. + * @return Number of bytes written to the ring buffer + */ + size_t read_microphone_(); + + /// @brief Allocates memory for input_buffer_, preprocessor_audio_buffer_, and ring_buffer_ + /// @return True if successful, false otherwise + bool allocate_buffers_(); + + /// @brief Frees memory allocated for input_buffer_ and preprocessor_audio_buffer_ + void deallocate_buffers_(); + + /// @brief Loads streaming models and prepares the feature generation frontend + /// @return True if successful, false otherwise + bool load_models_(); + + /// @brief Deletes each model's TFLite interpreters and frees tensor arena memory. Frees memory used by the feature + /// generation frontend. + void unload_models_(); + + /** Performs inference with each configured model + * + * If enough audio samples are available, it will generate one slice of new features. + * It then loops through and performs inference with each of the loaded models. + */ + void update_model_probabilities_(); + + /** Checks every model's recent probabilities to determine if the wake word has been predicted + * + * Verifies the models have processed enough new samples for accurate predictions. + * Sets detected_wake_word_ to the wake word, if one is detected. + * @return True if a wake word is predicted, false otherwise + */ + bool detect_wake_words_(); + + /** Generates features for a window of audio samples + * + * Reads samples from the ring buffer and feeds them into the preprocessor frontend. + * Adapted from TFLite microspeech frontend. + * @param features int8_t array to store the audio features + * @return True if successful, false otherwise. + */ + bool generate_features_for_window_(int8_t features[PREPROCESSOR_FEATURE_SIZE]); + + /// @brief Resets the ring buffer, ignore_windows_, and sliding window probabilities + void reset_states_(); + + /// @brief Returns true if successfully registered the streaming model's TensorFlow operations + bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver); + + inline uint16_t new_samples_to_get_() { return (this->features_step_size_ * (AUDIO_SAMPLE_FREQUENCY / 1000)); } +}; + +template class StartAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->start(); } +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop(); } +}; + +template class IsRunningCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_running(); } +}; + +} // namespace micro_wake_word +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/micro_wake_word/preprocessor_settings.h b/esphome/components/micro_wake_word/preprocessor_settings.h new file mode 100644 index 0000000000..03f4fb5230 --- /dev/null +++ b/esphome/components/micro_wake_word/preprocessor_settings.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include + +namespace esphome { +namespace micro_wake_word { + +// The number of features the audio preprocessor generates per slice +static const uint8_t PREPROCESSOR_FEATURE_SIZE = 40; +// Duration of each slice used as input into the preprocessor +static const uint8_t FEATURE_DURATION_MS = 30; +// Audio sample frequency in hertz +static const uint16_t AUDIO_SAMPLE_FREQUENCY = 16000; + +} // namespace micro_wake_word +} // namespace esphome + +#endif diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp new file mode 100644 index 0000000000..d0d2e2df05 --- /dev/null +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -0,0 +1,191 @@ +#ifdef USE_ESP_IDF + +#include "streaming_model.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +static const char *const TAG = "micro_wake_word"; + +namespace esphome { +namespace micro_wake_word { + +void WakeWordModel::log_model_config() { + ESP_LOGCONFIG(TAG, " - Wake Word: %s", this->wake_word_.c_str()); + ESP_LOGCONFIG(TAG, " Probability cutoff: %.3f", this->probability_cutoff_); + ESP_LOGCONFIG(TAG, " Sliding window size: %d", this->sliding_window_size_); +} + +void VADModel::log_model_config() { + ESP_LOGCONFIG(TAG, " - VAD Model"); + ESP_LOGCONFIG(TAG, " Probability cutoff: %.3f", this->probability_cutoff_); + ESP_LOGCONFIG(TAG, " Sliding window size: %d", this->sliding_window_size_); +} + +bool StreamingModel::load_model(tflite::MicroMutableOpResolver<20> &op_resolver) { + ExternalRAMAllocator arena_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + + if (this->tensor_arena_ == nullptr) { + this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_); + if (this->tensor_arena_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena."); + return false; + } + } + + if (this->var_arena_ == nullptr) { + this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE); + if (this->var_arena_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the streaming model's variable tensor arena."); + return false; + } + this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20); + } + + const tflite::Model *model = tflite::GetModel(this->model_start_); + if (model->version() != TFLITE_SCHEMA_VERSION) { + ESP_LOGE(TAG, "Streaming model's schema is not supported"); + return false; + } + + if (this->interpreter_ == nullptr) { + this->interpreter_ = make_unique( + tflite::GetModel(this->model_start_), op_resolver, this->tensor_arena_, this->tensor_arena_size_, this->mrv_); + if (this->interpreter_->AllocateTensors() != kTfLiteOk) { + ESP_LOGE(TAG, "Failed to allocate tensors for the streaming model"); + return false; + } + + // Verify input tensor matches expected values + // Dimension 3 will represent the first layer stride, so skip it may vary + TfLiteTensor *input = this->interpreter_->input(0); + if ((input->dims->size != 3) || (input->dims->data[0] != 1) || + (input->dims->data[2] != PREPROCESSOR_FEATURE_SIZE)) { + ESP_LOGE(TAG, "Streaming model tensor input dimensions has improper dimensions."); + return false; + } + + if (input->type != kTfLiteInt8) { + ESP_LOGE(TAG, "Streaming model tensor input is not int8."); + return false; + } + + // Verify output tensor matches expected values + TfLiteTensor *output = this->interpreter_->output(0); + if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) { + ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1."); + } + + if (output->type != kTfLiteUInt8) { + ESP_LOGE(TAG, "Streaming model tensor output is not uint8."); + return false; + } + } + + return true; +} + +void StreamingModel::unload_model() { + this->interpreter_.reset(); + + ExternalRAMAllocator arena_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + + arena_allocator.deallocate(this->tensor_arena_, this->tensor_arena_size_); + this->tensor_arena_ = nullptr; + arena_allocator.deallocate(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->var_arena_ = nullptr; +} + +bool StreamingModel::perform_streaming_inference(const int8_t features[PREPROCESSOR_FEATURE_SIZE]) { + if (this->interpreter_ != nullptr) { + TfLiteTensor *input = this->interpreter_->input(0); + + std::memmove( + (int8_t *) (tflite::GetTensorData(input)) + PREPROCESSOR_FEATURE_SIZE * this->current_stride_step_, + features, PREPROCESSOR_FEATURE_SIZE); + ++this->current_stride_step_; + + uint8_t stride = this->interpreter_->input(0)->dims->data[1]; + + if (this->current_stride_step_ >= stride) { + this->current_stride_step_ = 0; + + TfLiteStatus invoke_status = this->interpreter_->Invoke(); + if (invoke_status != kTfLiteOk) { + ESP_LOGW(TAG, "Streaming interpreter invoke failed"); + return false; + } + + TfLiteTensor *output = this->interpreter_->output(0); + + ++this->last_n_index_; + if (this->last_n_index_ == this->sliding_window_size_) + this->last_n_index_ = 0; + this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0]; // probability; + } + return true; + } + ESP_LOGE(TAG, "Streaming interpreter is not initialized."); + return false; +} + +void StreamingModel::reset_probabilities() { + for (auto &prob : this->recent_streaming_probabilities_) { + prob = 0; + } +} + +WakeWordModel::WakeWordModel(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_average_size, + const std::string &wake_word, size_t tensor_arena_size) { + this->model_start_ = model_start; + this->probability_cutoff_ = probability_cutoff; + this->sliding_window_size_ = sliding_window_average_size; + this->recent_streaming_probabilities_.resize(sliding_window_average_size, 0); + this->wake_word_ = wake_word; + this->tensor_arena_size_ = tensor_arena_size; +}; + +bool WakeWordModel::determine_detected() { + uint32_t sum = 0; + for (auto &prob : this->recent_streaming_probabilities_) { + sum += prob; + } + + float sliding_window_average = static_cast(sum) / static_cast(255 * this->sliding_window_size_); + + // Detect the wake word if the sliding window average is above the cutoff + if (sliding_window_average > this->probability_cutoff_) { + ESP_LOGD(TAG, "The '%s' model sliding average probability is %.3f and most recent probability is %.3f", + this->wake_word_.c_str(), sliding_window_average, + this->recent_streaming_probabilities_[this->last_n_index_] / (255.0)); + return true; + } + return false; +} + +VADModel::VADModel(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_size, + size_t tensor_arena_size) { + this->model_start_ = model_start; + this->probability_cutoff_ = probability_cutoff; + this->sliding_window_size_ = sliding_window_size; + this->recent_streaming_probabilities_.resize(sliding_window_size, 0); + this->tensor_arena_size_ = tensor_arena_size; +}; + +bool VADModel::determine_detected() { + uint32_t sum = 0; + for (auto &prob : this->recent_streaming_probabilities_) { + sum += prob; + } + + float sliding_window_average = static_cast(sum) / static_cast(255 * this->sliding_window_size_); + + return sliding_window_average > this->probability_cutoff_; +} + +} // namespace micro_wake_word +} // namespace esphome + +#endif diff --git a/esphome/components/micro_wake_word/streaming_model.h b/esphome/components/micro_wake_word/streaming_model.h new file mode 100644 index 0000000000..0d85579f35 --- /dev/null +++ b/esphome/components/micro_wake_word/streaming_model.h @@ -0,0 +1,84 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "preprocessor_settings.h" + +#include +#include +#include + +namespace esphome { +namespace micro_wake_word { + +static const uint32_t STREAMING_MODEL_VARIABLE_ARENA_SIZE = 1024; + +class StreamingModel { + public: + virtual void log_model_config() = 0; + virtual bool determine_detected() = 0; + + bool perform_streaming_inference(const int8_t features[PREPROCESSOR_FEATURE_SIZE]); + + /// @brief Sets all recent_streaming_probabilities to 0 + void reset_probabilities(); + + /// @brief Allocates tensor and variable arenas and sets up the model interpreter + /// @param op_resolver MicroMutableOpResolver object that must exist until the model is unloaded + /// @return True if successful, false otherwise + bool load_model(tflite::MicroMutableOpResolver<20> &op_resolver); + + /// @brief Destroys the TFLite interpreter and frees the tensor and variable arenas' memory + void unload_model(); + + protected: + uint8_t current_stride_step_{0}; + + float probability_cutoff_; + size_t sliding_window_size_; + size_t last_n_index_{0}; + size_t tensor_arena_size_; + std::vector recent_streaming_probabilities_; + + const uint8_t *model_start_; + uint8_t *tensor_arena_{nullptr}; + uint8_t *var_arena_{nullptr}; + std::unique_ptr interpreter_; + tflite::MicroResourceVariables *mrv_{nullptr}; + tflite::MicroAllocator *ma_{nullptr}; +}; + +class WakeWordModel final : public StreamingModel { + public: + WakeWordModel(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_average_size, + const std::string &wake_word, size_t tensor_arena_size); + + void log_model_config() override; + + /// @brief Checks for the wake word by comparing the mean probability in the sliding window with the probability + /// cutoff + /// @return True if wake word is detected, false otherwise + bool determine_detected() override; + + const std::string &get_wake_word() const { return this->wake_word_; } + + protected: + std::string wake_word_; +}; + +class VADModel final : public StreamingModel { + public: + VADModel(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_size, size_t tensor_arena_size); + + void log_model_config() override; + + /// @brief Checks for voice activity by comparing the max probability in the sliding window with the probability + /// cutoff + /// @return True if voice activity is detected, false otherwise + bool determine_detected() override; +}; + +} // namespace micro_wake_word +} // namespace esphome + +#endif diff --git a/esphome/components/micronova/__init__.py b/esphome/components/micronova/__init__.py new file mode 100644 index 0000000000..bd253f8ebd --- /dev/null +++ b/esphome/components/micronova/__init__.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import uart +from esphome.const import ( + CONF_ID, +) + +CODEOWNERS = ["@jorre05"] + +DEPENDENCIES = ["uart"] + +CONF_MICRONOVA_ID = "micronova_id" +CONF_ENABLE_RX_PIN = "enable_rx_pin" +CONF_MEMORY_LOCATION = "memory_location" +CONF_MEMORY_ADDRESS = "memory_address" + +micronova_ns = cg.esphome_ns.namespace("micronova") + +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.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) + .extend(cv.polling_component_schema("60s")) +) + + +def MICRONOVA_LISTENER_SCHEMA(default_memory_location, default_memory_address): + return 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(), + } + ) + + +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) + enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN]) + cg.add(var.set_enable_rx_pin(enable_rx_pin)) diff --git a/esphome/components/micronova/button/__init__.py b/esphome/components/micronova/button/__init__.py new file mode 100644 index 0000000000..442f69c08b --- /dev/null +++ b/esphome/components/micronova/button/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button + +from .. import ( + MicroNova, + MicroNovaFunctions, + CONF_MICRONOVA_ID, + CONF_MEMORY_LOCATION, + CONF_MEMORY_ADDRESS, + MICRONOVA_LISTENER_SCHEMA, + micronova_ns, +) + +MicroNovaButton = micronova_ns.class_("MicroNovaButton", button.Button, cg.Component) + +CONF_CUSTOM_BUTTON = "custom_button" +CONF_MEMORY_DATA = "memory_data" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova), + cv.Optional(CONF_CUSTOM_BUTTON): button.button_schema( + MicroNovaButton, + ) + .extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0xA0, default_memory_address=0x7D + ) + ) + .extend({cv.Required(CONF_MEMORY_DATA): cv.hex_int_range()}), + } +) + + +async def to_code(config): + mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) + + 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_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 new file mode 100644 index 0000000000..c1903fd878 --- /dev/null +++ b/esphome/components/micronova/button/micronova_button.cpp @@ -0,0 +1,18 @@ +#include "micronova_button.h" + +namespace esphome { +namespace 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(); +} + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/button/micronova_button.h b/esphome/components/micronova/button/micronova_button.h new file mode 100644 index 0000000000..77649051d6 --- /dev/null +++ b/esphome/components/micronova/button/micronova_button.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/micronova/micronova.h" +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace 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 set_memory_data(uint8_t f) { this->memory_data_ = f; } + uint8_t get_memory_data() { return this->memory_data_; } + + protected: + void press_action() override; +}; + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/micronova.cpp b/esphome/components/micronova/micronova.cpp new file mode 100644 index 0000000000..b96798ed12 --- /dev/null +++ b/esphome/components/micronova/micronova.cpp @@ -0,0 +1,148 @@ +#include "micronova.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace micronova { + +void MicroNova::setup() { + if (this->enable_rx_pin_ != nullptr) { + this->enable_rx_pin_->setup(); + this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->enable_rx_pin_->digital_write(false); + } + this->current_transmission_.request_transmission_time = millis(); + this->current_transmission_.memory_location = 0; + this->current_transmission_.memory_address = 0; + this->current_transmission_.reply_pending = false; + this->current_transmission_.initiating_listener = nullptr; +} + +void MicroNova::dump_config() { + ESP_LOGCONFIG(TAG, "MicroNova:"); + 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"); + for (auto &mv_listener : this->micronova_listeners_) { + mv_listener->set_needs_update(true); + } +} + +void MicroNova::loop() { + // Only read one sensor that needs update per loop + // If STOVE_REPLY_DELAY time has passed since last loop() + // check for a reply from the stove + if ((this->current_transmission_.reply_pending) && + (millis() - this->current_transmission_.request_transmission_time > STOVE_REPLY_DELAY)) { + int stove_reply_value = this->read_stove_reply(); + if (this->current_transmission_.initiating_listener != nullptr) { + this->current_transmission_.initiating_listener->process_value_from_stove(stove_reply_value); + this->current_transmission_.initiating_listener = nullptr; + } + this->current_transmission_.reply_pending = false; + return; + } else if (!this->current_transmission_.reply_pending) { + for (auto &mv_listener : this->micronova_listeners_) { + if (mv_listener->get_needs_update()) { + mv_listener->set_needs_update(false); + this->current_transmission_.initiating_listener = mv_listener; + mv_listener->request_value_from_stove(); + return; + } + } + } +} + +void MicroNova::request_address(uint8_t location, uint8_t address, MicroNovaSensorListener *listener) { + uint8_t write_data[2] = {0, 0}; + uint8_t trash_rx; + + if (this->reply_pending_mutex_.try_lock()) { + // clear rx buffer. + // Stove hickups may cause late replies in the rx + while (this->available()) { + this->read_byte(&trash_rx); + ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx); + } + + write_data[0] = location; + write_data[1] = address; + ESP_LOGV(TAG, "Request from stove [%02X,%02X]", write_data[0], write_data[1]); + + this->enable_rx_pin_->digital_write(true); + this->write_array(write_data, 2); + this->flush(); + this->enable_rx_pin_->digital_write(false); + + this->current_transmission_.request_transmission_time = millis(); + this->current_transmission_.memory_location = location; + this->current_transmission_.memory_address = address; + this->current_transmission_.reply_pending = true; + this->current_transmission_.initiating_listener = listener; + } else { + ESP_LOGE(TAG, "Reply is pending, skipping read request"); + } +} + +int MicroNova::read_stove_reply() { + uint8_t reply_data[2] = {0, 0}; + uint8_t checksum = 0; + + // assert enable_rx_pin is false + this->read_array(reply_data, 2); + + this->reply_pending_mutex_.unlock(); + ESP_LOGV(TAG, "Reply from stove [%02X,%02X]", reply_data[0], reply_data[1]); + + checksum = ((uint16_t) this->current_transmission_.memory_location + + (uint16_t) this->current_transmission_.memory_address + (uint16_t) reply_data[1]) & + 0xFF; + if (reply_data[0] != checksum) { + ESP_LOGE(TAG, "Checksum missmatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X", + this->current_transmission_.memory_location, this->current_transmission_.memory_address, reply_data[0], + reply_data[1], checksum, reply_data[0]); + return -1; + } + return ((int) reply_data[1]); +} + +void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) { + uint8_t write_data[4] = {0, 0, 0, 0}; + uint16_t checksum = 0; + + if (this->reply_pending_mutex_.try_lock()) { + write_data[0] = location; + write_data[1] = address; + write_data[2] = data; + + checksum = ((uint16_t) write_data[0] + (uint16_t) write_data[1] + (uint16_t) write_data[2]) & 0xFF; + write_data[3] = checksum; + + ESP_LOGV(TAG, "Write 4 bytes [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2], write_data[3]); + + this->enable_rx_pin_->digital_write(true); + this->write_array(write_data, 4); + this->flush(); + this->enable_rx_pin_->digital_write(false); + + this->current_transmission_.request_transmission_time = millis(); + this->current_transmission_.memory_location = location; + this->current_transmission_.memory_address = address; + this->current_transmission_.reply_pending = true; + this->current_transmission_.initiating_listener = nullptr; + } else { + ESP_LOGE(TAG, "Reply is pending, skipping write"); + } +} + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/micronova.h b/esphome/components/micronova/micronova.h new file mode 100644 index 0000000000..aebef277e5 --- /dev/null +++ b/esphome/components/micronova/micronova.h @@ -0,0 +1,164 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/log.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace 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; + +////////////////////////////////////////////////////////////////////// +// Interface classes. +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_; } + + protected: + MicroNova *micronova_{nullptr}; + MicroNovaFunctions function_ = MicroNovaFunctions::STOVE_FUNCTION_VOID; + uint8_t memory_location_ = 0; + uint8_t memory_address_ = 0; +}; + +class MicroNovaSensorListener : public MicroNovaBaseListener { + public: + MicroNovaSensorListener() {} + MicroNovaSensorListener(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; +}; + +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_; } + + 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) {} + + protected: + uint8_t memory_data_ = 0; +}; + +///////////////////////////////////////////////////////////////////// +// Main component class +class MicroNova : public PollingComponent, 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 request_address(uint8_t location, uint8_t address, MicroNovaSensorListener *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 { + uint32_t request_transmission_time; + uint8_t memory_location; + uint8_t memory_address; + bool reply_pending; + MicroNovaSensorListener *initiating_listener; + }; + + Mutex reply_pending_mutex_; + MicroNovaSerialTransmission current_transmission_; + + std::vector micronova_listeners_{}; + MicroNovaSwitchListener *stove_switch_{nullptr}; +}; + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/number/__init__.py b/esphome/components/micronova/number/__init__.py new file mode 100644 index 0000000000..7124bf50d0 --- /dev/null +++ b/esphome/components/micronova/number/__init__.py @@ -0,0 +1,110 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + UNIT_CELSIUS, + CONF_STEP, +) + +from .. import ( + MicroNova, + MicroNovaFunctions, + CONF_MICRONOVA_ID, + CONF_MEMORY_LOCATION, + CONF_MEMORY_ADDRESS, + MICRONOVA_LISTENER_SCHEMA, + micronova_ns, +) + +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) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova), + cv.Optional(CONF_THERMOSTAT_TEMPERATURE): number.number_schema( + MicroNovaNumber, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ) + .extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x20, default_memory_address=0x7D + ) + ) + .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( + {cv.Optional(CONF_MEMORY_WRITE_LOCATION, default=0xA0): cv.hex_int_range()} + ), + } +) + + +async def to_code(config): + mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) + + if thermostat_temperature_config := config.get(CONF_THERMOSTAT_TEMPERATURE): + numb = await number.new_number( + thermostat_temperature_config, + min_value=0, + max_value=40, + step=thermostat_temperature_config.get(CONF_STEP), + ) + 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) + ) + + if power_level_config := config.get(CONF_POWER_LEVEL): + numb = await number.new_number( + power_level_config, + min_value=1, + max_value=5, + step=1, + ) + 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 new file mode 100644 index 0000000000..244eb7ee9f --- /dev/null +++ b/esphome/components/micronova/number/micronova_number.cpp @@ -0,0 +1,45 @@ +#include "micronova_number.h" + +namespace esphome { +namespace 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; + } + this->publish_state(new_sensor_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; + } + this->micronova_->write_address(this->memory_write_location_, this->memory_address_, new_number); + this->micronova_->update(); +} + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/number/micronova_number.h b/esphome/components/micronova/number/micronova_number.h new file mode 100644 index 0000000000..49c6358255 --- /dev/null +++ b/esphome/components/micronova/number/micronova_number.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/micronova/micronova.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace micronova { + +class MicroNovaNumber : public number::Number, public MicroNovaSensorListener { + public: + MicroNovaNumber() {} + MicroNovaNumber(MicroNova *m) : MicroNovaSensorListener(m) {} + void dump_config() override { LOG_NUMBER("", "Micronova number", this); } + 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_; } + + protected: + uint8_t memory_write_location_ = 0; +}; + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/sensor/__init__.py b/esphome/components/micronova/sensor/__init__.py new file mode 100644 index 0000000000..32e42f3888 --- /dev/null +++ b/esphome/components/micronova/sensor/__init__.py @@ -0,0 +1,172 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_REVOLUTIONS_PER_MINUTE, +) + +from .. import ( + MicroNova, + MicroNovaFunctions, + CONF_MICRONOVA_ID, + CONF_MEMORY_LOCATION, + CONF_MEMORY_ADDRESS, + MICRONOVA_LISTENER_SCHEMA, + micronova_ns, +) + +UNIT_BAR = "bar" + +MicroNovaSensor = micronova_ns.class_("MicroNovaSensor", sensor.Sensor, cg.Component) + +CONF_ROOM_TEMPERATURE = "room_temperature" +CONF_FUMES_TEMPERATURE = "fumes_temperature" +CONF_STOVE_POWER = "stove_power" +CONF_FAN_SPEED = "fan_speed" +CONF_WATER_TEMPERATURE = "water_temperature" +CONF_WATER_PRESSURE = "water_pressure" +CONF_MEMORY_ADDRESS_SENSOR = "memory_address_sensor" +CONF_FAN_RPM_OFFSET = "fan_rpm_offset" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova), + cv.Optional(CONF_ROOM_TEMPERATURE): sensor.sensor_schema( + MicroNovaSensor, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ).extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x00, default_memory_address=0x01 + ) + ), + cv.Optional(CONF_FUMES_TEMPERATURE): sensor.sensor_schema( + MicroNovaSensor, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ).extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x00, default_memory_address=0x5A + ) + ), + cv.Optional(CONF_STOVE_POWER): sensor.sensor_schema( + MicroNovaSensor, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=0, + ).extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x00, default_memory_address=0x34 + ) + ), + cv.Optional(CONF_FAN_SPEED): sensor.sensor_schema( + MicroNovaSensor, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + ) + .extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x00, default_memory_address=0x37 + ) + ) + .extend( + {cv.Optional(CONF_FAN_RPM_OFFSET, default=0): cv.int_range(min=0, max=255)} + ), + cv.Optional(CONF_WATER_TEMPERATURE): sensor.sensor_schema( + MicroNovaSensor, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ).extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x00, default_memory_address=0x3B + ) + ), + cv.Optional(CONF_WATER_PRESSURE): sensor.sensor_schema( + MicroNovaSensor, + unit_of_measurement=UNIT_BAR, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ).extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x00, default_memory_address=0x3C + ) + ), + cv.Optional(CONF_MEMORY_ADDRESS_SENSOR): sensor.sensor_schema( + MicroNovaSensor, + ).extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x00, default_memory_address=0x00 + ) + ), + } +) + + +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)) + + 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)) + 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 new file mode 100644 index 0000000000..3f0c0feaf8 --- /dev/null +++ b/esphome/components/micronova/sensor/micronova_sensor.cpp @@ -0,0 +1,35 @@ +#include "micronova_sensor.h" + +namespace esphome { +namespace micronova { + +void MicroNovaSensor::process_value_from_stove(int value_from_stove) { + if (value_from_stove == -1) { + this->publish_state(NAN); + 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; + } + this->publish_state(new_sensor_value); +} + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/sensor/micronova_sensor.h b/esphome/components/micronova/sensor/micronova_sensor.h new file mode 100644 index 0000000000..9d5ae96b87 --- /dev/null +++ b/esphome/components/micronova/sensor/micronova_sensor.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/micronova/micronova.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace micronova { + +class MicroNovaSensor : public sensor::Sensor, public MicroNovaSensorListener { + public: + MicroNovaSensor(MicroNova *m) : MicroNovaSensorListener(m) {} + void dump_config() override { LOG_SENSOR("", "Micronova sensor", this); } + + 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_; } + + protected: + int fan_speed_offset_ = 0; +}; + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/switch/__init__.py b/esphome/components/micronova/switch/__init__.py new file mode 100644 index 0000000000..9846d46cc6 --- /dev/null +++ b/esphome/components/micronova/switch/__init__.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + ICON_POWER, +) + +from .. import ( + MicroNova, + MicroNovaFunctions, + CONF_MICRONOVA_ID, + CONF_MEMORY_LOCATION, + CONF_MEMORY_ADDRESS, + MICRONOVA_LISTENER_SCHEMA, + micronova_ns, +) + +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) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova), + cv.Optional(CONF_STOVE): switch.switch_schema( + MicroNovaSwitch, + icon=ICON_POWER, + ) + .extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x80, default_memory_address=0x21 + ) + ) + .extend( + { + cv.Optional(CONF_MEMORY_DATA_OFF, default=0x06): cv.hex_int_range(), + cv.Optional(CONF_MEMORY_DATA_ON, default=0x01): cv.hex_int_range(), + } + ), + } +) + + +async def to_code(config): + mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) + + 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])) + 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 new file mode 100644 index 0000000000..dcc96102db --- /dev/null +++ b/esphome/components/micronova/switch/micronova_switch.cpp @@ -0,0 +1,33 @@ +#include "micronova_switch.h" + +namespace esphome { +namespace 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; + } +} + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/switch/micronova_switch.h b/esphome/components/micronova/switch/micronova_switch.h new file mode 100644 index 0000000000..b0ca33b497 --- /dev/null +++ b/esphome/components/micronova/switch/micronova_switch.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/components/micronova/micronova.h" +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace micronova { + +class MicroNovaSwitch : public Component, public switch_::Switch, public MicroNovaSwitchListener { + 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; } + + 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; +}; + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/text_sensor/__init__.py b/esphome/components/micronova/text_sensor/__init__.py new file mode 100644 index 0000000000..dc27c4f32c --- /dev/null +++ b/esphome/components/micronova/text_sensor/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor + +from .. import ( + MicroNova, + MicroNovaFunctions, + CONF_MICRONOVA_ID, + CONF_MEMORY_LOCATION, + CONF_MEMORY_ADDRESS, + MICRONOVA_LISTENER_SCHEMA, + micronova_ns, +) + +CONF_STOVE_STATE = "stove_state" + +MicroNovaTextSensor = micronova_ns.class_( + "MicroNovaTextSensor", text_sensor.TextSensor, cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova), + cv.Optional(CONF_STOVE_STATE): text_sensor.text_sensor_schema( + MicroNovaTextSensor + ).extend( + MICRONOVA_LISTENER_SCHEMA( + default_memory_location=0x00, default_memory_address=0x21 + ) + ), + } +) + + +async def to_code(config): + mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) + + 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)) diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp new file mode 100644 index 0000000000..03b192ffd1 --- /dev/null +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp @@ -0,0 +1,31 @@ +#include "micronova_text_sensor.h" + +namespace esphome { +namespace micronova { + +void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) { + if (value_from_stove == -1) { + this->publish_state("unknown"); + 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; + } +} + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.h b/esphome/components/micronova/text_sensor/micronova_text_sensor.h new file mode 100644 index 0000000000..b4e5de9bb3 --- /dev/null +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/micronova/micronova.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace micronova { + +class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaSensorListener { + public: + MicroNovaTextSensor(MicroNova *m) : MicroNovaSensorListener(m) {} + void dump_config() override { LOG_TEXT_SENSOR("", "Micronova text sensor", this); } + 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 diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h index 5313f07f72..29c0ec5df2 100644 --- a/esphome/components/microphone/automation.h +++ b/esphome/components/microphone/automation.h @@ -23,7 +23,7 @@ class DataTrigger : public Trigger &> { } }; -template class IsCapturingActon : public Condition, public Parented { +template class IsCapturingCondition : public Condition, public Parented { public: bool check(Ts... x) override { return this->parent_->is_running(); } }; diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index 5b16a67c00..914ad80bea 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -1,6 +1,9 @@ #pragma once -#include "esphome/core/entity_base.h" +#include +#include +#include +#include #include "esphome/core/helpers.h" namespace esphome { @@ -20,8 +23,10 @@ class Microphone { void add_data_callback(std::function &)> &&data_callback) { this->data_callbacks_.add(std::move(data_callback)); } + virtual size_t read(int16_t *buf, size_t len) = 0; bool is_running() const { return this->state_ == STATE_RUNNING; } + bool is_stopped() const { return this->state_ == STATE_STOPPED; } protected: State state_{STATE_STOPPED}; diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index a14d7f2f80..ed2fc6c826 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -70,72 +70,62 @@ void MICS4514Component::update() { if (this->carbon_monoxide_sensor_ != nullptr) { float co = 0.0f; - if (red_f <= 0.425f) { - co = (0.425f - red_f) / 0.000405f; - if (co < 1.0f) - co = 0.0f; - if (co > 1000.0f) - co = 1000.0f; + if (red_f > 3.4f) { + co = 0.0; + } else if (red_f < 0.01) { + co = 1000.0; + } else { + co = 4.2 / pow(red_f, 1.2); } this->carbon_monoxide_sensor_->publish_state(co); } if (this->nitrogen_dioxide_sensor_ != nullptr) { float nitrogendioxide = 0.0f; - if (ox_f >= 1.1f) { - nitrogendioxide = (ox_f - 0.045f) / 6.13f; - if (nitrogendioxide < 0.1f) - nitrogendioxide = 0.0f; - if (nitrogendioxide > 10.0f) - nitrogendioxide = 10.0f; + if (ox_f < 0.3f) { + nitrogendioxide = 0.0; + } else { + nitrogendioxide = 0.164 * pow(ox_f, 0.975); } this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide); } if (this->methane_sensor_ != nullptr) { float methane = 0.0f; - if (red_f <= 0.786f) { - methane = (0.786f - red_f) / 0.000023f; - if (methane < 1000.0f) - methane = 0.0f; - if (methane > 25000.0f) - methane = 25000.0f; + if (red_f > 0.9f || red_f < 0.5) { // outside the range->unlikely + methane = 0.0; + } else { + methane = 630 / pow(red_f, 4.4); } this->methane_sensor_->publish_state(methane); } if (this->ethanol_sensor_ != nullptr) { float ethanol = 0.0f; - if (red_f <= 0.306f) { - ethanol = (0.306f - red_f) / 0.00057f; - if (ethanol < 10.0f) - ethanol = 0.0f; - if (ethanol > 500.0f) - ethanol = 500.0f; + if (red_f > 1.0f || red_f < 0.02) { // outside the range->unlikely + ethanol = 0.0; + } else { + ethanol = 1.52 / pow(red_f, 1.55); } this->ethanol_sensor_->publish_state(ethanol); } if (this->hydrogen_sensor_ != nullptr) { float hydrogen = 0.0f; - if (red_f <= 0.279f) { - hydrogen = (0.279f - red_f) / 0.00026f; - if (hydrogen < 1.0f) - hydrogen = 0.0f; - if (hydrogen > 1000.0f) - hydrogen = 1000.0f; + if (red_f > 0.9f || red_f < 0.02) { // outside the range->unlikely + hydrogen = 0.0; + } else { + hydrogen = 0.85 / pow(red_f, 1.75); } this->hydrogen_sensor_->publish_state(hydrogen); } if (this->ammonia_sensor_ != nullptr) { float ammonia = 0.0f; - if (red_f <= 0.8f) { - ammonia = (0.8f - red_f) / 0.0015f; - if (ammonia < 1.0f) - ammonia = 0.0f; - if (ammonia > 500.0f) - ammonia = 500.0f; + if (red_f > 0.98f || red_f < 0.2532) { // outside the ammonia range->unlikely + ammonia = 0.0; + } else { + ammonia = 0.9 / pow(red_f, 4.6); } this->ammonia_sensor_->publish_state(ammonia); } diff --git a/esphome/components/mics_4514/sensor.py b/esphome/components/mics_4514/sensor.py index 80c3524f66..59ccba235a 100644 --- a/esphome/components/mics_4514/sensor.py +++ b/esphome/components/mics_4514/sensor.py @@ -1,10 +1,14 @@ import esphome.codegen as cg +from esphome.components import i2c, sensor import esphome.config_validation as cv - -from esphome.components import sensor, i2c - from esphome.const import ( + CONF_AMMONIA, + CONF_CARBON_MONOXIDE, + CONF_ETHANOL, + CONF_HYDROGEN, CONF_ID, + CONF_METHANE, + CONF_NITROGEN_DIOXIDE, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) @@ -12,13 +16,6 @@ from esphome.const import ( CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["i2c"] -CONF_CARBON_MONOXIDE = "carbon_monoxide" -CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" -CONF_METHANE = "methane" -CONF_ETHANOL = "ethanol" -CONF_HYDROGEN = "hydrogen" -CONF_AMMONIA = "ammonia" - mics_4514_ns = cg.esphome_ns.namespace("mics_4514") MICS4514Component = mics_4514_ns.class_( @@ -31,6 +28,7 @@ SENSORS = [ CONF_ETHANOL, CONF_HYDROGEN, CONF_AMMONIA, + CONF_NITROGEN_DIOXIDE, ] common_sensor_schema = sensor.sensor_schema( @@ -40,16 +38,7 @@ common_sensor_schema = sensor.sensor_schema( ) CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(MICS4514Component), - cv.Optional(CONF_NITROGEN_DIOXIDE): sensor.sensor_schema( - unit_of_measurement=UNIT_PARTS_PER_MILLION, - state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=2, - ), - } - ) + cv.Schema({cv.GenerateID(): cv.declare_id(MICS4514Component)}) .extend({cv.Optional(sensor_type): common_sensor_schema for sensor_type in SENSORS}) .extend(i2c.i2c_device_schema(0x75)) .extend(cv.polling_component_schema("60s")) @@ -62,10 +51,6 @@ async def to_code(config): await i2c.register_i2c_device(var, config) for sensor_type in SENSORS: - if sensor_type in config: - sens = await sensor.new_sensor(config[sensor_type]) + if sensor_config := config.get(sensor_type): + sens = await sensor.new_sensor(sensor_config) cg.add(getattr(var, f"set_{sensor_type}_sensor")(sens)) - - if CONF_NITROGEN_DIOXIDE in config: - sens = await sensor.new_sensor(config[CONF_NITROGEN_DIOXIDE]) - cg.add(var.set_nitrogen_dioxide_sensor(sens)) diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 80b1461576..e5612796a3 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_CUSTOM_PRESETS, CONF_ID, CONF_NUM_ATTEMPTS, + CONF_OUTDOOR_TEMPERATURE, CONF_PERIOD, CONF_SUPPORTED_MODES, CONF_SUPPORTED_PRESETS, @@ -35,9 +36,8 @@ from esphome.components.climate import ( ) CODEOWNERS = ["@dudanov"] -DEPENDENCIES = ["climate", "uart", "wifi"] +DEPENDENCIES = ["climate", "uart"] AUTO_LOAD = ["sensor"] -CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_POWER_USAGE = "power_usage" CONF_HUMIDITY_SETPOINT = "humidity_setpoint" midea_ac_ns = cg.esphome_ns.namespace("midea").namespace("ac") @@ -293,4 +293,4 @@ async def to_code(config): if CONF_HUMIDITY_SETPOINT in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) cg.add(var.set_humidity_setpoint_sensor(sens)) - cg.add_library("dudanov/MideaUART", "1.1.8") + cg.add_library("dudanov/MideaUART", "1.1.9") diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py index 140e4ee4e0..8fea6b192b 100644 --- a/esphome/components/midea_ir/climate.py +++ b/esphome/components/midea_ir/climate.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate_ir -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir", "coolix"] CODEOWNERS = ["@dudanov"] @@ -9,7 +9,6 @@ CODEOWNERS = ["@dudanov"] midea_ir_ns = cg.esphome_ns.namespace("midea_ir") MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) -CONF_USE_FAHRENHEIT = "use_fahrenheit" CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( { diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py index 0df17a5d16..5e865c636f 100644 --- a/esphome/components/mitsubishi/climate.py +++ b/esphome/components/mitsubishi/climate.py @@ -9,9 +9,53 @@ AUTO_LOAD = ["climate_ir"] mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi") MitsubishiClimate = mitsubishi_ns.class_("MitsubishiClimate", climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend( +CONF_SET_FAN_MODE = "set_fan_mode" +SetFanMode = mitsubishi_ns.enum("SetFanMode") +SETFANMODE = { + "quiet_4levels": SetFanMode.MITSUBISHI_FAN_Q4L, + # "5levels": SetFanMode.MITSUBISHI_FAN_5L, + "4levels": SetFanMode.MITSUBISHI_FAN_4L, + "3levels": SetFanMode.MITSUBISHI_FAN_3L, +} + +CONF_SUPPORTS_DRY = "supports_dry" +CONF_SUPPORTS_FAN_ONLY = "supports_fan_only" + +CONF_HORIZONTAL_DEFAULT = "horizontal_default" +HorizontalDirections = mitsubishi_ns.enum("HorizontalDirections") +HORIZONTAL_DIRECTIONS = { + "left": HorizontalDirections.HORIZONTAL_DIRECTION_LEFT, + "middle-left": HorizontalDirections.HORIZONTAL_DIRECTION_MIDDLE_LEFT, + "middle": HorizontalDirections.HORIZONTAL_DIRECTION_MIDDLE, + "middle-right": HorizontalDirections.HORIZONTAL_DIRECTION_MIDDLE_RIGHT, + "right": HorizontalDirections.HORIZONTAL_DIRECTION_RIGHT, + "split": HorizontalDirections.HORIZONTAL_DIRECTION_SPLIT, +} + +CONF_VERTICAL_DEFAULT = "vertical_default" +VerticalDirections = mitsubishi_ns.enum("VerticalDirections") +VERTICAL_DIRECTIONS = { + "auto": VerticalDirections.VERTICAL_DIRECTION_AUTO, + "up": VerticalDirections.VERTICAL_DIRECTION_UP, + "middle-up": VerticalDirections.VERTICAL_DIRECTION_MIDDLE_UP, + "middle": VerticalDirections.VERTICAL_DIRECTION_MIDDLE, + "middle-down": VerticalDirections.VERTICAL_DIRECTION_MIDDLE_DOWN, + "down": VerticalDirections.VERTICAL_DIRECTION_DOWN, +} + + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(MitsubishiClimate), + cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE), + cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean, + cv.Optional(CONF_SUPPORTS_FAN_ONLY, default=False): cv.boolean, + cv.Optional(CONF_HORIZONTAL_DEFAULT, default="middle"): cv.enum( + HORIZONTAL_DIRECTIONS + ), + cv.Optional(CONF_VERTICAL_DEFAULT, default="middle"): cv.enum( + VERTICAL_DIRECTIONS + ), } ) @@ -19,3 +63,9 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.register_climate_ir(var, config) + + cg.add(var.set_fan_mode(config[CONF_SET_FAN_MODE])) + cg.add(var.set_supports_dry(config[CONF_SUPPORTS_DRY])) + cg.add(var.set_supports_fan_only(config[CONF_SUPPORTS_FAN_ONLY])) + cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT])) + cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index 99ca6d1cc5..449c8fc712 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -6,14 +6,33 @@ namespace mitsubishi { static const char *const TAG = "mitsubishi.climate"; -const uint32_t MITSUBISHI_OFF = 0x00; +const uint8_t MITSUBISHI_OFF = 0x00; + +const uint8_t MITSUBISHI_MODE_AUTO = 0x20; +const uint8_t MITSUBISHI_MODE_COOL = 0x18; +const uint8_t MITSUBISHI_MODE_DRY = 0x10; +const uint8_t MITSUBISHI_MODE_FAN_ONLY = 0x38; +const uint8_t MITSUBISHI_MODE_HEAT = 0x08; + +const uint8_t MITSUBISHI_MODE_A_HEAT = 0x00; +const uint8_t MITSUBISHI_MODE_A_DRY = 0x02; +const uint8_t MITSUBISHI_MODE_A_COOL = 0x06; +const uint8_t MITSUBISHI_MODE_A_AUTO = 0x06; + +const uint8_t MITSUBISHI_WIDE_VANE_SWING = 0xC0; -const uint8_t MITSUBISHI_COOL = 0x18; -const uint8_t MITSUBISHI_DRY = 0x10; -const uint8_t MITSUBISHI_AUTO = 0x20; -const uint8_t MITSUBISHI_HEAT = 0x08; const uint8_t MITSUBISHI_FAN_AUTO = 0x00; +const uint8_t MITSUBISHI_VERTICAL_VANE_SWING = 0x38; + +// const uint8_t MITSUBISHI_AUTO = 0X80; +const uint8_t MITSUBISHI_OTHERWISE = 0X40; +const uint8_t MITSUBISHI_POWERFUL = 0x08; + +// Optional presets used to enable some model features +const uint8_t MITSUBISHI_ECONOCOOL = 0x20; +const uint8_t MITSUBISHI_NIGHTMODE = 0xC1; + // Pulse parameters in usec const uint16_t MITSUBISHI_BIT_MARK = 430; const uint16_t MITSUBISHI_ONE_SPACE = 1250; @@ -22,43 +41,222 @@ const uint16_t MITSUBISHI_HEADER_MARK = 3500; const uint16_t MITSUBISHI_HEADER_SPACE = 1700; const uint16_t MITSUBISHI_MIN_GAP = 17500; +// Marker bytes +const uint8_t MITSUBISHI_BYTE00 = 0X23; +const uint8_t MITSUBISHI_BYTE01 = 0XCB; +const uint8_t MITSUBISHI_BYTE02 = 0X26; +const uint8_t MITSUBISHI_BYTE03 = 0X01; +const uint8_t MITSUBISHI_BYTE04 = 0X00; +const uint8_t MITSUBISHI_BYTE13 = 0X00; +const uint8_t MITSUBISHI_BYTE16 = 0X00; + +climate::ClimateTraits MitsubishiClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->sensor_ != nullptr); + traits.set_supports_action(false); + traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN); + traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX); + traits.set_visual_temperature_step(1.0f); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); + + if (this->supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (this->supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + + if (this->supports_cool_ && this->supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); + + if (this->supports_dry_) + traits.add_supported_mode(climate::CLIMATE_MODE_DRY); + if (this->supports_fan_only_) + traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); + + // Default to only 3 levels in ESPHome even if most unit supports 4. The 3rd level is not used. + traits.set_supported_fan_modes( + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}); + if (this->fan_mode_ == MITSUBISHI_FAN_Q4L) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET); + if (/*this->fan_mode_ == MITSUBISHI_FAN_5L ||*/ this->fan_mode_ >= MITSUBISHI_FAN_4L) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); // Shouldn't be used for this but it helps + + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); + + traits.set_supported_presets({climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, + climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_SLEEP}); + + return traits; +} + void MitsubishiClimate::transmit_state() { - uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x08, 0x00, 0x30, - 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + // Byte 0-4: Constant: 0x23, 0xCB, 0x26, 0x01, 0x00 + // Byte 5: On=0x20, Off: 0x00 + // Byte 6: MODE (See MODEs above (Heat/Dry/Cool/Auto/FanOnly) + // Byte 7: TEMP bits 0,1,2,3, added to MITSUBISHI_TEMP_MIN + // Example: 0x00 = 0°C+MITSUBISHI_TEMP_MIN = 16°C; 0x07 = 7°C+MITSUBISHI_TEMP_MIN = 23°C + // Byte 8: MODE_A & Wide Vane (if present) + // MODE_A bits 0,1,2 different than Byte 6 (See MODE_As above) + // Wide Vane bits 4,5,6,7 (Middle = 0x30) + // Byte 9: FAN/Vertical Vane/Switch To Auto + // FAN (Speed) bits 0,1,2 + // Vertical Vane bits 3,4,5 (Auto = 0x00) + // Switch To Auto bits 6,7 + // Byte 10: CLOCK Current time as configured on remote (0x00=Not used) + // Byte 11: END CLOCK Stop time of HVAC (0x00 for no setting) + // Byte 12: START CLOCK Start time of HVAC (0x00 for no setting) + // Byte 13: Constant 0x00 + // Byte 14: HVAC specfic, i.e. ECONO COOL, CLEAN MODE, always 0x00 + // Byte 15: HVAC specfic, i.e. POWERFUL, SMART SET, PLASMA, always 0x00 + // Byte 16: Constant 0x00 + // Byte 17: Checksum: SUM[Byte0...Byte16] + uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; switch (this->mode) { - case climate::CLIMATE_MODE_COOL: - remote_state[6] = MITSUBISHI_COOL; - break; case climate::CLIMATE_MODE_HEAT: - remote_state[6] = MITSUBISHI_HEAT; + remote_state[6] = MITSUBISHI_MODE_HEAT; + remote_state[8] = MITSUBISHI_MODE_A_HEAT; + break; + case climate::CLIMATE_MODE_DRY: + remote_state[6] = MITSUBISHI_MODE_DRY; + remote_state[8] = MITSUBISHI_MODE_A_DRY; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[6] = MITSUBISHI_MODE_COOL; + remote_state[8] = MITSUBISHI_MODE_A_COOL; break; case climate::CLIMATE_MODE_HEAT_COOL: - remote_state[6] = MITSUBISHI_AUTO; + remote_state[6] = MITSUBISHI_MODE_AUTO; + remote_state[8] = MITSUBISHI_MODE_A_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[6] = MITSUBISHI_MODE_FAN_ONLY; + remote_state[8] = MITSUBISHI_MODE_A_AUTO; break; case climate::CLIMATE_MODE_OFF: default: + remote_state[6] = MITSUBISHI_MODE_COOL; + remote_state[8] = MITSUBISHI_MODE_A_COOL; + if (this->supports_heat_) { + remote_state[6] = MITSUBISHI_MODE_HEAT; + remote_state[8] = MITSUBISHI_MODE_A_HEAT; + } remote_state[5] = MITSUBISHI_OFF; break; } - remote_state[7] = (uint8_t) roundf(clamp(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - - MITSUBISHI_TEMP_MIN); + // Temperature + if (this->mode == climate::CLIMATE_MODE_DRY) { + remote_state[7] = 24 - MITSUBISHI_TEMP_MIN; // Remote sends always 24°C if "Dry" mode is selected + } else { + remote_state[7] = (uint8_t) roundf( + clamp(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - MITSUBISHI_TEMP_MIN); + } - ESP_LOGV(TAG, "Sending Mitsubishi target temp: %.1f state: %02X mode: %02X temp: %02X", this->target_temperature, - remote_state[5], remote_state[6], remote_state[7]); + // Wide Vane + switch (this->swing_mode) { + case climate::CLIMATE_SWING_HORIZONTAL: + case climate::CLIMATE_SWING_BOTH: + remote_state[8] = remote_state[8] | MITSUBISHI_WIDE_VANE_SWING; // Wide Vane Swing + break; + case climate::CLIMATE_SWING_OFF: + default: + remote_state[8] = remote_state[8] | this->default_horizontal_direction_; // Off--> horizontal default position + break; + } + + ESP_LOGD(TAG, "default_horizontal_direction_: %02X", this->default_horizontal_direction_); + + // Fan Speed & Vertical Vane + // Map of Climate fan mode to this device expected value + // For 3Level: Low = 1, Medium = 2, High = 3 + // For 4Level: Low = 1, Middle = 2, Medium = 3, High = 4 + // For 5Level: Low = 1, Middle = 2, Medium = 3, High = 4 + // For 4Level + Quiet: Low = 1, Middle = 2, Medium = 3, High = 4, Quiet = 5 + + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + remote_state[9] = 1; + break; + case climate::CLIMATE_FAN_MEDIUM: + if (this->fan_mode_ == MITSUBISHI_FAN_3L) { + remote_state[9] = 2; + } else { + remote_state[9] = 3; + } + break; + case climate::CLIMATE_FAN_HIGH: + if (this->fan_mode_ == MITSUBISHI_FAN_3L) { + remote_state[9] = 3; + } else { + remote_state[9] = 4; + } + break; + case climate::CLIMATE_FAN_MIDDLE: + remote_state[9] = 2; + break; + case climate::CLIMATE_FAN_QUIET: + remote_state[9] = 5; + break; + default: + remote_state[9] = MITSUBISHI_FAN_AUTO; + break; + } + + ESP_LOGD(TAG, "fan: %02x state: %02x", this->fan_mode.value(), remote_state[9]); + + // Vertical Vane + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + case climate::CLIMATE_SWING_BOTH: + remote_state[9] = remote_state[9] | MITSUBISHI_VERTICAL_VANE_SWING | MITSUBISHI_OTHERWISE; // Vane Swing + break; + case climate::CLIMATE_SWING_OFF: + default: + remote_state[9] = remote_state[9] | this->default_vertical_direction_ | + MITSUBISHI_OTHERWISE; // Off--> vertical default position + break; + } + + ESP_LOGD(TAG, "default_vertical_direction_: %02X", this->default_vertical_direction_); + + // Special modes + switch (this->preset.value()) { + case climate::CLIMATE_PRESET_ECO: + remote_state[6] = MITSUBISHI_MODE_COOL | MITSUBISHI_OTHERWISE; + remote_state[8] = (remote_state[8] & ~7) | MITSUBISHI_MODE_A_COOL; + remote_state[14] = MITSUBISHI_ECONOCOOL; + break; + case climate::CLIMATE_PRESET_SLEEP: + remote_state[9] = MITSUBISHI_FAN_AUTO; + remote_state[14] = MITSUBISHI_NIGHTMODE; + break; + case climate::CLIMATE_PRESET_BOOST: + remote_state[6] |= MITSUBISHI_OTHERWISE; + remote_state[15] = MITSUBISHI_POWERFUL; + break; + case climate::CLIMATE_PRESET_NONE: + default: + break; + } // Checksum for (int i = 0; i < 17; i++) { remote_state[17] += remote_state[i]; } + ESP_LOGD(TAG, "sending: %02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17]); + auto transmit = this->transmitter_->transmit(); auto *data = transmit.get_data(); data->set_carrier_frequency(38000); // repeat twice - for (uint16_t r = 0; r < 2; r++) { + for (uint8_t r = 0; r < 2; r++) { // Header data->mark(MITSUBISHI_HEADER_MARK); data->space(MITSUBISHI_HEADER_SPACE); @@ -81,5 +279,119 @@ void MitsubishiClimate::transmit_state() { transmit.perform(); } +bool MitsubishiClimate::parse_state_frame_(const uint8_t frame[]) { return false; } + +bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[18] = {}; + + if (!data.expect_item(MITSUBISHI_HEADER_MARK, MITSUBISHI_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + for (uint8_t pos = 0; pos < 18; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(MITSUBISHI_BIT_MARK, MITSUBISHI_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(MITSUBISHI_BIT_MARK, MITSUBISHI_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", pos, bit); + return false; + } + } + state_frame[pos] = byte; + + // Check Header && Footer + if ((pos == 0 && byte != MITSUBISHI_BYTE00) || (pos == 1 && byte != MITSUBISHI_BYTE01) || + (pos == 2 && byte != MITSUBISHI_BYTE02) || (pos == 3 && byte != MITSUBISHI_BYTE03) || + (pos == 4 && byte != MITSUBISHI_BYTE04) || (pos == 13 && byte != MITSUBISHI_BYTE13) || + (pos == 16 && byte != MITSUBISHI_BYTE16)) { + ESP_LOGV(TAG, "Bytes 0,1,2,3,4,13 or 16 fail - invalid value"); + return false; + } + } + + // On/Off and Mode + if (state_frame[5] == MITSUBISHI_OFF) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + switch (state_frame[6]) { + case MITSUBISHI_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case MITSUBISHI_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case MITSUBISHI_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case MITSUBISHI_MODE_FAN_ONLY: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case MITSUBISHI_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + } + } + + // Temp + this->target_temperature = state_frame[7] + MITSUBISHI_TEMP_MIN; + + // Fan + uint8_t fan = state_frame[9] & 0x07; //(Bit 0,1,2 = Speed) + // Map of Climate fan mode to this device expected value + // For 3Level: Low = 1, Medium = 2, High = 3 + // For 4Level: Low = 1, Middle = 2, Medium = 3, High = 4 + // For 5Level: Low = 1, Middle = 2, Medium = 3, High = 4 + // For 4Level + Quiet: Low = 1, Middle = 2, Medium = 3, High = 4, Quiet = 5 + climate::ClimateFanMode modes_mapping[8] = { + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, + this->fan_mode_ == MITSUBISHI_FAN_3L ? climate::CLIMATE_FAN_MEDIUM : climate::CLIMATE_FAN_MIDDLE, + this->fan_mode_ == MITSUBISHI_FAN_3L ? climate::CLIMATE_FAN_HIGH : climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_QUIET, + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_AUTO}; + this->fan_mode = modes_mapping[fan]; + + // Wide Vane + uint8_t wide_vane = state_frame[8] & 0xF0; // Bits 4,5,6,7 + switch (wide_vane) { + case MITSUBISHI_WIDE_VANE_SWING: + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + break; + default: + this->swing_mode = climate::CLIMATE_SWING_OFF; + break; + } + + // Vertical Vane + uint8_t vertical_vane = state_frame[9] & 0x38; // Bits 3,4,5 + switch (vertical_vane) { + case MITSUBISHI_VERTICAL_VANE_SWING: + if (this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL) { + this->swing_mode = climate::CLIMATE_SWING_BOTH; + } else { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } + break; + } + + switch (state_frame[14]) { + case MITSUBISHI_ECONOCOOL: + this->preset = climate::CLIMATE_PRESET_ECO; + break; + case MITSUBISHI_NIGHTMODE: + this->preset = climate::CLIMATE_PRESET_SLEEP; + break; + } + + ESP_LOGV(TAG, "Receiving: %s", format_hex_pretty(state_frame, 18).c_str()); + + this->publish_state(); + return true; +} + } // namespace mitsubishi } // namespace esphome diff --git a/esphome/components/mitsubishi/mitsubishi.h b/esphome/components/mitsubishi/mitsubishi.h index e6bd7b8ebe..cfe12428da 100644 --- a/esphome/components/mitsubishi/mitsubishi.h +++ b/esphome/components/mitsubishi/mitsubishi.h @@ -2,6 +2,8 @@ #include "esphome/components/climate_ir/climate_ir.h" +#include + namespace esphome { namespace mitsubishi { @@ -9,13 +11,72 @@ namespace mitsubishi { const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius const uint8_t MITSUBISHI_TEMP_MAX = 31; // Celsius +// Fan mode +enum SetFanMode { + MITSUBISHI_FAN_3L = 0, // 3 levels + auto + MITSUBISHI_FAN_4L, // 4 levels + auto + MITSUBISHI_FAN_Q4L, // Quiet + 4 levels + auto + // MITSUBISHI_FAN_5L, // 5 levels + auto +}; + +// Enum to represent horizontal directios +enum HorizontalDirection { + HORIZONTAL_DIRECTION_LEFT = 0x10, + HORIZONTAL_DIRECTION_MIDDLE_LEFT = 0x20, + HORIZONTAL_DIRECTION_MIDDLE = 0x30, + HORIZONTAL_DIRECTION_MIDDLE_RIGHT = 0x40, + HORIZONTAL_DIRECTION_RIGHT = 0x50, + HORIZONTAL_DIRECTION_SPLIT = 0x80, +}; + +// Enum to represent vertical directions +enum VerticalDirection { + VERTICAL_DIRECTION_AUTO = 0x00, + VERTICAL_DIRECTION_UP = 0x08, + VERTICAL_DIRECTION_MIDDLE_UP = 0x10, + VERTICAL_DIRECTION_MIDDLE = 0x18, + VERTICAL_DIRECTION_MIDDLE_DOWN = 0x20, + VERTICAL_DIRECTION_DOWN = 0x28, +}; + class MitsubishiClimate : public climate_ir::ClimateIR { public: - MitsubishiClimate() : climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) {} + MitsubishiClimate() + : climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MIDDLE, + climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL}, + {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST, + climate::CLIMATE_PRESET_SLEEP}) {} + + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } + void set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + + void set_fan_mode(SetFanMode fan_mode) { this->fan_mode_ = fan_mode; } + + void set_horizontal_default(HorizontalDirection horizontal_direction) { + this->default_horizontal_direction_ = horizontal_direction; + } + void set_vertical_default(VerticalDirection vertical_direction) { + this->default_vertical_direction_ = vertical_direction; + } protected: - /// Transmit via IR the state of this climate controller. + // Transmit via IR the state of this climate controller. void transmit_state() override; + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); + + SetFanMode fan_mode_; + + HorizontalDirection default_horizontal_direction_; + VerticalDirection default_vertical_direction_; + + climate::ClimateTraits traits() override; }; } // namespace mitsubishi diff --git a/esphome/components/mlx90393/sensor.py b/esphome/components/mlx90393/sensor.py index 92ba30bea3..fe01d8ebfc 100644 --- a/esphome/components/mlx90393/sensor.py +++ b/esphome/components/mlx90393/sensor.py @@ -132,4 +132,4 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) cg.add(var.set_drdy_gpio(pin)) - cg.add_library("functionpointer/arduino-MLX90393", "1.0.0") + cg.add_library("functionpointer/arduino-MLX90393", "1.0.2") diff --git a/esphome/components/mmc5603/sensor.py b/esphome/components/mmc5603/sensor.py index 348a0e7dcc..cf16132470 100644 --- a/esphome/components/mmc5603/sensor.py +++ b/esphome/components/mmc5603/sensor.py @@ -3,6 +3,10 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ADDRESS, + CONF_FIELD_STRENGTH_X, + CONF_FIELD_STRENGTH_Y, + CONF_FIELD_STRENGTH_Z, + CONF_HEADING, CONF_ID, ICON_MAGNET, STATE_CLASS_MEASUREMENT, @@ -16,11 +20,6 @@ DEPENDENCIES = ["i2c"] mmc5603_ns = cg.esphome_ns.namespace("mmc5603") -CONF_FIELD_STRENGTH_X = "field_strength_x" -CONF_FIELD_STRENGTH_Y = "field_strength_y" -CONF_FIELD_STRENGTH_Z = "field_strength_z" -CONF_HEADING = "heading" - MMC5603Component = mmc5603_ns.class_( "MMC5603Component", cg.PollingComponent, i2c.I2CDevice ) diff --git a/esphome/components/mmc5983/__init__.py b/esphome/components/mmc5983/__init__.py new file mode 100644 index 0000000000..c8db8c4300 --- /dev/null +++ b/esphome/components/mmc5983/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@agoode"] diff --git a/esphome/components/mmc5983/mmc5983.cpp b/esphome/components/mmc5983/mmc5983.cpp new file mode 100644 index 0000000000..5b045ae38b --- /dev/null +++ b/esphome/components/mmc5983/mmc5983.cpp @@ -0,0 +1,141 @@ +// See https://github.com/sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library/tree/main +// for datasheets and an Arduino implementation. + +#include "mmc5983.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mmc5983 { + +static const char *const TAG = "mmc5983"; + +namespace { +constexpr uint8_t IC0_ADDR = 0x09; +constexpr uint8_t IC1_ADDR = 0x0a; +constexpr uint8_t IC2_ADDR = 0x0b; +constexpr uint8_t IC3_ADDR = 0x0c; +constexpr uint8_t PRODUCT_ID_ADDR = 0x2f; + +float convert_data_to_millitesla(uint8_t data_17_10, uint8_t data_9_2, uint8_t data_1_0) { + int32_t counts = (data_17_10 << 10) | (data_9_2 << 2) | data_1_0; + counts -= 131072; // "Null Field Output" from datasheet. + + // Sensitivity is 16384 counts/gauss, which is 163840 counts/mT. + return counts / 163840.0f; +} +} // namespace + +void MMC5983Component::update() { + // Schedule a SET/RESET. This will recalibrate the sensor. + // We are supposed to be able to set this once, and have it automatically continue every reading, but + // this does not appear to work in continuous mode, even with En_prd_set turned on in Internal Control 2. + // Bit 5 = Auto_SR_en (automatic SET/RESET enable). + const uint8_t ic0_value = 0b10000; + i2c::ErrorCode err = this->write_register(IC0_ADDR, &ic0_value, 1); + if (err != i2c::ErrorCode::ERROR_OK) { + ESP_LOGW(TAG, "Writing Internal Control 0 failed with i2c error %d", err); + this->status_set_warning(); + } + + // Read out the data, 7 bytes starting from 0x00. + uint8_t data[7]; + err = this->read_register(0x00, data, sizeof(data)); + if (err != i2c::ErrorCode::ERROR_OK) { + ESP_LOGW(TAG, "Reading data failed with i2c error %d", err); + this->status_set_warning(); + return; + } + + // Unpack the data and publish to sensors. + // Data is in this format: + // data[0]: Xout[17:10] + // data[1]: Xout[9:2] + // data[2]: Yout[17:10] + // data[3]: Yout[9:2] + // data[4]: Zout[17:10] + // data[5]: Zout[9:2] + // data[6]: { Xout[1], Xout[0], Yout[1], Yout[0], Zout[1], Zout[0], 0, 0 } + if (this->x_sensor_) { + this->x_sensor_->publish_state(convert_data_to_millitesla(data[0], data[1], (data[6] & 0b11000000) >> 6)); + } + if (this->y_sensor_) { + this->y_sensor_->publish_state(convert_data_to_millitesla(data[2], data[3], (data[6] & 0b00110000) >> 4)); + } + if (this->z_sensor_) { + this->z_sensor_->publish_state(convert_data_to_millitesla(data[4], data[5], (data[6] & 0b00001100) >> 2)); + } +} + +void MMC5983Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MMC5983..."); + + // Verify product id. + const uint8_t mmc5983_product_id = 0x30; + uint8_t id; + i2c::ErrorCode err = this->read_register(PRODUCT_ID_ADDR, &id, 1); + if (err != i2c::ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Reading product id failed with i2c error %d", err); + this->mark_failed(); + return; + } + if (id != mmc5983_product_id) { + ESP_LOGE(TAG, "Product id 0x%02x does not match expected value 0x%02x", id, mmc5983_product_id); + this->mark_failed(); + return; + } + + // Initialize Internal Control registers to 0. + // Internal Control 0. + const uint8_t zero = 0; + err = this->write_register(IC0_ADDR, &zero, 1); + if (err != i2c::ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Initializing Internal Control 0 failed with i2c error %d", err); + this->mark_failed(); + return; + } + // Internal Control 1. + err = this->write_register(IC1_ADDR, &zero, 1); + if (err != i2c::ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Initializing Internal Control 1 failed with i2c error %d", err); + this->mark_failed(); + return; + } + // Internal Control 2. + err = this->write_register(IC2_ADDR, &zero, 1); + if (err != i2c::ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Initializing Internal Control 2 failed with i2c error %d", err); + this->mark_failed(); + return; + } + // Internal Control 3. + err = this->write_register(IC3_ADDR, &zero, 1); + if (err != i2c::ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Initializing Internal Control 3 failed with i2c error %d", err); + this->mark_failed(); + return; + } + + // Enable continuous mode at 100 Hz, using Internal Control 2. + // Bit 3 = Cmm_en (continuous mode enable). + // Bit [2:0] = Cm_freq. 0b101 = 100 Hz, the fastest reading speed at Bandwidth=100 Hz. + const uint8_t ic2_value = 0b00001101; + err = this->write_register(IC2_ADDR, &ic2_value, 1); + if (err != i2c::ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Writing Internal Control 2 failed with i2c error %d", err); + this->mark_failed(); + return; + } +} + +void MMC5983Component::dump_config() { + ESP_LOGD(TAG, "MMC5983:"); + LOG_I2C_DEVICE(this); + LOG_SENSOR(" ", "X", this->x_sensor_); + LOG_SENSOR(" ", "Y", this->y_sensor_); + LOG_SENSOR(" ", "Z", this->z_sensor_); +} + +float MMC5983Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace mmc5983 +} // namespace esphome diff --git a/esphome/components/mmc5983/mmc5983.h b/esphome/components/mmc5983/mmc5983.h new file mode 100644 index 0000000000..d425418904 --- /dev/null +++ b/esphome/components/mmc5983/mmc5983.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mmc5983 { + +class MMC5983Component : public PollingComponent, public i2c::I2CDevice { + public: + void update() override; + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } + void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } + void set_z_sensor(sensor::Sensor *z_sensor) { z_sensor_ = z_sensor; } + + protected: + sensor::Sensor *x_sensor_{nullptr}; + sensor::Sensor *y_sensor_{nullptr}; + sensor::Sensor *z_sensor_{nullptr}; +}; + +} // namespace mmc5983 +} // namespace esphome diff --git a/esphome/components/mmc5983/sensor.py b/esphome/components/mmc5983/sensor.py new file mode 100644 index 0000000000..e3f4209cf9 --- /dev/null +++ b/esphome/components/mmc5983/sensor.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_FIELD_STRENGTH_X, + CONF_FIELD_STRENGTH_Y, + CONF_FIELD_STRENGTH_Z, + CONF_ID, + ICON_MAGNET, + STATE_CLASS_MEASUREMENT, + UNIT_MICROTESLA, +) + +DEPENDENCIES = ["i2c"] + +mmc5983_ns = cg.esphome_ns.namespace("mmc5983") +MMC5983Component = mmc5983_ns.class_( + "MMC5983Component", cg.PollingComponent, i2c.I2CDevice +) + +field_strength_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=4, + state_class=STATE_CLASS_MEASUREMENT, +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MMC5983Component), + cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x30)) +) + + +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) + + if x_config := config.get(CONF_FIELD_STRENGTH_X): + sens = await sensor.new_sensor(x_config) + cg.add(var.set_x_sensor(sens)) + if y_config := config.get(CONF_FIELD_STRENGTH_Y): + sens = await sensor.new_sensor(y_config) + cg.add(var.set_y_sensor(sens)) + if z_config := config.get(CONF_FIELD_STRENGTH_Z): + sens = await sensor.new_sensor(z_config) + cg.add(var.set_z_sensor(sens)) diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 6fea7033f2..ae0c818c28 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,5 +1,9 @@ +from __future__ import annotations +from typing import Literal + import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome.cpp_helpers import gpio_pin_expression from esphome.components import uart from esphome.const import ( @@ -17,13 +21,21 @@ Modbus = modbus_ns.class_("Modbus", cg.Component, uart.UARTDevice) ModbusDevice = modbus_ns.class_("ModbusDevice") MULTI_CONF = True +CONF_ROLE = "role" CONF_MODBUS_ID = "modbus_id" CONF_SEND_WAIT_TIME = "send_wait_time" +ModbusRole = modbus_ns.enum("ModbusRole") +MODBUS_ROLES = { + "client": ModbusRole.CLIENT, + "server": ModbusRole.SERVER, +} + CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(Modbus), + cv.Optional(CONF_ROLE, default="client"): cv.enum(MODBUS_ROLES), cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema, cv.Optional( CONF_SEND_WAIT_TIME, default="250ms" @@ -43,6 +55,7 @@ async def to_code(config): await uart.register_uart_device(var, config) + cg.add(var.set_role(config[CONF_ROLE])) if CONF_FLOW_CONTROL_PIN in config: pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN]) cg.add(var.set_flow_control_pin(pin)) @@ -62,6 +75,28 @@ def modbus_device_schema(default_address): return cv.Schema(schema) +def final_validate_modbus_device( + name: str, *, role: Literal["server", "client"] | None = None +): + def validate_role(value): + assert role in MODBUS_ROLES + if value != role: + raise cv.Invalid(f"Component {name} requires role to be {role}") + return value + + def validate_hub(hub_config): + hub_schema = {} + if role is not None: + hub_schema[cv.Required(CONF_ROLE)] = validate_role + + return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config) + + return cv.Schema( + {cv.Required(CONF_MODBUS_ID): fv.id_declaration_match_schema(validate_hub)}, + extra=cv.ALLOW_EXTRA, + ) + + async def register_modbus_device(var, config): parent = await cg.get_variable(config[CONF_MODBUS_ID]) cg.add(var.set_parent(parent)) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 137fb0b26b..8544b50261 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -15,23 +15,33 @@ void Modbus::setup() { void Modbus::loop() { const uint32_t now = millis(); - if (now - this->last_modbus_byte_ > 50) { - this->rx_buffer_.clear(); - this->last_modbus_byte_ = now; - } - // stop blocking new send commands after send_wait_time_ ms regardless if a response has been received since then - if (now - this->last_send_ > send_wait_time_) { - waiting_for_response = 0; - } - while (this->available()) { uint8_t byte; this->read_byte(&byte); if (this->parse_modbus_byte_(byte)) { this->last_modbus_byte_ = now; } else { + size_t at = this->rx_buffer_.size(); + if (at > 0) { + ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at); + this->rx_buffer_.clear(); + } + } + } + + if (now - this->last_modbus_byte_ > 50) { + size_t at = this->rx_buffer_.size(); + if (at > 0) { + ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at); this->rx_buffer_.clear(); } + + // stop blocking new send commands after sent_wait_time_ ms after response received + if (now - this->last_send_ > send_wait_time_) { + if (waiting_for_response > 0) + ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response); + waiting_for_response = 0; + } } } @@ -39,7 +49,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); const uint8_t *raw = &this->rx_buffer_[0]; - ESP_LOGV(TAG, "Modbus received Byte %d (0X%x)", byte, byte); + ESP_LOGVV(TAG, "Modbus received Byte %d (0X%x)", byte, byte); // Byte 0: modbus address (match all) if (at == 0) return true; @@ -77,7 +87,13 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code); } else { - // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands + // data starts at 2 and length is 4 for read registers commands + if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) { + data_offset = 2; + data_len = 4; + } + + // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { data_offset = 2; data_len = 4; @@ -123,6 +139,9 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Ignore modbus exception not related to a pending command ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); } + } else if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) { + device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), + uint16_t(data[3]) | (uint16_t(data[2]) << 8)); } else { device->on_modbus_data(data); } @@ -135,8 +154,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address); } - // return false to reset buffer - return false; + // reset buffer + ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at); + this->rx_buffer_.clear(); + return true; } void Modbus::dump_config() { @@ -164,16 +185,18 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address std::vector data; data.push_back(address); data.push_back(function_code); - data.push_back(start_address >> 8); - data.push_back(start_address >> 0); - if (function_code != 0x5 && function_code != 0x6) { - data.push_back(number_of_entities >> 8); - data.push_back(number_of_entities >> 0); + if (this->role == ModbusRole::CLIENT) { + data.push_back(start_address >> 8); + data.push_back(start_address >> 0); + if (function_code != 0x5 && function_code != 0x6) { + data.push_back(number_of_entities >> 8); + data.push_back(number_of_entities >> 0); + } } if (payload != nullptr) { - if (function_code == 0xF || function_code == 0x10) { // Write multiple - data.push_back(payload_len); // Byte count is required for write + if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) { // Write multiple + data.push_back(payload_len); // Byte count is required for write } else { payload_len = 2; // Write single register or coil } diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index dd8732c6e9..4a78ed4aab 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -8,6 +8,11 @@ namespace esphome { namespace modbus { +enum ModbusRole { + CLIENT, + SERVER, +}; + class ModbusDevice; class Modbus : public uart::UARTDevice, public Component { @@ -27,11 +32,14 @@ class Modbus : public uart::UARTDevice, public Component { void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr); void send_raw(const std::vector &payload); + void set_role(ModbusRole role) { this->role = role; } void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; } uint8_t waiting_for_response{0}; void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; } void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; } + ModbusRole role; + protected: GPIOPin *flow_control_pin_{nullptr}; @@ -50,6 +58,7 @@ class ModbusDevice { void set_address(uint8_t address) { address_ = address; } virtual void on_modbus_data(const std::vector &data) = 0; virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {} + virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){}; void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr) { this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 46bb2c4233..488baa245a 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -1,16 +1,30 @@ import binascii + +from esphome import automation import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import modbus -from esphome.const import CONF_ADDRESS, CONF_ID, CONF_NAME, CONF_LAMBDA, CONF_OFFSET +import esphome.config_validation as cv +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_LAMBDA, + CONF_NAME, + CONF_OFFSET, + CONF_TRIGGER_ID, +) from esphome.cpp_helpers import logging + from .const import ( + CONF_ALLOW_DUPLICATE_COMMANDS, CONF_BITMASK, CONF_BYTE_OFFSET, CONF_COMMAND_THROTTLE, CONF_CUSTOM_COMMAND, CONF_FORCE_NEW_RANGE, + CONF_MAX_CMD_RETRIES, CONF_MODBUS_CONTROLLER_ID, + CONF_OFFLINE_SKIP_UPDATES, + CONF_ON_COMMAND_SENT, CONF_REGISTER_COUNT, CONF_REGISTER_TYPE, CONF_RESPONSE_SIZE, @@ -22,6 +36,8 @@ CODEOWNERS = ["@martgras"] AUTO_LOAD = ["modbus"] +CONF_READ_LAMBDA = "read_lambda" +CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") @@ -30,6 +46,7 @@ ModbusController = modbus_controller_ns.class_( ) SensorItem = modbus_controller_ns.struct("SensorItem") +ServerRegister = modbus_controller_ns.struct("ServerRegister") ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") @@ -93,24 +110,48 @@ TYPE_REGISTER_MAP = { "FP32_R": 2, } -MULTI_CONF = True +ModbusCommandSentTrigger = modbus_controller_ns.class_( + "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) +) _LOGGER = logging.getLogger(__name__) +ModbusServerRegisterSchema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ServerRegister), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Required(CONF_READ_LAMBDA): cv.returning_lambda, + } +) + + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ModbusController), + cv.Optional(CONF_ALLOW_DUPLICATE_COMMANDS, default=False): cv.boolean, cv.Optional( CONF_COMMAND_THROTTLE, default="0ms" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int, + cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional( + CONF_SERVER_REGISTERS, + ): cv.ensure_list(ModbusServerRegisterSchema), + cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ModbusCommandSentTrigger + ), + } + ), } ) .extend(cv.polling_component_schema("60s")) .extend(modbus.modbus_device_schema(0x01)) ) - ModbusItemBaseSchema = cv.Schema( { cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), @@ -152,6 +193,17 @@ def validate_modbus_register(config): return config +def _final_validate(config): + if CONF_SERVER_REGISTERS in config: + return modbus.final_validate_modbus_device("modbus_controller", role="server")( + config + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + def modbus_calc_properties(config): byte_offset = 0 reg_count = 0 @@ -181,7 +233,7 @@ def modbus_calc_properties(config): async def add_modbus_base_properties( - var, config, sensor_type, lamdba_param_type=cg.float_, lamdba_return_type=float + var, config, sensor_type, lambda_param_type=cg.float_, lambda_return_type=float ): if CONF_CUSTOM_COMMAND in config: cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND])) @@ -194,21 +246,46 @@ async def add_modbus_base_properties( config[CONF_LAMBDA], [ (sensor_type.operator("ptr"), "item"), - (lamdba_param_type, "x"), + (lambda_param_type, "x"), ( cg.std_vector.template(cg.uint8).operator("const").operator("ref"), "data", ), ], - return_type=cg.optional.template(lamdba_return_type), + return_type=cg.optional.template(lambda_return_type), ) cg.add(var.set_template(template_)) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID], config[CONF_COMMAND_THROTTLE]) + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS])) cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) + cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) + cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) + if CONF_SERVER_REGISTERS in config: + for server_register in config[CONF_SERVER_REGISTERS]: + cg.add( + var.add_server_register( + cg.new_Pvariable( + server_register[CONF_ID], + server_register[CONF_ADDRESS], + server_register[CONF_VALUE_TYPE], + TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], + await cg.process_lambda( + server_register[CONF_READ_LAMBDA], + [], + return_type=cg.float_, + ), + ) + ) + ) await register_modbus_device(var, config) + for conf in config.get(CONF_ON_COMMAND_SENT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(int, "function_code"), (int, "address")], conf + ) async def register_modbus_device(var, config): diff --git a/esphome/components/modbus_controller/automation.h b/esphome/components/modbus_controller/automation.h new file mode 100644 index 0000000000..ad8de4b05d --- /dev/null +++ b/esphome/components/modbus_controller/automation.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/modbus_controller/modbus_controller.h" + +namespace esphome { +namespace modbus_controller { + +class ModbusCommandSentTrigger : public Trigger { + public: + ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) { + a_modbuscontroller->add_on_command_sent_callback( + [this](int function_code, int address) { this->trigger(function_code, address); }); + } +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index 5315167479..2ae008f630 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -1,16 +1,16 @@ +import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -import esphome.codegen as cg - from esphome.const import CONF_ADDRESS, CONF_ID + from .. import ( - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, - validate_modbus_register, + MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, - MODBUS_REGISTER_TYPE, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, ) from ..const import ( CONF_BITMASK, diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index baf72efb94..5cf7d230f1 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -1,10 +1,14 @@ +CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands" CONF_BITMASK = "bitmask" CONF_BYTE_OFFSET = "byte_offset" CONF_COMMAND_THROTTLE = "command_throttle" +CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates" CONF_CUSTOM_COMMAND = "custom_command" CONF_FORCE_NEW_RANGE = "force_new_range" +CONF_MAX_CMD_RETRIES = "max_cmd_retries" CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" +CONF_ON_COMMAND_SENT = "on_command_sent" CONF_RAW_ENCODE = "raw_encode" CONF_REGISTER_COUNT = "register_count" CONF_REGISTER_TYPE = "register_type" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 79c13e3f68..1dcb533629 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -7,10 +7,7 @@ namespace modbus_controller { static const char *const TAG = "modbus_controller"; -void ModbusController::setup() { - // Modbus::setup(); - this->create_register_ranges_(); -} +void ModbusController::setup() { this->create_register_ranges_(); } /* To work with the existing modbus class and avoid polling for responses a command queue is used. @@ -21,39 +18,64 @@ void ModbusController::setup() { bool ModbusController::send_next_command_() { uint32_t last_send = millis() - this->last_command_timestamp_; - if ((last_send > this->command_throttle_) && !waiting_for_response() && !command_queue_.empty()) { - auto &command = command_queue_.front(); + if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) { + auto &command = this->command_queue_.front(); // remove from queue if command was sent too often - if (command->send_countdown < 1) { - ESP_LOGD( - TAG, - "Modbus command to device=%d register=0x%02X countdown=%d no response received - removed from send queue", - this->address_, command->register_address, command->send_countdown); - command_queue_.pop_front(); + if (!command->should_retry(this->max_cmd_retries_)) { + if (!this->module_offline_) { + ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_); + + if (this->offline_skip_updates_ > 0) { + // Update skip_updates_counter to stop flooding channel with timeouts + for (auto &r : this->register_ranges_) { + r.skip_updates_counter = this->offline_skip_updates_; + } + } + } + this->module_offline_ = true; + ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue", + this->address_, command->register_address); + this->command_queue_.pop_front(); } else { ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, command->register_address, command->register_count); command->send(); + this->last_command_timestamp_ = millis(); + + this->command_sent_callback_.call((int) command->function_code, command->register_address); + // remove from queue if no handler is defined if (!command->on_data_func) { - command_queue_.pop_front(); + this->command_queue_.pop_front(); } } } - return (!command_queue_.empty()); + return (!this->command_queue_.empty()); } // Queue incoming response void ModbusController::on_modbus_data(const std::vector &data) { auto ¤t_command = this->command_queue_.front(); if (current_command != nullptr) { + if (this->module_offline_) { + ESP_LOGW(TAG, "Modbus device=%d back online", this->address_); + + if (this->offline_skip_updates_ > 0) { + // Restore skip_updates_counter to restore commands updates + for (auto &r : this->register_ranges_) { + r.skip_updates_counter = 0; + } + } + } + this->module_offline_ = false; + // Move the commandItem to the response queue current_command->payload = data; this->incoming_queue_.push(std::move(current_command)); ESP_LOGV(TAG, "Modbus response queued"); - command_queue_.pop_front(); + this->command_queue_.pop_front(); } } @@ -75,10 +97,56 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ "payload size=%zu", function_code, current_command->register_address, current_command->register_count, current_command->payload.size()); - command_queue_.pop_front(); + this->command_queue_.pop_front(); } } +void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address, + uint16_t number_of_registers) { + ESP_LOGD(TAG, + "Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " + "0x%X.", + this->address_, function_code, start_address, number_of_registers); + + std::vector sixteen_bit_response; + for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { + bool found = false; + for (auto *server_register : this->server_registers_) { + if (server_register->address == current_address) { + float value = server_register->read_lambda(); + + ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.", + server_register->address, static_cast(server_register->value_type), + server_register->register_count, value); + std::vector payload = float_to_payload(value, server_register->value_type); + sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); + current_address += server_register->register_count; + found = true; + break; + } + } + + if (!found) { + ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); + std::vector error_response; + error_response.push_back(this->address_); + error_response.push_back(0x81); + error_response.push_back(0x02); + this->send_raw(error_response); + return; + } + } + + std::vector response; + for (auto v : sixteen_bit_response) { + auto decoded_value = decode_value(v); + response.push_back(decoded_value[0]); + response.push_back(decoded_value[1]); + } + + this->send(function_code, start_address, number_of_registers, response.size(), response.data()); +} + SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { auto reg_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); @@ -105,19 +173,21 @@ void ModbusController::on_register_data(ModbusRegisterType register_type, uint16 } void ModbusController::queue_command(const ModbusCommandItem &command) { - // check if this command is already qeued. - // not very effective but the queue is never really large - for (auto &item : command_queue_) { - if (item->is_equal(command)) { - ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u", - static_cast(command.register_type), command.register_address, command.register_count); - // update the payload of the queued command - // replaces a previous command - item->payload = command.payload; - return; + if (!this->allow_duplicate_commands_) { + // check if this command is already qeued. + // not very effective but the queue is never really large + for (auto &item : this->command_queue_) { + if (item->is_equal(command)) { + ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u", + static_cast(command.register_type), command.register_address, command.register_count); + // update the payload of the queued command + // replaces a previous command + item->payload = command.payload; + return; + } } } - command_queue_.push_back(make_unique(command)); + this->command_queue_.push_back(make_unique(command)); } void ModbusController::update_range_(RegisterRange &r) { @@ -152,8 +222,8 @@ void ModbusController::update_range_(RegisterRange &r) { // Once we get a response to the command it is removed from the queue and the next command is send // void ModbusController::update() { - if (!command_queue_.empty()) { - ESP_LOGV(TAG, "%zu modbus commands already in queue", command_queue_.size()); + if (!this->command_queue_.empty()) { + ESP_LOGV(TAG, "%zu modbus commands already in queue", this->command_queue_.size()); } else { ESP_LOGV(TAG, "Updating modbus component"); } @@ -167,7 +237,7 @@ void ModbusController::update() { // walk through the sensors and determine the register ranges to read size_t ModbusController::create_register_ranges_() { register_ranges_.clear(); - if (sensorset_.empty()) { + if (this->parent_->role == modbus::ModbusRole::CLIENT && sensorset_.empty()) { ESP_LOGW(TAG, "No sensors registered"); return 0; } @@ -274,6 +344,8 @@ size_t ModbusController::create_register_ranges_() { void ModbusController::dump_config() { ESP_LOGCONFIG(TAG, "ModbusController:"); ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + ESP_LOGCONFIG(TAG, " Max Command Retries: %d", this->max_cmd_retries_); + ESP_LOGCONFIG(TAG, " Offline Skip Updates: %d", this->offline_skip_updates_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); for (auto &it : sensorset_) { @@ -286,6 +358,11 @@ void ModbusController::dump_config() { ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast(it.register_type), it.start_address, it.register_count, it.skip_updates); } + ESP_LOGCONFIG(TAG, "server registers"); + for (auto &r : server_registers_) { + ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address, + static_cast(r->value_type), r->register_count); + } #endif } @@ -483,8 +560,9 @@ bool ModbusCommandItem::send() { } else { modbusdevice->send_raw(this->payload); } - ESP_LOGV(TAG, "Command sent %d 0x%X %d", uint8_t(this->function_code), this->register_address, this->register_count); - send_countdown--; + this->send_count_++; + ESP_LOGV(TAG, "Command sent %d 0x%X %d send_count: %d", uint8_t(this->function_code), this->register_address, + this->register_count, this->send_count_); return true; } @@ -588,5 +666,9 @@ int64_t payload_to_number(const std::vector &data, SensorValueType sens return value; } +void ModbusController::add_on_command_sent_callback(std::function &&callback) { + this->command_sent_callback_.add(std::move(callback)); +} + } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index ccb0edf9c6..1fa35e1535 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace esphome { @@ -251,6 +252,21 @@ class SensorItem { bool force_new_range{false}; }; +class ServerRegister { + public: + ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count, + std::function read_lambda) { + this->address = address; + this->value_type = value_type; + this->register_count = register_count; + this->read_lambda = std::move(read_lambda); + } + uint16_t address; + SensorValueType value_type; + uint8_t register_count; + std::function read_lambda; +}; + // ModbusController::create_register_ranges_ tries to optimize register range // for this the sensors must be ordered by register_type, start_address and bitmask class SensorItemsComparator { @@ -296,7 +312,6 @@ struct RegisterRange { class ModbusCommandItem { public: static const size_t MAX_PAYLOAD_BYTES = 240; - static const uint8_t MAX_SEND_REPEATS = 5; ModbusController *modbusdevice; uint16_t register_address; uint16_t register_count; @@ -306,9 +321,9 @@ class ModbusCommandItem { on_data_func; std::vector payload = {}; bool send(); - // wrong commands (esp. custom commands) can block the send queue - // limit the number of repeats - uint8_t send_countdown{MAX_SEND_REPEATS}; + /// Check if the command should be retried based on the max_retries parameter + bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; }; + /// factory methods /** Create modbus read command * Function code 02-04 @@ -397,6 +412,11 @@ class ModbusCommandItem { &&handler = nullptr); bool is_equal(const ModbusCommandItem &other); + + protected: + // wrong commands (esp. custom commands) can block the send queue, limit the number of repeats. + /// How many times this command has been sent + uint8_t send_count_{0}; }; /** Modbus controller class. @@ -409,7 +429,6 @@ class ModbusCommandItem { class ModbusController : public PollingComponent, public modbus::ModbusDevice { public: - ModbusController(uint16_t throttle = 0) : command_throttle_(throttle){}; void dump_config() override; void loop() override; void setup() override; @@ -419,18 +438,40 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void queue_command(const ModbusCommandItem &command); /// Registers a sensor with the controller. Called by esphomes code generator void add_sensor_item(SensorItem *item) { sensorset_.insert(item); } + /// Registers a server register with the controller. Called by esphomes code generator + void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); } /// called when a modbus response was parsed without errors void on_modbus_data(const std::vector &data) override; /// called when a modbus error response was received void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; + /// called when a modbus request (function code 3 or 4) was parsed without errors + void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); /// default delegate called by process_modbus_data when a response for a write response has retrieved from the /// incoming queue void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); + /// Allow a duplicate command to be sent + void set_allow_duplicate_commands(bool allow_duplicate_commands) { + this->allow_duplicate_commands_ = allow_duplicate_commands; + } + /// get if a duplicate command can be sent + bool get_allow_duplicate_commands() { return this->allow_duplicate_commands_; } /// called by esphome generated code to set the command_throttle period void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; } + /// called by esphome generated code to set the offline_skip_updates + void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; } + /// get the number of queued modbus commands (should be mostly empty) + size_t get_command_queue_length() { return command_queue_.size(); } + /// get if the module is offline, didn't respond the last command + bool get_module_offline() { return module_offline_; } + /// Set callback for commands + void add_on_command_sent_callback(std::function &&callback); + /// called by esphome generated code to set the max_cmd_retries. + void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; } + /// get how many times a command will be (re)sent if no response is received + uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; } protected: /// parse sensormap_ and create range of sequential addresses @@ -443,22 +484,31 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void process_modbus_data_(const ModbusCommandItem *response); /// send the next modbus command from the send queue bool send_next_command_(); - /// get the number of queued modbus commands (should be mostly empty) - size_t get_command_queue_length_() { return command_queue_.size(); } /// dump the parsed sensormap for diagnostics void dump_sensors_(); /// Collection of all sensors for this component SensorSet sensorset_; + /// Collection of all server registers for this component + std::vector server_registers_; /// Continuous range of modbus registers std::vector register_ranges_; /// Hold the pending requests to be sent std::list> command_queue_; /// modbus response data waiting to get processed std::queue> incoming_queue_; + /// if duplicate commands can be sent + bool allow_duplicate_commands_; /// when was the last send operation uint32_t last_command_timestamp_; /// min time in ms between sending modbus commands uint16_t command_throttle_; + /// if module didn't respond the last command + bool module_offline_; + /// how many updates to skip if module is offline + uint16_t offline_skip_updates_; + /// How many times we will retry a command if we get no response + uint8_t max_cmd_retries_{4}; + CallbackManager command_sent_callback_{}; }; /** Convert vector response payload to float. diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index fe99b28a00..b5efd7abf0 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import number +import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, CONF_ID, @@ -12,14 +12,13 @@ from esphome.const import ( from .. import ( MODBUS_WRITE_REGISTER_TYPE, - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, + SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, - SENSOR_VALUE_TYPE, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, ) - from ..const import ( CONF_BITMASK, CONF_CUSTOM_COMMAND, diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 1bf989ce8b..1800a90d57 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,20 +1,15 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import output -from esphome.const import ( - CONF_ADDRESS, - CONF_ID, - CONF_MULTIPLY, -) +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY from .. import ( - modbus_controller_ns, - modbus_calc_properties, + SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, - SENSOR_VALUE_TYPE, + modbus_calc_properties, + modbus_controller_ns, ) - from ..const import ( CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, @@ -65,6 +60,7 @@ CONFIG_SCHEMA = cv.typed_schema( async def to_code(config): byte_offset, reg_count = modbus_calc_properties(config) # Binary Output + write_template = None if config[CONF_REGISTER_TYPE] == "coil": var = cg.new_Pvariable( config[CONF_ID], @@ -72,7 +68,7 @@ async def to_code(config): byte_offset, ) if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( + write_template = await cg.process_lambda( config[CONF_WRITE_LAMBDA], [ (ModbusBinaryOutput.operator("ptr"), "item"), @@ -92,7 +88,7 @@ async def to_code(config): ) cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( + write_template = await cg.process_lambda( config[CONF_WRITE_LAMBDA], [ (ModbusFloatOutput.operator("ptr"), "item"), @@ -105,5 +101,5 @@ async def to_code(config): parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) cg.add(var.set_parent(parent)) - if CONF_WRITE_LAMBDA in config: - cg.add(var.set_write_template(template_)) + if write_template: + cg.add(var.set_write_template(write_template)) diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index 5692fea3e3..c94532da51 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import select +import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC from .. import ( diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index 0e4588cfef..d8fce54ece 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -1,17 +1,17 @@ +import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -import esphome.codegen as cg +from esphome.const import CONF_ADDRESS, CONF_ID -from esphome.const import CONF_ID, CONF_ADDRESS from .. import ( - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, - validate_modbus_register, - ModbusItemBaseSchema, - SensorItem, MODBUS_REGISTER_TYPE, SENSOR_VALUE_TYPE, + ModbusItemBaseSchema, + SensorItem, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, ) from ..const import ( CONF_BITMASK, diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index 9490325968..258d87fd25 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -1,17 +1,16 @@ +import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv -import esphome.codegen as cg +from esphome.const import CONF_ADDRESS, CONF_ID - -from esphome.const import CONF_ID, CONF_ADDRESS from .. import ( - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, - validate_modbus_register, + MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, - MODBUS_REGISTER_TYPE, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, ) from ..const import ( CONF_BITMASK, diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 763336e104..35cae645e1 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -1,26 +1,25 @@ +import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv -import esphome.codegen as cg - - from esphome.const import CONF_ADDRESS, CONF_ID + from .. import ( - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, - validate_modbus_register, + MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, - MODBUS_REGISTER_TYPE, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, ) from ..const import ( CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, + CONF_RAW_ENCODE, CONF_REGISTER_COUNT, + CONF_REGISTER_TYPE, CONF_RESPONSE_SIZE, CONF_SKIP_UPDATES, - CONF_RAW_ENCODE, - CONF_REGISTER_TYPE, ) DEPENDENCIES = ["modbus_controller"] @@ -37,6 +36,7 @@ RAW_ENCODING = { "NONE": RawEncoding.NONE, "HEXBYTES": RawEncoding.HEXBYTES, "COMMA": RawEncoding.COMMA, + "ANSI": RawEncoding.ANSI, } CONFIG_SCHEMA = cv.All( @@ -49,7 +49,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.positive_int, - cv.Optional(CONF_RAW_ENCODE, default="NONE"): cv.enum(RAW_ENCODING), + cv.Optional(CONF_RAW_ENCODE, default="ANSI"): cv.enum(RAW_ENCODING), } ), validate_modbus_register, diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp index c90890c88f..acdcacc083 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -13,10 +13,10 @@ void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Te void ModbusTextSensor::parse_and_publish(const std::vector &data) { std::ostringstream output; - uint8_t max_items = this->response_bytes; + uint8_t items_left = this->response_bytes; uint8_t index = this->offset; - char buffer[4]; - while ((max_items != 0) && index < data.size()) { + char buffer[5]; + while ((items_left > 0) && index < data.size()) { uint8_t b = data[index]; switch (this->encode_) { case RawEncoding::HEXBYTES: @@ -27,13 +27,16 @@ void ModbusTextSensor::parse_and_publish(const std::vector &data) { sprintf(buffer, index != this->offset ? ",%d" : "%d", b); output << buffer; break; + case RawEncoding::ANSI: + if (b < 0x20) + break; + // FALLTHROUGH // Anything else no encoding - case RawEncoding::NONE: default: output << (char) b; break; } - + items_left--; index++; } diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index 9cc0db05a5..d6eb5fd230 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -9,7 +9,7 @@ namespace esphome { namespace modbus_controller { -enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2 }; +enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2, ANSI = 3 }; class ModbusTextSensor : public Component, public text_sensor::TextSensor, public SensorItem { public: diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index 02d77a6b33..9527f09f59 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -17,6 +17,8 @@ void MopekaProCheck::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); LOG_SENSOR(" ", "Reading Distance", this->distance_); + LOG_SENSOR(" ", "Read Quality", this->read_quality_); + LOG_SENSOR(" ", "Ignored Reads", this->ignored_reads_); } /** @@ -54,7 +56,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP && static_cast(manu_data.data[0]) != LIPPERT_BOTTOM_UP && - static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP) { + static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP && + static_cast(manu_data.data[0]) != PRO_UNIVERSAL) { ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); return false; } @@ -65,34 +68,49 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) this->battery_level_->publish_state(level); } + // Get the quality value + SensorReadQuality quality_value = this->parse_read_quality_(manu_data.data); + if (this->read_quality_ != nullptr) { + this->read_quality_->publish_state(static_cast(quality_value)); + } + + // Determine if we have a good enough quality of read to report level and distance + // sensors. This sensor is reported regardless of distance or level sensors being enabled + if (quality_value < this->min_signal_quality_) { + ESP_LOGW(TAG, "Read Quality too low to report distance or level"); + this->ignored_read_count_++; + } else { + // reset to zero since read quality was sufficient + this->ignored_read_count_ = 0; + } + // Report number of contiguous ignored reads if sensor defined + if (this->ignored_reads_ != nullptr) { + this->ignored_reads_->publish_state(this->ignored_read_count_); + } + // Get distance and level if either are sensors if ((this->distance_ != nullptr) || (this->level_ != nullptr)) { uint32_t distance_value = this->parse_distance_(manu_data.data); - SensorReadQuality quality_value = this->parse_read_quality_(manu_data.data); - ESP_LOGD(TAG, "Distance Sensor: Quality (0x%X) Distance (%dmm)", quality_value, distance_value); - if (quality_value < QUALITY_HIGH) { - ESP_LOGW(TAG, "Poor read quality."); - } - if (quality_value < QUALITY_MED) { - // if really bad reading set to 0 - ESP_LOGW(TAG, "Setting distance to 0"); - distance_value = 0; - } + ESP_LOGD(TAG, "Distance Sensor: Quality (0x%X) Distance (%" PRId32 "mm)", quality_value, distance_value); - // update distance sensor - if (this->distance_ != nullptr) { - this->distance_->publish_state(distance_value); - } - - // update level sensor - if (this->level_ != nullptr) { - uint8_t tank_level = 0; - if (distance_value >= this->full_mm_) { - tank_level = 100; // cap at 100% - } else if (distance_value > this->empty_mm_) { - tank_level = ((100.0f / (this->full_mm_ - this->empty_mm_)) * (distance_value - this->empty_mm_)); + // only update distance and level sensors if read quality was sufficient. This can be determined by + // if the ignored_read_count is zero. + if (this->ignored_read_count_ == 0) { + // update distance sensor + if (this->distance_ != nullptr) { + this->distance_->publish_state(distance_value); + } + + // update level sensor + if (this->level_ != nullptr) { + uint8_t tank_level = 0; + if (distance_value >= this->full_mm_) { + tank_level = 100; // cap at 100% + } else if (distance_value > this->empty_mm_) { + tank_level = ((100.0f / (this->full_mm_ - this->empty_mm_)) * (distance_value - this->empty_mm_)); + } + this->level_->publish_state(tank_level); } - this->level_->publish_state(tank_level); } } @@ -130,6 +148,8 @@ uint32_t MopekaProCheck::parse_distance_(const std::vector &message) { uint8_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 + // value and the static cast is safe. return static_cast(message[4] >> 6); } diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index 8b126a204c..c58406ac18 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -1,11 +1,12 @@ #pragma once +#include +#include + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#include - #ifdef USE_ESP32 namespace esphome { @@ -16,14 +17,16 @@ enum SensorType { TOP_DOWN_AIR_ABOVE = 0x04, BOTTOM_UP_WATER = 0x05, LIPPERT_BOTTOM_UP = 0x06, - PLUS_BOTTOM_UP = 0x08 + PLUS_BOTTOM_UP = 0x08, + PRO_UNIVERSAL = 0xC // Pro Check Universal + // all other values are reserved }; // Sensor read quality. If sensor is poorly placed or tank level -// gets too low the read quality will show and the distanace +// gets too low the read quality will show and the distance // measurement may be inaccurate. -enum SensorReadQuality { QUALITY_HIGH = 0x3, QUALITY_MED = 0x2, QUALITY_LOW = 0x1, QUALITY_NONE = 0x0 }; +enum SensorReadQuality { QUALITY_HIGH = 0x3, QUALITY_MED = 0x2, QUALITY_LOW = 0x1, QUALITY_ZERO = 0x0 }; class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -32,11 +35,14 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } + void set_min_signal_quality(SensorReadQuality min) { this->min_signal_quality_ = min; }; void set_level(sensor::Sensor *level) { level_ = level; }; void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }; void set_battery_level(sensor::Sensor *bat) { battery_level_ = bat; }; void set_distance(sensor::Sensor *distance) { distance_ = distance; }; + void set_signal_quality(sensor::Sensor *rq) { read_quality_ = rq; }; + void set_ignored_reads(sensor::Sensor *ir) { ignored_reads_ = ir; }; void set_tank_full(float full) { full_mm_ = full; }; void set_tank_empty(float empty) { empty_mm_ = empty; }; @@ -46,9 +52,13 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi sensor::Sensor *temperature_{nullptr}; sensor::Sensor *distance_{nullptr}; sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *read_quality_{nullptr}; + sensor::Sensor *ignored_reads_{nullptr}; uint32_t full_mm_; uint32_t empty_mm_; + uint32_t ignored_read_count_ = 0; + SensorReadQuality min_signal_quality_ = QUALITY_MED; uint8_t parse_battery_level_(const std::vector &message); uint32_t parse_distance_(const std::vector &message); diff --git a/esphome/components/mopeka_pro_check/sensor.py b/esphome/components/mopeka_pro_check/sensor.py index 51a515ef0c..95ade53013 100644 --- a/esphome/components/mopeka_pro_check/sensor.py +++ b/esphome/components/mopeka_pro_check/sensor.py @@ -5,28 +5,34 @@ from esphome.const import ( CONF_DISTANCE, CONF_MAC_ADDRESS, CONF_ID, + ICON_COUNTER, ICON_THERMOMETER, ICON_RULER, + ICON_SIGNAL, UNIT_PERCENT, + UNIT_EMPTY, CONF_LEVEL, CONF_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS, + UNIT_MILLIMETER, STATE_CLASS_MEASUREMENT, CONF_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, ) CONF_TANK_TYPE = "tank_type" CONF_CUSTOM_DISTANCE_FULL = "custom_distance_full" CONF_CUSTOM_DISTANCE_EMPTY = "custom_distance_empty" +CONF_SIGNAL_QUALITY = "signal_quality" +CONF_MINIMUM_SIGNAL_QUALITY = "minimum_signal_quality" +CONF_IGNORED_READS = "ignored_reads" ICON_PROPANE_TANK = "mdi:propane-tank" TANK_TYPE_CUSTOM = "CUSTOM" -UNIT_MILLIMETER = "mm" - def small_distance(value): """small_distance is stored in mm""" @@ -57,6 +63,14 @@ MopekaProCheck = mopeka_pro_check_ns.class_( "MopekaProCheck", esp32_ble_tracker.ESPBTDeviceListener, cg.Component ) +SensorReadQuality = mopeka_pro_check_ns.enum("SensorReadQuality") +SIGNAL_QUALITIES = { + "ZERO": SensorReadQuality.QUALITY_ZERO, + "LOW": SensorReadQuality.QUALITY_LOW, + "MEDIUM": SensorReadQuality.QUALITY_MED, + "HIGH": SensorReadQuality.QUALITY_HIGH, +} + CONFIG_SCHEMA = ( cv.Schema( { @@ -90,6 +104,21 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_SIGNAL_QUALITY): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + icon=ICON_SIGNAL, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IGNORED_READS): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_MINIMUM_SIGNAL_QUALITY, default="MEDIUM"): cv.enum( + SIGNAL_QUALITIES, upper=True + ), } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) @@ -120,6 +149,11 @@ async def to_code(config): cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0])) cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1])) + if ( + minimum_signal_quality := config.get(CONF_MINIMUM_SIGNAL_QUALITY, None) + ) is not None: + cg.add(var.set_min_signal_quality(minimum_signal_quality)) + if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature(sens)) @@ -132,3 +166,9 @@ async def to_code(config): if CONF_BATTERY_LEVEL in config: sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) cg.add(var.set_battery_level(sens)) + if CONF_SIGNAL_QUALITY in config: + sens = await sensor.new_sensor(config[CONF_SIGNAL_QUALITY]) + cg.add(var.set_signal_quality(sens)) + if CONF_IGNORED_READS in config: + sens = await sensor.new_sensor(config[CONF_IGNORED_READS]) + cg.add(var.set_ignored_reads(sens)) diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index ae7b646b9d..6685a23c41 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -16,8 +16,8 @@ static const uint16_t MANUFACTURER_ID = 0x000D; 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: %imm", this->empty_mm_); - ESP_LOGCONFIG(TAG, " Tank distance full: %imm", this->full_mm_); + ESP_LOGCONFIG(TAG, " Tank distance empty: %" PRIi32 "mm", this->empty_mm_); + ESP_LOGCONFIG(TAG, " Tank distance full: %" PRIi32 "mm", this->full_mm_); LOG_SENSOR(" ", "Level", this->level_); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -54,16 +54,16 @@ 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)", device.address_str().c_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()); + ESP_LOGVV(TAG, "[%s] Manufacturer data: %s", device.address_str().c_str(), format_hex_pretty(manu_data.data).c_str()); 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)", device.address_str().c_str(), manu_data.data.size()); return false; } @@ -71,21 +71,22 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const auto *mopeka_data = (const mopeka_std_package *) manu_data.data.data(); const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; - if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL) { - ESP_LOGE(TAG, "%s: Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); + if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL && + static_cast(hardware_id) != ETRAILER) { + ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_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", 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); 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, + 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, + 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, + 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, + 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); } @@ -146,19 +147,19 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // This value is better than a previous one. best_value = measurements_value[i]; best_time = measurement_time; - // Reset measurement_time or next values. - measurement_time = 0; } + // Reset measurement_time or next values. + measurement_time = 0; } } } - ESP_LOGV(TAG, "%s: Found %u values with best data %u time %u.", device.address_str().c_str(), + 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); - if (number_of_usable_values < 2 || best_value < 2 || best_time < 2) { + 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.", device.address_str().c_str()); if (this->distance_ != nullptr) { this->distance_->publish_state(0); } @@ -167,7 +168,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", device.address_str().c_str(), lpg_speed_of_sound); uint32_t distance_value = lpg_speed_of_sound * best_time / 100.0f; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index e4d81afbd7..2a1d9d2dfc 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -14,6 +15,7 @@ namespace mopeka_std_check { enum SensorType { STANDARD = 0x02, XL = 0x03, + ETRAILER = 0x46, }; // 4 values in one struct so it aligns to 8 byte. One `mopeka_std_values` is 40 bit long. diff --git a/esphome/components/mopeka_std_check/sensor.py b/esphome/components/mopeka_std_check/sensor.py index bbba798e95..ac745cf3d5 100644 --- a/esphome/components/mopeka_std_check/sensor.py +++ b/esphome/components/mopeka_std_check/sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS, + UNIT_MILLIMETER, STATE_CLASS_MEASUREMENT, CONF_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, @@ -26,8 +27,6 @@ ICON_PROPANE_TANK = "mdi:propane-tank" TANK_TYPE_CUSTOM = "CUSTOM" -UNIT_MILLIMETER = "mm" - def small_distance(value): """small_distance is stored in mm""" diff --git a/esphome/components/mpr121/__init__.py b/esphome/components/mpr121/__init__.py index dabfb47ad6..1f8e804e88 100644 --- a/esphome/components/mpr121/__init__.py +++ b/esphome/components/mpr121/__init__.py @@ -1,19 +1,32 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome import pins from esphome.components import i2c -from esphome.const import CONF_ID +from esphome.const import ( + CONF_BINARY_SENSOR, + CONF_CHANNEL, + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, +) CONF_TOUCH_THRESHOLD = "touch_threshold" CONF_RELEASE_THRESHOLD = "release_threshold" CONF_TOUCH_DEBOUNCE = "touch_debounce" CONF_RELEASE_DEBOUNCE = "release_debounce" +CONF_MAX_TOUCH_CHANNEL = "max_touch_channel" +CONF_MPR121 = "mpr121" +CONF_MPR121_ID = "mpr121_id" DEPENDENCIES = ["i2c"] -AUTO_LOAD = ["binary_sensor"] mpr121_ns = cg.esphome_ns.namespace("mpr121") -CONF_MPR121_ID = "mpr121_id" MPR121Component = mpr121_ns.class_("MPR121Component", cg.Component, i2c.I2CDevice) +MPR121GPIOPin = mpr121_ns.class_("MPR121GPIOPin", cg.GPIOPin) MULTI_CONF = True CONFIG_SCHEMA = ( @@ -28,6 +41,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_RELEASE_THRESHOLD, default=0x06): cv.int_range( min=0x05, max=0x30 ), + cv.Optional(CONF_MAX_TOUCH_CHANNEL): cv.int_range(min=3, max=11), } ) .extend(cv.COMPONENT_SCHEMA) @@ -35,11 +49,79 @@ CONFIG_SCHEMA = ( ) +def _final_validate(config): + fconf = fv.full_config.get() + max_touch_channel = 3 + if (binary_sensors := fconf.get(CONF_BINARY_SENSOR)) is not None: + for binary_sensor in binary_sensors: + if binary_sensor.get(CONF_MPR121_ID) == config[CONF_ID]: + max_touch_channel = max(max_touch_channel, binary_sensor[CONF_CHANNEL]) + if max_touch_channel_in_config := config.get(CONF_MAX_TOUCH_CHANNEL): + if max_touch_channel != max_touch_channel_in_config: + raise cv.Invalid( + "Max touch channel must equal the highest binary sensor channel or be removed for auto calculation", + path=[CONF_MAX_TOUCH_CHANNEL], + ) + path = fconf.get_path_for_id(config[CONF_ID])[:-1] + this_config = fconf.get_config_for_path(path) + this_config[CONF_MAX_TOUCH_CHANNEL] = max_touch_channel + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_touch_debounce(config[CONF_TOUCH_DEBOUNCE])) cg.add(var.set_release_debounce(config[CONF_RELEASE_DEBOUNCE])) cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD])) cg.add(var.set_release_threshold(config[CONF_RELEASE_THRESHOLD])) + cg.add(var.set_max_touch_channel(config[CONF_MAX_TOUCH_CHANNEL])) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + + +def validate_mode(value): + if bool(value[CONF_INPUT]) == bool(value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + return value + + +# https://www.nxp.com/docs/en/data-sheet/MPR121.pdf, page 4 +# +# Among the 12 electrode inputs, 8 inputs are designed as multifunctional pins. When these pins are +# not configured as electrodes, they may be used to drive LEDs or used for general purpose input or +# output. +MPR121_GPIO_PIN_SCHEMA = pins.gpio_base_schema( + MPR121GPIOPin, + cv.int_range(min=4, max=11), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, +).extend( + { + cv.Required(CONF_MPR121): cv.use_id(MPR121Component), + } +) + + +def mpr121_pin_final_validate(pin_config, parent_config): + if pin_config[CONF_NUMBER] <= parent_config[CONF_MAX_TOUCH_CHANNEL]: + raise cv.Invalid( + "Pin number must be higher than the max touch channel of the MPR121 component", + ) + + +@pins.PIN_SCHEMA_REGISTRY.register( + CONF_MPR121, MPR121_GPIO_PIN_SCHEMA, mpr121_pin_final_validate +) +async def mpr121_gpio_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_MPR121]) + + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/mpr121/binary_sensor.py b/esphome/components/mpr121/binary_sensor.py deleted file mode 100644 index 131fbcfc5b..0000000000 --- a/esphome/components/mpr121/binary_sensor.py +++ /dev/null @@ -1,36 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import binary_sensor -from esphome.const import CONF_CHANNEL -from . import ( - mpr121_ns, - MPR121Component, - CONF_MPR121_ID, - CONF_TOUCH_THRESHOLD, - CONF_RELEASE_THRESHOLD, -) - -DEPENDENCIES = ["mpr121"] -MPR121Channel = mpr121_ns.class_("MPR121Channel", binary_sensor.BinarySensor) - -CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(MPR121Channel).extend( - { - cv.GenerateID(CONF_MPR121_ID): cv.use_id(MPR121Component), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=11), - cv.Optional(CONF_TOUCH_THRESHOLD): cv.int_range(min=0x05, max=0x30), - cv.Optional(CONF_RELEASE_THRESHOLD): cv.int_range(min=0x05, max=0x30), - } -) - - -async def to_code(config): - var = await binary_sensor.new_binary_sensor(config) - hub = await cg.get_variable(config[CONF_MPR121_ID]) - cg.add(var.set_channel(config[CONF_CHANNEL])) - - if CONF_TOUCH_THRESHOLD in config: - cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD])) - if CONF_RELEASE_THRESHOLD in config: - cg.add(var.set_release_threshold(config[CONF_RELEASE_THRESHOLD])) - - cg.add(hub.register_channel(var)) diff --git a/esphome/components/mpr121/binary_sensor/__init__.py b/esphome/components/mpr121/binary_sensor/__init__.py new file mode 100644 index 0000000000..dfae92a9af --- /dev/null +++ b/esphome/components/mpr121/binary_sensor/__init__.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_CHANNEL +from .. import ( + mpr121_ns, + MPR121Component, + CONF_MPR121_ID, + CONF_TOUCH_THRESHOLD, + CONF_RELEASE_THRESHOLD, +) + +DEPENDENCIES = ["mpr121"] +MPR121BinarySensor = mpr121_ns.class_("MPR121BinarySensor", binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(MPR121BinarySensor).extend( + { + cv.GenerateID(CONF_MPR121_ID): cv.use_id(MPR121Component), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=11), + cv.Optional(CONF_TOUCH_THRESHOLD): cv.int_range(min=0x05, max=0x30), + cv.Optional(CONF_RELEASE_THRESHOLD): cv.int_range(min=0x05, max=0x30), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + hub = await cg.get_variable(config[CONF_MPR121_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) + await cg.register_parented(var, hub) + + if CONF_TOUCH_THRESHOLD in config: + cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD])) + if CONF_RELEASE_THRESHOLD in config: + cg.add(var.set_release_threshold(config[CONF_RELEASE_THRESHOLD])) + + cg.add(hub.register_channel(var)) diff --git a/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.cpp b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.cpp new file mode 100644 index 0000000000..dce0e73b9a --- /dev/null +++ b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.cpp @@ -0,0 +1,20 @@ +#include "mpr121_binary_sensor.h" + +namespace esphome { +namespace mpr121 { + +void MPR121BinarySensor::setup() { + uint8_t touch_threshold = this->touch_threshold_.value_or(this->parent_->get_touch_threshold()); + this->parent_->write_byte(MPR121_TOUCHTH_0 + 2 * this->channel_, touch_threshold); + + uint8_t release_threshold = this->release_threshold_.value_or(this->parent_->get_release_threshold()); + this->parent_->write_byte(MPR121_RELEASETH_0 + 2 * this->channel_, release_threshold); +} + +void MPR121BinarySensor::process(uint16_t data) { + bool new_state = data & (1 << this->channel_); + this->publish_state(new_state); +} + +} // namespace mpr121 +} // namespace esphome diff --git a/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h new file mode 100644 index 0000000000..577ba82893 --- /dev/null +++ b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" + +#include "../mpr121.h" + +namespace esphome { +namespace mpr121 { + +class MPR121BinarySensor : public binary_sensor::BinarySensor, public MPR121Channel, public Parented { + public: + void set_channel(uint8_t channel) { this->channel_ = channel; } + void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; }; + void set_release_threshold(uint8_t release_threshold) { this->release_threshold_ = release_threshold; }; + + void setup() override; + void process(uint16_t data) override; + + protected: + uint8_t channel_{0}; + optional touch_threshold_{}; + optional release_threshold_{}; +}; + +} // namespace mpr121 +} // namespace esphome diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 7ba3da7b4d..de364c59ff 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -1,6 +1,9 @@ #include "mpr121.h" -#include "esphome/core/log.h" + +#include + #include "esphome/core/hal.h" +#include "esphome/core/log.h" namespace esphome { namespace mpr121 { @@ -20,10 +23,7 @@ void MPR121Component::setup() { // set touch sensitivity for all 12 channels for (auto *channel : this->channels_) { - this->write_byte(MPR121_TOUCHTH_0 + 2 * channel->channel_, - channel->touch_threshold_.value_or(this->touch_threshold_)); - this->write_byte(MPR121_RELEASETH_0 + 2 * channel->channel_, - channel->release_threshold_.value_or(this->release_threshold_)); + channel->setup(); } this->write_byte(MPR121_MHDR, 0x01); this->write_byte(MPR121_NHDR, 0x01); @@ -44,8 +44,15 @@ void MPR121Component::setup() { this->write_byte(MPR121_CONFIG1, 0x10); // 0.5uS encoding, 1ms period this->write_byte(MPR121_CONFIG2, 0x20); - // start with first 5 bits of baseline tracking - this->write_byte(MPR121_ECR, 0x8F); + + // Write the Electrode Configuration Register + // * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits. + // * The 2 bits below is "Proximity Enable" and are left at 0. + // * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled + // as a range, starting at 0 up to the highest channel index used. + this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); + + this->flush_gpio_(); } void MPR121Component::set_touch_debounce(uint8_t debounce) { @@ -86,6 +93,72 @@ void MPR121Component::loop() { for (auto *channel : this->channels_) channel->process(val); + + this->read_byte(MPR121_GPIODATA, &this->gpio_input_); +} + +bool MPR121Component::digital_read(uint8_t ionum) { return (this->gpio_input_ & (1 << ionum)) != 0; } + +void MPR121Component::digital_write(uint8_t ionum, bool value) { + if (value) { + this->gpio_output_ |= (1 << ionum); + } else { + this->gpio_output_ &= ~(1 << ionum); + } + this->flush_gpio_(); +} + +void MPR121Component::pin_mode(uint8_t ionum, gpio::Flags flags) { + this->gpio_enable_ |= (1 << ionum); + if (flags & gpio::FLAG_INPUT) { + this->gpio_direction_ &= ~(1 << ionum); + } else if (flags & gpio::FLAG_OUTPUT) { + this->gpio_direction_ |= 1 << ionum; + } + this->flush_gpio_(); +} + +bool MPR121Component::flush_gpio_() { + if (this->is_failed()) { + return false; + } + + // TODO: The CTL registers can configure internal pullup/pulldown resistors. + this->write_byte(MPR121_GPIOCTL0, 0x00); + this->write_byte(MPR121_GPIOCTL1, 0x00); + this->write_byte(MPR121_GPIOEN, this->gpio_enable_); + this->write_byte(MPR121_GPIODIR, this->gpio_direction_); + + if (!this->write_byte(MPR121_GPIODATA, this->gpio_output_)) { + this->status_set_warning(); + return false; + } + + this->status_clear_warning(); + return true; +} + +void MPR121GPIOPin::setup() { this->pin_mode(this->flags_); } + +void MPR121GPIOPin::pin_mode(gpio::Flags flags) { + assert(this->pin_ >= 4); + this->parent_->pin_mode(this->pin_ - 4, flags); +} + +bool MPR121GPIOPin::digital_read() { + assert(this->pin_ >= 4); + return this->parent_->digital_read(this->pin_ - 4) != this->inverted_; +} + +void MPR121GPIOPin::digital_write(bool value) { + assert(this->pin_ >= 4); + 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; } } // namespace mpr121 diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index 8b7735fa28..f2dc2fe9c9 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -1,8 +1,10 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + #include "esphome/components/i2c/i2c.h" -#include "esphome/components/binary_sensor/binary_sensor.h" #include @@ -39,6 +41,9 @@ enum { MPR121_UPLIMIT = 0x7D, MPR121_LOWLIMIT = 0x7E, MPR121_TARGETLIMIT = 0x7F, + MPR121_GPIOCTL0 = 0x73, + MPR121_GPIOCTL1 = 0x74, + MPR121_GPIODATA = 0x75, MPR121_GPIODIR = 0x76, MPR121_GPIOEN = 0x77, MPR121_GPIOSET = 0x78, @@ -47,19 +52,10 @@ enum { MPR121_SOFTRESET = 0x80, }; -class MPR121Channel : public binary_sensor::BinarySensor { - friend class MPR121Component; - +class MPR121Channel { public: - void set_channel(uint8_t channel) { channel_ = channel; } - void process(uint16_t data) { this->publish_state(static_cast(data & (1 << this->channel_))); } - void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; }; - void set_release_threshold(uint8_t release_threshold) { this->release_threshold_ = release_threshold; }; - - protected: - uint8_t channel_{0}; - optional touch_threshold_{}; - optional release_threshold_{}; + virtual void setup() = 0; + virtual void process(uint16_t data) = 0; }; class MPR121Component : public Component, public i2c::I2CDevice { @@ -69,23 +65,63 @@ class MPR121Component : public Component, public i2c::I2CDevice { void set_release_debounce(uint8_t debounce); void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; }; void set_release_threshold(uint8_t release_threshold) { this->release_threshold_ = release_threshold; }; - uint8_t get_touch_threshold() { return this->touch_threshold_; }; - uint8_t get_release_threshold() { return this->release_threshold_; }; + uint8_t get_touch_threshold() const { return this->touch_threshold_; }; + uint8_t get_release_threshold() const { return this->release_threshold_; }; void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } + float get_setup_priority() const override { return setup_priority::IO; } void loop() override; + void set_max_touch_channel(uint8_t max_touch_channel) { this->max_touch_channel_ = max_touch_channel; } + + // GPIO helper functions. + bool digital_read(uint8_t ionum); + void digital_write(uint8_t ionum, bool value); + void pin_mode(uint8_t ionum, gpio::Flags flags); + protected: std::vector channels_{}; uint8_t debounce_{0}; uint8_t touch_threshold_{}; uint8_t release_threshold_{}; + uint8_t max_touch_channel_{3}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, WRONG_CHIP_STATE, } error_code_{NONE}; + + bool flush_gpio_(); + + /// The enable mask - zero means high Z, 1 means GPIO usage + uint8_t gpio_enable_{0x00}; + /// Mask for the pin mode - 1 means output, 0 means input + uint8_t gpio_direction_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t gpio_output_{0x00}; + /// The mask to read as input state - 1 means HIGH, 0 means LOW + uint8_t gpio_input_{0x00}; +}; + +/// Helper class to expose a MPR121 pin as an internal input GPIO pin. +class MPR121GPIOPin : public GPIOPin { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(MPR121Component *parent) { this->parent_ = parent; } + 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; } + + protected: + MPR121Component *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; }; } // namespace mpr121 diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index 51e3ec2383..64fcd3a2a8 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -77,7 +77,7 @@ void MPU6050Component::setup() { accel_config &= 0b11100111; accel_config |= (MPU6050_RANGE_2G << 3); ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config)); - if (!this->write_byte(MPU6050_REGISTER_GYRO_CONFIG, gyro_config)) { + if (!this->write_byte(MPU6050_REGISTER_ACCEL_CONFIG, accel_config)) { this->mark_failed(); return; } diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 7207eaddc1..86d163e61d 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,31 +1,36 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg from esphome.components import logger +from esphome.components.esp32 import add_idf_sdkconfig_option +import esphome.config_validation as cv from esphome.const import ( CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CERTIFICATE_AUTHORITY, + CONF_CLEAN_SESSION, + CONF_CLIENT_CERTIFICATE, + CONF_CLIENT_CERTIFICATE_KEY, CONF_CLIENT_ID, - CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, + CONF_COMMAND_TOPIC, CONF_DISCOVERY, + CONF_DISCOVERY_OBJECT_ID_GENERATOR, CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, CONF_DISCOVERY_UNIQUE_ID_GENERATOR, - CONF_DISCOVERY_OBJECT_ID_GENERATOR, + CONF_ENABLE_ON_BOOT, CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, CONF_LOG_TOPIC, - CONF_ON_JSON_MESSAGE, - CONF_ON_MESSAGE, CONF_ON_CONNECT, CONF_ON_DISCONNECT, + CONF_ON_JSON_MESSAGE, + CONF_ON_MESSAGE, CONF_PASSWORD, CONF_PAYLOAD, CONF_PAYLOAD_AVAILABLE, @@ -37,20 +42,29 @@ from esphome.const import ( CONF_SHUTDOWN_MESSAGE, CONF_SSL_FINGERPRINTS, CONF_STATE_TOPIC, + CONF_SUBSCRIBE_QOS, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_TRIGGER_ID, CONF_USE_ABBREVIATIONS, CONF_USERNAME, CONF_WILL_MESSAGE, + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, ) -from esphome.core import coroutine_with_priority, CORE -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.core import CORE, coroutine_with_priority DEPENDENCIES = ["network"] -AUTO_LOAD = ["json"] +def AUTO_LOAD(): + if CORE.is_esp8266 or CORE.is_libretiny: + return ["async_tcp", "json"] + return ["json"] + + +CONF_DISCOVER_IP = "discover_ip" CONF_IDF_SEND_ASYNC = "idf_send_async" CONF_SKIP_CERT_CN_CHECK = "skip_cert_cn_check" @@ -86,6 +100,8 @@ MQTTMessage = mqtt_ns.struct("MQTTMessage") MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component) MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action) MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action) +MQTTEnableAction = mqtt_ns.class_("MQTTEnableAction", automation.Action) +MQTTDisableAction = mqtt_ns.class_("MQTTDisableAction", automation.Action) MQTTMessageTrigger = mqtt_ns.class_( "MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component ) @@ -99,6 +115,9 @@ MQTTDisconnectTrigger = mqtt_ns.class_( MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) +MQTTAlarmControlPanelComponent = mqtt_ns.class_( + "MQTTAlarmControlPanelComponent", MQTTComponent +) MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent) MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent) MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent) @@ -108,9 +127,16 @@ MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent) MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent) MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) +MQTTDateComponent = mqtt_ns.class_("MQTTDateComponent", MQTTComponent) +MQTTTimeComponent = mqtt_ns.class_("MQTTTimeComponent", MQTTComponent) +MQTTDateTimeComponent = mqtt_ns.class_("MQTTDateTimeComponent", MQTTComponent) +MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent) MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent) MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent) +MQTTEventComponent = mqtt_ns.class_("MQTTEventComponent", MQTTComponent) +MQTTUpdateComponent = mqtt_ns.class_("MQTTUpdateComponent", MQTTComponent) +MQTTValveComponent = mqtt_ns.class_("MQTTValveComponent", MQTTComponent) MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator") MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = { @@ -129,33 +155,47 @@ def validate_config(value): # Populate default fields out = value.copy() topic_prefix = value[CONF_TOPIC_PREFIX] + # If the topic prefix is not null and these messages are not configured, then set them to the default + # If the topic prefix is null and these messages are not configured, then set them to null if CONF_BIRTH_MESSAGE not in value: - out[CONF_BIRTH_MESSAGE] = { - CONF_TOPIC: f"{topic_prefix}/status", - CONF_PAYLOAD: "online", - CONF_QOS: 0, - CONF_RETAIN: True, - } + if topic_prefix != "": + out[CONF_BIRTH_MESSAGE] = { + CONF_TOPIC: f"{topic_prefix}/status", + CONF_PAYLOAD: "online", + CONF_QOS: 0, + CONF_RETAIN: True, + } + else: + out[CONF_BIRTH_MESSAGE] = {} if CONF_WILL_MESSAGE not in value: - out[CONF_WILL_MESSAGE] = { - CONF_TOPIC: f"{topic_prefix}/status", - CONF_PAYLOAD: "offline", - CONF_QOS: 0, - CONF_RETAIN: True, - } + if topic_prefix != "": + out[CONF_WILL_MESSAGE] = { + CONF_TOPIC: f"{topic_prefix}/status", + CONF_PAYLOAD: "offline", + CONF_QOS: 0, + CONF_RETAIN: True, + } + else: + out[CONF_WILL_MESSAGE] = {} if CONF_SHUTDOWN_MESSAGE not in value: - out[CONF_SHUTDOWN_MESSAGE] = { - CONF_TOPIC: f"{topic_prefix}/status", - CONF_PAYLOAD: "offline", - CONF_QOS: 0, - CONF_RETAIN: True, - } + if topic_prefix != "": + out[CONF_SHUTDOWN_MESSAGE] = { + CONF_TOPIC: f"{topic_prefix}/status", + CONF_PAYLOAD: "offline", + CONF_QOS: 0, + CONF_RETAIN: True, + } + else: + out[CONF_SHUTDOWN_MESSAGE] = {} if CONF_LOG_TOPIC not in value: - out[CONF_LOG_TOPIC] = { - CONF_TOPIC: f"{topic_prefix}/debug", - CONF_QOS: 0, - CONF_RETAIN: True, - } + if topic_prefix != "": + out[CONF_LOG_TOPIC] = { + CONF_TOPIC: f"{topic_prefix}/debug", + CONF_QOS: 0, + CONF_RETAIN: True, + } + else: + out[CONF_LOG_TOPIC] = {} return out @@ -171,9 +211,11 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(MQTTClientComponent), cv.Required(CONF_BROKER): cv.string_strict, + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_PORT, default=1883): cv.port, cv.Optional(CONF_USERNAME, default=""): cv.string, 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 @@ -181,6 +223,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All( cv.string, cv.only_with_esp_idf ), + cv.Inclusive(CONF_CLIENT_CERTIFICATE, "cert-key-pair"): cv.All( + cv.string, cv.only_on_esp32 + ), + 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 ), @@ -188,6 +236,7 @@ CONFIG_SCHEMA = cv.All( cv.boolean, cv.one_of("CLEAN", upper=True) ), cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean, + cv.Optional(CONF_DISCOVER_IP, default=True): cv.boolean, cv.Optional( CONF_DISCOVERY_PREFIX, default="homeassistant" ): cv.publish_topic, @@ -250,7 +299,7 @@ CONFIG_SCHEMA = cv.All( } ), validate_config, - cv.only_on(["esp32", "esp8266"]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), ) @@ -271,18 +320,20 @@ def exp_mqtt_message(config): async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # Add required libraries for arduino - if CORE.using_arduino: - # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json - cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6") + # Add required libraries for ESP8266 and LibreTiny + if CORE.is_esp8266 or CORE.is_libretiny: + # https://github.com/heman/async-mqtt-client/blob/master/library.json + cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) cg.add(var.set_broker_address(config[CONF_BROKER])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_broker_port(config[CONF_PORT])) cg.add(var.set_username(config[CONF_USERNAME])) cg.add(var.set_password(config[CONF_PASSWORD])) + cg.add(var.set_clean_session(config[CONF_CLEAN_SESSION])) if CONF_CLIENT_ID in config: cg.add(var.set_client_id(config[CONF_CLIENT_ID])) @@ -291,8 +342,12 @@ async def to_code(config): discovery_prefix = config[CONF_DISCOVERY_PREFIX] discovery_unique_id_generator = config[CONF_DISCOVERY_UNIQUE_ID_GENERATOR] discovery_object_id_generator = config[CONF_DISCOVERY_OBJECT_ID_GENERATOR] + discover_ip = config[CONF_DISCOVER_IP] if not discovery: + discovery_prefix = "" + + if not discovery and not discover_ip: cg.add(var.disable_discovery()) elif discovery == "CLEAN": cg.add( @@ -301,6 +356,7 @@ async def to_code(config): discovery_unique_id_generator, discovery_object_id_generator, discovery_retain, + discover_ip, True, ) ) @@ -311,6 +367,7 @@ async def to_code(config): discovery_unique_id_generator, discovery_object_id_generator, discovery_retain, + discover_ip, ) ) @@ -360,6 +417,9 @@ async def to_code(config): if CONF_CERTIFICATE_AUTHORITY in config: cg.add(var.set_ca_certificate(config[CONF_CERTIFICATE_AUTHORITY])) cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK])) + if CONF_CLIENT_CERTIFICATE in config: + cg.add(var.set_cl_certificate(config[CONF_CLIENT_CERTIFICATE])) + cg.add(var.set_cl_key(config[CONF_CLIENT_CERTIFICATE_KEY])) # prevent error -0x428e # See https://github.com/espressif/esp-idf/issues/139 @@ -460,8 +520,12 @@ def get_default_topic_for(data, component_type, name, suffix): async def register_mqtt_component(var, config): await cg.register_component(var, {}) + if CONF_QOS in config: + cg.add(var.set_qos(config[CONF_QOS])) if CONF_RETAIN in config: cg.add(var.set_retain(config[CONF_RETAIN])) + if CONF_SUBSCRIBE_QOS in config: + cg.add(var.set_subscribe_qos(config[CONF_SUBSCRIBE_QOS])) if not config.get(CONF_DISCOVERY, True): cg.add(var.disable_discovery()) if CONF_STATE_TOPIC in config: @@ -496,3 +560,31 @@ async def register_mqtt_component(var, config): async def mqtt_connected_to_code(config, condition_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren) + + +@automation.register_action( + "mqtt.enable", + MQTTEnableAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + } + ), +) +async def mqtt_enable_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( + "mqtt.disable", + MQTTDisableAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + } + ), +) +async def mqtt_disable_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) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp new file mode 100644 index 0000000000..660a030d11 --- /dev/null +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -0,0 +1,128 @@ +#include "mqtt_alarm_control_panel.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_ALARM_CONTROL_PANEL + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.alarm_control_panel"; + +using namespace esphome::alarm_control_panel; + +MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) + : alarm_control_panel_(alarm_control_panel) {} +void MQTTAlarmControlPanelComponent::setup() { + this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { + auto call = this->alarm_control_panel_->make_call(); + if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) { + call.arm_away(); + } else if (strcasecmp(payload.c_str(), "ARM_HOME") == 0) { + call.arm_home(); + } else if (strcasecmp(payload.c_str(), "ARM_NIGHT") == 0) { + call.arm_night(); + } else if (strcasecmp(payload.c_str(), "ARM_VACATION") == 0) { + call.arm_vacation(); + } else if (strcasecmp(payload.c_str(), "ARM_CUSTOM_BYPASS") == 0) { + call.arm_custom_bypass(); + } else if (strcasecmp(payload.c_str(), "DISARM") == 0) { + call.disarm(); + } else if (strcasecmp(payload.c_str(), "PENDING") == 0) { + call.pending(); + } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { + call.triggered(); + } else { + ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); + } + call.perform(); + }); +} + +void MQTTAlarmControlPanelComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) + ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->alarm_control_panel_->get_supported_features()); + ESP_LOGCONFIG(TAG, " Requires Code to Disarm: %s", YESNO(this->alarm_control_panel_->get_requires_code())); + ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->alarm_control_panel_->get_requires_code_to_arm())); +} + +void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); + 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"); + } + if (acp_supported_features & ACP_FEAT_ARM_HOME) { + supported_features.add("arm_home"); + } + if (acp_supported_features & ACP_FEAT_ARM_NIGHT) { + supported_features.add("arm_night"); + } + if (acp_supported_features & ACP_FEAT_ARM_VACATION) { + supported_features.add("arm_vacation"); + } + if (acp_supported_features & ACP_FEAT_ARM_CUSTOM_BYPASS) { + supported_features.add("arm_custom_bypass"); + } + if (acp_supported_features & ACP_FEAT_TRIGGER) { + supported_features.add("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(); +} + +std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; } +const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; } + +bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } +bool MQTTAlarmControlPanelComponent::publish_state() { + bool success = true; + const char *state_s = ""; + switch (this->alarm_control_panel_->get_state()) { + case ACP_STATE_DISARMED: + state_s = "disarmed"; + break; + case ACP_STATE_ARMED_HOME: + state_s = "armed_home"; + break; + case ACP_STATE_ARMED_AWAY: + state_s = "armed_away"; + break; + case ACP_STATE_ARMED_NIGHT: + state_s = "armed_night"; + break; + case ACP_STATE_ARMED_VACATION: + state_s = "armed_vacation"; + break; + case ACP_STATE_ARMED_CUSTOM_BYPASS: + state_s = "armed_custom_bypass"; + break; + case ACP_STATE_PENDING: + state_s = "pending"; + break; + case ACP_STATE_ARMING: + state_s = "arming"; + break; + case ACP_STATE_DISARMING: + state_s = "disarming"; + break; + case ACP_STATE_TRIGGERED: + state_s = "triggered"; + break; + default: + state_s = "unknown"; + } + if (!this->publish(this->get_state_topic_(), state_s)) + success = false; + return success; +} + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h new file mode 100644 index 0000000000..4ad37b7314 --- /dev/null +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_ALARM_CONTROL_PANEL + +#include "mqtt_component.h" +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" + +namespace esphome { +namespace mqtt { + +class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { + public: + explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel); + + void setup() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(); + + void dump_config() override; + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + alarm_control_panel::AlarmControlPanel *alarm_control_panel_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h index d23cda578d..3962c40a42 100644 --- a/esphome/components/mqtt/mqtt_backend.h +++ b/esphome/components/mqtt/mqtt_backend.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_MQTT #include #include #include "esphome/components/network/ip_address.h" @@ -67,3 +68,4 @@ class MQTTBackend { } // namespace mqtt } // namespace esphome +#endif diff --git a/esphome/components/mqtt/mqtt_backend_arduino.h b/esphome/components/mqtt/mqtt_backend_arduino.h deleted file mode 100644 index 6399ec88e0..0000000000 --- a/esphome/components/mqtt/mqtt_backend_arduino.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#ifdef USE_ARDUINO - -#include "mqtt_backend.h" -#include - -namespace esphome { -namespace mqtt { - -class MQTTBackendArduino final : public MQTTBackend { - public: - void set_keep_alive(uint16_t keep_alive) final { mqtt_client_.setKeepAlive(keep_alive); } - void set_client_id(const char *client_id) final { mqtt_client_.setClientId(client_id); } - void set_clean_session(bool clean_session) final { mqtt_client_.setCleanSession(clean_session); } - void set_credentials(const char *username, const char *password) final { - mqtt_client_.setCredentials(username, password); - } - void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { - mqtt_client_.setWill(topic, qos, retain, payload); - } - void set_server(network::IPAddress ip, uint16_t port) final { - mqtt_client_.setServer(IPAddress(static_cast(ip)), port); - } - void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - - void set_on_connect(std::function &&callback) final { - this->mqtt_client_.onConnect(std::move(callback)); - } - void set_on_disconnect(std::function &&callback) final { - auto async_callback = [callback](AsyncMqttClientDisconnectReason reason) { - // int based enum so casting isn't a problem - callback(static_cast(reason)); - }; - this->mqtt_client_.onDisconnect(std::move(async_callback)); - } - void set_on_subscribe(std::function &&callback) final { - this->mqtt_client_.onSubscribe(std::move(callback)); - } - void set_on_unsubscribe(std::function &&callback) final { - this->mqtt_client_.onUnsubscribe(std::move(callback)); - } - void set_on_message(std::function &&callback) final { - auto async_callback = [callback](const char *topic, const char *payload, - AsyncMqttClientMessageProperties async_properties, size_t len, size_t index, - size_t total) { callback(topic, payload, len, index, total); }; - mqtt_client_.onMessage(std::move(async_callback)); - } - void set_on_publish(std::function &&callback) final { - this->mqtt_client_.onPublish(std::move(callback)); - } - - bool connected() const final { return mqtt_client_.connected(); } - void connect() final { mqtt_client_.connect(); } - void disconnect() final { mqtt_client_.disconnect(true); } - bool subscribe(const char *topic, uint8_t qos) final { return mqtt_client_.subscribe(topic, qos) != 0; } - bool unsubscribe(const char *topic) final { return mqtt_client_.unsubscribe(topic) != 0; } - bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { - return mqtt_client_.publish(topic, qos, retain, payload, length, false, 0) != 0; - } - using MQTTBackend::publish; - - protected: - AsyncMqttClient mqtt_client_; -}; - -} // namespace mqtt -} // namespace esphome - -#endif // defined(USE_ARDUINO) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp new file mode 100644 index 0000000000..ed500c6d44 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -0,0 +1,194 @@ +#include "mqtt_backend_esp32.h" + +#ifdef USE_MQTT +#ifdef USE_ESP32 + +#include +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.idf"; + +bool MQTTBackendESP32::initialize_() { +#if ESP_IDF_VERSION_MAJOR < 5 + mqtt_cfg_.user_context = (void *) this; + mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE; + + mqtt_cfg_.host = this->host_.c_str(); + mqtt_cfg_.port = this->port_; + mqtt_cfg_.keepalive = this->keep_alive_; + mqtt_cfg_.disable_clean_session = !this->clean_session_; + + if (!this->username_.empty()) { + mqtt_cfg_.username = this->username_.c_str(); + if (!this->password_.empty()) { + mqtt_cfg_.password = this->password_.c_str(); + } + } + + if (!this->lwt_topic_.empty()) { + mqtt_cfg_.lwt_topic = this->lwt_topic_.c_str(); + this->mqtt_cfg_.lwt_qos = this->lwt_qos_; + this->mqtt_cfg_.lwt_retain = this->lwt_retain_; + + if (!this->lwt_message_.empty()) { + mqtt_cfg_.lwt_msg = this->lwt_message_.c_str(); + mqtt_cfg_.lwt_msg_len = this->lwt_message_.size(); + } + } + + if (!this->client_id_.empty()) { + mqtt_cfg_.client_id = this->client_id_.c_str(); + } + if (ca_certificate_.has_value()) { + mqtt_cfg_.cert_pem = ca_certificate_.value().c_str(); + mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_; + mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL; + + if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) { + mqtt_cfg_.client_cert_pem = this->cl_certificate_.value().c_str(); + mqtt_cfg_.client_key_pem = this->cl_key_.value().c_str(); + } + } else { + mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; + } +#else + mqtt_cfg_.broker.address.hostname = this->host_.c_str(); + mqtt_cfg_.broker.address.port = this->port_; + mqtt_cfg_.session.keepalive = this->keep_alive_; + mqtt_cfg_.session.disable_clean_session = !this->clean_session_; + + if (!this->username_.empty()) { + mqtt_cfg_.credentials.username = this->username_.c_str(); + if (!this->password_.empty()) { + mqtt_cfg_.credentials.authentication.password = this->password_.c_str(); + } + } + + if (!this->lwt_topic_.empty()) { + mqtt_cfg_.session.last_will.topic = this->lwt_topic_.c_str(); + this->mqtt_cfg_.session.last_will.qos = this->lwt_qos_; + this->mqtt_cfg_.session.last_will.retain = this->lwt_retain_; + + if (!this->lwt_message_.empty()) { + mqtt_cfg_.session.last_will.msg = this->lwt_message_.c_str(); + mqtt_cfg_.session.last_will.msg_len = this->lwt_message_.size(); + } + } + + if (!this->client_id_.empty()) { + mqtt_cfg_.credentials.client_id = this->client_id_.c_str(); + } + if (ca_certificate_.has_value()) { + mqtt_cfg_.broker.verification.certificate = ca_certificate_.value().c_str(); + mqtt_cfg_.broker.verification.skip_cert_common_name_check = skip_cert_cn_check_; + mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_SSL; + + if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) { + mqtt_cfg_.credentials.authentication.certificate = this->cl_certificate_.value().c_str(); + mqtt_cfg_.credentials.authentication.key = this->cl_key_.value().c_str(); + } + } else { + mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; + } +#endif + auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_); + if (mqtt_client) { + handler_.reset(mqtt_client); + is_initalized_ = true; + esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this); + return true; + } else { + ESP_LOGE(TAG, "Failed to initialize IDF-MQTT"); + return false; + } +} + +void MQTTBackendESP32::loop() { + // process new events + // handle only 1 message per loop iteration + if (!mqtt_events_.empty()) { + auto &event = mqtt_events_.front(); + mqtt_event_handler_(event); + mqtt_events_.pop(); + } +} + +void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { + ESP_LOGV(TAG, "Event dispatched from event loop event_id=%d", event.event_id); + switch (event.event_id) { + case MQTT_EVENT_BEFORE_CONNECT: + ESP_LOGV(TAG, "MQTT_EVENT_BEFORE_CONNECT"); + break; + + case MQTT_EVENT_CONNECTED: + ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED"); + this->is_connected_ = true; + this->on_connect_.call(event.session_present); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED"); + // TODO is there a way to get the disconnect reason? + this->is_connected_ = false; + this->on_disconnect_.call(MQTTClientDisconnectReason::TCP_DISCONNECTED); + break; + + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGV(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event.msg_id); + // hardcode QoS to 0. QoS is not used in this context but required to mirror the AsyncMqtt interface + this->on_subscribe_.call((int) event.msg_id, 0); + break; + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGV(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event.msg_id); + this->on_unsubscribe_.call((int) event.msg_id); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGV(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event.msg_id); + this->on_publish_.call((int) event.msg_id); + break; + case MQTT_EVENT_DATA: { + static std::string topic; + if (event.topic.length() > 0) { + topic = event.topic; + } + ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str()); + this->on_message_.call(event.topic.length() > 0 ? topic.c_str() : nullptr, event.data.data(), event.data.size(), + event.current_data_offset, event.total_data_len); + } break; + 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)); + } 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 { + ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle.error_type); + } + break; + default: + ESP_LOGV(TAG, "Other event id:%d", event.event_id); + break; + } +} + +/// static - Dispatch event to instance method +void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, + void *event_data) { + MQTTBackendESP32 *instance = static_cast(handler_args); + // queue event to decouple processing + if (instance) { + auto event = *static_cast(event_data); + instance->mqtt_events_.push(Event(event)); + } +} + +} // namespace mqtt +} // namespace esphome +#endif // USE_ESP32 +#endif diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h new file mode 100644 index 0000000000..9054702115 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -0,0 +1,178 @@ +#pragma once + +#include "mqtt_backend.h" +#ifdef USE_MQTT +#ifdef USE_ESP32 + +#include +#include +#include +#include "esphome/components/network/ip_address.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace mqtt { + +struct Event { + esp_mqtt_event_id_t event_id; + std::vector data; + int total_data_len; + int current_data_offset; + std::string topic; + int msg_id; + bool retain; + int qos; + bool dup; + bool session_present; + esp_mqtt_error_codes_t error_handle; + + // Construct from esp_mqtt_event_t + // Any pointer values that are unsafe to keep are converted to safe copies + Event(const esp_mqtt_event_t &event) + : event_id(event.event_id), + data(event.data, event.data + event.data_len), + total_data_len(event.total_data_len), + current_data_offset(event.current_data_offset), + topic(event.topic, event.topic_len), + msg_id(event.msg_id), + retain(event.retain), + qos(event.qos), + dup(event.dup), + session_present(event.session_present), + error_handle(*event.error_handle) {} +}; + +class MQTTBackendESP32 final : public MQTTBackend { + public: + static const size_t MQTT_BUFFER_SIZE = 4096; + + void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } + void set_client_id(const char *client_id) final { this->client_id_ = client_id; } + void set_clean_session(bool clean_session) final { this->clean_session_ = clean_session; } + + void set_credentials(const char *username, const char *password) final { + if (username) + this->username_ = username; + if (password) + this->password_ = password; + } + void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { + if (topic) + this->lwt_topic_ = topic; + this->lwt_qos_ = qos; + if (payload) + this->lwt_message_ = payload; + this->lwt_retain_ = retain; + } + void set_server(network::IPAddress ip, uint16_t port) final { + this->host_ = ip.str(); + this->port_ = port; + } + void set_server(const char *host, uint16_t port) final { + this->host_ = host; + this->port_ = port; + } + void set_on_connect(std::function &&callback) final { + this->on_connect_.add(std::move(callback)); + } + void set_on_disconnect(std::function &&callback) final { + this->on_disconnect_.add(std::move(callback)); + } + void set_on_subscribe(std::function &&callback) final { + this->on_subscribe_.add(std::move(callback)); + } + void set_on_unsubscribe(std::function &&callback) final { + this->on_unsubscribe_.add(std::move(callback)); + } + void set_on_message(std::function &&callback) final { + this->on_message_.add(std::move(callback)); + } + void set_on_publish(std::function &&callback) final { + this->on_publish_.add(std::move(callback)); + } + bool connected() const final { return this->is_connected_; } + + void connect() final { + if (!is_initalized_) { + if (initialize_()) { + esp_mqtt_client_start(handler_.get()); + } + } + } + void disconnect() final { + if (is_initalized_) + esp_mqtt_client_disconnect(handler_.get()); + } + + bool subscribe(const char *topic, uint8_t qos) final { + return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1; + } + bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; } + + bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + // use the non-blocking version + // it can delay sending a couple of seconds but won't block + return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1; +#else + // might block for several seconds, either due to network timeout (10s) + // or if publishing payloads longer than internal buffer (due to message fragmentation) + return esp_mqtt_client_publish(handler_.get(), topic, payload, length, qos, retain) != -1; +#endif + } + using MQTTBackend::publish; + + void loop() final; + + void set_ca_certificate(const std::string &cert) { ca_certificate_ = cert; } + void set_cl_certificate(const std::string &cert) { cl_certificate_ = cert; } + void set_cl_key(const std::string &key) { cl_key_ = key; } + void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } + + protected: + bool initialize_(); + void mqtt_event_handler_(const Event &event); + static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); + + struct MqttClientDeleter { + void operator()(esp_mqtt_client *client_handler) { esp_mqtt_client_destroy(client_handler); } + }; + using ClientHandler_ = std::unique_ptr; + ClientHandler_ handler_; + + bool is_connected_{false}; + bool is_initalized_{false}; + + esp_mqtt_client_config_t mqtt_cfg_{}; + + std::string host_; + uint16_t port_; + std::string username_; + std::string password_; + std::string lwt_topic_; + std::string lwt_message_; + uint8_t lwt_qos_; + bool lwt_retain_; + std::string client_id_; + uint16_t keep_alive_; + bool clean_session_; + optional ca_certificate_; + optional cl_certificate_; + optional cl_key_; + bool skip_cert_cn_check_{false}; + + // callbacks + CallbackManager on_connect_; + CallbackManager on_disconnect_; + CallbackManager on_subscribe_; + CallbackManager on_unsubscribe_; + CallbackManager on_message_; + CallbackManager on_publish_; + std::queue mqtt_events_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h new file mode 100644 index 0000000000..a979634bf4 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -0,0 +1,74 @@ +#pragma once +#include "mqtt_backend.h" + +#ifdef USE_MQTT +#ifdef USE_ESP8266 + +#include + +namespace esphome { +namespace mqtt { + +class MQTTBackendESP8266 final : public MQTTBackend { + public: + void set_keep_alive(uint16_t keep_alive) final { mqtt_client_.setKeepAlive(keep_alive); } + void set_client_id(const char *client_id) final { mqtt_client_.setClientId(client_id); } + void set_clean_session(bool clean_session) final { mqtt_client_.setCleanSession(clean_session); } + void set_credentials(const char *username, const char *password) final { + mqtt_client_.setCredentials(username, password); + } + void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { + mqtt_client_.setWill(topic, qos, retain, payload); + } + void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); } + void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } +#if ASYNC_TCP_SSL_ENABLED + void set_secure(bool secure) { mqtt_client.setSecure(secure); } + void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } +#endif + + void set_on_connect(std::function &&callback) final { + this->mqtt_client_.onConnect(std::move(callback)); + } + void set_on_disconnect(std::function &&callback) final { + auto async_callback = [callback](AsyncMqttClientDisconnectReason reason) { + // int based enum so casting isn't a problem + callback(static_cast(reason)); + }; + this->mqtt_client_.onDisconnect(std::move(async_callback)); + } + void set_on_subscribe(std::function &&callback) final { + this->mqtt_client_.onSubscribe(std::move(callback)); + } + void set_on_unsubscribe(std::function &&callback) final { + this->mqtt_client_.onUnsubscribe(std::move(callback)); + } + void set_on_message(std::function &&callback) final { + auto async_callback = [callback](const char *topic, const char *payload, + AsyncMqttClientMessageProperties async_properties, size_t len, size_t index, + size_t total) { callback(topic, payload, len, index, total); }; + mqtt_client_.onMessage(std::move(async_callback)); + } + void set_on_publish(std::function &&callback) final { + this->mqtt_client_.onPublish(std::move(callback)); + } + + bool connected() const final { return mqtt_client_.connected(); } + void connect() final { mqtt_client_.connect(); } + void disconnect() final { mqtt_client_.disconnect(true); } + bool subscribe(const char *topic, uint8_t qos) final { return mqtt_client_.subscribe(topic, qos) != 0; } + bool unsubscribe(const char *topic) final { return mqtt_client_.unsubscribe(topic) != 0; } + bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { + return mqtt_client_.publish(topic, qos, retain, payload, length, false, 0) != 0; + } + using MQTTBackend::publish; + + protected: + AsyncMqttClient mqtt_client_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // defined(USE_ESP8266) +#endif diff --git a/esphome/components/mqtt/mqtt_backend_idf.cpp b/esphome/components/mqtt/mqtt_backend_idf.cpp deleted file mode 100644 index 812e36d522..0000000000 --- a/esphome/components/mqtt/mqtt_backend_idf.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#ifdef USE_ESP_IDF - -#include -#include "mqtt_backend_idf.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace mqtt { - -static const char *const TAG = "mqtt.idf"; - -bool MQTTBackendIDF::initialize_() { - mqtt_cfg_.user_context = (void *) this; - mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE; - - mqtt_cfg_.host = this->host_.c_str(); - mqtt_cfg_.port = this->port_; - mqtt_cfg_.keepalive = this->keep_alive_; - mqtt_cfg_.disable_clean_session = !this->clean_session_; - - if (!this->username_.empty()) { - mqtt_cfg_.username = this->username_.c_str(); - if (!this->password_.empty()) { - mqtt_cfg_.password = this->password_.c_str(); - } - } - - if (!this->lwt_topic_.empty()) { - mqtt_cfg_.lwt_topic = this->lwt_topic_.c_str(); - this->mqtt_cfg_.lwt_qos = this->lwt_qos_; - this->mqtt_cfg_.lwt_retain = this->lwt_retain_; - - if (!this->lwt_message_.empty()) { - mqtt_cfg_.lwt_msg = this->lwt_message_.c_str(); - mqtt_cfg_.lwt_msg_len = this->lwt_message_.size(); - } - } - - if (!this->client_id_.empty()) { - mqtt_cfg_.client_id = this->client_id_.c_str(); - } - if (ca_certificate_.has_value()) { - mqtt_cfg_.cert_pem = ca_certificate_.value().c_str(); - mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_; - mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL; - } else { - mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; - } - auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_); - if (mqtt_client) { - handler_.reset(mqtt_client); - is_initalized_ = true; - esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this); - return true; - } else { - ESP_LOGE(TAG, "Failed to initialize IDF-MQTT"); - return false; - } -} - -void MQTTBackendIDF::loop() { - // process new events - // handle only 1 message per loop iteration - if (!mqtt_events_.empty()) { - auto &event = mqtt_events_.front(); - mqtt_event_handler_(event); - mqtt_events_.pop(); - } -} - -void MQTTBackendIDF::mqtt_event_handler_(const Event &event) { - ESP_LOGV(TAG, "Event dispatched from event loop event_id=%d", event.event_id); - switch (event.event_id) { - case MQTT_EVENT_BEFORE_CONNECT: - ESP_LOGV(TAG, "MQTT_EVENT_BEFORE_CONNECT"); - break; - - case MQTT_EVENT_CONNECTED: - ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED"); - // TODO session present check - this->is_connected_ = true; - this->on_connect_.call(!mqtt_cfg_.disable_clean_session); - break; - case MQTT_EVENT_DISCONNECTED: - ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED"); - // TODO is there a way to get the disconnect reason? - this->is_connected_ = false; - this->on_disconnect_.call(MQTTClientDisconnectReason::TCP_DISCONNECTED); - break; - - case MQTT_EVENT_SUBSCRIBED: - ESP_LOGV(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event.msg_id); - // hardcode QoS to 0. QoS is not used in this context but required to mirror the AsyncMqtt interface - this->on_subscribe_.call((int) event.msg_id, 0); - break; - case MQTT_EVENT_UNSUBSCRIBED: - ESP_LOGV(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event.msg_id); - this->on_unsubscribe_.call((int) event.msg_id); - break; - case MQTT_EVENT_PUBLISHED: - ESP_LOGV(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event.msg_id); - this->on_publish_.call((int) event.msg_id); - break; - case MQTT_EVENT_DATA: { - static std::string topic; - if (event.topic.length() > 0) { - topic = event.topic; - } - ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str()); - this->on_message_.call(event.topic.length() > 0 ? topic.c_str() : nullptr, event.data.data(), event.data.size(), - event.current_data_offset, event.total_data_len); - } break; - 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)); - } 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 { - ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle.error_type); - } - break; - default: - ESP_LOGV(TAG, "Other event id:%d", event.event_id); - break; - } -} - -/// static - Dispatch event to instance method -void MQTTBackendIDF::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { - MQTTBackendIDF *instance = static_cast(handler_args); - // queue event to decouple processing - if (instance) { - auto event = *static_cast(event_data); - instance->mqtt_events_.push(Event(event)); - } -} - -} // namespace mqtt -} // namespace esphome -#endif // USE_ESP_IDF diff --git a/esphome/components/mqtt/mqtt_backend_idf.h b/esphome/components/mqtt/mqtt_backend_idf.h deleted file mode 100644 index 900ee9709b..0000000000 --- a/esphome/components/mqtt/mqtt_backend_idf.h +++ /dev/null @@ -1,170 +0,0 @@ -#pragma once - -#ifdef USE_ESP_IDF - -#include -#include -#include -#include "esphome/components/network/ip_address.h" -#include "esphome/core/helpers.h" -#include "mqtt_backend.h" - -namespace esphome { -namespace mqtt { - -struct Event { - esp_mqtt_event_id_t event_id; - std::vector data; - int total_data_len; - int current_data_offset; - std::string topic; - int msg_id; - bool retain; - int qos; - bool dup; - esp_mqtt_error_codes_t error_handle; - - // Construct from esp_mqtt_event_t - // Any pointer values that are unsafe to keep are converted to safe copies - Event(const esp_mqtt_event_t &event) - : event_id(event.event_id), - data(event.data, event.data + event.data_len), - total_data_len(event.total_data_len), - current_data_offset(event.current_data_offset), - topic(event.topic, event.topic_len), - msg_id(event.msg_id), - retain(event.retain), - qos(event.qos), - dup(event.dup), - error_handle(*event.error_handle) {} -}; - -class MQTTBackendIDF final : public MQTTBackend { - public: - static const size_t MQTT_BUFFER_SIZE = 4096; - - void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } - void set_client_id(const char *client_id) final { this->client_id_ = client_id; } - void set_clean_session(bool clean_session) final { this->clean_session_ = clean_session; } - - void set_credentials(const char *username, const char *password) final { - if (username) - this->username_ = username; - if (password) - this->password_ = password; - } - void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { - if (topic) - this->lwt_topic_ = topic; - this->lwt_qos_ = qos; - if (payload) - this->lwt_message_ = payload; - this->lwt_retain_ = retain; - } - void set_server(network::IPAddress ip, uint16_t port) final { - this->host_ = ip.str(); - this->port_ = port; - } - void set_server(const char *host, uint16_t port) final { - this->host_ = host; - this->port_ = port; - } - void set_on_connect(std::function &&callback) final { - this->on_connect_.add(std::move(callback)); - } - void set_on_disconnect(std::function &&callback) final { - this->on_disconnect_.add(std::move(callback)); - } - void set_on_subscribe(std::function &&callback) final { - this->on_subscribe_.add(std::move(callback)); - } - void set_on_unsubscribe(std::function &&callback) final { - this->on_unsubscribe_.add(std::move(callback)); - } - void set_on_message(std::function &&callback) final { - this->on_message_.add(std::move(callback)); - } - void set_on_publish(std::function &&callback) final { - this->on_publish_.add(std::move(callback)); - } - bool connected() const final { return this->is_connected_; } - - void connect() final { - if (!is_initalized_) { - if (initialize_()) { - esp_mqtt_client_start(handler_.get()); - } - } - } - void disconnect() final { - if (is_initalized_) - esp_mqtt_client_disconnect(handler_.get()); - } - - bool subscribe(const char *topic, uint8_t qos) final { - return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1; - } - bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; } - - bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { -#if defined(USE_MQTT_IDF_ENQUEUE) - // use the non-blocking version - // it can delay sending a couple of seconds but won't block - return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1; -#else - // might block for several seconds, either due to network timeout (10s) - // or if publishing payloads longer than internal buffer (due to message fragmentation) - return esp_mqtt_client_publish(handler_.get(), topic, payload, length, qos, retain) != -1; -#endif - } - using MQTTBackend::publish; - - void loop() final; - - void set_ca_certificate(const std::string &cert) { ca_certificate_ = cert; } - void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } - - protected: - bool initialize_(); - void mqtt_event_handler_(const Event &event); - static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); - - struct MqttClientDeleter { - void operator()(esp_mqtt_client *client_handler) { esp_mqtt_client_destroy(client_handler); } - }; - using ClientHandler_ = std::unique_ptr; - ClientHandler_ handler_; - - bool is_connected_{false}; - bool is_initalized_{false}; - - esp_mqtt_client_config_t mqtt_cfg_{}; - - std::string host_; - uint16_t port_; - std::string username_; - std::string password_; - std::string lwt_topic_; - std::string lwt_message_; - uint8_t lwt_qos_; - bool lwt_retain_; - std::string client_id_; - uint16_t keep_alive_; - bool clean_session_; - optional ca_certificate_; - bool skip_cert_cn_check_{false}; - - // callbacks - CallbackManager on_connect_; - CallbackManager on_disconnect_; - CallbackManager on_subscribe_; - CallbackManager on_unsubscribe_; - CallbackManager on_message_; - CallbackManager on_publish_; - std::queue mqtt_events_; -}; - -} // namespace mqtt -} // namespace esphome - -#endif diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h new file mode 100644 index 0000000000..2578ae9941 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -0,0 +1,74 @@ +#pragma once +#include "mqtt_backend.h" + +#ifdef USE_MQTT +#ifdef USE_LIBRETINY + +#include + +namespace esphome { +namespace mqtt { + +class MQTTBackendLibreTiny final : public MQTTBackend { + public: + void set_keep_alive(uint16_t keep_alive) final { mqtt_client_.setKeepAlive(keep_alive); } + void set_client_id(const char *client_id) final { mqtt_client_.setClientId(client_id); } + void set_clean_session(bool clean_session) final { mqtt_client_.setCleanSession(clean_session); } + void set_credentials(const char *username, const char *password) final { + mqtt_client_.setCredentials(username, password); + } + void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { + mqtt_client_.setWill(topic, qos, retain, payload); + } + void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } + void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } +#if ASYNC_TCP_SSL_ENABLED + void set_secure(bool secure) { mqtt_client.setSecure(secure); } + void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } +#endif + + void set_on_connect(std::function &&callback) final { + this->mqtt_client_.onConnect(std::move(callback)); + } + void set_on_disconnect(std::function &&callback) final { + auto async_callback = [callback](AsyncMqttClientDisconnectReason reason) { + // int based enum so casting isn't a problem + callback(static_cast(reason)); + }; + this->mqtt_client_.onDisconnect(std::move(async_callback)); + } + void set_on_subscribe(std::function &&callback) final { + this->mqtt_client_.onSubscribe(std::move(callback)); + } + void set_on_unsubscribe(std::function &&callback) final { + this->mqtt_client_.onUnsubscribe(std::move(callback)); + } + void set_on_message(std::function &&callback) final { + auto async_callback = [callback](const char *topic, const char *payload, + AsyncMqttClientMessageProperties async_properties, size_t len, size_t index, + size_t total) { callback(topic, payload, len, index, total); }; + mqtt_client_.onMessage(std::move(async_callback)); + } + void set_on_publish(std::function &&callback) final { + this->mqtt_client_.onPublish(std::move(callback)); + } + + bool connected() const final { return mqtt_client_.connected(); } + void connect() final { mqtt_client_.connect(); } + void disconnect() final { mqtt_client_.disconnect(true); } + bool subscribe(const char *topic, uint8_t qos) final { return mqtt_client_.subscribe(topic, qos) != 0; } + bool unsubscribe(const char *topic) final { return mqtt_client_.unsubscribe(topic) != 0; } + bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { + return mqtt_client_.publish(topic, qos, retain, payload, length, false, 0) != 0; + } + using MQTTBackend::publish; + + protected: + AsyncMqttClient mqtt_client_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // defined(USE_LIBRETINY) +#endif diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 79e6989a8f..6d12e88391 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -25,7 +25,7 @@ void MQTTBinarySensorComponent::dump_config() { MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor) : binary_sensor_(binary_sensor) { if (this->binary_sensor_->is_status_binary_sensor()) { - this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic); + this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic.c_str()); } } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index cb5d306976..106192c0e3 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -50,6 +50,8 @@ void MQTTClientComponent::setup() { } }); this->mqtt_backend_.set_on_disconnect([this](MQTTClientDisconnectReason reason) { + if (this->state_ == MQTT_CLIENT_DISABLED) + return; this->state_ = MQTT_CLIENT_DISCONNECTED; this->disconnect_reason_ = reason; }); @@ -66,31 +68,43 @@ void MQTTClientComponent::setup() { } #endif - this->subscribe( - "esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, - 2); + if (this->is_discovery_ip_enabled()) { + this->subscribe( + "esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, + 2); - std::string topic = "esphome/ping/"; - topic.append(App.get_name()); - this->subscribe( - topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); + std::string topic = "esphome/ping/"; + topic.append(App.get_name()); + this->subscribe( + topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); + } - this->last_connected_ = millis(); - this->start_dnslookup_(); + if (this->enable_on_boot_) { + this->enable(); + } } void MQTTClientComponent::send_device_info_() { - if (!this->is_connected()) { + if (!this->is_connected() or !this->is_discovery_ip_enabled()) { return; } std::string topic = "esphome/discover/"; topic.append(App.get_name()); + this->publish_json( topic, [](JsonObject root) { - auto ip = network::get_ip_address(); - root["ip"] = ip.str(); + uint8_t index = 0; + for (auto &ip : network::get_ip_addresses()) { + if (ip.is_set()) { + root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str(); + index++; + } + } root["name"] = App.get_name(); + if (!App.get_friendly_name().empty()) { + root["friendly_name"] = App.get_friendly_name(); + } #ifdef USE_API root["port"] = api::global_api_server->get_port(); #endif @@ -103,6 +117,9 @@ void MQTTClientComponent::send_device_info_() { #ifdef USE_ESP32 root["platform"] = "ESP32"; #endif +#ifdef USE_LIBRETINY + root["platform"] = lt_cpu_get_model_name(); +#endif root["board"] = ESPHOME_BOARD; #if defined(USE_WIFI) @@ -119,6 +136,10 @@ void MQTTClientComponent::send_device_info_() { #ifdef USE_DASHBOARD_IMPORT root["package_import_url"] = dashboard_import::get_package_import_url(); #endif + +#ifdef USE_API_NOISE + root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; +#endif }, 2, this->discovery_info_.retain); } @@ -129,6 +150,10 @@ void MQTTClientComponent::dump_config() { this->ip_.str().c_str()); ESP_LOGCONFIG(TAG, " Username: " LOG_SECRET("'%s'"), this->credentials_.username.c_str()); ESP_LOGCONFIG(TAG, " Client ID: " LOG_SECRET("'%s'"), this->credentials_.client_id.c_str()); + ESP_LOGCONFIG(TAG, " Clean Session: %s", YESNO(this->credentials_.clean_session)); + if (this->is_discovery_ip_enabled()) { + ESP_LOGCONFIG(TAG, " Discovery IP enabled"); + } if (!this->discovery_info_.prefix.empty()) { ESP_LOGCONFIG(TAG, " Discovery prefix: '%s'", this->discovery_info_.prefix.c_str()); ESP_LOGCONFIG(TAG, " Discovery retain: %s", YESNO(this->discovery_info_.retain)); @@ -141,7 +166,9 @@ void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str()); } } -bool MQTTClientComponent::can_proceed() { return this->is_connected(); } +bool MQTTClientComponent::can_proceed() { + return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected(); +} void MQTTClientComponent::start_dnslookup_() { for (auto &subscription : this->subscriptions_) { @@ -153,28 +180,18 @@ void MQTTClientComponent::start_dnslookup_() { this->dns_resolve_error_ = false; this->dns_resolved_ = false; ip_addr_t addr; -#ifdef USE_ESP32 +#if USE_NETWORK_IPV6 + err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, + MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4); +#else err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4); -#endif -#ifdef USE_ESP8266 - err_t err = dns_gethostbyname(this->credentials_.address.c_str(), &addr, - esphome::mqtt::MQTTClientComponent::dns_found_callback, this); -#endif +#endif /* USE_NETWORK_IPV6 */ switch (err) { case ERR_OK: { // Got IP immediately this->dns_resolved_ = true; -#ifdef USE_ESP32 -#if LWIP_IPV6 - this->ip_ = addr.u_addr.ip4.addr; -#else - this->ip_ = addr.addr; -#endif -#endif -#ifdef USE_ESP8266 - this->ip_ = addr.addr; -#endif + this->ip_ = network::IPAddress(&addr); this->start_connect_(); return; } @@ -186,11 +203,7 @@ void MQTTClientComponent::start_dnslookup_() { default: case ERR_ARG: { // error -#if defined(USE_ESP8266) - ESP_LOGW(TAG, "Error resolving MQTT broker IP address: %ld", err); -#else ESP_LOGW(TAG, "Error resolving MQTT broker IP address: %d", err); -#endif break; } } @@ -225,16 +238,7 @@ void MQTTClientComponent::dns_found_callback(const char *name, const ip_addr_t * if (ipaddr == nullptr) { a_this->dns_resolve_error_ = true; } else { -#ifdef USE_ESP32 -#if LWIP_IPV6 - a_this->ip_ = ipaddr->u_addr.ip4.addr; -#else - a_this->ip_ = ipaddr->addr; -#endif -#endif // USE_ESP32 -#ifdef USE_ESP8266 - a_this->ip_ = ipaddr->addr; -#endif + a_this->ip_ = network::IPAddress(ipaddr); a_this->dns_resolved_ = true; } } @@ -248,6 +252,7 @@ void MQTTClientComponent::start_connect_() { this->mqtt_backend_.disconnect(); this->mqtt_backend_.set_client_id(this->credentials_.client_id.c_str()); + this->mqtt_backend_.set_clean_session(this->credentials_.clean_session); const char *username = nullptr; if (!this->credentials_.username.empty()) username = this->credentials_.username.c_str(); @@ -339,6 +344,8 @@ void MQTTClientComponent::loop() { const uint32_t now = millis(); switch (this->state_) { + case MQTT_CLIENT_DISABLED: + return; // Return to avoid a reboot when disabled case MQTT_CLIENT_DISCONNECTED: if (now - this->connect_begin_ > 5000) { this->start_dnslookup_(); @@ -422,7 +429,10 @@ void MQTTClientComponent::subscribe(const std::string &topic, mqtt_callback_t ca void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) { auto f = [callback](const std::string &topic, const std::string &payload) { - json::parse_json(payload, [topic, callback](JsonObject root) { callback(topic, root); }); + json::parse_json(payload, [topic, callback](JsonObject root) -> bool { + callback(topic, root); + return true; + }); }; MQTTSubscription subscription{ .topic = topic, @@ -482,8 +492,8 @@ bool MQTTClientComponent::publish(const MQTTMessage &message) { if (!logging_topic) { if (ret) { - ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", message.topic.c_str(), message.payload.c_str(), - message.retain); + ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d qos=%d)", message.topic.c_str(), message.payload.c_str(), + message.retain, message.qos); } else { ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", message.topic.c_str(), message.payload.length()); @@ -498,6 +508,23 @@ bool MQTTClientComponent::publish_json(const std::string &topic, const json::jso return this->publish(topic, message, qos, retain); } +void MQTTClientComponent::enable() { + if (this->state_ != MQTT_CLIENT_DISABLED) + return; + ESP_LOGD(TAG, "Enabling MQTT..."); + this->state_ = MQTT_CLIENT_DISCONNECTED; + this->last_connected_ = millis(); + this->start_dnslookup_(); +} + +void MQTTClientComponent::disable() { + if (this->state_ == MQTT_CLIENT_DISABLED) + return; + ESP_LOGD(TAG, "Disabling MQTT..."); + this->state_ = MQTT_CLIENT_DISABLED; + this->on_shutdown(); +} + /** Check if the message topic matches the given subscription topic * * INFO: MQTT spec mandates that topics must not be empty and must be valid NULL-terminated UTF-8 strings. @@ -556,8 +583,8 @@ static bool topic_match(const char *message, const char *subscription) { } void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { -#ifdef USE_ARDUINO - // on Arduino, this is called in lwIP/AsyncTCP task; some components do not like running +#ifdef USE_ESP8266 + // on ESP8266, this is called in lwIP/AsyncTCP task; some components do not like running // from a different task. this->defer([this, topic, payload]() { #endif @@ -565,7 +592,7 @@ void MQTTClientComponent::on_message(const std::string &topic, const std::string if (topic_match(topic.c_str(), subscription.topic.c_str())) subscription.callback(topic, payload); } -#ifdef USE_ARDUINO +#ifdef USE_ESP8266 }); #endif } @@ -590,6 +617,7 @@ void MQTTClientComponent::disable_shutdown_message() { this->recalculate_availability_(); } bool MQTTClientComponent::is_discovery_enabled() const { return !this->discovery_info_.prefix.empty(); } +bool MQTTClientComponent::is_discovery_ip_enabled() const { return this->discovery_info_.discover_ip; } const Availability &MQTTClientComponent::get_availability() { return this->availability_; } void MQTTClientComponent::recalculate_availability_() { if (this->birth_message_.topic.empty() || this->birth_message_.topic != this->last_will_.topic) { @@ -615,8 +643,9 @@ void MQTTClientComponent::set_shutdown_message(MQTTMessage &&message) { this->sh void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, - bool clean) { + bool discover_ip, bool clean) { this->discovery_info_.prefix = std::move(prefix); + this->discovery_info_.discover_ip = discover_ip; this->discovery_info_.unique_id_generator = unique_id_generator; this->discovery_info_.object_id_generator = object_id_generator; this->discovery_info_.retain = retain; @@ -629,6 +658,7 @@ void MQTTClientComponent::disable_discovery() { this->discovery_info_ = MQTTDiscoveryInfo{ .prefix = "", .retain = false, + .discover_ip = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 83ed3cc645..7ae3a6c5e8 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -9,10 +9,12 @@ #include "esphome/core/log.h" #include "esphome/components/json/json_util.h" #include "esphome/components/network/ip_address.h" -#if defined(USE_ESP_IDF) -#include "mqtt_backend_idf.h" -#elif defined(USE_ARDUINO) -#include "mqtt_backend_arduino.h" +#if defined(USE_ESP32) +#include "mqtt_backend_esp32.h" +#elif defined(USE_ESP8266) +#include "mqtt_backend_esp8266.h" +#elif defined(USE_LIBRETINY) +#include "mqtt_backend_libretiny.h" #endif #include "lwip/ip_addr.h" @@ -49,6 +51,7 @@ struct MQTTCredentials { std::string username; std::string password; std::string client_id; ///< The client ID. Will automatically be truncated to 23 characters. + bool clean_session; ///< Whether the session will be cleaned or remembered between connects. }; /// Simple data struct for Home Assistant component availability. @@ -77,13 +80,15 @@ enum MQTTDiscoveryObjectIdGenerator { struct MQTTDiscoveryInfo { std::string prefix; ///< The Home Assistant discovery prefix. Empty means disabled. bool retain; ///< Whether to retain discovery messages. + bool discover_ip; ///< Enable the Home Assistant device discovery. bool clean; MQTTDiscoveryUniqueIdGenerator unique_id_generator; MQTTDiscoveryObjectIdGenerator object_id_generator; }; enum MQTTClientState { - MQTT_CLIENT_DISCONNECTED = 0, + MQTT_CLIENT_DISABLED = 0, + MQTT_CLIENT_DISCONNECTED, MQTT_CLIENT_RESOLVING_ADDRESS, MQTT_CLIENT_CONNECTING, MQTT_CLIENT_CONNECTED, @@ -120,12 +125,14 @@ class MQTTClientComponent : public Component { * @param retain Whether to retain discovery messages. */ void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, - MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool clean = false); + MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool discover_ip, + bool clean = false); /// Get Home Assistant discovery info. const MQTTDiscoveryInfo &get_discovery_info() const; /// Globally disable Home Assistant discovery. void disable_discovery(); bool is_discovery_enabled() const; + bool is_discovery_ip_enabled() const; #if ASYNC_TCP_SSL_ENABLED /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. @@ -142,8 +149,10 @@ class MQTTClientComponent : public Component { */ void add_ssl_fingerprint(const std::array &fingerprint); #endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } + void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } + void set_cl_key(const char *key) { this->mqtt_backend_.set_cl_key(key); } void set_skip_cert_cn_check(bool skip_check) { this->mqtt_backend_.set_skip_cert_cn_check(skip_check); } #endif const Availability &get_availability(); @@ -239,6 +248,9 @@ class MQTTClientComponent : public Component { void register_mqtt_component(MQTTComponent *component); bool is_connected(); + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + void enable(); + void disable(); void on_shutdown() override; @@ -247,6 +259,7 @@ class MQTTClientComponent : public Component { void set_username(const std::string &username) { this->credentials_.username = username; } void set_password(const std::string &password) { this->credentials_.password = password; } void set_client_id(const std::string &client_id) { this->credentials_.client_id = client_id; } + void set_clean_session(const bool &clean_session) { this->credentials_.clean_session = clean_session; } void set_on_connect(mqtt_on_connect_callback_t &&callback); void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback); @@ -286,6 +299,7 @@ class MQTTClientComponent : public Component { MQTTDiscoveryInfo discovery_info_{ .prefix = "homeassistant", .retain = true, + .discover_ip = true, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, @@ -296,16 +310,19 @@ class MQTTClientComponent : public Component { int log_level_{ESPHOME_LOG_LEVEL}; std::vector subscriptions_; -#if defined(USE_ESP_IDF) - MQTTBackendIDF mqtt_backend_; -#elif defined(USE_ARDUINO) - MQTTBackendArduino mqtt_backend_; +#if defined(USE_ESP32) + MQTTBackendESP32 mqtt_backend_; +#elif defined(USE_ESP8266) + MQTTBackendESP8266 mqtt_backend_; +#elif defined(USE_LIBRETINY) + MQTTBackendLibreTiny mqtt_backend_; #endif - MQTTClientState state_{MQTT_CLIENT_DISCONNECTED}; + MQTTClientState state_{MQTT_CLIENT_DISABLED}; network::IPAddress ip_; bool dns_resolved_{false}; bool dns_resolve_error_{false}; + bool enable_on_boot_{true}; std::vector children_; uint32_t reboot_timeout_{300000}; uint32_t connect_begin_; @@ -402,6 +419,26 @@ template class MQTTConnectedCondition : public Condition MQTTClientComponent *parent_; }; +template class MQTTEnableAction : public Action { + public: + MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->enable(); } + + protected: + MQTTClientComponent *parent_; +}; + +template class MQTTDisableAction : public Action { + public: + MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->disable(); } + + protected: + MQTTClientComponent *parent_; +}; + } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index d63885fa04..773d863835 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -17,9 +17,12 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { - // current_temperature_topic root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic(); } + // current_humidity_topic + if (traits.get_supports_current_humidity()) { + root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic(); + } // mode_command_topic root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic(); // mode_state_topic @@ -57,15 +60,29 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic(); } + if (traits.get_supports_target_humidity()) { + // target_humidity_command_topic + root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic(); + // target_humidity_state_topic + root[MQTT_TARGET_HUMIDITY_STATE_TOPIC] = this->get_target_humidity_state_topic(); + } + // min_temp root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature(); // max_temp root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature(); - // temp_step - root["temp_step"] = traits.get_visual_target_temperature_step(); + // target_temp_step + root[MQTT_TARGET_TEMPERATURE_STEP] = traits.get_visual_target_temperature_step(); + // current_temp_step + root[MQTT_CURRENT_TEMPERATURE_STEP] = traits.get_visual_current_temperature_step(); // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; + // min_humidity + root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity(); + // max_humidity + root[MQTT_MAX_HUMIDITY] = traits.get_visual_max_humidity(); + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { // preset_mode_command_topic root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic(); @@ -192,6 +209,20 @@ void MQTTClimateComponent::setup() { }); } + if (traits.get_supports_target_humidity()) { + this->subscribe(this->get_target_humidity_command_topic(), + [this](const std::string &topic, const std::string &payload) { + auto val = parse_number(payload); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); + return; + } + auto call = this->device_->make_call(); + call.set_target_humidity(*val); + call.perform(); + }); + } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) { auto call = this->device_->make_call(); @@ -216,7 +247,7 @@ void MQTTClimateComponent::setup() { }); } - this->device_->add_on_state_callback([this]() { this->publish_state_(); }); + this->device_->add_on_state_callback([this](Climate & /*unused*/) { this->publish_state_(); }); } MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {} bool MQTTClimateComponent::send_initial_state() { return this->publish_state_(); } @@ -273,6 +304,17 @@ bool MQTTClimateComponent::publish_state_() { success = false; } + if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) { + std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0); + if (!this->publish(this->get_current_humidity_state_topic(), payload)) + success = false; + } + if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) { + std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0); + if (!this->publish(this->get_target_humidity_state_topic(), payload)) + success = false; + } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { std::string payload; if (this->device_->preset.has_value()) { diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index a93070fe66..4e54230e68 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -20,6 +20,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { void setup() override; MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state) + MQTT_COMPONENT_CUSTOM_TOPIC(current_humidity, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature, state) @@ -28,6 +29,8 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_low, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, state) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command) + MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, state) + MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, command) MQTT_COMPONENT_CUSTOM_TOPIC(away, state) MQTT_COMPONENT_CUSTOM_TOPIC(away, command) MQTT_COMPONENT_CUSTOM_TOPIC(action, state) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index cf228efd1b..3b9d367a7b 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -2,9 +2,9 @@ #ifdef USE_MQTT -#include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/version.h" #include "mqtt_const.h" @@ -14,6 +14,10 @@ namespace mqtt { static const char *const TAG = "mqtt.component"; +void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } + +void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; } + void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { @@ -23,32 +27,37 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove } std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const { - return global_mqtt_client->get_topic_prefix() + "/" + this->component_type() + "/" + this->get_default_object_id_() + - "/" + suffix; + const std::string &topic_prefix = global_mqtt_client->get_topic_prefix(); + if (topic_prefix.empty()) { + // If the topic_prefix is null, the default topic should be null + return ""; + } + + return topic_prefix + "/" + this->component_type() + "/" + this->get_default_object_id_() + "/" + suffix; } std::string MQTTComponent::get_state_topic_() const { - if (this->custom_state_topic_.empty()) - return this->get_default_topic_for_("state"); - return this->custom_state_topic_; + if (this->has_custom_state_topic_) + return this->custom_state_topic_.str(); + return this->get_default_topic_for_("state"); } std::string MQTTComponent::get_command_topic_() const { - if (this->custom_command_topic_.empty()) - return this->get_default_topic_for_("command"); - return this->custom_command_topic_; + if (this->has_custom_command_topic_) + return this->custom_command_topic_.str(); + return this->get_default_topic_for_("command"); } bool MQTTComponent::publish(const std::string &topic, const std::string &payload) { if (topic.empty()) return false; - return global_mqtt_client->publish(topic, payload, 0, this->retain_); + return global_mqtt_client->publish(topic, payload, this->qos_, this->retain_); } bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) { if (topic.empty()) return false; - return global_mqtt_client->publish_json(topic, f, 0, this->retain_); + return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_); } bool MQTTComponent::send_discovery_() { @@ -56,7 +65,7 @@ bool MQTTComponent::send_discovery_() { if (discovery_info.clean) { ESP_LOGV(TAG, "'%s': Cleaning discovery...", this->friendly_name().c_str()); - return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, 0, true); + return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); } ESP_LOGV(TAG, "'%s': Sending discovery...", this->friendly_name().c_str()); @@ -69,9 +78,17 @@ bool MQTTComponent::send_discovery_() { config.command_topic = true; this->send_discovery(root, config); + // Set subscription QoS (default is 0) + if (this->subscribe_qos_ != 0) { + root[MQTT_QOS] = this->subscribe_qos_; + } // Fields from EntityBase - root[MQTT_NAME] = this->friendly_name(); + if (this->get_entity()->has_own_name()) { + root[MQTT_NAME] = this->friendly_name(); + } else { + root[MQTT_NAME] = ""; + } if (this->is_disabled_by_default()) root[MQTT_ENABLED_BY_DEFAULT] = false; if (!this->get_icon().empty()) @@ -118,7 +135,7 @@ bool MQTTComponent::send_discovery_() { } else { if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; - sprintf(friendly_name_hash, "%08x", fnv1_hash(this->friendly_name())); + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); friendly_name_hash[8] = 0; // ensure the hash-string ends with null root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { @@ -132,16 +149,53 @@ bool MQTTComponent::send_discovery_() { if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_(); + std::string node_friendly_name = App.get_friendly_name(); + if (node_friendly_name.empty()) { + node_friendly_name = node_name; + } + const std::string &node_area = App.get_area(); + JsonObject device_info = root.createNestedObject(MQTT_DEVICE); - device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address(); - device_info[MQTT_DEVICE_NAME] = node_name; - device_info[MQTT_DEVICE_SW_VERSION] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); + const auto mac = get_mac_address(); + device_info[MQTT_DEVICE_IDENTIFIERS] = mac; + device_info[MQTT_DEVICE_NAME] = node_friendly_name; +#ifdef ESPHOME_PROJECT_NAME + device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")"; + const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.'); + if (model == nullptr) { // must never happen but check anyway + device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; + device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME; + } else { + device_info[MQTT_DEVICE_MODEL] = model + 1; + device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); + } +#else + device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")"; device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; - device_info[MQTT_DEVICE_MANUFACTURER] = "espressif"; +#if defined(USE_ESP8266) || defined(USE_ESP32) + device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif"; +#elif defined(USE_RP2040) + device_info[MQTT_DEVICE_MANUFACTURER] = "Raspberry Pi"; +#elif defined(USE_BK72XX) + device_info[MQTT_DEVICE_MANUFACTURER] = "Beken"; +#elif defined(USE_RTL87XX) + device_info[MQTT_DEVICE_MANUFACTURER] = "Realtek"; +#elif defined(USE_HOST) + device_info[MQTT_DEVICE_MANUFACTURER] = "Host"; +#endif +#endif + if (!node_area.empty()) { + device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area; + } + + device_info[MQTT_DEVICE_CONNECTIONS][0][0] = "mac"; + device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; }, - 0, discovery_info.retain); + this->qos_, discovery_info.retain); } +uint8_t MQTTComponent::get_qos() const { return this->qos_; } + bool MQTTComponent::get_retain() const { return this->retain_; } bool MQTTComponent::is_discovery_enabled() const { @@ -164,11 +218,13 @@ MQTTComponent::MQTTComponent() = default; float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; } -void MQTTComponent::set_custom_state_topic(const std::string &custom_state_topic) { - this->custom_state_topic_ = custom_state_topic; +void MQTTComponent::set_custom_state_topic(const char *custom_state_topic) { + this->custom_state_topic_ = StringRef(custom_state_topic); + this->has_custom_state_topic_ = true; } -void MQTTComponent::set_custom_command_topic(const std::string &custom_command_topic) { - this->custom_command_topic_ = custom_command_topic; +void MQTTComponent::set_custom_command_topic(const char *custom_command_topic) { + this->custom_command_topic_ = StringRef(custom_command_topic); + this->has_custom_command_topic_ = true; } void MQTTComponent::set_command_retain(bool command_retain) { this->command_retain_ = command_retain; } @@ -235,7 +291,28 @@ bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connec std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); } std::string MQTTComponent::get_icon() const { return this->get_entity()->get_icon(); } bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); } -bool MQTTComponent::is_internal() { return this->get_entity()->is_internal(); } +bool MQTTComponent::is_internal() { + if (this->has_custom_state_topic_) { + // If the custom state_topic is null, return true as it is internal and should not publish + // else, return false, as it is explicitly set to a topic, so it is not internal and should publish + return this->get_state_topic_().empty(); + } + + if (this->has_custom_command_topic_) { + // If the custom command_topic is null, return true as it is internal and should not publish + // else, return false, as it is explicitly set to a topic, so it is not internal and should publish + return this->get_command_topic_().empty(); + } + + // No custom topics have been set + if (this->get_default_topic_for_("").empty()) { + // If the default topic prefix is null, then the component, by default, is internal and should not publish + return true; + } + + // Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic + return this->get_entity()->is_internal(); +} } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 16a00cfdde..01ba98ad40 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -8,6 +8,7 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/string_ref.h" #include "mqtt_client.h" namespace esphome { @@ -76,6 +77,10 @@ class MQTTComponent : public Component { virtual bool is_internal(); + /// Set QOS for state messages. + void set_qos(uint8_t qos); + uint8_t get_qos() const; + /// Set whether state message should be retained. void set_retain(bool retain); bool get_retain() const; @@ -84,13 +89,16 @@ class MQTTComponent : public Component { void disable_discovery(); bool is_discovery_enabled() const; + /// Set the QOS for subscribe messages (used in discovery). + void set_subscribe_qos(uint8_t qos); + /// Override this method to return the component type (e.g. "light", "sensor", ...) virtual std::string component_type() const = 0; /// Set a custom state topic. Set to "" for default behavior. - void set_custom_state_topic(const std::string &custom_state_topic); + void set_custom_state_topic(const char *custom_state_topic); /// Set a custom command topic. Set to "" for default behavior. - void set_custom_command_topic(const std::string &custom_command_topic); + void set_custom_command_topic(const char *custom_command_topic); /// Set whether command message should be retained. void set_command_retain(bool command_retain); @@ -188,12 +196,19 @@ class MQTTComponent : public Component { /// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name. std::string get_default_object_id_() const; - std::string custom_state_topic_{}; - std::string custom_command_topic_{}; + StringRef custom_state_topic_{}; + StringRef custom_command_topic_{}; + + std::unique_ptr availability_; + + bool has_custom_state_topic_{false}; + bool has_custom_command_topic_{false}; + bool command_retain_{false}; bool retain_{true}; + uint8_t qos_{0}; + uint8_t subscribe_qos_{0}; bool discovery_enabled_{true}; - std::unique_ptr availability_; bool resend_state_{false}; }; diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 7f74197ab4..445457a27f 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -9,8 +9,8 @@ namespace mqtt { #ifdef USE_MQTT_ABBREVIATIONS -constexpr const char *const MQTT_ACTION_TOPIC = "act_t"; 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"; @@ -21,58 +21,72 @@ 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_COLOR_TEMP_COMMAND_TEMPLATE = "clr_temp_cmd_tpl"; -constexpr const char *const MQTT_BATTERY_LEVEL_TOPIC = "bat_lev_t"; -constexpr const char *const MQTT_BATTERY_LEVEL_TEMPLATE = "bat_lev_tpl"; -constexpr const char *const MQTT_CONFIGURATION_URL = "cu"; -constexpr const char *const MQTT_CHARGING_TOPIC = "chrg_t"; 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_CLEANING_TOPIC = "cln_t"; -constexpr const char *const MQTT_CLEANING_TEMPLATE = "cln_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_TOPIC = "cmd_t"; constexpr const char *const MQTT_COMMAND_RETAIN = "ret"; constexpr const char *const MQTT_COMMAND_TEMPLATE = "cmd_tpl"; -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_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; +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_DOCKED_TOPIC = "dock_t"; +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_DOCKED_TEMPLATE = "dock_tpl"; -constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "en"; -constexpr const char *const MQTT_ERROR_TOPIC = "err_t"; -constexpr const char *const MQTT_ERROR_TEMPLATE = "err_tpl"; -constexpr const char *const MQTT_FAN_SPEED_TOPIC = "fanspd_t"; -constexpr const char *const MQTT_FAN_SPEED_TEMPLATE = "fanspd_tpl"; -constexpr const char *const MQTT_FAN_SPEED_LIST = "fanspd_lst"; -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_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"; @@ -84,56 +98,50 @@ 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_TARGET_HUMIDITY_COMMAND_TOPIC = "hum_cmd_t"; -constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "hum_cmd_tpl"; -constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "hum_stat_t"; -constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "hum_state_tpl"; constexpr const char *const MQTT_JSON_ATTRIBUTES = "json_attr"; -constexpr const char *const MQTT_JSON_ATTRIBUTES_TOPIC = "json_attr_t"; 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_MIN = "min"; constexpr const char *const MQTT_MAX_HUMIDITY = "max_hum"; -constexpr const char *const MQTT_MIN_HUMIDITY = "min_hum"; constexpr const char *const MQTT_MAX_MIREDS = "max_mirs"; -constexpr const char *const MQTT_MIN_MIREDS = "min_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_TOPIC = "mode_stat_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_OPTIONS = "ops"; constexpr const char *const MQTT_OPTIMISTIC = "opt"; -constexpr const char *const MQTT_OSCILLATION_COMMAND_TOPIC = "osc_cmd_t"; +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_PERCENTAGE_COMMAND_TOPIC = "pct_cmd_t"; -constexpr const char *const MQTT_PERCENTAGE_COMMAND_TEMPLATE = "pct_cmd_tpl"; -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_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_ARM_CUSTOM_BYPASS = "pl_arm_custom_b"; 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_LOCK = "pl_lock"; +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"; @@ -150,23 +158,30 @@ 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_STOP = "pl_stop"; +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_RETURN_TO_BASE = "pl_ret"; +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_TOPIC = "pow_stat_t"; constexpr const char *const MQTT_POWER_STATE_TEMPLATE = "pow_stat_tpl"; -constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "pr_mode_cmd_t"; +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"; @@ -186,36 +201,39 @@ 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_POSITION_TOPIC = "pos_t"; -constexpr const char *const MQTT_POSITION_TEMPLATE = "pos_tpl"; +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_STATE_TOPIC = "spd_stat_t"; -constexpr const char *const MQTT_SPEED_RANGE_MIN = "spd_rng_min"; 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_SOURCE_TYPE = "src_type"; 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_LOCKED = "stat_locked"; -constexpr const char *const MQTT_STATE_UNLOCKED = "stat_unlocked"; -constexpr const char *const MQTT_STATE_TOPIC = "stat_t"; 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_FEATURES = "sup_feat"; 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"; @@ -230,15 +248,15 @@ 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_TOPIC = "tilt_cmd_t"; 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_TOPIC = "tilt_status_t"; 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"; @@ -253,18 +271,10 @@ 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"; -constexpr const char *const MQTT_DEVICE_CONNECTIONS = "cns"; -constexpr const char *const MQTT_DEVICE_IDENTIFIERS = "ids"; -constexpr const char *const MQTT_DEVICE_NAME = "name"; -constexpr const char *const MQTT_DEVICE_MANUFACTURER = "mf"; -constexpr const char *const MQTT_DEVICE_MODEL = "mdl"; -constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw"; -constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "sa"; - #else -constexpr const char *const MQTT_ACTION_TOPIC = "action_topic"; 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"; @@ -275,58 +285,72 @@ 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_COLOR_TEMP_COMMAND_TEMPLATE = "color_temp_command_template"; -constexpr const char *const MQTT_BATTERY_LEVEL_TOPIC = "battery_level_topic"; -constexpr const char *const MQTT_BATTERY_LEVEL_TEMPLATE = "battery_level_template"; -constexpr const char *const MQTT_CONFIGURATION_URL = "configuration_url"; -constexpr const char *const MQTT_CHARGING_TOPIC = "charging_topic"; 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_CLEANING_TOPIC = "cleaning_topic"; -constexpr const char *const MQTT_CLEANING_TEMPLATE = "cleaning_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_TOPIC = "command_topic"; constexpr const char *const MQTT_COMMAND_RETAIN = "retain"; constexpr const char *const MQTT_COMMAND_TEMPLATE = "command_template"; -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_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; +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_DOCKED_TOPIC = "docked_topic"; +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_DOCKED_TEMPLATE = "docked_template"; -constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "enabled_by_default"; -constexpr const char *const MQTT_ERROR_TOPIC = "error_topic"; -constexpr const char *const MQTT_ERROR_TEMPLATE = "error_template"; -constexpr const char *const MQTT_FAN_SPEED_TOPIC = "fan_speed_topic"; -constexpr const char *const MQTT_FAN_SPEED_TEMPLATE = "fan_speed_template"; -constexpr const char *const MQTT_FAN_SPEED_LIST = "fan_speed_list"; -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_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"; @@ -338,56 +362,50 @@ 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_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic"; -constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template"; -constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"; -constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template"; constexpr const char *const MQTT_JSON_ATTRIBUTES = "json_attributes"; -constexpr const char *const MQTT_JSON_ATTRIBUTES_TOPIC = "json_attributes_topic"; 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_MIN = "min"; constexpr const char *const MQTT_MAX_HUMIDITY = "max_humidity"; -constexpr const char *const MQTT_MIN_HUMIDITY = "min_humidity"; constexpr const char *const MQTT_MAX_MIREDS = "max_mireds"; -constexpr const char *const MQTT_MIN_MIREDS = "min_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_TOPIC = "mode_state_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_OPTIONS = "options"; constexpr const char *const MQTT_OPTIMISTIC = "optimistic"; -constexpr const char *const MQTT_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"; +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_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"; -constexpr const char *const MQTT_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"; -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_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_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass"; 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_LOCK = "payload_lock"; +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"; @@ -404,23 +422,30 @@ constexpr const char *const MQTT_PAYLOAD_RESET_HUMIDITY = "payload_reset_humidit 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_STOP = "payload_stop"; +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_RETURN_TO_BASE = "payload_return_to_base"; +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_TOPIC = "power_state_topic"; constexpr const char *const MQTT_POWER_STATE_TEMPLATE = "power_state_template"; -constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"; +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"; @@ -440,36 +465,39 @@ 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_POSITION_TOPIC = "position_topic"; -constexpr const char *const MQTT_POSITION_TEMPLATE = "position_template"; +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_STATE_TOPIC = "speed_state_topic"; -constexpr const char *const MQTT_SPEED_RANGE_MIN = "speed_range_min"; 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_SOURCE_TYPE = "source_type"; 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_LOCKED = "state_locked"; -constexpr const char *const MQTT_STATE_UNLOCKED = "state_unlocked"; -constexpr const char *const MQTT_STATE_TOPIC = "state_topic"; 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_FEATURES = "supported_features"; 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"; @@ -484,15 +512,15 @@ constexpr const char *const MQTT_TEMPERATURE_STATE_TEMPLATE = "temperature_state 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_TOPIC = "tilt_command_topic"; 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_TOPIC = "tilt_status_topic"; 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"; @@ -507,19 +535,8 @@ 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"; -constexpr const char *const MQTT_DEVICE_CONNECTIONS = "connections"; -constexpr const char *const MQTT_DEVICE_IDENTIFIERS = "identifiers"; -constexpr const char *const MQTT_DEVICE_NAME = "name"; -constexpr const char *const MQTT_DEVICE_MANUFACTURER = "manufacturer"; -constexpr const char *const MQTT_DEVICE_MODEL = "model"; -constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw_version"; -constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "suggested_area"; #endif -// Additional MQTT fields where no abbreviation is defined in HA source -constexpr const char *const MQTT_ENTITY_CATEGORY = "entity_category"; -constexpr const char *const MQTT_MODE = "mode"; - } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp new file mode 100644 index 0000000000..088a4788ed --- /dev/null +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -0,0 +1,68 @@ +#include "mqtt_date.h" + +#include +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_DATE + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.datetime"; + +using namespace esphome::datetime; + +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.containsKey("year")) { + call.set_year(root["year"]); + } + if (root.containsKey("month")) { + call.set_month(root["month"]); + } + if (root.containsKey("day")) { + call.set_day(root["day"]); + } + call.perform(); + }); + this->date_->add_on_state_callback( + [this]() { this->publish_state(this->date_->year, this->date_->month, this->date_->day); }); +} + +void MQTTDateComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) +} + +std::string MQTTDateComponent::component_type() const { return "date"; } +const EntityBase *MQTTDateComponent::get_entity() const { return this->date_; } + +void MQTTDateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // Nothing extra to add here +} +bool MQTTDateComponent::send_initial_state() { + if (this->date_->has_state()) { + return this->publish_state(this->date_->year, this->date_->month, this->date_->day); + } else { + return true; + } +} +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) { + root["year"] = year; + root["month"] = month; + root["day"] = day; + }); +} + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_DATE +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_date.h b/esphome/components/mqtt/mqtt_date.h new file mode 100644 index 0000000000..5147afe7e7 --- /dev/null +++ b/esphome/components/mqtt/mqtt_date.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_DATE + +#include "esphome/components/datetime/date_entity.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTDateComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTDateComponent instance with the provided friendly_name and date + * + * @param date The date component. + */ + explicit MQTTDateComponent(datetime::DateEntity *date); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(uint16_t year, uint8_t month, uint8_t day); + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + datetime::DateEntity *date_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_DATE +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp new file mode 100644 index 0000000000..4ae6d0d416 --- /dev/null +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -0,0 +1,84 @@ +#include "mqtt_datetime.h" + +#include +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_DATETIME + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.datetime.datetime"; + +using namespace esphome::datetime; + +MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetime_(datetime) {} + +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.containsKey("year")) { + call.set_year(root["year"]); + } + if (root.containsKey("month")) { + call.set_month(root["month"]); + } + if (root.containsKey("day")) { + call.set_day(root["day"]); + } + if (root.containsKey("hour")) { + call.set_hour(root["hour"]); + } + if (root.containsKey("minute")) { + call.set_minute(root["minute"]); + } + if (root.containsKey("second")) { + call.set_second(root["second"]); + } + call.perform(); + }); + this->datetime_->add_on_state_callback([this]() { + this->publish_state(this->datetime_->year, this->datetime_->month, this->datetime_->day, this->datetime_->hour, + this->datetime_->minute, this->datetime_->second); + }); +} + +void MQTTDateTimeComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) +} + +std::string MQTTDateTimeComponent::component_type() const { return "datetime"; } +const EntityBase *MQTTDateTimeComponent::get_entity() const { return this->datetime_; } + +void MQTTDateTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // Nothing extra to add here +} +bool MQTTDateTimeComponent::send_initial_state() { + if (this->datetime_->has_state()) { + return this->publish_state(this->datetime_->year, this->datetime_->month, this->datetime_->day, + this->datetime_->hour, this->datetime_->minute, this->datetime_->second); + } else { + return true; + } +} +bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, + uint8_t second) { + return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { + root["year"] = year; + root["month"] = month; + root["day"] = day; + root["hour"] = hour; + root["minute"] = minute; + root["second"] = second; + }); +} + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_DATETIME +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_datetime.h b/esphome/components/mqtt/mqtt_datetime.h new file mode 100644 index 0000000000..ba81c06cb3 --- /dev/null +++ b/esphome/components/mqtt/mqtt_datetime.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_DATETIME + +#include "esphome/components/datetime/datetime_entity.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTDateTimeComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTDateTimeComponent instance with the provided friendly_name and time + * + * @param time The time entity. + */ + explicit MQTTDateTimeComponent(datetime::DateTimeEntity *datetime); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second); + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + datetime::DateTimeEntity *datetime_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_DATETIME +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp new file mode 100644 index 0000000000..cf0b90e3d6 --- /dev/null +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -0,0 +1,54 @@ +#include "mqtt_event.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_EVENT + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.event"; + +using namespace esphome::event; + +MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} + +void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES); + for (const auto &event_type : this->event_->get_event_types()) + event_types.add(event_type); + + if (!this->event_->get_device_class().empty()) + root[MQTT_DEVICE_CLASS] = this->event_->get_device_class(); + + config.command_topic = false; +} + +void MQTTEventComponent::setup() { + this->event_->add_on_event_callback([this](const std::string &event_type) { this->publish_event_(event_type); }); +} + +void MQTTEventComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Event '%s': ", this->event_->get_name().c_str()); + ESP_LOGCONFIG(TAG, "Event Types: "); + for (const auto &event_type : this->event_->get_event_types()) { + ESP_LOGCONFIG(TAG, "- %s", event_type.c_str()); + } + LOG_MQTT_COMPONENT(true, true); +} + +bool MQTTEventComponent::publish_event_(const std::string &event_type) { + return this->publish_json(this->get_state_topic_(), + [event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); +} + +std::string MQTTEventComponent::component_type() const { return "event"; } +const EntityBase *MQTTEventComponent::get_entity() const { return this->event_; } + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_event.h b/esphome/components/mqtt/mqtt_event.h new file mode 100644 index 0000000000..4335820e53 --- /dev/null +++ b/esphome/components/mqtt/mqtt_event.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_EVENT + +#include "esphome/components/event/event.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTEventComponent : public mqtt::MQTTComponent { + public: + explicit MQTTEventComponent(event::Event *event); + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + void setup() override; + + void dump_config() override; + + /// Events do not send a state so just return true. + bool send_initial_state() override { return true; } + + protected: + bool publish_event_(const std::string &event_type); + std::string component_type() const override; + const EntityBase *get_entity() const override; + + event::Event *event_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 197d0c32d4..f4a5126d0c 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -40,6 +40,8 @@ const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->lock_->traits.get_assumed_state()) root[MQTT_OPTIMISTIC] = true; + if (this->lock_->traits.get_supports_open()) + root[MQTT_PAYLOAD_OPEN] = "OPEN"; } bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 4946cfb924..fff75a3c00 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -1,3 +1,4 @@ +#include #include "mqtt_sensor.h" #include "esphome/core/log.h" @@ -26,7 +27,7 @@ void MQTTSensorComponent::setup() { void MQTTSensorComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Sensor '%s':", this->sensor_->get_name().c_str()); if (this->get_expire_after() > 0) { - ESP_LOGCONFIG(TAG, " Expire After: %us", this->get_expire_after() / 1000); + ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000); } LOG_MQTT_COMPONENT(true, false) } diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp new file mode 100644 index 0000000000..cb852b64cd --- /dev/null +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -0,0 +1,63 @@ +#include "mqtt_text.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_TEXT + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.text"; + +using namespace esphome::text; + +MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {} + +void MQTTTextComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { + auto call = this->text_->make_call(); + call.set_value(state); + call.perform(); + }); + + this->text_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); }); +} + +void MQTTTextComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) +} + +std::string MQTTTextComponent::component_type() const { return "text"; } +const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } + +void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + switch (this->text_->traits.get_mode()) { + case TEXT_MODE_TEXT: + root[MQTT_MODE] = "text"; + break; + case TEXT_MODE_PASSWORD: + root[MQTT_MODE] = "password"; + break; + } + + config.command_topic = true; +} +bool MQTTTextComponent::send_initial_state() { + if (this->text_->has_state()) { + return this->publish_state(this->text_->state); + } else { + return true; + } +} +bool MQTTTextComponent::publish_state(const std::string &value) { + return this->publish(this->get_state_topic_(), value); +} + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_text.h b/esphome/components/mqtt/mqtt_text.h new file mode 100644 index 0000000000..d9486fcbf8 --- /dev/null +++ b/esphome/components/mqtt/mqtt_text.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_TEXT + +#include "esphome/components/text/text.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTTextComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTTextComponent instance with the provided friendly_name and text + * + * @param text The text input. + */ + explicit MQTTTextComponent(text::Text *text); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(const std::string &value); + + protected: + /// Override for MQTTComponent, returns "text". + std::string component_type() const override; + const EntityBase *get_entity() const override; + + text::Text *text_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index d0d3174bfe..b0754bc8b3 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -1,6 +1,8 @@ #include "mqtt_text_sensor.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_TEXT_SENSOR @@ -13,6 +15,8 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + if (!this->sensor_->get_device_class().empty()) + root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_time.cpp b/esphome/components/mqtt/mqtt_time.cpp new file mode 100644 index 0000000000..332ef53cbc --- /dev/null +++ b/esphome/components/mqtt/mqtt_time.cpp @@ -0,0 +1,68 @@ +#include "mqtt_time.h" + +#include +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_TIME + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.datetime.time"; + +using namespace esphome::datetime; + +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.containsKey("hour")) { + call.set_hour(root["hour"]); + } + if (root.containsKey("minute")) { + call.set_minute(root["minute"]); + } + if (root.containsKey("second")) { + call.set_second(root["second"]); + } + call.perform(); + }); + this->time_->add_on_state_callback( + [this]() { this->publish_state(this->time_->hour, this->time_->minute, this->time_->second); }); +} + +void MQTTTimeComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) +} + +std::string MQTTTimeComponent::component_type() const { return "time"; } +const EntityBase *MQTTTimeComponent::get_entity() const { return this->time_; } + +void MQTTTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // Nothing extra to add here +} +bool MQTTTimeComponent::send_initial_state() { + if (this->time_->has_state()) { + return this->publish_state(this->time_->hour, this->time_->minute, this->time_->second); + } else { + return true; + } +} +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) { + root["hour"] = hour; + root["minute"] = minute; + root["second"] = second; + }); +} + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_TIME +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_time.h b/esphome/components/mqtt/mqtt_time.h new file mode 100644 index 0000000000..b9dd822a73 --- /dev/null +++ b/esphome/components/mqtt/mqtt_time.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_DATETIME_TIME + +#include "esphome/components/datetime/time_entity.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTTimeComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTTimeComponent instance with the provided friendly_name and time + * + * @param time The time entity. + */ + explicit MQTTTimeComponent(datetime::TimeEntity *time); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(uint8_t hour, uint8_t minute, uint8_t second); + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + datetime::TimeEntity *time_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_DATETIME_DATE +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp new file mode 100644 index 0000000000..2ed8faf074 --- /dev/null +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -0,0 +1,62 @@ +#include "mqtt_update.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_UPDATE + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.update"; + +using namespace esphome::update; + +MQTTUpdateComponent::MQTTUpdateComponent(UpdateEntity *update) : update_(update) {} + +void MQTTUpdateComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { + if (payload == "INSTALL") { + this->update_->perform(); + } else { + ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str()); + this->status_momentary_warning("state", 5000); + } + }); + + this->update_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); +} + +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; + if (!this->update_->update_info.summary.empty()) + root["release_summary"] = this->update_->update_info.summary; + if (!this->update_->update_info.release_url.empty()) + root["release_url"] = this->update_->update_info.release_url; + }); +} + +void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + root["schema"] = "json"; + root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; +} + +bool MQTTUpdateComponent::send_initial_state() { return this->publish_state(); } + +void MQTTUpdateComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Update '%s': ", this->update_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true); +} + +std::string MQTTUpdateComponent::component_type() const { return "update"; } +const EntityBase *MQTTUpdateComponent::get_entity() const { return this->update_; } + +} // namespace mqtt +} // namespace esphome + +#endif // USE_UPDATE +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_update.h b/esphome/components/mqtt/mqtt_update.h new file mode 100644 index 0000000000..6fe04c4ea7 --- /dev/null +++ b/esphome/components/mqtt/mqtt_update.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_UPDATE + +#include "esphome/components/update/update_entity.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTUpdateComponent : public mqtt::MQTTComponent { + public: + explicit MQTTUpdateComponent(update::UpdateEntity *update); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(); + + protected: + /// "update" component type. + std::string component_type() const override; + const EntityBase *get_entity() const override; + + update::UpdateEntity *update_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_UPDATE +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp new file mode 100644 index 0000000000..07eeca08d6 --- /dev/null +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -0,0 +1,90 @@ +#include "mqtt_valve.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_VALVE + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.valve"; + +using namespace esphome::valve; + +MQTTValveComponent::MQTTValveComponent(Valve *valve) : valve_(valve) {} +void MQTTValveComponent::setup() { + auto traits = this->valve_->get_traits(); + this->valve_->add_on_state_callback([this]() { this->publish_state(); }); + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { + auto call = this->valve_->make_call(); + call.set_command(payload.c_str()); + call.perform(); + }); + if (traits.get_supports_position()) { + this->subscribe(this->get_position_command_topic(), [this](const std::string &topic, const std::string &payload) { + auto value = parse_number(payload); + if (!value.has_value()) { + ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str()); + return; + } + auto call = this->valve_->make_call(); + call.set_position(*value / 100.0f); + call.perform(); + }); + } +} + +void MQTTValveComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT valve '%s':", this->valve_->get_name().c_str()); + auto traits = this->valve_->get_traits(); + bool has_command_topic = traits.get_supports_position(); + LOG_MQTT_COMPONENT(true, has_command_topic) + if (traits.get_supports_position()) { + ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic().c_str()); + ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic().c_str()); + } +} +void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + if (!this->valve_->get_device_class().empty()) + root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); + + auto traits = this->valve_->get_traits(); + if (traits.get_is_assumed_state()) { + root[MQTT_OPTIMISTIC] = true; + } + if (traits.get_supports_position()) { + root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); + root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); + } +} + +std::string MQTTValveComponent::component_type() const { return "valve"; } +const EntityBase *MQTTValveComponent::get_entity() const { return this->valve_; } + +bool MQTTValveComponent::send_initial_state() { return this->publish_state(); } +bool MQTTValveComponent::publish_state() { + auto traits = this->valve_->get_traits(); + bool success = true; + if (traits.get_supports_position()) { + std::string pos = value_accuracy_to_string(roundf(this->valve_->position * 100), 0); + if (!this->publish(this->get_position_state_topic(), pos)) + success = false; + } + const char *state_s = this->valve_->current_operation == VALVE_OPERATION_OPENING ? "opening" + : this->valve_->current_operation == VALVE_OPERATION_CLOSING ? "closing" + : this->valve_->position == VALVE_CLOSED ? "closed" + : this->valve_->position == VALVE_OPEN ? "open" + : traits.get_supports_position() ? "open" + : "unknown"; + if (!this->publish(this->get_state_topic_(), state_s)) + success = false; + return success; +} + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_valve.h b/esphome/components/mqtt/mqtt_valve.h new file mode 100644 index 0000000000..63a0462193 --- /dev/null +++ b/esphome/components/mqtt/mqtt_valve.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "mqtt_component.h" + +#ifdef USE_MQTT +#ifdef USE_VALVE + +#include "esphome/components/valve/valve.h" + +namespace esphome { +namespace mqtt { + +class MQTTValveComponent : public mqtt::MQTTComponent { + public: + explicit MQTTValveComponent(valve::Valve *valve); + + void setup() override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + MQTT_COMPONENT_CUSTOM_TOPIC(position, command) + MQTT_COMPONENT_CUSTOM_TOPIC(position, state) + + bool send_initial_state() override; + + bool publish_state(); + + void dump_config() override; + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + valve::Valve *valve_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/ms8607/__init__.py b/esphome/components/ms8607/__init__.py new file mode 100644 index 0000000000..e1cd49ec7b --- /dev/null +++ b/esphome/components/ms8607/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@e28eta"] diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp new file mode 100644 index 0000000000..4ad6ac336d --- /dev/null +++ b/esphome/components/ms8607/ms8607.cpp @@ -0,0 +1,444 @@ +#include "ms8607.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ms8607 { + +/// TAG used for logging calls +static const char *const TAG = "ms8607"; + +/// Reset the Pressure/Temperature sensor +static const uint8_t MS8607_PT_CMD_RESET = 0x1E; + +/// Beginning of PROM register addresses. Same for both i2c addresses. Each address has 16 bits of data, and +/// PROM addresses step by two, so the LSB is always 0 +static const uint8_t MS8607_PROM_START = 0xA0; +/// Last PROM register address. +static const uint8_t MS8607_PROM_END = 0xAE; +/// Number of PROM registers. +static const uint8_t MS8607_PROM_COUNT = (MS8607_PROM_END - MS8607_PROM_START) >> 1; + +/// Reset the Humidity sensor +static const uint8_t MS8607_CMD_H_RESET = 0xFE; +/// Read relative humidity, without holding i2c master +static const uint8_t MS8607_CMD_H_MEASURE_NO_HOLD = 0xF5; +/// Temperature correction coefficient for Relative Humidity from datasheet +static const float MS8607_H_TEMP_COEFFICIENT = -0.18; + +/// Read the converted analog value, either D1 (pressure) or D2 (temperature) +static const uint8_t MS8607_CMD_ADC_READ = 0x00; + +// TODO: allow OSR to be turned down for speed and/or lower power consumption via configuration. +// ms8607 supports 6 different settings + +/// Request conversion of analog D1 (pressure) with OSR=8192 (highest oversampling ratio). Takes maximum of 17.2ms +static const uint8_t MS8607_CMD_CONV_D1_OSR_8K = 0x4A; +/// Request conversion of analog D2 (temperature) with OSR=8192 (highest oversampling ratio). Takes maximum of 17.2ms +static const uint8_t MS8607_CMD_CONV_D2_OSR_8K = 0x5A; + +enum class MS8607Component::ErrorCode { + /// Component hasn't failed (yet?) + NONE = 0, + /// Both the Pressure/Temperature address and the Humidity address failed to reset + PTH_RESET_FAILED = 1, + /// Asking the Pressure/Temperature sensor to reset failed + PT_RESET_FAILED = 2, + /// Asking the Humidity sensor to reset failed + H_RESET_FAILED = 3, + /// Reading the PROM calibration values failed + PROM_READ_FAILED = 4, + /// The PROM calibration values failed the CRC check + PROM_CRC_FAILED = 5, +}; + +enum class MS8607Component::SetupStatus { + /// This component has not successfully reset the PT & H devices + NEEDS_RESET, + /// Reset commands succeeded, need to wait >= 15ms to read PROM + NEEDS_PROM_READ, + /// Successfully read PROM and ready to update sensors + SUCCESSFUL, +}; + +static uint8_t crc4(uint16_t *buffer, size_t length); +static uint8_t hsensor_crc_check(uint16_t value); + +void MS8607Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MS8607..."); + this->error_code_ = ErrorCode::NONE; + this->setup_status_ = SetupStatus::NEEDS_RESET; + + // I do not know why the device sometimes NACKs the reset command, but + // try 3 times in case it's a transitory issue on this boot + this->set_retry( + "reset", 5, 3, + [this](const uint8_t remaining_setup_attempts) { + ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, + this->humidity_device_->get_address()); + // I believe sending the reset command to both addresses is preferable to + // skipping humidity if PT fails for some reason. + // However, only consider the reset successful if they both ACK + bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0); + bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0); + + if (!(pt_successful && h_successful)) { + ESP_LOGE(TAG, "Resetting I2C devices failed"); + if (!pt_successful && !h_successful) { + this->error_code_ = ErrorCode::PTH_RESET_FAILED; + } else if (!pt_successful) { + this->error_code_ = ErrorCode::PT_RESET_FAILED; + } else { + this->error_code_ = ErrorCode::H_RESET_FAILED; + } + + if (remaining_setup_attempts > 0) { + this->status_set_error(); + } else { + this->mark_failed(); + } + return RetryResult::RETRY; + } + + this->setup_status_ = SetupStatus::NEEDS_PROM_READ; + this->error_code_ = ErrorCode::NONE; + this->status_clear_error(); + + // 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library + this->set_timeout("prom-read", 15, [this]() { + if (this->read_calibration_values_from_prom_()) { + this->setup_status_ = SetupStatus::SUCCESSFUL; + this->status_clear_error(); + } else { + this->mark_failed(); + return; + } + }); + + return RetryResult::DONE; + }, + 5.0f); // executes at now, +5ms, +25ms +} + +void MS8607Component::update() { + if (this->setup_status_ != SetupStatus::SUCCESSFUL) { + // setup is still occurring, either because reset had to retry or due to the 15ms + // delay needed between reset & reading the PROM values + return; + } + + // Updating happens async and sequentially. + // Temperature, then pressure, then humidity + this->request_read_temperature_(); +} + +void MS8607Component::dump_config() { + ESP_LOGCONFIG(TAG, "MS8607:"); + LOG_I2C_DEVICE(this); + // LOG_I2C_DEVICE doesn't work for humidity, the `address_` is protected. Log using get_address() + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->humidity_device_->get_address()); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MS8607 failed."); + switch (this->error_code_) { + case ErrorCode::PT_RESET_FAILED: + ESP_LOGE(TAG, "Temperature/Pressure RESET failed"); + break; + case ErrorCode::H_RESET_FAILED: + ESP_LOGE(TAG, "Humidity RESET failed"); + break; + case ErrorCode::PTH_RESET_FAILED: + ESP_LOGE(TAG, "Temperature/Pressure && Humidity RESET failed"); + break; + case ErrorCode::PROM_READ_FAILED: + ESP_LOGE(TAG, "Reading PROM failed"); + break; + case ErrorCode::PROM_CRC_FAILED: + ESP_LOGE(TAG, "PROM values failed CRC"); + break; + case ErrorCode::NONE: + default: + ESP_LOGE(TAG, "Error reason unknown %u", static_cast(this->error_code_)); + break; + } + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +bool MS8607Component::read_calibration_values_from_prom_() { + ESP_LOGD(TAG, "Reading calibration values from PROM"); + + uint16_t buffer[MS8607_PROM_COUNT]; + bool successful = true; + + for (uint8_t idx = 0; idx < MS8607_PROM_COUNT; ++idx) { + uint8_t const address_to_read = MS8607_PROM_START + (idx * 2); + successful &= this->read_byte_16(address_to_read, &buffer[idx]); + } + + if (!successful) { + ESP_LOGE(TAG, "Reading calibration values from PROM failed"); + this->error_code_ = ErrorCode::PROM_READ_FAILED; + return false; + } + + ESP_LOGD(TAG, "Checking CRC of calibration values from PROM"); + uint8_t const expected_crc = (buffer[0] & 0xF000) >> 12; // first 4 bits + buffer[0] &= 0x0FFF; // strip CRC from buffer, in order to run CRC + uint8_t const actual_crc = crc4(buffer, MS8607_PROM_COUNT); + + if (expected_crc != actual_crc) { + ESP_LOGE(TAG, "Incorrect CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc, actual_crc); + this->error_code_ = ErrorCode::PROM_CRC_FAILED; + return false; + } + + this->calibration_values_.pressure_sensitivity = buffer[1]; + this->calibration_values_.pressure_offset = buffer[2]; + this->calibration_values_.pressure_sensitivity_temperature_coefficient = buffer[3]; + this->calibration_values_.pressure_offset_temperature_coefficient = buffer[4]; + this->calibration_values_.reference_temperature = buffer[5]; + this->calibration_values_.temperature_coefficient_of_temperature = buffer[6]; + ESP_LOGD(TAG, "Finished reading calibration values"); + + // Skipping reading Humidity PROM, since it doesn't have anything interesting for us + + return true; +} + +/** + CRC-4 algorithm from datasheet. It operates on a buffer of 16-bit values, one byte at a time, using a 16-bit + value to collect the CRC result into. + + The provided/expected CRC value must already be zeroed out from the buffer. + */ +static uint8_t crc4(uint16_t *buffer, size_t length) { + uint16_t crc_remainder = 0; + + // algorithm to add a byte into the crc + auto apply_crc = [&crc_remainder](uint8_t next) { + crc_remainder ^= next; + for (uint8_t bit = 8; bit > 0; --bit) { + if (crc_remainder & 0x8000) { + crc_remainder = (crc_remainder << 1) ^ 0x3000; + } else { + crc_remainder = (crc_remainder << 1); + } + } + }; + + // add all the bytes + for (size_t idx = 0; idx < length; ++idx) { + for (auto byte : decode_value(buffer[idx])) { + apply_crc(byte); + } + } + // For the MS8607 CRC, add a pair of zeros to shift the last byte from `buffer` through + apply_crc(0); + apply_crc(0); + + return (crc_remainder >> 12) & 0xF; // only the most significant 4 bits +} + +/** + * @brief Calculates CRC value for the provided humidity (+ status bits) value + * + * CRC-8 check comes from other MS8607 libraries on github. I did not find it in the datasheet, + * and it differs from the crc8 implementation that's already part of esphome. + * + * @param value two byte humidity sensor value read from i2c + * @return uint8_t computed crc value + */ +static uint8_t hsensor_crc_check(uint16_t value) { + uint32_t polynom = 0x988000; // x^8 + x^5 + x^4 + 1 + uint32_t msb = 0x800000; + uint32_t mask = 0xFF8000; + uint32_t result = (uint32_t) value << 8; // Pad with zeros as specified in spec + + while (msb != 0x80) { + // Check if msb of current value is 1 and apply XOR mask + if (result & msb) { + result = ((result ^ polynom) & mask) | (result & ~mask); + } + + // Shift by one + msb >>= 1; + mask >>= 1; + polynom >>= 1; + } + return result & 0xFF; +} + +void MS8607Component::request_read_temperature_() { + // Tell MS8607 to start ADC conversion of temperature sensor + if (!this->write_bytes(MS8607_CMD_CONV_D2_OSR_8K, nullptr, 0)) { + this->status_set_warning(); + return; + } + + auto f = std::bind(&MS8607Component::read_temperature_, this); + // datasheet says 17.2ms max conversion time at OSR 8192 + this->set_timeout("temperature", 20, f); +} + +void MS8607Component::read_temperature_() { + uint8_t bytes[3]; // 24 bits + if (!this->read_bytes(MS8607_CMD_ADC_READ, bytes, 3)) { + this->status_set_warning(); + return; + } + + const uint32_t d2_raw_temperature = encode_uint32(0, bytes[0], bytes[1], bytes[2]); + this->request_read_pressure_(d2_raw_temperature); +} + +void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) { + if (!this->write_bytes(MS8607_CMD_CONV_D1_OSR_8K, nullptr, 0)) { + this->status_set_warning(); + return; + } + + auto f = std::bind(&MS8607Component::read_pressure_, this, d2_raw_temperature); + // datasheet says 17.2ms max conversion time at OSR 8192 + this->set_timeout("pressure", 20, f); +} + +void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) { + uint8_t bytes[3]; // 24 bits + if (!this->read_bytes(MS8607_CMD_ADC_READ, bytes, 3)) { + this->status_set_warning(); + return; + } + const uint32_t d1_raw_pressure = encode_uint32(0, bytes[0], bytes[1], bytes[2]); + this->calculate_values_(d2_raw_temperature, d1_raw_pressure); +} + +void MS8607Component::request_read_humidity_(float temperature_float) { + if (!this->humidity_device_->write_bytes(MS8607_CMD_H_MEASURE_NO_HOLD, nullptr, 0)) { + ESP_LOGW(TAG, "Request to measure humidity failed"); + this->status_set_warning(); + return; + } + + auto f = std::bind(&MS8607Component::read_humidity_, this, temperature_float); + // datasheet says 15.89ms max conversion time at OSR 8192 + this->set_timeout("humidity", 20, f); +} + +void MS8607Component::read_humidity_(float temperature_float) { + uint8_t bytes[3]; + if (!this->humidity_device_->read_bytes_raw(bytes, 3)) { + ESP_LOGW(TAG, "Failed to read the measured humidity value"); + this->status_set_warning(); + return; + } + + // "the measurement is stored into 14 bits. The two remaining LSBs are used for transmitting status information. + // Bit1 of the two LSBS must be set to '1'. Bit0 is currently not assigned" + uint16_t humidity = encode_uint16(bytes[0], bytes[1]); + uint8_t const expected_crc = bytes[2]; + uint8_t const actual_crc = hsensor_crc_check(humidity); + if (expected_crc != actual_crc) { + ESP_LOGE(TAG, "Incorrect Humidity CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc, + actual_crc); + this->status_set_warning(); + return; + } + if (!(humidity & 0x2)) { + // data sheet says Bit1 should always set, but nothing about what happens if it isn't + ESP_LOGE(TAG, "Humidity status bit was not set to 1?"); + } + humidity &= ~(0b11); // strip status & unassigned bits from data + + // map 16 bit humidity value into range [-6%, 118%] + float const humidity_partial = double(humidity) / (1 << 16); + float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0); + float const compensated_humidity_percentage = + humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; + ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); + + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->publish_state(compensated_humidity_percentage); + } + this->status_clear_warning(); +} + +void MS8607Component::calculate_values_(uint32_t d2_raw_temperature, uint32_t d1_raw_pressure) { + // Perform the first order pressure/temperature calculation + + // d_t: "difference between actual and reference temperature" = D2 - [C5] * 2**8 + const int32_t d_t = int32_t(d2_raw_temperature) - (int32_t(this->calibration_values_.reference_temperature) << 8); + // actual temperature as hundredths of degree celsius in range [-4000, 8500] + // 2000 + d_t * [C6] / (2**23) + int32_t temperature = + 2000 + ((int64_t(d_t) * this->calibration_values_.temperature_coefficient_of_temperature) >> 23); + + // offset at actual temperature. [C2] * (2**17) + (d_t * [C4] / (2**6)) + int64_t pressure_offset = (int64_t(this->calibration_values_.pressure_offset) << 17) + + ((int64_t(d_t) * this->calibration_values_.pressure_offset_temperature_coefficient) >> 6); + // sensitivity at actual temperature. [C1] * (2**16) + ([C3] * d_t) / (2**7) + int64_t pressure_sensitivity = + (int64_t(this->calibration_values_.pressure_sensitivity) << 16) + + ((int64_t(d_t) * this->calibration_values_.pressure_sensitivity_temperature_coefficient) >> 7); + + // Perform the second order compensation, for non-linearity over temperature range + const int64_t d_t_squared = int64_t(d_t) * d_t; + int64_t temperature_2 = 0; + int32_t pressure_offset_2 = 0; + int32_t pressure_sensitivity_2 = 0; + if (temperature < 2000) { + // (TEMP - 2000)**2 / 2**4 + const int32_t low_temperature_adjustment = (temperature - 2000) * (temperature - 2000) >> 4; + + // T2 = 3 * (d_t**2) / 2**33 + temperature_2 = (3 * d_t_squared) >> 33; + // OFF2 = 61 * (TEMP-2000)**2 / 2**4 + pressure_offset_2 = 61 * low_temperature_adjustment; + // SENS2 = 29 * (TEMP-2000)**2 / 2**4 + pressure_sensitivity_2 = 29 * low_temperature_adjustment; + + if (temperature < -1500) { + // (TEMP+1500)**2 + const int32_t very_low_temperature_adjustment = (temperature + 1500) * (temperature + 1500); + + // OFF2 = OFF2 + 17 * (TEMP+1500)**2 + pressure_offset_2 += 17 * very_low_temperature_adjustment; + // SENS2 = SENS2 + 9 * (TEMP+1500)**2 + pressure_sensitivity_2 += 9 * very_low_temperature_adjustment; + } + } else { + // T2 = 5 * (d_t**2) / 2**38 + temperature_2 = (5 * d_t_squared) >> 38; + } + + temperature -= temperature_2; + pressure_offset -= pressure_offset_2; + pressure_sensitivity -= pressure_sensitivity_2; + + // Temperature compensated pressure. [1000, 120000] => [10.00 mbar, 1200.00 mbar] + const int32_t pressure = (((d1_raw_pressure * pressure_sensitivity) >> 21) - pressure_offset) >> 15; + + const float temperature_float = temperature / 100.0f; + const float pressure_float = pressure / 100.0f; + ESP_LOGD(TAG, "Temperature=%0.2f°C, Pressure=%0.2fhPa", temperature_float, pressure_float); + + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(temperature_float); + } + if (this->pressure_sensor_ != nullptr) { + this->pressure_sensor_->publish_state(pressure_float); // hPa aka mbar + } + this->status_clear_warning(); + + if (this->humidity_sensor_ != nullptr) { + // now that we have temperature (to compensate the humidity with), kick off that read + this->request_read_humidity_(temperature_float); + } +} + +} // namespace ms8607 +} // namespace esphome diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h new file mode 100644 index 0000000000..0bee7e97b7 --- /dev/null +++ b/esphome/components/ms8607/ms8607.h @@ -0,0 +1,109 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ms8607 { + +/** + Class for I2CDevice used to communicate with the Humidity sensor + on the chip. See MS8607Component instead + */ +class MS8607HumidityDevice : public i2c::I2CDevice { + public: + uint8_t get_address() { return address_; } +}; + +/** + Temperature, pressure, and humidity sensor. + + By default, the MS8607 measures sensors at the highest resolution. + A potential enhancement would be to expose the resolution as a configurable + setting. A lower resolution speeds up ADC conversion time & uses less power. + + Datasheet: + https://www.te.com/commerce/DocumentDelivery/DDEController?Action=showdoc&DocId=Data+Sheet%7FMS8607-02BA01%7FB3%7Fpdf%7FEnglish%7FENG_DS_MS8607-02BA01_B3.pdf%7FCAT-BLPS0018 + + Other implementations: + - https://github.com/TEConnectivity/MS8607_Generic_C_Driver + - https://github.com/adafruit/Adafruit_MS8607 + - https://github.com/sparkfun/SparkFun_PHT_MS8607_Arduino_Library + */ +class MS8607Component : public PollingComponent, public i2c::I2CDevice { + public: + virtual ~MS8607Component() = default; + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; } + + protected: + /** + Read and store the Pressure & Temperature calibration settings from the PROM. + Intended to be called during setup(), this will set the `failure_reason_` + */ + bool read_calibration_values_from_prom_(); + + /// Start async temperature read + void request_read_temperature_(); + /// Process async temperature read + void read_temperature_(); + /// start async pressure read + void request_read_pressure_(uint32_t raw_temperature); + /// process async pressure read + void read_pressure_(uint32_t raw_temperature); + /// start async humidity read + void request_read_humidity_(float temperature_float); + /// process async humidity read + void read_humidity_(float temperature_float); + /// use raw temperature & pressure to calculate & publish values + void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure); + + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + sensor::Sensor *humidity_sensor_; + + /** I2CDevice object to communicate with secondary I2C address for the humidity sensor + * + * The MS8607 only has one set of I2C pins, despite using two different addresses. + * + * Default address for humidity is 0x40 + */ + MS8607HumidityDevice *humidity_device_; + + /// This device's pressure & temperature calibration values, read from PROM + struct CalibrationValues { + /// Pressure sensitivity | SENS-T1. [C1] + uint16_t pressure_sensitivity; + /// Temperature coefficient of pressure sensitivity | TCS. [C3] + uint16_t pressure_sensitivity_temperature_coefficient; + /// Pressure offset | OFF-T1. [C2] + uint16_t pressure_offset; + /// Temperature coefficient of pressure offset | TCO. [C4] + uint16_t pressure_offset_temperature_coefficient; + /// Reference temperature | T-REF. [C5] + uint16_t reference_temperature; + /// Temperature coefficient of the temperature | TEMPSENS. [C6] + uint16_t temperature_coefficient_of_temperature; + } calibration_values_; + + /// Possible failure reasons of this component + enum class ErrorCode; + /// Keep track of the reason why this component failed, to augment the dumped config + ErrorCode error_code_; + + /// Current progress through required component setup + enum class SetupStatus; + /// Current step in the multi-step & possibly delayed setup() process + SetupStatus setup_status_; +}; + +} // namespace ms8607 +} // namespace esphome diff --git a/esphome/components/ms8607/sensor.py b/esphome/components/ms8607/sensor.py new file mode 100644 index 0000000000..1113e14af2 --- /dev/null +++ b/esphome/components/ms8607/sensor.py @@ -0,0 +1,83 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +ms8607_ns = cg.esphome_ns.namespace("ms8607") +MS8607Component = ms8607_ns.class_( + "MS8607Component", cg.PollingComponent, i2c.I2CDevice +) + +CONF_HUMIDITY_I2C_ID = "humidity_i2c_id" +MS8607HumidityDevice = ms8607_ns.class_("MS8607HumidityDevice", i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MS8607Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, # Resolution: 0.01 + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=2, # Resolution: 0.016 + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, # Resolution: 0.04 + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_HUMIDITY_I2C_ID): cv.declare_id( + MS8607HumidityDevice + ), + } + ) + .extend(i2c.i2c_device_schema(0x40)), # default address for humidity + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x76)) # default address for temp/pressure +) + + +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) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + humidity_device = cg.new_Pvariable(humidity_config[CONF_HUMIDITY_I2C_ID]) + await i2c.register_i2c_device(humidity_device, humidity_config) + cg.add(var.set_humidity_device(humidity_device)) diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index a97587b7be..c511591856 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -1,5 +1,6 @@ #include "my9231.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace my9231 { @@ -51,7 +52,11 @@ void MY9231OutputComponent::setup() { MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE; ESP_LOGV(TAG, " Command: 0x%02X", command); - this->init_chips_(command); + { + InterruptLock lock; + this->send_dcki_pulses_(32 * this->num_chips_); + this->init_chips_(command); + } ESP_LOGV(TAG, " Chips initialized."); } void MY9231OutputComponent::dump_config() { @@ -66,11 +71,14 @@ void MY9231OutputComponent::loop() { if (!this->update_) return; - for (auto pwm_amount : this->pwm_amounts_) { - this->write_word_(pwm_amount, this->bit_depth_); + { + InterruptLock lock; + for (auto pwm_amount : this->pwm_amounts_) { + this->write_word_(pwm_amount, this->bit_depth_); + } + // Send 8 DI pulses. After 8 falling edges, the duty data are store. + this->send_di_pulses_(8); } - // Send 8 DI pulses. After 8 falling edges, the duty data are store. - this->send_di_pulses_(8); this->update_ = false; } void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) { @@ -92,6 +100,7 @@ void MY9231OutputComponent::init_chips_(uint8_t command) { // Send 16 DI pulse. After 14 falling edges, the command data are // stored and after 16 falling edges the duty mode is activated. this->send_di_pulses_(16); + delayMicroseconds(12); } void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) { for (uint8_t i = bits; i > 0; i--) { @@ -106,6 +115,13 @@ void MY9231OutputComponent::send_di_pulses_(uint8_t count) { this->pin_di_->digital_write(false); } } +void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) { + delayMicroseconds(12); + for (uint8_t i = 0; i < count; i++) { + this->pin_dcki_->digital_write(true); + this->pin_dcki_->digital_write(false); + } +} } // namespace my9231 } // namespace esphome diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index a777dcc960..77c1259853 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -49,6 +49,7 @@ class MY9231OutputComponent : public Component { void init_chips_(uint8_t command); void write_word_(uint16_t value, uint8_t bits); void send_di_pulses_(uint8_t count); + void send_dcki_pulses_(uint8_t count); GPIOPin *pin_di_; GPIOPin *pin_dcki_; diff --git a/esphome/components/nau7802/__init__.py b/esphome/components/nau7802/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp new file mode 100644 index 0000000000..ea6c0258af --- /dev/null +++ b/esphome/components/nau7802/nau7802.cpp @@ -0,0 +1,323 @@ +#include "nau7802.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace nau7802 { + +static const char *const TAG = "nau7802"; + +// Only define what we need + +static const uint8_t READ_BIT = 0x01; + +static const uint8_t PU_CTRL_REG = 0x00; +static const uint8_t PU_CTRL_REGISTER_RESET = 0x01; +static const uint8_t PU_CTRL_POWERUP_DIGITAL = 0x02; +static const uint8_t PU_CTRL_POWERUP_ANALOG = 0x04; +static const uint8_t PU_CTRL_POWERUP_READY = 0x08; +static const uint8_t PU_CTRL_CYCLE_START = 0x10; +static const uint8_t PU_CTRL_CYCLE_READY = 0x20; +static const uint8_t PU_CTRL_AVDD_EXTERNAL = 0x80; + +static const uint8_t CTRL1_REG = 0x01; +static const uint8_t CTRL1_LDO_SHIFT = 3; +static const uint8_t CTRL1_LDO_MASK = (0x7 << CTRL1_LDO_SHIFT); +static const uint8_t CTRL1_GAIN_MASK = 0x7; + +static const uint8_t CTRL2_REG = 0x02; +static const uint8_t CTRL2_CRS_SHIFT = 4; +static const uint8_t CTRL2_CRS_MASK = (0x7 << CTRL2_CRS_SHIFT); +static const uint8_t CTRL2_CALS = 0x04; +static const uint8_t CTRL2_CAL_ERR = 0x08; +static const uint8_t CTRL2_GAIN_CALIBRATION = 0x03; +static const uint8_t CTRL2_CONFIG_MASK = 0xF0; + +static const uint8_t OCAL1_B2_REG = 0x03; +static const uint8_t GCAL1_B3_REG = 0x06; +static const uint8_t GCAL1_FRACTIONAL = 23; + +// only need the first data register for sequential read method +static const uint8_t ADCO_B2_REG = 0x12; + +static const uint8_t ADC_REG = 0x15; +static const uint8_t ADC_CHPS_DISABLE = 0x30; + +static const uint8_t PGA_REG = 0x1B; +static const uint8_t PGA_LDOMODE_ESR = 0x40; + +static const uint8_t POWER_REG = 0x1C; +static const uint8_t POWER_PGA_CAP_EN = 0x80; + +static const uint8_t DEVICE_REV = 0x1F; + +void NAU7802Sensor::setup() { + i2c::I2CRegister pu_ctrl = this->reg(PU_CTRL_REG); + ESP_LOGCONFIG(TAG, "Setting up NAU7802 '%s'...", this->name_.c_str()); + uint8_t rev; + + if (this->read_register(DEVICE_REV | READ_BIT, &rev, 1)) { + ESP_LOGE(TAG, "Failed I2C read during setup()"); + this->mark_failed(); + return; + } + ESP_LOGI(TAG, "Setting up NAU7802 Rev %d", rev); + + // reset + pu_ctrl |= PU_CTRL_REGISTER_RESET; + delay(10); + pu_ctrl &= ~PU_CTRL_REGISTER_RESET; + + // power up digital hw + pu_ctrl |= PU_CTRL_POWERUP_DIGITAL; + + delay(1); + if (!(pu_ctrl.get() & PU_CTRL_POWERUP_READY)) { + ESP_LOGE(TAG, "Failed to reset sensor during setup()"); + this->mark_failed(); + return; + } + + uint32_t gcal = (uint32_t) (round(this->gain_calibration_ * (1 << GCAL1_FRACTIONAL))); + this->write_value_(OCAL1_B2_REG, 3, this->offset_calibration_); + this->write_value_(GCAL1_B3_REG, 4, gcal); + + // turn on AFE + pu_ctrl |= PU_CTRL_POWERUP_ANALOG; + auto f = std::bind(&NAU7802Sensor::complete_setup_, this); + this->set_timeout(600, f); +} + +void NAU7802Sensor::complete_setup_() { + i2c::I2CRegister pu_ctrl = this->reg(PU_CTRL_REG); + i2c::I2CRegister ctrl1 = this->reg(CTRL1_REG); + i2c::I2CRegister ctrl2 = this->reg(CTRL2_REG); + pu_ctrl |= PU_CTRL_CYCLE_START; + + // set gain + ctrl1 &= ~CTRL1_GAIN_MASK; + ctrl1 |= this->gain_; + + // enable internal LDO + if (this->ldo_ != NAU7802_LDO_EXTERNAL) { + pu_ctrl |= PU_CTRL_AVDD_EXTERNAL; + ctrl1 &= ~CTRL1_LDO_MASK; + ctrl1 |= this->ldo_ << CTRL1_LDO_SHIFT; + } + + // set sps + ctrl2 &= ~CTRL2_CRS_MASK; + ctrl2 |= this->sps_ << CTRL2_CRS_SHIFT; + + // disable ADC chopper clock + i2c::I2CRegister adc_reg = this->reg(ADC_REG); + adc_reg |= ADC_CHPS_DISABLE; + + // use low ESR caps + i2c::I2CRegister pga_reg = this->reg(PGA_REG); + pga_reg &= ~PGA_LDOMODE_ESR; + + // PGA stabilizer cap on output + i2c::I2CRegister pwr_reg = this->reg(POWER_REG); + pwr_reg |= POWER_PGA_CAP_EN; + + this->setup_complete_ = true; +} + +void NAU7802Sensor::dump_config() { + LOG_SENSOR("", "NAU7802", this); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with NAU7802 failed earlier, during setup"); + return; + } + // Note these may differ from the values on the device if calbration has been run + ESP_LOGCONFIG(TAG, " Offset Calibration: %s", to_string(this->offset_calibration_).c_str()); + ESP_LOGCONFIG(TAG, " Gain Calibration: %f", this->gain_calibration_); + + std::string voltage = "unknown"; + switch (this->ldo_) { + case NAU7802_LDO_2V4: + voltage = "2.4V"; + break; + case NAU7802_LDO_2V7: + voltage = "2.7V"; + break; + case NAU7802_LDO_3V0: + voltage = "3.0V"; + break; + case NAU7802_LDO_3V3: + voltage = "3.3V"; + break; + case NAU7802_LDO_3V6: + voltage = "3.6V"; + break; + case NAU7802_LDO_3V9: + voltage = "3.9V"; + break; + case NAU7802_LDO_4V2: + voltage = "4.2V"; + break; + case NAU7802_LDO_4V5: + voltage = "4.5V"; + break; + case NAU7802_LDO_EXTERNAL: + voltage = "External"; + break; + } + ESP_LOGCONFIG(TAG, " LDO Voltage: %s", voltage.c_str()); + int gain = 0; + switch (this->gain_) { + case NAU7802_GAIN_128: + gain = 128; + break; + case NAU7802_GAIN_64: + gain = 64; + break; + case NAU7802_GAIN_32: + gain = 32; + break; + case NAU7802_GAIN_16: + gain = 16; + break; + case NAU7802_GAIN_8: + gain = 8; + break; + case NAU7802_GAIN_4: + gain = 4; + break; + case NAU7802_GAIN_2: + gain = 2; + break; + case NAU7802_GAIN_1: + gain = 1; + break; + } + ESP_LOGCONFIG(TAG, " Gain: %dx", gain); + int sps = 0; + switch (this->sps_) { + case NAU7802_SPS_320: + sps = 320; + break; + case NAU7802_SPS_80: + sps = 80; + break; + case NAU7802_SPS_40: + sps = 40; + break; + case NAU7802_SPS_20: + sps = 20; + break; + case NAU7802_SPS_10: + sps = 10; + break; + } + ESP_LOGCONFIG(TAG, " Samples Per Second: %d", sps); + LOG_UPDATE_INTERVAL(this); +} + +void NAU7802Sensor::write_value_(uint8_t start_reg, size_t size, int32_t value) { + uint8_t data[4]; + for (int i = 0; i < size; i++) { + data[i] = 0xFF & (value >> (size - 1 - i) * 8); + } + this->write_register(start_reg, data, size); +} + +int32_t NAU7802Sensor::read_value_(uint8_t start_reg, size_t size) { + uint8_t data[4]; + this->read_register(start_reg, data, size); + int32_t result = 0; + for (int i = 0; i < size; i++) { + result |= data[i] << (size - 1 - i) * 8; + } + // extend sign bit + if (result & 0x800000 && size == 3) { + result |= 0xFF000000; + } + return result; +} + +bool NAU7802Sensor::calibrate_(enum NAU7802CalibrationModes mode) { + // check if already calbrating + if (this->state_ != CalibrationState::INACTIVE) { + ESP_LOGW(TAG, "Calibration already in progress"); + return false; + } + + this->state_ = mode == NAU7802_CALIBRATE_GAIN ? CalibrationState::GAIN : CalibrationState::OFFSET; + + i2c::I2CRegister ctrl2 = this->reg(CTRL2_REG); + // clear calibraye registers + ctrl2 &= CTRL2_CONFIG_MASK; + // Calibrate + ctrl2 |= mode; + ctrl2 |= CTRL2_CALS; + return true; +} + +void NAU7802Sensor::set_calibration_failure_(bool failed) { + switch (this->state_) { + case CalibrationState::GAIN: + this->gain_calibration_failed_ = failed; + break; + case CalibrationState::OFFSET: + this->offset_calibration_failed_ = failed; + break; + case CalibrationState::INACTIVE: + // shouldn't happen + break; + } +} + +void NAU7802Sensor::loop() { + i2c::I2CRegister ctrl2 = this->reg(CTRL2_REG); + + if (this->state_ != CalibrationState::INACTIVE && !(ctrl2.get() & CTRL2_CALS)) { + if (ctrl2.get() & CTRL2_CAL_ERR) { + this->set_calibration_failure_(true); + this->state_ = CalibrationState::INACTIVE; + ESP_LOGE(TAG, "Failed to calibrate sensor"); + this->status_set_error("Calibration Failed"); + return; + } + + this->set_calibration_failure_(false); + this->state_ = CalibrationState::INACTIVE; + + if (!this->offset_calibration_failed_ && !this->gain_calibration_failed_) + 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()); + 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); + } +} + +float NAU7802Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void NAU7802Sensor::update() { + if (!this->is_data_ready_()) { + ESP_LOGW(TAG, "No measurements ready!"); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + // Get the most recent sample to publish + int32_t result = this->read_value_(ADCO_B2_REG, 3); + + ESP_LOGD(TAG, "'%s': Got value %" PRId32, this->name_.c_str(), result); + this->publish_state(result); +} + +bool NAU7802Sensor::is_data_ready_() { return this->reg(PU_CTRL_REG).get() & PU_CTRL_CYCLE_READY; } + +bool NAU7802Sensor::can_proceed() { return this->setup_complete_; } + +} // namespace nau7802 +} // namespace esphome diff --git a/esphome/components/nau7802/nau7802.h b/esphome/components/nau7802/nau7802.h new file mode 100644 index 0000000000..3b0372aa89 --- /dev/null +++ b/esphome/components/nau7802/nau7802.h @@ -0,0 +1,121 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace nau7802 { + +enum NAU7802Gain { + NAU7802_GAIN_128 = 0b111, + NAU7802_GAIN_64 = 0b110, + NAU7802_GAIN_32 = 0b101, + NAU7802_GAIN_16 = 0b100, + NAU7802_GAIN_8 = 0b011, + NAU7802_GAIN_4 = 0b010, + NAU7802_GAIN_2 = 0b001, + NAU7802_GAIN_1 = 0b000, +}; + +enum NAU7802SPS { + NAU7802_SPS_320 = 0b111, + NAU7802_SPS_80 = 0b011, + NAU7802_SPS_40 = 0b010, + NAU7802_SPS_20 = 0b001, + NAU7802_SPS_10 = 0b000, +}; + +enum NAU7802LDO { + NAU7802_LDO_2V4 = 0b111, + NAU7802_LDO_2V7 = 0b110, + NAU7802_LDO_3V0 = 0b101, + NAU7802_LDO_3V3 = 0b100, + NAU7802_LDO_3V6 = 0b011, + NAU7802_LDO_3V9 = 0b010, + NAU7802_LDO_4V2 = 0b001, + NAU7802_LDO_4V5 = 0b000, + // Never write this to a register + NAU7802_LDO_EXTERNAL = 0b1000, +}; + +enum NAU7802CalibrationModes { + NAU7802_CALIBRATE_EXTERNAL_OFFSET = 0b10, + NAU7802_CALIBRATE_INTERNAL_OFFSET = 0b00, + NAU7802_CALIBRATE_GAIN = 0b11, +}; + +class NAU7802Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void set_samples_per_second(NAU7802SPS sps) { this->sps_ = sps; } + void set_ldo_voltage(NAU7802LDO ldo) { this->ldo_ = ldo; } + void set_gain(NAU7802Gain gain) { this->gain_ = gain; } + void set_gain_calibration(float gain_calibration) { this->gain_calibration_ = gain_calibration; } + void set_offset_calibration(int32_t offset_calibration) { this->offset_calibration_ = offset_calibration; } + bool calibrate_external_offset() { return this->calibrate_(NAU7802_CALIBRATE_EXTERNAL_OFFSET); } + bool calibrate_internal_offset() { return this->calibrate_(NAU7802_CALIBRATE_INTERNAL_OFFSET); } + bool calibrate_gain() { return this->calibrate_(NAU7802_CALIBRATE_GAIN); } + + void setup() override; + void loop() override; + bool can_proceed() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + // + // Internal state + // + enum class CalibrationState : uint8_t { + INACTIVE, + OFFSET, + GAIN, + } state_{CalibrationState::INACTIVE}; + + float gain_calibration_; + int32_t offset_calibration_; + bool offset_calibration_failed_ = false; + bool gain_calibration_failed_ = false; + bool setup_complete_ = false; + + // + // Config values + // + NAU7802LDO ldo_; + NAU7802SPS sps_; + NAU7802Gain gain_; + + // + // Internal Methods + // + bool calibrate_(enum NAU7802CalibrationModes mode); + void complete_setup_(); + void write_value_(uint8_t start_reg, size_t size, int32_t value); + int32_t read_value_(uint8_t start_reg, size_t size); + bool is_data_ready_(); + void set_calibration_failure_(bool failed); +}; + +template +class NAU7802CalbrateExternalOffsetAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->calibrate_external_offset(); } +}; + +template +class NAU7802CalbrateInternalOffsetAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->calibrate_internal_offset(); } +}; + +template class NAU7802CalbrateGainAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->calibrate_gain(); } +}; + +} // namespace nau7802 +} // namespace esphome diff --git a/esphome/components/nau7802/sensor.py b/esphome/components/nau7802/sensor.py new file mode 100644 index 0000000000..9192f48f53 --- /dev/null +++ b/esphome/components/nau7802/sensor.py @@ -0,0 +1,134 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import CONF_GAIN, CONF_ID, ICON_SCALE, STATE_CLASS_MEASUREMENT + +CODEOWNERS = ["@cujomalainey"] +DEPENDENCIES = ["i2c"] + +CONF_GAIN_CALIBRATION = "gain_calibration" +CONF_OFFSET_CALIBRATION = "offset_calibration" +CONF_LDO_VOLTAGE = "ldo_voltage" +CONF_SAMPLES_PER_SECOND = "samples_per_second" + +nau7802_ns = cg.esphome_ns.namespace("nau7802") +NAU7802Sensor = nau7802_ns.class_( + "NAU7802Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) +NAU7802CalbrateExternalOffsetAction = nau7802_ns.class_( + "NAU7802CalbrateExternalOffsetAction", + automation.Action, + cg.Parented.template(NAU7802Sensor), +) +NAU7802CalbrateInternalOffsetAction = nau7802_ns.class_( + "NAU7802CalbrateInternalOffsetAction", + automation.Action, + cg.Parented.template(NAU7802Sensor), +) +NAU7802CalbrateGainAction = nau7802_ns.class_( + "NAU7802CalbrateGainAction", automation.Action, cg.Parented.template(NAU7802Sensor) +) + +NAU7802Gain = nau7802_ns.enum("NAU7802Gain") +GAINS = { + 128: NAU7802Gain.NAU7802_GAIN_128, + 64: NAU7802Gain.NAU7802_GAIN_64, + 32: NAU7802Gain.NAU7802_GAIN_32, + 16: NAU7802Gain.NAU7802_GAIN_16, + 8: NAU7802Gain.NAU7802_GAIN_8, + 4: NAU7802Gain.NAU7802_GAIN_4, + 2: NAU7802Gain.NAU7802_GAIN_2, + 1: NAU7802Gain.NAU7802_GAIN_1, +} + +NAU7802SPS = nau7802_ns.enum("NAU7802SPS") +SAMPLES_PER_SECOND = { + 320: NAU7802SPS.NAU7802_SPS_320, + 80: NAU7802SPS.NAU7802_SPS_80, + 40: NAU7802SPS.NAU7802_SPS_40, + 20: NAU7802SPS.NAU7802_SPS_20, + 10: NAU7802SPS.NAU7802_SPS_10, +} + +NAU7802LDO = nau7802_ns.enum("NAU7802LDO") +LDO = { + "2.4V": NAU7802LDO.NAU7802_LDO_2V4, + "2.7V": NAU7802LDO.NAU7802_LDO_2V7, + "3.0V": NAU7802LDO.NAU7802_LDO_3V0, + "3.3V": NAU7802LDO.NAU7802_LDO_3V3, + "3.6V": NAU7802LDO.NAU7802_LDO_3V6, + "3.9V": NAU7802LDO.NAU7802_LDO_3V9, + "4.2V": NAU7802LDO.NAU7802_LDO_4V2, + "4.5V": NAU7802LDO.NAU7802_LDO_4V5, + "EXTERNAL": NAU7802LDO.NAU7802_LDO_EXTERNAL, + "EXT": NAU7802LDO.NAU7802_LDO_EXTERNAL, +} + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + NAU7802Sensor, + icon=ICON_SCALE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Optional(CONF_LDO_VOLTAGE, default="3.0V"): cv.enum(LDO, upper=True), + cv.Optional(CONF_SAMPLES_PER_SECOND, default=10): cv.enum( + SAMPLES_PER_SECOND, int=True + ), + cv.Optional(CONF_GAIN, default=128): cv.enum(GAINS, int=True), + cv.Optional(CONF_OFFSET_CALIBRATION, default=0): cv.int_range( + min=-8388608, max=8388607 + ), + cv.Optional(CONF_GAIN_CALIBRATION, default=1.0): cv.float_range( + min=0, max=511.9999998807907 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x2A)) +) + + +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 sensor.register_sensor(var, config) + + cg.add(var.set_samples_per_second(config[CONF_SAMPLES_PER_SECOND])) + cg.add(var.set_ldo_voltage(config[CONF_LDO_VOLTAGE])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_gain_calibration(config[CONF_GAIN_CALIBRATION])) + cg.add(var.set_offset_calibration(config[CONF_OFFSET_CALIBRATION])) + + +NAU7802_CALIBRATE_SCHEMA = maybe_simple_id( + { + cv.GenerateID(CONF_ID): cv.use_id(NAU7802Sensor), + } +) + + +@automation.register_action( + "nau7802.calibrate_internal_offset", + NAU7802CalbrateInternalOffsetAction, + NAU7802_CALIBRATE_SCHEMA, +) +@automation.register_action( + "nau7802.calibrate_external_offset", + NAU7802CalbrateExternalOffsetAction, + NAU7802_CALIBRATE_SCHEMA, +) +@automation.register_action( + "nau7802.calibrate_gain", + NAU7802CalbrateGainAction, + NAU7802_CALIBRATE_SCHEMA, +) +async def nau7802_calibrate_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/network/__init__.py b/esphome/components/network/__init__.py index 96cfc51ff5..be4e102930 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,10 +1,8 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components.esp32 import add_idf_sdkconfig_option - -from esphome.const import ( - CONF_ENABLE_IPV6, -) +import esphome.config_validation as cv +from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] @@ -14,16 +12,47 @@ IPAddress = network_ns.class_("IPAddress") CONFIG_SCHEMA = cv.Schema( { - cv.SplitDefault(CONF_ENABLE_IPV6, esp32_idf=False): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault( + CONF_ENABLE_IPV6, + esp8266=False, + esp32=False, + rp2040=False, + bk72xx=False, + ): cv.All( + cv.boolean, + cv.Any( + cv.require_framework_version( + esp_idf=cv.Version(0, 0, 0), + esp32_arduino=cv.Version(0, 0, 0), + esp8266_arduino=cv.Version(0, 0, 0), + rp2040_arduino=cv.Version(0, 0, 0), + bk72xx_libretiny=cv.Version(1, 7, 0), + ), + cv.boolean_false, + ), ), + cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, } ) async def to_code(config): - if CONF_ENABLE_IPV6 in config: - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", config[CONF_ENABLE_IPV6]) - add_idf_sdkconfig_option( - "CONFIG_LWIP_IPV6_AUTOCONFIG", config[CONF_ENABLE_IPV6] - ) + cg.add_define("USE_NETWORK") + if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: + cg.add_define("USE_NETWORK_IPV6", enable_ipv6) + if enable_ipv6: + cg.add_define( + "USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT] + ) + if CORE.using_esp_idf: + 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") + if CORE.is_rp2040: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") + if CORE.is_esp8266: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") + if CORE.is_bk72xx: + cg.add_build_flag("-DCONFIG_IPV6") diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index af198179ba..941934cf0a 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -1,45 +1,145 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include #include #include +#include "esphome/core/macros.h" +#include "esphome/core/helpers.h" + +#if defined(USE_ESP_IDF) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0) +#include +#endif + +#if USE_ARDUINO +#include +#include +#endif /* USE_ADRDUINO */ + +#ifdef USE_HOST +#include +using ip_addr_t = in_addr; +using ip4_addr_t = in_addr; +#define ipaddr_aton(x, y) inet_aton((x), (y)) +#endif + +#if USE_ESP32_FRAMEWORK_ARDUINO +#define arduino_ns Arduino_h +#elif USE_LIBRETINY +#define arduino_ns arduino +#elif USE_ARDUINO +#define arduino_ns +#endif + +#ifdef USE_ESP32 +#include +#include +#endif namespace esphome { namespace network { struct IPAddress { public: - IPAddress() : addr_({0, 0, 0, 0}) {} - IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) : addr_({first, second, third, fourth}) {} - IPAddress(uint32_t raw) { - addr_[0] = (uint8_t) (raw >> 0); - addr_[1] = (uint8_t) (raw >> 8); - addr_[2] = (uint8_t) (raw >> 16); - addr_[3] = (uint8_t) (raw >> 24); +#ifdef USE_HOST + IPAddress() { ip_addr_.s_addr = 0; } + IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { + this->ip_addr_.s_addr = htonl((first << 24) | (second << 16) | (third << 8) | fourth); } - operator uint32_t() const { - uint32_t res = 0; - res |= ((uint32_t) addr_[0]) << 0; - res |= ((uint32_t) addr_[1]) << 8; - res |= ((uint32_t) addr_[2]) << 16; - res |= ((uint32_t) addr_[3]) << 24; - return res; + 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; } +#else + IPAddress() { ip_addr_set_zero(&ip_addr_); } + IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { + IP_ADDR4(&ip_addr_, first, second, third, fourth); } - std::string str() const { - char buffer[24]; - snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", addr_[0], addr_[1], addr_[2], addr_[3]); - return buffer; + IPAddress(const ip_addr_t *other_ip) { ip_addr_copy(ip_addr_, *other_ip); } + IPAddress(const std::string &in_address) { ipaddr_aton(in_address.c_str(), &ip_addr_); } + IPAddress(ip4_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t)); +#if USE_ESP32 && LWIP_IPV6 + ip_addr_.type = IPADDR_TYPE_V4; +#endif } - bool operator==(const IPAddress &other) const { - return addr_[0] == other.addr_[0] && addr_[1] == other.addr_[1] && addr_[2] == other.addr_[2] && - addr_[3] == other.addr_[3]; +#if USE_ARDUINO + IPAddress(const arduino_ns::IPAddress &other_ip) { ip_addr_set_ip4_u32(&ip_addr_, other_ip); } +#endif +#if LWIP_IPV6 + IPAddress(ip6_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip6_addr_t)); + ip_addr_.type = IPADDR_TYPE_V6; } - uint8_t operator[](int index) const { return addr_[index]; } - uint8_t &operator[](int index) { return addr_[index]; } +#endif /* LWIP_IPV6 */ + +#ifdef USE_ESP32 +#if LWIP_IPV6 + IPAddress(esp_ip6_addr_t *other_ip) { + memcpy((void *) &ip_addr_.u_addr.ip6, (void *) other_ip, sizeof(esp_ip6_addr_t)); + 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_ip_addr_t *other_ip) { +#if LWIP_IPV6 + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_)); +#else + memcpy((void *) &ip_addr_, (void *) &other_ip->u_addr.ip4, sizeof(ip_addr_)); +#endif + } + operator esp_ip_addr_t() const { + esp_ip_addr_t tmp; +#if LWIP_IPV6 + memcpy((void *) &tmp, (void *) &ip_addr_, sizeof(ip_addr_)); +#else + memcpy((void *) &tmp.u_addr.ip4, (void *) &ip_addr_, sizeof(ip_addr_)); +#endif /* LWIP_IPV6 */ + return tmp; + } + operator esp_ip4_addr_t() const { + esp_ip4_addr_t tmp; +#if LWIP_IPV6 + memcpy((void *) &tmp, (void *) &ip_addr_.u_addr.ip4, sizeof(esp_ip4_addr_t)); +#else + memcpy((void *) &tmp, (void *) &ip_addr_, sizeof(ip_addr_)); +#endif /* LWIP_IPV6 */ + return tmp; + } +#endif /* USE_ESP32 */ + + operator ip_addr_t() const { return ip_addr_; } +#if LWIP_IPV6 + operator ip4_addr_t() const { return *ip_2_ip4(&ip_addr_); } +#endif /* LWIP_IPV6 */ + +#if USE_ARDUINO + operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); } +#endif + + bool is_set() { return !ip_addr_isany(&ip_addr_); } + bool is_ip4() { return IP_IS_V4(&ip_addr_); } + bool is_ip6() { return IP_IS_V6(&ip_addr_); } + std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } + 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) { + if (IP_IS_V4(&ip_addr_)) { +#if LWIP_IPV6 + (((u8_t *) (&ip_addr_.u_addr.ip4))[3]) += increase; +#else + (((u8_t *) (&ip_addr_.addr))[3]) += increase; +#endif /* LWIP_IPV6 */ + } + return *this; + } +#endif protected: - std::array addr_; + ip_addr_t ip_addr_; }; +using IPAddresses = std::array; + } // namespace network } // namespace esphome +#endif diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 941102d6c1..ed519f738a 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -1,6 +1,6 @@ #include "util.h" #include "esphome/core/defines.h" - +#ifdef USE_NETWORK #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" #endif @@ -29,14 +29,22 @@ bool is_connected() { return false; } -network::IPAddress get_ip_address() { +bool is_disabled() { +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->is_disabled(); +#endif + return false; +} + +network::IPAddresses get_ip_addresses() { #ifdef USE_ETHERNET if (ethernet::global_eth_component != nullptr) - return ethernet::global_eth_component->get_ip_address(); + return ethernet::global_eth_component->get_ip_addresses(); #endif #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->get_ip_address(); + return wifi::global_wifi_component->get_ip_addresses(); #endif return {}; } @@ -55,3 +63,4 @@ std::string get_use_address() { } // namespace network } // namespace esphome +#endif diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index f248d5cbf4..b518696e68 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include "ip_address.h" @@ -8,9 +9,12 @@ namespace network { /// Return whether the node is connected to the network (through wifi, eth, ...) bool is_connected(); +/// Return whether the network is disabled (only wifi for now) +bool is_disabled(); /// Get the active network hostname std::string get_use_address(); -IPAddress get_ip_address(); +IPAddresses get_ip_addresses(); } // namespace network } // namespace esphome +#endif diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 210d7b2e2b..f51fe6b4f8 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -33,5 +33,14 @@ class PageTrigger : public Trigger { } }; +class TouchTrigger : public Trigger { + public: + explicit TouchTrigger(Nextion *nextion) { + nextion->add_touch_event_callback([this](uint8_t page_id, uint8_t component_id, bool touch_event) { + this->trigger(page_id, component_id, touch_event); + }); + } +}; + } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index b2a857c888..2924f66d3c 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -1,12 +1,11 @@ from string import ascii_letters, digits -import esphome.config_validation as cv + import esphome.codegen as cg from esphome.components import color -from esphome.const import ( - CONF_VISIBLE, -) -from . import CONF_NEXTION_ID -from . import Nextion +import esphome.config_validation as cv +from esphome.const import CONF_BACKGROUND_COLOR, CONF_FOREGROUND_COLOR, CONF_VISIBLE + +from . import CONF_NEXTION_ID, Nextion CONF_VARIABLE_NAME = "variable_name" CONF_COMPONENT_NAME = "component_name" @@ -21,24 +20,25 @@ CONF_ON_SETUP = "on_setup" CONF_ON_PAGE = "on_page" CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" CONF_WAKE_UP_PAGE = "wake_up_page" +CONF_START_UP_PAGE = "start_up_page" CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" CONF_WAVE_MAX_LENGTH = "wave_max_length" -CONF_BACKGROUND_COLOR = "background_color" CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" -CONF_FOREGROUND_COLOR = "foreground_color" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_FONT_ID = "font_id" +CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" +CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake" def NextionName(value): - valid_chars = f"{ascii_letters + digits}." + valid_chars = f"{ascii_letters + digits + '_'}." if not isinstance(value, str) or len(value) > 29: raise cv.Invalid("Must be a string less than 29 characters") for char in value: if char not in valid_chars: raise cv.Invalid( - f"Must only consist of upper/lowercase characters, numbers and the period '.'. The character '{char}' cannot be used." + f"Must only consist of upper/lowercase characters, numbers, the underscore '_', and the period '.'. The character '{char}' cannot be used." ) return value diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp index bf6e74cb38..499cd901c0 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -27,7 +27,7 @@ void NextionBinarySensor::process_touch(uint8_t page_id, uint8_t component_id, b } void NextionBinarySensor::update() { - if (!this->nextion_->is_setup()) + if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; if (this->variable_name_.empty()) // This is a touch component @@ -37,7 +37,7 @@ void NextionBinarySensor::update() { } void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nextion) { - if (!this->nextion_->is_setup()) + if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; if (this->component_id_ == 0) // This is a legacy touch component diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 72f56bd6f3..e403ba7ae8 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -2,11 +2,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import display, uart +from esphome.components import esp32 from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_BRIGHTNESS, CONF_TRIGGER_ID, + CONF_ON_TOUCH, ) from esphome.core import CORE from . import Nextion, nextion_ns, nextion_ref @@ -18,10 +20,13 @@ from .base_component import ( CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, + CONF_START_UP_PAGE, CONF_AUTO_WAKE_ON_TOUCH, + CONF_EXIT_REPARSE_ON_START, + CONF_SKIP_CONNECTION_HANDSHAKE, ) -CODEOWNERS = ["@senexcrenshaw"] +CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] @@ -30,12 +35,13 @@ SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) PageTrigger = nextion_ns.class_("PageTrigger", automation.Trigger.template()) +TouchTrigger = nextion_ns.class_("TouchTrigger", automation.Trigger.template()) CONFIG_SCHEMA = ( display.BASIC_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Nextion), - cv.Optional(CONF_TFT_URL): cv.All(cv.string, cv.only_with_arduino), + cv.Optional(CONF_TFT_URL): cv.url, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_ON_SETUP): automation.validate_automation( { @@ -57,9 +63,17 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PageTrigger), } ), + cv.Optional(CONF_ON_TOUCH): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TouchTrigger), + } + ), cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), - cv.Optional(CONF_WAKE_UP_PAGE): cv.positive_int, + cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t, + cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, cv.Optional(CONF_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean, + cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, + cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("5s")) @@ -69,7 +83,6 @@ CONFIG_SCHEMA = ( 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_BRIGHTNESS in config: @@ -83,10 +96,15 @@ async def to_code(config): if CONF_TFT_URL in config: cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) - if CORE.is_esp32: + if CORE.is_esp32 and CORE.using_arduino: cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) - if CORE.is_esp8266: + elif CORE.is_esp32 and CORE.using_esp_idf: + 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: cg.add_library("ESP8266HTTPClient", None) if CONF_TOUCH_SLEEP_TIMEOUT in config: @@ -95,8 +113,14 @@ async def to_code(config): if CONF_WAKE_UP_PAGE in config: cg.add(var.set_wake_up_page_internal(config[CONF_WAKE_UP_PAGE])) - if CONF_AUTO_WAKE_ON_TOUCH in config: - cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + if CONF_START_UP_PAGE in config: + cg.add(var.set_start_up_page_internal(config[CONF_START_UP_PAGE])) + + cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + + cg.add(var.set_exit_reparse_on_start_internal(config[CONF_EXIT_REPARSE_ON_START])) + + cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE])) await display.register_display(var, config) @@ -115,3 +139,15 @@ async def to_code(config): for conf in config.get(CONF_ON_PAGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(cg.uint8, "x")], conf) + + for conf in config.get(CONF_ON_TOUCH, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + (cg.uint8, "page_id"), + (cg.uint8, "component_id"), + (cg.bool_, "touch_event"), + ], + conf, + ) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index f8beaeab78..a80f6efc91 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -2,6 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include namespace esphome { namespace nextion { @@ -42,11 +43,24 @@ bool Nextion::check_connect_() { if (this->get_is_connected_()) return true; + // Check if the handshake should be skipped for the Nextion connection + if (this->skip_connection_handshake_) { + // Log the connection status without handshake + ESP_LOGW(TAG, "Nextion display set as connected without performing handshake"); + // Set the connection status to true + this->is_connected_ = true; + // Return true indicating the connection is set + return true; + } + if (this->comok_sent_ == 0) { this->reset_(false); this->ignore_is_setup_ = true; this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating + if (this->exit_reparse_on_start_) { + this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); + } this->send_command_("connect"); this->comok_sent_ = millis(); @@ -61,6 +75,11 @@ bool Nextion::check_connect_() { std::string response; this->recv_ret_string_(response, 0, false); + if (!response.empty() && response[0] == 0x1A) { + // Swallow invalid variable name responses that may be caused by the above commands + ESP_LOGD(TAG, "0x1A error ignored during setup"); + return false; + } if (response.empty() || response.find("comok") == std::string::npos) { #ifdef NEXTION_PROTOCOL_LOG ESP_LOGN(TAG, "Bad connect request %s", response.c_str()); @@ -88,7 +107,8 @@ bool Nextion::check_connect_() { connect_info.push_back(response.substr(start, end - start)); } - if (connect_info.size() == 7) { + this->is_detected_ = (connect_info.size() == 7); + if (this->is_detected_) { ESP_LOGN(TAG, "Received connect_info %zu", connect_info.size()); this->device_model_ = connect_info[2]; @@ -111,22 +131,32 @@ void Nextion::reset_(bool reset_nextion) { this->read_byte(&d); }; this->nextion_queue_.clear(); + this->waveform_queue_.clear(); } void Nextion::dump_config() { ESP_LOGCONFIG(TAG, "Nextion:"); - ESP_LOGCONFIG(TAG, " Device Model: %s", this->device_model_.c_str()); - ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str()); - ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str()); - ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str()); - ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False"); + if (this->skip_connection_handshake_) { + ESP_LOGCONFIG(TAG, " Skip handshake: %s", YESNO(this->skip_connection_handshake_)); + } else { + ESP_LOGCONFIG(TAG, " Device Model: %s", this->device_model_.c_str()); + ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str()); + ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str()); + } + ESP_LOGCONFIG(TAG, " Wake On Touch: %s", YESNO(this->auto_wake_on_touch_)); + ESP_LOGCONFIG(TAG, " Exit reparse: %s", YESNO(this->exit_reparse_on_start_)); if (this->touch_sleep_timeout_ != 0) { - ESP_LOGCONFIG(TAG, " Touch Timeout: %d", this->touch_sleep_timeout_); + ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); } if (this->wake_up_page_ != -1) { - ESP_LOGCONFIG(TAG, " Wake Up Page : %d", this->wake_up_page_); + ESP_LOGCONFIG(TAG, " Wake Up Page: %" PRId16, this->wake_up_page_); + } + + if (this->start_up_page_ != -1) { + ESP_LOGCONFIG(TAG, " Start Up Page: %" PRId16, this->start_up_page_); } } @@ -156,6 +186,10 @@ void Nextion::add_new_page_callback(std::function &&callback) { this->page_callback_.add(std::move(callback)); } +void Nextion::add_touch_event_callback(std::function &&callback) { + this->touch_callback_.add(std::move(callback)); +} + void Nextion::update_all_components() { if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) return; @@ -174,6 +208,17 @@ void Nextion::update_all_components() { } } +bool Nextion::send_command(const char *command) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return false; + + if (this->send_command_(command)) { + this->add_no_result_to_queue_("send_command"); + return true; + } + return false; +} + bool Nextion::send_command_printf(const char *format, ...) { if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) return false; @@ -225,9 +270,15 @@ void Nextion::loop() { this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. this->set_backlight_brightness(this->brightness_); - this->goto_page("0"); + // Check if a startup page has been set and send the command + if (this->start_up_page_ != -1) { + this->goto_page(this->start_up_page_); + } + + // This could probably be removed from the loop area, as those are redundant. this->set_auto_wake_on_touch(this->auto_wake_on_touch_); + this->set_exit_reparse_on_start(this->exit_reparse_on_start_); if (this->touch_sleep_timeout_ != 0) { this->set_touch_sleep_timeout(this->touch_sleep_timeout_); @@ -359,37 +410,21 @@ void Nextion::process_nextion_commands_() { ESP_LOGW(TAG, "Nextion reported baud rate invalid!"); break; case 0x12: // invalid Waveform ID or Channel # was used + if (this->waveform_queue_.empty()) { + ESP_LOGW(TAG, + "Nextion reported invalid Waveform ID or Channel # was used but no waveform sensor in queue found!"); + } else { + auto &nb = this->waveform_queue_.front(); + NextionComponentBase *component = nb->component; - if (!this->nextion_queue_.empty()) { - int index = 0; - int found = -1; - for (auto &nb : this->nextion_queue_) { - NextionComponentBase *component = nb->component; + ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!", + component->get_component_id(), component->get_wave_channel_id()); - if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { - ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!", - component->get_component_id(), component->get_wave_channel_id()); + ESP_LOGN(TAG, "Removing waveform from queue with component id %d and waveform id %d", + component->get_component_id(), component->get_wave_channel_id()); - ESP_LOGN(TAG, "Removing waveform from queue with component id %d and waveform id %d", - component->get_component_id(), component->get_wave_channel_id()); - - found = index; - - delete component; // NOLINT(cppcoreguidelines-owning-memory) - delete nb; // NOLINT(cppcoreguidelines-owning-memory) - - break; - } - ++index; - } - - if (found != -1) { - this->nextion_queue_.erase(this->nextion_queue_.begin() + found); - } else { - ESP_LOGW( - TAG, - "Nextion reported invalid Waveform ID or Channel # was used but no waveform sensor in queue found!"); - } + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->waveform_queue_.pop_front(); } break; case 0x1A: // variable name invalid @@ -434,11 +469,14 @@ void Nextion::process_nextion_commands_() { uint8_t page_id = to_process[0]; uint8_t component_id = to_process[1]; uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Got touch page=%u component=%u type=%s", page_id, component_id, - touch_event ? "PRESS" : "RELEASE"); + ESP_LOGD(TAG, "Got touch event:"); + ESP_LOGD(TAG, " page_id: %u", page_id); + ESP_LOGD(TAG, " component_id: %u", component_id); + ESP_LOGD(TAG, " event type: %s", touch_event ? "PRESS" : "RELEASE"); for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } + this->touch_callback_.call(page_id, component_id, touch_event != 0); break; } case 0x66: { // Nextion initiated new page event return data. @@ -449,7 +487,7 @@ void Nextion::process_nextion_commands_() { } uint8_t page_id = to_process[0]; - ESP_LOGD(TAG, "Got new page=%u", page_id); + ESP_LOGD(TAG, "Got new page: %u", page_id); this->page_callback_.call(page_id); break; } @@ -467,7 +505,10 @@ void Nextion::process_nextion_commands_() { uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Got touch at x=%u y=%u type=%s", x, y, touch_event ? "PRESS" : "RELEASE"); + ESP_LOGD(TAG, "Got touch event:"); + ESP_LOGD(TAG, " x: %u", x); + ESP_LOGD(TAG, " y: %u", y); + ESP_LOGD(TAG, " type: %s", touch_event ? "PRESS" : "RELEASE"); break; } @@ -589,7 +630,9 @@ void Nextion::process_nextion_commands_() { variable_name = to_process.substr(0, index); ++index; - ESP_LOGN(TAG, "Got Switch variable_name=%s value=%d", variable_name.c_str(), to_process[0] != 0); + ESP_LOGN(TAG, "Got Switch:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", to_process[0] != 0); for (auto *switchtype : this->switchtype_) { switchtype->process_bool(variable_name, to_process[index] != 0); @@ -620,7 +663,9 @@ void Nextion::process_nextion_commands_() { value += to_process[i + index + 1] << (8 * i); } - ESP_LOGN(TAG, "Got sensor variable_name=%s value=%d", variable_name.c_str(), value); + ESP_LOGN(TAG, "Got sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", value); for (auto *sensor : this->sensortype_) { sensor->process_sensor(variable_name, value); @@ -652,7 +697,9 @@ void Nextion::process_nextion_commands_() { text_value = to_process.substr(index); - ESP_LOGN(TAG, "Got Text Sensor variable_name=%s value=%s", variable_name.c_str(), text_value.c_str()); + ESP_LOGN(TAG, "Got Text Sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %s", text_value.c_str()); // NextionTextSensorResponseQueue *nq = new NextionTextSensorResponseQueue; // nq->variable_name = variable_name; @@ -683,7 +730,9 @@ void Nextion::process_nextion_commands_() { variable_name = to_process.substr(0, index); ++index; - ESP_LOGN(TAG, "Got Binary Sensor variable_name=%s value=%d", variable_name.c_str(), to_process[index] != 0); + ESP_LOGN(TAG, "Got Binary Sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", to_process[index] != 0); for (auto *binarysensortype : this->binarysensortype_) { binarysensortype->process_bool(&variable_name[0], to_process[index] != 0); @@ -692,44 +741,29 @@ void Nextion::process_nextion_commands_() { } case 0xFD: { // data transparent transmit finished ESP_LOGVV(TAG, "Nextion reported data transmit finished!"); + this->check_pending_waveform_(); break; } case 0xFE: { // data transparent transmit ready ESP_LOGVV(TAG, "Nextion reported ready for transmit!"); - - int index = 0; - int found = -1; - for (auto &nb : this->nextion_queue_) { - auto *component = nb->component; - if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { - size_t buffer_to_send = component->get_wave_buffer().size() < 255 ? component->get_wave_buffer().size() - : 255; // ADDT command can only send 255 - - this->write_array(component->get_wave_buffer().data(), static_cast(buffer_to_send)); - - ESP_LOGN(TAG, "Nextion sending waveform data for component id %d and waveform id %d, size %zu", - component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); - - if (component->get_wave_buffer().size() <= 255) { - component->get_wave_buffer().clear(); - } else { - component->get_wave_buffer().erase(component->get_wave_buffer().begin(), - component->get_wave_buffer().begin() + buffer_to_send); - } - found = index; - delete component; // NOLINT(cppcoreguidelines-owning-memory) - delete nb; // NOLINT(cppcoreguidelines-owning-memory) - break; - } - ++index; - } - - if (found == -1) { + if (this->waveform_queue_.empty()) { ESP_LOGE(TAG, "No waveforms in queue to send data!"); break; - } else { - this->nextion_queue_.erase(this->nextion_queue_.begin() + found); } + + auto &nb = this->waveform_queue_.front(); + auto *component = nb->component; + size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() + : 255; // ADDT command can only send 255 + + this->write_array(component->get_wave_buffer().data(), static_cast(buffer_to_send)); + + ESP_LOGN(TAG, "Nextion sending waveform data for component id %d and waveform id %d, size %zu", + component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); + + component->clear_wave_buffer(buffer_to_send); + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->waveform_queue_.pop_front(); break; } default: @@ -788,7 +822,10 @@ void Nextion::set_nextion_sensor_state(int queue_type, const std::string &name, } void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state) { - ESP_LOGN(TAG, "Received state for variable %s, state %lf for queue type %d", name.c_str(), state, queue_type); + ESP_LOGN(TAG, "Received state:"); + ESP_LOGN(TAG, " variable: %s", name.c_str()); + ESP_LOGN(TAG, " state: %lf", state); + ESP_LOGN(TAG, " queue type: %d", queue_type); switch (queue_type) { case NextionQueueType::SENSOR: { @@ -825,7 +862,9 @@ void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::s } void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { - ESP_LOGD(TAG, "Received state for variable %s, state %s", name.c_str(), state.c_str()); + ESP_LOGD(TAG, "Received state:"); + ESP_LOGD(TAG, " variable: %s", name.c_str()); + ESP_LOGD(TAG, " state: %s", state.c_str()); for (auto *sensor : this->textsensortype_) { if (name == sensor->get_variable_name()) { @@ -885,6 +924,12 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool start = millis(); while ((timeout == 0 && this->available()) || millis() - start <= timeout) { + if (!this->available()) { + App.feed_wdt(); + delay(1); + continue; + } + this->read_byte(&c); if (c == 0xFF) { nr_of_ff_bytes++; @@ -903,7 +948,7 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool } } App.feed_wdt(); - delay(1); + delay(2); if (exit_flag || ff_flag) { break; @@ -1005,23 +1050,23 @@ bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_na * @param is_sleep_safe The command is safe to send when the Nextion is sleeping */ -void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) { +void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, int32_t state_value) { this->add_no_result_to_queue_with_set(component->get_variable_name(), component->get_variable_name_to_send(), state_value); } void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name, - const std::string &variable_name_to_send, int state_value) { + const std::string &variable_name_to_send, int32_t state_value) { this->add_no_result_to_queue_with_set_internal_(variable_name, variable_name_to_send, state_value); } void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name, - const std::string &variable_name_to_send, int state_value, + const std::string &variable_name_to_send, int32_t state_value, bool is_sleep_safe) { if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) return; - this->add_no_result_to_queue_with_ignore_sleep_printf_(variable_name, "%s=%d", variable_name_to_send.c_str(), + this->add_no_result_to_queue_with_ignore_sleep_printf_(variable_name, "%s=%" PRId32, variable_name_to_send.c_str(), state_value); } @@ -1088,17 +1133,28 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component = component; nextion_queue->queue_time = millis(); + this->waveform_queue_.push_back(nextion_queue); + if (this->waveform_queue_.size() == 1) + this->check_pending_waveform_(); +} + +void Nextion::check_pending_waveform_() { + if (this->waveform_queue_.empty()) + return; + + auto *nb = this->waveform_queue_.front(); + auto *component = nb->component; size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255; // ADDT command can only send 255 std::string command = "addt " + to_string(component->get_component_id()) + "," + to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send); - if (this->send_command_(command)) { - this->nextion_queue_.push_back(nextion_queue); + if (!this->send_command_(command)) { + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->waveform_queue_.pop_front(); } } @@ -1107,5 +1163,7 @@ void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = write ESPDEPRECATED("set_wait_for_ack(bool) is deprecated and has no effect", "v1.20") void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "This command is deprecated"); } +bool Nextion::is_updating() { return this->is_updating_; } + } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 86610ef564..732ee9b455 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -4,24 +4,26 @@ #include #include "esphome/core/defines.h" +#include "esphome/core/time.h" + #include "esphome/components/uart/uart.h" #include "nextion_base.h" #include "nextion_component.h" #include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD +#ifdef USE_ARDUINO #ifdef USE_ESP32 #include -#endif +#endif // USE_ESP32 #ifdef USE_ESP8266 #include #include -#endif -#endif - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#endif // USE_ESP8266 +#elif defined(USE_ESP_IDF) +#include +#endif // ARDUINO vs USE_ESP_IDF +#endif // USE_NEXTION_TFT_UPLOAD namespace esphome { namespace nextion { @@ -48,6 +50,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * This will set the `txt` property of the component `textview` to `Hello World`. */ void set_component_text(const char *component, const char *text); + /** * Set the text of a component to a formatted string * @param component The component name. @@ -64,6 +67,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * For example when `uptime_sensor` = 506, then, `The uptime is: 506` will be displayed. */ void set_component_text_printf(const char *component, const char *format, ...) __attribute__((format(printf, 3, 4))); + /** * Set the integer value of a component * @param component The component name. @@ -76,33 +80,38 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * This will change the property `value` of the component `gauge` to 50. */ - void set_component_value(const char *component, int value); + void set_component_value(const char *component, int32_t value); + /** * Set the picture of an image component. * @param component The component name. - * @param value The picture name. + * @param value The picture id. * * Example: * ```cpp - * it.set_component_picture("pic", "4"); + * it.set_component_picture("pic", 4); * ``` * * This will change the image of the component `pic` to the image with ID `4`. */ - void set_component_picture(const char *component, const char *picture); + void set_component_picture(const char *component, uint8_t picture_id); + /** * Set the background color of a component. * @param component The component name. - * @param color The color (as a uint32_t). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_background_color("button", 0xFF0000); + * it.set_component_background_color("button", 63488); * ``` * * This will change the background color of the component `button` to red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_background_color(const char *component, uint32_t color); + void set_component_background_color(const char *component, uint16_t color); + /** * Set the background color of a component. * @param component The component name. @@ -113,11 +122,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_background_color("button", "RED"); * ``` * - * This will change the background color of the component `button` to blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the background color of the component `button` to red. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_background_color(const char *component, const char *color); + /** * Set the background color of a component. * @param component The component name. @@ -125,26 +134,31 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_background_color("button", color); + * auto blue = Color(0, 0, 255); + * it.set_component_background_color("button", blue); * ``` * - * This will change the background color of the component `button` to what color contains. + * This will change the background color of the component `button` to blue. */ void set_component_background_color(const char *component, Color color) override; + /** * Set the pressed background color of a component. * @param component The component name. - * @param color The color (as a int). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_pressed_background_color("button", 0xFF0000 ); + * it.set_component_pressed_background_color("button", 63488); * ``` * * This will change the pressed background color of the component `button` to red. This is the background color that * is shown when the component is pressed. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_pressed_background_color(const char *component, uint32_t color); + void set_component_pressed_background_color(const char *component, uint16_t color); + /** * Set the pressed background color of a component. * @param component The component name. @@ -155,12 +169,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_pressed_background_color("button", "RED"); * ``` * - * This will change the pressed background color of the component `button` to blue. This is the background color that - * is shown when the component is pressed. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * This will change the pressed background color of the component `button` to red. This is the background color that + * is shown when the component is pressed. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_pressed_background_color(const char *component, const char *color); + /** * Set the pressed background color of a component. * @param component The component name. @@ -168,16 +182,109 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_pressed_background_color("button", color); + * auto red = Color(255, 0, 0); + * it.set_component_pressed_background_color("button", red); * ``` * - * This will change the pressed background color of the component `button` to blue. This is the background color that - * is shown when the component is pressed. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * This will change the pressed background color of the component `button` to red. This is the background color that + * is shown when the component is pressed. */ void set_component_pressed_background_color(const char *component, Color color) override; + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as a uint16_t). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", 63488); + * ``` + * + * This will change the foreground color of the component `button` to red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_foreground_color(const char *component, uint16_t color); + + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as a string). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", "RED"); + * ``` + * + * This will change the foreground color of the component `button` to red. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. + */ + void set_component_foreground_color(const char *component, const char *color); + + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", Color::BLACK); + * ``` + * + * This will change the foreground color of the component `button` to black. + */ + void set_component_foreground_color(const char *component, Color color) override; + + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as a uint16_t). + * + * Example: + * ```cpp + * it.set_component_pressed_foreground_color("button", 63488 ); + * ``` + * + * This will change the pressed foreground color of the component `button` to red. This is the foreground color that + * is shown when the component is pressed. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_pressed_foreground_color(const char *component, uint16_t color); + + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as a string). + * + * Example: + * ```cpp + * it.set_component_pressed_foreground_color("button", "RED"); + * ``` + * + * This will change the pressed foreground color of the component `button` to red. This is the foreground color that + * is shown when the component is pressed. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. + */ + void set_component_pressed_foreground_color(const char *component, const char *color); + + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * auto blue = Color(0, 0, 255); + * it.set_component_pressed_foreground_color("button", blue); + * ``` + * + * This will change the pressed foreground color of the component `button` to blue. This is the foreground color that + * is shown when the component is pressed. + */ + void set_component_pressed_foreground_color(const char *component, Color color) override; + /** * Set the picture id of a component. * @param component The component name. @@ -191,6 +298,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); + /** * Set the background picture id of component. * @param component The component name. @@ -208,16 +316,19 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe /** * Set the font color of a component. * @param component The component name. - * @param color The color (as a uint32_t ). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_font_color("textview", 0xFF0000); + * it.set_component_font_color("textview", 63488); * ``` * * This will change the font color of the component `textview` to a red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_font_color(const char *component, uint32_t color); + void set_component_font_color(const char *component, uint16_t color); + /** * Set the font color of a component. * @param component The component name. @@ -228,11 +339,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_font_color("textview", "RED"); * ``` * - * This will change the font color of the component `textview` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the font color of the component `textview` to a red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_font_color(const char *component, const char *color); + /** * Set the font color of a component. * @param component The component name. @@ -240,27 +351,29 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_font_color("textview", color); + * it.set_component_font_color("textview", Color::BLACK); * ``` * - * This will change the font color of the component `textview` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the font color of the component `textview` to black. */ void set_component_font_color(const char *component, Color color) override; + /** * Set the pressed font color of a component. * @param component The component name. - * @param color The color (as a uint32_t). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_pressed_font_color("button", 0xFF0000); + * it.set_component_pressed_font_color("button", 63488); * ``` * * This will change the pressed font color of the component `button` to a red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_pressed_font_color(const char *component, uint32_t color); + void set_component_pressed_font_color(const char *component, uint16_t color); + /** * Set the pressed font color of a component. * @param component The component name. @@ -271,11 +384,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_pressed_font_color("button", "RED"); * ``` * - * This will change the pressed font color of the component `button` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the pressed font color of the component `button` to a red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_pressed_font_color(const char *component, const char *color); + /** * Set the pressed font color of a component. * @param component The component name. @@ -283,14 +396,13 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_pressed_font_color("button", color); + * it.set_component_pressed_font_color("button", Color::BLACK); * ``` * - * This will change the pressed font color of the component `button` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the pressed font color of the component `button` to black. */ void set_component_pressed_font_color(const char *component, Color color) override; + /** * Set the coordinates of a component on screen. * @param component The component name. @@ -304,7 +416,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * This will move the position of the component `pic` to the x coordinate `55` and y coordinate `100`. */ - void set_component_coordinates(const char *component, int x, int y); + void set_component_coordinates(const char *component, uint16_t x, uint16_t y); + /** * Set the font id for a component. * @param component The component name. @@ -318,13 +431,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Changes the font of the component named `textveiw`. Font IDs are set in the Nextion Editor. */ void set_component_font(const char *component, uint8_t font_id) override; -#ifdef USE_TIME + /** * Send the current time to the nextion display. * @param time The time instance to send (get this with id(my_time).now() ). */ - void set_nextion_rtc_time(time::ESPTime time); -#endif + void set_nextion_rtc_time(ESPTime time); /** * Show the page with a given name. @@ -338,6 +450,20 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Switches to the page named `main`. Pages are named in the Nextion Editor. */ void goto_page(const char *page); + + /** + * Show the page with a given id. + * @param page The id of the page. + * + * Example: + * ```cpp + * it.goto_page(2); + * ``` + * + * Switches to the page named `main`. Pages are named in the Nextion Editor. + */ + void goto_page(uint8_t page); + /** * Hide a component. * @param component The component name. @@ -350,6 +476,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Hides the component named `button`. */ void hide_component(const char *component) override; + /** * Show a component. * @param component The component name. @@ -362,6 +489,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Shows the component named `button`. */ void show_component(const char *component) override; + /** * Enable touch for a component. * @param component The component name. @@ -374,6 +502,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Enables touch for component named `button`. */ void enable_component_touch(const char *component); + /** * Disable touch for a component. * @param component The component name. @@ -386,14 +515,17 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Disables touch for component named `button`. */ void disable_component_touch(const char *component); + /** * Add waveform data to a waveform component * @param component_id The integer component id. * @param channel_number The channel number to write to. * @param value The value to write. */ - void add_waveform_data(int component_id, uint8_t channel_number, uint8_t value); - void open_waveform_channel(int component_id, uint8_t channel_number, uint8_t value); + void add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value); + + void open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value); + /** * Display a picture at coordinates. * @param picture_id The picture id. @@ -407,7 +539,28 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Displays the picture who has the id `2` at the x coordinates `15` and y coordinates `25`. */ - void display_picture(int picture_id, int x_start, int y_start); + void display_picture(uint16_t picture_id, uint16_t x_start, uint16_t y_start); + + /** + * Fill a rectangle with a color. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width to draw. + * @param height The height to draw. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * fill_area(50, 50, 100, 100, 63488); + * ``` + * + * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with + * the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint16_t color); + /** * Fill a rectangle with a color. * @param x1 The starting x coordinate. @@ -422,10 +575,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * ``` * * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with - * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to - * convert color codes to Nextion HMI colors + * the red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ - void fill_area(int x1, int y1, int width, int height, const char *color); + void fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color); + /** * Fill a rectangle with a color. * @param x1 The starting x coordinate. @@ -436,14 +590,35 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * fill_area(50, 50, 100, 100, color); + * auto blue = Color(0, 0, 255); + * fill_area(50, 50, 100, 100, blue); * ``` * * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with - * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to - * convert color codes to Nextion HMI colors + * blue color. */ - void fill_area(int x1, int y1, int width, int height, Color color); + void fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, Color color); + + /** + * Draw a line on the screen. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param x2 The ending x coordinate. + * @param y2 The ending y coordinate. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.line(50, 50, 75, 75, 63488); + * ``` + * + * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate + * `75` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color); + /** * Draw a line on the screen. * @param x1 The starting x coordinate. @@ -454,15 +629,15 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.line(50, 50, 75, 75, "17013"); + * it.line(50, 50, 75, 75, "BLUE"); * ``` * * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate - * `75` with the color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * `75` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ - void line(int x1, int y1, int x2, int y2, const char *color); + void line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, const char *color); + /** * Draw a line on the screen. * @param x1 The starting x coordinate. @@ -473,15 +648,35 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.line(50, 50, 75, 75, "17013"); + * auto blue = Color(0, 0, 255); + * it.line(50, 50, 75, 75, blue); * ``` * * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate - * `75` with the color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * `75` with blue color. */ - void line(int x1, int y1, int x2, int y2, Color color); + void line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, Color color); + + /** + * Draw a rectangle outline. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.rectangle(25, 35, 40, 50, 63488); + * ``` + * + * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a + * length of `50` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void rectangle(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint16_t color); + /** * Draw a rectangle outline. * @param x1 The starting x coordinate. @@ -492,15 +687,15 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.rectangle(25, 35, 40, 50, "17013"); + * it.rectangle(25, 35, 40, 50, "BLUE"); * ``` * * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a - * length of `50` with color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * length of `50` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ - void rectangle(int x1, int y1, int width, int height, const char *color); + void rectangle(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color); + /** * Draw a rectangle outline. * @param x1 The starting x coordinate. @@ -511,23 +706,36 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.rectangle(25, 35, 40, 50, "17013"); + * auto blue = Color(0, 0, 255); + * it.rectangle(25, 35, 40, 50, blue); * ``` * * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a - * length of `50` with color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * length of `50` with blue color. */ - void rectangle(int x1, int y1, int width, int height, Color color); + void rectangle(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, Color color); + + /** + * Draw a circle outline + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (number). + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void circle(uint16_t center_x, uint16_t center_y, uint16_t radius, uint16_t color); + /** * Draw a circle outline * @param center_x The center x coordinate. * @param center_y The center y coordinate. * @param radius The circle radius. * @param color The color to draw with (as a string). + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ - void circle(int center_x, int center_y, int radius, const char *color); + void circle(uint16_t center_x, uint16_t center_y, uint16_t radius, const char *color); + /** * Draw a circle outline * @param center_x The center x coordinate. @@ -535,7 +743,26 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @param radius The circle radius. * @param color The color to draw with (as Color). */ - void circle(int center_x, int center_y, int radius, Color color); + void circle(uint16_t center_x, uint16_t center_y, uint16_t radius, Color color); + + /** + * Draw a filled circled. + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.filled_cricle(25, 25, 10, 63488); + * ``` + * + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radius, uint16_t color); + /** * Draw a filled circled. * @param center_x The center x coordinate. @@ -545,14 +772,14 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.filled_cricle(25, 25, 10, "17013"); + * it.filled_cricle(25, 25, 10, "BLUE"); * ``` * - * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ - void filled_circle(int center_x, int center_y, int radius, const char *color); + void filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radius, const char *color); + /** * Draw a filled circled. * @param center_x The center x coordinate. @@ -562,14 +789,59 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.filled_cricle(25, 25, 10, color); + * auto blue = Color(0, 0, 255); + * it.filled_cricle(25, 25, 10, blue); * ``` * - * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with blue color. */ - void filled_circle(int center_x, int center_y, int radius, Color color); + void filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radius, Color color); + + /** + * Draws a QR code in the screen + * @param x1 The top left x coordinate to start the QR code. + * @param y1 The top left y coordinate to start the QR code. + * @param content The content of the QR code (as a plain text - Nextion will generate the QR code). + * @param size The size (in pixels) for the QR code. Defaults to 200px. + * @param background_color The background color to draw with (as rgb565 integer). Defaults to 65535 (white). + * @param foreground_color The foreground color to draw with (as rgb565 integer). Defaults to 0 (black). + * @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo). + * @param border_width The border width (in pixels) for the QR code. Defaults to 8px. + * + * Example: + * ```cpp + * it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;"); + * ``` + * + * 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); + + /** + * Draws a QR code in the screen + * @param x1 The top left x coordinate to start the QR code. + * @param y1 The top left y coordinate to start the QR code. + * @param content The content of the QR code (as a plain text - Nextion will generate the QR code). + * @param size The size (in pixels) for the QR code. Defaults to 200px. + * @param background_color The background color to draw with (as Color). Defaults to 65535 (white). + * @param foreground_color The foreground color to draw with (as Color). Defaults to 0 (black). + * @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo). + * @param border_width The border width (in pixels) for the QR code. Defaults to 8px. + * + * Example: + * ```cpp + * auto blue = Color(0, 0, 255); + * auto red = Color(255, 0, 0); + * it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;", 150, blue, red); + * ``` + * + * Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25) with size of 150px in + * red on a blue background. + */ + 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); /** Set the brightness of the backlight. * @@ -583,6 +855,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Changes the brightness of the display to 30%. */ void set_backlight_brightness(float brightness); + /** * Set the touch sleep timeout of the display. * @param timeout Timeout in seconds. @@ -596,6 +869,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * `thup`. */ void set_touch_sleep_timeout(uint16_t timeout); + /** * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. * @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to @@ -609,6 +883,21 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * The display will wake up to page 2. */ void set_wake_up_page(uint8_t page_id = 255); + + /** + * Sets which page Nextion loads when connecting to ESPHome. + * @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * wakes up to current page. + * + * Example: + * ```cpp + * it.set_start_up_page(2); + * ``` + * + * The display will go to page 2 when it establishes a connection to ESPHome. + */ + void set_start_up_page(uint8_t page_id = 255); + /** * Sets if Nextion should auto-wake from sleep when touch press occurs. * @param auto_wake True or false. When auto_wake is true and Nextion is in sleep mode, @@ -622,12 +911,129 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * The display will wake up by touch. */ void set_auto_wake_on_touch(bool auto_wake); + + /** + * Sets if Nextion should exit the active reparse mode before the "connect" command is sent + * @param exit_reparse True or false. When exit_reparse is true, the exit reparse command + * will be sent before requesting the connection from Nextion. + * + * Example: + * ```cpp + * it.set_exit_reparse_on_start(true); + * ``` + * + * The display will be requested to leave active reparse mode before setup. + */ + void set_exit_reparse_on_start(bool exit_reparse); + + /** + * Sets whether the Nextion display should skip the connection handshake process. + * @param skip_handshake True or false. When skip_connection_handshake is true, + * the connection will be established without performing the handshake. + * This can be useful when using Nextion Simulator. + * + * Example: + * ```cpp + * it.set_skip_connection_handshake(true); + * ``` + * + * When set to true, the display will be marked as connected without performing a handshake. + */ + void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; } + /** * Sets Nextion mode between sleep and awake * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. */ void sleep(bool sleep); + /** + * @brief Sets the Nextion display's protocol reparse mode. + * + * This function toggles the Nextion display's protocol reparse mode between active and passive. + * In active mode, the display actively parses incoming data. + * In passive mode, it does not parse data unless specifically instructed to do so. + * This is useful for managing how the Nextion display interprets incoming commands, + * especially during initialization or in scenarios where precise control over command processing is needed. + * + * @param active_mode A boolean value indicating the desired reparse mode. + * - true to set the display to active protocol reparse mode, where it actively parses incoming commands. + * - false to set the display to passive protocol reparse mode, where command parsing is done only on explicit + * instruction. + * + * @return bool Returns true if all commands were sent successfully to the Nextion display, indicating that the mode + * was set as expected. Returns false if any of the commands failed to send, indicating that the desired reparse mode + * may not be correctly set. + */ + bool set_protocol_reparse_mode(bool active_mode); + + // ======== Nextion Intelligent Series ======== + + /** + * Set the video id of a component. + * @param component The component name. + * @param vid_id The video ID. + * + * Example: + * ```cpp + * it.set_component_vid("textview", 1); + * ``` + * + * This will change the video id of the component `textview`. + * + * Note: Requires Nextion Intelligent series display. + */ + void set_component_vid(const char *component, uint8_t vid_id); + + /** + * Set the drag availability of a component. + * @param component The component name. + * @param drag False: Drag not available, True: Drag available. + * + * Example: + * ```cpp + * it.set_component_drag("textview", true); + * ``` + * + * This will enable drag to the component `textview`. + * + * Note: Requires Nextion Intelligent series display. + */ + void set_component_drag(const char *component, bool drag); + + /** + * Set the opaqueness (fading) of a component. + * @param component The component name. + * @param aph An integer between 0 and 127 related to the opaqueness/fading level. + * + * Example: + * ```cpp + * it.set_component_aph("textview", 64); + * ``` + * + * This will set the opaqueness level of the component `textview` to 64. + * + * Note: Requires Nextion Intelligent series display. + */ + void set_component_aph(const char *component, uint8_t aph); + + /** + * Set the position of a component. + * @param component The component name. + * @param x The new X (horizontal) coordinate for the component. + * @param y The new Y (vertical) coordinate for the component. + * + * Example: + * ```cpp + * it.set_component_aph("textview", 64, 35); + * ``` + * + * This will move the component `textview` to the column 64 of row 35 of the display. + * + * Note: Requires Nextion Intelligent series display. + */ + void set_component_position(const char *component, uint32_t x, uint32_t y); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void register_touch_component(NextionComponentBase *obj) { this->touch_.push_back(obj); } @@ -646,6 +1052,13 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe // This function has been deprecated void set_wait_for_ack(bool wait_for_ack); + /** + * Manually send a raw command to the display. + * @param command The pcommand, like "page 0" + * @return Whether the send was successful. + */ + bool send_command(const char *command); + /** * Manually send a raw formatted command to the display. * @param format The printf-style command format, like "vis %s,0" @@ -656,16 +1069,34 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #ifdef USE_NEXTION_TFT_UPLOAD /** - * Set the tft file URL. https seems problamtic with arduino.. + * Set the tft file URL. https seems problematic with Arduino.. */ void set_tft_url(const std::string &tft_url) { this->tft_url_ = tft_url; } -#endif - /** - * Upload the tft file and softreset the Nextion + * @brief Uploads the TFT file to the Nextion display. + * + * This function initiates the upload of a TFT file to the Nextion display. Users can specify a target baud rate for + * the transfer. If the provided baud rate is not supported by Nextion, the function defaults to using the current + * baud rate set for the display. If no baud rate is specified (or if 0 is passed), the current baud rate is used. + * + * Supported baud rates are: 2400, 4800, 9600, 19200, 31250, 38400, 57600, 115200, 230400, 250000, 256000, 512000 + * and 921600. Selecting a baud rate supported by both the Nextion display and the host hardware is essential for + * ensuring a successful upload process. + * + * @param baud_rate The desired baud rate for the TFT file transfer, specified as an unsigned 32-bit integer. + * If the specified baud rate is not supported, or if 0 is passed, the function will use the current baud rate. + * The default value is 0, which implies using the current baud rate. + * @param exit_reparse If true, the function exits reparse mode before uploading the TFT file. This parameter + * defaults to true, ensuring that the display is ready to receive and apply the new TFT file without needing + * to manually reset or reconfigure. Exiting reparse mode is recommended for most upload scenarios to ensure + * the display properly processes the uploaded file command. + * @return bool True: Transfer completed successfuly, False: Transfer failed. */ - void upload_tft(); + bool upload_tft(uint32_t baud_rate = 0, bool exit_reparse = true); + +#endif // USE_NEXTION_TFT_UPLOAD + void dump_config() override; /** @@ -697,6 +1128,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void add_new_page_callback(std::function &&callback); + /** Add a callback to be notified when Nextion has a touch event. + * + * @param callback The void() callback. + */ + void add_touch_event_callback(std::function &&callback); + void update_all_components(); /** @@ -715,9 +1152,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state); void set_nextion_text_state(const std::string &name, const std::string &state); - void add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) override; + void add_no_result_to_queue_with_set(NextionComponentBase *component, int32_t state_value) override; void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, - int state_value) override; + int32_t state_value) override; void add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) override; void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, @@ -733,19 +1170,61 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe this->touch_sleep_timeout_ = touch_sleep_timeout; } void set_wake_up_page_internal(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; } + void set_start_up_page_internal(uint8_t start_up_page) { this->start_up_page_ = start_up_page; } void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } + void set_exit_reparse_on_start_internal(bool exit_reparse_on_start) { + this->exit_reparse_on_start_ = exit_reparse_on_start; + } + + /** + * @brief Retrieves the number of commands pending in the Nextion command queue. + * + * This function returns the current count of commands that have been queued but not yet processed + * for the Nextion display. The Nextion command queue is used to store commands that are sent to + * the Nextion display for various operations like updating the display, changing interface elements, + * or other interactive features. A larger queue size might indicate a higher processing time or potential + * delays in command execution. This function is useful for monitoring the command flow and managing + * the execution efficiency of the Nextion display interface. + * + * @return size_t The number of commands currently in the Nextion queue. This count includes all commands + * that have been added to the queue and are awaiting processing. + */ + size_t queue_size() { return this->nextion_queue_.size(); } + + /** + * @brief Check if the TFT update process is currently running. + * + * This method provides a way to determine if the Nextion display is in the + * process of updating its TFT firmware. When a TFT update is in progress, + * certain operations or commands may be restricted or could interfere with the + * update process. By checking the state of the update process, the system can + * make informed decisions about performing actions that involve communication + * with the Nextion display. + * + * @return true if the TFT update process is active, indicating that the Nextion + * display is currently updating its firmware. This implies that caution + * should be taken with commands sent to the display to avoid interrupting + * the update process. + * @return false if the TFT update process is not active, indicating that the Nextion + * display is not currently updating its firmware and is in a normal operational + * state, ready to receive and process commands as usual. + */ + bool is_updating() override; protected: std::deque nextion_queue_; + std::deque waveform_queue_; uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); void all_components_send_state_(bool force_update = false); uint64_t comok_sent_ = 0; bool remove_from_q_(bool report_empty = true); + /** * @brief * Sends commands ignoring of the Nextion has been setup. */ bool ignore_is_setup_ = false; + bool nextion_reports_is_setup_ = false; uint8_t nextion_event_; @@ -753,8 +1232,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void process_serial_(); bool is_updating_ = false; uint32_t touch_sleep_timeout_ = 0; - int wake_up_page_ = -1; + int16_t wake_up_page_ = -1; + int16_t start_up_page_ = -1; bool auto_wake_on_touch_ = true; + bool exit_reparse_on_start_ = false; + bool skip_connection_handshake_ = false; /** * Manually send a raw command to the display and don't wait for an acknowledgement packet. @@ -770,42 +1252,60 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe __attribute__((format(printf, 3, 4))); void add_no_result_to_queue_with_set_internal_(const std::string &variable_name, - const std::string &variable_name_to_send, int state_value, + const std::string &variable_name_to_send, int32_t state_value, bool is_sleep_safe = false); void add_no_result_to_queue_with_set_internal_(const std::string &variable_name, const std::string &variable_name_to_send, const std::string &state_value, bool is_sleep_safe = false); + void check_pending_waveform_(); + #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP8266 WiFiClient *wifi_client_{nullptr}; BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr}; WiFiClient *get_wifi_client_(); -#endif +#endif // USE_ESP8266 + std::string tft_url_; + uint32_t content_length_ = 0; + int tft_size_ = 0; + 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 int contentLength Total size of the file - * @param uint32_t chunk_size - * @return true if success, false for failure. + * @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 content_length_ = 0; - int tft_size_ = 0; - int upload_by_chunks_(HTTPClient *http, int range_start); - - bool upload_with_range_(uint32_t range_start, uint32_t range_end); + int upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start); +#elif defined(USE_ESP_IDF) + /** + * will request 4096 bytes chunks from the web server + * and send each to Nextion + * @param esp_http_client_handle_t 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_(esp_http_client_handle_t http_client, uint32_t &range_start); +#endif // USE_ARDUINO vs USE_ESP_IDF /** - * start update tft file to nextion. - * - * @param const uint8_t *file_buf - * @param size_t buf_size - * @return true if success, false for failure. + * Ends the upload process, restart Nextion and, if successful, + * restarts ESP + * @param bool url successful True: Transfer completed successfuly, False: Transfer failed. + * @return bool True: Transfer completed successfuly, False: Transfer failed. */ - bool upload_from_buffer_(const uint8_t *file_buf, size_t buf_size); - void upload_end_(); + 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 @@ -822,6 +1322,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager sleep_callback_{}; CallbackManager wake_callback_{}; CallbackManager page_callback_{}; + CallbackManager touch_callback_{}; optional writer_; float brightness_{1.0}; @@ -833,22 +1334,15 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void remove_front_no_sensors_(); -#ifdef USE_NEXTION_TFT_UPLOAD - std::string tft_url_; - uint8_t *transfer_buffer_{nullptr}; - size_t transfer_buffer_size_; - bool upload_first_chunk_sent_ = false; -#endif - #ifdef NEXTION_PROTOCOL_LOG void print_queue_members_(); -#endif +#endif // NEXTION_PROTOCOL_LOG void reset_(bool reset_nextion = true); std::string command_data_; bool is_connected_ = false; - uint32_t startup_override_ms_ = 8000; - uint32_t max_q_age_ms_ = 8000; + const uint16_t startup_override_ms_ = 8000; + const uint16_t max_q_age_ms_ = 8000; uint32_t started_ms_ = 0; bool sent_setup_commands_ = false; }; diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index a24fd74060..b88dd399f8 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -24,9 +24,9 @@ class NextionBase; class NextionBase { public: - virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) = 0; + virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, int32_t state_value) = 0; virtual void add_no_result_to_queue_with_set(const std::string &variable_name, - const std::string &variable_name_to_send, int state_value) = 0; + const std::string &variable_name_to_send, int32_t state_value) = 0; virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) = 0; virtual void add_no_result_to_queue_with_set(const std::string &variable_name, @@ -39,6 +39,8 @@ class NextionBase { virtual void set_component_background_color(const char *component, Color color) = 0; virtual void set_component_pressed_background_color(const char *component, Color color) = 0; + virtual void set_component_foreground_color(const char *component, Color color) = 0; + virtual void set_component_pressed_foreground_color(const char *component, Color color) = 0; virtual void set_component_font_color(const char *component, Color color) = 0; virtual void set_component_pressed_font_color(const char *component, Color color) = 0; virtual void set_component_font(const char *component, uint8_t font_id) = 0; @@ -46,12 +48,16 @@ class NextionBase { virtual void show_component(const char *component) = 0; virtual void hide_component(const char *component) = 0; + virtual bool is_updating() { return false; } + bool is_sleeping() { return this->is_sleeping_; } bool is_setup() { return this->is_setup_; } + bool is_detected() { return this->is_detected_; } protected: bool is_setup_ = false; bool is_sleeping_ = false; + bool is_detected_ = false; }; } // namespace nextion diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 308e02bce8..398e9dd502 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -1,6 +1,7 @@ #include "nextion.h" #include "esphome/core/util.h" #include "esphome/core/log.h" +#include namespace esphome { namespace nextion { @@ -10,15 +11,13 @@ static const char *const TAG = "nextion"; void Nextion::soft_reset() { this->send_command_("rest"); } void Nextion::set_wake_up_page(uint8_t page_id) { - if (page_id > 255) { - ESP_LOGD(TAG, "Wake up page of bounds, range 0-255"); - return; - } this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", page_id, true); } +void Nextion::set_start_up_page(uint8_t page_id) { this->start_up_page_ = page_id; } + void Nextion::set_touch_sleep_timeout(uint16_t timeout) { - if (timeout < 3 || timeout > 65535) { + if (timeout < 3) { ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535"); return; } @@ -36,9 +35,30 @@ void Nextion::sleep(bool sleep) { } // End sleep safe commands -// Set Colors -void Nextion::set_component_background_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, color); +// Protocol reparse mode +bool Nextion::set_protocol_reparse_mode(bool active_mode) { + ESP_LOGV(TAG, "Set Nextion protocol reparse mode: %s", YESNO(active_mode)); + this->ignore_is_setup_ = true; // if not in reparse mode setup will fail, so it should be ignored + bool all_commands_sent = true; + if (active_mode) { // Sets active protocol reparse mode + all_commands_sent &= this->send_command_("recmod=1"); + } else { // Sets passive protocol reparse mode + all_commands_sent &= + this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); // To exit active reparse mode this sequence must be sent + all_commands_sent &= this->send_command_("recmod=0"); // Sending recmode=0 twice is recommended + all_commands_sent &= this->send_command_("recmod=0"); + } + if (!this->nextion_reports_is_setup_) { // No need to connect if is already setup + all_commands_sent &= this->send_command_("connect"); + } + this->ignore_is_setup_ = false; + return all_commands_sent; +} +void Nextion::set_exit_reparse_on_start(bool exit_reparse) { this->exit_reparse_on_start_ = exit_reparse; } + +// Set Colors - Background +void Nextion::set_component_background_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu16, component, color); } void Nextion::set_component_background_color(const char *component, const char *color) { @@ -50,8 +70,10 @@ void Nextion::set_component_background_color(const char *component, Color color) display::ColorUtil::color_to_565(color)); } -void Nextion::set_component_pressed_background_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, color); +// Set Colors - Background (pressed) +void Nextion::set_component_pressed_background_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu16, component, + color); } void Nextion::set_component_pressed_background_color(const char *component, const char *color) { @@ -63,16 +85,38 @@ void Nextion::set_component_pressed_background_color(const char *component, Colo display::ColorUtil::color_to_565(color)); } -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=%d", component, pic_id); +// Set Colors - Foreground +void Nextion::set_component_foreground_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%" PRIu16, component, color); } -void Nextion::set_component_picc(const char *component, uint8_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.picc=%d", component, pic_id); +void Nextion::set_component_foreground_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%s", component, color); } -void Nextion::set_component_font_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, color); +void Nextion::set_component_foreground_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%d", component, + display::ColorUtil::color_to_565(color)); +} + +// Set Colors - Foreground (pressed) +void Nextion::set_component_pressed_foreground_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%" PRIu16, component, + color); +} + +void Nextion::set_component_pressed_foreground_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", " %s.pco2=%s", component, color); +} + +void Nextion::set_component_pressed_foreground_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%d", component, + display::ColorUtil::color_to_565(color)); +} + +// Set Colors - Font +void Nextion::set_component_font_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_font_color(const char *component, const char *color) { @@ -84,8 +128,9 @@ void Nextion::set_component_font_color(const char *component, Color color) { display::ColorUtil::color_to_565(color)); } -void Nextion::set_component_pressed_font_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, color); +// Set Colors - Font (pressed) +void Nextion::set_component_pressed_font_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { @@ -97,6 +142,33 @@ void Nextion::set_component_pressed_font_color(const char *component, Color colo display::ColorUtil::color_to_565(color)); } +// 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_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); +} + +// Set video +void Nextion::set_component_vid(const char *component, uint8_t vid_id) { + this->add_no_result_to_queue_with_printf_("set_component_vid", "%s.vid=%" PRIu8, component, vid_id); +} + +void Nextion::set_component_drag(const char *component, bool drag) { + this->add_no_result_to_queue_with_printf_("set_component_drag", "%s.drag=%i", component, drag ? 1 : 0); +} + +void Nextion::set_component_aph(const char *component, uint8_t aph) { + this->add_no_result_to_queue_with_printf_("set_component_aph", "%s.aph=%" PRIu8, component, aph); +} + +void Nextion::set_component_position(const char *component, uint32_t x, uint32_t y) { + this->add_no_result_to_queue_with_printf_("set_component_position_x", "%s.x=%" PRIu32, component, x); + this->add_no_result_to_queue_with_printf_("set_component_position_y", "%s.y=%" PRIu32, component, y); +} + void Nextion::set_component_text_printf(const char *component, const char *format, ...) { va_list arg; va_start(arg, format); @@ -109,6 +181,7 @@ void Nextion::set_component_text_printf(const char *component, const char *forma // General Nextion void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %s", page); } +void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %i", page); } void Nextion::set_backlight_brightness(float brightness) { if (brightness < 0 || brightness > 1.0) { @@ -124,7 +197,7 @@ void Nextion::set_auto_wake_on_touch(bool auto_wake) { // General Component void Nextion::set_component_font(const char *component, uint8_t font_id) { - this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%d", component, font_id); + this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id); } void Nextion::hide_component(const char *component) { @@ -143,84 +216,134 @@ 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, const char *picture) { - this->add_no_result_to_queue_with_printf_("set_component_picture", "%s.val=%s", component, picture); +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); } -void Nextion::set_component_value(const char *component, int value) { - this->add_no_result_to_queue_with_printf_("set_component_value", "%s.val=%d", component, value); +void Nextion::set_component_value(const char *component, int32_t value) { + this->add_no_result_to_queue_with_printf_("set_component_value", "%s.val=%" PRId32, component, value); } -void Nextion::add_waveform_data(int component_id, uint8_t channel_number, uint8_t value) { - this->add_no_result_to_queue_with_printf_("add_waveform_data", "add %d,%u,%u", component_id, channel_number, value); +void Nextion::add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value) { + this->add_no_result_to_queue_with_printf_("add_waveform_data", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, + channel_number, value); } -void Nextion::open_waveform_channel(int component_id, uint8_t channel_number, uint8_t value) { - this->add_no_result_to_queue_with_printf_("open_waveform_channel", "addt %d,%u,%u", component_id, channel_number, - value); +void Nextion::open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value) { + this->add_no_result_to_queue_with_printf_("open_waveform_channel", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, + channel_number, value); } -void Nextion::set_component_coordinates(const char *component, int x, int y) { - this->add_no_result_to_queue_with_printf_("set_component_coordinates command 1", "%s.xcen=%d", component, x); - this->add_no_result_to_queue_with_printf_("set_component_coordinates command 2", "%s.ycen=%d", component, y); +void Nextion::set_component_coordinates(const char *component, uint16_t x, uint16_t y) { + this->add_no_result_to_queue_with_printf_("set_component_coordinates command 1", "%s.xcen=%" PRIu16, component, x); + this->add_no_result_to_queue_with_printf_("set_component_coordinates command 2", "%s.ycen=%" PRIu16, component, y); } // Drawing -void Nextion::display_picture(int picture_id, int x_start, int y_start) { - this->add_no_result_to_queue_with_printf_("display_picture", "pic %d, %d, %d", x_start, y_start, picture_id); +void Nextion::display_picture(uint16_t picture_id, uint16_t x_start, uint16_t y_start) { + this->add_no_result_to_queue_with_printf_("display_picture", "pic %" PRIu16 ", %" PRIu16 ", %" PRIu16, x_start, + y_start, picture_id); } -void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { - this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%s", x1, y1, width, height, color); +void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint16_t color) { + this->add_no_result_to_queue_with_printf_( + "fill_area", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, width, height, color); } -void Nextion::fill_area(int x1, int y1, int width, int height, Color color) { - this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%d", x1, y1, width, height, +void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color) { + this->add_no_result_to_queue_with_printf_("fill_area", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, + y1, width, height, color); +} + +void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, Color color) { + this->add_no_result_to_queue_with_printf_("fill_area", + "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, + width, height, display::ColorUtil::color_to_565(color)); +} + +void Nextion::line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { + this->add_no_result_to_queue_with_printf_("line", "line %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, + y1, x2, y2, color); +} + +void Nextion::line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, const char *color) { + this->add_no_result_to_queue_with_printf_("line", "line %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, y1, + x2, y2, color); +} + +void Nextion::line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, Color color) { + this->add_no_result_to_queue_with_printf_("line", "line %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, + y1, x2, y2, display::ColorUtil::color_to_565(color)); +} + +void Nextion::rectangle(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint16_t color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, + y1, static_cast(x1 + width), static_cast(y1 + height), + color); +} + +void Nextion::rectangle(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, y1, + static_cast(x1 + width), static_cast(y1 + height), + color); +} + +void Nextion::rectangle(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, Color color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, + y1, static_cast(x1 + width), static_cast(y1 + height), display::ColorUtil::color_to_565(color)); } -void Nextion::line(int x1, int y1, int x2, int y2, const char *color) { - this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%s", x1, y1, x2, y2, color); +void Nextion::circle(uint16_t center_x, uint16_t center_y, uint16_t radius, uint16_t color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, center_x, + center_y, radius, color); } -void Nextion::line(int x1, int y1, int x2, int y2, Color color) { - this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%d", x1, y1, x2, y2, - display::ColorUtil::color_to_565(color)); +void Nextion::circle(uint16_t center_x, uint16_t center_y, uint16_t radius, const char *color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", center_x, center_y, + radius, color); } -void Nextion::rectangle(int x1, int y1, int width, int height, const char *color) { - this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%s", x1, y1, x1 + width, y1 + height, color); +void Nextion::circle(uint16_t center_x, uint16_t center_y, uint16_t radius, Color color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, center_x, + center_y, radius, display::ColorUtil::color_to_565(color)); } -void Nextion::rectangle(int x1, int y1, int width, int height, Color color) { - this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%d", x1, y1, x1 + width, y1 + height, - display::ColorUtil::color_to_565(color)); +void Nextion::filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radius, uint16_t color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, center_x, + center_y, radius, color); } -void Nextion::circle(int center_x, int center_y, int radius, const char *color) { - this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%s", center_x, center_y, radius, color); +void Nextion::filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radius, const char *color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", center_x, center_y, + radius, color); } -void Nextion::circle(int center_x, int center_y, int radius, Color color) { - this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%d", center_x, center_y, radius, - display::ColorUtil::color_to_565(color)); +void Nextion::filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radius, Color color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, center_x, + center_y, radius, display::ColorUtil::color_to_565(color)); } -void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { - this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%s", center_x, center_y, radius, color); +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) { + 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::filled_circle(int center_x, int center_y, int radius, Color color) { - this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%d", center_x, center_y, radius, - display::ColorUtil::color_to_565(color)); +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) { + 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), + logo_pic, border_width, content); } -#ifdef USE_TIME -void Nextion::set_nextion_rtc_time(time::ESPTime time) { +void Nextion::set_nextion_rtc_time(ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year); this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month); this->add_no_result_to_queue_with_printf_("rtc2", "rtc2=%u", time.day_of_month); @@ -228,7 +351,6 @@ void Nextion::set_nextion_rtc_time(time::ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc4", "rtc4=%u", time.minute); this->add_no_result_to_queue_with_printf_("rtc5", "rtc5=%u", time.second); } -#endif } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index bbb2cf6cb2..cfb4e3600c 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -99,11 +99,11 @@ void NextionComponent::update_component_settings(bool force_update) { this->bco2_needs_update_ = false; } if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) { - this->nextion_->set_component_font_color(this->variable_name_.c_str(), this->pco_); + this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_); this->pco_needs_update_ = false; } if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) { - this->nextion_->set_component_pressed_font_color(this->variable_name_.c_str(), this->pco2_); + this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_); this->pco2_needs_update_ = false; } diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index e0ef8f93bc..42e1b00998 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -69,6 +69,13 @@ class NextionComponentBase { std::vector get_wave_buffer() { return this->wave_buffer_; } size_t get_wave_buffer_size() { return this->wave_buffer_.size(); } + void clear_wave_buffer(size_t buffer_sent) { + if (this->wave_buffer_.size() <= buffer_sent) { + this->wave_buffer_.clear(); + } else { + this->wave_buffer_.erase(this->wave_buffer_.begin(), this->wave_buffer_.begin() + buffer_sent); + } + } std::string get_variable_name() { return this->variable_name_; } std::string get_variable_name_to_send() { return this->variable_name_to_send_; } diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp deleted file mode 100644 index 9e6884398c..0000000000 --- a/esphome/components/nextion/nextion_upload.cpp +++ /dev/null @@ -1,339 +0,0 @@ -#include "nextion.h" - -#ifdef USE_NEXTION_TFT_UPLOAD - -#include "esphome/core/application.h" -#include "esphome/core/defines.h" -#include "esphome/core/util.h" -#include "esphome/core/log.h" -#include "esphome/components/network/util.h" - -#ifdef USE_ESP32 -#include -#endif - -namespace esphome { -namespace nextion { -static const char *const TAG = "nextion_upload"; - -// Followed guide -// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 - -int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { - int range_end = 0; - - if (range_start == 0 && this->transfer_buffer_size_ > 16384) { // Start small at the first run in case of a big skip - range_end = 16384 - 1; - } else { - range_end = range_start + this->transfer_buffer_size_ - 1; - } - - if (range_end > this->tft_size_) - range_end = this->tft_size_; - -#ifdef USE_ESP8266 -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) - http->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); -#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) - http->setFollowRedirects(true); -#endif -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) - http->setRedirectLimit(3); -#endif -#endif - - char range_header[64]; - sprintf(range_header, "bytes=%d-%d", range_start, range_end); - - ESP_LOGD(TAG, "Requesting range: %s", range_header); - - int tries = 1; - int code = 0; - bool begin_status = false; - while (tries <= 5) { -#ifdef USE_ESP32 - begin_status = http->begin(this->tft_url_.c_str()); -#endif -#ifdef USE_ESP8266 - begin_status = http->begin(*this->get_wifi_client_(), this->tft_url_.c_str()); -#endif - - ++tries; - if (!begin_status) { - ESP_LOGD(TAG, "upload_by_chunks_: connection failed"); - continue; - } - - http->addHeader("Range", range_header); - - code = http->GET(); - if (code == 200 || code == 206) { - break; - } - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retries(%d/5)", this->tft_url_.c_str(), - HTTPClient::errorToString(code).c_str(), tries); - http->end(); - App.feed_wdt(); - delay(500); // NOLINT - } - - if (tries > 5) { - return -1; - } - - std::string recv_string; - size_t size = 0; - int sent = 0; - int range = range_end - range_start; - - while (sent < range) { - size = http->getStreamPtr()->available(); - if (!size) { - App.feed_wdt(); - delay(0); - continue; - } - int c = http->getStreamPtr()->readBytes( - &this->transfer_buffer_[sent], ((size > this->transfer_buffer_size_) ? this->transfer_buffer_size_ : size)); - sent += c; - } - http->end(); - ESP_LOGN(TAG, "this->content_length_ %d sent %d", this->content_length_, sent); - for (int i = 0; i < range; i += 4096) { - this->write_array(&this->transfer_buffer_[i], 4096); - this->content_length_ -= 4096; - ESP_LOGN(TAG, "this->content_length_ %d range %d range_end %d range_start %d", this->content_length_, range, - range_end, range_start); - - if (!this->upload_first_chunk_sent_) { - this->upload_first_chunk_sent_ = true; - delay(500); // NOLINT - App.feed_wdt(); - } - - this->recv_ret_string_(recv_string, 2048, true); - if (recv_string[0] == 0x08) { - uint32_t result = 0; - for (int i = 0; i < 4; ++i) { - result += static_cast(recv_string[i + 1]) << (8 * i); - } - if (result > 0) { - ESP_LOGD(TAG, "Nextion reported new range %d", result); - this->content_length_ = this->tft_size_ - result; - return result; - } - } - recv_string.clear(); - } - return range_end + 1; -} - -void Nextion::upload_tft() { - if (this->is_updating_) { - ESP_LOGD(TAG, "Currently updating"); - return; - } - - if (!network::is_connected()) { - ESP_LOGD(TAG, "network is not connected"); - return; - } - - this->is_updating_ = true; - - HTTPClient http; - http.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along - bool begin_status = false; -#ifdef USE_ESP32 - begin_status = http.begin(this->tft_url_.c_str()); -#endif -#ifdef USE_ESP8266 -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) - http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); -#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) - http.setFollowRedirects(true); -#endif -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) - http.setRedirectLimit(3); -#endif - begin_status = http.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); -#endif - - if (!begin_status) { - this->is_updating_ = false; - ESP_LOGD(TAG, "connection failed"); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); - return; - } else { - ESP_LOGD(TAG, "Connected"); - } - - http.addHeader("Range", "bytes=0-255"); - const char *header_names[] = {"Content-Range"}; - http.collectHeaders(header_names, 1); - ESP_LOGD(TAG, "Requesting URL: %s", this->tft_url_.c_str()); - - http.setReuse(true); - // try up to 5 times. DNS sometimes needs a second try or so - int tries = 1; - int code = http.GET(); - delay(100); // NOLINT - - App.feed_wdt(); - while (code != 200 && code != 206 && tries <= 5) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", this->tft_url_.c_str(), - HTTPClient::errorToString(code).c_str(), tries); - - delay(250); // NOLINT - App.feed_wdt(); - code = http.GET(); - ++tries; - } - - if ((code != 200 && code != 206) || tries > 5) { - this->upload_end_(); - } - - String content_range_string = http.header("Content-Range"); - content_range_string.remove(0, 12); - this->content_length_ = content_range_string.toInt(); - this->tft_size_ = content_length_; - http.end(); - - if (this->content_length_ < 4096) { - ESP_LOGE(TAG, "Failed to get file size"); - this->upload_end_(); - } - - ESP_LOGD(TAG, "Updating Nextion %s...", this->device_model_.c_str()); - // The Nextion will ignore the update command if it is sleeping - - this->send_command_("sleep=0"); - this->set_backlight_brightness(1.0); - delay(250); // NOLINT - - 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_, this->parent_->get_baud_rate()); - - // Clear serial receive buffer - uint8_t d; - while (this->available()) { - this->read_byte(&d); - }; - - this->send_command_(command); - - App.feed_wdt(); - - std::string response; - ESP_LOGD(TAG, "Waiting for upgrade response"); - this->recv_ret_string_(response, 2000, true); // This can take some time to return - - // The Nextion display will, if it's ready to accept data, send a 0x05 byte. - ESP_LOGD(TAG, "Upgrade response is %s %zu", response.c_str(), response.length()); - - for (size_t i = 0; i < response.length(); i++) { - ESP_LOGD(TAG, "Available %d : 0x%02X", i, response[i]); - } - - if (response.find(0x05) != std::string::npos) { - ESP_LOGD(TAG, "preparation for tft update done"); - } else { - ESP_LOGD(TAG, "preparation for tft update failed %d \"%s\"", response[0], response.c_str()); - this->upload_end_(); - } - - // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 -#ifdef USE_ESP32 - uint32_t chunk_size = 8192; - if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0) { - chunk_size = this->content_length_; - } else { - if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand - int chunk = int((ESP.getFreeHeap() - 32768) / 4096); - chunk_size = chunk * 4096; - chunk_size = chunk_size > 65536 ? 65536 : chunk_size; - } else if (ESP.getFreeHeap() < 10240) { - chunk_size = 4096; - } - } -#else - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - uint32_t chunk_size = ESP.getFreeHeap() < 10240 ? 4096 : 8192; -#endif - - if (this->transfer_buffer_ == nullptr) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); - this->transfer_buffer_ = allocator.allocate(chunk_size); - if (this->transfer_buffer_ == nullptr) { // Try a smaller size - ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); - chunk_size = 4096; - ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); - this->transfer_buffer_ = allocator.allocate(chunk_size); - - if (!this->transfer_buffer_) - this->upload_end_(); - } - - this->transfer_buffer_size_ = chunk_size; - } - - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - ESP_LOGD(TAG, "Updating tft from \"%s\" with a file size of %d using %zu chunksize, Heap Size %d", - this->tft_url_.c_str(), this->content_length_, this->transfer_buffer_size_, ESP.getFreeHeap()); - - int result = 0; - while (this->content_length_ > 0) { - result = this->upload_by_chunks_(&http, result); - if (result < 0) { - ESP_LOGD(TAG, "Error updating Nextion!"); - this->upload_end_(); - } - App.feed_wdt(); - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - ESP_LOGD(TAG, "Heap Size %d, Bytes left %d", ESP.getFreeHeap(), this->content_length_); - } - ESP_LOGD(TAG, "Successfully updated Nextion!"); - - this->upload_end_(); -} - -void Nextion::upload_end_() { - ESP_LOGD(TAG, "Restarting Nextion"); - this->soft_reset(); - delay(1500); // NOLINT - ESP_LOGD(TAG, "Restarting esphome"); - ESP.restart(); // NOLINT(readability-static-accessed-through-instance) -} - -#ifdef USE_ESP8266 -WiFiClient *Nextion::get_wifi_client_() { - if (this->tft_url_.compare(0, 6, "https:") == 0) { - if (this->wifi_client_secure_ == nullptr) { - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); - this->wifi_client_secure_->setInsecure(); - this->wifi_client_secure_->setBufferSizes(512, 512); - } - return this->wifi_client_secure_; - } - - if (this->wifi_client_ == nullptr) { - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - this->wifi_client_ = new WiFiClient(); - } - return this->wifi_client_; -} -#endif -} // namespace nextion -} // namespace esphome - -#endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp new file mode 100644 index 0000000000..1187c77c8e --- /dev/null +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -0,0 +1,387 @@ +#include "nextion.h" + +#ifdef USE_NEXTION_TFT_UPLOAD +#ifdef USE_ARDUINO + +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" +#include "esphome/components/network/util.h" +#include + +#ifdef USE_ESP32 +#include +#endif + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion.upload.arduino"; + +// 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, "Free heap: %" PRIu32, this->get_free_heap_()); + 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"); + return -1; + } + + char range_header[32]; + sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end); + ESP_LOGV(TAG, "Requesting range: %s", range_header); + http_client.addHeader("Range", range_header); + int code = http_client.GET(); + if (code != HTTP_CODE_OK and code != HTTP_CODE_PARTIAL_CONTENT) { + ESP_LOGW(TAG, "HTTP Request failed; Error: %s", HTTPClient::errorToString(code).c_str()); + return -1; + } + + // Allocate the buffer dynamically + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + uint8_t *buffer = allocator.allocate(4096); + if (!buffer) { + ESP_LOGE(TAG, "Failed to allocate upload buffer"); + return -1; + } + + std::string recv_string; + while (true) { + App.feed_wdt(); + const uint16_t buffer_size = + this->content_length_ < 4096 ? this->content_length_ : 4096; // Limits buffer to the remaining data + ESP_LOGV(TAG, "Fetching %" PRIu16 " bytes from HTTP", buffer_size); + uint16_t read_len = 0; + int partial_read_len = 0; + const uint32_t start_time = millis(); + while (read_len < buffer_size && millis() - start_time < 5000) { + if (http_client.getStreamPtr()->available() > 0) { + partial_read_len = + http_client.getStreamPtr()->readBytes(reinterpret_cast(buffer) + read_len, buffer_size - read_len); + read_len += partial_read_len; + if (partial_read_len > 0) { + App.feed_wdt(); + delay(2); + } + } + } + if (read_len != buffer_size) { + // Did not receive the full package within the timeout period + ESP_LOGE(TAG, "Failed to read full package, received only %" PRIu16 " of %" PRIu16 " bytes", read_len, + buffer_size); + // Deallocate buffer + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } + ESP_LOGV(TAG, "%d bytes fetched, writing it to UART", read_len); + if (read_len > 0) { + recv_string.clear(); + this->write_array(buffer, buffer_size); + App.feed_wdt(); + 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, + "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " (DRAM) + %" PRIu32 " (PSRAM) bytes", + 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, "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " bytes", upload_percentage, + this->content_length_, this->get_free_heap_()); +#endif + upload_first_chunk_sent_ = true; + if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request + ESP_LOGD(TAG, "recv_string [%s]", + format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + uint32_t result = 0; + for (int j = 0; j < 4; ++j) { + result += static_cast(recv_string[j + 1]) << (8 * j); + } + if (result > 0) { + ESP_LOGI(TAG, "Nextion reported new range %" PRIu32, result); + this->content_length_ = this->tft_size_ - result; + range_start = result; + } else { + range_start = range_end + 1; + } + // Deallocate buffer + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return range_end + 1; + } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" + ESP_LOGE(TAG, "Invalid response from Nextion: [%s]", + format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + // Deallocate buffer + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } + + recv_string.clear(); + } else if (read_len == 0) { + ESP_LOGV(TAG, "End of HTTP response reached"); + break; // Exit the loop if there is no more data to read + } else { + ESP_LOGE(TAG, "Failed to read from HTTP client, error code: %d", read_len); + break; // Exit the loop on error + } + } + range_start = range_end + 1; + // Deallocate buffer + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return range_end + 1; +} + +bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { + ESP_LOGD(TAG, "Nextion TFT upload requested"); + ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); + ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); + + if (this->is_updating_) { + ESP_LOGW(TAG, "Currently uploading"); + return false; + } + + if (!network::is_connected()) { + ESP_LOGE(TAG, "Network is not connected"); + return false; + } + + this->is_updating_ = true; + + if (exit_reparse) { + ESP_LOGD(TAG, "Exiting Nextion reparse mode"); + if (!this->set_protocol_reparse_mode(false)) { + ESP_LOGW(TAG, "Failed to request Nextion to exit reparse mode"); + return false; + } + } + + // Check if baud rate is supported + this->original_baud_rate_ = this->parent_->get_baud_rate(); + static const std::vector SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600, + 115200, 230400, 250000, 256000, 512000, 921600}; + if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { + baud_rate = this->original_baud_rate_; + } + ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); + + // Define the configuration for the HTTP client + ESP_LOGV(TAG, "Initializing HTTP client"); + ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); + 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); +#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) + http_client.setFollowRedirects(true); +#endif +#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) + http_client.setRedirectLimit(3); +#endif + begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); +#endif // USE_ESP8266 + if (!begin_status) { + this->is_updating_ = false; + ESP_LOGD(TAG, "Connection failed"); + return false; + } else { + ESP_LOGD(TAG, "Connected"); + } + http_client.addHeader("Range", "bytes=0-255"); + const char *header_names[] = {"Content-Range"}; + http_client.collectHeaders(header_names, 1); + ESP_LOGD(TAG, "Requesting URL: %s", this->tft_url_.c_str()); + http_client.setReuse(true); + // try up to 5 times. DNS sometimes needs a second try or so + int tries = 1; + int code = http_client.GET(); + delay(100); // NOLINT + + App.feed_wdt(); + while (code != 200 && code != 206 && tries <= 5) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", this->tft_url_.c_str(), + HTTPClient::errorToString(code).c_str(), tries); + + delay(250); // NOLINT + App.feed_wdt(); + code = http_client.GET(); + ++tries; + } + + if (code != 200 and code != 206) { + return this->upload_end_(false); + } + + String content_range_string = http_client.header("Content-Range"); + content_range_string.remove(0, 12); + this->tft_size_ = content_range_string.toInt(); + + ESP_LOGD(TAG, "TFT file size: %zu bytes", this->tft_size_); + if (this->tft_size_ < 4096) { + ESP_LOGE(TAG, "File size check failed."); + ESP_LOGD(TAG, "Close HTTP connection"); + http_client.end(); + ESP_LOGV(TAG, "Connection closed"); + return this->upload_end_(false); + } else { + ESP_LOGV(TAG, "File size check passed. Proceeding..."); + } + this->content_length_ = this->tft_size_; + + ESP_LOGD(TAG, "Uploading Nextion"); + + // The Nextion will ignore the upload command if it is sleeping + ESP_LOGV(TAG, "Wake-up Nextion"); + this->ignore_is_setup_ = true; + this->send_command_("sleep=0"); + this->send_command_("dim=100"); + delay(250); // NOLINT + ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); + + 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); + + // Clear serial receive buffer + ESP_LOGV(TAG, "Clear serial receive buffer"); + this->reset_(false); + delay(250); // NOLINT + ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); + + ESP_LOGV(TAG, "Send upload instruction: %s", command); + this->send_command_(command); + + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Changing baud rate from %" PRIu32 " to %" PRIu32 " bps", this->original_baud_rate_, baud_rate); + this->parent_->set_baud_rate(baud_rate); + this->parent_->load_settings(); + } + + App.feed_wdt(); + + std::string response; + ESP_LOGV(TAG, "Waiting for upgrade response"); + 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. + ESP_LOGD(TAG, "Upgrade response is [%s] - %zu byte(s)", + format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + response.length()); + ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); + + if (response.find(0x05) != std::string::npos) { + ESP_LOGV(TAG, "Preparation for TFT upload done"); + } else { + ESP_LOGE(TAG, "Preparation for TFT upload failed %d \"%s\"", response[0], response.c_str()); + ESP_LOGD(TAG, "Close HTTP connection"); + http_client.end(); + ESP_LOGV(TAG, "Connection closed"); + return this->upload_end_(false); + } + + ESP_LOGD(TAG, "Uploading TFT to Nextion:"); + ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str()); + ESP_LOGD(TAG, " File size: %d bytes", this->content_length_); + ESP_LOGD(TAG, " Free heap: %" PRIu32, this->get_free_heap_()); + + // Proceed with the content download as before + + ESP_LOGV(TAG, "Starting transfer by chunks loop"); + + uint32_t position = 0; + while (this->content_length_ > 0) { + int upload_result = upload_by_chunks_(http_client, position); + if (upload_result < 0) { + ESP_LOGE(TAG, "Error uploading TFT to Nextion!"); + ESP_LOGD(TAG, "Close HTTP connection"); + http_client.end(); + ESP_LOGV(TAG, "Connection closed"); + return this->upload_end_(false); + } + App.feed_wdt(); + ESP_LOGV(TAG, "Free heap: %" PRIu32 ", Bytes left: %" PRIu32, this->get_free_heap_(), this->content_length_); + } + + ESP_LOGD(TAG, "Successfully uploaded TFT to Nextion!"); + + ESP_LOGD(TAG, "Close HTTP connection"); + http_client.end(); + ESP_LOGV(TAG, "Connection closed"); + return upload_end_(true); +} + +bool Nextion::upload_end_(bool successful) { + ESP_LOGD(TAG, "Nextion TFT upload finished: %s", YESNO(successful)); + this->is_updating_ = false; + this->ignore_is_setup_ = false; + + uint32_t baud_rate = this->parent_->get_baud_rate(); + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Changing baud rate back from %" PRIu32 " to %" PRIu32 " bps", baud_rate, this->original_baud_rate_); + this->parent_->set_baud_rate(this->original_baud_rate_); + this->parent_->load_settings(); + } + + if (successful) { + ESP_LOGD(TAG, "Restarting ESPHome"); + delay(1500); // NOLINT + arch_restart(); + } else { + ESP_LOGE(TAG, "Nextion TFT upload failed"); + } + return successful; +} + +#ifdef USE_ESP8266 +WiFiClient *Nextion::get_wifi_client_() { + if (this->tft_url_.compare(0, 6, "https:") == 0) { + if (this->wifi_client_secure_ == nullptr) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); + this->wifi_client_secure_->setInsecure(); + this->wifi_client_secure_->setBufferSizes(512, 512); + } + return this->wifi_client_secure_; + } + + if (this->wifi_client_ == nullptr) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->wifi_client_ = new WiFiClient(); + } + return this->wifi_client_; +} +#endif // USE_ESP8266 + +} // namespace nextion +} // namespace esphome + +#endif // USE_ARDUINO +#endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp new file mode 100644 index 0000000000..b5bb5478c1 --- /dev/null +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -0,0 +1,367 @@ +#include "nextion.h" + +#ifdef USE_NEXTION_TFT_UPLOAD +#ifdef USE_ESP_IDF + +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" +#include "esphome/components/network/util.h" +#include +#include +#include + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion.upload.idf"; + +// Followed guide +// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 + +int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start) { + uint32_t range_size = this->tft_size_ - range_start; + ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size()); + 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"); + return -1; + } + + char range_header[32]; + sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end); + ESP_LOGV(TAG, "Requesting range: %s", range_header); + esp_http_client_set_header(http_client, "Range", range_header); + ESP_LOGV(TAG, "Opening HTTP connetion"); + esp_err_t err; + if ((err = esp_http_client_open(http_client, 0)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); + return -1; + } + + ESP_LOGV(TAG, "Fetch content length"); + const int chunk_size = esp_http_client_fetch_headers(http_client); + ESP_LOGV(TAG, "content_length = %d", chunk_size); + if (chunk_size <= 0) { + ESP_LOGE(TAG, "Failed to get chunk's content length: %d", chunk_size); + return -1; + } + + // Allocate the buffer dynamically + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + uint8_t *buffer = allocator.allocate(4096); + if (!buffer) { + ESP_LOGE(TAG, "Failed to allocate upload buffer"); + return -1; + } + + std::string recv_string; + while (true) { + App.feed_wdt(); + const uint16_t buffer_size = + this->content_length_ < 4096 ? this->content_length_ : 4096; // Limits buffer to the remaining data + ESP_LOGV(TAG, "Fetching %" PRIu16 " bytes from HTTP", buffer_size); + uint16_t read_len = 0; + int partial_read_len = 0; + uint8_t retries = 0; + // Attempt to read the chunk with retries. + while (retries < 5 && read_len < buffer_size) { + partial_read_len = + esp_http_client_read(http_client, reinterpret_cast(buffer) + read_len, buffer_size - read_len); + if (partial_read_len > 0) { + read_len += partial_read_len; // Accumulate the total read length. + // Reset retries on successful read. + retries = 0; + } else { + // If no data was read, increment retries. + retries++; + vTaskDelay(pdMS_TO_TICKS(2)); // NOLINT + } + App.feed_wdt(); // Feed the watchdog timer. + } + if (read_len != buffer_size) { + // Did not receive the full package within the timeout period + ESP_LOGE(TAG, "Failed to read full package, received only %" PRIu16 " of %" PRIu16 " bytes", read_len, + buffer_size); + // Deallocate buffer + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } + ESP_LOGV(TAG, "%d bytes fetched, writing it to UART", read_len); + if (read_len > 0) { + recv_string.clear(); + this->write_array(buffer, buffer_size); + App.feed_wdt(); + 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_; +#ifdef USE_PSRAM + ESP_LOGD( + TAG, + "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " (DRAM) + %" PRIu32 " (PSRAM) bytes", + 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, "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " bytes", upload_percentage, + this->content_length_, static_cast(esp_get_free_heap_size())); +#endif + upload_first_chunk_sent_ = true; + if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request + ESP_LOGD(TAG, "recv_string [%s]", + format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + uint32_t result = 0; + for (int j = 0; j < 4; ++j) { + result += static_cast(recv_string[j + 1]) << (8 * j); + } + if (result > 0) { + ESP_LOGI(TAG, "Nextion reported new range %" PRIu32, result); + this->content_length_ = this->tft_size_ - result; + range_start = result; + } else { + range_start = range_end + 1; + } + // Deallocate buffer + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return range_end + 1; + } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" + ESP_LOGE(TAG, "Invalid response from Nextion: [%s]", + format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + // Deallocate buffer + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } + + recv_string.clear(); + } else if (read_len == 0) { + ESP_LOGV(TAG, "End of HTTP response reached"); + break; // Exit the loop if there is no more data to read + } else { + ESP_LOGE(TAG, "Failed to read from HTTP client, error code: %" PRIu16, read_len); + break; // Exit the loop on error + } + } + range_start = range_end + 1; + // Deallocate buffer + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return range_end + 1; +} + +bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { + ESP_LOGD(TAG, "Nextion TFT upload requested"); + ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); + ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); + + if (this->is_updating_) { + ESP_LOGW(TAG, "Currently uploading"); + return false; + } + + if (!network::is_connected()) { + ESP_LOGE(TAG, "Network is not connected"); + return false; + } + + this->is_updating_ = true; + + if (exit_reparse) { + ESP_LOGD(TAG, "Exiting Nextion reparse mode"); + if (!this->set_protocol_reparse_mode(false)) { + ESP_LOGW(TAG, "Failed to request Nextion to exit reparse mode"); + return false; + } + } + + // Check if baud rate is supported + this->original_baud_rate_ = this->parent_->get_baud_rate(); + static const std::vector SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600, + 115200, 230400, 250000, 256000, 512000, 921600}; + if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { + baud_rate = this->original_baud_rate_; + } + ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); + + // Define the configuration for the HTTP client + ESP_LOGV(TAG, "Initializing HTTP client"); + ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size()); + esp_http_client_config_t config = { + .url = this->tft_url_.c_str(), + .cert_pem = nullptr, + .method = HTTP_METHOD_HEAD, + .timeout_ms = 15000, + .disable_auto_redirect = false, + .max_redirection_count = 10, + }; + // Initialize the HTTP client with the configuration + esp_http_client_handle_t http_client = esp_http_client_init(&config); + if (!http_client) { + ESP_LOGE(TAG, "Failed to initialize HTTP client."); + return this->upload_end_(false); + } + + esp_err_t err = esp_http_client_set_header(http_client, "Connection", "keep-alive"); + if (err != ESP_OK) { + ESP_LOGE(TAG, "HTTP set header failed: %s", esp_err_to_name(err)); + esp_http_client_cleanup(http_client); + return this->upload_end_(false); + } + + // Perform the HTTP request + ESP_LOGV(TAG, "Check if the client could connect"); + ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size()); + err = esp_http_client_perform(http_client); + if (err != ESP_OK) { + ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err)); + esp_http_client_cleanup(http_client); + return this->upload_end_(false); + } + + // Check the HTTP Status Code + ESP_LOGV(TAG, "Check the HTTP Status Code"); + ESP_LOGV(TAG, "Free 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); + } + + this->tft_size_ = esp_http_client_get_content_length(http_client); + + ESP_LOGD(TAG, "TFT file size: %zu bytes", this->tft_size_); + if (this->tft_size_ < 4096 || this->tft_size_ > 134217728) { + ESP_LOGE(TAG, "File size check failed."); + ESP_LOGD(TAG, "Close HTTP connection"); + esp_http_client_close(http_client); + esp_http_client_cleanup(http_client); + ESP_LOGV(TAG, "Connection closed"); + return this->upload_end_(false); + } else { + ESP_LOGV(TAG, "File size check passed. Proceeding..."); + } + this->content_length_ = this->tft_size_; + + ESP_LOGD(TAG, "Uploading Nextion"); + + // The Nextion will ignore the upload command if it is sleeping + ESP_LOGV(TAG, "Wake-up Nextion"); + this->ignore_is_setup_ = true; + this->send_command_("sleep=0"); + this->send_command_("dim=100"); + vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT + ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size()); + + 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 %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate); + + // Clear serial receive buffer + ESP_LOGV(TAG, "Clear serial receive buffer"); + this->reset_(false); + vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT + ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size()); + + ESP_LOGV(TAG, "Send upload instruction: %s", command); + this->send_command_(command); + + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Changing baud rate from %" PRIu32 " to %" PRIu32 " bps", this->original_baud_rate_, baud_rate); + this->parent_->set_baud_rate(baud_rate); + this->parent_->load_settings(); + } + + std::string response; + ESP_LOGV(TAG, "Waiting for upgrade response"); + 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. + ESP_LOGD(TAG, "Upgrade response is [%s] - %zu byte(s)", + format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + response.length()); + ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size()); + + if (response.find(0x05) != std::string::npos) { + ESP_LOGV(TAG, "Preparation for TFT upload done"); + } else { + ESP_LOGE(TAG, "Preparation for TFT upload failed %d \"%s\"", response[0], response.c_str()); + ESP_LOGD(TAG, "Close HTTP connection"); + esp_http_client_close(http_client); + esp_http_client_cleanup(http_client); + ESP_LOGV(TAG, "Connection closed"); + return this->upload_end_(false); + } + + ESP_LOGV(TAG, "Change the method to GET before starting the download"); + esp_err_t set_method_result = esp_http_client_set_method(http_client, HTTP_METHOD_GET); + if (set_method_result != ESP_OK) { + ESP_LOGE(TAG, "Failed to set HTTP method to GET: %s", esp_err_to_name(set_method_result)); + return this->upload_end_(false); + } + + ESP_LOGD(TAG, "Uploading TFT to Nextion:"); + ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str()); + ESP_LOGD(TAG, " File size: %" PRIu32 " bytes", this->content_length_); + ESP_LOGD(TAG, " Free heap: %" PRIu32, esp_get_free_heap_size()); + + // Proceed with the content download as before + + ESP_LOGV(TAG, "Starting transfer by chunks loop"); + + uint32_t position = 0; + while (this->content_length_ > 0) { + int upload_result = upload_by_chunks_(http_client, position); + if (upload_result < 0) { + ESP_LOGE(TAG, "Error uploading TFT to Nextion!"); + ESP_LOGD(TAG, "Close HTTP connection"); + esp_http_client_close(http_client); + esp_http_client_cleanup(http_client); + ESP_LOGV(TAG, "Connection closed"); + return this->upload_end_(false); + } + App.feed_wdt(); + ESP_LOGV(TAG, "Free heap: %" PRIu32 ", Bytes left: %" PRIu32, esp_get_free_heap_size(), this->content_length_); + } + + ESP_LOGD(TAG, "Successfully uploaded TFT to Nextion!"); + + ESP_LOGD(TAG, "Close HTTP connection"); + esp_http_client_close(http_client); + esp_http_client_cleanup(http_client); + ESP_LOGV(TAG, "Connection closed"); + return this->upload_end_(true); +} + +bool Nextion::upload_end_(bool successful) { + ESP_LOGD(TAG, "Nextion TFT upload finished: %s", YESNO(successful)); + this->is_updating_ = false; + this->ignore_is_setup_ = false; + + uint32_t baud_rate = this->parent_->get_baud_rate(); + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Changing baud rate back from %" PRIu32 " to %" PRIu32 " bps", baud_rate, this->original_baud_rate_); + this->parent_->set_baud_rate(this->original_baud_rate_); + this->parent_->load_settings(); + } + + if (successful) { + ESP_LOGD(TAG, "Restarting ESPHome"); + delay(1500); // NOLINT + arch_restart(); + } else { + ESP_LOGE(TAG, "Nextion TFT upload failed"); + } + return successful; +} + +} // namespace nextion +} // namespace esphome + +#endif // USE_ESP_IDF +#endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 32bfccf9f8..6cc641fcf3 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -30,7 +30,7 @@ void NextionSensor::add_to_wave_buffer(float state) { } void NextionSensor::update() { - if (!this->nextion_->is_setup()) + if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; if (this->wave_chan_id_ == UINT8_MAX) { @@ -45,7 +45,7 @@ void NextionSensor::update() { } void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { - if (!this->nextion_->is_setup()) + if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; if (std::isnan(state)) @@ -76,9 +76,15 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { } } + float published_state = state; if (this->wave_chan_id_ == UINT8_MAX) { if (publish) { - this->publish_state(state); + if (this->precision_ > 0) { + double to_multiply = pow(10, -this->precision_); + published_state = (float) (state * to_multiply); + } + + this->publish_state(published_state); } else { this->raw_state = state; this->state = state; @@ -87,7 +93,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { } this->update_component_settings(); - ESP_LOGN(TAG, "Wrote state for sensor \"%s\" state %lf", this->variable_name_.c_str(), state); + ESP_LOGN(TAG, "Wrote state for sensor \"%s\" state %lf", this->variable_name_.c_str(), published_state); } void NextionSensor::wave_update_() { diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp index 1f32ad3425..63c1882b48 100644 --- a/esphome/components/nextion/switch/nextion_switch.cpp +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -18,13 +18,13 @@ void NextionSwitch::process_bool(const std::string &variable_name, bool on) { } void NextionSwitch::update() { - if (!this->nextion_->is_setup()) + if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; this->nextion_->add_to_get_queue(this); } void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { - if (!this->nextion_->is_setup()) + if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; if (send_to_nextion) { diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index 08f032df74..a3fc9390f5 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -16,13 +16,13 @@ void NextionTextSensor::process_text(const std::string &variable_name, const std } void NextionTextSensor::update() { - if (!this->nextion_->is_setup()) + if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; this->nextion_->add_to_get_queue(this); } void NextionTextSensor::set_state(const std::string &state, bool publish, bool send_to_nextion) { - if (!this->nextion_->is_setup()) + if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; if (send_to_nextion) { diff --git a/esphome/components/nfc/__init__.py b/esphome/components/nfc/__init__.py index c3bbc50bf9..eea1a47b24 100644 --- a/esphome/components/nfc/__init__.py +++ b/esphome/components/nfc/__init__.py @@ -1,12 +1,13 @@ from esphome import automation import esphome.codegen as cg -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kbx81"] nfc_ns = cg.esphome_ns.namespace("nfc") +Nfcc = nfc_ns.class_("Nfcc") NfcTag = nfc_ns.class_("NfcTag") - +NfcTagListener = nfc_ns.class_("NfcTagListener") NfcOnTagTrigger = nfc_ns.class_( "NfcOnTagTrigger", automation.Trigger.template(cg.std_string, NfcTag) ) diff --git a/esphome/components/nfc/binary_sensor/__init__.py b/esphome/components/nfc/binary_sensor/__init__.py new file mode 100644 index 0000000000..21c8298ea8 --- /dev/null +++ b/esphome/components/nfc/binary_sensor/__init__.py @@ -0,0 +1,72 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_UID +from esphome.core import HexInt +from .. import nfc_ns, Nfcc, NfcTagListener + +DEPENDENCIES = ["nfc"] + +CONF_NDEF_CONTAINS = "ndef_contains" +CONF_NFCC_ID = "nfcc_id" +CONF_TAG_ID = "tag_id" + +NfcTagBinarySensor = nfc_ns.class_( + "NfcTagBinarySensor", + binary_sensor.BinarySensor, + cg.Component, + NfcTagListener, + cg.Parented.template(Nfcc), +) + + +def validate_uid(value): + value = cv.string_strict(value) + for x in value.split("-"): + if len(x) != 2: + raise cv.Invalid( + "Each part (separated by '-') of the UID must be two characters " + "long." + ) + try: + x = int(x, 16) + except ValueError as err: + raise cv.Invalid( + "Valid characters for parts of a UID are 0123456789ABCDEF." + ) from err + if x < 0 or x > 255: + raise cv.Invalid( + "Valid values for UID parts (separated by '-') are 00 to FF" + ) + return value + + +CONFIG_SCHEMA = cv.All( + binary_sensor.binary_sensor_schema(NfcTagBinarySensor) + .extend( + { + cv.GenerateID(CONF_NFCC_ID): cv.use_id(Nfcc), + cv.Optional(CONF_NDEF_CONTAINS): cv.string, + cv.Optional(CONF_TAG_ID): cv.string, + cv.Optional(CONF_UID): validate_uid, + } + ) + .extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_NDEF_CONTAINS, CONF_TAG_ID, CONF_UID), +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_NFCC_ID]) + + hub = await cg.get_variable(config[CONF_NFCC_ID]) + cg.add(hub.register_listener(var)) + if CONF_NDEF_CONTAINS in config: + cg.add(var.set_ndef_match_string(config[CONF_NDEF_CONTAINS])) + if CONF_TAG_ID in config: + cg.add(var.set_tag_name(config[CONF_TAG_ID])) + elif CONF_UID in config: + addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split("-")] + cg.add(var.set_uid(addr)) diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.cpp b/esphome/components/nfc/binary_sensor/binary_sensor.cpp new file mode 100644 index 0000000000..8f1f6acd51 --- /dev/null +++ b/esphome/components/nfc/binary_sensor/binary_sensor.cpp @@ -0,0 +1,114 @@ +#include "binary_sensor.h" +#include "../nfc_helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.binary_sensor"; + +void NfcTagBinarySensor::setup() { + this->parent_->register_listener(this); + this->publish_initial_state(false); +} + +void NfcTagBinarySensor::dump_config() { + std::string match_str = "name"; + + LOG_BINARY_SENSOR("", "NFC Tag Binary Sensor", this); + if (!this->match_string_.empty()) { + if (!this->match_tag_name_) { + match_str = "contains"; + } + ESP_LOGCONFIG(TAG, " Tag %s: %s", match_str.c_str(), this->match_string_.c_str()); + return; + } + if (!this->uid_.empty()) { + ESP_LOGCONFIG(TAG, " Tag UID: %s", format_bytes(this->uid_).c_str()); + } +} + +void NfcTagBinarySensor::set_ndef_match_string(const std::string &str) { + this->match_string_ = str; + this->match_tag_name_ = false; +} + +void NfcTagBinarySensor::set_tag_name(const std::string &str) { + this->match_string_ = str; + this->match_tag_name_ = true; +} + +void NfcTagBinarySensor::set_uid(const std::vector &uid) { this->uid_ = uid; } + +bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr &msg) { + for (const auto &record : msg->get_records()) { + if (record->get_payload().find(this->match_string_) != std::string::npos) { + return true; + } + } + return false; +} + +bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr &msg) { + for (const auto &record : msg->get_records()) { + if (record->get_payload().find(HA_TAG_ID_PREFIX) != std::string::npos) { + auto rec_substr = record->get_payload().substr(sizeof(HA_TAG_ID_PREFIX) - 1); + if (rec_substr.find(this->match_string_) != std::string::npos) { + return true; + } + } + } + return false; +} + +bool NfcTagBinarySensor::tag_match_uid(const std::vector &data) { + if (data.size() != this->uid_.size()) { + return false; + } + + for (size_t i = 0; i < data.size(); i++) { + if (data[i] != this->uid_[i]) { + return false; + } + } + return true; +} + +void NfcTagBinarySensor::tag_off(NfcTag &tag) { + if (!this->match_string_.empty() && tag.has_ndef_message()) { + if (this->match_tag_name_) { + if (this->tag_match_tag_name(tag.get_ndef_message())) { + this->publish_state(false); + } + } else { + if (this->tag_match_ndef_string(tag.get_ndef_message())) { + this->publish_state(false); + } + } + return; + } + if (!this->uid_.empty() && this->tag_match_uid(tag.get_uid())) { + this->publish_state(false); + } +} + +void NfcTagBinarySensor::tag_on(NfcTag &tag) { + if (!this->match_string_.empty() && tag.has_ndef_message()) { + if (this->match_tag_name_) { + if (this->tag_match_tag_name(tag.get_ndef_message())) { + this->publish_state(true); + } + } else { + if (this->tag_match_ndef_string(tag.get_ndef_message())) { + this->publish_state(true); + } + } + return; + } + if (!this->uid_.empty() && this->tag_match_uid(tag.get_uid())) { + this->publish_state(true); + } +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.h b/esphome/components/nfc/binary_sensor/binary_sensor.h new file mode 100644 index 0000000000..cc313c2f2b --- /dev/null +++ b/esphome/components/nfc/binary_sensor/binary_sensor.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_tag.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace nfc { + +class NfcTagBinarySensor : public binary_sensor::BinarySensor, + public Component, + public NfcTagListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_ndef_match_string(const std::string &str); + void set_tag_name(const std::string &str); + void set_uid(const std::vector &uid); + + bool tag_match_ndef_string(const std::shared_ptr &msg); + bool tag_match_tag_name(const std::shared_ptr &msg); + bool tag_match_uid(const std::vector &data); + + void tag_off(NfcTag &tag) override; + void tag_on(NfcTag &tag) override; + + protected: + bool match_tag_name_{false}; + std::string match_string_; + std::vector uid_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h new file mode 100644 index 0000000000..fdaf6d0cc5 --- /dev/null +++ b/esphome/components/nfc/nci_core.h @@ -0,0 +1,144 @@ +#pragma once + +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace nfc { + +// Header info +static const uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes +static const uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets +static const uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset +static const uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset +static const uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset +// Important masks +static const uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask +static const uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit +static const uint8_t NCI_PKT_GID_MASK = 0x0F; +static const uint8_t NCI_PKT_OID_MASK = 0x3F; +// Message types +static const uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) +static const uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC +static const uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands +static const uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC +// GIDs +static const uint8_t NCI_CORE_GID = 0x0; +static const uint8_t RF_GID = 0x1; +static const uint8_t NFCEE_GID = 0x1; +static const uint8_t NCI_PROPRIETARY_GID = 0xF; +// OIDs +static const uint8_t NCI_CORE_RESET_OID = 0x00; +static const uint8_t NCI_CORE_INIT_OID = 0x01; +static const uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; +static const uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; +static const uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; +static const uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; +static const uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; +static const uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; +static const uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; + +static const uint8_t RF_DISCOVER_MAP_OID = 0x00; +static const uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; +static const uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; +static const uint8_t RF_DISCOVER_OID = 0x03; +static const uint8_t RF_DISCOVER_SELECT_OID = 0x04; +static const uint8_t RF_INTF_ACTIVATED_OID = 0x05; +static const uint8_t RF_DEACTIVATE_OID = 0x06; +static const uint8_t RF_FIELD_INFO_OID = 0x07; +static const uint8_t RF_T3T_POLLING_OID = 0x08; +static const uint8_t RF_NFCEE_ACTION_OID = 0x09; +static const uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; +static const uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; + +static const uint8_t NFCEE_DISCOVER_OID = 0x00; +static const uint8_t NFCEE_MODE_SET_OID = 0x01; +// Interfaces +static const uint8_t INTF_NFCEE_DIRECT = 0x00; +static const uint8_t INTF_FRAME = 0x01; +static const uint8_t INTF_ISODEP = 0x02; +static const uint8_t INTF_NFCDEP = 0x03; +static const uint8_t INTF_TAGCMD = 0x80; // NXP proprietary +// Bit rates +static const uint8_t NFC_BIT_RATE_106 = 0x00; +static const uint8_t NFC_BIT_RATE_212 = 0x01; +static const uint8_t NFC_BIT_RATE_424 = 0x02; +static const uint8_t NFC_BIT_RATE_848 = 0x03; +static const uint8_t NFC_BIT_RATE_1695 = 0x04; +static const uint8_t NFC_BIT_RATE_3390 = 0x05; +static const uint8_t NFC_BIT_RATE_6780 = 0x06; +// Protocols +static const uint8_t PROT_UNDETERMINED = 0x00; +static const uint8_t PROT_T1T = 0x01; +static const uint8_t PROT_T2T = 0x02; +static const uint8_t PROT_T3T = 0x03; +static const uint8_t PROT_ISODEP = 0x04; +static const uint8_t PROT_NFCDEP = 0x05; +static const uint8_t PROT_T5T = 0x06; +static const uint8_t PROT_MIFARE = 0x80; +// RF Technologies +static const uint8_t NFC_RF_TECH_A = 0x00; +static const uint8_t NFC_RF_TECH_B = 0x01; +static const uint8_t NFC_RF_TECH_F = 0x02; +static const uint8_t NFC_RF_TECH_15693 = 0x03; +// RF Technology & Modes +static const uint8_t MODE_MASK = 0xF0; +static const uint8_t MODE_LISTEN_MASK = 0x80; +static const uint8_t MODE_POLL = 0x00; + +static const uint8_t TECH_PASSIVE_NFCA = 0x00; +static const uint8_t TECH_PASSIVE_NFCB = 0x01; +static const uint8_t TECH_PASSIVE_NFCF = 0x02; +static const uint8_t TECH_ACTIVE_NFCA = 0x03; +static const uint8_t TECH_ACTIVE_NFCF = 0x05; +static const uint8_t TECH_PASSIVE_15693 = 0x06; +// Status codes +static const uint8_t STATUS_OK = 0x00; +static const uint8_t STATUS_REJECTED = 0x01; +static const uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; +static const uint8_t STATUS_FAILED = 0x03; +static const uint8_t STATUS_NOT_INITIALIZED = 0x04; +static const uint8_t STATUS_SYNTAX_ERROR = 0x05; +static const uint8_t STATUS_SEMANTIC_ERROR = 0x06; +static const uint8_t STATUS_INVALID_PARAM = 0x09; +static const uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; +static const uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; +static const uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; +static const uint8_t DISCOVERY_TEAR_DOWN = 0xA2; +static const uint8_t RF_TRANSMISSION_ERROR = 0xB0; +static const uint8_t RF_PROTOCOL_ERROR = 0xB1; +static const uint8_t RF_TIMEOUT_ERROR = 0xB2; +static const uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; +static const uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; +static const uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; +static const uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; +// Deactivation types/reasons +static const uint8_t DEACTIVATION_TYPE_IDLE = 0x00; +static const uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; +static const uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; +static const uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; +// RF discover map modes +static const uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; +static const uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; +// RF discover notification types +static const uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; +static const uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; +static const uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; +// Important message offsets +static const uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_message.cpp b/esphome/components/nfc/nci_message.cpp new file mode 100644 index 0000000000..c6b21f6ae0 --- /dev/null +++ b/esphome/components/nfc/nci_message.cpp @@ -0,0 +1,166 @@ +#include "nci_core.h" +#include "nci_message.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace nfc { + +static const char *const TAG = "NciMessage"; + +NciMessage::NciMessage(const uint8_t message_type, const std::vector &payload) { + this->set_message(message_type, payload); +} + +NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid) { + this->set_header(message_type, gid, oid); +} + +NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid, + const std::vector &payload) { + this->set_message(message_type, gid, oid, payload); +} + +NciMessage::NciMessage(const std::vector &raw_packet) { this->nci_message_ = raw_packet; }; + +std::vector NciMessage::encode() { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + std::vector message = this->nci_message_; + return message; +} + +void NciMessage::reset() { this->nci_message_ = {0, 0, 0}; } + +uint8_t NciMessage::get_message_type() const { + return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK; +} + +uint8_t NciMessage::get_gid() const { return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK; } + +uint8_t NciMessage::get_oid() const { return this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK; } + +uint8_t NciMessage::get_payload_size(const bool recompute) { + if (!this->nci_message_.empty()) { + if (recompute) { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + } + return this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET]; + } + return 0; +} + +uint8_t NciMessage::get_simple_status_response() const { + if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) { + return this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET]; + } + return STATUS_FAILED; +} + +uint8_t NciMessage::get_message_byte(const uint8_t offset) const { + if (this->nci_message_.size() > offset) { + return this->nci_message_[offset]; + } + return 0; +} + +std::vector &NciMessage::get_message() { return this->nci_message_; } + +bool NciMessage::has_payload() const { return this->nci_message_.size() > nfc::NCI_PKT_HEADER_SIZE; } + +bool NciMessage::message_type_is(const uint8_t message_type) const { + if (!this->nci_message_.empty()) { + return message_type == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK); + } + return false; +} + +bool NciMessage::message_length_is(const uint8_t message_length, const bool recompute) { + if (this->nci_message_.size() > nfc::NCI_PKT_LENGTH_OFFSET) { + if (recompute) { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + } + return message_length == this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET]; + } + return false; +} + +bool NciMessage::gid_is(const uint8_t gid) const { + if (this->nci_message_.size() > nfc::NCI_PKT_MT_GID_OFFSET) { + return gid == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK); + } + return false; +} + +bool NciMessage::oid_is(const uint8_t oid) const { + if (this->nci_message_.size() > nfc::NCI_PKT_OID_OFFSET) { + return oid == (this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK); + } + return false; +} + +bool NciMessage::simple_status_response_is(const uint8_t response) const { + if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) { + return response == this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET]; + } + return false; +} + +void NciMessage::set_header(const uint8_t message_type, const uint8_t gid, const uint8_t oid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = + (message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK); + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; +} + +void NciMessage::set_message(const uint8_t message_type, const std::vector &payload) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end()); +} + +void NciMessage::set_message(const uint8_t message_type, const uint8_t gid, const uint8_t oid, + const std::vector &payload) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = + (message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK); + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end()); +} + +void NciMessage::set_message_type(const uint8_t message_type) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + auto mt_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_MT_MASK; + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = mt_masked | (message_type & nfc::NCI_PKT_MT_MASK); +} + +void NciMessage::set_gid(const uint8_t gid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + auto gid_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_GID_MASK; + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = gid_masked | (gid & nfc::NCI_PKT_GID_MASK); +} + +void NciMessage::set_oid(const uint8_t oid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; +} + +void NciMessage::set_payload(const std::vector &payload) { + std::vector message(this->nci_message_.begin(), this->nci_message_.begin() + nfc::NCI_PKT_HEADER_SIZE); + + message.insert(message.end(), payload.begin(), payload.end()); + message[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_ = message; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_message.h b/esphome/components/nfc/nci_message.h new file mode 100644 index 0000000000..c6b8537402 --- /dev/null +++ b/esphome/components/nfc/nci_message.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace nfc { + +class NciMessage { + public: + NciMessage() {} + NciMessage(uint8_t message_type, const std::vector &payload); + NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid); + NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector &payload); + NciMessage(const std::vector &raw_packet); + + std::vector encode(); + void reset(); + + uint8_t get_message_type() const; + uint8_t get_gid() const; + uint8_t get_oid() const; + uint8_t get_payload_size(bool recompute = false); + uint8_t get_simple_status_response() const; + uint8_t get_message_byte(uint8_t offset) const; + std::vector &get_message(); + + bool has_payload() const; + bool message_type_is(uint8_t message_type) const; + bool message_length_is(uint8_t message_length, bool recompute = false); + bool gid_is(uint8_t gid) const; + bool oid_is(uint8_t oid) const; + bool simple_status_response_is(uint8_t response) const; + + void set_header(uint8_t message_type, uint8_t gid, uint8_t oid); + void set_message(uint8_t message_type, const std::vector &payload); + void set_message(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector &payload); + void set_message_type(uint8_t message_type); + void set_gid(uint8_t gid); + void set_oid(uint8_t oid); + void set_payload(const std::vector &payload); + + protected: + std::vector nci_message_{0, 0, 0}; // three bytes, MT/PBF/GID, OID, payload length/size +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp index 461856d377..e7304445c5 100644 --- a/esphome/components/nfc/ndef_message.cpp +++ b/esphome/components/nfc/ndef_message.cpp @@ -1,4 +1,5 @@ #include "ndef_message.h" +#include namespace esphome { namespace nfc { @@ -32,7 +33,7 @@ NdefMessage::NdefMessage(std::vector &data) { id_length = data[index++]; } - ESP_LOGVV(TAG, "Lengths: type=%d, payload=%d, id=%d", type_length, payload_length, id_length); + ESP_LOGVV(TAG, "Lengths: type=%d, payload=%" PRIu32 ", id=%d", type_length, payload_length, id_length); std::string type_str(data.begin() + index, data.begin() + index + type_length); diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index b7c7215028..cf5a7f5ef1 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -53,8 +53,8 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { } bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index) { - uint8_t i = get_mifare_classic_ndef_start_index(data); - if (i < 0 || data[i] != 0x03) { + auto i = get_mifare_classic_ndef_start_index(data); + if (data[i] != 0x03) { ESP_LOGE(TAG, "Error, Can't decode message length."); return false; } diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index d4d66f970f..23bfdd8ef0 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -66,5 +66,19 @@ bool mifare_classic_is_trailer_block(uint8_t block_num); uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length); +class NfcTagListener { + public: + virtual void tag_off(NfcTag &tag) {} + virtual void tag_on(NfcTag &tag) {} +}; + +class Nfcc { + public: + void register_listener(NfcTagListener *listener) { this->tag_listeners_.push_back(listener); } + + protected: + std::vector tag_listeners_; +}; + } // namespace nfc } // namespace esphome diff --git a/esphome/components/nfc/nfc_helpers.cpp b/esphome/components/nfc/nfc_helpers.cpp new file mode 100644 index 0000000000..bfaed6e486 --- /dev/null +++ b/esphome/components/nfc/nfc_helpers.cpp @@ -0,0 +1,47 @@ +#include "nfc_helpers.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.helpers"; + +bool has_ha_tag_ndef(NfcTag &tag) { return !get_ha_tag_ndef(tag).empty(); } + +std::string get_ha_tag_ndef(NfcTag &tag) { + if (!tag.has_ndef_message()) { + return std::string(); + } + auto message = tag.get_ndef_message(); + auto records = message->get_records(); + for (const auto &record : records) { + std::string payload = record->get_payload(); + size_t pos = payload.find(HA_TAG_ID_PREFIX); + if (pos != std::string::npos) { + return payload.substr(pos + sizeof(HA_TAG_ID_PREFIX) - 1); + } + } + return std::string(); +} + +std::string get_random_ha_tag_ndef() { + static const char ALPHANUM[] = "0123456789abcdef"; + std::string uri = HA_TAG_ID_PREFIX; + for (int i = 0; i < 8; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + uri += "-"; + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 4; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + uri += "-"; + } + for (int i = 0; i < 12; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + ESP_LOGD("pn7160", "Payload to be written: %s", uri.c_str()); + return uri; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc_helpers.h b/esphome/components/nfc/nfc_helpers.h new file mode 100644 index 0000000000..74f5beba13 --- /dev/null +++ b/esphome/components/nfc/nfc_helpers.h @@ -0,0 +1,17 @@ +#pragma once + +#include "nfc_tag.h" + +namespace esphome { +namespace nfc { + +static const char HA_TAG_ID_EXT_RECORD_TYPE[] = "android.com:pkg"; +static const char HA_TAG_ID_EXT_RECORD_PAYLOAD[] = "io.homeassistant.companion.android"; +static const char HA_TAG_ID_PREFIX[] = "https://www.home-assistant.io/tag/"; + +std::string get_ha_tag_ndef(NfcTag &tag); +std::string get_random_ha_tag_ndef(); +bool has_ha_tag_ndef(NfcTag &tag); + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/noblex/__init__.py b/esphome/components/noblex/__init__.py new file mode 100644 index 0000000000..ceceff9df9 --- /dev/null +++ b/esphome/components/noblex/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@AGalfra"] diff --git a/esphome/components/noblex/climate.py b/esphome/components/noblex/climate.py new file mode 100644 index 0000000000..2025cb5d9e --- /dev/null +++ b/esphome/components/noblex/climate.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +noblex_ns = cg.esphome_ns.namespace("noblex") +NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NoblexClimate), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/noblex/noblex.cpp b/esphome/components/noblex/noblex.cpp new file mode 100644 index 0000000000..3521745bdc --- /dev/null +++ b/esphome/components/noblex/noblex.cpp @@ -0,0 +1,309 @@ +#include "noblex.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace noblex { + +static const char *const TAG = "noblex.climate"; + +const uint16_t NOBLEX_HEADER_MARK = 9000; +const uint16_t NOBLEX_HEADER_SPACE = 4500; +const uint16_t NOBLEX_BIT_MARK = 660; +const uint16_t NOBLEX_ONE_SPACE = 1640; +const uint16_t NOBLEX_ZERO_SPACE = 520; +const uint32_t NOBLEX_GAP = 20000; +const uint8_t NOBLEX_POWER = 0x10; + +using IRNoblexMode = enum IRNoblexMode { + IR_NOBLEX_MODE_AUTO = 0b000, + IR_NOBLEX_MODE_COOL = 0b100, + IR_NOBLEX_MODE_DRY = 0b010, + IR_NOBLEX_MODE_FAN = 0b110, + IR_NOBLEX_MODE_HEAT = 0b001, +}; + +using IRNoblexFan = enum IRNoblexFan { + IR_NOBLEX_FAN_AUTO = 0b00, + IR_NOBLEX_FAN_LOW = 0b10, + IR_NOBLEX_FAN_MEDIUM = 0b01, + IR_NOBLEX_FAN_HIGH = 0b11, +}; + +// Transmit via IR the state of this climate controller. +void NoblexClimate::transmit_state() { + uint8_t remote_state[8] = {0x80, 0x10, 0x00, 0x0A, 0x50, 0x00, 0x20, 0x00}; // OFF, COOL, 24C, FAN_AUTO + + auto powered_on = this->mode != climate::CLIMATE_MODE_OFF; + if (powered_on) { + remote_state[0] |= 0x10; // power bit + remote_state[2] = 0x02; + } + if (powered_on != this->powered_on_assumed) + this->powered_on_assumed = powered_on; + + auto temp = (uint8_t) roundf(clamp(this->target_temperature, NOBLEX_TEMP_MIN, NOBLEX_TEMP_MAX)); + remote_state[1] = reverse_bits(uint8_t((temp - NOBLEX_TEMP_MIN) & 0x0F)); + + switch (this->mode) { + case climate::CLIMATE_MODE_HEAT_COOL: + remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_AUTO << 5); + remote_state[1] = 0x90; // NOBLEX_TEMP_MAP 25C + break; + case climate::CLIMATE_MODE_COOL: + remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_COOL << 5); + break; + case climate::CLIMATE_MODE_DRY: + remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_DRY << 5); + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_FAN << 5); + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_HEAT << 5); + break; + case climate::CLIMATE_MODE_OFF: + default: + powered_on = false; + this->powered_on_assumed = powered_on; + remote_state[0] &= 0xEF; + remote_state[2] = 0x00; + break; + } + + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_LOW << 2); + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_MEDIUM << 2); + break; + case climate::CLIMATE_FAN_HIGH: + remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_HIGH << 2); + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_AUTO << 2); + break; + } + + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + remote_state[0] |= 0x02; + remote_state[4] = 0x58; + break; + case climate::CLIMATE_SWING_OFF: + default: + remote_state[0] &= 0xFD; + remote_state[4] = 0x50; + break; + } + + uint8_t crc = 0; + for (uint8_t i : remote_state) { + crc += reverse_bits(i); + } + crc = reverse_bits(uint8_t(crc & 0x0F)) >> 4; + + ESP_LOGD(TAG, "Sending noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1], + remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]); + + ESP_LOGV(TAG, "CRC: %01X", crc); + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(38000); + + // Header + data->mark(NOBLEX_HEADER_MARK); + data->space(NOBLEX_HEADER_SPACE); + // Data (sent remote_state from the MSB to the LSB) + for (uint8_t i : remote_state) { + for (int8_t j = 7; j >= 0; j--) { + if ((i == 4) & (j == 4)) { + // Header intermediate + data->mark(NOBLEX_BIT_MARK); + data->space(NOBLEX_GAP); // gap en bit 36 + } else { + data->mark(NOBLEX_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE); + } + } + } + // send crc + for (int8_t i = 3; i >= 0; i--) { + data->mark(NOBLEX_BIT_MARK); + bool bit = crc & (1 << i); + data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE); + } + // Footer + data->mark(NOBLEX_BIT_MARK); + + transmit.perform(); +} // end transmit_state() + +// Handle received IR Buffer +bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t remote_state[8] = {0}; + uint8_t crc = 0, crc_calculated = 0; + + if (!receiving_) { + // Validate header + if (data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header"); + receiving_ = true; + // Read first 36 bits + for (int i = 0; i < 5; i++) { + // Read bit + for (int j = 7; j >= 0; j--) { + if ((i == 4) & (j == 4)) { + remote_state[i] |= 1 << j; + // Header intermediate + ESP_LOGVV(TAG, "GAP"); + return false; + } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + remote_state[i] |= 1 << j; + } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); + } + + } else { + ESP_LOGV(TAG, "Header fail"); + receiving_ = false; + return false; + } + + } else { + // Read the remaining 28 bits + for (int i = 4; i < 8; i++) { + // Read bit + for (int j = 7; j >= 0; j--) { + if ((i == 4) & (j >= 4)) { + // nothing + } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + remote_state[i] |= 1 << j; + } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); + } + + // Read crc + for (int i = 3; i >= 0; i--) { + if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + crc |= 1 << i; + } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Bit %d CRC fail", i); + return false; + } + } + ESP_LOGV(TAG, "CRC %02X", crc); + + // Validate footer + if (!data.expect_mark(NOBLEX_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + receiving_ = false; + } + + for (uint8_t i : remote_state) + crc_calculated += reverse_bits(i); + crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4; + ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated); + + if (crc != crc_calculated) { + ESP_LOGV(TAG, "CRC fail"); + return false; + } + + ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1], + remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]); + + auto powered_on = false; + if ((remote_state[0] & NOBLEX_POWER) == NOBLEX_POWER) { + powered_on = true; + this->powered_on_assumed = powered_on; + } else { + powered_on = false; + this->powered_on_assumed = powered_on; + this->mode = climate::CLIMATE_MODE_OFF; + } + // powr on/off button + ESP_LOGV(TAG, "Power: %01X", powered_on); + + // Set received mode + if (powered_on_assumed) { + auto mode = (remote_state[0] & 0xE0) >> 5; + ESP_LOGV(TAG, "Mode: %02X", mode); + switch (mode) { + case IRNoblexMode::IR_NOBLEX_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case IRNoblexMode::IR_NOBLEX_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case IRNoblexMode::IR_NOBLEX_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case IRNoblexMode::IR_NOBLEX_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case IRNoblexMode::IR_NOBLEX_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + } + } + + // Set received temp + uint8_t temp = remote_state[1]; + ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); + + temp = 0x0F & reverse_bits(temp); + temp += NOBLEX_TEMP_MIN; + ESP_LOGV(TAG, "Temperature Climate: %u", temp); + this->target_temperature = temp; + + // Set received fan speed + auto fan = (remote_state[0] & 0x0C) >> 2; + ESP_LOGV(TAG, "Fan: %02X", fan); + switch (fan) { + case IRNoblexFan::IR_NOBLEX_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case IRNoblexFan::IR_NOBLEX_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case IRNoblexFan::IR_NOBLEX_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case IRNoblexFan::IR_NOBLEX_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + + // Set received swing status + if (remote_state[0] & 0x02) { + ESP_LOGV(TAG, "Swing vertical"); + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + ESP_LOGV(TAG, "Swing OFF"); + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + + for (uint8_t &i : remote_state) + i = 0; + this->publish_state(); + return true; +} // end on_receive() + +} // namespace noblex +} // namespace esphome diff --git a/esphome/components/noblex/noblex.h b/esphome/components/noblex/noblex.h new file mode 100644 index 0000000000..a8e5f41547 --- /dev/null +++ b/esphome/components/noblex/noblex.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace noblex { + +// Temperature +const uint8_t NOBLEX_TEMP_MIN = 16; // Celsius +const uint8_t NOBLEX_TEMP_MAX = 30; // Celsius + +class NoblexClimate : public climate_ir::ClimateIR { + public: + NoblexClimate() + : climate_ir::ClimateIR(NOBLEX_TEMP_MIN, NOBLEX_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + void setup() override { + climate_ir::ClimateIR::setup(); + this->powered_on_assumed = this->mode != climate::CLIMATE_MODE_OFF; + } + + // Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + // swing resets after unit powered off + if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + this->swing_mode = climate::CLIMATE_SWING_OFF; + climate_ir::ClimateIR::control(call); + } + + // used to track when to send the power toggle command. + bool powered_on_assumed; + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer. + bool on_receive(remote_base::RemoteReceiveData data) override; + bool send_swing_cmd_{false}; + bool receiving_ = false; +}; + +} // namespace noblex +} // namespace esphome diff --git a/esphome/components/npi19/__init__.py b/esphome/components/npi19/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp new file mode 100644 index 0000000000..ca1fc39943 --- /dev/null +++ b/esphome/components/npi19/npi19.cpp @@ -0,0 +1,111 @@ +#include "npi19.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace npi19 { + +static const char *const TAG = "npi19"; + +static const uint8_t READ_COMMAND = 0xAC; + +void NPI19Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up NPI19..."); + + uint16_t raw_temperature(0); + uint16_t raw_pressure(0); + i2c::ErrorCode err = this->read_(raw_temperature, raw_pressure); + if (err != i2c::ERROR_OK) { + ESP_LOGCONFIG(TAG, " I2C Communication Failed..."); + this->mark_failed(); + return; + } + + ESP_LOGCONFIG(TAG, " Success..."); +} + +void NPI19Component::dump_config() { + ESP_LOGCONFIG(TAG, "NPI19:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Raw Pressure", this->raw_pressure_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); +} + +float NPI19Component::get_setup_priority() const { return setup_priority::DATA; } + +i2c::ErrorCode NPI19Component::read_(uint16_t &raw_temperature, uint16_t &raw_pressure) { + // initiate data read from device + i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND), true); + if (w_err != i2c::ERROR_OK) { + return w_err; + } + + // read 4 bytes from senesor + uint8_t response[4] = {0x00, 0x00, 0x00, 0x00}; + i2c::ErrorCode r_err = this->read(response, 4); + + if (r_err != i2c::ERROR_OK) { + return r_err; + } + + // extract top 6 bits of first byte and all bits of second byte for pressure + raw_pressure = ((response[0] & 0x3F) << 8) | response[1]; + + // extract all bytes of 3rd byte and top 3 bits of fourth byte for temperature + raw_temperature = (response[2] << 3) | ((response[3] & 0xE0) >> 5); + + return i2c::ERROR_OK; +} + +inline float convert_temperature(uint16_t raw_temperature) { + /* + * Correspondance with Amphenol confirmed the appropriate equation for computing temperature is: + * T (°C) =(((((Th*8)+Tl)/2048)*200)-50), where Th is the high (third) byte and Tl is the low (fourth) byte. + * + * Tl is actually the upper 3 bits of the fourth data byte; the first 5 (LSBs) must be masked out. + * + * + * The NPI-19 I2C has a temperature output, however the manufacturer does + * not specify its accuracy on the published datasheet. They indicate + * that the sensor should not be used as a calibrated temperature + * reading; it’s only intended for curve fitting data during + * compensation. + */ + const float temperature_bits_span = 2048; + const float temperature_max = 150; + const float temperature_min = -50; + const float temperature_span = temperature_max - temperature_min; + + float temperature = (raw_temperature * temperature_span / temperature_bits_span) + temperature_min; + + return temperature; +} + +void NPI19Component::update() { + uint16_t raw_temperature(0); + uint16_t raw_pressure(0); + + i2c::ErrorCode err = this->read_(raw_temperature, raw_pressure); + + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "I2C Communication Failed"); + this->status_set_warning(); + return; + } + + float temperature = convert_temperature(raw_temperature); + + ESP_LOGD(TAG, "Got raw pressure=%d, temperature=%.1f°C", raw_pressure, temperature); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->raw_pressure_sensor_ != nullptr) + this->raw_pressure_sensor_->publish_state(raw_pressure); + + this->status_clear_warning(); +} + +} // namespace npi19 +} // namespace esphome diff --git a/esphome/components/npi19/npi19.h b/esphome/components/npi19/npi19.h new file mode 100644 index 0000000000..df289dffc1 --- /dev/null +++ b/esphome/components/npi19/npi19.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace npi19 { + +/// This class implements support for the npi19 pressure and temperature i2c sensors. +class NPI19Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_raw_pressure_sensor(sensor::Sensor *raw_pressure_sensor) { + this->raw_pressure_sensor_ = raw_pressure_sensor; + } + + float get_setup_priority() const override; + void setup() override; + void dump_config() override; + void update() override; + + protected: + i2c::ErrorCode read_(uint16_t &raw_temperature, uint16_t &raw_pressure); + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *raw_pressure_sensor_{nullptr}; +}; + +} // namespace npi19 +} // namespace esphome diff --git a/esphome/components/npi19/sensor.py b/esphome/components/npi19/sensor.py new file mode 100644 index 0000000000..d13e72f5f8 --- /dev/null +++ b/esphome/components/npi19/sensor.py @@ -0,0 +1,52 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@bakerkj"] +DEPENDENCIES = ["i2c"] + +npi19_ns = cg.esphome_ns.namespace("npi19") + +NPI19Component = npi19_ns.class_("NPI19Component", cg.PollingComponent, i2c.I2CDevice) + +CONF_RAW_PRESSURE = "raw_pressure" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(NPI19Component), + 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_RAW_PRESSURE): sensor.sensor_schema( + accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +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) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if raw_pressure_config := config.get(CONF_RAW_PRESSURE): + sens = await sensor.new_sensor(raw_pressure_config) + cg.add(var.set_raw_pressure_sensor(sens)) diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index ba8d3df9d8..961511fe00 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -2,7 +2,7 @@ from math import log import esphome.config_validation as cv import esphome.codegen as cg -from esphome.components import sensor +from esphome.components import sensor, resistance_sampler from esphome.const import ( CONF_CALIBRATION, CONF_REFERENCE_RESISTANCE, @@ -15,6 +15,8 @@ from esphome.const import ( UNIT_CELSIUS, ) +AUTO_LOAD = ["resistance_sampler"] + ntc_ns = cg.esphome_ns.namespace("ntc") NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor) @@ -98,7 +100,7 @@ def process_calibration(value): elif isinstance(value, list): if len(value) != 3: raise cv.Invalid( - "Steinhart–Hart Calibration must consist of exactly three values" + "Steinhart-Hart Calibration must consist of exactly three values" ) value = cv.Schema([validate_calibration_parameter])(value) a, b, c = calc_steinhart_hart(value) @@ -124,7 +126,7 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_SENSOR): cv.use_id(resistance_sampler.ResistanceSampler), cv.Required(CONF_CALIBRATION): process_calibration, } ) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 73fbfd6e90..f45cfd54f2 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -1,33 +1,36 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ABOVE, CONF_BELOW, + CONF_CYCLE, CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, - CONF_ID, CONF_ICON, + CONF_ID, CONF_MODE, + CONF_MQTT_ID, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, + CONF_OPERATION, CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, - CONF_MQTT_ID, CONF_VALUE, - CONF_OPERATION, - CONF_CYCLE, + CONF_WEB_SERVER, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, + DEVICE_CLASS_CONDUCTIVITY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_DATA_RATE, DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_DURATION, DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY_STORAGE, @@ -42,6 +45,7 @@ from esphome.const import ( DEVICE_CLASS_NITROGEN_MONOXIDE, DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_OZONE, + DEVICE_CLASS_PH, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, @@ -60,14 +64,15 @@ from esphome.const import ( DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_WATER, DEVICE_CLASS_WEIGHT, DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ @@ -77,10 +82,12 @@ DEVICE_CLASSES = [ DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, + DEVICE_CLASS_CONDUCTIVITY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_DATA_RATE, DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_DURATION, DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY_STORAGE, @@ -95,6 +102,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_NITROGEN_MONOXIDE, DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_OZONE, + DEVICE_CLASS_PH, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, @@ -113,6 +121,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_WATER, DEVICE_CLASS_WEIGHT, @@ -161,26 +170,30 @@ NUMBER_OPERATION_OPTIONS = { validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_unit_of_measurement = cv.string_strict -NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NumberStateTrigger), - } - ), - cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), - cv.Optional(CONF_ABOVE): cv.templatable(cv.float_), - cv.Optional(CONF_BELOW): cv.templatable(cv.float_), - }, - cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), - ), - cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, - cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True), - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - } +NUMBER_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NumberStateTrigger), + } + ), + cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), + cv.Optional(CONF_ABOVE): cv.templatable(cv.float_), + cv.Optional(CONF_BELOW): cv.templatable(cv.float_), + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), + ), + cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, + cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + } + ) ) _UNDEF = object() @@ -233,13 +246,16 @@ async def setup_number_core_( cg.add(trigger.set_max(template_)) await automation.build_automation(trigger, [(float, "x")], conf) - if CONF_UNIT_OF_MEASUREMENT in config: - cg.add(var.traits.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: + cg.add(var.traits.set_unit_of_measurement(unit_of_measurement)) + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.traits.set_device_class(device_class)) + + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) - if CONF_DEVICE_CLASS in config: - cg.add(var.traits.set_device_class(config[CONF_DEVICE_CLASS])) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) async def register_number( @@ -253,8 +269,8 @@ async def register_number( ) -async def new_number(config, *, min_value: float, max_value: float, step: float): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_number(config, *args, min_value: float, max_value: float, step: float): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_number( var, config, min_value=min_value, max_value=max_value, step=step ) @@ -278,15 +294,15 @@ async def number_in_range_to_code(config, condition_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(condition_id, template_arg, paren) - if CONF_ABOVE in config: - cg.add(var.set_min(config[CONF_ABOVE])) - if CONF_BELOW in config: - cg.add(var.set_max(config[CONF_BELOW])) + if (above := config.get(CONF_ABOVE)) is not None: + cg.add(var.set_min(above)) + if (below := config.get(CONF_BELOW)) is not None: + cg.add(var.set_max(below)) return var -@coroutine_with_priority(40.0) +@coroutine_with_priority(100.0) async def to_code(config): cg.add_define("USE_NUMBER") cg.add_global(number_ns.using) @@ -385,14 +401,14 @@ async def number_set_to_code(config, action_id, template_arg, args): async def number_to_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_OPERATION in config: - to_ = await cg.templatable(config[CONF_OPERATION], args, NumberOperation) + if (operation := config.get(CONF_OPERATION)) is not None: + to_ = await cg.templatable(operation, args, NumberOperation) cg.add(var.set_operation(to_)) - if CONF_CYCLE in config: - cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) - cg.add(var.set_cycle(cycle_)) - if CONF_MODE in config: - cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[config[CONF_MODE]])) - if CONF_CYCLE in config: - cg.add(var.set_cycle(config[CONF_CYCLE])) + if (cycle := config.get(CONF_CYCLE)) is not None: + template_ = await cg.templatable(cycle, args, bool) + cg.add(var.set_cycle(template_)) + if (mode := config.get(CONF_MODE)) is not None: + cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[mode])) + if (cycle := config.get(CONF_CYCLE)) is not None: + cg.add(var.set_cycle(cycle)) return var diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py new file mode 100644 index 0000000000..99a1ccd1eb --- /dev/null +++ b/esphome/components/one_wire/__init__.py @@ -0,0 +1,40 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS + +CODEOWNERS = ["@ssieb"] + +IS_PLATFORM_COMPONENT = True + +CONF_ONE_WIRE_ID = "one_wire_id" + +one_wire_ns = cg.esphome_ns.namespace("one_wire") +OneWireBus = one_wire_ns.class_("OneWireBus") +OneWireDevice = one_wire_ns.class_("OneWireDevice") + + +def one_wire_device_schema(): + """Create a schema for a 1-wire device. + + :return: The 1-wire device schema, `extend` this in your config schema. + """ + schema = cv.Schema( + { + cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus), + cv.Optional(CONF_ADDRESS): cv.hex_uint64_t, + } + ) + return schema + + +async def register_one_wire_device(var, config): + """Register an 1-wire device with the given 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! + """ + parent = await cg.get_variable(config[CONF_ONE_WIRE_ID]) + cg.add(var.set_one_wire_bus(parent)) + if (address := config.get(CONF_ADDRESS)) is not None: + cg.add(var.set_address(address)) diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp new file mode 100644 index 0000000000..131bc4fbfe --- /dev/null +++ b/esphome/components/one_wire/one_wire.cpp @@ -0,0 +1,40 @@ +#include "one_wire.h" + +namespace esphome { +namespace one_wire { + +static const char *const TAG = "one_wire"; + +const std::string &OneWireDevice::get_address_name() { + if (this->address_name_.empty()) + this->address_name_ = std::string("0x") + format_hex(this->address_); + return this->address_name_; +} + +std::string OneWireDevice::unique_id() { return "dallas-" + str_lower_case(format_hex(this->address_)); } + +bool OneWireDevice::send_command_(uint8_t cmd) { + if (!this->bus_->select(this->address_)) + return false; + this->bus_->write8(cmd); + return true; +} + +bool OneWireDevice::check_address_() { + if (this->address_ != 0) + return true; + auto devices = this->bus_->get_devices(); + if (devices.empty()) { + ESP_LOGE(TAG, "No devices, can't auto-select address"); + return false; + } + if (devices.size() > 1) { + ESP_LOGE(TAG, "More than one device, can't auto-select address"); + return false; + } + this->address_ = devices[0]; + return true; +} + +} // namespace one_wire +} // namespace esphome diff --git a/esphome/components/one_wire/one_wire.h b/esphome/components/one_wire/one_wire.h new file mode 100644 index 0000000000..bf10e4f82e --- /dev/null +++ b/esphome/components/one_wire/one_wire.h @@ -0,0 +1,44 @@ +#pragma once + +#include "one_wire_bus.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace one_wire { + +#define LOG_ONE_WIRE_DEVICE(this) \ + ESP_LOGCONFIG(TAG, " Address: %s (%s)", this->get_address_name().c_str(), \ + LOG_STR_ARG(this->bus_->get_model_str(this->address_ & 0xff))); + +class OneWireDevice { + public: + /// @brief store the address of the device + /// @param address of the device + void set_address(uint64_t address) { this->address_ = address; } + + /// @brief store the pointer to the OneWireBus to use + /// @param bus pointer to the OneWireBus object + void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; } + + /// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29". + const std::string &get_address_name(); + + std::string unique_id(); + + protected: + uint64_t address_{0}; + OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance + std::string address_name_; + + /// @brief find an address if necessary + /// should be called from setup + bool check_address_(); + + /// @brief send command on the bus + /// @param cmd command to send + bool send_command_(uint8_t cmd); +}; + +} // namespace one_wire +} // namespace esphome diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp new file mode 100644 index 0000000000..a8d29428d3 --- /dev/null +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -0,0 +1,88 @@ +#include "one_wire_bus.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace one_wire { + +static const char *const TAG = "one_wire"; + +static const uint8_t DALLAS_MODEL_DS18S20 = 0x10; +static const uint8_t DALLAS_MODEL_DS1822 = 0x22; +static const uint8_t DALLAS_MODEL_DS18B20 = 0x28; +static const uint8_t DALLAS_MODEL_DS1825 = 0x3B; +static const uint8_t DALLAS_MODEL_DS28EA00 = 0x42; + +const uint8_t ONE_WIRE_ROM_SELECT = 0x55; +const uint8_t ONE_WIRE_ROM_SEARCH = 0xF0; + +const std::vector &OneWireBus::get_devices() { return this->devices_; } + +bool IRAM_ATTR OneWireBus::select(uint64_t address) { + if (!this->reset()) + return false; + this->write8(ONE_WIRE_ROM_SELECT); + this->write64(address); + return true; +} + +void OneWireBus::search() { + this->devices_.clear(); + + this->reset_search(); + uint64_t address; + while (true) { + { + InterruptLock lock; + if (!this->reset()) { + // Reset failed or no devices present + return; + } + + this->write8(ONE_WIRE_ROM_SEARCH); + address = this->search_int(); + } + if (address == 0) + 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()); + } else { + this->devices_.push_back(address); + } + } +} + +void OneWireBus::skip() { + this->write8(0xCC); // skip ROM +} + +const LogString *OneWireBus::get_model_str(uint8_t model) { + switch (model) { + case DALLAS_MODEL_DS18S20: + return LOG_STR("DS18S20"); + case DALLAS_MODEL_DS1822: + return LOG_STR("DS1822"); + case DALLAS_MODEL_DS18B20: + return LOG_STR("DS18B20"); + case DALLAS_MODEL_DS1825: + return LOG_STR("DS1825"); + case DALLAS_MODEL_DS28EA00: + return LOG_STR("DS28EA00"); + default: + return LOG_STR("Unknown"); + } +} + +void OneWireBus::dump_devices_(const char *tag) { + if (this->devices_.empty()) { + ESP_LOGW(tag, " Found no devices!"); + } else { + ESP_LOGCONFIG(tag, " Found devices:"); + for (auto &address : this->devices_) { + ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex(address).c_str(), LOG_STR_ARG(get_model_str(address & 0xff))); + } + } +} + +} // namespace one_wire +} // namespace esphome diff --git a/esphome/components/one_wire/one_wire_bus.h b/esphome/components/one_wire/one_wire_bus.h new file mode 100644 index 0000000000..6818b17499 --- /dev/null +++ b/esphome/components/one_wire/one_wire_bus.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace one_wire { + +class OneWireBus { + public: + /** Reset the bus, should be done before all write operations. + * + * Takes approximately 1ms. + * + * @return Whether the operation was successful. + */ + virtual bool reset() = 0; + + /// Write a word to the bus. LSB first. + virtual void write8(uint8_t val) = 0; + + /// Write a 64 bit unsigned integer to the bus. LSB first. + virtual void write64(uint64_t val) = 0; + + /// Write a command to the bus that addresses all devices by skipping the ROM. + void skip(); + + /// Read an 8 bit word from the bus. + virtual uint8_t read8() = 0; + + /// Read an 64-bit unsigned integer from the bus. + virtual uint64_t read64() = 0; + + /// Select a specific address on the bus for the following command. + bool select(uint64_t address); + + /// Return the list of found devices. + const std::vector &get_devices(); + + /// Search for 1-Wire devices on the bus. + void search(); + + /// Get the description string for this model. + const LogString *get_model_str(uint8_t model); + + protected: + std::vector devices_; + + /// log the found devices + void dump_devices_(const char *tag); + + /// Reset the device search. + virtual void reset_search() = 0; + + /// Search for a 1-Wire device on the bus. Returns 0 if all devices have been found. + virtual uint64_t search_int() = 0; +}; + +} // namespace one_wire +} // namespace esphome diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py new file mode 100644 index 0000000000..dfb10137aa --- /dev/null +++ b/esphome/components/online_image/__init__.py @@ -0,0 +1,167 @@ +import logging + +from esphome import automation +import esphome.codegen as cg +from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent +from esphome.components.image import ( + CONF_USE_TRANSPARENCY, + IMAGE_TYPE, + Image_, + validate_cross_dependencies, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BUFFER_SIZE, + CONF_FORMAT, + CONF_ID, + CONF_ON_ERROR, + CONF_RESIZE, + CONF_TRIGGER_ID, + CONF_TYPE, + CONF_URL, +) + +AUTO_LOAD = ["image"] +DEPENDENCIES = ["display", "http_request"] +CODEOWNERS = ["@guillempages"] +MULTI_CONF = True + +CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" +CONF_PLACEHOLDER = "placeholder" + +_LOGGER = logging.getLogger(__name__) + +online_image_ns = cg.esphome_ns.namespace("online_image") + +ImageFormat = online_image_ns.enum("ImageFormat") + +FORMAT_PNG = "PNG" + +IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here + +OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) + +# Actions +SetUrlAction = online_image_ns.class_( + "OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage) +) +ReleaseImageAction = online_image_ns.class_( + "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) +) + +# Triggers +DownloadFinishedTrigger = online_image_ns.class_( + "DownloadFinishedTrigger", automation.Trigger.template() +) +DownloadErrorTrigger = online_image_ns.class_( + "DownloadErrorTrigger", automation.Trigger.template() +) + +ONLINE_IMAGE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(OnlineImage), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + # + # Common image options + # + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + # Not setting default here on purpose; the default depends on the image type, + # and thus will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, + # + # Online Image specific options + # + cv.Required(CONF_URL): cv.url, + cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger), + } + ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), + } + ), + } +).extend(cv.polling_component_schema("never")) + +CONFIG_SCHEMA = cv.Schema( + cv.All( + ONLINE_IMAGE_SCHEMA, + validate_cross_dependencies, + cv.require_framework_version( + # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed + # esp8266_arduino=cv.Version(2, 7, 0), + esp32_arduino=cv.Version(0, 0, 0), + esp_idf=cv.Version(4, 0, 0), + ), + ) +) + +SET_URL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(OnlineImage), + cv.Required(CONF_URL): cv.templatable(cv.url), + } +) + +RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(OnlineImage), + } +) + + +@automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA) +@automation.register_action( + "online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA +) +async def online_image_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if CONF_URL in config: + template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) + cg.add(var.set_url(template_)) + return var + + +async def to_code(config): + format = config[CONF_FORMAT] + if format in [FORMAT_PNG]: + cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") + cg.add_library("pngle", "1.0.2") + + url = config[CONF_URL] + width, height = config.get(CONF_RESIZE, (0, 0)) + transparent = config[CONF_USE_TRANSPARENCY] + + var = cg.new_Pvariable( + config[CONF_ID], + url, + width, + height, + format, + config[CONF_TYPE], + config[CONF_BUFFER_SIZE], + ) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) + + cg.add(var.set_transparency(transparent)) + + if placeholder_id := config.get(CONF_PLACEHOLDER): + placeholder = await cg.get_variable(placeholder_id) + cg.add(var.set_placeholder(placeholder)) + + for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp new file mode 100644 index 0000000000..50ec39dfcc --- /dev/null +++ b/esphome/components/online_image/image_decoder.cpp @@ -0,0 +1,44 @@ +#include "image_decoder.h" +#include "online_image.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace online_image { + +static const char *const TAG = "online_image.decoder"; + +void ImageDecoder::set_size(int width, int height) { + this->image_->resize_(width, height); + this->x_scale_ = static_cast(this->image_->buffer_width_) / width; + this->y_scale_ = static_cast(this->image_->buffer_height_) / height; +} + +void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { + auto width = std::min(this->image_->buffer_width_, static_cast(std::ceil((x + w) * this->x_scale_))); + auto height = std::min(this->image_->buffer_height_, static_cast(std::ceil((y + h) * this->y_scale_))); + for (int i = x * this->x_scale_; i < width; i++) { + for (int j = y * this->y_scale_; j < height; j++) { + this->image_->draw_pixel_(i, j, color); + } + } +} + +uint8_t *DownloadBuffer::data(size_t offset) { + if (offset > this->size_) { + ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); + return this->buffer_; + } + return this->buffer_ + offset; +} + +size_t DownloadBuffer::read(size_t len) { + this->unread_ -= len; + if (this->unread_ > 0) { + memmove(this->data(), this->data(len), this->unread_); + } + return this->unread_; +} + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h new file mode 100644 index 0000000000..908efab987 --- /dev/null +++ b/esphome/components/online_image/image_decoder.h @@ -0,0 +1,112 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" + +namespace esphome { +namespace online_image { + +class OnlineImage; + +/** + * @brief Class to abstract decoding different image formats. + */ +class ImageDecoder { + public: + /** + * @brief Construct a new Image Decoder object + * + * @param image The image to decode the stream into. + */ + ImageDecoder(OnlineImage *image) : image_(image) {} + virtual ~ImageDecoder() = default; + + /** + * @brief Initialize the decoder. + * + * @param download_size The total number of bytes that need to be download for the image. + */ + virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } + + /** + * @brief Decode a part of the image. It will try reading from the buffer. + * There is no guarantee that the whole available buffer will be read/decoded; + * the method will return the amount of bytes actually decoded, so that the + * unread content can be moved to the beginning. + * + * @param buffer The buffer to read from. + * @param size The maximum amount of bytes that can be read from the buffer. + * @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully + * decode anything, or negative in case of a decoding error. + */ + virtual int decode(uint8_t *buffer, size_t size); + + /** + * @brief Request the image to be resized once the actual dimensions are known. + * Called by the callback functions, to be able to access the parent Image class. + * + * @param width The image's width. + * @param height The image's height. + */ + void set_size(int width, int height); + + /** + * @brief Draw a rectangle on the display_buffer using the defined color. + * Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly. + * In case of binary displays, the color will be converted to binary as well. + * Called by the callback functions, to be able to access the parent Image class. + * + * @param x The left-most coordinate of the rectangle. + * @param y The top-most coordinate of the rectangle. + * @param w The width of the rectangle. + * @param h The height of the rectangle. + * @param color The color to draw the rectangle with. + */ + void draw(int x, int y, int w, int h, const Color &color); + + bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } + + protected: + OnlineImage *image_; + // Initializing to 1, to ensure it is different than initial "decoded_bytes_". + // Will be overwritten anyway once the download size is known. + uint32_t download_size_ = 1; + uint32_t decoded_bytes_ = 0; + double x_scale_ = 1.0; + double y_scale_ = 1.0; +}; + +class DownloadBuffer { + public: + DownloadBuffer(size_t size) : size_(size) { + this->buffer_ = this->allocator_.allocate(size); + this->reset(); + } + + virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + + uint8_t *data(size_t offset = 0); + + uint8_t *append() { return this->data(this->unread_); } + + size_t unread() const { return this->unread_; } + size_t size() const { return this->size_; } + size_t free_capacity() const { return this->size_ - this->unread_; } + + size_t read(size_t len); + size_t write(size_t len) { + this->unread_ += len; + return this->unread_; + } + + void reset() { this->unread_ = 0; } + + protected: + ExternalRAMAllocator allocator_; + uint8_t *buffer_; + size_t size_; + /** Total number of downloaded bytes not yet read. */ + size_t unread_; +}; + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp new file mode 100644 index 0000000000..1786809dfa --- /dev/null +++ b/esphome/components/online_image/online_image.cpp @@ -0,0 +1,277 @@ +#include "online_image.h" + +#include "esphome/core/log.h" + +static const char *const TAG = "online_image"; + +#include "image_decoder.h" + +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_image.h" +#endif + +namespace esphome { +namespace online_image { + +using image::ImageType; + +inline bool is_color_on(const Color &color) { + // This produces the most accurate monochrome conversion, but is slightly slower. + // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; + + // Approximation using fast integer computations; produces acceptable results + // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B + return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; +} + +OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, + uint32_t download_buffer_size) + : Image(nullptr, 0, 0, type), + buffer_(nullptr), + download_buffer_(download_buffer_size), + format_(format), + fixed_width_(width), + fixed_height_(height) { + this->set_url(url); +} + +void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + if (this->data_start_) { + Image::draw(x, y, display, color_on, color_off); + } else if (this->placeholder_) { + this->placeholder_->draw(x, y, display, color_on, color_off); + } +} + +void OnlineImage::release() { + if (this->buffer_) { + ESP_LOGD(TAG, "Deallocating old buffer..."); + this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); + this->data_start_ = nullptr; + this->buffer_ = nullptr; + this->width_ = 0; + this->height_ = 0; + this->buffer_width_ = 0; + this->buffer_height_ = 0; + this->end_connection_(); + } +} + +bool OnlineImage::resize_(int width_in, int height_in) { + int width = this->fixed_width_; + int height = this->fixed_height_; + if (this->auto_resize_()) { + width = width_in; + height = height_in; + if (this->width_ != width && this->height_ != height) { + this->release(); + } + } + if (this->buffer_) { + return false; + } + auto new_size = this->get_buffer_size_(width, height); + ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); + delay_microseconds_safe(2000); + this->buffer_ = this->allocator_.allocate(new_size); + if (this->buffer_) { + this->buffer_width_ = width; + this->buffer_height_ = height; + this->width_ = width; + ESP_LOGD(TAG, "New size: (%d, %d)", width, height); + } else { +#if defined(USE_ESP8266) + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + int max_block = ESP.getMaxFreeBlockSize(); +#elif defined(USE_ESP32) + int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); +#else + int max_block = -1; +#endif + ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block); + this->end_connection_(); + return false; + } + return true; +} + +void OnlineImage::update() { + if (this->decoder_) { + ESP_LOGW(TAG, "Image already being updated."); + return; + } else { + ESP_LOGI(TAG, "Updating image"); + } + + this->downloader_ = this->parent_->get(this->url_); + + if (this->downloader_ == nullptr) { + ESP_LOGE(TAG, "Download failed."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + int http_code = this->downloader_->status_code; + if (http_code == HTTP_CODE_NOT_MODIFIED) { + // Image hasn't changed on server. Skip download. + this->end_connection_(); + return; + } + if (http_code != HTTP_CODE_OK) { + ESP_LOGE(TAG, "HTTP result: %d", http_code); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + ESP_LOGD(TAG, "Starting download"); + size_t total_size = this->downloader_->content_length; + +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT + if (this->format_ == ImageFormat::PNG) { + this->decoder_ = esphome::make_unique(this); + } +#endif // ONLINE_IMAGE_PNG_SUPPORT + + if (!this->decoder_) { + ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + this->decoder_->prepare(total_size); + ESP_LOGI(TAG, "Downloading image"); +} + +void OnlineImage::loop() { + if (!this->decoder_) { + // Not decoding at the moment => nothing to do. + return; + } + if (!this->downloader_ || this->decoder_->is_finished()) { + ESP_LOGD(TAG, "Image fully downloaded"); + this->data_start_ = buffer_; + this->width_ = buffer_width_; + this->height_ = buffer_height_; + this->end_connection_(); + this->download_finished_callback_.call(); + return; + } + if (this->downloader_ == nullptr) { + ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); + return; + } + size_t available = this->download_buffer_.free_capacity(); + if (available) { + auto len = this->downloader_->read(this->download_buffer_.append(), available); + if (len > 0) { + this->download_buffer_.write(len); + auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread()); + if (fed < 0) { + ESP_LOGE(TAG, "Error when decoding image."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + this->download_buffer_.read(fed); + } + } +} + +void OnlineImage::draw_pixel_(int x, int y, Color color) { + if (!this->buffer_) { + ESP_LOGE(TAG, "Buffer not allocated!"); + return; + } + if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { + ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); + return; + } + uint32_t pos = this->get_position_(x, y); + switch (this->type_) { + case ImageType::IMAGE_TYPE_BINARY: { + const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; + const uint32_t pos = x + y * width_8; + if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { + this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u)); + } else { + this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); + } + break; + } + case ImageType::IMAGE_TYPE_GRAYSCALE: { + uint8_t gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + if (this->has_transparency()) { + if (gray == 1) { + gray = 0; + } + if (color.w < 0x80) { + gray = 1; + } + } + this->buffer_[pos] = gray; + break; + } + case ImageType::IMAGE_TYPE_RGB565: { + uint16_t col565 = display::ColorUtil::color_to_565(color); + this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(col565 & 0xFF); + if (this->has_transparency()) + this->buffer_[pos + 2] = color.w; + break; + } + case ImageType::IMAGE_TYPE_RGBA: { + this->buffer_[pos + 0] = color.r; + this->buffer_[pos + 1] = color.g; + this->buffer_[pos + 2] = color.b; + this->buffer_[pos + 3] = color.w; + break; + } + case ImageType::IMAGE_TYPE_RGB24: + default: { + if (this->has_transparency()) { + if (color.b == 1 && color.r == 0 && color.g == 0) { + color.b = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = 0; + color.b = 1; + } + } + this->buffer_[pos + 0] = color.r; + this->buffer_[pos + 1] = color.g; + this->buffer_[pos + 2] = color.b; + break; + } + } +} + +void OnlineImage::end_connection_() { + if (this->downloader_) { + this->downloader_->end(); + this->downloader_ = nullptr; + } + this->decoder_.reset(); + this->download_buffer_.reset(); +} + +bool OnlineImage::validate_url_(const std::string &url) { + if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); + return false; + } + return true; +} + +void OnlineImage::add_on_finished_callback(std::function &&callback) { + this->download_finished_callback_.add(std::move(callback)); +} + +void OnlineImage::add_on_error_callback(std::function &&callback) { + this->download_error_callback_.add(std::move(callback)); +} + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h new file mode 100644 index 0000000000..017402a088 --- /dev/null +++ b/esphome/components/online_image/online_image.h @@ -0,0 +1,190 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/components/http_request/http_request.h" +#include "esphome/components/image/image.h" + +#include "image_decoder.h" + +namespace esphome { +namespace online_image { + +using t_http_codes = enum { + HTTP_CODE_OK = 200, + HTTP_CODE_NOT_MODIFIED = 304, + HTTP_CODE_NOT_FOUND = 404, +}; + +/** + * @brief Format that the image is encoded with. + */ +enum ImageFormat { + /** Automatically detect from MIME type. Not supported yet. */ + AUTO, + /** JPEG format. Not supported yet. */ + JPEG, + /** PNG format. */ + PNG, +}; + +/** + * @brief Download an image from a given URL, and decode it using the specified decoder. + * The image will then be stored in a buffer, so that it can be re-displayed without the + * need to re-download or re-decode. + */ +class OnlineImage : public PollingComponent, + public image::Image, + public Parented { + public: + /** + * @brief Construct a new OnlineImage object. + * + * @param url URL to download the image from. + * @param width Desired width of the target image area. + * @param height Desired height of the target image area. + * @param format Format that the image is encoded in (@see ImageFormat). + * @param buffer_size Size of the buffer used to download the image. + */ + OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, + uint32_t buffer_size); + + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + + void update() override; + void loop() override; + + /** Set the URL to download the image from. */ + void set_url(const std::string &url) { + if (this->validate_url_(url)) { + this->url_ = url; + } + } + + /** + * @brief Set the image that needs to be shown as long as the downloaded image + * is not available. + * + * @param placeholder Pointer to the (@link Image) to show as placeholder. + */ + void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; } + + /** + * Release the buffer storing the image. The image will need to be downloaded again + * to be able to be displayed. + */ + void release(); + + void add_on_finished_callback(std::function &&callback); + void add_on_error_callback(std::function &&callback); + + protected: + bool validate_url_(const std::string &url); + + using Allocator = ExternalRAMAllocator; + Allocator allocator_{Allocator::Flags::ALLOW_FAILURE}; + + uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } + int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; } + + int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } + + ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } + + bool resize_(int width, int height); + + /** + * @brief Draw a pixel into the buffer. + * + * This is used by the decoder to fill the buffer that will later be displayed + * by the `draw` method. This will internally convert the supplied 32 bit RGBA + * color into the requested image storage format. + * + * @param x Horizontal pixel position. + * @param y Vertical pixel position. + * @param color 32 bit color to put into the pixel. + */ + void draw_pixel_(int x, int y, Color color); + + void end_connection_(); + + CallbackManager download_finished_callback_{}; + CallbackManager download_error_callback_{}; + + std::shared_ptr downloader_{nullptr}; + std::unique_ptr decoder_{nullptr}; + + uint8_t *buffer_; + DownloadBuffer download_buffer_; + + const ImageFormat format_; + image::Image *placeholder_{nullptr}; + + std::string url_{""}; + + /** width requested on configuration, or 0 if non specified. */ + const int fixed_width_; + /** height requested on configuration, or 0 if non specified. */ + const int fixed_height_; + /** + * Actual width of the current image. If fixed_width_ is specified, + * this will be equal to it; otherwise it will be set once the decoding + * starts and the original size is known. + * This needs to be separate from "BaseImage::get_width()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). + */ + int buffer_width_; + /** + * Actual height of the current image. If fixed_height_ is specified, + * this will be equal to it; otherwise it will be set once the decoding + * starts and the original size is known. + * This needs to be separate from "BaseImage::get_height()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). + */ + int buffer_height_; + + friend void ImageDecoder::set_size(int width, int height); + friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); +}; + +template class OnlineImageSetUrlAction : public Action { + public: + OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, url) + void play(Ts... x) override { + this->parent_->set_url(this->url_.value(x...)); + this->parent_->update(); + } + + protected: + OnlineImage *parent_; +}; + +template class OnlineImageReleaseAction : public Action { + public: + OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {} + void play(Ts... x) override { this->parent_->release(); } + + protected: + OnlineImage *parent_; +}; + +class DownloadFinishedTrigger : public Trigger<> { + public: + explicit DownloadFinishedTrigger(OnlineImage *parent) { + parent->add_on_finished_callback([this]() { this->trigger(); }); + } +}; + +class DownloadErrorTrigger : public Trigger<> { + public: + explicit DownloadErrorTrigger(OnlineImage *parent) { + parent->add_on_error_callback([this]() { this->trigger(); }); + } +}; + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp new file mode 100644 index 0000000000..c8e215a91d --- /dev/null +++ b/esphome/components/online_image/png_image.cpp @@ -0,0 +1,68 @@ +#include "png_image.h" +#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" + +static const char *const TAG = "online_image.png"; + +namespace esphome { +namespace online_image { + +/** + * @brief Callback method that will be called by the PNGLE engine when the basic + * data of the image is received (i.e. width and height); + * + * @param pngle The PNGLE object, including the context data. + * @param w The width of the image. + * @param h The height of the image. + */ +static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) { + PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); + decoder->set_size(w, h); +} + +/** + * @brief Callback method that will be called by the PNGLE engine when a chunk + * of the image is decoded. + * + * @param pngle The PNGLE object, including the context data. + * @param x The X coordinate to draw the rectangle on. + * @param y The Y coordinate to draw the rectangle on. + * @param w The width of the rectangle to draw. + * @param h The height of the rectangle to draw. + * @param rgba The color to paint the rectangle in. + */ +static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) { + 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); +} + +void PngDecoder::prepare(uint32_t download_size) { + ImageDecoder::prepare(download_size); + pngle_set_user_data(this->pngle_, this); + pngle_set_init_callback(this->pngle_, init_callback); + pngle_set_draw_callback(this->pngle_, draw_callback); +} + +int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { + if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { + ESP_LOGD(TAG, "Waiting for data"); + return 0; + } + auto fed = pngle_feed(this->pngle_, buffer, size); + if (fed < 0) { + ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_)); + } else { + this->decoded_bytes_ += fed; + } + return fed; +} + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_PNG_SUPPORT diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h new file mode 100644 index 0000000000..a928276dcc --- /dev/null +++ b/esphome/components/online_image/png_image.h @@ -0,0 +1,33 @@ +#pragma once + +#include "image_decoder.h" +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include + +namespace esphome { +namespace online_image { + +/** + * @brief Image decoder specialization for PNG images. + */ +class PngDecoder : public ImageDecoder { + public: + /** + * @brief Construct a new PNG Decoder object. + * + * @param display The image to decode the stream into. + */ + PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} + ~PngDecoder() override { pngle_destroy(this->pngle_); } + + void prepare(uint32_t download_size) override; + int HOT decode(uint8_t *buffer, size_t size) override; + + protected: + pngle_t *pngle_; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_PNG_SUPPORT diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py new file mode 100644 index 0000000000..ee19818a29 --- /dev/null +++ b/esphome/components/opentherm/__init__.py @@ -0,0 +1,59 @@ +from typing import Any + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 +from . import generate + +CODEOWNERS = ["@olegtarasov"] +MULTI_CONF = True + +CONF_IN_PIN = "in_pin" +CONF_OUT_PIN = "out_pin" +CONF_CH_ENABLE = "ch_enable" +CONF_DHW_ENABLE = "dhw_enable" +CONF_COOLING_ENABLE = "cooling_enable" +CONF_OTC_ACTIVE = "otc_active" +CONF_CH2_ACTIVE = "ch2_active" +CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" +CONF_DHW_BLOCK = "dhw_block" +CONF_SYNC_MODE = "sync_mode" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(generate.OpenthermHub), + cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_CH_ENABLE, True): cv.boolean, + cv.Optional(CONF_DHW_ENABLE, True): cv.boolean, + cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean, + cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean, + cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean, + cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, + cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, + cv.Optional(CONF_SYNC_MODE, False): cv.boolean, + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), +) + + +async def to_code(config: dict[str, Any]) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + # Set pins + in_pin = await cg.gpio_pin_expression(config[CONF_IN_PIN]) + cg.add(var.set_in_pin(in_pin)) + + out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN]) + cg.add(var.set_out_pin(out_pin)) + + non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN} + for key, value in config.items(): + if key in non_sensors: + continue + + cg.add(getattr(var, f"set_{key}")(value)) diff --git a/esphome/components/opentherm/const.py b/esphome/components/opentherm/const.py new file mode 100644 index 0000000000..1f997c5d9c --- /dev/null +++ b/esphome/components/opentherm/const.py @@ -0,0 +1,5 @@ +OPENTHERM = "opentherm" + +CONF_OPENTHERM_ID = "opentherm_id" + +SENSOR = "sensor" diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py new file mode 100644 index 0000000000..6a97835a57 --- /dev/null +++ b/esphome/components/opentherm/generate.py @@ -0,0 +1,140 @@ +from collections.abc import Awaitable +from typing import Any, Callable + +import esphome.codegen as cg +from esphome.const import CONF_ID +from . import const +from .schema import TSchema + +opentherm_ns = cg.esphome_ns.namespace("opentherm") +OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) + + +def define_has_component(component_type: str, keys: list[str]) -> None: + cg.add_define( + f"OPENTHERM_{component_type.upper()}_LIST(F, sep)", + cg.RawExpression( + " sep ".join(map(lambda key: f"F({key}_{component_type.lower()})", keys)) + ), + ) + for key in keys: + cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}") + + +def define_message_handler( + component_type: str, keys: list[str], schemas: dict[str, TSchema] +) -> None: + # The macros defined here should be able to generate things like this: + # // Parsing a message and publishing to sensors + # case MessageId::Message: + # // Can have multiple sensors here, for example for a Status message with multiple flags + # this->thing_binary_sensor->publish_state(parse_flag8_lb_0(response)); + # this->other_binary_sensor->publish_state(parse_flag8_lb_1(response)); + # break; + # // Building a message for a write request + # case MessageId::Message: { + # unsigned int data = 0; + # data = write_flag8_lb_0(some_input_switch->state, data); // Where input_sensor can also be a number/output/switch + # data = write_u8_hb(some_number->state, data); + # return opentherm_->build_request_(MessageType::WriteData, MessageId::Message, data); + # } + + messages: dict[str, list[tuple[str, str]]] = {} + for key in keys: + msg = schemas[key].message + if msg not in messages: + messages[msg] = [] + messages[msg].append((key, schemas[key].message_data)) + + cg.add_define( + f"OPENTHERM_{component_type.upper()}_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)", + cg.RawExpression( + " msg_sep ".join( + [ + f"MESSAGE({msg}) " + + " entity_sep ".join( + [ + f"ENTITY({key}_{component_type.lower()}, {msg_data})" + for key, msg_data in keys + ] + ) + + " postscript" + for msg, keys in messages.items() + ] + ) + ), + ) + + +def define_readers(component_type: str, keys: list[str]) -> None: + for key in keys: + cg.add_define( + f"OPENTHERM_READ_{key}", + cg.RawExpression(f"this->{key}_{component_type.lower()}->state"), + ) + + +def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): + messages: set[tuple[str, bool]] = set() + for key in keys: + messages.add((schemas[key].message, schemas[key].keep_updated)) + for msg, keep_updated in messages: + msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}") + if keep_updated: + cg.add(hub.add_repeating_message(msg_expr)) + else: + cg.add(hub.add_initial_message(msg_expr)) + + +def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None: + if config_key in config: + cg.add(getattr(var, f"set_{config_key}")(config[config_key])) + + +Create = Callable[[dict[str, Any], str, cg.MockObj], Awaitable[cg.Pvariable]] + + +def create_only_conf( + create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]] +) -> Create: + return lambda conf, _key, _hub: create(conf) + + +async def component_to_code( + component_type: str, + schemas: dict[str, TSchema], + type: cg.MockObjClass, + create: Create, + config: dict[str, Any], +) -> list[str]: + """Generate the code for each configured component in the schema of a component type. + + Parameters: + - component_type: The type of component, e.g. "sensor" or "binary_sensor" + - schema_: The schema for that component type, a list of available components + - type: The type of the component, e.g. sensor.Sensor or OpenthermOutput + - create: A constructor function for the component, which receives the config, + the key and the hub and should asynchronously return the new component + - config: The configuration for this component type + + Returns: The list of keys for the created components + """ + cg.add_define(f"OPENTHERM_USE_{component_type.upper()}") + + hub = await cg.get_variable(config[const.CONF_OPENTHERM_ID]) + + keys: list[str] = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf[CONF_ID] + if id and id.type == type: + entity = await create(conf, key, hub) + cg.add(getattr(hub, f"set_{key}_{component_type.lower()}")(entity)) + keys.append(key) + + define_has_component(component_type, keys) + define_message_handler(component_type, keys, schemas) + add_messages(hub, keys, schemas) + + return keys diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp new file mode 100644 index 0000000000..770bbd82b7 --- /dev/null +++ b/esphome/components/opentherm/hub.cpp @@ -0,0 +1,343 @@ +#include "hub.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace opentherm { + +static const char *const TAG = "opentherm"; +namespace message_data { +bool parse_flag8_lb_0(OpenthermData &data) { return read_bit(data.valueLB, 0); } +bool parse_flag8_lb_1(OpenthermData &data) { return read_bit(data.valueLB, 1); } +bool parse_flag8_lb_2(OpenthermData &data) { return read_bit(data.valueLB, 2); } +bool parse_flag8_lb_3(OpenthermData &data) { return read_bit(data.valueLB, 3); } +bool parse_flag8_lb_4(OpenthermData &data) { return read_bit(data.valueLB, 4); } +bool parse_flag8_lb_5(OpenthermData &data) { return read_bit(data.valueLB, 5); } +bool parse_flag8_lb_6(OpenthermData &data) { return read_bit(data.valueLB, 6); } +bool parse_flag8_lb_7(OpenthermData &data) { return read_bit(data.valueLB, 7); } +bool parse_flag8_hb_0(OpenthermData &data) { return read_bit(data.valueHB, 0); } +bool parse_flag8_hb_1(OpenthermData &data) { return read_bit(data.valueHB, 1); } +bool parse_flag8_hb_2(OpenthermData &data) { return read_bit(data.valueHB, 2); } +bool parse_flag8_hb_3(OpenthermData &data) { return read_bit(data.valueHB, 3); } +bool parse_flag8_hb_4(OpenthermData &data) { return read_bit(data.valueHB, 4); } +bool parse_flag8_hb_5(OpenthermData &data) { return read_bit(data.valueHB, 5); } +bool parse_flag8_hb_6(OpenthermData &data) { return read_bit(data.valueHB, 6); } +bool parse_flag8_hb_7(OpenthermData &data) { return read_bit(data.valueHB, 7); } +uint8_t parse_u8_lb(OpenthermData &data) { return data.valueLB; } +uint8_t parse_u8_hb(OpenthermData &data) { return data.valueHB; } +int8_t parse_s8_lb(OpenthermData &data) { return (int8_t) data.valueLB; } +int8_t parse_s8_hb(OpenthermData &data) { return (int8_t) data.valueHB; } +uint16_t parse_u16(OpenthermData &data) { return data.u16(); } +int16_t parse_s16(OpenthermData &data) { return data.s16(); } +float parse_f88(OpenthermData &data) { return data.f88(); } + +void write_flag8_lb_0(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 0, value); } +void write_flag8_lb_1(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 1, value); } +void write_flag8_lb_2(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 2, value); } +void write_flag8_lb_3(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 3, value); } +void write_flag8_lb_4(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 4, value); } +void write_flag8_lb_5(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 5, value); } +void write_flag8_lb_6(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 6, value); } +void write_flag8_lb_7(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 7, value); } +void write_flag8_hb_0(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 0, value); } +void write_flag8_hb_1(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 1, value); } +void write_flag8_hb_2(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 2, value); } +void write_flag8_hb_3(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 3, value); } +void write_flag8_hb_4(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 4, value); } +void write_flag8_hb_5(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 5, value); } +void write_flag8_hb_6(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 6, value); } +void write_flag8_hb_7(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 7, value); } +void write_u8_lb(const uint8_t value, OpenthermData &data) { data.valueLB = value; } +void write_u8_hb(const uint8_t value, OpenthermData &data) { data.valueHB = value; } +void write_s8_lb(const int8_t value, OpenthermData &data) { data.valueLB = (uint8_t) value; } +void write_s8_hb(const int8_t value, OpenthermData &data) { data.valueHB = (uint8_t) value; } +void write_u16(const uint16_t value, OpenthermData &data) { data.u16(value); } +void write_s16(const int16_t value, OpenthermData &data) { data.s16(value); } +void write_f88(const float value, OpenthermData &data) { data.f88(value); } + +} // namespace message_data + +OpenthermData OpenthermHub::build_request_(MessageId request_id) const { + OpenthermData data; + data.type = 0; + data.id = 0; + data.valueHB = 0; + data.valueLB = 0; + + // We need this special logic for STATUS message because we have two options for specifying boiler modes: + // with static config values in the hub, or with separate switches. + if (request_id == MessageId::STATUS) { + // NOLINTBEGIN + bool const ch_enabled = this->ch_enable && OPENTHERM_READ_ch_enable && OPENTHERM_READ_t_set > 0.0; + bool const dhw_enabled = this->dhw_enable && OPENTHERM_READ_dhw_enable; + bool const cooling_enabled = + this->cooling_enable && OPENTHERM_READ_cooling_enable && OPENTHERM_READ_cooling_control > 0.0; + bool const otc_enabled = this->otc_active && OPENTHERM_READ_otc_active; + bool const ch2_enabled = this->ch2_active && OPENTHERM_READ_ch2_active && OPENTHERM_READ_t_set_ch2 > 0.0; + bool const summer_mode_is_active = this->summer_mode_active && OPENTHERM_READ_summer_mode_active; + bool const dhw_blocked = this->dhw_block && OPENTHERM_READ_dhw_block; + // NOLINTEND + + data.type = MessageType::READ_DATA; + data.id = MessageId::STATUS; + data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) | + (summer_mode_is_active << 5) | (dhw_blocked << 6); + + return data; + } + +// Disable incomplete switch statement warnings, because the cases in each +// switch are generated based on the configured sensors and inputs. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" + + switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) } + +#pragma GCC diagnostic pop + + // And if we get here, a message was requested which somehow wasn't handled. + // This shouldn't happen due to the way the defines are configured, so we + // log an error and just return a 0 message. + ESP_LOGE(TAG, "Tried to create a request with unknown id %d. This should never happen, so please open an issue.", + request_id); + return {}; +} + +OpenthermHub::OpenthermHub() : Component(), in_pin_{}, out_pin_{} {} + +void OpenthermHub::process_response(OpenthermData &data) { + ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id, + this->opentherm_->message_id_to_str((MessageId) data.id)); + ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str()); + + switch (data.id) { + OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, , + OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, ) + } +} + +void OpenthermHub::setup() { + ESP_LOGD(TAG, "Setting up OpenTherm component"); + this->opentherm_ = make_unique(this->in_pin_, this->out_pin_); + if (!this->opentherm_->initialize()) { + ESP_LOGE(TAG, "Failed to initialize OpenTherm protocol. See previous log messages for details."); + this->mark_failed(); + return; + } + + // Ensure that there is at least one request, as we are required to + // communicate at least once every second. Sending the status request is + // good practice anyway. + this->add_repeating_message(MessageId::STATUS); + + this->current_message_iterator_ = this->initial_messages_.begin(); +} + +void OpenthermHub::on_shutdown() { this->opentherm_->stop(); } + +void OpenthermHub::loop() { + if (this->sync_mode_) { + this->sync_loop_(); + return; + } + + auto cur_time = millis(); + auto const cur_mode = this->opentherm_->get_mode(); + switch (cur_mode) { + case OperationMode::WRITE: + case OperationMode::READ: + case OperationMode::LISTEN: + if (!this->check_timings_(cur_time)) { + break; + } + this->last_mode_ = cur_mode; + break; + case OperationMode::ERROR_PROTOCOL: + if (this->last_mode_ == OperationMode::WRITE) { + this->handle_protocol_write_error_(); + } else if (this->last_mode_ == OperationMode::READ) { + this->handle_protocol_read_error_(); + } + + this->stop_opentherm_(); + break; + case OperationMode::ERROR_TIMEOUT: + this->handle_timeout_error_(); + this->stop_opentherm_(); + break; + case OperationMode::IDLE: + if (this->should_skip_loop_(cur_time)) { + break; + } + this->start_conversation_(); + break; + case OperationMode::SENT: + // Message sent, now listen for the response. + this->opentherm_->listen(); + break; + case OperationMode::RECEIVED: + this->read_response_(); + break; + } +} + +void OpenthermHub::sync_loop_() { + if (!this->opentherm_->is_idle()) { + ESP_LOGE(TAG, "OpenTherm is not idle at the start of the loop"); + return; + } + + auto cur_time = millis(); + + this->check_timings_(cur_time); + + if (this->should_skip_loop_(cur_time)) { + return; + } + + this->start_conversation_(); + + if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { + ESP_LOGE(TAG, "Hub timeout triggered during send"); + this->stop_opentherm_(); + return; + } + + if (this->opentherm_->is_error()) { + this->handle_protocol_write_error_(); + this->stop_opentherm_(); + return; + } else if (!this->opentherm_->is_sent()) { + ESP_LOGW(TAG, "Unexpected state after sending request: %s", + this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); + this->stop_opentherm_(); + return; + } + + // Listen for the response + this->opentherm_->listen(); + if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { + ESP_LOGE(TAG, "Hub timeout triggered during receive"); + this->stop_opentherm_(); + return; + } + + if (this->opentherm_->is_timeout()) { + this->handle_timeout_error_(); + this->stop_opentherm_(); + return; + } else if (this->opentherm_->is_protocol_error()) { + this->handle_protocol_read_error_(); + this->stop_opentherm_(); + return; + } else if (!this->opentherm_->has_message()) { + ESP_LOGW(TAG, "Unexpected state after receiving response: %s", + this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); + this->stop_opentherm_(); + return; + } + + this->read_response_(); +} + +bool OpenthermHub::check_timings_(uint32_t cur_time) { + if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) { + ESP_LOGW(TAG, + "%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other " + "components that might slow the loop down.", + (int) (cur_time - this->last_conversation_start_)); + this->stop_opentherm_(); + return false; + } + + return true; +} + +bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { + if (this->last_conversation_end_ > 0 && (cur_time - this->last_conversation_end_) < 100) { + ESP_LOGV(TAG, "Less than 100 ms elapsed since last convo, skipping this iteration"); + return true; + } + + return false; +} + +void OpenthermHub::start_conversation_() { + if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) { + this->sending_initial_ = false; + this->current_message_iterator_ = this->repeating_messages_.begin(); + } else if (this->current_message_iterator_ == this->repeating_messages_.end()) { + this->current_message_iterator_ = this->repeating_messages_.begin(); + } + + auto request = this->build_request_(*this->current_message_iterator_); + + ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id, + this->opentherm_->message_id_to_str((MessageId) request.id)); + ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(request).c_str()); + // Send the request + this->last_conversation_start_ = millis(); + this->opentherm_->send(request); +} + +void OpenthermHub::read_response_() { + OpenthermData response; + if (!this->opentherm_->get_message(response)) { + ESP_LOGW(TAG, "Couldn't get the response, but flags indicated success. This is a bug."); + this->stop_opentherm_(); + return; + } + + this->stop_opentherm_(); + + this->process_response(response); + + this->current_message_iterator_++; +} + +void OpenthermHub::stop_opentherm_() { + this->opentherm_->stop(); + this->last_conversation_end_ = millis(); +} + +void OpenthermHub::handle_protocol_write_error_() { + ESP_LOGW(TAG, "Error while sending request: %s", + this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); + ESP_LOGW(TAG, "%s", this->opentherm_->debug_data(this->last_request_).c_str()); +} + +void OpenthermHub::handle_protocol_read_error_() { + OpenThermError error; + this->opentherm_->get_protocol_error(error); + ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", this->opentherm_->debug_error(error).c_str()); +} + +void OpenthermHub::handle_timeout_error_() { + ESP_LOGW(TAG, "Receive response timed out at a protocol level"); + this->stop_opentherm_(); +} + +void OpenthermHub::dump_config() { + ESP_LOGCONFIG(TAG, "OpenTherm:"); + LOG_PIN(" In: ", this->in_pin_); + LOG_PIN(" Out: ", this->out_pin_); + ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_); + ESP_LOGCONFIG(TAG, " Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, ))); + ESP_LOGCONFIG(TAG, " Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, ))); + ESP_LOGCONFIG(TAG, " Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, ))); + ESP_LOGCONFIG(TAG, " Input sensors: %s", SHOW(OPENTHERM_INPUT_SENSOR_LIST(ID, ))); + ESP_LOGCONFIG(TAG, " Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, ))); + ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); + ESP_LOGCONFIG(TAG, " Initial requests:"); + for (auto type : this->initial_messages_) { + ESP_LOGCONFIG(TAG, " - %d", type); + } + ESP_LOGCONFIG(TAG, " Repeating requests:"); + for (auto type : this->repeating_messages_) { + ESP_LOGCONFIG(TAG, " - %d", type); + } +} + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h new file mode 100644 index 0000000000..3b90cdf427 --- /dev/null +++ b/esphome/components/opentherm/hub.h @@ -0,0 +1,123 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/core/component.h" +#include "esphome/core/log.h" + +#include "opentherm.h" + +#ifdef OPENTHERM_USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#include +#include +#include +#include + +#include "opentherm_macros.h" + +namespace esphome { +namespace opentherm { + +// OpenTherm component for ESPHome +class OpenthermHub : public Component { + protected: + // Communication pins for the OpenTherm interface + InternalGPIOPin *in_pin_, *out_pin_; + // The OpenTherm interface + std::unique_ptr opentherm_; + + OPENTHERM_SENSOR_LIST(OPENTHERM_DECLARE_SENSOR, ) + + // The set of initial messages to send on starting communication with the boiler + std::unordered_set initial_messages_; + // and the repeating messages which are sent repeatedly to update various sensors + // and boiler parameters (like the setpoint). + std::unordered_set repeating_messages_; + // Indicates if we are still working on the initial requests or not + bool sending_initial_ = true; + // Index for the current request in one of the _requests sets. + std::unordered_set::const_iterator current_message_iterator_; + + uint32_t last_conversation_start_ = 0; + uint32_t last_conversation_end_ = 0; + OperationMode last_mode_ = IDLE; + OpenthermData last_request_; + + // Synchronous communication mode prevents other components from disabling interrupts while + // we are talking to the boiler. Enable if you experience random intermittent invalid response errors. + // Very likely to happen while using Dallas temperature sensors. + bool sync_mode_ = false; + + // Create OpenTherm messages based on the message id + OpenthermData build_request_(MessageId request_id) const; + void handle_protocol_write_error_(); + void handle_protocol_read_error_(); + void handle_timeout_error_(); + void stop_opentherm_(); + void start_conversation_(); + void read_response_(); + bool check_timings_(uint32_t cur_time); + bool should_skip_loop_(uint32_t cur_time) const; + void sync_loop_(); + + template bool spin_wait_(uint32_t timeout, F func) { + auto start_time = millis(); + while (func()) { + yield(); + auto cur_time = millis(); + if (cur_time - start_time >= timeout) { + return false; + } + } + return true; + } + + public: + // Constructor with references to the global interrupt handlers + OpenthermHub(); + + // Handle responses from the OpenTherm interface + void process_response(OpenthermData &data); + + // Setters for the input and output OpenTherm interface pins + void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; } + void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; } + + OPENTHERM_SENSOR_LIST(OPENTHERM_SET_SENSOR, ) + + // Add a request to the set of initial requests + void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); } + // Add a request to the set of repeating requests. Note that a large number of repeating + // requests will slow down communication with the boiler. Each request may take up to 1 second, + // so with all sensors enabled, it may take about half a minute before a change in setpoint + // will be processed. + void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); } + + // There are seven status variables, which can either be set as a simple variable, + // or using a switch. ch_enable and dhw_enable default to true, the others to false. + bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false, + summer_mode_active = false, dhw_block = false; + + // Setters for the status variables + void set_ch_enable(bool value) { this->ch_enable = value; } + void set_dhw_enable(bool value) { this->dhw_enable = value; } + void set_cooling_enable(bool value) { this->cooling_enable = value; } + void set_otc_active(bool value) { this->otc_active = value; } + void set_ch2_active(bool value) { this->ch2_active = value; } + void set_summer_mode_active(bool value) { this->summer_mode_active = value; } + void set_dhw_block(bool value) { this->dhw_block = value; } + void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void setup() override; + void on_shutdown() override; + void loop() override; + void dump_config() override; +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp new file mode 100644 index 0000000000..4a23bb94cf --- /dev/null +++ b/esphome/components/opentherm/opentherm.cpp @@ -0,0 +1,571 @@ +/* + * OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but + * heavily modified to comply with ESPHome coding standards and provide better logging. + * Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International + * Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project. + */ + +#include "opentherm.h" +#include "esphome/core/helpers.h" +#if defined(ESP32) || defined(USE_ESP_IDF) +#include "driver/timer.h" +#include "esp_err.h" +#endif +#ifdef ESP8266 +#include "Arduino.h" +#endif +#include +#include +#include + +namespace esphome { +namespace opentherm { + +using std::string; +using std::bitset; +using std::stringstream; +using std::to_string; + +static const char *const TAG = "opentherm"; + +#ifdef ESP8266 +OpenTherm *OpenTherm::instance_ = nullptr; +#endif + +OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout) + : in_pin_(in_pin), + out_pin_(out_pin), +#if defined(ESP32) || defined(USE_ESP_IDF) + timer_group_(TIMER_GROUP_0), + timer_idx_(TIMER_0), +#endif + mode_(OperationMode::IDLE), + error_type_(ProtocolErrorType::NO_ERROR), + capture_(0), + clock_(0), + data_(0), + bit_pos_(0), + timeout_counter_(-1), + device_timeout_(device_timeout) { + this->isr_in_pin_ = in_pin->to_isr(); + this->isr_out_pin_ = out_pin->to_isr(); +} + +bool OpenTherm::initialize() { +#ifdef ESP8266 + OpenTherm::instance_ = this; +#endif + this->in_pin_->pin_mode(gpio::FLAG_INPUT); + this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->out_pin_->digital_write(true); + +#if defined(ESP32) || defined(USE_ESP_IDF) + return this->init_esp32_timer_(); +#else + return true; +#endif +} + +void OpenTherm::listen() { + this->stop_timer_(); + this->timeout_counter_ = this->device_timeout_ * 5; // timer_ ticks at 5 ticks/ms + + this->mode_ = OperationMode::LISTEN; + this->data_ = 0; + this->bit_pos_ = 0; + + this->start_read_timer_(); +} + +void OpenTherm::send(OpenthermData &data) { + this->stop_timer_(); + this->data_ = data.type; + this->data_ = (this->data_ << 12) | data.id; + this->data_ = (this->data_ << 8) | data.valueHB; + this->data_ = (this->data_ << 8) | data.valueLB; + if (!check_parity_(this->data_)) { + this->data_ = this->data_ | 0x80000000; + } + + this->clock_ = 1; // clock starts at HIGH + this->bit_pos_ = 33; // count down (33 == start bit, 32-1 data, 0 == stop bit) + this->mode_ = OperationMode::WRITE; + + this->start_write_timer_(); +} + +bool OpenTherm::get_message(OpenthermData &data) { + if (this->mode_ == OperationMode::RECEIVED) { + data.type = (this->data_ >> 28) & 0x7; + data.id = (this->data_ >> 16) & 0xFF; + data.valueHB = (this->data_ >> 8) & 0xFF; + data.valueLB = this->data_ & 0xFF; + return true; + } + return false; +} + +bool OpenTherm::get_protocol_error(OpenThermError &error) { + if (this->mode_ != OperationMode::ERROR_PROTOCOL) { + return false; + } + + error.error_type = this->error_type_; + error.bit_pos = this->bit_pos_; + error.capture = this->capture_; + error.clock = this->clock_; + error.data = this->data_; + + return true; +} + +void OpenTherm::stop() { + this->stop_timer_(); + this->mode_ = OperationMode::IDLE; +} + +void IRAM_ATTR OpenTherm::read_() { + this->data_ = 0; + this->bit_pos_ = 0; + this->mode_ = OperationMode::READ; + this->capture_ = 1; // reset counter and add as if read start bit + this->clock_ = 1; // clock is high at the start of comm + this->start_read_timer_(); // get us into 1/4 of manchester code. 5 timer ticks constitute 1 ms, which is 1 bit + // period in OpenTherm. +} + +bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) { + if (arg->mode_ == OperationMode::LISTEN) { + if (arg->timeout_counter_ == 0) { + arg->mode_ = OperationMode::ERROR_TIMEOUT; + arg->stop_timer_(); + return false; + } + bool const value = arg->isr_in_pin_.digital_read(); + if (value) { // incoming data (rising signal) + arg->read_(); + } + if (arg->timeout_counter_ > 0) { + arg->timeout_counter_--; + } + } else if (arg->mode_ == OperationMode::READ) { + bool const value = arg->isr_in_pin_.digital_read(); + uint8_t const last = (arg->capture_ & 1); + if (value != last) { + // transition of signal from last sampling + if (arg->clock_ == 1 && arg->capture_ > 0xF) { + // no transition in the middle of the bit + arg->mode_ = OperationMode::ERROR_PROTOCOL; + arg->error_type_ = ProtocolErrorType::NO_TRANSITION; + arg->stop_timer_(); + return false; + } else if (arg->clock_ == 1 || arg->capture_ > 0xF) { + // transition in the middle of the bit OR no transition between two bit, both are valid data points + if (arg->bit_pos_ == BitPositions::STOP_BIT) { + // expecting stop bit + auto stop_bit_error = arg->verify_stop_bit_(last); + if (stop_bit_error == ProtocolErrorType::NO_ERROR) { + arg->mode_ = OperationMode::RECEIVED; + arg->stop_timer_(); + return false; + } else { + // end of data not verified, invalid data + arg->mode_ = OperationMode::ERROR_PROTOCOL; + arg->error_type_ = stop_bit_error; + arg->stop_timer_(); + return false; + } + } else { + // normal data point at clock high + arg->bit_read_(last); + arg->clock_ = 0; + } + } else { + // clock low, not a data point, switch clock + arg->clock_ = 1; + } + arg->capture_ = 1; // reset counter + } else if (arg->capture_ > 0xFF) { + // no change for too long, invalid mancheter encoding + arg->mode_ = OperationMode::ERROR_PROTOCOL; + arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; + arg->stop_timer_(); + return false; + } + arg->capture_ = (arg->capture_ << 1) | value; + } else if (arg->mode_ == OperationMode::WRITE) { + // write data to pin + if (arg->bit_pos_ == 33 || arg->bit_pos_ == 0) { // start bit + arg->write_bit_(1, arg->clock_); + } else { // data bits + arg->write_bit_(read_bit(arg->data_, arg->bit_pos_ - 1), arg->clock_); + } + if (arg->clock_ == 0) { + if (arg->bit_pos_ <= 0) { // check termination + arg->mode_ = OperationMode::SENT; // all data written + arg->stop_timer_(); + } + arg->bit_pos_--; + arg->clock_ = 1; + } else { + arg->clock_ = 0; + } + } + + return false; +} + +#ifdef ESP8266 +void IRAM_ATTR OpenTherm::esp8266_timer_isr() { OpenTherm::timer_isr(OpenTherm::instance_); } +#endif + +void IRAM_ATTR OpenTherm::bit_read_(uint8_t value) { + this->data_ = (this->data_ << 1) | value; + this->bit_pos_++; +} + +ProtocolErrorType OpenTherm::verify_stop_bit_(uint8_t value) { + if (value) { // stop bit detected + return check_parity_(this->data_) ? ProtocolErrorType::NO_ERROR : ProtocolErrorType::PARITY_ERROR; + } else { // no stop bit detected, error + return ProtocolErrorType::INVALID_STOP_BIT; + } +} + +void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) { + if (clock == 1) { // left part of manchester encoding + this->isr_out_pin_.digital_write(!high); // low means logical 1 to protocol + } else { // right part of manchester encoding + this->isr_out_pin_.digital_write(high); // high means logical 0 to protocol + } +} + +#if defined(ESP32) || defined(USE_ESP_IDF) + +bool OpenTherm::init_esp32_timer_() { + // Search for a free timer. Maybe unstable, we'll see. + int cur_timer = 0; + timer_group_t timer_group = TIMER_GROUP_0; + timer_idx_t timer_idx = TIMER_0; + bool timer_found = false; + + for (; cur_timer < SOC_TIMER_GROUP_TOTAL_TIMERS; cur_timer++) { + timer_config_t temp_config; + timer_group = cur_timer < 2 ? TIMER_GROUP_0 : TIMER_GROUP_1; + timer_idx = cur_timer < 2 ? (timer_idx_t) cur_timer : (timer_idx_t) (cur_timer - 2); + + auto err = timer_get_config(timer_group, timer_idx, &temp_config); + if (err == ESP_ERR_INVALID_ARG) { + // Error means timer was not initialized (or other things, but we are careful with our args) + timer_found = true; + break; + } + + ESP_LOGD(TAG, "Timer %d:%d seems to be occupied, will try another", timer_group, timer_idx); + } + + if (!timer_found) { + ESP_LOGE(TAG, "No free timer was found! OpenTherm cannot function without a timer."); + return false; + } + + ESP_LOGD(TAG, "Found free timer %d:%d", timer_group, timer_idx); + this->timer_group_ = timer_group; + this->timer_idx_ = timer_idx; + + timer_config_t const config = { + .alarm_en = TIMER_ALARM_EN, + .counter_en = TIMER_PAUSE, + .intr_type = TIMER_INTR_LEVEL, + .counter_dir = TIMER_COUNT_UP, + .auto_reload = TIMER_AUTORELOAD_EN, +#if ESP_IDF_VERSION_MAJOR >= 5 + .clk_src = TIMER_SRC_CLK_DEFAULT, +#endif + .divider = 80, +#if defined(SOC_TIMER_GROUP_SUPPORT_XTAL) && ESP_IDF_VERSION_MAJOR < 5 + .clk_src = TIMER_SRC_CLK_APB +#endif + }; + + esp_err_t result; + + result = timer_init(this->timer_group_, this->timer_idx_, &config); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to init timer. Error: %s", error); + return false; + } + + result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to set counter value. Error: %s", error); + return false; + } + + result = timer_isr_callback_add(this->timer_group_, this->timer_idx_, reinterpret_cast(timer_isr), + this, 0); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to register timer interrupt. Error: %s", error); + return false; + } + + return true; +} + +void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { + esp_err_t result; + + result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error); + return; + } + + result = timer_start(this->timer_group_, this->timer_idx_); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error); + return; + } +} + +// 5 kHz timer_ +void IRAM_ATTR OpenTherm::start_read_timer_() { + InterruptLock const lock; + this->start_esp32_timer_(200); +} + +// 2 kHz timer_ +void IRAM_ATTR OpenTherm::start_write_timer_() { + InterruptLock const lock; + this->start_esp32_timer_(500); +} + +void IRAM_ATTR OpenTherm::stop_timer_() { + InterruptLock const lock; + + esp_err_t result; + + result = timer_pause(this->timer_group_, this->timer_idx_); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error); + return; + } + + result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error); + return; + } +} + +#endif // END ESP32 + +#ifdef ESP8266 +// 5 kHz timer_ +void OpenTherm::start_read_timer_() { + InterruptLock const lock; + timer1_attachInterrupt(OpenTherm::esp8266_timer_isr); + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max) + timer1_write(1000); // 5kHz +} + +// 2 kHz timer_ +void OpenTherm::start_write_timer_() { + InterruptLock const lock; + timer1_attachInterrupt(OpenTherm::esp8266_timer_isr); + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max) + timer1_write(2500); // 2kHz +} + +void OpenTherm::stop_timer_() { + InterruptLock const lock; + timer1_disable(); + timer1_detachInterrupt(); +} + +#endif // END ESP8266 + +// https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd +bool OpenTherm::check_parity_(uint32_t val) { + val ^= val >> 16; + val ^= val >> 8; + val ^= val >> 4; + val ^= val >> 2; + val ^= val >> 1; + return (~val) & 1; +} + +#define TO_STRING_MEMBER(name) \ + case name: \ + return #name; + +const char *OpenTherm::operation_mode_to_str(OperationMode mode) { + switch (mode) { + TO_STRING_MEMBER(IDLE) + TO_STRING_MEMBER(LISTEN) + TO_STRING_MEMBER(READ) + TO_STRING_MEMBER(RECEIVED) + TO_STRING_MEMBER(WRITE) + TO_STRING_MEMBER(SENT) + TO_STRING_MEMBER(ERROR_PROTOCOL) + TO_STRING_MEMBER(ERROR_TIMEOUT) + default: + return ""; + } +} +const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { + switch (error_type) { + TO_STRING_MEMBER(NO_ERROR) + TO_STRING_MEMBER(NO_TRANSITION) + TO_STRING_MEMBER(INVALID_STOP_BIT) + TO_STRING_MEMBER(PARITY_ERROR) + TO_STRING_MEMBER(NO_CHANGE_TOO_LONG) + default: + return ""; + } +} +const char *OpenTherm::message_type_to_str(MessageType message_type) { + switch (message_type) { + TO_STRING_MEMBER(READ_DATA) + TO_STRING_MEMBER(READ_ACK) + TO_STRING_MEMBER(WRITE_DATA) + TO_STRING_MEMBER(WRITE_ACK) + TO_STRING_MEMBER(INVALID_DATA) + TO_STRING_MEMBER(DATA_INVALID) + TO_STRING_MEMBER(UNKNOWN_DATAID) + default: + return ""; + } +} + +const char *OpenTherm::message_id_to_str(MessageId id) { + switch (id) { + TO_STRING_MEMBER(STATUS) + TO_STRING_MEMBER(CH_SETPOINT) + TO_STRING_MEMBER(CONTROLLER_CONFIG) + TO_STRING_MEMBER(DEVICE_CONFIG) + TO_STRING_MEMBER(COMMAND_CODE) + TO_STRING_MEMBER(FAULT_FLAGS) + TO_STRING_MEMBER(REMOTE) + TO_STRING_MEMBER(COOLING_CONTROL) + TO_STRING_MEMBER(CH2_SETPOINT) + TO_STRING_MEMBER(CH_SETPOINT_OVERRIDE) + TO_STRING_MEMBER(TSP_COUNT) + TO_STRING_MEMBER(TSP_COMMAND) + TO_STRING_MEMBER(FHB_SIZE) + TO_STRING_MEMBER(FHB_COMMAND) + TO_STRING_MEMBER(MAX_MODULATION_LEVEL) + TO_STRING_MEMBER(MAX_BOILER_CAPACITY) + TO_STRING_MEMBER(ROOM_SETPOINT) + TO_STRING_MEMBER(MODULATION_LEVEL) + TO_STRING_MEMBER(CH_WATER_PRESSURE) + TO_STRING_MEMBER(DHW_FLOW_RATE) + TO_STRING_MEMBER(DAY_TIME) + TO_STRING_MEMBER(DATE) + TO_STRING_MEMBER(YEAR) + TO_STRING_MEMBER(ROOM_SETPOINT_CH2) + TO_STRING_MEMBER(ROOM_TEMP) + TO_STRING_MEMBER(FEED_TEMP) + TO_STRING_MEMBER(DHW_TEMP) + TO_STRING_MEMBER(OUTSIDE_TEMP) + TO_STRING_MEMBER(RETURN_WATER_TEMP) + TO_STRING_MEMBER(SOLAR_STORE_TEMP) + TO_STRING_MEMBER(SOLAR_COLLECT_TEMP) + TO_STRING_MEMBER(FEED_TEMP_CH2) + TO_STRING_MEMBER(DHW2_TEMP) + TO_STRING_MEMBER(EXHAUST_TEMP) + TO_STRING_MEMBER(FAN_SPEED) + TO_STRING_MEMBER(FLAME_CURRENT) + TO_STRING_MEMBER(DHW_BOUNDS) + TO_STRING_MEMBER(CH_BOUNDS) + TO_STRING_MEMBER(OTC_CURVE_BOUNDS) + TO_STRING_MEMBER(DHW_SETPOINT) + TO_STRING_MEMBER(MAX_CH_SETPOINT) + TO_STRING_MEMBER(OTC_CURVE_RATIO) + TO_STRING_MEMBER(HVAC_STATUS) + TO_STRING_MEMBER(REL_VENT_SETPOINT) + TO_STRING_MEMBER(DEVICE_VENT) + TO_STRING_MEMBER(REL_VENTILATION) + TO_STRING_MEMBER(REL_HUMID_EXHAUST) + TO_STRING_MEMBER(SUPPLY_INLET_TEMP) + TO_STRING_MEMBER(SUPPLY_OUTLET_TEMP) + TO_STRING_MEMBER(EXHAUST_INLET_TEMP) + TO_STRING_MEMBER(EXHAUST_OUTLET_TEMP) + TO_STRING_MEMBER(NOM_REL_VENTILATION) + TO_STRING_MEMBER(OVERRIDE_FUNC) + TO_STRING_MEMBER(OEM_DIAGNOSTIC) + TO_STRING_MEMBER(BURNER_STARTS) + TO_STRING_MEMBER(CH_PUMP_STARTS) + TO_STRING_MEMBER(DHW_PUMP_STARTS) + TO_STRING_MEMBER(DHW_BURNER_STARTS) + TO_STRING_MEMBER(BURNER_HOURS) + TO_STRING_MEMBER(CH_PUMP_HOURS) + TO_STRING_MEMBER(DHW_PUMP_HOURS) + TO_STRING_MEMBER(DHW_BURNER_HOURS) + TO_STRING_MEMBER(OT_VERSION_CONTROLLER) + TO_STRING_MEMBER(OT_VERSION_DEVICE) + TO_STRING_MEMBER(VERSION_CONTROLLER) + TO_STRING_MEMBER(VERSION_DEVICE) + default: + return ""; + } +} + +string OpenTherm::debug_data(OpenthermData &data) { + stringstream result; + result << bitset<8>(data.type) << " " << bitset<8>(data.id) << " " << bitset<8>(data.valueHB) << " " + << bitset<8>(data.valueLB) << "\n"; + result << "type: " << this->message_type_to_str((MessageType) data.type) << "; "; + result << "id: " << to_string(data.id) << "; "; + result << "HB: " << to_string(data.valueHB) << "; "; + result << "LB: " << to_string(data.valueLB) << "; "; + result << "uint_16: " << to_string(data.u16()) << "; "; + result << "float: " << to_string(data.f88()); + + return result.str(); +} +std::string OpenTherm::debug_error(OpenThermError &error) { + stringstream result; + result << "type: " << this->protocol_error_to_to_str(error.error_type) << "; "; + result << "data: "; + result << format_hex(error.data); + result << "; clock: " << to_string(clock_); + result << "; capture: " << bitset<32>(error.capture); + result << "; bit_pos: " << to_string(error.bit_pos); + + return result.str(); +} + +float OpenthermData::f88() { return ((float) this->s16()) / 256.0; } + +void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); } + +uint16_t OpenthermData::u16() { + uint16_t const value = this->valueHB; + return (value << 8) | this->valueLB; +} + +void OpenthermData::u16(uint16_t value) { + this->valueLB = value & 0xFF; + this->valueHB = (value >> 8) & 0xFF; +} + +int16_t OpenthermData::s16() { + int16_t const value = this->valueHB; + return (value << 8) | this->valueLB; +} + +void OpenthermData::s16(int16_t value) { + this->valueLB = value & 0xFF; + this->valueHB = (value >> 8) & 0xFF; +} + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h new file mode 100644 index 0000000000..23f4b39a1a --- /dev/null +++ b/esphome/components/opentherm/opentherm.h @@ -0,0 +1,346 @@ +/* + * OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but + * heavily modified to comply with ESPHome coding standards and provide better logging. + * Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International + * Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project. + */ + +#pragma once + +#include +#include +#include +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#if defined(ESP32) || defined(USE_ESP_IDF) +#include "driver/timer.h" +#endif + +namespace esphome { +namespace opentherm { + +template constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; } + +template constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); } + +template constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); } + +template constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) { + return bit_value ? set_bit(value, bit) : clear_bit(value, bit); +} + +enum OperationMode { + IDLE = 0, // no operation + + LISTEN = 1, // waiting for transmission to start + READ = 2, // reading 32-bit data frame + RECEIVED = 3, // data frame received with valid start and stop bit + + WRITE = 4, // writing data with timer_ + SENT = 5, // all data written to output + + ERROR_PROTOCOL = 8, // manchester protocol data transfer error + ERROR_TIMEOUT = 9 // read timeout +}; + +enum ProtocolErrorType { + NO_ERROR = 0, // No error + NO_TRANSITION = 1, // No transition in the middle of the bit + INVALID_STOP_BIT = 2, // Stop bit wasn't present when expected + PARITY_ERROR = 3, // Parity check didn't pass + NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks +}; + +enum MessageType { + READ_DATA = 0, + READ_ACK = 4, + WRITE_DATA = 1, + WRITE_ACK = 5, + INVALID_DATA = 2, + DATA_INVALID = 6, + UNKNOWN_DATAID = 7 +}; + +enum MessageId { + STATUS = 0, + CH_SETPOINT = 1, + CONTROLLER_CONFIG = 2, + DEVICE_CONFIG = 3, + COMMAND_CODE = 4, + FAULT_FLAGS = 5, + REMOTE = 6, + COOLING_CONTROL = 7, + CH2_SETPOINT = 8, + CH_SETPOINT_OVERRIDE = 9, + TSP_COUNT = 10, + TSP_COMMAND = 11, + FHB_SIZE = 12, + FHB_COMMAND = 13, + MAX_MODULATION_LEVEL = 14, + MAX_BOILER_CAPACITY = 15, // u8_hb - u8_lb gives min modulation level + ROOM_SETPOINT = 16, + MODULATION_LEVEL = 17, + CH_WATER_PRESSURE = 18, + DHW_FLOW_RATE = 19, + DAY_TIME = 20, + DATE = 21, + YEAR = 22, + ROOM_SETPOINT_CH2 = 23, + ROOM_TEMP = 24, + FEED_TEMP = 25, + DHW_TEMP = 26, + OUTSIDE_TEMP = 27, + RETURN_WATER_TEMP = 28, + SOLAR_STORE_TEMP = 29, + SOLAR_COLLECT_TEMP = 30, + FEED_TEMP_CH2 = 31, + DHW2_TEMP = 32, + EXHAUST_TEMP = 33, + FAN_SPEED = 35, + FLAME_CURRENT = 36, + DHW_BOUNDS = 48, + CH_BOUNDS = 49, + OTC_CURVE_BOUNDS = 50, + DHW_SETPOINT = 56, + MAX_CH_SETPOINT = 57, + OTC_CURVE_RATIO = 58, + + // HVAC Specific Message IDs + HVAC_STATUS = 70, + REL_VENT_SETPOINT = 71, + DEVICE_VENT = 74, + REL_VENTILATION = 77, + REL_HUMID_EXHAUST = 78, + SUPPLY_INLET_TEMP = 80, + SUPPLY_OUTLET_TEMP = 81, + EXHAUST_INLET_TEMP = 82, + EXHAUST_OUTLET_TEMP = 83, + NOM_REL_VENTILATION = 87, + + OVERRIDE_FUNC = 100, + OEM_DIAGNOSTIC = 115, + BURNER_STARTS = 116, + CH_PUMP_STARTS = 117, + DHW_PUMP_STARTS = 118, + DHW_BURNER_STARTS = 119, + BURNER_HOURS = 120, + CH_PUMP_HOURS = 121, + DHW_PUMP_HOURS = 122, + DHW_BURNER_HOURS = 123, + OT_VERSION_CONTROLLER = 124, + OT_VERSION_DEVICE = 125, + VERSION_CONTROLLER = 126, + VERSION_DEVICE = 127 +}; + +enum BitPositions { STOP_BIT = 33 }; + +/** + * Structure to hold Opentherm data packet content. + * Use f88(), u16() or s16() functions to get appropriate value of data packet accoridng to id of message. + */ +struct OpenthermData { + uint8_t type; + uint8_t id; + uint8_t valueHB; + uint8_t valueLB; + + OpenthermData() : type(0), id(0), valueHB(0), valueLB(0) {} + + /** + * @return float representation of data packet value + */ + float f88(); + + /** + * @param float number to set as value of this data packet + */ + void f88(float value); + + /** + * @return unsigned 16b integer representation of data packet value + */ + uint16_t u16(); + + /** + * @param unsigned 16b integer number to set as value of this data packet + */ + void u16(uint16_t value); + + /** + * @return signed 16b integer representation of data packet value + */ + int16_t s16(); + + /** + * @param signed 16b integer number to set as value of this data packet + */ + void s16(int16_t value); +}; + +struct OpenThermError { + ProtocolErrorType error_type; + uint32_t capture; + uint8_t clock; + uint32_t data; + uint8_t bit_pos; +}; + +/** + * Opentherm static class that supports either listening or sending Opentherm data packets in the same time + */ +class OpenTherm { + public: + OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout = 800); + + /** + * Setup pins. + */ + bool initialize(); + + /** + * Start listening for Opentherm data packet comming from line connected to given pin. + * If data packet is received then has_message() function returns true and data packet can be retrieved by calling + * get_message() function. If timeout > 0 then this function waits for incomming data package for timeout millis and + * if no data packet is recevived, error state is indicated by is_error() function. If either data packet is received + * or timeout is reached listening is stopped. + */ + void listen(); + + /** + * Use this function to check whether listen() function already captured a valid data packet. + * + * @return true if data packet has been captured from line by listen() function. + */ + bool has_message() { return mode_ == OperationMode::RECEIVED; } + + /** + * Use this to retrive data packed captured by listen() function. Data packet is ready when has_message() function + * returns true. This function can be called multiple times until stop() is called. + * + * @param data reference to data structure to which fill the data packet data. + * @return true if packet was ready and was filled into data structure passed, false otherwise. + */ + bool get_message(OpenthermData &data); + + /** + * Immediately send out Opentherm data packet to line connected on given pin. + * Completed data transfer is indicated by is_sent() function. + * Error state is indicated by is_error() function. + * + * @param data Opentherm data packet. + */ + void send(OpenthermData &data); + + /** + * Stops listening for data packet or sending out data packet and resets internal state of this class. + * Stops all timers and unattaches all interrupts. + */ + void stop(); + + /** + * Get protocol error details in case a protocol error occured. + * @param error reference to data structure to which fill the error details + * @return true if protocol error occured during last conversation, false otherwise. + */ + bool get_protocol_error(OpenThermError &error); + + /** + * Use this function to check whether send() function already finished sending data packed to line. + * + * @return true if data packet has been sent, false otherwise. + */ + bool is_sent() { return mode_ == OperationMode::SENT; } + + /** + * Indicates whether listinig or sending is not in progress. + * That also means that no timers are running and no interrupts are attached. + * + * @return true if listening nor sending is in progress. + */ + bool is_idle() { return mode_ == OperationMode::IDLE; } + + /** + * Indicates whether last listen() or send() operation ends up with an error. Includes both timeout and + * protocol errors. + * + * @return true if last listen() or send() operation ends up with an error. + */ + bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; } + + /** + * Indicates whether last listen() or send() operation ends up with a *timeout* error + * @return true if last listen() or send() operation ends up with a *timeout* error. + */ + bool is_timeout() { return mode_ == OperationMode::ERROR_TIMEOUT; } + + /** + * Indicates whether last listen() or send() operation ends up with a *protocol* error + * @return true if last listen() or send() operation ends up with a *protocol* error. + */ + bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; } + + bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; } + + OperationMode get_mode() { return mode_; } + + std::string debug_data(OpenthermData &data); + std::string debug_error(OpenThermError &error); + + const char *protocol_error_to_to_str(ProtocolErrorType error_type); + const char *message_type_to_str(MessageType message_type); + const char *operation_mode_to_str(OperationMode mode); + const char *message_id_to_str(MessageId id); + + static bool timer_isr(OpenTherm *arg); + +#ifdef ESP8266 + static void esp8266_timer_isr(); +#endif + + private: + InternalGPIOPin *in_pin_; + InternalGPIOPin *out_pin_; + ISRInternalGPIOPin isr_in_pin_; + ISRInternalGPIOPin isr_out_pin_; + +#if defined(ESP32) || defined(USE_ESP_IDF) + timer_group_t timer_group_; + timer_idx_t timer_idx_; +#endif + + OperationMode mode_; + ProtocolErrorType error_type_; + uint32_t capture_; + uint8_t clock_; + uint32_t data_; + uint8_t bit_pos_; + int32_t timeout_counter_; // <0 no timeout + + int32_t device_timeout_; + +#if defined(ESP32) || defined(USE_ESP_IDF) + bool init_esp32_timer_(); + void start_esp32_timer_(uint64_t alarm_value); +#endif + + void stop_timer_(); + + void read_(); // data detected start reading + void start_read_timer_(); // reading timer_ to sample at 1/5 of manchester code bit length (at 5kHz) + void start_write_timer_(); // writing timer_ to send manchester code (at 2kHz) + bool check_parity_(uint32_t val); + + void bit_read_(uint8_t value); + ProtocolErrorType verify_stop_bit_(uint8_t value); + void write_bit_(uint8_t high, uint8_t clock); + +#ifdef ESP8266 + // ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm + static OpenTherm *instance_; +#endif +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/opentherm_macros.h b/esphome/components/opentherm/opentherm_macros.h new file mode 100644 index 0000000000..0389e975ff --- /dev/null +++ b/esphome/components/opentherm/opentherm_macros.h @@ -0,0 +1,91 @@ +#pragma once +namespace esphome { +namespace opentherm { + +// ===== hub.h macros ===== + +// *_LIST macros will be generated in defines.h if at least one sensor from each platform is used. +// These lists will look like this: +// #define OPENTHERM_BINARY_SENSOR_LIST(F, sep) F(sensor_1) sep F(sensor_2) +// These lists will be used in hub.h to define sensor fields (passing macros like OPENTHERM_DECLARE_SENSOR as F) +// and setters (passing macros like OPENTHERM_SET_SENSOR as F) (see below) +// In order for things not to break, we define empty lists here in case some platforms are not used in config. +#ifndef OPENTHERM_SENSOR_LIST +#define OPENTHERM_SENSOR_LIST(F, sep) +#endif + +// Use macros to create fields for every entity specified in the ESPHome configuration +#define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; + +// Setter macros +#define OPENTHERM_SET_SENSOR(entity) \ + void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } + +// ===== hub.cpp macros ===== + +// *_MESSAGE_HANDLERS are generated in defines.h and look like this: +// OPENTHERM_NUMBER_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) MESSAGE(COOLING_CONTROL) +// ENTITY(cooling_control_number, f88) postscript msg_sep They contain placeholders for message part and entities parts, +// since one message can contain multiple entities. MESSAGE part is substituted with OPENTHERM_MESSAGE_WRITE_MESSAGE, +// OPENTHERM_MESSAGE_READ_MESSAGE or OPENTHERM_MESSAGE_RESPONSE_MESSAGE. ENTITY part is substituted with +// OPENTHERM_MESSAGE_WRITE_ENTITY or OPENTHERM_MESSAGE_RESPONSE_ENTITY. OPENTHERM_IGNORE is used for sensor read +// requests since no data needs to be sent or processed, just the data id. + +// In order for things not to break, we define empty lists here in case some platforms are not used in config. +#ifndef OPENTHERM_SENSOR_MESSAGE_HANDLERS +#define OPENTHERM_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) +#endif + +// Read data request builder +#define OPENTHERM_MESSAGE_READ_MESSAGE(msg) \ + case MessageId::msg: \ + data.type = MessageType::READ_DATA; \ + data.id = request_id; \ + return data; + +// Data processing builders +#define OPENTHERM_MESSAGE_RESPONSE_MESSAGE(msg) case MessageId::msg: +#define OPENTHERM_MESSAGE_RESPONSE_ENTITY(key, msg_data) this->key->publish_state(message_data::parse_##msg_data(data)); +#define OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT break; + +#define OPENTHERM_IGNORE(x, y) + +// Default macros for STATUS entities +#ifndef OPENTHERM_READ_ch_enable +#define OPENTHERM_READ_ch_enable true +#endif +#ifndef OPENTHERM_READ_dhw_enable +#define OPENTHERM_READ_dhw_enable true +#endif +#ifndef OPENTHERM_READ_t_set +#define OPENTHERM_READ_t_set 0.0 +#endif +#ifndef OPENTHERM_READ_cooling_enable +#define OPENTHERM_READ_cooling_enable false +#endif +#ifndef OPENTHERM_READ_cooling_control +#define OPENTHERM_READ_cooling_control 0.0 +#endif +#ifndef OPENTHERM_READ_otc_active +#define OPENTHERM_READ_otc_active false +#endif +#ifndef OPENTHERM_READ_ch2_active +#define OPENTHERM_READ_ch2_active false +#endif +#ifndef OPENTHERM_READ_t_set_ch2 +#define OPENTHERM_READ_t_set_ch2 0.0 +#endif +#ifndef OPENTHERM_READ_summer_mode_active +#define OPENTHERM_READ_summer_mode_active false +#endif +#ifndef OPENTHERM_READ_dhw_block +#define OPENTHERM_READ_dhw_block false +#endif + +// These macros utilize the structure of *_LIST macros in order +#define ID(x) x +#define SHOW_INNER(x) #x +#define SHOW(x) SHOW_INNER(x) + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/schema.py b/esphome/components/opentherm/schema.py new file mode 100644 index 0000000000..6ed0029437 --- /dev/null +++ b/esphome/components/opentherm/schema.py @@ -0,0 +1,438 @@ +# This file contains a schema for all supported sensors, binary sensors and +# inputs of the OpenTherm component. + +from dataclasses import dataclass +from typing import Optional, TypeVar + +from esphome.const import ( + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_KILOWATT, + UNIT_MICROAMP, + UNIT_PERCENT, + UNIT_REVOLUTIONS_PER_MINUTE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, +) + + +@dataclass +class EntitySchema: + description: str + """Description of the item, based on the OpenTherm spec""" + + message: str + """OpenTherm message id used to read or write the value""" + + keep_updated: bool + """Whether the value should be read or write repeatedly (True) or only during + the initialization phase (False) + """ + + message_data: str + """Instructions on how to interpret the data in the message + - flag8_[hb|lb]_[0-7]: data is a byte of single bit flags, + this flag is set in the high (hb) or low byte (lb), + at position 0 to 7 + - u8_[hb|lb]: data is an unsigned 8-bit integer, + in the high (hb) or low byte (lb) + - s8_[hb|lb]: data is an signed 8-bit integer, + in the high (hb) or low byte (lb) + - f88: data is a signed fixed point value with + 1 sign bit, 7 integer bits, 8 fractional bits + - u16: data is an unsigned 16-bit integer + - s16: data is a signed 16-bit integer + """ + + +TSchema = TypeVar("TSchema", bound=EntitySchema) + + +@dataclass +class SensorSchema(EntitySchema): + accuracy_decimals: int + state_class: str + unit_of_measurement: Optional[str] = None + icon: Optional[str] = None + device_class: Optional[str] = None + disabled_by_default: bool = False + + +SENSORS: dict[str, SensorSchema] = { + "rel_mod_level": SensorSchema( + description="Relative modulation level", + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + icon="mdi:percent", + state_class=STATE_CLASS_MEASUREMENT, + message="MODULATION_LEVEL", + keep_updated=True, + message_data="f88", + ), + "ch_pressure": SensorSchema( + description="Water pressure in CH circuit", + unit_of_measurement="bar", + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + message="CH_WATER_PRESSURE", + keep_updated=True, + message_data="f88", + ), + "dhw_flow_rate": SensorSchema( + description="Water flow rate in DHW circuit", + unit_of_measurement="l/min", + accuracy_decimals=2, + icon="mdi:waves-arrow-right", + state_class=STATE_CLASS_MEASUREMENT, + message="DHW_FLOW_RATE", + keep_updated=True, + message_data="f88", + ), + "t_boiler": SensorSchema( + description="Boiler water temperature", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="FEED_TEMP", + keep_updated=True, + message_data="f88", + ), + "t_dhw": SensorSchema( + description="DHW temperature", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="DHW_TEMP", + keep_updated=True, + message_data="f88", + ), + "t_outside": SensorSchema( + description="Outside temperature", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="OUTSIDE_TEMP", + keep_updated=True, + message_data="f88", + ), + "t_ret": SensorSchema( + description="Return water temperature", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="RETURN_WATER_TEMP", + keep_updated=True, + message_data="f88", + ), + "t_storage": SensorSchema( + description="Solar storage temperature", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="SOLAR_STORE_TEMP", + keep_updated=True, + message_data="f88", + ), + "t_collector": SensorSchema( + description="Solar collector temperature", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="SOLAR_COLLECT_TEMP", + keep_updated=True, + message_data="s16", + ), + "t_flow_ch2": SensorSchema( + description="Flow water temperature CH2 circuit", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="FEED_TEMP_CH2", + keep_updated=True, + message_data="f88", + ), + "t_dhw2": SensorSchema( + description="Domestic hot water temperature 2", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="DHW2_TEMP", + keep_updated=True, + message_data="f88", + ), + "t_exhaust": SensorSchema( + description="Boiler exhaust temperature", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="EXHAUST_TEMP", + keep_updated=True, + message_data="s16", + ), + "fan_speed": SensorSchema( + description="Boiler fan speed", + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + message="FAN_SPEED", + keep_updated=True, + message_data="u16", + ), + "flame_current": SensorSchema( + description="Boiler flame current", + unit_of_measurement=UNIT_MICROAMP, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + message="FLAME_CURRENT", + keep_updated=True, + message_data="f88", + ), + "burner_starts": SensorSchema( + description="Number of starts burner", + accuracy_decimals=0, + icon="mdi:gas-burner", + state_class=STATE_CLASS_TOTAL_INCREASING, + message="BURNER_STARTS", + keep_updated=True, + message_data="u16", + ), + "ch_pump_starts": SensorSchema( + description="Number of starts CH pump", + accuracy_decimals=0, + icon="mdi:pump", + state_class=STATE_CLASS_TOTAL_INCREASING, + message="CH_PUMP_STARTS", + keep_updated=True, + message_data="u16", + ), + "dhw_pump_valve_starts": SensorSchema( + description="Number of starts DHW pump/valve", + accuracy_decimals=0, + icon="mdi:water-pump", + state_class=STATE_CLASS_TOTAL_INCREASING, + message="DHW_PUMP_STARTS", + keep_updated=True, + message_data="u16", + ), + "dhw_burner_starts": SensorSchema( + description="Number of starts burner during DHW mode", + accuracy_decimals=0, + icon="mdi:gas-burner", + state_class=STATE_CLASS_TOTAL_INCREASING, + message="DHW_BURNER_STARTS", + keep_updated=True, + message_data="u16", + ), + "burner_operation_hours": SensorSchema( + description="Number of hours that burner is in operation", + accuracy_decimals=0, + icon="mdi:clock-outline", + state_class=STATE_CLASS_TOTAL_INCREASING, + message="BURNER_HOURS", + keep_updated=True, + message_data="u16", + ), + "ch_pump_operation_hours": SensorSchema( + description="Number of hours that CH pump has been running", + accuracy_decimals=0, + icon="mdi:clock-outline", + state_class=STATE_CLASS_TOTAL_INCREASING, + message="CH_PUMP_HOURS", + keep_updated=True, + message_data="u16", + ), + "dhw_pump_valve_operation_hours": SensorSchema( + description="Number of hours that DHW pump has been running or DHW valve has been opened", + accuracy_decimals=0, + icon="mdi:clock-outline", + state_class=STATE_CLASS_TOTAL_INCREASING, + message="DHW_PUMP_HOURS", + keep_updated=True, + message_data="u16", + ), + "dhw_burner_operation_hours": SensorSchema( + description="Number of hours that burner is in operation during DHW mode", + accuracy_decimals=0, + icon="mdi:clock-outline", + state_class=STATE_CLASS_TOTAL_INCREASING, + message="DHW_BURNER_HOURS", + keep_updated=True, + message_data="u16", + ), + "t_dhw_set_ub": SensorSchema( + description="Upper bound for adjustment of DHW setpoint", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="DHW_BOUNDS", + keep_updated=False, + message_data="s8_hb", + ), + "t_dhw_set_lb": SensorSchema( + description="Lower bound for adjustment of DHW setpoint", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="DHW_BOUNDS", + keep_updated=False, + message_data="s8_lb", + ), + "max_t_set_ub": SensorSchema( + description="Upper bound for adjustment of max CH setpoint", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="CH_BOUNDS", + keep_updated=False, + message_data="s8_hb", + ), + "max_t_set_lb": SensorSchema( + description="Lower bound for adjustment of max CH setpoint", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="CH_BOUNDS", + keep_updated=False, + message_data="s8_lb", + ), + "t_dhw_set": SensorSchema( + description="Domestic hot water temperature setpoint", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="DHW_SETPOINT", + keep_updated=True, + message_data="f88", + ), + "max_t_set": SensorSchema( + description="Maximum allowable CH water setpoint", + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + message="MAX_CH_SETPOINT", + keep_updated=True, + message_data="f88", + ), + "oem_fault_code": SensorSchema( + description="OEM fault code", + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + message="FAULT_FLAGS", + keep_updated=True, + message_data="u8_lb", + ), + "oem_diagnostic_code": SensorSchema( + description="OEM diagnostic code", + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + message="OEM_DIAGNOSTIC", + keep_updated=True, + message_data="u16", + ), + "max_capacity": SensorSchema( + description="Maximum boiler capacity (KW)", + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + disabled_by_default=True, + message="MAX_BOILER_CAPACITY", + keep_updated=False, + message_data="u8_hb", + ), + "min_mod_level": SensorSchema( + description="Minimum modulation level", + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + icon="mdi:percent", + disabled_by_default=True, + state_class=STATE_CLASS_MEASUREMENT, + message="MAX_BOILER_CAPACITY", + keep_updated=False, + message_data="u8_lb", + ), + "opentherm_version_device": SensorSchema( + description="Version of OpenTherm implemented by device", + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + disabled_by_default=True, + message="OT_VERSION_DEVICE", + keep_updated=False, + message_data="f88", + ), + "device_type": SensorSchema( + description="Device product type", + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + disabled_by_default=True, + message="VERSION_DEVICE", + keep_updated=False, + message_data="u8_hb", + ), + "device_version": SensorSchema( + description="Device product version", + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + disabled_by_default=True, + message="VERSION_DEVICE", + keep_updated=False, + message_data="u8_lb", + ), + "device_id": SensorSchema( + description="Device ID code", + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + disabled_by_default=True, + message="DEVICE_CONFIG", + keep_updated=False, + message_data="u8_lb", + ), + "otc_hc_ratio_ub": SensorSchema( + description="OTC heat curve ratio upper bound", + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + disabled_by_default=True, + message="OTC_CURVE_BOUNDS", + keep_updated=False, + message_data="u8_hb", + ), + "otc_hc_ratio_lb": SensorSchema( + description="OTC heat curve ratio lower bound", + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, + disabled_by_default=True, + message="OTC_CURVE_BOUNDS", + keep_updated=False, + message_data="u8_lb", + ), +} diff --git a/esphome/components/opentherm/sensor/__init__.py b/esphome/components/opentherm/sensor/__init__.py new file mode 100644 index 0000000000..20224e0eda --- /dev/null +++ b/esphome/components/opentherm/sensor/__init__.py @@ -0,0 +1,35 @@ +from typing import Any + +import esphome.config_validation as cv +from esphome.components import sensor +from .. import const, schema, validate, generate + +DEPENDENCIES = [const.OPENTHERM] +COMPONENT_TYPE = const.SENSOR + + +def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: + return sensor.sensor_schema( + unit_of_measurement=entity.unit_of_measurement + or sensor._UNDEF, # pylint: disable=protected-access + accuracy_decimals=entity.accuracy_decimals, + device_class=entity.device_class + or sensor._UNDEF, # pylint: disable=protected-access + icon=entity.icon or sensor._UNDEF, # pylint: disable=protected-access + state_class=entity.state_class, + ) + + +CONFIG_SCHEMA = validate.create_component_schema( + schema.SENSORS, get_entity_validation_schema +) + + +async def to_code(config: dict[str, Any]) -> None: + await generate.component_to_code( + COMPONENT_TYPE, + schema.SENSORS, + sensor.Sensor, + generate.create_only_conf(sensor.new_sensor), + config, + ) diff --git a/esphome/components/opentherm/validate.py b/esphome/components/opentherm/validate.py new file mode 100644 index 0000000000..d4507672a5 --- /dev/null +++ b/esphome/components/opentherm/validate.py @@ -0,0 +1,31 @@ +from typing import Callable + +from voluptuous import Schema + +import esphome.config_validation as cv + +from . import const, schema, generate +from .schema import TSchema + + +def create_entities_schema( + entities: dict[str, schema.EntitySchema], + get_entity_validation_schema: Callable[[TSchema], cv.Schema], +) -> Schema: + entity_schema = {} + for key, entity in entities.items(): + entity_schema[cv.Optional(key)] = get_entity_validation_schema(entity) + return cv.Schema(entity_schema) + + +def create_component_schema( + entities: dict[str, schema.EntitySchema], + get_entity_validation_schema: Callable[[TSchema], cv.Schema], +) -> Schema: + return ( + cv.Schema( + {cv.GenerateID(const.CONF_OPENTHERM_ID): cv.use_id(generate.OpenthermHub)} + ) + .extend(create_entities_schema(entities, get_entity_validation_schema)) + .extend(cv.COMPONENT_SCHEMA) + ) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index a966157ffa..d9917a2aae 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,62 +1,71 @@ -from esphome.cpp_generator import RawExpression +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation from esphome.const import ( - CONF_ID, - CONF_NUM_ATTEMPTS, - CONF_PASSWORD, - CONF_PORT, - CONF_REBOOT_TIMEOUT, - CONF_SAFE_MODE, - CONF_TRIGGER_ID, + CONF_ESPHOME, + CONF_ON_ERROR, CONF_OTA, - KEY_PAST_SAFE_MODE, + CONF_PLATFORM, + CONF_TRIGGER_ID, ) from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] -DEPENDENCIES = ["network"] -AUTO_LOAD = ["socket", "md5"] +AUTO_LOAD = ["md5", "safe_mode"] -CONF_ON_STATE_CHANGE = "on_state_change" +IS_PLATFORM_COMPONENT = True + +CONF_ON_ABORT = "on_abort" CONF_ON_BEGIN = "on_begin" -CONF_ON_PROGRESS = "on_progress" CONF_ON_END = "on_end" -CONF_ON_ERROR = "on_error" +CONF_ON_PROGRESS = "on_progress" +CONF_ON_STATE_CHANGE = "on_state_change" + ota_ns = cg.esphome_ns.namespace("ota") -OTAState = ota_ns.enum("OTAState") OTAComponent = ota_ns.class_("OTAComponent", cg.Component) +OTAState = ota_ns.enum("OTAState") +OTAAbortTrigger = ota_ns.class_("OTAAbortTrigger", automation.Trigger.template()) +OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template()) +OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template()) +OTAProgressTrigger = ota_ns.class_("OTAProgressTrigger", automation.Trigger.template()) +OTAStartTrigger = ota_ns.class_("OTAStartTrigger", automation.Trigger.template()) OTAStateChangeTrigger = ota_ns.class_( "OTAStateChangeTrigger", automation.Trigger.template() ) -OTAStartTrigger = ota_ns.class_("OTAStartTrigger", automation.Trigger.template()) -OTAProgressTrigger = ota_ns.class_("OTAProgressTrigger", automation.Trigger.template()) -OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template()) -OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template()) -CONFIG_SCHEMA = cv.Schema( +def _ota_final_validate(config): + if len(config) < 1: + raise cv.Invalid( + f"At least one platform must be specified for '{CONF_OTA}'; add '{CONF_PLATFORM}: {CONF_ESPHOME}' for original OTA functionality" + ) + + +FINAL_VALIDATE_SCHEMA = _ota_final_validate + +BASE_OTA_SCHEMA = cv.Schema( { - cv.GenerateID(): cv.declare_id(OTAComponent), - cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, - cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232, rp2040=2040): cv.port, - cv.Optional(CONF_PASSWORD): cv.string, - cv.Optional( - CONF_REBOOT_TIMEOUT, default="5min" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStateChangeTrigger), } ), + cv.Optional(CONF_ON_ABORT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAAbortTrigger), + } + ), cv.Optional(CONF_ON_BEGIN): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStartTrigger), } ), + cv.Optional(CONF_ON_END): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAEndTrigger), + } + ), cv.Optional(CONF_ON_ERROR): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAErrorTrigger), @@ -67,34 +76,13 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAProgressTrigger), } ), - cv.Optional(CONF_ON_END): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAEndTrigger), - } - ), } -).extend(cv.COMPONENT_SCHEMA) +) -@coroutine_with_priority(50.0) +@coroutine_with_priority(54.0) async def to_code(config): - CORE.data[CONF_OTA] = {} - - var = cg.new_Pvariable(config[CONF_ID]) - cg.add(var.set_port(config[CONF_PORT])) cg.add_define("USE_OTA") - if CONF_PASSWORD in config: - cg.add(var.set_auth_password(config[CONF_PASSWORD])) - cg.add_define("USE_OTA_PASSWORD") - - await cg.register_component(var, config) - - if config[CONF_SAFE_MODE]: - condition = var.should_enter_safe_mode( - config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT] - ) - cg.add(RawExpression(f"if ({condition}) return")) - CORE.data[CONF_OTA][KEY_PAST_SAFE_MODE] = True if CORE.is_esp32 and CORE.using_arduino: cg.add_library("Update", None) @@ -102,11 +90,17 @@ async def to_code(config): if CORE.is_rp2040 and CORE.using_arduino: cg.add_library("Updater", None) + +async def ota_to_code(var, config): use_state_callback = False for conf in config.get(CONF_ON_STATE_CHANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(OTAState, "state")], conf) use_state_callback = True + for conf in config.get(CONF_ON_ABORT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True for conf in config.get(CONF_ON_BEGIN, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -121,7 +115,7 @@ async def to_code(config): use_state_callback = True for conf in config.get(CONF_ON_ERROR, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(int, "x")], conf) + await automation.build_automation(trigger, [(cg.uint8, "x")], conf) use_state_callback = True if use_state_callback: cg.add_define("USE_OTA_STATE_CALLBACK") diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index 6c8aca3705..4605193480 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -1,11 +1,8 @@ #pragma once - -#include "esphome/core/defines.h" #ifdef USE_OTA_STATE_CALLBACK +#include "ota_backend.h" -#include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/components/ota/ota_component.h" namespace esphome { namespace ota { @@ -54,7 +51,18 @@ class OTAEndTrigger : public Trigger<> { } }; -class OTAErrorTrigger : public Trigger { +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(); + } + }); + } +}; + +class OTAErrorTrigger : public Trigger { public: explicit OTAErrorTrigger(OTAComponent *parent) { parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { @@ -67,5 +75,4 @@ class OTAErrorTrigger : public Trigger { } // namespace ota } // namespace esphome - -#endif // USE_OTA_STATE_CALLBACK +#endif diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp new file mode 100644 index 0000000000..30de4ec4b3 --- /dev/null +++ b/esphome/components/ota/ota_backend.cpp @@ -0,0 +1,20 @@ +#include "ota_backend.h" + +namespace esphome { +namespace ota { + +#ifdef USE_OTA_STATE_CALLBACK +OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +OTAGlobalCallback *get_global_ota_callback() { + if (global_ota_callback == nullptr) { + global_ota_callback = new OTAGlobalCallback(); // NOLINT(cppcoreguidelines-owning-memory) + } + return global_ota_callback; +} + +void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); } +#endif + +} // namespace ota +} // namespace esphome diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 5c5b61a278..bc8ab46643 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -1,9 +1,53 @@ #pragma once -#include "ota_component.h" + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_OTA_STATE_CALLBACK +#include "esphome/core/automation.h" +#endif namespace esphome { namespace ota { +enum OTAResponseTypes { + OTA_RESPONSE_OK = 0x00, + OTA_RESPONSE_REQUEST_AUTH = 0x01, + + OTA_RESPONSE_HEADER_OK = 0x40, + OTA_RESPONSE_AUTH_OK = 0x41, + OTA_RESPONSE_UPDATE_PREPARE_OK = 0x42, + OTA_RESPONSE_BIN_MD5_OK = 0x43, + OTA_RESPONSE_RECEIVE_OK = 0x44, + OTA_RESPONSE_UPDATE_END_OK = 0x45, + OTA_RESPONSE_SUPPORTS_COMPRESSION = 0x46, + OTA_RESPONSE_CHUNK_OK = 0x47, + + OTA_RESPONSE_ERROR_MAGIC = 0x80, + OTA_RESPONSE_ERROR_UPDATE_PREPARE = 0x81, + OTA_RESPONSE_ERROR_AUTH_INVALID = 0x82, + OTA_RESPONSE_ERROR_WRITING_FLASH = 0x83, + OTA_RESPONSE_ERROR_UPDATE_END = 0x84, + OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 0x85, + OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 0x86, + OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 0x87, + OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88, + OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89, + OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A, + OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B, + OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C, + OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, +}; + +enum OTAState { + OTA_COMPLETED = 0, + OTA_STARTED, + OTA_IN_PROGRESS, + OTA_ABORT, + OTA_ERROR, +}; + class OTABackend { public: virtual ~OTABackend() = default; @@ -15,5 +59,38 @@ class OTABackend { virtual bool supports_compression() = 0; }; +class OTAComponent : public Component { +#ifdef USE_OTA_STATE_CALLBACK + public: + void add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); + } + + protected: + CallbackManager state_callback_{}; +#endif +}; + +#ifdef USE_OTA_STATE_CALLBACK +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)); + } + + protected: + CallbackManager state_callback_{}; +}; + +OTAGlobalCallback *get_global_ota_callback(); +void register_ota_platform(OTAComponent *ota_caller); +#endif +std::unique_ptr make_ota_backend(); + } // namespace ota } // namespace esphome diff --git a/esphome/components/ota/ota_backend_arduino_esp32.cpp b/esphome/components/ota/ota_backend_arduino_esp32.cpp index 4759737dbd..15dfc98a6c 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.cpp +++ b/esphome/components/ota/ota_backend_arduino_esp32.cpp @@ -1,15 +1,19 @@ -#include "esphome/core/defines.h" #ifdef USE_ESP32_FRAMEWORK_ARDUINO +#include "esphome/core/defines.h" +#include "esphome/core/log.h" -#include "ota_backend_arduino_esp32.h" -#include "ota_component.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) { bool ret = Update.begin(image_size, U_FLASH); if (ret) { @@ -19,6 +23,9 @@ OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) { 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; } @@ -26,16 +33,25 @@ void ArduinoESP32OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5 OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); - if (written != len) { - return OTA_RESPONSE_ERROR_WRITING_FLASH; + if (written == len) { + return OTA_RESPONSE_OK; } - 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() { - if (!Update.end()) - return OTA_RESPONSE_ERROR_UPDATE_END; - return OTA_RESPONSE_OK; + if (Update.end()) { + 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(); } diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h index f86a70d678..ac7fe9f14f 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.h +++ b/esphome/components/ota/ota_backend_arduino_esp32.h @@ -1,10 +1,10 @@ #pragma once -#include "esphome/core/defines.h" #ifdef USE_ESP32_FRAMEWORK_ARDUINO - -#include "ota_component.h" #include "ota_backend.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + namespace esphome { namespace ota { diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp index 23dc0d4e21..42edbf5d2b 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.cpp +++ b/esphome/components/ota/ota_backend_arduino_esp8266.cpp @@ -1,17 +1,21 @@ -#include "esphome/core/defines.h" #ifdef USE_ARDUINO #ifdef USE_ESP8266 - #include "ota_backend_arduino_esp8266.h" -#include "ota_component.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) { bool ret = Update.begin(image_size, U_FLASH); if (ret) { @@ -28,6 +32,9 @@ OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { 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; } @@ -35,16 +42,25 @@ void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { Update.setMD5(m OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); - if (written != len) { - return OTA_RESPONSE_ERROR_WRITING_FLASH; + if (written == len) { + return OTA_RESPONSE_OK; } - 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() { - if (!Update.end()) - return OTA_RESPONSE_ERROR_UPDATE_END; - return OTA_RESPONSE_OK; + if (Update.end()) { + return OTA_RESPONSE_OK; + } + + uint8_t error = Update.getError(); + ESP_LOGE(TAG, "End error: %d", error); + + return OTA_RESPONSE_ERROR_UPDATE_END; } void ArduinoESP8266OTABackend::abort() { diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h index 7937c665b0..7f44d7c965 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -1,10 +1,9 @@ #pragma once -#include "esphome/core/defines.h" #ifdef USE_ARDUINO #ifdef USE_ESP8266 - -#include "ota_component.h" #include "ota_backend.h" + +#include "esphome/core/defines.h" #include "esphome/core/macros.h" namespace esphome { diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp new file mode 100644 index 0000000000..6b2cf80684 --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -0,0 +1,62 @@ +#ifdef USE_LIBRETINY +#include "ota_backend_arduino_libretiny.h" +#include "ota_backend.h" + +#include "esphome/core/defines.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace ota { + +static const char *const TAG = "ota.arduino_libretiny"; + +std::unique_ptr make_ota_backend() { return make_unique(); } + +OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { + 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 ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } + +OTAResponseTypes ArduinoLibreTinyOTABackend::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 ArduinoLibreTinyOTABackend::end() { + if (Update.end()) { + return OTA_RESPONSE_OK; + } + + uint8_t error = Update.getError(); + ESP_LOGE(TAG, "End error: %d", error); + + return OTA_RESPONSE_ERROR_UPDATE_END; +} + +void ArduinoLibreTinyOTABackend::abort() { Update.abort(); } + +} // namespace ota +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h new file mode 100644 index 0000000000..11deb6e2f2 --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -0,0 +1,23 @@ +#pragma once +#ifdef USE_LIBRETINY +#include "ota_backend.h" + +#include "esphome/core/defines.h" + +namespace esphome { +namespace ota { + +class ArduinoLibreTinyOTABackend : 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 ota +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index 260387cec1..ffeab2e93f 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -1,17 +1,21 @@ -#include "esphome/core/defines.h" #ifdef USE_ARDUINO #ifdef USE_RP2040 +#include "ota_backend_arduino_rp2040.h" +#include "ota_backend.h" #include "esphome/components/rp2040/preferences.h" -#include "ota_backend.h" -#include "ota_backend_arduino_rp2040.h" -#include "ota_component.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" #include namespace esphome { namespace ota { +static const char *const TAG = "ota.arduino_rp2040"; + +std::unique_ptr make_ota_backend() { return make_unique(); } + OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { bool ret = Update.begin(image_size, U_FLASH); if (ret) { @@ -28,6 +32,9 @@ OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; if (error == UPDATE_ERROR_SPACE) return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE; + + ESP_LOGE(TAG, "Begin error: %d", error); + return OTA_RESPONSE_ERROR_UNKNOWN; } @@ -35,16 +42,25 @@ void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { Update.setMD5(md OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); - if (written != len) { - return OTA_RESPONSE_ERROR_WRITING_FLASH; + if (written == len) { + return OTA_RESPONSE_OK; } - return OTA_RESPONSE_OK; + + uint8_t error = Update.getError(); + ESP_LOGE(TAG, "Write error: %d", error); + + return OTA_RESPONSE_ERROR_WRITING_FLASH; } OTAResponseTypes ArduinoRP2040OTABackend::end() { - if (!Update.end()) - return OTA_RESPONSE_ERROR_UPDATE_END; - return OTA_RESPONSE_OK; + if (Update.end()) { + return OTA_RESPONSE_OK; + } + + uint8_t error = Update.getError(); + ESP_LOGE(TAG, "End error: %d", error); + + return OTA_RESPONSE_ERROR_UPDATE_END; } void ArduinoRP2040OTABackend::abort() { diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index 5aa2ec9435..b189964ab3 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -1,11 +1,10 @@ #pragma once -#include "esphome/core/defines.h" #ifdef USE_ARDUINO #ifdef USE_RP2040 - -#include "esphome/core/macros.h" #include "ota_backend.h" -#include "ota_component.h" + +#include "esphome/core/defines.h" +#include "esphome/core/macros.h" namespace esphome { namespace ota { diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 7688629e39..6f45fb75e4 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -1,12 +1,11 @@ -#include "esphome/core/defines.h" #ifdef USE_ESP_IDF - -#include - #include "ota_backend_esp_idf.h" -#include "ota_component.h" -#include + #include "esphome/components/md5/md5.h" +#include "esphome/core/defines.h" + +#include +#include #if ESP_IDF_VERSION_MAJOR >= 5 #include @@ -15,31 +14,43 @@ namespace esphome { namespace ota { +std::unique_ptr make_ota_backend() { return make_unique(); } + OTAResponseTypes IDFOTABackend::begin(size_t image_size) { this->partition_ = esp_ota_get_next_update_partition(nullptr); if (this->partition_ == nullptr) { return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; } +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // The following function takes longer than the 5 seconds timeout of WDT #if ESP_IDF_VERSION_MAJOR >= 5 esp_task_wdt_config_t wdtc; - wdtc.timeout_ms = 15000; wdtc.idle_core_mask = 0; +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 + wdtc.idle_core_mask |= (1 << 0); +#endif +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 + wdtc.idle_core_mask |= (1 << 1); +#endif + wdtc.timeout_ms = 15000; wdtc.trigger_panic = false; esp_task_wdt_reconfigure(&wdtc); #else esp_task_wdt_init(15, false); +#endif #endif esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // Set the WDT back to the configured timeout #if ESP_IDF_VERSION_MAJOR >= 5 - wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S; + wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; esp_task_wdt_reconfigure(&wdtc); #else esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); +#endif #endif if (err != ESP_OK) { diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index af09d0d693..ed66d9b970 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -1,11 +1,11 @@ #pragma once -#include "esphome/core/defines.h" #ifdef USE_ESP_IDF - -#include "ota_component.h" #include "ota_backend.h" -#include + #include "esphome/components/md5/md5.h" +#include "esphome/core/defines.h" + +#include namespace esphome { namespace ota { diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp deleted file mode 100644 index 39ba3dbed4..0000000000 --- a/esphome/components/ota/ota_component.cpp +++ /dev/null @@ -1,521 +0,0 @@ -#include "ota_component.h" -#include "ota_backend.h" -#include "ota_backend_arduino_esp32.h" -#include "ota_backend_arduino_esp8266.h" -#include "ota_backend_arduino_rp2040.h" -#include "ota_backend_esp_idf.h" - -#include "esphome/core/log.h" -#include "esphome/core/application.h" -#include "esphome/core/hal.h" -#include "esphome/core/util.h" -#include "esphome/components/md5/md5.h" -#include "esphome/components/network/util.h" - -#include -#include - -namespace esphome { -namespace ota { - -static const char *const TAG = "ota"; - -static const uint8_t OTA_VERSION_1_0 = 1; - -OTAComponent *global_ota_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -std::unique_ptr make_ota_backend() { -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 - return make_unique(); -#endif // USE_ESP8266 -#ifdef USE_ESP32 - return make_unique(); -#endif // USE_ESP32 -#endif // USE_ARDUINO -#ifdef USE_ESP_IDF - return make_unique(); -#endif // USE_ESP_IDF -#ifdef USE_RP2040 - return make_unique(); -#endif // USE_RP2040 -} - -OTAComponent::OTAComponent() { global_ota_component = this; } - -void OTAComponent::setup() { - server_ = socket::socket_ip(SOCK_STREAM, 0); - if (server_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket."); - this->mark_failed(); - return; - } - int enable = 1; - int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); - if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); - // we can still continue - } - err = server_->setblocking(false); - if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); - this->mark_failed(); - return; - } - - struct sockaddr_storage server; - - socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); - if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); - this->mark_failed(); - return; - } - - err = server_->bind((struct sockaddr *) &server, sizeof(server)); - if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); - return; - } - - err = server_->listen(4); - if (err != 0) { - ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); - this->mark_failed(); - return; - } - - this->dump_config(); -} - -void OTAComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Over-The-Air Updates:"); - ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->port_); -#ifdef USE_OTA_PASSWORD - if (!this->password_.empty()) { - ESP_LOGCONFIG(TAG, " Using Password."); - } -#endif - if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 && - this->safe_mode_rtc_value_ != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) { - ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %d restarts", - this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_); - } -} - -void OTAComponent::loop() { - this->handle_(); - - if (this->has_safe_mode_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) { - this->has_safe_mode_ = false; - // successful boot, reset counter - ESP_LOGI(TAG, "Boot seems successful, resetting boot loop counter."); - this->clean_rtc(); - } -} - -static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; - -void OTAComponent::handle_() { - OTAResponseTypes error_code = OTA_RESPONSE_ERROR_UNKNOWN; - bool update_started = false; - size_t total = 0; - uint32_t last_progress = 0; - uint8_t buf[1024]; - char *sbuf = reinterpret_cast(buf); - size_t ota_size; - uint8_t ota_features; - std::unique_ptr backend; - (void) ota_features; - - if (client_ == nullptr) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len); - } - if (client_ == nullptr) - return; - - int enable = 1; - int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); - if (err != 0) { - ESP_LOGW(TAG, "Socket could not enable tcp nodelay, errno: %d", errno); - return; - } - - ESP_LOGD(TAG, "Starting OTA Update from %s...", this->client_->getpeername().c_str()); - this->status_set_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(OTA_STARTED, 0.0f, 0); -#endif - - if (!this->readall_(buf, 5)) { - ESP_LOGW(TAG, "Reading magic bytes failed!"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - // 0x6C, 0x26, 0xF7, 0x5C, 0x45 - if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) { - ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3], - buf[4]); - error_code = OTA_RESPONSE_ERROR_MAGIC; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - - // Send OK and version - 2 bytes - buf[0] = OTA_RESPONSE_OK; - buf[1] = OTA_VERSION_1_0; - this->writeall_(buf, 2); - - backend = make_ota_backend(); - - // Read features - 1 byte - if (!this->readall_(buf, 1)) { - ESP_LOGW(TAG, "Reading features failed!"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - ota_features = buf[0]; // NOLINT - ESP_LOGV(TAG, "OTA features is 0x%02X", ota_features); - - // Acknowledge header - 1 byte - buf[0] = OTA_RESPONSE_HEADER_OK; - if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) { - buf[0] = OTA_RESPONSE_SUPPORTS_COMPRESSION; - } - - this->writeall_(buf, 1); - -#ifdef USE_OTA_PASSWORD - if (!this->password_.empty()) { - buf[0] = OTA_RESPONSE_REQUEST_AUTH; - this->writeall_(buf, 1); - md5::MD5Digest md5{}; - md5.init(); - sprintf(sbuf, "%08X", random_uint32()); - md5.add(sbuf, 8); - md5.calculate(); - md5.get_hex(sbuf); - ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf); - - // Send nonce, 32 bytes hex MD5 - if (!this->writeall_(reinterpret_cast(sbuf), 32)) { - ESP_LOGW(TAG, "Auth: Writing nonce failed!"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - - // prepare challenge - md5.init(); - md5.add(this->password_.c_str(), this->password_.length()); - // add nonce - md5.add(sbuf, 32); - - // Receive cnonce, 32 bytes hex MD5 - if (!this->readall_(buf, 32)) { - ESP_LOGW(TAG, "Auth: Reading cnonce failed!"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - sbuf[32] = '\0'; - ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf); - // add cnonce - md5.add(sbuf, 32); - - // calculate result - md5.calculate(); - md5.get_hex(sbuf); - ESP_LOGV(TAG, "Auth: Result is %s", sbuf); - - // Receive result, 32 bytes hex MD5 - if (!this->readall_(buf + 64, 32)) { - ESP_LOGW(TAG, "Auth: Reading response failed!"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - sbuf[64 + 32] = '\0'; - ESP_LOGV(TAG, "Auth: Response is %s", sbuf + 64); - - bool matches = true; - for (uint8_t i = 0; i < 32; i++) - matches = matches && buf[i] == buf[64 + i]; - - if (!matches) { - ESP_LOGW(TAG, "Auth failed! Passwords do not match!"); - error_code = OTA_RESPONSE_ERROR_AUTH_INVALID; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - } -#endif // USE_OTA_PASSWORD - - // Acknowledge auth OK - 1 byte - buf[0] = OTA_RESPONSE_AUTH_OK; - this->writeall_(buf, 1); - - // Read size, 4 bytes MSB first - if (!this->readall_(buf, 4)) { - ESP_LOGW(TAG, "Reading size failed!"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - ota_size = 0; - for (uint8_t i = 0; i < 4; i++) { - ota_size <<= 8; - ota_size |= buf[i]; - } - ESP_LOGV(TAG, "OTA size is %u bytes", ota_size); - - error_code = backend->begin(ota_size); - if (error_code != OTA_RESPONSE_OK) - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - update_started = true; - - // Acknowledge prepare OK - 1 byte - buf[0] = OTA_RESPONSE_UPDATE_PREPARE_OK; - this->writeall_(buf, 1); - - // Read binary MD5, 32 bytes - if (!this->readall_(buf, 32)) { - ESP_LOGW(TAG, "Reading binary MD5 checksum failed!"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - sbuf[32] = '\0'; - ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf); - backend->set_update_md5(sbuf); - - // Acknowledge MD5 OK - 1 byte - buf[0] = OTA_RESPONSE_BIN_MD5_OK; - this->writeall_(buf, 1); - - while (total < ota_size) { - // TODO: timeout check - size_t requested = std::min(sizeof(buf), ota_size - total); - ssize_t read = this->client_->read(buf, requested); - if (read == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); - continue; - } - ESP_LOGW(TAG, "Error receiving data for update, errno: %d", errno); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } else if (read == 0) { - // $ man recv - // "When a stream socket peer has performed an orderly shutdown, the return value will - // be 0 (the traditional "end-of-file" return)." - ESP_LOGW(TAG, "Remote end closed connection"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - - error_code = backend->write(buf, read); - if (error_code != OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Error writing binary data to flash!, error_code: %d", error_code); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - total += read; - - uint32_t now = millis(); - if (now - last_progress > 1000) { - last_progress = now; - float percentage = (total * 100.0f) / ota_size; - ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(OTA_IN_PROGRESS, percentage, 0); -#endif - // feed watchdog and give other tasks a chance to run - App.feed_wdt(); - yield(); - } - } - - // Acknowledge receive OK - 1 byte - buf[0] = OTA_RESPONSE_RECEIVE_OK; - this->writeall_(buf, 1); - - error_code = backend->end(); - if (error_code != OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Error ending OTA!, error_code: %d", error_code); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - - // Acknowledge Update end OK - 1 byte - buf[0] = OTA_RESPONSE_UPDATE_END_OK; - this->writeall_(buf, 1); - - // Read ACK - if (!this->readall_(buf, 1) || buf[0] != OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Reading back acknowledgement failed!"); - // do not go to error, this is not fatal - } - - this->client_->close(); - this->client_ = nullptr; - delay(10); - ESP_LOGI(TAG, "OTA update finished!"); - this->status_clear_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(OTA_COMPLETED, 100.0f, 0); -#endif - delay(100); // NOLINT - App.safe_reboot(); - -error: - buf[0] = static_cast(error_code); - this->writeall_(buf, 1); - this->client_->close(); - this->client_ = nullptr; - - if (backend != nullptr && update_started) { - backend->abort(); - } - - this->status_momentary_error("onerror", 5000); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(OTA_ERROR, 0.0f, static_cast(error_code)); -#endif -} - -bool OTAComponent::readall_(uint8_t *buf, size_t len) { - uint32_t start = millis(); - uint32_t at = 0; - while (len - at > 0) { - uint32_t now = millis(); - if (now - start > 1000) { - ESP_LOGW(TAG, "Timed out reading %d bytes of data", len); - return false; - } - - ssize_t read = this->client_->read(buf + at, len - at); - if (read == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); - continue; - } - ESP_LOGW(TAG, "Failed to read %d bytes of data, errno: %d", len, errno); - return false; - } else if (read == 0) { - ESP_LOGW(TAG, "Remote closed connection"); - return false; - } else { - at += read; - } - App.feed_wdt(); - delay(1); - } - - return true; -} -bool OTAComponent::writeall_(const uint8_t *buf, size_t len) { - uint32_t start = millis(); - uint32_t at = 0; - while (len - at > 0) { - uint32_t now = millis(); - if (now - start > 1000) { - ESP_LOGW(TAG, "Timed out writing %d bytes of data", len); - return false; - } - - ssize_t written = this->client_->write(buf + at, len - at); - if (written == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); - continue; - } - ESP_LOGW(TAG, "Failed to write %d bytes of data, errno: %d", len, errno); - return false; - } else { - at += written; - } - App.feed_wdt(); - delay(1); - } - return true; -} - -float OTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } -uint16_t OTAComponent::get_port() const { return this->port_; } -void OTAComponent::set_port(uint16_t port) { this->port_ = port; } - -void OTAComponent::set_safe_mode_pending(const bool &pending) { - if (!this->has_safe_mode_) - return; - - uint32_t current_rtc = this->read_rtc_(); - - if (pending && current_rtc != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) { - ESP_LOGI(TAG, "Device will enter safe mode on next boot."); - this->write_rtc_(esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC); - } - - if (!pending && current_rtc == esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) { - ESP_LOGI(TAG, "Safe mode pending has been cleared"); - this->clean_rtc(); - } -} -bool OTAComponent::get_safe_mode_pending() { - return this->has_safe_mode_ && this->read_rtc_() == esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC; -} - -bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) { - this->has_safe_mode_ = true; - this->safe_mode_start_time_ = millis(); - this->safe_mode_enable_time_ = enable_time; - this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences->make_preference(233825507UL, false); - this->safe_mode_rtc_value_ = this->read_rtc_(); - - bool is_manual_safe_mode = this->safe_mode_rtc_value_ == esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC; - - if (is_manual_safe_mode) { - ESP_LOGI(TAG, "Safe mode has been entered manually"); - } else { - ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_); - } - - if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) { - this->clean_rtc(); - - if (!is_manual_safe_mode) { - ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode."); - } - - this->status_set_error(); - this->set_timeout(enable_time, []() { - ESP_LOGE(TAG, "No OTA attempt made, restarting."); - App.reboot(); - }); - - // Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised. - delay(300); // NOLINT - App.setup(); - - ESP_LOGI(TAG, "Waiting for OTA attempt."); - - return true; - } else { - // increment counter - this->write_rtc_(this->safe_mode_rtc_value_ + 1); - return false; - } -} -void OTAComponent::write_rtc_(uint32_t val) { - this->rtc_.save(&val); - global_preferences->sync(); -} -uint32_t OTAComponent::read_rtc_() { - uint32_t val; - if (!this->rtc_.load(&val)) - return 0; - return val; -} -void OTAComponent::clean_rtc() { this->write_rtc_(0); } -void OTAComponent::on_safe_shutdown() { - if (this->has_safe_mode_ && this->read_rtc_() != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) - this->clean_rtc(); -} - -#ifdef USE_OTA_STATE_CALLBACK -void OTAComponent::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} -#endif - -} // namespace ota -} // namespace esphome diff --git a/esphome/components/ota/ota_component.h b/esphome/components/ota/ota_component.h deleted file mode 100644 index 50d095be6c..0000000000 --- a/esphome/components/ota/ota_component.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include "esphome/components/socket/socket.h" -#include "esphome/core/component.h" -#include "esphome/core/preferences.h" -#include "esphome/core/helpers.h" -#include "esphome/core/defines.h" - -namespace esphome { -namespace ota { - -enum OTAResponseTypes { - OTA_RESPONSE_OK = 0, - OTA_RESPONSE_REQUEST_AUTH = 1, - - OTA_RESPONSE_HEADER_OK = 64, - OTA_RESPONSE_AUTH_OK = 65, - OTA_RESPONSE_UPDATE_PREPARE_OK = 66, - OTA_RESPONSE_BIN_MD5_OK = 67, - OTA_RESPONSE_RECEIVE_OK = 68, - OTA_RESPONSE_UPDATE_END_OK = 69, - OTA_RESPONSE_SUPPORTS_COMPRESSION = 70, - - OTA_RESPONSE_ERROR_MAGIC = 128, - OTA_RESPONSE_ERROR_UPDATE_PREPARE = 129, - OTA_RESPONSE_ERROR_AUTH_INVALID = 130, - OTA_RESPONSE_ERROR_WRITING_FLASH = 131, - OTA_RESPONSE_ERROR_UPDATE_END = 132, - OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 133, - OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 134, - OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 135, - OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 136, - OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137, - OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 138, - OTA_RESPONSE_ERROR_MD5_MISMATCH = 139, - OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 140, - OTA_RESPONSE_ERROR_UNKNOWN = 255, -}; - -enum OTAState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR }; - -/// OTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. -class OTAComponent : public Component { - public: - OTAComponent(); -#ifdef USE_OTA_PASSWORD - void set_auth_password(const std::string &password) { password_ = password; } -#endif // USE_OTA_PASSWORD - - /// Manually set the port OTA should listen on. - void set_port(uint16_t port); - - bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time); - - /// Set to true if the next startup will enter safe mode - void set_safe_mode_pending(const bool &pending); - bool get_safe_mode_pending(); - -#ifdef USE_OTA_STATE_CALLBACK - void add_on_state_callback(std::function &&callback); -#endif - - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - void loop() override; - - uint16_t get_port() const; - - void clean_rtc(); - - void on_safe_shutdown() override; - - protected: - void write_rtc_(uint32_t val); - uint32_t read_rtc_(); - - void handle_(); - bool readall_(uint8_t *buf, size_t len); - bool writeall_(const uint8_t *buf, size_t len); - -#ifdef USE_OTA_PASSWORD - std::string password_; -#endif // USE_OTA_PASSWORD - - uint16_t port_; - - std::unique_ptr server_; - std::unique_ptr client_; - - bool has_safe_mode_{false}; ///< stores whether safe mode can be enabled. - uint32_t safe_mode_start_time_; ///< stores when safe mode was enabled. - uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should be on for. - uint32_t safe_mode_rtc_value_; - uint8_t safe_mode_num_attempts_; - ESPPreferenceObject rtc_; - - static const uint32_t ENTER_SAFE_MODE_MAGIC = - 0x5afe5afe; ///< a magic number to indicate that safe mode should be entered on next boot - -#ifdef USE_OTA_STATE_CALLBACK - CallbackManager state_callback_{}; -#endif -}; - -extern OTAComponent *global_ota_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -} // namespace ota -} // namespace esphome diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 4f1fb33fe7..726d1ac084 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -106,4 +106,5 @@ async def output_set_level_to_code(config, action_id, template_arg, args): async def to_code(config): + cg.add_define("USE_OUTPUT") cg.add_global(output_ns.using) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 98483e8ae9..2b064a90cf 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -135,21 +135,26 @@ def _process_base_package(config: dict) -> dict: packages[file] = new_yaml except EsphomeError as e: raise cv.Invalid( - f"{file} is not a valid YAML file. Please check the file contents." + f"{file} is not a valid YAML file. Please check the file contents.\n{e}" ) from e return packages - packages = {} + packages = None + error = "" try: packages = get_packages(files) - except cv.Invalid: - if revert is not None: - revert() - packages = get_packages(files) - finally: - if packages is None: - raise cv.Invalid("Failed to load packages") + except cv.Invalid as e: + error = e + try: + if revert is not None: + revert() + packages = get_packages(files) + except cv.Invalid as er: + error = er + + if packages is None: + raise cv.Invalid(f"Failed to load packages. {error}") return {"packages": packages} diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index 574d8dce91..93be148169 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -64,7 +64,7 @@ PCA6416A_PIN_SCHEMA = cv.All( ) -@pins.PIN_SCHEMA_REGISTRY.register("pca6416a", PCA6416A_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(CONF_PCA6416A, PCA6416A_PIN_SCHEMA) async def pca6416a_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_PCA6416A]) diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index 76d6ddaf32..da31dbd9d9 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -11,9 +11,10 @@ from esphome.const import ( CONF_OUTPUT, ) -CODEOWNERS = ["@hwstar"] +CODEOWNERS = ["@hwstar", "@clydebarrow"] DEPENDENCIES = ["i2c"] MULTI_CONF = True +CONF_PIN_COUNT = "pin_count" pca9554_ns = cg.esphome_ns.namespace("pca9554") PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice) @@ -23,7 +24,12 @@ PCA9554GPIOPin = pca9554_ns.class_( CONF_PCA9554 = "pca9554" CONFIG_SCHEMA = ( - cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA9554Component)}) + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PCA9554Component), + cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16), + } + ) .extend(cv.COMPONENT_SCHEMA) .extend( i2c.i2c_device_schema(0x20) @@ -33,6 +39,7 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_pin_count(config[CONF_PIN_COUNT])) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -45,24 +52,27 @@ def validate_mode(value): return value -PCA9554_PIN_SCHEMA = cv.All( +PCA9554_PIN_SCHEMA = pins.gpio_base_schema( + PCA9554GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, +).extend( { - cv.GenerateID(): cv.declare_id(PCA9554GPIOPin), cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=8), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) -@pins.PIN_SCHEMA_REGISTRY.register("pca9554", PCA9554_PIN_SCHEMA) +def pca9554_pin_final_validate(pin_config, parent_config): + count = parent_config[CONF_PIN_COUNT] + if pin_config[CONF_NUMBER] >= count: + raise cv.Invalid(f"Pin number must be in range 0-{count - 1}") + + +@pins.PIN_SCHEMA_REGISTRY.register( + CONF_PCA9554, PCA9554_PIN_SCHEMA, pca9554_pin_final_validate +) async def pca9554_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_PCA9554]) diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index 39093fcf54..c5a4bcfb09 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -4,6 +4,7 @@ namespace esphome { namespace pca9554 { +// for 16 bit expanders, these addresses will be doubled. const uint8_t INPUT_REG = 0; const uint8_t OUTPUT_REG = 1; const uint8_t INVERT_REG = 2; @@ -13,9 +14,10 @@ static const char *const TAG = "pca9554"; void PCA9554Component::setup() { ESP_LOGCONFIG(TAG, "Setting up PCA9554/PCA9554A..."); + this->reg_width_ = (this->pin_count_ + 7) / 8; // Test to see if device exists if (!this->read_inputs_()) { - ESP_LOGE(TAG, "PCA9554 not available under 0x%02X", this->address_); + ESP_LOGE(TAG, "PCA95xx not detected at 0x%02X", this->address_); this->mark_failed(); return; } @@ -26,7 +28,7 @@ void PCA9554Component::setup() { this->config_mask_ = 0; // Invert mask as the part sees a 1 as an input this->write_register_(CONFIG_REG, ~this->config_mask_); - // All ouputs low + // All outputs low this->output_mask_ = 0; this->write_register_(OUTPUT_REG, this->output_mask_); // Read the inputs @@ -34,8 +36,17 @@ void PCA9554Component::setup() { ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), this->status_has_error()); } + +void PCA9554Component::loop() { + // The read_inputs_() method will cache the input values from the chip. + this->read_inputs_(); + // Clear all the previously read flags. + this->was_previously_read_ = 0x00; +} + void PCA9554Component::dump_config() { ESP_LOGCONFIG(TAG, "PCA9554:"); + ESP_LOGCONFIG(TAG, " I/O Pins: %d", this->pin_count_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, "Communication with PCA9554 failed!"); @@ -43,7 +54,16 @@ void PCA9554Component::dump_config() { } bool PCA9554Component::digital_read(uint8_t pin) { - this->read_inputs_(); + // Note: We want to try and avoid doing any I2C bus read transactions here + // to conserve I2C bus bandwidth. So what we do is check to see if we + // have seen a read during the time esphome is running this loop. If we have, + // we do an I2C bus transaction to get the latest value. If we haven't + // we return a cached value which was read at the time loop() was called. + if (this->was_previously_read_ & (1 << pin)) + this->read_inputs_(); // Force a read of a new value + // Indicate we saw a read request for this pin in case a + // read happens later in the same loop. + this->was_previously_read_ |= (1 << pin); return this->input_mask_ & (1 << pin); } @@ -68,25 +88,33 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) { } bool PCA9554Component::read_inputs_() { - uint8_t inputs; + uint8_t inputs[2]; if (this->is_failed()) { ESP_LOGD(TAG, "Device marked failed"); return false; } - if ((this->last_error_ = this->read_register(INPUT_REG, &inputs, 1, true)) != esphome::i2c::ERROR_OK) { + if ((this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_, true)) != + esphome::i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); return false; } this->status_clear_warning(); - this->input_mask_ = inputs; + this->input_mask_ = inputs[0]; + if (this->reg_width_ == 2) { + this->input_mask_ |= inputs[1] << 8; + } return true; } -bool PCA9554Component::write_register_(uint8_t reg, uint8_t value) { - if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) { +bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { + uint8_t outputs[2]; + outputs[0] = (uint8_t) value; + outputs[1] = (uint8_t) (value >> 8); + if ((this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_, true)) != + esphome::i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); return false; @@ -98,6 +126,10 @@ bool PCA9554Component::write_register_(uint8_t reg, uint8_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } +// Run our loop() method very early in the loop, so that we cache read values before +// before other components call our digital_read() method. +float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI + 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_; } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index d1bfc36bec..c548bec619 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -13,6 +13,8 @@ class PCA9554Component : public Component, public i2c::I2CDevice { /// Check i2c availability and setup masks void setup() override; + /// Poll for input changes periodically + void loop() override; /// Helper function to read the value of a pin. bool digital_read(uint8_t pin); /// Helper function to write the value of a pin. @@ -22,19 +24,29 @@ class PCA9554Component : public Component, public i2c::I2CDevice { float get_setup_priority() const override; + float get_loop_priority() const override; + void dump_config() override; + void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; } + protected: bool read_inputs_(); - bool write_register_(uint8_t reg, uint8_t value); + bool write_register_(uint8_t reg, uint16_t value); + /// number of bits the expander has + size_t pin_count_{8}; + /// width of registers + size_t reg_width_{1}; /// Mask for the pin config - 1 means OUTPUT, 0 means INPUT - uint8_t config_mask_{0x00}; + uint16_t config_mask_{0x00}; /// The mask to write as output state - 1 means HIGH, 0 means LOW - uint8_t output_mask_{0x00}; + uint16_t output_mask_{0x00}; /// The state of the actual input pin states - 1 means HIGH, 0 means LOW - uint8_t input_mask_{0x00}; + uint16_t input_mask_{0x00}; + /// Flags to check if read previously during this loop + uint16_t was_previously_read_ = {0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; }; diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index c61251b66f..d92312355a 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -29,13 +29,10 @@ void PCA9685Output::setup() { ESP_LOGCONFIG(TAG, "Setting up PCA9685OutputComponent..."); ESP_LOGV(TAG, " Resetting devices..."); - uint8_t address_tmp = this->address_; - this->set_i2c_address(0x00); if (!this->write_bytes(PCA9685_REGISTER_SOFTWARE_RESET, nullptr, 0)) { this->mark_failed(); return; } - this->set_i2c_address(address_tmp); if (!this->write_byte(PCA9685_REGISTER_MODE1, PCA9685_MODE1_RESTART | PCA9685_MODE1_AUTOINC)) { this->mark_failed(); diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index d0184a58d3..2c24b133da 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -1,15 +1,15 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import display, spi +import esphome.config_validation as cv from esphome.const import ( + CONF_CONTRAST, + CONF_CS_PIN, CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_RESET_PIN, - CONF_CS_PIN, - CONF_CONTRAST, ) DEPENDENCIES = ["spi"] @@ -35,11 +35,14 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "pcd8544", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) @@ -52,6 +55,6 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h index 9a69a9fec7..cfdb96de61 100644 --- a/esphome/components/pcd8544/pcd_8544.h +++ b/esphome/components/pcd8544/pcd_8544.h @@ -7,8 +7,7 @@ namespace esphome { namespace pcd8544 { -class PCD8544 : public PollingComponent, - public display::DisplayBuffer, +class PCD8544 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 5073522655..debc007cb8 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -37,7 +37,7 @@ void PCF85063Component::read_time() { ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); return; } - time::ESPTime rtc_time{ + ESPTime rtc_time{ .second = uint8_t(pcf85063_.reg.second + 10 * pcf85063_.reg.second_10), .minute = uint8_t(pcf85063_.reg.minute + 10u * pcf85063_.reg.minute_10), .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10), diff --git a/esphome/components/pcf8563/__init__.py b/esphome/components/pcf8563/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp new file mode 100644 index 0000000000..f2a82735c5 --- /dev/null +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -0,0 +1,109 @@ +#include "pcf8563.h" +#include "esphome/core/log.h" + +// Datasheet: +// - https://nl.mouser.com/datasheet/2/302/PCF8563-1127619.pdf + +namespace esphome { +namespace pcf8563 { + +static const char *const TAG = "PCF8563"; + +void PCF8563Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up PCF8563..."); + if (!this->read_rtc_()) { + this->mark_failed(); + } +} + +void PCF8563Component::update() { this->read_time(); } + +void PCF8563Component::dump_config() { + ESP_LOGCONFIG(TAG, "PCF8563:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PCF8563 failed!"); + } + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +} + +float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } + +void PCF8563Component::read_time() { + if (!this->read_rtc_()) { + return; + } + if (pcf8563_.reg.stop) { + ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); + return; + } + ESPTime rtc_time{ + .second = uint8_t(pcf8563_.reg.second + 10 * pcf8563_.reg.second_10), + .minute = uint8_t(pcf8563_.reg.minute + 10u * pcf8563_.reg.minute_10), + .hour = uint8_t(pcf8563_.reg.hour + 10u * pcf8563_.reg.hour_10), + .day_of_week = uint8_t(pcf8563_.reg.weekday), + .day_of_month = uint8_t(pcf8563_.reg.day + 10u * pcf8563_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(pcf8563_.reg.month + 10u * pcf8563_.reg.month_10), + .year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000), + .is_dst = false, // not used + .timestamp = 0, // overwritten by recalc_timestamp_utc(false) + }; + 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); +} + +void PCF8563Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + pcf8563_.reg.year = (now.year - 2000) % 10; + pcf8563_.reg.year_10 = (now.year - 2000) / 10 % 10; + pcf8563_.reg.month = now.month % 10; + pcf8563_.reg.month_10 = now.month / 10; + pcf8563_.reg.day = now.day_of_month % 10; + pcf8563_.reg.day_10 = now.day_of_month / 10; + pcf8563_.reg.weekday = now.day_of_week; + pcf8563_.reg.hour = now.hour % 10; + pcf8563_.reg.hour_10 = now.hour / 10; + pcf8563_.reg.minute = now.minute % 10; + pcf8563_.reg.minute_10 = now.minute / 10; + pcf8563_.reg.second = now.second % 10; + pcf8563_.reg.second_10 = now.second / 10; + pcf8563_.reg.stop = false; + + this->write_rtc_(); +} + +bool PCF8563Component::read_rtc_() { + if (!this->read_bytes(0, this->pcf8563_.raw, sizeof(this->pcf8563_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + return false; + } + ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u STOP:%s CLKOUT:%0u", pcf8563_.reg.hour_10, + pcf8563_.reg.hour, pcf8563_.reg.minute_10, pcf8563_.reg.minute, pcf8563_.reg.second_10, pcf8563_.reg.second, + pcf8563_.reg.year_10, pcf8563_.reg.year, pcf8563_.reg.month_10, pcf8563_.reg.month, pcf8563_.reg.day_10, + pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled); + + return true; +} + +bool PCF8563Component::write_rtc_() { + if (!this->write_bytes(0, this->pcf8563_.raw, sizeof(this->pcf8563_.raw))) { + ESP_LOGE(TAG, "Can't write I2C data."); + return false; + } + ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u OSC:%s CLKOUT:%0u", pcf8563_.reg.hour_10, + pcf8563_.reg.hour, pcf8563_.reg.minute_10, pcf8563_.reg.minute, pcf8563_.reg.second_10, pcf8563_.reg.second, + pcf8563_.reg.year_10, pcf8563_.reg.year, pcf8563_.reg.month_10, pcf8563_.reg.month, pcf8563_.reg.day_10, + pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled); + return true; +} +} // namespace pcf8563 +} // namespace esphome diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h new file mode 100644 index 0000000000..b6832efe72 --- /dev/null +++ b/esphome/components/pcf8563/pcf8563.h @@ -0,0 +1,124 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace pcf8563 { + +class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + void read_time(); + void write_time(); + + protected: + bool read_rtc_(); + bool write_rtc_(); + union PCF8563Reg { + struct { + // Control_1 register + bool : 3; + bool power_on_reset : 1; + bool : 1; + bool stop : 1; + bool : 1; + bool ext_test : 1; + + // Control_2 register + bool time_int : 1; + bool alarm_int : 1; + bool timer_flag : 1; + bool alarm_flag : 1; + bool timer_int_timer_pulse : 1; + bool : 3; + + // Seconds register + uint8_t second : 4; + uint8_t second_10 : 3; + bool clock_int : 1; + + // Minutes register + uint8_t minute : 4; + uint8_t minute_10 : 3; + uint8_t : 1; + + // Hours register + uint8_t hour : 4; + uint8_t hour_10 : 2; + uint8_t : 2; + + // Days register + uint8_t day : 4; + uint8_t day_10 : 2; + uint8_t : 2; + + // Weekdays register + uint8_t weekday : 3; + uint8_t unused_3 : 5; + + // Months register + uint8_t month : 4; + uint8_t month_10 : 1; + uint8_t : 2; + uint8_t century : 1; + + // Years register + uint8_t year : 4; + uint8_t year_10 : 4; + + // Minute Alarm register + uint8_t minute_alarm : 4; + uint8_t minute_alarm_10 : 3; + bool minute_alarm_enabled : 1; + + // Hour Alarm register + uint8_t hour_alarm : 4; + uint8_t hour_alarm_10 : 2; + uint8_t : 1; + bool hour_alarm_enabled : 1; + + // Day Alarm register + uint8_t day_alarm : 4; + uint8_t day_alarm_10 : 2; + uint8_t : 1; + bool day_alarm_enabled : 1; + + // Weekday Alarm register + uint8_t weekday_alarm : 3; + uint8_t : 4; + bool weekday_alarm_enabled : 1; + + // CLKout control register + uint8_t frequency_output : 2; + uint8_t : 5; + uint8_t clkout_enabled : 1; + + // Timer control register + uint8_t timer_source_frequency : 2; + uint8_t : 5; + uint8_t timer_enabled : 1; + + // Timer register + uint8_t countdown_period : 8; + + } reg; + mutable uint8_t raw[sizeof(reg)]; + } pcf8563_; +}; + +template class WriteAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->read_time(); } +}; +} // namespace pcf8563 +} // namespace esphome diff --git a/esphome/components/pcf8563/time.py b/esphome/components/pcf8563/time.py new file mode 100644 index 0000000000..2e6456a72d --- /dev/null +++ b/esphome/components/pcf8563/time.py @@ -0,0 +1,62 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.components import i2c, time +from esphome.const import CONF_ID + +CODEOWNERS = ["@KoenBreeman"] + +DEPENDENCIES = ["i2c"] + + +pcf8563_ns = cg.esphome_ns.namespace("pcf8563") +pcf8563Component = pcf8563_ns.class_( + "PCF8563Component", time.RealTimeClock, i2c.I2CDevice +) +WriteAction = pcf8563_ns.class_("WriteAction", automation.Action) +ReadAction = pcf8563_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(pcf8563Component), + } +).extend(i2c.i2c_device_schema(0xA3)) + + +@automation.register_action( + "pcf8563.write_time", + WriteAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(pcf8563Component), + } + ), +) +async def pcf8563_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( + "pcf8563.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(pcf8563Component), + } + ), +) +async def pcf8563_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/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index a5f963707f..ebf112b85b 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -48,24 +48,20 @@ def validate_mode(value): return value -PCF8574_PIN_SCHEMA = cv.All( +PCF8574_PIN_SCHEMA = pins.gpio_base_schema( + PCF8574GPIOPin, + cv.int_range(min=0, max=17), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(PCF8574GPIOPin), cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=17), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) -@pins.PIN_SCHEMA_REGISTRY.register("pcf8574", PCF8574_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(CONF_PCF8574, PCF8574_PIN_SCHEMA) async def pcf8574_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_PCF8574]) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 7cd414f912..2c4ef688a5 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import climate, sensor, output -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.const import CONF_HUMIDITY_SENSOR, CONF_ID, CONF_SENSOR pid_ns = cg.esphome_ns.namespace("pid") PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component) @@ -45,6 +45,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PIDClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), @@ -86,6 +87,10 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + if CONF_COOL_OUTPUT in config: out = await cg.get_variable(config[CONF_COOL_OUTPUT]) cg.add(var.set_cool_output(out)) diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index 1b3ddebcc5..28d16e17ab 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -1,5 +1,6 @@ #include "pid_autotuner.h" #include "esphome/core/log.h" +#include #ifndef M_PI #define M_PI 3.1415926535897932384626433 @@ -108,7 +109,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } uint32_t phase = this->relay_function_.phase_count; ESP_LOGVV(TAG, "%s: >", this->id_.c_str()); - ESP_LOGVV(TAG, " Phase %u, enough=%u", phase, enough_data_phase_); + ESP_LOGVV(TAG, " Phase %" PRIu32 ", enough=%" PRIu32, phase, enough_data_phase_); if (this->enough_data_phase_ == 0) { this->enough_data_phase_ = phase; @@ -186,8 +187,8 @@ void PIDAutotuner::dump_config() { 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: %u", relay_function_.phase_count); - ESP_LOGD(TAG, " Detected %u zero-crossings", frequency_detector_.zerocrossing_intervals.size()); // NOLINT + 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, amplitude_detector_.phase_max); } diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index dab4502d40..93b6999a00 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -14,6 +14,16 @@ void PIDClimate::setup() { this->update_pid_(); }); this->current_temperature = this->sensor_->state; + + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { @@ -47,6 +57,9 @@ climate::ClimateTraits PIDClimate::traits() { traits.set_supports_current_temperature(true); traits.set_supports_two_point_target_temperature(false); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); if (supports_cool_()) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index da57209a7e..b5275e9775 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -19,6 +19,7 @@ class PIDClimate : public climate::Climate, public Component { void dump_config() override; void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { humidity_sensor_ = sensor; } void set_cool_output(output::FloatOutput *cool_output) { cool_output_ = cool_output; } void set_heat_output(output::FloatOutput *heat_output) { heat_output_ = heat_output; } void set_kp(float kp) { controller_.kp_ = kp; } @@ -43,6 +44,8 @@ class PIDClimate : public climate::Climate, public Component { float get_kp() { return controller_.kp_; } float get_ki() { return controller_.ki_; } float get_kd() { return controller_.kd_; } + float get_min_integral() { return controller_.min_integral_; } + float get_max_integral() { return controller_.max_integral_; } float get_proportional_term() const { return controller_.proportional_term_; } float get_integral_term() const { return controller_.integral_term_; } float get_derivative_term() const { return controller_.derivative_term_; } @@ -85,6 +88,8 @@ class PIDClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; output::FloatOutput *cool_output_{nullptr}; output::FloatOutput *heat_output_{nullptr}; PIDController controller_; diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index afc2d91000..1a16f14542 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -16,7 +16,7 @@ float PIDController::update(float setpoint, float process_value) { calculate_proportional_term_(); calculate_integral_term_(); - calculate_derivative_term_(); + calculate_derivative_term_(setpoint); // u(t) := p(t) + i(t) + d(t) float output = proportional_term_ + integral_term_ + derivative_term_; @@ -29,7 +29,7 @@ float PIDController::update(float setpoint, float process_value) { bool PIDController::in_deadband() { // return (fabs(error) < deadband_threshold); float err = -error_; - return ((err > 0 && err < threshold_high_) || (err < 0 && err > threshold_low_)); + return (threshold_low_ < err && err < threshold_high_); } void PIDController::calculate_proportional_term_() { @@ -69,13 +69,18 @@ void PIDController::calculate_integral_term_() { integral_term_ = accumulated_integral_; } -void PIDController::calculate_derivative_term_() { +void PIDController::calculate_derivative_term_(float setpoint) { // derivative_term_ // d(t) := K_d * de(t)/dt float derivative = 0.0f; - if (dt_ != 0.0f) + if (dt_ != 0.0f) { + // remove changes to setpoint from error + if (!std::isnan(previous_setpoint_) && previous_setpoint_ != setpoint) + previous_error_ -= previous_setpoint_ - setpoint; derivative = (error_ - previous_error_) / dt_; + } previous_error_ = error_; + previous_setpoint_ = setpoint; // smooth the derivative samples derivative = weighted_average_(derivative_list_, derivative, derivative_samples_); diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 05ce5f9224..e2a7030b57 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -49,12 +49,13 @@ struct PIDController { void calculate_proportional_term_(); void calculate_integral_term_(); - void calculate_derivative_term_(); + void calculate_derivative_term_(float setpoint); float weighted_average_(std::deque &list, float new_value, int samples); float calculate_relative_time_(); /// Error from previous update used for derivative term float previous_error_ = 0; + float previous_setpoint_ = NAN; /// Accumulated integral value float accumulated_integral_ = 0; uint32_t last_time_ = 0; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index c9d1ed00f6..c4bc018b75 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -1,5 +1,6 @@ #include "pipsolar.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace pipsolar { @@ -135,6 +136,9 @@ void Pipsolar::loop() { if (this->output_source_priority_battery_switch_) { this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2); } + if (this->output_source_priority_hybrid_switch_) { + this->output_source_priority_hybrid_switch_->publish_state(value_output_source_priority_ == 3); + } if (this->charger_source_priority_) { this->charger_source_priority_->publish_state(value_charger_source_priority_); } @@ -768,7 +772,7 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) { uint8_t Pipsolar::check_incoming_crc_() { uint16_t crc16; - crc16 = cal_crc_half_(read_buffer_, read_pos_ - 3); + crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3); ESP_LOGD(TAG, "checking crc on incoming message"); if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && ((uint8_t) ((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { @@ -797,7 +801,7 @@ uint8_t Pipsolar::send_next_command_() { this->command_start_millis_ = millis(); this->empty_uart_buffer_(); this->read_pos_ = 0; - crc16 = cal_crc_half_(byte_command, length); + crc16 = this->pipsolar_crc_(byte_command, length); this->write_str(command); // checksum this->write(((uint8_t) ((crc16) >> 8))); // highbyte @@ -824,8 +828,8 @@ void Pipsolar::send_next_poll_() { this->command_start_millis_ = millis(); this->empty_uart_buffer_(); this->read_pos_ = 0; - crc16 = cal_crc_half_(this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); + crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); this->write_array(this->used_polling_commands_[this->last_polling_command_].command, this->used_polling_commands_[this->last_polling_command_].length); // checksum @@ -892,41 +896,16 @@ void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand poll } } -uint16_t Pipsolar::cal_crc_half_(uint8_t *msg, uint8_t len) { - uint16_t crc; - - uint8_t da; - uint8_t *ptr; - uint8_t b_crc_hign; - uint8_t b_crc_low; - - uint16_t crc_ta[16] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef}; - - ptr = msg; - crc = 0; - - while (len-- != 0) { - da = ((uint8_t) (crc >> 8)) >> 4; - crc <<= 4; - crc ^= crc_ta[da ^ (*ptr >> 4)]; - da = ((uint8_t) (crc >> 8)) >> 4; - crc <<= 4; - crc ^= crc_ta[da ^ (*ptr & 0x0f)]; - ptr++; - } - - b_crc_low = crc; - b_crc_hign = (uint8_t) (crc >> 8); - - if (b_crc_low == 0x28 || b_crc_low == 0x0d || b_crc_low == 0x0a) - b_crc_low++; - if (b_crc_hign == 0x28 || b_crc_hign == 0x0d || b_crc_hign == 0x0a) - b_crc_hign++; - - crc = ((uint16_t) b_crc_hign) << 8; - crc += b_crc_low; - return (crc); +uint16_t Pipsolar::pipsolar_crc_(uint8_t *msg, uint8_t len) { + uint16_t crc = crc16be(msg, len); + uint8_t crc_low = crc & 0xff; + uint8_t crc_high = crc >> 8; + if (crc_low == 0x28 || crc_low == 0x0d || crc_low == 0x0a) + crc_low++; + if (crc_high == 0x28 || crc_high == 0x0d || crc_high == 0x0a) + crc_high++; + crc = (crc_high << 8) | crc_low; + return crc; } } // namespace pipsolar diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index 65fd3c670d..373911b2d7 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -174,6 +174,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { PIPSOLAR_SWITCH(output_source_priority_utility_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_solar_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_battery_switch, QPIRI) + PIPSOLAR_SWITCH(output_source_priority_hybrid_switch, QPIRI) PIPSOLAR_SWITCH(input_voltage_range_switch, QPIRI) PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI) PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI) @@ -193,7 +194,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { void empty_uart_buffer_(); uint8_t check_incoming_crc_(); uint8_t check_incoming_length_(uint8_t length); - uint16_t cal_crc_half_(uint8_t *msg, uint8_t len); + uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len); uint8_t send_next_command_(); void send_next_poll_(); void queue_command_(const char *command, uint8_t length); diff --git a/esphome/components/pipsolar/switch/__init__.py b/esphome/components/pipsolar/switch/__init__.py index 7658c7d4f8..80bcdad62e 100644 --- a/esphome/components/pipsolar/switch/__init__.py +++ b/esphome/components/pipsolar/switch/__init__.py @@ -9,6 +9,7 @@ DEPENDENCIES = ["uart"] CONF_OUTPUT_SOURCE_PRIORITY_UTILITY = "output_source_priority_utility" CONF_OUTPUT_SOURCE_PRIORITY_SOLAR = "output_source_priority_solar" CONF_OUTPUT_SOURCE_PRIORITY_BATTERY = "output_source_priority_battery" +CONF_OUTPUT_SOURCE_PRIORITY_HYBRID = "output_source_priority_hybrid" CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" CONF_PV_POWER_BALANCE = "pv_power_balance" @@ -17,6 +18,7 @@ TYPES = { CONF_OUTPUT_SOURCE_PRIORITY_UTILITY: ("POP00", None), CONF_OUTPUT_SOURCE_PRIORITY_SOLAR: ("POP01", None), CONF_OUTPUT_SOURCE_PRIORITY_BATTERY: ("POP02", None), + CONF_OUTPUT_SOURCE_PRIORITY_HYBRID: ("POP03", None), CONF_INPUT_VOLTAGE_RANGE: ("PGR01", "PGR00"), CONF_PV_OK_CONDITION_FOR_PARALLEL: ("PPVOKC1", "PPVOKC0"), CONF_PV_POWER_BALANCE: ("PSPB1", "PSPB0"), diff --git a/esphome/components/pmsa003i/pmsa003i.cpp b/esphome/components/pmsa003i/pmsa003i.cpp index ca3d28367a..a9665c6a5a 100644 --- a/esphome/components/pmsa003i/pmsa003i.cpp +++ b/esphome/components/pmsa003i/pmsa003i.cpp @@ -13,6 +13,15 @@ void PMSA003IComponent::setup() { PM25AQIData data; bool successful_read = this->read_data_(&data); + if (!successful_read) { + for (int i = 0; i < 3; i++) { + successful_read = this->read_data_(&data); + if (successful_read) { + break; + } + } + } + if (!successful_read) { this->mark_failed(); return; diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h index 10176218ed..1fe4139951 100644 --- a/esphome/components/pmsa003i/pmsa003i.h +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -17,12 +17,12 @@ struct PM25AQIData { uint16_t pm10_env, ///< Environmental PM1.0 pm25_env, ///< Environmental PM2.5 pm100_env; ///< Environmental PM10.0 - uint16_t particles_03um, ///< 0.3um Particle Count - particles_05um, ///< 0.5um Particle Count - particles_10um, ///< 1.0um Particle Count - particles_25um, ///< 2.5um Particle Count - particles_50um, ///< 5.0um Particle Count - particles_100um; ///< 10.0um Particle Count + uint16_t particles_03um, ///> 0.3um Particle Count + particles_05um, ///> 0.5um Particle Count + particles_10um, ///> 1.0um Particle Count + particles_25um, ///> 2.5um Particle Count + particles_50um, ///> 5.0um Particle Count + particles_100um; ///> 10.0um Particle Count uint16_t unused; ///< Unused uint16_t checksum; ///< Packet checksum }; diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 04aba4382b..de2b23b8eb 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -195,7 +195,7 @@ void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) { void PMSX003Component::parse_data_() { switch (this->type_) { case PMSX003_TYPE_5003ST: { - float temperature = this->get_16_bit_uint_(30) / 10.0f; + float temperature = (int16_t) this->get_16_bit_uint_(30) / 10.0f; float humidity = this->get_16_bit_uint_(32) / 10.0f; ESP_LOGD(TAG, "Got Temperature: %.1f°C, Humidity: %.1f%%", temperature, humidity); @@ -279,7 +279,7 @@ void PMSX003Component::parse_data_() { // Note the pm particles 50um & 100um are not returned, // as PMS5003T uses those data values for temperature and humidity. - float temperature = this->get_16_bit_uint_(24) / 10.0f; + float temperature = (int16_t) this->get_16_bit_uint_(24) / 10.0f; float humidity = this->get_16_bit_uint_(26) / 10.0f; ESP_LOGD(TAG, diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index eb33f66909..cb5c16aecf 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -1,16 +1,17 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" namespace esphome { namespace pmsx003 { // known command bytes -#define PMS_CMD_AUTO_MANUAL 0xE1 // data=0: perform measurement manually, data=1: perform measurement automatically -#define PMS_CMD_TRIG_MANUAL 0xE2 // trigger a manual measurement -#define PMS_CMD_ON_STANDBY 0xE4 // data=0: go to standby mode, data=1: go to normal mode +static const uint8_t PMS_CMD_AUTO_MANUAL = + 0xE1; // data=0: perform measurement manually, data=1: perform measurement automatically +static const uint8_t PMS_CMD_TRIG_MANUAL = 0xE2; // trigger a manual measurement +static const uint8_t PMS_CMD_ON_STANDBY = 0xE4; // data=0: go to standby mode, data=1: go to normal mode static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index eefcb529f2..08ccd6096e 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -92,66 +92,78 @@ CONFIG_SCHEMA = ( icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_0_3UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_0_5UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_1_0UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_5_0UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0UM): sensor.sensor_schema( unit_of_measurement=UNIT_COUNT_DECILITRE, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/pmwcs3/__init__.py b/esphome/components/pmwcs3/__init__.py new file mode 100644 index 0000000000..b04c30a005 --- /dev/null +++ b/esphome/components/pmwcs3/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@SeByDocKy"] diff --git a/esphome/components/pmwcs3/pmwcs3.cpp b/esphome/components/pmwcs3/pmwcs3.cpp new file mode 100644 index 0000000000..97ce4c9ae0 --- /dev/null +++ b/esphome/components/pmwcs3/pmwcs3.cpp @@ -0,0 +1,116 @@ +#include "pmwcs3.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pmwcs3 { + +static const uint8_t PMWCS3_I2C_ADDRESS = 0x63; +static const uint8_t PMWCS3_REG_READ_START = 0x01; +static const uint8_t PMWCS3_REG_READ_E25 = 0x02; +static const uint8_t PMWCS3_REG_READ_EC = 0x03; +static const uint8_t PMWCS3_REG_READ_TEMP = 0x04; +static const uint8_t PMWCS3_REG_READ_VWC = 0x05; +static const uint8_t PMWCS3_REG_CALIBRATE_AIR = 0x06; +static const uint8_t PMWCS3_REG_CALIBRATE_WATER = 0x07; +static const uint8_t PMWCS3_SET_I2C_ADDRESS = 0x08; +static const uint8_t PMWCS3_REG_GET_DATA = 0x09; +static const uint8_t PMWCS3_REG_CALIBRATE_EC = 0x10; +static const uint8_t PMWCS3_REG_CAP = 0x0A; +static const uint8_t PMWCS3_REG_RES = 0x0B; +static const uint8_t PMWCS3_REG_RC = 0x0C; +static const uint8_t PMWCS3_REG_RT = 0x0D; + +static const char *const TAG = "pmwcs3"; + +void PMWCS3Component::new_i2c_address(uint8_t address) { + if (!this->write_byte(PMWCS3_SET_I2C_ADDRESS, address)) { + this->status_set_warning(); + ESP_LOGW(TAG, "couldn't write the new I2C address %d", address); + return; + } + this->set_i2c_address(address); // Allows device to continue working until new firmware is written with new address. + ESP_LOGVV(TAG, "changed I2C address to %d", address); + this->status_clear_warning(); +} + +void PMWCS3Component::air_calibration() { + if (!this->write_bytes(PMWCS3_REG_CALIBRATE_AIR, nullptr, 0)) { + this->status_set_warning(); + ESP_LOGW(TAG, "couldn't start air calibration"); + return; + } + ESP_LOGW(TAG, "Start air calibration during the next 300s"); +} +void PMWCS3Component::water_calibration() { + if (!this->write_bytes(PMWCS3_REG_CALIBRATE_WATER, nullptr, 0)) { + this->status_set_warning(); + ESP_LOGW(TAG, "couldn't start water calibration"); + return; + } + ESP_LOGW(TAG, "Start water calibration during the next 300s"); +} + +void PMWCS3Component::setup() { ESP_LOGCONFIG(TAG, "Setting up PMWCS3..."); } + +void PMWCS3Component::update() { this->read_data_(); } + +float PMWCS3Component::get_setup_priority() const { return setup_priority::DATA; } + +void PMWCS3Component::dump_config() { + ESP_LOGCONFIG(TAG, "PMWCS3"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PMWCS3 failed!"); + } + ESP_LOGI(TAG, "%s", this->is_failed() ? "FAILED" : "OK"); + + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "e25", this->e25_sensor_); + LOG_SENSOR(" ", "ec", this->ec_sensor_); + LOG_SENSOR(" ", "temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "vwc", this->vwc_sensor_); +} +void PMWCS3Component::read_data_() { + /////// Super important !!!! first activate reading PMWCS3_REG_READ_START (if not, return always the same values) //// + if (!this->write_bytes(PMWCS3_REG_READ_START, nullptr, 0)) { + this->status_set_warning(); + ESP_LOGVV(TAG, "Failed to write into REG_READ_START register !!!"); + return; + } + + // Wait for the sensor to be ready. + // 80ms empirically determined (conservative). + this->set_timeout(80, [this] { + uint8_t data[8]; + float e25, ec, temperature, vwc; + if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { + ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); + this->mark_failed(); + return; + } + if (this->e25_sensor_ != nullptr) { + e25 = ((data[1] << 8) | data[0]) / 100.0; + this->e25_sensor_->publish_state(e25); + ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); + } + if (this->ec_sensor_ != nullptr) { + ec = ((data[3] << 8) | data[2]) / 10.0; + this->ec_sensor_->publish_state(ec); + ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); + } + if (this->temperature_sensor_ != nullptr) { + temperature = ((data[5] << 8) | data[4]) / 100.0; + this->temperature_sensor_->publish_state(temperature); + ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); + } + if (this->vwc_sensor_ != nullptr) { + vwc = ((data[7] << 8) | data[6]) / 10.0; + this->vwc_sensor_->publish_state(vwc); + ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); + } + }); +} + +} // namespace pmwcs3 +} // namespace esphome diff --git a/esphome/components/pmwcs3/pmwcs3.h b/esphome/components/pmwcs3/pmwcs3.h new file mode 100644 index 0000000000..f3916bb868 --- /dev/null +++ b/esphome/components/pmwcs3/pmwcs3.h @@ -0,0 +1,70 @@ +#pragma once +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +// ref: +// https://github.com/tinovi/i2cArduino/blob/master/i2cArduino.h + +namespace esphome { +namespace pmwcs3 { + +class PMWCS3Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_e25_sensor(sensor::Sensor *e25_sensor) { e25_sensor_ = e25_sensor; } + void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_vwc_sensor(sensor::Sensor *vwc_sensor) { vwc_sensor_ = vwc_sensor; } + + void new_i2c_address(uint8_t newaddress); + void air_calibration(); + void water_calibration(); + + protected: + void read_data_(); + + sensor::Sensor *e25_sensor_{nullptr}; + sensor::Sensor *ec_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *vwc_sensor_{nullptr}; +}; + +template class PMWCS3AirCalibrationAction : public Action { + public: + PMWCS3AirCalibrationAction(PMWCS3Component *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->air_calibration(); } + + protected: + PMWCS3Component *parent_; +}; + +template class PMWCS3WaterCalibrationAction : public Action { + public: + PMWCS3WaterCalibrationAction(PMWCS3Component *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->water_calibration(); } + + protected: + PMWCS3Component *parent_; +}; + +template class PMWCS3NewI2cAddressAction : public Action { + public: + PMWCS3NewI2cAddressAction(PMWCS3Component *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(int, new_address) + + void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } + + protected: + PMWCS3Component *parent_; +}; + +} // namespace pmwcs3 +} // namespace esphome diff --git a/esphome/components/pmwcs3/sensor.py b/esphome/components/pmwcs3/sensor.py new file mode 100644 index 0000000000..81be327d14 --- /dev/null +++ b/esphome/components/pmwcs3/sensor.py @@ -0,0 +1,140 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_ADDRESS, + CONF_TEMPERATURE, + CONF_EC, + STATE_CLASS_MEASUREMENT, + ICON_THERMOMETER, +) + +CODEOWNERS = ["@SeByDocKy"] +DEPENDENCIES = ["i2c"] + +CONF_E25 = "e25" +CONF_VWC = "vwc" + +ICON_EPSILON = "mdi:epsilon" +ICON_SIGMA = "mdi:sigma-lower" +ICON_ALPHA = "mdi:alpha-h-circle-outline" + +pmwcs3_ns = cg.esphome_ns.namespace("pmwcs3") +PMWCS3Component = pmwcs3_ns.class_( + "PMWCS3Component", cg.PollingComponent, i2c.I2CDevice +) + +# Actions +PMWCS3AirCalibrationAction = pmwcs3_ns.class_( + "PMWCS3AirCalibrationAction", automation.Action +) +PMWCS3WaterCalibrationAction = pmwcs3_ns.class_( + "PMWCS3WaterCalibrationAction", automation.Action +) +PMWCS3NewI2cAddressAction = pmwcs3_ns.class_( + "PMWCS3NewI2cAddressAction", automation.Action +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PMWCS3Component), + cv.Optional(CONF_E25): sensor.sensor_schema( + icon=ICON_EPSILON, + accuracy_decimals=3, + unit_of_measurement="dS/m", + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EC): sensor.sensor_schema( + icon=ICON_SIGMA, + accuracy_decimals=2, + unit_of_measurement="mS/m", + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + icon=ICON_THERMOMETER, + accuracy_decimals=3, + unit_of_measurement="°C", + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VWC): sensor.sensor_schema( + icon=ICON_ALPHA, + accuracy_decimals=3, + unit_of_measurement="cm3cm−3", + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x63)) +) + + +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) + + if CONF_E25 in config: + sens = await sensor.new_sensor(config[CONF_E25]) + cg.add(var.set_e25_sensor(sens)) + + if CONF_EC in config: + sens = await sensor.new_sensor(config[CONF_EC]) + cg.add(var.set_ec_sensor(sens)) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_VWC in config: + sens = await sensor.new_sensor(config[CONF_VWC]) + cg.add(var.set_vwc_sensor(sens)) + + +# Actions +PMWCS3_CALIBRATION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PMWCS3Component), + } +) + + +@automation.register_action( + "pmwcs3.air_calibration", + PMWCS3AirCalibrationAction, + PMWCS3_CALIBRATION_SCHEMA, +) +@automation.register_action( + "pmwcs3.water_calibration", + PMWCS3WaterCalibrationAction, + PMWCS3_CALIBRATION_SCHEMA, +) +async def pmwcs3_calibration_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + return var + + +PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(PMWCS3Component), + cv.Required(CONF_ADDRESS): cv.templatable(cv.i2c_address), + }, + key=CONF_ADDRESS, +) + + +@automation.register_action( + "pmwcs3.new_i2c_address", + PMWCS3NewI2cAddressAction, + PMWCS3_NEW_I2C_ADDRESS_SCHEMA, +) +async def pmwcs3newi2caddress_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + address = await cg.templatable(config[CONF_ADDRESS], args, int) + cg.add(var.set_new_address(address)) + return var diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index 2f120bc983..cdcaf4267c 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -2,14 +2,19 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import nfc -from esphome.const import CONF_ID, CONF_ON_TAG_REMOVED, CONF_ON_TAG, CONF_TRIGGER_ID +from esphome.const import ( + CONF_ID, + CONF_ON_FINISHED_WRITE, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) CODEOWNERS = ["@OttoWinter", "@jesserockz"] AUTO_LOAD = ["binary_sensor", "nfc"] MULTI_CONF = True CONF_PN532_ID = "pn532_id" -CONF_ON_FINISHED_WRITE = "on_finished_write" pn532_ns = cg.esphome_ns.namespace("pn532") PN532 = pn532_ns.class_("PN532", cg.PollingComponent) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index cc28d7078b..8088e6c022 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -127,8 +127,18 @@ void PN532::loop() { if (!this->requested_read_) return; + auto ready = this->read_ready_(false); + if (ready == WOULDBLOCK) + return; + + bool success = false; std::vector read; - bool success = this->read_response(PN532_COMMAND_INLISTPASSIVETARGET, read); + + if (ready == READY) { + success = this->read_response(PN532_COMMAND_INLISTPASSIVETARGET, read); + } else { + this->send_ack_(); // abort still running InListPassiveTarget + } this->requested_read_ = false; @@ -286,12 +296,58 @@ bool PN532::read_ack_() { return matches; } +void PN532::send_ack_() { + ESP_LOGV(TAG, "Sending ACK for abort"); + this->write_data({0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00}); + delay(10); +} void PN532::send_nack_() { ESP_LOGV(TAG, "Sending NACK for retransmit"); this->write_data({0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00}); delay(10); } +enum PN532ReadReady PN532::read_ready_(bool block) { + if (this->rd_ready_ == READY) { + if (block) { + this->rd_start_time_ = 0; + this->rd_ready_ = WOULDBLOCK; + } + return READY; + } + + if (!this->rd_start_time_) { + this->rd_start_time_ = millis(); + } + + while (true) { + if (this->is_read_ready()) { + this->rd_ready_ = READY; + break; + } + + if (millis() - this->rd_start_time_ > 100) { + ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); + this->rd_ready_ = TIMEOUT; + break; + } + + if (!block) { + this->rd_ready_ = WOULDBLOCK; + break; + } + + yield(); + } + + auto rdy = this->rd_ready_; + if (block || rdy == TIMEOUT) { + this->rd_start_time_ = 0; + this->rd_ready_ = WOULDBLOCK; + } + return rdy; +} + void PN532::turn_off_rf_() { ESP_LOGV(TAG, "Turning RF field OFF"); this->write_command_({ diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 73b349e328..8194d86477 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -7,6 +7,7 @@ #include "esphome/components/nfc/nfc.h" #include "esphome/components/nfc/automation.h" +#include #include namespace esphome { @@ -19,6 +20,12 @@ static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; static const uint8_t PN532_COMMAND_POWERDOWN = 0x16; +enum PN532ReadReady { + WOULDBLOCK = 0, + TIMEOUT, + READY, +}; + class PN532BinarySensor; class PN532 : public PollingComponent { @@ -53,8 +60,11 @@ class PN532 : public PollingComponent { void turn_off_rf_(); bool write_command_(const std::vector &data); bool read_ack_(); + void send_ack_(); void send_nack_(); + enum PN532ReadReady read_ready_(bool block); + virtual bool is_read_ready() = 0; virtual bool write_data(const std::vector &data) = 0; virtual bool read_data(std::vector &data, uint8_t len) = 0; virtual bool read_response(uint8_t command, std::vector &data) = 0; @@ -74,10 +84,11 @@ class PN532 : public PollingComponent { bool write_mifare_classic_tag_(std::vector &uid, nfc::NdefMessage *message); std::unique_ptr read_mifare_ultralight_tag_(std::vector &uid); - bool read_mifare_ultralight_page_(uint8_t page_num, std::vector &data); - bool is_mifare_ultralight_formatted_(); + bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); uint16_t read_mifare_ultralight_capacity_(); - bool find_mifare_ultralight_ndef_(uint8_t &message_length, uint8_t &message_start_index); + bool find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); bool write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); bool write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message); bool clean_mifare_ultralight_(); @@ -89,6 +100,8 @@ class PN532 : public PollingComponent { std::vector triggers_ontagremoved_; std::vector current_uid_; nfc::NdefMessage *next_task_message_to_write_; + uint32_t rd_start_time_{0}; + enum PN532ReadReady rd_ready_ { WOULDBLOCK }; enum NfcTask { READ = 0, CLEAN, diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index 1b91ae919e..b08a7336c7 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -9,93 +9,104 @@ namespace pn532 { static const char *const TAG = "pn532.mifare_ultralight"; std::unique_ptr PN532::read_mifare_ultralight_tag_(std::vector &uid) { - if (!this->is_mifare_ultralight_formatted_()) { - ESP_LOGD(TAG, "Not NDEF formatted"); + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data)) { + return make_unique(uid, nfc::NFC_FORUM_TYPE_2); + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); return make_unique(uid, nfc::NFC_FORUM_TYPE_2); } uint8_t message_length; uint8_t message_start_index; - if (!this->find_mifare_ultralight_ndef_(message_length, message_start_index)) { + if (!this->find_mifare_ultralight_ndef_(data, message_length, message_start_index)) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); return make_unique(uid, nfc::NFC_FORUM_TYPE_2); } - ESP_LOGVV(TAG, "message length: %d, start: %d", message_length, message_start_index); + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); if (message_length == 0) { return make_unique(uid, nfc::NFC_FORUM_TYPE_2); } - std::vector data; - for (uint8_t page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; page < nfc::MIFARE_ULTRALIGHT_MAX_PAGE; page++) { - std::vector page_data; - if (!this->read_mifare_ultralight_page_(page, page_data)) { - ESP_LOGE(TAG, "Error reading page %d", page); + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (!read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data)) { + ESP_LOGE(TAG, "Error reading tag data"); return make_unique(uid, nfc::NFC_FORUM_TYPE_2); } - data.insert(data.end(), page_data.begin(), page_data.end()); - - if (data.size() >= (message_length + message_start_index)) - break; } - - data.erase(data.begin(), data.begin() + message_start_index); - data.erase(data.begin() + message_length, data.end()); + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); return make_unique(uid, nfc::NFC_FORUM_TYPE_2, data); } -bool PN532::read_mifare_ultralight_page_(uint8_t page_num, std::vector &data) { - if (!this->write_command_({ - PN532_COMMAND_INDATAEXCHANGE, - 0x01, // One card - nfc::MIFARE_CMD_READ, - page_num, - })) { - return false; +bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + std::vector response; + + for (uint8_t i = 0; i * read_increment < num_bytes; i++) { + if (!this->write_command_({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_READ, + uint8_t(i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page), + })) { + return false; + } + + if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response) || response[0] != 0x00) { + return false; + } + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? response.end() : response.end() - (bytes_offset - num_bytes); + + if ((pages_in_end_itr > response.begin()) && (pages_in_end_itr <= response.end())) { + data.insert(data.end(), response.begin() + 1, pages_in_end_itr); + } } - if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, data) || data[0] != 0x00) { - return false; - } - data.erase(data.begin()); - // We only want 1 page of data but the PN532 returns 4 at once. - data.erase(data.begin() + 4, data.end()); - - ESP_LOGVV(TAG, "Pages %d-%d: %s", page_num, page_num + 4, nfc::format_bytes(data).c_str()); + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); return true; } -bool PN532::is_mifare_ultralight_formatted_() { - std::vector data; - if (this->read_mifare_ultralight_page_(4, data)) { - return !(data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[3] == 0xFF); - } - return true; +bool PN532::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); } uint16_t PN532::read_mifare_ultralight_capacity_() { std::vector data; - if (this->read_mifare_ultralight_page_(3, data)) { + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data)) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); return data[2] * 8U; } return 0; } -bool PN532::find_mifare_ultralight_ndef_(uint8_t &message_length, uint8_t &message_start_index) { - std::vector data; - for (int page = 4; page < 6; page++) { - std::vector page_data; - if (!this->read_mifare_ultralight_page_(page, page_data)) { - return false; - } - data.insert(data.end(), page_data.begin(), page_data.end()); +bool PN532::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return false; } - if (data[0] == 0x03) { - message_length = data[1]; + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; message_start_index = 2; return true; - } else if (data[5] == 0x03) { - message_length = data[6]; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; message_start_index = 7; return true; } @@ -111,7 +122,7 @@ bool PN532::write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMes uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); if (buffer_length > capacity) { - ESP_LOGE(TAG, "Message length exceeds tag capacity %d > %d", buffer_length, capacity); + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); return false; } @@ -164,13 +175,13 @@ bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector }); data.insert(data.end(), write_data.begin(), write_data.end()); if (!this->write_command_(data)) { - ESP_LOGE(TAG, "Error writing page %d", page_num); + ESP_LOGE(TAG, "Error writing page %u", page_num); return false; } std::vector response; if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response)) { - ESP_LOGE(TAG, "Error writing page %d", page_num); + ESP_LOGE(TAG, "Error writing page %u", page_num); return false; } diff --git a/esphome/components/pn532_i2c/__init__.py b/esphome/components/pn532_i2c/__init__.py index 36af2f8aa0..f7b8743967 100644 --- a/esphome/components/pn532_i2c/__init__.py +++ b/esphome/components/pn532_i2c/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID AUTO_LOAD = ["pn532"] CODEOWNERS = ["@OttoWinter", "@jesserockz"] DEPENDENCIES = ["i2c"] +MULTI_CONF = True pn532_i2c_ns = cg.esphome_ns.namespace("pn532_i2c") PN532I2C = pn532_i2c_ns.class_("PN532I2C", pn532.PN532, i2c.I2CDevice) diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index e7c99e94b0..b306222a21 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -12,6 +12,14 @@ namespace pn532_i2c { static const char *const TAG = "pn532_i2c"; +bool PN532I2C::is_read_ready() { + uint8_t ready; + if (!this->read_bytes_raw(&ready, 1)) { + return false; + } + return ready == 0x01; +} + bool PN532I2C::write_data(const std::vector &data) { return this->write(data.data(), data.size()) == i2c::ERROR_OK; } @@ -19,19 +27,8 @@ bool PN532I2C::write_data(const std::vector &data) { bool PN532I2C::read_data(std::vector &data, uint8_t len) { delay(1); - std::vector ready; - ready.resize(1); - uint32_t start_time = millis(); - while (true) { - if (this->read_bytes_raw(ready.data(), 1)) { - if (ready[0] == 0x01) - break; - } - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } data.resize(len + 1); diff --git a/esphome/components/pn532_i2c/pn532_i2c.h b/esphome/components/pn532_i2c/pn532_i2c.h index 95cf8eeb36..00c0df206d 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.h +++ b/esphome/components/pn532_i2c/pn532_i2c.h @@ -14,6 +14,7 @@ class PN532I2C : public pn532::PN532, public i2c::I2CDevice { void dump_config() override; protected: + bool is_read_ready() override; bool write_data(const std::vector &data) override; bool read_data(std::vector &data, uint8_t len) override; bool read_response(uint8_t command, std::vector &data) override; diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index be58f265b9..d55d8161d8 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -21,6 +21,14 @@ void PN532Spi::setup() { PN532::setup(); } +bool PN532Spi::is_read_ready() { + this->enable(); + this->write_byte(0x02); + bool ready = this->read_byte() == 0x01; + this->disable(); + return ready; +} + bool PN532Spi::write_data(const std::vector &data) { this->enable(); delay(2); @@ -34,24 +42,8 @@ bool PN532Spi::write_data(const std::vector &data) { } bool PN532Spi::read_data(std::vector &data, uint8_t len) { - ESP_LOGV(TAG, "Waiting for ready byte..."); - - uint32_t start_time = millis(); - while (true) { - this->enable(); - // First byte, communication mode: Read state - this->write_byte(0x02); - bool ready = this->read_byte() == 0x01; - this->disable(); - if (ready) - break; - ESP_LOGV(TAG, "Not ready yet..."); - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } - yield(); + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } // Read data (transmission from the PN532 to the host) @@ -72,22 +64,8 @@ bool PN532Spi::read_data(std::vector &data, uint8_t len) { bool PN532Spi::read_response(uint8_t command, std::vector &data) { ESP_LOGV(TAG, "Reading response"); - uint32_t start_time = millis(); - while (true) { - this->enable(); - // First byte, communication mode: Read state - this->write_byte(0x02); - bool ready = this->read_byte() == 0x01; - this->disable(); - if (ready) - break; - ESP_LOGV(TAG, "Not ready yet..."); - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } - yield(); + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } this->enable(); diff --git a/esphome/components/pn532_spi/pn532_spi.h b/esphome/components/pn532_spi/pn532_spi.h index 2d8312813d..b7adca22e9 100644 --- a/esphome/components/pn532_spi/pn532_spi.h +++ b/esphome/components/pn532_spi/pn532_spi.h @@ -18,6 +18,7 @@ class PN532Spi : public pn532::PN532, void dump_config() override; protected: + bool is_read_ready() override; bool write_data(const std::vector &data) override; bool read_data(std::vector &data, uint8_t len) override; bool read_response(uint8_t command, std::vector &data) override; diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py new file mode 100644 index 0000000000..e3589ea449 --- /dev/null +++ b/esphome/components/pn7150/__init__.py @@ -0,0 +1,215 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import nfc +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_MESSAGE, + CONF_ON_FINISHED_WRITE, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["binary_sensor", "nfc"] +CODEOWNERS = ["@kbx81", "@jesserockz"] + +CONF_EMULATION_MESSAGE = "emulation_message" +CONF_EMULATION_OFF = "emulation_off" +CONF_EMULATION_ON = "emulation_on" +CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" +CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan" +CONF_PN7150_ID = "pn7150_id" +CONF_POLLING_OFF = "polling_off" +CONF_POLLING_ON = "polling_on" +CONF_SET_CLEAN_MODE = "set_clean_mode" +CONF_SET_EMULATION_MESSAGE = "set_emulation_message" +CONF_SET_FORMAT_MODE = "set_format_mode" +CONF_SET_READ_MODE = "set_read_mode" +CONF_SET_WRITE_MESSAGE = "set_write_message" +CONF_SET_WRITE_MODE = "set_write_mode" +CONF_TAG_TTL = "tag_ttl" +CONF_VEN_PIN = "ven_pin" + +pn7150_ns = cg.esphome_ns.namespace("pn7150") +PN7150 = pn7150_ns.class_("PN7150", nfc.Nfcc, cg.Component) + +EmulationOffAction = pn7150_ns.class_("EmulationOffAction", automation.Action) +EmulationOnAction = pn7150_ns.class_("EmulationOnAction", automation.Action) +PollingOffAction = pn7150_ns.class_("PollingOffAction", automation.Action) +PollingOnAction = pn7150_ns.class_("PollingOnAction", automation.Action) +SetCleanModeAction = pn7150_ns.class_("SetCleanModeAction", automation.Action) +SetEmulationMessageAction = pn7150_ns.class_( + "SetEmulationMessageAction", automation.Action +) +SetFormatModeAction = pn7150_ns.class_("SetFormatModeAction", automation.Action) +SetReadModeAction = pn7150_ns.class_("SetReadModeAction", automation.Action) +SetWriteMessageAction = pn7150_ns.class_("SetWriteMessageAction", automation.Action) +SetWriteModeAction = pn7150_ns.class_("SetWriteModeAction", automation.Action) + + +PN7150OnEmulatedTagScanTrigger = pn7150_ns.class_( + "PN7150OnEmulatedTagScanTrigger", automation.Trigger.template() +) + +PN7150OnFinishedWriteTrigger = pn7150_ns.class_( + "PN7150OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN7150IsWritingCondition = pn7150_ns.class_( + "PN7150IsWritingCondition", automation.Condition +) + + +IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition) + + +SIMPLE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PN7150), + } +) + +SET_MESSAGE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7150), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean, + } +) + +PN7150_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN7150), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7150OnEmulatedTagScanTrigger + ), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7150OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EMULATION_MESSAGE): cv.string, + cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "tag.set_emulation_message", + SetEmulationMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +@automation.register_action( + "tag.set_write_message", + SetWriteMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +async def pn7150_set_message_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_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + template_ = await cg.templatable( + config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_ + ) + cg.add(var.set_include_android_app_record(template_)) + return var + + +@automation.register_action( + "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action( + "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA +) +async def pn7150_simple_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 + + +async def setup_pn7150(var, config): + await cg.register_component(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN]) + cg.add(var.set_ven_pin(pin)) + + if emulation_message_config := config.get(CONF_EMULATION_MESSAGE): + cg.add(var.set_tag_emulation_message(emulation_message_config)) + cg.add(var.set_tag_emulation_on()) + + if CONF_TAG_TTL in config: + cg.add(var.set_tag_ttl(config[CONF_TAG_TTL])) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn7150.is_writing", + PN7150IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7150), + } + ), +) +async def pn7150_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h new file mode 100644 index 0000000000..aebb1b7573 --- /dev/null +++ b/esphome/components/pn7150/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/pn7150/pn7150.h" + +namespace esphome { +namespace pn7150 { + +class PN7150OnEmulatedTagScanTrigger : public Trigger<> { + public: + explicit PN7150OnEmulatedTagScanTrigger(PN7150 *parent) { + parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); + } +}; + +class PN7150OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN7150OnFinishedWriteTrigger(PN7150 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN7150IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } +}; + +template class EmulationOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } +}; + +template class EmulationOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } +}; + +template class PollingOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_off(); } +}; + +template class PollingOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_on(); } +}; + +template class SetCleanModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->clean_mode(); } +}; + +template class SetFormatModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->format_mode(); } +}; + +template class SetReadModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->read_mode(); } +}; + +template class SetEmulationMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_write_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->write_mode(); } +}; + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp new file mode 100644 index 0000000000..be4d6c1bb7 --- /dev/null +++ b/esphome/components/pn7150/pn7150.cpp @@ -0,0 +1,1143 @@ +#include "automation.h" +#include "pn7150.h" + +#include + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150"; + +void PN7150::setup() { + this->irq_pin_->setup(); + this->ven_pin_->setup(); + + this->nci_fsm_transition_(); // kick off reset & init processes +} + +void PN7150::dump_config() { + ESP_LOGCONFIG(TAG, "PN7150:"); + LOG_PIN(" IRQ pin: ", this->irq_pin_); + LOG_PIN(" VEN pin: ", this->ven_pin_); +} + +void PN7150::loop() { + this->nci_fsm_transition_(); + this->purge_old_tags_(); +} + +void PN7150::set_tag_emulation_message(std::shared_ptr message) { + this->card_emulation_message_ = std::move(message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7150::set_tag_emulation_message(const optional &message, + const optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->card_emulation_message_ = std::move(ndef_message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7150::set_tag_emulation_message(const char *message, const bool include_android_app_record) { + this->set_tag_emulation_message(std::string(message), include_android_app_record); +} + +void PN7150::set_tag_emulation_off() { + if (this->listening_enabled_) { + this->listening_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation disabled"); +} + +void PN7150::set_tag_emulation_on() { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation cannot be enabled"); + return; + } + if (!this->listening_enabled_) { + this->listening_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation enabled"); +} + +void PN7150::set_polling_off() { + if (this->polling_enabled_) { + this->polling_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling disabled"); +} + +void PN7150::set_polling_on() { + if (!this->polling_enabled_) { + this->polling_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling enabled"); +} + +void PN7150::read_mode() { + this->next_task_ = EP_READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} + +void PN7150::clean_mode() { + this->next_task_ = EP_CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} + +void PN7150::format_mode() { + this->next_task_ = EP_FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} + +void PN7150::write_mode() { + if (this->next_task_message_to_write_ == nullptr) { + ESP_LOGW(TAG, "Message to write must be set before setting write mode"); + return; + } + + this->next_task_ = EP_WRITE; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +void PN7150::set_tag_write_message(std::shared_ptr message) { + this->next_task_message_to_write_ = std::move(message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +void PN7150::set_tag_write_message(optional message, optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->next_task_message_to_write_ = std::move(ndef_message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +uint8_t PN7150::set_test_mode(const TestMode test_mode, const std::vector &data, + std::vector &result) { + auto test_oid = TEST_PRBS_OID; + + switch (test_mode) { + case TestMode::TEST_PRBS: + // test_oid = TEST_PRBS_OID; + break; + + case TestMode::TEST_ANTENNA: + test_oid = TEST_ANTENNA_OID; + break; + + case TestMode::TEST_GET_REGISTER: + test_oid = TEST_GET_REGISTER_OID; + break; + + case TestMode::TEST_NONE: + default: + ESP_LOGD(TAG, "Exiting test mode"); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_OK; + } + + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::TEST); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, test_oid, data); + + ESP_LOGW(TAG, "Starting test mode, OID 0x%02X", test_oid); + auto status = this->transceive_(tx, rx, NFCC_INIT_TIMEOUT); + + if (status != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to start test mode, OID 0x%02X", test_oid); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + result.clear(); + } else { + result = rx.get_message(); + result.erase(result.begin(), result.begin() + 4); // remove NCI header + if (!result.empty()) { + ESP_LOGW(TAG, "Test results: %s", nfc::format_bytes(result).c_str()); + } + } + return status; +} + +uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { + if (power) { + this->ven_pin_->digital_write(true); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(true); + delay(NFCC_INIT_TIMEOUT); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_RESET_OID, + {(uint8_t) reset_config}); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending reset command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid reset response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return rx.get_simple_status_response(); + } + // verify reset response + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_RESPONSE)) || (!rx.message_length_is(3)) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] != 0x11) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] != (uint8_t) reset_config)) { + ESP_LOGE(TAG, "Reset response was malformed: %s", nfc::format_bytes(rx.get_message()).c_str()); + 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"); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::init_core_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_INIT_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending initialise command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid initialise response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + uint8_t manf_id = rx.get_message()[15 + rx.get_message()[8]]; + uint8_t hw_version = rx.get_message()[16 + rx.get_message()[8]]; + uint8_t rom_code_version = rx.get_message()[17 + rx.get_message()[8]]; + 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); + + return rx.get_simple_status_response(); +} + +uint8_t PN7150::send_init_config_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, nfc::NCI_CORE_SET_CONFIG_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error enabling proprietary extensions"); + return nfc::STATUS_FAILED; + } + + tx.set_message(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(std::begin(PMU_CFG), std::end(PMU_CFG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending PMU config"); + return nfc::STATUS_FAILED; + } + + return this->send_core_config_(); +} + +uint8_t PN7150::send_core_config_() { + const auto *core_config_begin = std::begin(CORE_CONFIG_SOLO); + const auto *core_config_end = std::end(CORE_CONFIG_SOLO); + this->core_config_is_solo_ = true; + + if (this->listening_enabled_ && this->polling_enabled_) { + core_config_begin = std::begin(CORE_CONFIG_RW_CE); + core_config_end = std::end(CORE_CONFIG_RW_CE); + this->core_config_is_solo_ = false; + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(core_config_begin, core_config_end)); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error sending core config"); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::refresh_core_config_() { + bool core_config_should_be_solo = !(this->listening_enabled_ && this->polling_enabled_); + + if (this->nci_state_ == NCIState::RFST_DISCOVERY) { + if (this->stop_discovery_() != nfc::STATUS_OK) { + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_FAILED; + } + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + + if (this->core_config_is_solo_ != core_config_should_be_solo) { + if (this->send_core_config_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to refresh core config"); + return nfc::STATUS_FAILED; + } + } + this->config_refresh_pending_ = false; + return nfc::STATUS_OK; +} + +uint8_t PN7150::set_discover_map_() { + std::vector discover_map = {sizeof(RF_DISCOVER_MAP_CONFIG) / 3}; + discover_map.insert(discover_map.end(), std::begin(RF_DISCOVER_MAP_CONFIG), std::end(RF_DISCOVER_MAP_CONFIG)); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_MAP_OID, discover_map); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending discover map poll config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::set_listen_mode_routing_() { + nfc::NciMessage rx; + nfc::NciMessage tx( + nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_SET_LISTEN_MODE_ROUTING_OID, + std::vector(std::begin(RF_LISTEN_MODE_ROUTING_CONFIG), std::end(RF_LISTEN_MODE_ROUTING_CONFIG))); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error setting listen mode routing config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::start_discovery_() { + const uint8_t *rf_discovery_config = RF_DISCOVERY_CONFIG; + uint8_t length = sizeof(RF_DISCOVERY_CONFIG); + + if (!this->listening_enabled_) { + length = sizeof(RF_DISCOVERY_POLL_CONFIG); + rf_discovery_config = RF_DISCOVERY_POLL_CONFIG; + } else if (!this->polling_enabled_) { + length = sizeof(RF_DISCOVERY_LISTEN_CONFIG); + rf_discovery_config = RF_DISCOVERY_LISTEN_CONFIG; + } + + std::vector discover_config = std::vector((length * 2) + 1); + + discover_config[0] = length; + for (uint8_t i = 0; i < length; i++) { + discover_config[(i * 2) + 1] = rf_discovery_config[i]; + discover_config[(i * 2) + 2] = 0x01; // RF Technology and Mode will be executed in every discovery period + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, discover_config); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + switch (rx.get_simple_status_response()) { + // in any of these cases, we are either already in or will remain in discovery, which satisfies the function call + case nfc::STATUS_OK: + case nfc::DISCOVERY_ALREADY_STARTED: + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + case nfc::DISCOVERY_TEAR_DOWN: + return nfc::STATUS_OK; + + default: + ESP_LOGE(TAG, "Error starting discovery"); + return nfc::STATUS_FAILED; + } + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::stop_discovery_() { return this->deactivate_(nfc::DEACTIVATION_TYPE_IDLE, NFCC_TAG_WRITE_TIMEOUT); } + +uint8_t PN7150::deactivate_(const uint8_t type, const uint16_t timeout) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {type}); + + auto status = this->transceive_(tx, rx, timeout); + // if (status != nfc::STATUS_OK) { + // ESP_LOGE(TAG, "Error sending deactivate type %u", type); + // return nfc::STATUS_FAILED; + // } + return status; +} + +void PN7150::select_endpoint_() { + if (this->discovered_endpoint_.empty()) { + ESP_LOGW(TAG, "No cached tags to select"); + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + return; + } + std::vector endpoint_data = {this->discovered_endpoint_[0].id, this->discovered_endpoint_[0].protocol, + 0x01}; // that last byte is the interface ID + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (!this->discovered_endpoint_[i].trig_called) { + endpoint_data = {this->discovered_endpoint_[i].id, this->discovered_endpoint_[i].protocol, + 0x01}; // that last byte is the interface ID + this->selecting_endpoint_ = i; + break; + } + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_SELECT_OID, endpoint_data); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error selecting endpoint"); + } else { + this->nci_fsm_set_state_(NCIState::EP_SELECTING); + } +} + +uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) { + uint8_t type = nfc::guess_tag_type(tag.get_uid().size()); + + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + ESP_LOGV(TAG, "Reading Mifare classic"); + return this->read_mifare_classic_tag_(tag); + + case nfc::TAG_TYPE_2: + ESP_LOGV(TAG, "Reading Mifare ultralight"); + return this->read_mifare_ultralight_tag_(tag); + + case nfc::TAG_TYPE_UNKNOWN: + default: + ESP_LOGV(TAG, "Cannot determine tag type"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::clean_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_mifare_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for cleaning"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::format_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_ndef_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for formatting"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::write_endpoint_(std::vector &uid, std::shared_ptr &message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->write_mifare_classic_tag_(message); + + case nfc::TAG_TYPE_2: + return this->write_mifare_ultralight_tag_(uid, message); + + default: + ESP_LOGE(TAG, "Unsupported tag for writing"); + break; + } + return nfc::STATUS_FAILED; +} + +std::unique_ptr PN7150::build_tag_(const uint8_t mode_tech, const std::vector &data) { + switch (mode_tech) { + case (nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA): { + uint8_t uid_length = data[2]; + if (!uid_length) { + ESP_LOGE(TAG, "UID length cannot be zero"); + return nullptr; + } + std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + const auto *tag_type_str = + nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; + return make_unique(uid, tag_type_str); + } + } + return nullptr; +} + +optional PN7150::find_tag_uid_(const std::vector &uid) { + if (!this->discovered_endpoint_.empty()) { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); + bool uid_match = (uid.size() == existing_tag_uid.size()); + + if (uid_match) { + for (size_t i = 0; i < uid.size(); i++) { + uid_match &= (uid[i] == existing_tag_uid[i]); + } + if (uid_match) { + return i; + } + } + } + } + return nullopt; +} + +void PN7150::purge_old_tags_() { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { + this->erase_tag_(i); + } + } +} + +void PN7150::erase_tag_(const uint8_t tag_index) { + if (tag_index < this->discovered_endpoint_.size()) { + for (auto *trigger : this->triggers_ontagremoved_) { + trigger->process(this->discovered_endpoint_[tag_index].tag); + } + for (auto *listener : this->tag_listeners_) { + listener->tag_off(*this->discovered_endpoint_[tag_index].tag); + } + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); + } +} + +void PN7150::nci_fsm_transition_() { + switch (this->nci_state_) { + case NCIState::NFCC_RESET: + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + // fall through + + case NCIState::NFCC_INIT: + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); + } + // fall through + + case NCIState::NFCC_CONFIG: + if (this->send_init_config_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to send initial config"); + this->nci_fsm_set_error_state_(NCIState::NFCC_CONFIG); + return; + } else { + this->config_refresh_pending_ = false; + this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); + } + // fall through + + case NCIState::NFCC_SET_DISCOVER_MAP: + if (this->set_discover_map_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set discover map"); + this->nci_fsm_set_error_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + } + // fall through + + case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: + if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set listen mode routing"); + this->nci_fsm_set_error_state_(NCIState::RFST_IDLE); + return; + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + // fall through + + case NCIState::RFST_IDLE: + if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { + this->stop_discovery_(); + } + + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + + if (!this->listening_enabled_ && !this->polling_enabled_) { + return; + } + + if (this->start_discovery_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to start discovery"); + this->nci_fsm_set_error_state_(NCIState::RFST_DISCOVERY); + } else { + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + } + return; + + case NCIState::RFST_W4_HOST_SELECT: + select_endpoint_(); + // fall through + + // All cases below are waiting for NOTIFICATION messages + case NCIState::RFST_DISCOVERY: + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + // fall through + + case NCIState::RFST_LISTEN_ACTIVE: + case NCIState::RFST_LISTEN_SLEEP: + case NCIState::RFST_POLL_ACTIVE: + case NCIState::EP_SELECTING: + case NCIState::EP_DEACTIVATING: + if (this->irq_pin_->digital_read()) { + this->process_message_(); + } + break; + + case NCIState::TEST: + case NCIState::FAILED: + case NCIState::NONE: + default: + return; + } +} + +void PN7150::nci_fsm_set_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_state_(%u)", (uint8_t) new_state); + this->nci_state_ = new_state; + this->nci_state_error_ = NCIState::NONE; + this->error_count_ = 0; + this->last_nci_state_change_ = millis(); +} + +bool PN7150::nci_fsm_set_error_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_error_state_(%u); error_count_ = %u", (uint8_t) new_state, this->error_count_); + this->nci_state_error_ = new_state; + if (this->error_count_++ > NFCC_MAX_ERROR_COUNT) { + if ((this->nci_state_error_ == NCIState::NFCC_RESET) || (this->nci_state_error_ == NCIState::NFCC_INIT) || + (this->nci_state_error_ == NCIState::NFCC_CONFIG)) { + ESP_LOGE(TAG, "Too many initialization failures -- check device connections"); + this->mark_failed(); + this->nci_fsm_set_state_(NCIState::FAILED); + } else { + ESP_LOGW(TAG, "Too many errors transitioning to state %u; resetting NFCC", (uint8_t) this->nci_state_error_); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + } + } + return this->error_count_ > NFCC_MAX_ERROR_COUNT; +} + +void PN7150::process_message_() { + nfc::NciMessage rx; + if (this->read_nfcc(rx, NFCC_DEFAULT_TIMEOUT) != nfc::STATUS_OK) { + return; // No data + } + + switch (rx.get_message_type()) { + case nfc::NCI_PKT_MT_CTRL_NOTIFICATION: + if (rx.get_gid() == nfc::RF_GID) { + switch (rx.get_oid()) { + case nfc::RF_INTF_ACTIVATED_OID: + ESP_LOGVV(TAG, "RF_INTF_ACTIVATED_OID"); + this->process_rf_intf_activated_oid_(rx); + return; + + case nfc::RF_DISCOVER_OID: + ESP_LOGVV(TAG, "RF_DISCOVER_OID"); + this->process_rf_discover_oid_(rx); + return; + + case nfc::RF_DEACTIVATE_OID: + ESP_LOGVV(TAG, "RF_DEACTIVATE_OID: type: 0x%02X, reason: 0x%02X", rx.get_message()[3], rx.get_message()[4]); + this->process_rf_deactivate_oid_(rx); + return; + + default: + ESP_LOGV(TAG, "Unimplemented RF OID received: 0x%02X", rx.get_oid()); + } + } else if (rx.get_gid() == nfc::NCI_CORE_GID) { + switch (rx.get_oid()) { + case nfc::NCI_CORE_GENERIC_ERROR_OID: + ESP_LOGV(TAG, "NCI_CORE_GENERIC_ERROR_OID:"); + switch (rx.get_simple_status_response()) { + case nfc::DISCOVERY_ALREADY_STARTED: + ESP_LOGV(TAG, " DISCOVERY_ALREADY_STARTED"); + break; + + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + // Tag removed too soon + ESP_LOGV(TAG, " DISCOVERY_TARGET_ACTIVATION_FAILED"); + if (this->nci_state_ == NCIState::EP_SELECTING) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + if (!this->discovered_endpoint_.empty()) { + this->erase_tag_(this->selecting_endpoint_); + } + } else { + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + case nfc::DISCOVERY_TEAR_DOWN: + ESP_LOGV(TAG, " DISCOVERY_TEAR_DOWN"); + break; + + default: + ESP_LOGW(TAG, "Unknown error: 0x%02X", rx.get_simple_status_response()); + break; + } + break; + + default: + ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); + } + } else { + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + break; + + case nfc::NCI_PKT_MT_CTRL_RESPONSE: + ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), + nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_CTRL_COMMAND: + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_DATA: + this->process_data_message_(rx); + break; + + default: + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + } +} + +void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoint was activated + uint8_t discovery_id = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_DISCOVERY_ID); + uint8_t interface = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_INTERFACE); + uint8_t protocol = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_PROTOCOL); + uint8_t mode_tech = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MODE_TECH); + uint8_t max_size = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MAX_SIZE); + + ESP_LOGVV(TAG, "Endpoint activated -- interface: 0x%02X, protocol: 0x%02X, mode&tech: 0x%02X, max payload: %u", + interface, protocol, mode_tech, max_size); + + if (mode_tech & nfc::MODE_LISTEN_MASK) { + ESP_LOGVV(TAG, "Tag activated in listen mode"); + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_ACTIVE); + return; + } + + this->nci_fsm_set_state_(NCIState::RFST_POLL_ACTIVE); + auto incoming_tag = + this->build_tag_(mode_tech, std::vector(rx.get_message().begin() + 10, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = discovery_id; + this->discovered_endpoint_[tag_loc.value()].protocol = protocol; + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag cache updated"); + } else { + this->discovered_endpoint_.emplace_back( + DiscoveredEndpoint{discovery_id, protocol, millis(), std::move(incoming_tag), false}); + tag_loc = this->discovered_endpoint_.size() - 1; + ESP_LOGVV(TAG, "Tag added to cache"); + } + + auto &working_endpoint = this->discovered_endpoint_[tag_loc.value()]; + + switch (this->next_task_) { + case EP_CLEAN: + ESP_LOGD(TAG, " Tag cleaning..."); + if (this->clean_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag cleaning incomplete"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + break; + + case EP_FORMAT: + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + break; + + case EP_WRITE: + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " 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 { + ESP_LOGD(TAG, " Writing NDEF data"); + if (this->write_endpoint_(working_endpoint.tag->get_uid(), this->next_task_message_to_write_) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + break; + + case EP_READ: + default: + if (!working_endpoint.trig_called) { + ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), + nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { + ESP_LOGW(TAG, " Unable to read NDEF record(s)"); + } else if (working_endpoint.tag->has_ndef_message()) { + const auto message = working_endpoint.tag->get_ndef_message(); + const auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF record(s):"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } else { + ESP_LOGW(TAG, " No NDEF records found"); + } + for (auto *trigger : this->triggers_ontag_) { + trigger->process(working_endpoint.tag); + } + for (auto *listener : this->tag_listeners_) { + listener->tag_on(*working_endpoint.tag); + } + working_endpoint.trig_called = true; + break; + } + } + if (working_endpoint.tag->get_tag_type() == nfc::MIFARE_CLASSIC) { + this->halt_mifare_classic_tag_(); + } + } + if (this->next_task_ != EP_READ) { + this->read_mode(); + } + + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::EP_DEACTIVATING); +} + +void PN7150::process_rf_discover_oid_(nfc::NciMessage &rx) { + auto incoming_tag = this->build_tag_(rx.get_message_byte(nfc::RF_DISCOVER_NTF_MODE_TECH), + std::vector(rx.get_message().begin() + 7, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag!"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID); + this->discovered_endpoint_[tag_loc.value()].protocol = rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL); + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag found & updated"); + } else { + this->discovered_endpoint_.emplace_back(DiscoveredEndpoint{rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID), + rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL), + millis(), std::move(incoming_tag), false}); + ESP_LOGVV(TAG, "Tag saved"); + } + } + + if (rx.get_message().back() != nfc::RF_DISCOVER_NTF_NT_MORE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + ESP_LOGVV(TAG, "Discovered %u endpoints", this->discovered_endpoint_.size()); + } +} + +void PN7150::process_rf_deactivate_oid_(nfc::NciMessage &rx) { + this->ce_state_ = CardEmulationState::CARD_EMU_IDLE; + + switch (rx.get_simple_status_response()) { + case nfc::DEACTIVATION_TYPE_DISCOVERY: + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + break; + + case nfc::DEACTIVATION_TYPE_IDLE: + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + break; + + case nfc::DEACTIVATION_TYPE_SLEEP: + case nfc::DEACTIVATION_TYPE_SLEEP_AF: + if (this->nci_state_ == NCIState::RFST_LISTEN_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_SLEEP); + } else if (this->nci_state_ == NCIState::RFST_POLL_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + default: + break; + } +} + +void PN7150::process_data_message_(nfc::NciMessage &rx) { + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + + std::vector ndef_response; + this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); + + uint16_t ndef_response_size = ndef_response.size(); + if (!ndef_response_size) { + return; // no message returned, we cannot respond + } + + std::vector tx_msg = {nfc::NCI_PKT_MT_DATA, uint8_t((ndef_response_size & 0xFF00) >> 8), + uint8_t(ndef_response_size & 0x00FF)}; + tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); + nfc::NciMessage tx(tx_msg); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending reply for card emulation failed"); + } +} + +void PN7150::card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response) { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation not possible"); + ndef_response.clear(); + return; + } + + if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_APP_SELECT))) { + // CARD_EMU_T4T_APP_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_APP_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_APP_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_CC_SELECT))) { + // CARD_EMU_T4T_CC_SELECT + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_APP_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_CC_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_CC_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_NDEF_SELECT))) { + // CARD_EMU_T4T_NDEF_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_READ), + std::begin(CARD_EMU_T4T_READ))) { + // CARD_EMU_T4T_READ + if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED"); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + if (length <= (sizeof(CARD_EMU_T4T_CC) + offset + 2)) { + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_CC) + offset, + std::begin(CARD_EMU_T4T_CC) + offset + length); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED"); + auto ndef_message = this->card_emulation_message_->encode(); + uint16_t ndef_msg_size = ndef_message.size(); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + + if (length <= (ndef_msg_size + offset + 2)) { + if (offset == 0) { + ndef_response.resize(2); + ndef_response[0] = (ndef_msg_size & 0xFF00) >> 8; + ndef_response[1] = (ndef_msg_size & 0x00FF); + if (length > 2) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 2); + } + } else if (offset == 1) { + ndef_response.resize(1); + ndef_response[0] = (ndef_msg_size & 0x00FF); + if (length > 1) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 1); + } + } else { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length); + } + + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + + if ((offset + length) >= (ndef_msg_size + 2)) { + ESP_LOGD(TAG, "NDEF message sent"); + this->on_emulated_tag_scan_callback_.call(); + } + } + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_WRITE), + std::begin(CARD_EMU_T4T_WRITE))) { + // CARD_EMU_T4T_WRITE + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_T4T_WRITE"); + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + std::vector ndef_msg_written; + + ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } +} + +uint8_t PN7150::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, + const bool expect_notification) { + uint8_t retries = NFCC_MAX_COMM_FAILS; + + while (retries) { + // first, send the message we need to send + if (this->write_nfcc(tx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + // next, the NFCC should send back a response + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error receiving message"); + if (!retries--) { + ESP_LOGE(TAG, " ...giving up"); + return nfc::STATUS_FAILED; + } + } else { + break; + } + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + // validate the response based on the message type that was sent (command vs. data) + if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { + // for commands, the GID and OID should match and the status should be OK + if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + return rx.get_simple_status_response(); + } else { + // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || + (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (expect_notification) { + // if the NFCC said "OK", there will be additional data to read; this comes back in a notification message + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error receiving data from endpoint"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + + return nfc::STATUS_OK; + } +} + +uint8_t PN7150::wait_for_irq_(uint16_t timeout, bool pin_state) { + auto start_time = millis(); + + while (millis() - start_time < timeout) { + if (this->irq_pin_->digital_read() == pin_state) { + return nfc::STATUS_OK; + } + } + ESP_LOGW(TAG, "Timed out waiting for IRQ state"); + return nfc::STATUS_FAILED; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h new file mode 100644 index 0000000000..54038f5085 --- /dev/null +++ b/esphome/components/pn7150/pn7150.h @@ -0,0 +1,296 @@ +#pragma once + +#include "esphome/components/nfc/automation.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/nfc/nci_message.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_helpers.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace pn7150 { + +static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static const uint16_t NFCC_INIT_TIMEOUT = 50; +static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; + +static const uint8_t NFCC_MAX_COMM_FAILS = 3; +static const uint8_t NFCC_MAX_ERROR_COUNT = 10; + +static const uint8_t XCHG_DATA_OID = 0x10; +static const uint8_t MF_SECTORSEL_OID = 0x32; +static const uint8_t MFC_AUTHENTICATE_OID = 0x40; +static const uint8_t TEST_PRBS_OID = 0x30; +static const uint8_t TEST_ANTENNA_OID = 0x3D; +static const uint8_t TEST_GET_REGISTER_OID = 0x33; + +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; + +static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; + +static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms + +static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms + +static const uint8_t PMU_CFG[] = { + 0x01, // Number of parameters + 0xA0, 0x0E, // ext. tag + 3, // length + 0x06, // VBAT1 connected to 5V (CFG2) + 0x64, // TVDD monitoring threshold = 5.0V; TxLDO voltage = 4.7V (in reader & card modes) + 0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2 +}; + +static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes + nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN, + nfc::INTF_ISODEP, // poll & listen mode + nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_TAGCMD}; // poll mode + +static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode + +static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 1, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x01, // power state + nfc::PROT_ISODEP}; // protocol + +enum class CardEmulationState : uint8_t { + CARD_EMU_IDLE, + CARD_EMU_NDEF_APP_SELECTED, + CARD_EMU_CC_SELECTED, + CARD_EMU_NDEF_SELECTED, + CARD_EMU_DESFIRE_PROD, +}; + +enum class NCIState : uint8_t { + NONE = 0x00, + NFCC_RESET, + NFCC_INIT, + NFCC_CONFIG, + NFCC_SET_DISCOVER_MAP, + NFCC_SET_LISTEN_MODE_ROUTING, + RFST_IDLE, + RFST_DISCOVERY, + RFST_W4_ALL_DISCOVERIES, + RFST_W4_HOST_SELECT, + RFST_LISTEN_ACTIVE, + RFST_LISTEN_SLEEP, + RFST_POLL_ACTIVE, + EP_DEACTIVATING, + EP_SELECTING, + TEST = 0XFE, + FAILED = 0XFF, +}; + +enum class TestMode : uint8_t { + TEST_NONE = 0x00, + TEST_PRBS, + TEST_ANTENNA, + TEST_GET_REGISTER, +}; + +struct DiscoveredEndpoint { + uint8_t id; + uint8_t protocol; + uint32_t last_seen; + std::unique_ptr tag; + bool trig_called; +}; + +class PN7150 : public nfc::Nfcc, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } + void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; } + + void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; } + void set_tag_emulation_message(std::shared_ptr message); + void set_tag_emulation_message(const optional &message, optional include_android_app_record); + void set_tag_emulation_message(const char *message, bool include_android_app_record = true); + void set_tag_emulation_off(); + void set_tag_emulation_on(); + bool tag_emulation_enabled() { return this->listening_enabled_; } + + void set_polling_off(); + void set_polling_on(); + bool polling_enabled() { return this->polling_enabled_; } + + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_emulated_tag_scan_callback(std::function callback) { + this->on_emulated_tag_scan_callback_.add(std::move(callback)); + } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != EP_READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(); + void set_tag_write_message(std::shared_ptr message); + void set_tag_write_message(optional message, optional include_android_app_record); + + uint8_t set_test_mode(TestMode test_mode, const std::vector &data, std::vector &result); + + protected: + uint8_t reset_core_(bool reset_config, bool power); + uint8_t init_core_(); + uint8_t send_init_config_(); + uint8_t send_core_config_(); + uint8_t refresh_core_config_(); + + uint8_t set_discover_map_(); + + uint8_t set_listen_mode_routing_(); + + uint8_t start_discovery_(); + uint8_t stop_discovery_(); + uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT); + + void select_endpoint_(); + + uint8_t read_endpoint_data_(nfc::NfcTag &tag); + uint8_t clean_endpoint_(std::vector &uid); + uint8_t format_endpoint_(std::vector &uid); + uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + + std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); + optional find_tag_uid_(const std::vector &uid); + void purge_old_tags_(); + void erase_tag_(uint8_t tag_index); + + /// advance controller state as required + void nci_fsm_transition_(); + /// set new controller state + void nci_fsm_set_state_(NCIState new_state); + /// setting controller to this state caused an error; returns true if too many errors/failures + bool nci_fsm_set_error_state_(NCIState new_state); + /// parse & process incoming messages from the NFCC + void process_message_(); + void process_rf_intf_activated_oid_(nfc::NciMessage &rx); + void process_rf_discover_oid_(nfc::NciMessage &rx); + void process_rf_deactivate_oid_(nfc::NciMessage &rx); + void process_data_message_(nfc::NciMessage &rx); + + void card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response); + + uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT, + bool expect_notification = true); + virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0; + virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0; + + uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true); + + uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); + uint8_t sect_to_auth_(uint8_t block_num); + uint8_t format_mifare_classic_mifare_(); + uint8_t format_mifare_classic_ndef_(); + uint8_t write_mifare_classic_tag_(const std::shared_ptr &message); + uint8_t halt_mifare_classic_tag_(); + + uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); + uint16_t read_mifare_ultralight_capacity_(); + uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t clean_mifare_ultralight_(); + + enum NfcTask : uint8_t { + EP_READ = 0, + EP_CLEAN, + EP_FORMAT, + EP_WRITE, + } next_task_{EP_READ}; + + bool config_refresh_pending_{false}; + bool core_config_is_solo_{false}; + bool listening_enabled_{false}; + bool polling_enabled_{true}; + + uint8_t error_count_{0}; + uint8_t fail_count_{0}; + uint32_t last_nci_state_change_{0}; + uint8_t selecting_endpoint_{0}; + uint32_t tag_ttl_{250}; + + GPIOPin *irq_pin_{nullptr}; + GPIOPin *ven_pin_{nullptr}; + + CallbackManager on_emulated_tag_scan_callback_; + CallbackManager on_finished_write_callback_; + + std::vector discovered_endpoint_; + + CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE}; + NCIState nci_state_{NCIState::NFCC_RESET}; + NCIState nci_state_error_{NCIState::NONE}; + + std::shared_ptr card_emulation_message_; + std::shared_ptr next_task_message_to_write_; + + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; +}; + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp new file mode 100644 index 0000000000..0443929f69 --- /dev/null +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -0,0 +1,322 @@ +#include + +#include "pn7150.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150.mifare_classic"; + +uint8_t PN7150::read_mifare_classic_tag_(nfc::NfcTag &tag) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data"); + return nfc::STATUS_FAILED; + } + std::vector data; + + if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return nfc::STATUS_FAILED; + } + } else { + ESP_LOGE(TAG, "Failed to read block %u", current_block); + return nfc::STATUS_FAILED; + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Block authentication failed for %u", current_block); + return nfc::STATUS_FAILED; + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading block %u", current_block); + return nfc::STATUS_FAILED; + } else { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + + if (buffer.begin() + message_start_index < buffer.end()) { + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + } else { + return nfc::STATUS_FAILED; + } + + tag.set_ndef_message(make_unique(buffer)); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Timeout reading tag data"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (!rx.message_length_is(18))) { + ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1); + + ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes(data).c_str()); + return nfc::STATUS_OK; +} + +uint8_t PN7150::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num}); + + switch (key_num) { + case nfc::MIFARE_CMD_AUTH_A: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A; + break; + + case nfc::MIFARE_CMD_AUTH_B: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B; + break; + + default: + break; + } + + if (key != nullptr) { + tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY; + tx.get_message().insert(tx.get_message().end(), key, key + 6); + } + + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); + return nfc::STATUS_FAILED; + } + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || + (rx.get_message()[4] != nfc::STATUS_OK)) { + ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num); + return nfc::STATUS_OK; +} + +uint8_t PN7150::sect_to_auth_(const uint8_t block_num) { + const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + if (block_num >= first_high_block) { + return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) + + nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + } + return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW; +} + +uint8_t PN7150::format_mifare_classic_mifare_() { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + auto status = nfc::STATUS_OK; + + for (int block = 0; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + continue; + } + if (block != 0) { + if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + + return status; +} + +uint8_t PN7150::format_mifare_classic_ndef_() { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Sector 0 formatted with NDEF"); + + auto status = nfc::STATUS_OK; + + for (int block = 4; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (block == 4) { + if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } else { + if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + return status; +} + +uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + // write command part two + tx.set_payload({XCHG_DATA_OID}); + tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) { + ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr &message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::halt_mifare_classic_tag_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0}); + + ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp new file mode 100644 index 0000000000..791b0634d6 --- /dev/null +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "pn7150.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150.mifare_ultralight"; + +uint8_t PN7150::read_mifare_ultralight_tag_(nfc::NfcTag &tag) { + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); + return nfc::STATUS_FAILED; + } + + uint8_t message_length; + uint8_t message_start_index; + if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); + + if (message_length == 0) { + return nfc::STATUS_FAILED; + } + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + + tag.set_ndef_message(make_unique(data)); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page}); + + for (size_t i = 0; i * read_increment < num_bytes; i++) { + tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page; + do { // loop because sometimes we struggle here...???... + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } while (rx.get_payload_size() < read_increment); + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1 + : rx.get_message().end() - (bytes_offset - num_bytes + 1); + + if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) { + data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr); + } + } + + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + + return nfc::STATUS_OK; +} + +bool PN7150::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); +} + +uint16_t PN7150::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); + return data[2] * 8U; + } + return 0; +} + +uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return nfc::STATUS_FAILED; + } + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; + message_start_index = 2; + return nfc::STATUS_OK; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; + message_start_index = 7; + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::write_mifare_ultralight_tag_(std::vector &uid, + const std::shared_ptr &message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); + return nfc::STATUS_FAILED; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; + payload.insert(payload.end(), write_data.begin(), write_data.end()); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); + + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error writing page %u", page_num); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150_i2c/__init__.py b/esphome/components/pn7150_i2c/__init__.py new file mode 100644 index 0000000000..5f48a0f3cb --- /dev/null +++ b/esphome/components/pn7150_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn7150 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7150"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn7150_i2c_ns = cg.esphome_ns.namespace("pn7150_i2c") +PN7150I2C = pn7150_i2c_ns.class_("PN7150I2C", pn7150.PN7150, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn7150.PN7150_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7150I2C), + } + ).extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7150.setup_pn7150(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.cpp b/esphome/components/pn7150_i2c/pn7150_i2c.cpp new file mode 100644 index 0000000000..38b3102b37 --- /dev/null +++ b/esphome/components/pn7150_i2c/pn7150_i2c.cpp @@ -0,0 +1,49 @@ +#include "pn7150_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pn7150_i2c { + +static const char *const TAG = "pn7150_i2c"; + +uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE)) { + return nfc::STATUS_FAILED; + } + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length)) { + return nfc::STATUS_FAILED; + } + } + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7150::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150I2C::write_nfcc(nfc::NciMessage &tx) { + if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +void PN7150I2C::dump_config() { + PN7150::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn7150_i2c +} // namespace esphome diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.h b/esphome/components/pn7150_i2c/pn7150_i2c.h new file mode 100644 index 0000000000..9308dddd26 --- /dev/null +++ b/esphome/components/pn7150_i2c/pn7150_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn7150/pn7150.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace pn7150_i2c { + +class PN7150I2C : public pn7150::PN7150, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7150_i2c +} // namespace esphome diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py new file mode 100644 index 0000000000..b102b38f98 --- /dev/null +++ b/esphome/components/pn7160/__init__.py @@ -0,0 +1,227 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import nfc +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_MESSAGE, + CONF_ON_FINISHED_WRITE, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["binary_sensor", "nfc"] +CODEOWNERS = ["@kbx81", "@jesserockz"] + +CONF_DWL_REQ_PIN = "dwl_req_pin" +CONF_EMULATION_MESSAGE = "emulation_message" +CONF_EMULATION_OFF = "emulation_off" +CONF_EMULATION_ON = "emulation_on" +CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" +CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan" +CONF_PN7160_ID = "pn7160_id" +CONF_POLLING_OFF = "polling_off" +CONF_POLLING_ON = "polling_on" +CONF_SET_CLEAN_MODE = "set_clean_mode" +CONF_SET_EMULATION_MESSAGE = "set_emulation_message" +CONF_SET_FORMAT_MODE = "set_format_mode" +CONF_SET_READ_MODE = "set_read_mode" +CONF_SET_WRITE_MESSAGE = "set_write_message" +CONF_SET_WRITE_MODE = "set_write_mode" +CONF_TAG_TTL = "tag_ttl" +CONF_VEN_PIN = "ven_pin" +CONF_WKUP_REQ_PIN = "wkup_req_pin" + +pn7160_ns = cg.esphome_ns.namespace("pn7160") +PN7160 = pn7160_ns.class_("PN7160", nfc.Nfcc, cg.Component) + +EmulationOffAction = pn7160_ns.class_("EmulationOffAction", automation.Action) +EmulationOnAction = pn7160_ns.class_("EmulationOnAction", automation.Action) +PollingOffAction = pn7160_ns.class_("PollingOffAction", automation.Action) +PollingOnAction = pn7160_ns.class_("PollingOnAction", automation.Action) +SetCleanModeAction = pn7160_ns.class_("SetCleanModeAction", automation.Action) +SetEmulationMessageAction = pn7160_ns.class_( + "SetEmulationMessageAction", automation.Action +) +SetFormatModeAction = pn7160_ns.class_("SetFormatModeAction", automation.Action) +SetReadModeAction = pn7160_ns.class_("SetReadModeAction", automation.Action) +SetWriteMessageAction = pn7160_ns.class_("SetWriteMessageAction", automation.Action) +SetWriteModeAction = pn7160_ns.class_("SetWriteModeAction", automation.Action) + + +PN7160OnEmulatedTagScanTrigger = pn7160_ns.class_( + "PN7160OnEmulatedTagScanTrigger", automation.Trigger.template() +) + +PN7160OnFinishedWriteTrigger = pn7160_ns.class_( + "PN7160OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN7160IsWritingCondition = pn7160_ns.class_( + "PN7160IsWritingCondition", automation.Condition +) + + +IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition) + + +SIMPLE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PN7160), + } +) + +SET_MESSAGE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7160), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean, + } +) + +PN7160_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN7160), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7160OnEmulatedTagScanTrigger + ), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7160OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_DWL_REQ_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_WKUP_REQ_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EMULATION_MESSAGE): cv.string, + cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "tag.set_emulation_message", + SetEmulationMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +@automation.register_action( + "tag.set_write_message", + SetWriteMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +async def pn7160_set_message_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_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + template_ = await cg.templatable( + config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_ + ) + cg.add(var.set_include_android_app_record(template_)) + return var + + +@automation.register_action( + "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action( + "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA +) +async def pn7160_simple_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 + + +async def setup_pn7160(var, config): + await cg.register_component(var, config) + + if dwl_req_pin_config := config.get(CONF_DWL_REQ_PIN): + pin = await cg.gpio_pin_expression(dwl_req_pin_config) + cg.add(var.set_dwl_req_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN]) + cg.add(var.set_ven_pin(pin)) + + if wakeup_req_pin_config := config.get(CONF_WKUP_REQ_PIN): + pin = await cg.gpio_pin_expression(wakeup_req_pin_config) + cg.add(var.set_wkup_req_pin(pin)) + + if emulation_message_config := config.get(CONF_EMULATION_MESSAGE): + cg.add(var.set_tag_emulation_message(emulation_message_config)) + cg.add(var.set_tag_emulation_on()) + + if CONF_TAG_TTL in config: + cg.add(var.set_tag_ttl(config[CONF_TAG_TTL])) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn7160.is_writing", + PN7160IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7160), + } + ), +) +async def pn7160_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h new file mode 100644 index 0000000000..854fb11684 --- /dev/null +++ b/esphome/components/pn7160/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/pn7160/pn7160.h" + +namespace esphome { +namespace pn7160 { + +class PN7160OnEmulatedTagScanTrigger : public Trigger<> { + public: + explicit PN7160OnEmulatedTagScanTrigger(PN7160 *parent) { + parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); + } +}; + +class PN7160OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN7160OnFinishedWriteTrigger(PN7160 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN7160IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } +}; + +template class EmulationOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } +}; + +template class EmulationOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } +}; + +template class PollingOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_off(); } +}; + +template class PollingOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_on(); } +}; + +template class SetCleanModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->clean_mode(); } +}; + +template class SetFormatModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->format_mode(); } +}; + +template class SetReadModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->read_mode(); } +}; + +template class SetEmulationMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_write_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->write_mode(); } +}; + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp new file mode 100644 index 0000000000..a7d3b38fb7 --- /dev/null +++ b/esphome/components/pn7160/pn7160.cpp @@ -0,0 +1,1167 @@ +#include + +#include "automation.h" +#include "pn7160.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160"; + +void PN7160::setup() { + this->irq_pin_->setup(); + this->ven_pin_->setup(); + if (this->dwl_req_pin_ != nullptr) { + this->dwl_req_pin_->setup(); + } + if (this->wkup_req_pin_ != nullptr) { + this->wkup_req_pin_->setup(); + } + + this->nci_fsm_transition_(); // kick off reset & init processes +} + +void PN7160::dump_config() { + ESP_LOGCONFIG(TAG, "PN7160:"); + if (this->dwl_req_pin_ != nullptr) { + LOG_PIN(" DWL_REQ pin: ", this->dwl_req_pin_); + } + LOG_PIN(" IRQ pin: ", this->irq_pin_); + LOG_PIN(" VEN pin: ", this->ven_pin_); + if (this->wkup_req_pin_ != nullptr) { + LOG_PIN(" WKUP_REQ pin: ", this->wkup_req_pin_); + } +} + +void PN7160::loop() { + this->nci_fsm_transition_(); + this->purge_old_tags_(); +} + +void PN7160::set_tag_emulation_message(std::shared_ptr message) { + this->card_emulation_message_ = std::move(message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7160::set_tag_emulation_message(const optional &message, + const optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->card_emulation_message_ = std::move(ndef_message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7160::set_tag_emulation_message(const char *message, const bool include_android_app_record) { + this->set_tag_emulation_message(std::string(message), include_android_app_record); +} + +void PN7160::set_tag_emulation_off() { + if (this->listening_enabled_) { + this->listening_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation disabled"); +} + +void PN7160::set_tag_emulation_on() { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation cannot be enabled"); + return; + } + if (!this->listening_enabled_) { + this->listening_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation enabled"); +} + +void PN7160::set_polling_off() { + if (this->polling_enabled_) { + this->polling_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling disabled"); +} + +void PN7160::set_polling_on() { + if (!this->polling_enabled_) { + this->polling_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling enabled"); +} + +void PN7160::read_mode() { + this->next_task_ = EP_READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} + +void PN7160::clean_mode() { + this->next_task_ = EP_CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} + +void PN7160::format_mode() { + this->next_task_ = EP_FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} + +void PN7160::write_mode() { + if (this->next_task_message_to_write_ == nullptr) { + ESP_LOGW(TAG, "Message to write must be set before setting write mode"); + return; + } + + this->next_task_ = EP_WRITE; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +void PN7160::set_tag_write_message(std::shared_ptr message) { + this->next_task_message_to_write_ = std::move(message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +void PN7160::set_tag_write_message(optional message, optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->next_task_message_to_write_ = std::move(ndef_message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +uint8_t PN7160::set_test_mode(const TestMode test_mode, const std::vector &data, + std::vector &result) { + auto test_oid = TEST_PRBS_OID; + + switch (test_mode) { + case TestMode::TEST_PRBS: + // test_oid = TEST_PRBS_OID; + break; + + case TestMode::TEST_ANTENNA: + test_oid = TEST_ANTENNA_OID; + break; + + case TestMode::TEST_GET_REGISTER: + test_oid = TEST_GET_REGISTER_OID; + break; + + case TestMode::TEST_NONE: + default: + ESP_LOGD(TAG, "Exiting test mode"); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_OK; + } + + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::TEST); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, test_oid, data); + + ESP_LOGW(TAG, "Starting test mode, OID 0x%02X", test_oid); + auto status = this->transceive_(tx, rx, NFCC_INIT_TIMEOUT); + + if (status != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to start test mode, OID 0x%02X", test_oid); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + result.clear(); + } else { + result = rx.get_message(); + result.erase(result.begin(), result.begin() + 4); // remove NCI header + if (!result.empty()) { + ESP_LOGW(TAG, "Test results: %s", nfc::format_bytes(result).c_str()); + } + } + return status; +} + +uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { + if (this->dwl_req_pin_ != nullptr) { + this->dwl_req_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + } + + if (power) { + this->ven_pin_->digital_write(true); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(true); + delay(NFCC_INIT_TIMEOUT); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_RESET_OID, + {(uint8_t) reset_config}); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending reset command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid reset response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return rx.get_simple_status_response(); + } + // read reset notification + if (this->read_nfcc(rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Reset notification was not received"); + return nfc::STATUS_FAILED; + } + // verify reset notification + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.message_length_is(9)) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET] != 0x02) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] != (uint8_t) reset_config)) { + ESP_LOGE(TAG, "Reset notification was malformed: %s", nfc::format_bytes(rx.get_message()).c_str()); + 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]); + 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()); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::init_core_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_INIT_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending initialise command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid initialise response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + uint8_t hw_version = rx.get_message()[17 + rx.get_message()[8]]; + uint8_t rom_code_version = rx.get_message()[18 + rx.get_message()[8]]; + uint8_t flash_major_version = rx.get_message()[19 + rx.get_message()[8]]; + 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()); + + return rx.get_simple_status_response(); +} + +uint8_t PN7160::send_init_config_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, nfc::NCI_CORE_SET_CONFIG_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error enabling proprietary extensions"); + return nfc::STATUS_FAILED; + } + + tx.set_message(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(std::begin(PMU_CFG), std::end(PMU_CFG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending PMU config"); + return nfc::STATUS_FAILED; + } + + return this->send_core_config_(); +} + +uint8_t PN7160::send_core_config_() { + const auto *core_config_begin = std::begin(CORE_CONFIG_SOLO); + const auto *core_config_end = std::end(CORE_CONFIG_SOLO); + this->core_config_is_solo_ = true; + + if (this->listening_enabled_ && this->polling_enabled_) { + core_config_begin = std::begin(CORE_CONFIG_RW_CE); + core_config_end = std::end(CORE_CONFIG_RW_CE); + this->core_config_is_solo_ = false; + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(core_config_begin, core_config_end)); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error sending core config"); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::refresh_core_config_() { + bool core_config_should_be_solo = !(this->listening_enabled_ && this->polling_enabled_); + + if (this->nci_state_ == NCIState::RFST_DISCOVERY) { + if (this->stop_discovery_() != nfc::STATUS_OK) { + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_FAILED; + } + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + + if (this->core_config_is_solo_ != core_config_should_be_solo) { + if (this->send_core_config_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to refresh core config"); + return nfc::STATUS_FAILED; + } + } + this->config_refresh_pending_ = false; + return nfc::STATUS_OK; +} + +uint8_t PN7160::set_discover_map_() { + std::vector discover_map = {sizeof(RF_DISCOVER_MAP_CONFIG) / 3}; + discover_map.insert(discover_map.end(), std::begin(RF_DISCOVER_MAP_CONFIG), std::end(RF_DISCOVER_MAP_CONFIG)); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_MAP_OID, discover_map); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending discover map poll config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::set_listen_mode_routing_() { + nfc::NciMessage rx; + nfc::NciMessage tx( + nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_SET_LISTEN_MODE_ROUTING_OID, + std::vector(std::begin(RF_LISTEN_MODE_ROUTING_CONFIG), std::end(RF_LISTEN_MODE_ROUTING_CONFIG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error setting listen mode routing config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::start_discovery_() { + const uint8_t *rf_discovery_config = RF_DISCOVERY_CONFIG; + uint8_t length = sizeof(RF_DISCOVERY_CONFIG); + + if (!this->listening_enabled_) { + length = sizeof(RF_DISCOVERY_POLL_CONFIG); + rf_discovery_config = RF_DISCOVERY_POLL_CONFIG; + } else if (!this->polling_enabled_) { + length = sizeof(RF_DISCOVERY_LISTEN_CONFIG); + rf_discovery_config = RF_DISCOVERY_LISTEN_CONFIG; + } + + std::vector discover_config = std::vector((length * 2) + 1); + + discover_config[0] = length; + for (uint8_t i = 0; i < length; i++) { + discover_config[(i * 2) + 1] = rf_discovery_config[i]; + discover_config[(i * 2) + 2] = 0x01; // RF Technology and Mode will be executed in every discovery period + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, discover_config); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + switch (rx.get_simple_status_response()) { + // in any of these cases, we are either already in or will remain in discovery, which satisfies the function call + case nfc::STATUS_OK: + case nfc::DISCOVERY_ALREADY_STARTED: + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + case nfc::DISCOVERY_TEAR_DOWN: + return nfc::STATUS_OK; + + default: + ESP_LOGE(TAG, "Error starting discovery"); + return nfc::STATUS_FAILED; + } + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::stop_discovery_() { return this->deactivate_(nfc::DEACTIVATION_TYPE_IDLE, NFCC_TAG_WRITE_TIMEOUT); } + +uint8_t PN7160::deactivate_(const uint8_t type, const uint16_t timeout) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {type}); + + auto status = this->transceive_(tx, rx, timeout); + // if (status != nfc::STATUS_OK) { + // ESP_LOGE(TAG, "Error sending deactivate type %u", type); + // return nfc::STATUS_FAILED; + // } + return status; +} + +void PN7160::select_endpoint_() { + if (this->discovered_endpoint_.empty()) { + ESP_LOGW(TAG, "No cached tags to select"); + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + return; + } + std::vector endpoint_data = {this->discovered_endpoint_[0].id, this->discovered_endpoint_[0].protocol, + 0x01}; // that last byte is the interface ID + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (!this->discovered_endpoint_[i].trig_called) { + endpoint_data = {this->discovered_endpoint_[i].id, this->discovered_endpoint_[i].protocol, + 0x01}; // that last byte is the interface ID + this->selecting_endpoint_ = i; + break; + } + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_SELECT_OID, endpoint_data); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error selecting endpoint"); + } else { + this->nci_fsm_set_state_(NCIState::EP_SELECTING); + } +} + +uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) { + uint8_t type = nfc::guess_tag_type(tag.get_uid().size()); + + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + ESP_LOGV(TAG, "Reading Mifare classic"); + return this->read_mifare_classic_tag_(tag); + + case nfc::TAG_TYPE_2: + ESP_LOGV(TAG, "Reading Mifare ultralight"); + return this->read_mifare_ultralight_tag_(tag); + + case nfc::TAG_TYPE_UNKNOWN: + default: + ESP_LOGV(TAG, "Cannot determine tag type"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::clean_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_mifare_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for cleaning"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::format_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_ndef_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for formatting"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::write_endpoint_(std::vector &uid, std::shared_ptr &message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->write_mifare_classic_tag_(message); + + case nfc::TAG_TYPE_2: + return this->write_mifare_ultralight_tag_(uid, message); + + default: + ESP_LOGE(TAG, "Unsupported tag for writing"); + break; + } + return nfc::STATUS_FAILED; +} + +std::unique_ptr PN7160::build_tag_(const uint8_t mode_tech, const std::vector &data) { + switch (mode_tech) { + case (nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA): { + uint8_t uid_length = data[2]; + if (!uid_length) { + ESP_LOGE(TAG, "UID length cannot be zero"); + return nullptr; + } + std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + const auto *tag_type_str = + nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; + return make_unique(uid, tag_type_str); + } + } + return nullptr; +} + +optional PN7160::find_tag_uid_(const std::vector &uid) { + if (!this->discovered_endpoint_.empty()) { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); + bool uid_match = (uid.size() == existing_tag_uid.size()); + + if (uid_match) { + for (size_t i = 0; i < uid.size(); i++) { + uid_match &= (uid[i] == existing_tag_uid[i]); + } + if (uid_match) { + return i; + } + } + } + } + return nullopt; +} + +void PN7160::purge_old_tags_() { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { + this->erase_tag_(i); + } + } +} + +void PN7160::erase_tag_(const uint8_t tag_index) { + if (tag_index < this->discovered_endpoint_.size()) { + for (auto *trigger : this->triggers_ontagremoved_) { + trigger->process(this->discovered_endpoint_[tag_index].tag); + } + for (auto *listener : this->tag_listeners_) { + listener->tag_off(*this->discovered_endpoint_[tag_index].tag); + } + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); + } +} + +void PN7160::nci_fsm_transition_() { + switch (this->nci_state_) { + case NCIState::NFCC_RESET: + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + // fall through + + case NCIState::NFCC_INIT: + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); + } + // fall through + + case NCIState::NFCC_CONFIG: + if (this->send_init_config_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to send initial config"); + this->nci_fsm_set_error_state_(NCIState::NFCC_CONFIG); + return; + } else { + this->config_refresh_pending_ = false; + this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); + } + // fall through + + case NCIState::NFCC_SET_DISCOVER_MAP: + if (this->set_discover_map_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set discover map"); + this->nci_fsm_set_error_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + } + // fall through + + case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: + if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set listen mode routing"); + this->nci_fsm_set_error_state_(NCIState::RFST_IDLE); + return; + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + // fall through + + case NCIState::RFST_IDLE: + if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { + this->stop_discovery_(); + } + + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + + if (!this->listening_enabled_ && !this->polling_enabled_) { + return; + } + + if (this->start_discovery_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to start discovery"); + this->nci_fsm_set_error_state_(NCIState::RFST_DISCOVERY); + } else { + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + } + return; + + case NCIState::RFST_W4_HOST_SELECT: + select_endpoint_(); + // fall through + + // All cases below are waiting for NOTIFICATION messages + case NCIState::RFST_DISCOVERY: + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + // fall through + + case NCIState::RFST_LISTEN_ACTIVE: + case NCIState::RFST_LISTEN_SLEEP: + case NCIState::RFST_POLL_ACTIVE: + case NCIState::EP_SELECTING: + case NCIState::EP_DEACTIVATING: + if (this->irq_pin_->digital_read()) { + this->process_message_(); + } + break; + + case NCIState::FAILED: + case NCIState::NONE: + default: + return; + } +} + +void PN7160::nci_fsm_set_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_state_(%u)", (uint8_t) new_state); + this->nci_state_ = new_state; + this->nci_state_error_ = NCIState::NONE; + this->error_count_ = 0; + this->last_nci_state_change_ = millis(); +} + +bool PN7160::nci_fsm_set_error_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_error_state_(%u); error_count_ = %u", (uint8_t) new_state, this->error_count_); + this->nci_state_error_ = new_state; + if (this->error_count_++ > NFCC_MAX_ERROR_COUNT) { + if ((this->nci_state_error_ == NCIState::NFCC_RESET) || (this->nci_state_error_ == NCIState::NFCC_INIT) || + (this->nci_state_error_ == NCIState::NFCC_CONFIG)) { + ESP_LOGE(TAG, "Too many initialization failures -- check device connections"); + this->mark_failed(); + this->nci_fsm_set_state_(NCIState::FAILED); + } else { + ESP_LOGW(TAG, "Too many errors transitioning to state %u; resetting NFCC", (uint8_t) this->nci_state_error_); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + } + } + return this->error_count_ > NFCC_MAX_ERROR_COUNT; +} + +void PN7160::process_message_() { + nfc::NciMessage rx; + if (this->read_nfcc(rx, NFCC_DEFAULT_TIMEOUT) != nfc::STATUS_OK) { + return; // No data + } + + switch (rx.get_message_type()) { + case nfc::NCI_PKT_MT_CTRL_NOTIFICATION: + if (rx.get_gid() == nfc::RF_GID) { + switch (rx.get_oid()) { + case nfc::RF_INTF_ACTIVATED_OID: + ESP_LOGVV(TAG, "RF_INTF_ACTIVATED_OID"); + this->process_rf_intf_activated_oid_(rx); + return; + + case nfc::RF_DISCOVER_OID: + ESP_LOGVV(TAG, "RF_DISCOVER_OID"); + this->process_rf_discover_oid_(rx); + return; + + case nfc::RF_DEACTIVATE_OID: + ESP_LOGVV(TAG, "RF_DEACTIVATE_OID: type: 0x%02X, reason: 0x%02X", rx.get_message()[3], rx.get_message()[4]); + this->process_rf_deactivate_oid_(rx); + return; + + default: + ESP_LOGV(TAG, "Unimplemented RF OID received: 0x%02X", rx.get_oid()); + } + } else if (rx.get_gid() == nfc::NCI_CORE_GID) { + switch (rx.get_oid()) { + case nfc::NCI_CORE_GENERIC_ERROR_OID: + ESP_LOGV(TAG, "NCI_CORE_GENERIC_ERROR_OID:"); + switch (rx.get_simple_status_response()) { + case nfc::DISCOVERY_ALREADY_STARTED: + ESP_LOGV(TAG, " DISCOVERY_ALREADY_STARTED"); + break; + + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + // Tag removed too soon + ESP_LOGV(TAG, " DISCOVERY_TARGET_ACTIVATION_FAILED"); + if (this->nci_state_ == NCIState::EP_SELECTING) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + if (!this->discovered_endpoint_.empty()) { + this->erase_tag_(this->selecting_endpoint_); + } + } else { + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + case nfc::DISCOVERY_TEAR_DOWN: + ESP_LOGV(TAG, " DISCOVERY_TEAR_DOWN"); + break; + + default: + ESP_LOGW(TAG, "Unknown error: 0x%02X", rx.get_simple_status_response()); + break; + } + break; + + default: + ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); + } + } else { + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + break; + + case nfc::NCI_PKT_MT_CTRL_RESPONSE: + ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), + nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_CTRL_COMMAND: + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_DATA: + this->process_data_message_(rx); + break; + + default: + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + } +} + +void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoint was activated + uint8_t discovery_id = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_DISCOVERY_ID); + uint8_t interface = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_INTERFACE); + uint8_t protocol = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_PROTOCOL); + uint8_t mode_tech = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MODE_TECH); + uint8_t max_size = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MAX_SIZE); + + ESP_LOGVV(TAG, "Endpoint activated -- interface: 0x%02X, protocol: 0x%02X, mode&tech: 0x%02X, max payload: %u", + interface, protocol, mode_tech, max_size); + + if (mode_tech & nfc::MODE_LISTEN_MASK) { + ESP_LOGVV(TAG, "Tag activated in listen mode"); + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_ACTIVE); + return; + } + + this->nci_fsm_set_state_(NCIState::RFST_POLL_ACTIVE); + auto incoming_tag = + this->build_tag_(mode_tech, std::vector(rx.get_message().begin() + 10, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = discovery_id; + this->discovered_endpoint_[tag_loc.value()].protocol = protocol; + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag cache updated"); + } else { + this->discovered_endpoint_.emplace_back( + DiscoveredEndpoint{discovery_id, protocol, millis(), std::move(incoming_tag), false}); + tag_loc = this->discovered_endpoint_.size() - 1; + ESP_LOGVV(TAG, "Tag added to cache"); + } + + auto &working_endpoint = this->discovered_endpoint_[tag_loc.value()]; + + switch (this->next_task_) { + case EP_CLEAN: + ESP_LOGD(TAG, " Tag cleaning..."); + if (this->clean_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag cleaning incomplete"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + break; + + case EP_FORMAT: + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + break; + + case EP_WRITE: + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " 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 { + ESP_LOGD(TAG, " Writing NDEF data"); + if (this->write_endpoint_(working_endpoint.tag->get_uid(), this->next_task_message_to_write_) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + break; + + case EP_READ: + default: + if (!working_endpoint.trig_called) { + ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), + nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { + ESP_LOGW(TAG, " Unable to read NDEF record(s)"); + } else if (working_endpoint.tag->has_ndef_message()) { + const auto message = working_endpoint.tag->get_ndef_message(); + const auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF record(s):"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } else { + ESP_LOGW(TAG, " No NDEF records found"); + } + for (auto *trigger : this->triggers_ontag_) { + trigger->process(working_endpoint.tag); + } + for (auto *listener : this->tag_listeners_) { + listener->tag_on(*working_endpoint.tag); + } + working_endpoint.trig_called = true; + break; + } + } + if (working_endpoint.tag->get_tag_type() == nfc::MIFARE_CLASSIC) { + this->halt_mifare_classic_tag_(); + } + } + if (this->next_task_ != EP_READ) { + this->read_mode(); + } + + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::EP_DEACTIVATING); +} + +void PN7160::process_rf_discover_oid_(nfc::NciMessage &rx) { + auto incoming_tag = this->build_tag_(rx.get_message_byte(nfc::RF_DISCOVER_NTF_MODE_TECH), + std::vector(rx.get_message().begin() + 7, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag!"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID); + this->discovered_endpoint_[tag_loc.value()].protocol = rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL); + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag found & updated"); + } else { + this->discovered_endpoint_.emplace_back(DiscoveredEndpoint{rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID), + rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL), + millis(), std::move(incoming_tag), false}); + ESP_LOGVV(TAG, "Tag saved"); + } + } + + if (rx.get_message().back() != nfc::RF_DISCOVER_NTF_NT_MORE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + ESP_LOGVV(TAG, "Discovered %u endpoints", this->discovered_endpoint_.size()); + } +} + +void PN7160::process_rf_deactivate_oid_(nfc::NciMessage &rx) { + this->ce_state_ = CardEmulationState::CARD_EMU_IDLE; + + switch (rx.get_simple_status_response()) { + case nfc::DEACTIVATION_TYPE_DISCOVERY: + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + break; + + case nfc::DEACTIVATION_TYPE_IDLE: + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + break; + + case nfc::DEACTIVATION_TYPE_SLEEP: + case nfc::DEACTIVATION_TYPE_SLEEP_AF: + if (this->nci_state_ == NCIState::RFST_LISTEN_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_SLEEP); + } else if (this->nci_state_ == NCIState::RFST_POLL_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + default: + break; + } +} + +void PN7160::process_data_message_(nfc::NciMessage &rx) { + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + + std::vector ndef_response; + this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); + + uint16_t ndef_response_size = ndef_response.size(); + if (!ndef_response_size) { + return; // no message returned, we cannot respond + } + + std::vector tx_msg = {nfc::NCI_PKT_MT_DATA, uint8_t((ndef_response_size & 0xFF00) >> 8), + uint8_t(ndef_response_size & 0x00FF)}; + tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); + nfc::NciMessage tx(tx_msg); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending reply for card emulation failed"); + } +} + +void PN7160::card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response) { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation not possible"); + ndef_response.clear(); + return; + } + + if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_APP_SELECT))) { + // CARD_EMU_T4T_APP_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_APP_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_APP_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_CC_SELECT))) { + // CARD_EMU_T4T_CC_SELECT + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_APP_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_CC_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_CC_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_NDEF_SELECT))) { + // CARD_EMU_T4T_NDEF_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_READ), + std::begin(CARD_EMU_T4T_READ))) { + // CARD_EMU_T4T_READ + if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED"); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + if (length <= (sizeof(CARD_EMU_T4T_CC) + offset + 2)) { + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_CC) + offset, + std::begin(CARD_EMU_T4T_CC) + offset + length); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED"); + auto ndef_message = this->card_emulation_message_->encode(); + uint16_t ndef_msg_size = ndef_message.size(); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + + if (length <= (ndef_msg_size + offset + 2)) { + if (offset == 0) { + ndef_response.resize(2); + ndef_response[0] = (ndef_msg_size & 0xFF00) >> 8; + ndef_response[1] = (ndef_msg_size & 0x00FF); + if (length > 2) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 2); + } + } else if (offset == 1) { + ndef_response.resize(1); + ndef_response[0] = (ndef_msg_size & 0x00FF); + if (length > 1) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 1); + } + } else { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length); + } + + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + + if ((offset + length) >= (ndef_msg_size + 2)) { + ESP_LOGD(TAG, "NDEF message sent"); + this->on_emulated_tag_scan_callback_.call(); + } + } + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_WRITE), + std::begin(CARD_EMU_T4T_WRITE))) { + // CARD_EMU_T4T_WRITE + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_T4T_WRITE"); + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + std::vector ndef_msg_written; + + ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } +} + +uint8_t PN7160::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, + const bool expect_notification) { + uint8_t retries = NFCC_MAX_COMM_FAILS; + + while (retries) { + // first, send the message we need to send + if (this->write_nfcc(tx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + // next, the NFCC should send back a response + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error receiving message"); + if (!retries--) { + ESP_LOGE(TAG, " ...giving up"); + return nfc::STATUS_FAILED; + } + } else { + break; + } + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + // validate the response based on the message type that was sent (command vs. data) + if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { + // for commands, the GID and OID should match and the status should be OK + if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + return rx.get_simple_status_response(); + } else { + // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || + (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (expect_notification) { + // if the NFCC said "OK", there will be additional data to read; this comes back in a notification message + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error receiving data from endpoint"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + + return nfc::STATUS_OK; + } +} + +uint8_t PN7160::wait_for_irq_(uint16_t timeout, bool pin_state) { + auto start_time = millis(); + + while (millis() - start_time < timeout) { + if (this->irq_pin_->digital_read() == pin_state) { + return nfc::STATUS_OK; + } + } + ESP_LOGW(TAG, "Timed out waiting for IRQ state"); + return nfc::STATUS_FAILED; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h new file mode 100644 index 0000000000..f2e05ea1d0 --- /dev/null +++ b/esphome/components/pn7160/pn7160.h @@ -0,0 +1,315 @@ +#pragma once + +#include "esphome/components/nfc/automation.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/nfc/nci_message.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_helpers.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace pn7160 { + +static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static const uint16_t NFCC_INIT_TIMEOUT = 50; +static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; + +static const uint8_t NFCC_MAX_COMM_FAILS = 3; +static const uint8_t NFCC_MAX_ERROR_COUNT = 10; + +static const uint8_t XCHG_DATA_OID = 0x10; +static const uint8_t MF_SECTORSEL_OID = 0x32; +static const uint8_t MFC_AUTHENTICATE_OID = 0x40; +static const uint8_t TEST_PRBS_OID = 0x30; +static const uint8_t TEST_ANTENNA_OID = 0x3D; +static const uint8_t TEST_GET_REGISTER_OID = 0x33; + +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; + +static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; + +static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms + +static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms + +static const uint8_t PMU_CFG[] = { + 0x01, // Number of parameters + 0xA0, 0x0E, // ext. tag + 11, // length + 0x11, // IRQ Enable: PVDD + temp sensor IRQs + 0x01, // RFU + 0x01, // Power and Clock Configuration, device on (CFG1) + 0x01, // Power and Clock Configuration, device off (CFG1) + 0x00, // RFU + 0x00, // DC-DC 0 + 0x00, // DC-DC 1 + // 0x14, // TXLDO (3.3V / 4.75V) + // 0xBB, // TXLDO (4.7V / 4.7V) + 0xFF, // TXLDO (5.0V / 5.0V) + 0x00, // RFU + 0xD0, // TXLDO check + 0x0C, // RFU +}; + +static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes + nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN, + nfc::INTF_ISODEP, // poll & listen mode + nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_TAGCMD}; // poll mode + +static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode + +static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 2, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::PROT_ISODEP, // protocol + 0x00, // type = technology-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::TECH_PASSIVE_NFCA}; // technology + +enum class CardEmulationState : uint8_t { + CARD_EMU_IDLE, + CARD_EMU_NDEF_APP_SELECTED, + CARD_EMU_CC_SELECTED, + CARD_EMU_NDEF_SELECTED, + CARD_EMU_DESFIRE_PROD, +}; + +enum class NCIState : uint8_t { + NONE = 0x00, + NFCC_RESET, + NFCC_INIT, + NFCC_CONFIG, + NFCC_SET_DISCOVER_MAP, + NFCC_SET_LISTEN_MODE_ROUTING, + RFST_IDLE, + RFST_DISCOVERY, + RFST_W4_ALL_DISCOVERIES, + RFST_W4_HOST_SELECT, + RFST_LISTEN_ACTIVE, + RFST_LISTEN_SLEEP, + RFST_POLL_ACTIVE, + EP_DEACTIVATING, + EP_SELECTING, + TEST = 0XFE, + FAILED = 0XFF, +}; + +enum class TestMode : uint8_t { + TEST_NONE = 0x00, + TEST_PRBS, + TEST_ANTENNA, + TEST_GET_REGISTER, +}; + +struct DiscoveredEndpoint { + uint8_t id; + uint8_t protocol; + uint32_t last_seen; + std::unique_ptr tag; + bool trig_called; +}; + +class PN7160 : public nfc::Nfcc, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; } + void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } + void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; } + void set_wkup_req_pin(GPIOPin *wkup_req_pin) { this->wkup_req_pin_ = wkup_req_pin; } + + void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; } + void set_tag_emulation_message(std::shared_ptr message); + void set_tag_emulation_message(const optional &message, optional include_android_app_record); + void set_tag_emulation_message(const char *message, bool include_android_app_record = true); + void set_tag_emulation_off(); + void set_tag_emulation_on(); + bool tag_emulation_enabled() { return this->listening_enabled_; } + + void set_polling_off(); + void set_polling_on(); + bool polling_enabled() { return this->polling_enabled_; } + + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_emulated_tag_scan_callback(std::function callback) { + this->on_emulated_tag_scan_callback_.add(std::move(callback)); + } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != EP_READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(); + void set_tag_write_message(std::shared_ptr message); + void set_tag_write_message(optional message, optional include_android_app_record); + + uint8_t set_test_mode(TestMode test_mode, const std::vector &data, std::vector &result); + + protected: + uint8_t reset_core_(bool reset_config, bool power); + uint8_t init_core_(); + uint8_t send_init_config_(); + uint8_t send_core_config_(); + uint8_t refresh_core_config_(); + + uint8_t set_discover_map_(); + + uint8_t set_listen_mode_routing_(); + + uint8_t start_discovery_(); + uint8_t stop_discovery_(); + uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT); + + void select_endpoint_(); + + uint8_t read_endpoint_data_(nfc::NfcTag &tag); + uint8_t clean_endpoint_(std::vector &uid); + uint8_t format_endpoint_(std::vector &uid); + uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + + std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); + optional find_tag_uid_(const std::vector &uid); + void purge_old_tags_(); + void erase_tag_(uint8_t tag_index); + + /// advance controller state as required + void nci_fsm_transition_(); + /// set new controller state + void nci_fsm_set_state_(NCIState new_state); + /// setting controller to this state caused an error; returns true if too many errors/failures + bool nci_fsm_set_error_state_(NCIState new_state); + /// parse & process incoming messages from the NFCC + void process_message_(); + void process_rf_intf_activated_oid_(nfc::NciMessage &rx); + void process_rf_discover_oid_(nfc::NciMessage &rx); + void process_rf_deactivate_oid_(nfc::NciMessage &rx); + void process_data_message_(nfc::NciMessage &rx); + + void card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response); + + uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT, + bool expect_notification = true); + virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0; + virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0; + + uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true); + + uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); + uint8_t sect_to_auth_(uint8_t block_num); + uint8_t format_mifare_classic_mifare_(); + uint8_t format_mifare_classic_ndef_(); + uint8_t write_mifare_classic_tag_(const std::shared_ptr &message); + uint8_t halt_mifare_classic_tag_(); + + uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); + uint16_t read_mifare_ultralight_capacity_(); + uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t clean_mifare_ultralight_(); + + enum NfcTask : uint8_t { + EP_READ = 0, + EP_CLEAN, + EP_FORMAT, + EP_WRITE, + } next_task_{EP_READ}; + + bool config_refresh_pending_{false}; + bool core_config_is_solo_{false}; + bool listening_enabled_{false}; + bool polling_enabled_{true}; + + uint8_t error_count_{0}; + uint8_t fail_count_{0}; + uint32_t last_nci_state_change_{0}; + uint8_t selecting_endpoint_{0}; + uint32_t tag_ttl_{250}; + + GPIOPin *dwl_req_pin_{nullptr}; + GPIOPin *irq_pin_{nullptr}; + GPIOPin *ven_pin_{nullptr}; + GPIOPin *wkup_req_pin_{nullptr}; + + CallbackManager on_emulated_tag_scan_callback_; + CallbackManager on_finished_write_callback_; + + std::vector discovered_endpoint_; + + CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE}; + NCIState nci_state_{NCIState::NFCC_RESET}; + NCIState nci_state_error_{NCIState::NONE}; + + std::shared_ptr card_emulation_message_; + std::shared_ptr next_task_message_to_write_; + + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; +}; + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp new file mode 100644 index 0000000000..fa63cc00d5 --- /dev/null +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -0,0 +1,322 @@ +#include + +#include "pn7160.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160.mifare_classic"; + +uint8_t PN7160::read_mifare_classic_tag_(nfc::NfcTag &tag) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data"); + return nfc::STATUS_FAILED; + } + std::vector data; + + if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return nfc::STATUS_FAILED; + } + } else { + ESP_LOGE(TAG, "Failed to read block %u", current_block); + return nfc::STATUS_FAILED; + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Block authentication failed for %u", current_block); + return nfc::STATUS_FAILED; + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading block %u", current_block); + return nfc::STATUS_FAILED; + } else { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + + if (buffer.begin() + message_start_index < buffer.end()) { + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + } else { + return nfc::STATUS_FAILED; + } + + tag.set_ndef_message(make_unique(buffer)); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Timeout reading tag data"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (!rx.message_length_is(18))) { + ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1); + + ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes(data).c_str()); + return nfc::STATUS_OK; +} + +uint8_t PN7160::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num}); + + switch (key_num) { + case nfc::MIFARE_CMD_AUTH_A: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A; + break; + + case nfc::MIFARE_CMD_AUTH_B: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B; + break; + + default: + break; + } + + if (key != nullptr) { + tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY; + tx.get_message().insert(tx.get_message().end(), key, key + 6); + } + + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); + return nfc::STATUS_FAILED; + } + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || + (rx.get_message()[4] != nfc::STATUS_OK)) { + ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num); + return nfc::STATUS_OK; +} + +uint8_t PN7160::sect_to_auth_(const uint8_t block_num) { + const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + if (block_num >= first_high_block) { + return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) + + nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + } + return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW; +} + +uint8_t PN7160::format_mifare_classic_mifare_() { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + auto status = nfc::STATUS_OK; + + for (int block = 0; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + continue; + } + if (block != 0) { + if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + + return status; +} + +uint8_t PN7160::format_mifare_classic_ndef_() { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Sector 0 formatted with NDEF"); + + auto status = nfc::STATUS_OK; + + for (int block = 4; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (block == 4) { + if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } else { + if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + return status; +} + +uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + // write command part two + tx.set_payload({XCHG_DATA_OID}); + tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) { + ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr &message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::halt_mifare_classic_tag_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0}); + + ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp new file mode 100644 index 0000000000..a74f23d4f2 --- /dev/null +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "pn7160.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160.mifare_ultralight"; + +uint8_t PN7160::read_mifare_ultralight_tag_(nfc::NfcTag &tag) { + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); + return nfc::STATUS_FAILED; + } + + uint8_t message_length; + uint8_t message_start_index; + if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); + + if (message_length == 0) { + return nfc::STATUS_FAILED; + } + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + + tag.set_ndef_message(make_unique(data)); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page}); + + for (size_t i = 0; i * read_increment < num_bytes; i++) { + tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page; + do { // loop because sometimes we struggle here...???... + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } while (rx.get_payload_size() < read_increment); + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1 + : rx.get_message().end() - (bytes_offset - num_bytes + 1); + + if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) { + data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr); + } + } + + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + + return nfc::STATUS_OK; +} + +bool PN7160::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); +} + +uint16_t PN7160::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); + return data[2] * 8U; + } + return 0; +} + +uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return nfc::STATUS_FAILED; + } + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; + message_start_index = 2; + return nfc::STATUS_OK; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; + message_start_index = 7; + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::write_mifare_ultralight_tag_(std::vector &uid, + const std::shared_ptr &message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); + return nfc::STATUS_FAILED; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; + payload.insert(payload.end(), write_data.begin(), write_data.end()); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); + + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error writing page %u", page_num); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160_i2c/__init__.py b/esphome/components/pn7160_i2c/__init__.py new file mode 100644 index 0000000000..87c4719ca8 --- /dev/null +++ b/esphome/components/pn7160_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn7160 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7160"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn7160_i2c_ns = cg.esphome_ns.namespace("pn7160_i2c") +PN7160I2C = pn7160_i2c_ns.class_("PN7160I2C", pn7160.PN7160, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn7160.PN7160_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7160I2C), + } + ).extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7160.setup_pn7160(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.cpp b/esphome/components/pn7160_i2c/pn7160_i2c.cpp new file mode 100644 index 0000000000..7c6da9dd06 --- /dev/null +++ b/esphome/components/pn7160_i2c/pn7160_i2c.cpp @@ -0,0 +1,49 @@ +#include "pn7160_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pn7160_i2c { + +static const char *const TAG = "pn7160_i2c"; + +uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE)) { + return nfc::STATUS_FAILED; + } + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length)) { + return nfc::STATUS_FAILED; + } + } + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160I2C::write_nfcc(nfc::NciMessage &tx) { + if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +void PN7160I2C::dump_config() { + PN7160::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn7160_i2c +} // namespace esphome diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.h b/esphome/components/pn7160_i2c/pn7160_i2c.h new file mode 100644 index 0000000000..eb253085eb --- /dev/null +++ b/esphome/components/pn7160_i2c/pn7160_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn7160/pn7160.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace pn7160_i2c { + +class PN7160I2C : public pn7160::PN7160, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7160_i2c +} // namespace esphome diff --git a/esphome/components/pn7160_spi/__init__.py b/esphome/components/pn7160_spi/__init__.py new file mode 100644 index 0000000000..ae1235655a --- /dev/null +++ b/esphome/components/pn7160_spi/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, pn7160 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7160"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +pn7160_spi_ns = cg.esphome_ns.namespace("pn7160_spi") +PN7160Spi = pn7160_spi_ns.class_("PN7160Spi", pn7160.PN7160, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + pn7160.PN7160_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7160Spi), + } + ).extend(spi.spi_device_schema(cs_pin_required=True)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7160.setup_pn7160(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/pn7160_spi/pn7160_spi.cpp b/esphome/components/pn7160_spi/pn7160_spi.cpp new file mode 100644 index 0000000000..09f673f700 --- /dev/null +++ b/esphome/components/pn7160_spi/pn7160_spi.cpp @@ -0,0 +1,54 @@ +#include "pn7160_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160_spi { + +static const char *const TAG = "pn7160_spi"; + +void PN7160Spi::setup() { + this->spi_setup(); + this->cs_->digital_write(false); + PN7160::setup(); +} + +uint8_t PN7160Spi::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + this->enable(); + this->write_byte(TDD_SPI_READ); // send "transfer direction detector" + this->read_array(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE); + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + this->read_array(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length); + } + this->disable(); + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160Spi::write_nfcc(nfc::NciMessage &tx) { + this->enable(); + this->write_byte(TDD_SPI_WRITE); // send "transfer direction detector" + this->write_array(tx.encode().data(), tx.encode().size()); + this->disable(); + return nfc::STATUS_OK; +} + +void PN7160Spi::dump_config() { + PN7160::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +} // namespace pn7160_spi +} // namespace esphome diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h new file mode 100644 index 0000000000..7d4460a76d --- /dev/null +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/pn7160/pn7160.h" +#include "esphome/components/spi/spi.h" + +#include + +namespace esphome { +namespace pn7160_spi { + +static const uint8_t TDD_SPI_READ = 0xFF; +static const uint8_t TDD_SPI_WRITE = 0x0A; + +class PN7160Spi : public pn7160::PN7160, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7160_spi +} // namespace esphome diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py index f7dd8bca84..01b541e4b5 100644 --- a/esphome/components/power_supply/__init__.py +++ b/esphome/components/power_supply/__init__.py @@ -1,7 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ENABLE_TIME, CONF_ID, CONF_KEEP_ON_TIME, CONF_PIN +from esphome.const import ( + CONF_ENABLE_ON_BOOT, + CONF_ENABLE_TIME, + CONF_ID, + CONF_KEEP_ON_TIME, + CONF_PIN, +) CODEOWNERS = ["@esphome/core"] power_supply_ns = cg.esphome_ns.namespace("power_supply") @@ -18,6 +24,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional( CONF_KEEP_ON_TIME, default="10s" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ENABLE_ON_BOOT, default=False): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -30,5 +37,6 @@ async def to_code(config): cg.add(var.set_pin(pin)) cg.add(var.set_enable_time(config[CONF_ENABLE_TIME])) cg.add(var.set_keep_on_time(config[CONF_KEEP_ON_TIME])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add_define("USE_POWER_SUPPLY") diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index a492919202..7474075302 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -11,47 +11,42 @@ void PowerSupply::setup() { this->pin_->setup(); this->pin_->digital_write(false); - this->enabled_ = false; + if (this->enable_on_boot_) + this->request_high_power(); } void PowerSupply::dump_config() { ESP_LOGCONFIG(TAG, "Power Supply:"); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Time to enable: %u ms", this->enable_time_); + ESP_LOGCONFIG(TAG, " Time to enable: %" PRIu32 " ms", this->enable_time_); ESP_LOGCONFIG(TAG, " Keep on time: %.1f s", this->keep_on_time_ / 1000.0f); + if (this->enable_on_boot_) + ESP_LOGCONFIG(TAG, " Enabled at startup: True"); } float PowerSupply::get_setup_priority() const { return setup_priority::IO; } -bool PowerSupply::is_enabled() const { return this->enabled_; } +bool PowerSupply::is_enabled() const { return this->active_requests_ != 0; } void PowerSupply::request_high_power() { - this->cancel_timeout("power-supply-off"); - this->pin_->digital_write(true); - if (this->active_requests_ == 0) { - // we need to enable the power supply. - // cancel old timeout if it exists because we now definitely have a high power mode. + this->cancel_timeout("power-supply-off"); ESP_LOGD(TAG, "Enabling power supply."); + this->pin_->digital_write(true); delay(this->enable_time_); } - this->enabled_ = true; - // increase active requests this->active_requests_++; } void PowerSupply::unrequest_high_power() { - this->active_requests_--; - if (this->active_requests_ < 0) { - // we're just going to use 0 as our new counter. - this->active_requests_ = 0; - } - if (this->active_requests_ == 0) { - // set timeout for power supply off + ESP_LOGW(TAG, "Invalid call to unrequest_high_power"); + return; + } + this->active_requests_--; + if (this->active_requests_ == 0) { this->set_timeout("power-supply-off", this->keep_on_time_, [this]() { ESP_LOGD(TAG, "Disabling power supply."); this->pin_->digital_write(false); - this->enabled_ = false; }); } } diff --git a/esphome/components/power_supply/power_supply.h b/esphome/components/power_supply/power_supply.h index 66e4a7565a..0b06105ae9 100644 --- a/esphome/components/power_supply/power_supply.h +++ b/esphome/components/power_supply/power_supply.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace power_supply { @@ -11,6 +13,7 @@ class PowerSupply : public Component { void set_pin(GPIOPin *pin) { pin_ = pin; } void set_enable_time(uint32_t enable_time) { enable_time_ = enable_time; } void set_keep_on_time(uint32_t keep_on_time) { keep_on_time_ = keep_on_time; } + void set_enable_on_boot(bool enable_on_boot) { enable_on_boot_ = enable_on_boot; } /// Is this power supply currently on? bool is_enabled() const; @@ -33,7 +36,7 @@ class PowerSupply : public Component { protected: GPIOPin *pin_; - bool enabled_{false}; + bool enable_on_boot_{false}; uint32_t enable_time_; uint32_t keep_on_time_; int16_t active_requests_{0}; // use signed integer to make catching negative requests easier. diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index abb5111aaf..5d1861202a 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -1,6 +1,5 @@ -#ifdef USE_ARDUINO - #include "prometheus_handler.h" +#ifdef USE_NETWORK #include "esphome/core/application.h" namespace esphome { @@ -8,47 +7,56 @@ namespace prometheus { void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { AsyncResponseStream *stream = req->beginResponseStream("text/plain; version=0.0.4; charset=utf-8"); + std::string area = App.get_area(); + std::string node = App.get_name(); + std::string friendly_name = App.get_friendly_name(); #ifdef USE_SENSOR this->sensor_type_(stream); for (auto *obj : App.get_sensors()) - this->sensor_row_(stream, obj); + this->sensor_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_BINARY_SENSOR this->binary_sensor_type_(stream); for (auto *obj : App.get_binary_sensors()) - this->binary_sensor_row_(stream, obj); + this->binary_sensor_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_FAN this->fan_type_(stream); for (auto *obj : App.get_fans()) - this->fan_row_(stream, obj); + this->fan_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_LIGHT this->light_type_(stream); for (auto *obj : App.get_lights()) - this->light_row_(stream, obj); + this->light_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_COVER this->cover_type_(stream); for (auto *obj : App.get_covers()) - this->cover_row_(stream, obj); + this->cover_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_SWITCH this->switch_type_(stream); for (auto *obj : App.get_switches()) - this->switch_row_(stream, obj); + this->switch_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_LOCK this->lock_type_(stream); for (auto *obj : App.get_locks()) - this->lock_row_(stream, obj); + this->lock_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_TEXT_SENSOR + this->text_sensor_type_(stream); + for (auto *obj : App.get_text_sensors()) + this->text_sensor_row_(stream, obj, area, node, friendly_name); #endif req->send(stream); @@ -64,36 +72,67 @@ std::string PrometheusHandler::relabel_name_(EntityBase *obj) { return item == relabel_map_name_.end() ? obj->get_name() : item->second; } +void PrometheusHandler::add_area_label_(AsyncResponseStream *stream, std::string &area) { + if (!area.empty()) { + stream->print(F("\",area=\"")); + stream->print(area.c_str()); + } +} + +void PrometheusHandler::add_node_label_(AsyncResponseStream *stream, std::string &node) { + if (!node.empty()) { + stream->print(F("\",node=\"")); + stream->print(node.c_str()); + } +} + +void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name) { + if (!friendly_name.empty()) { + stream->print(F("\",friendly_name=\"")); + stream->print(friendly_name.c_str()); + } +} + // Type-specific implementation #ifdef USE_SENSOR void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_sensor_value GAUGE\n")); - stream->print(F("#TYPE esphome_sensor_failed GAUGE\n")); + stream->print(F("#TYPE esphome_sensor_value gauge\n")); + stream->print(F("#TYPE esphome_sensor_failed gauge\n")); } -void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { +void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, + std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; if (!std::isnan(obj->state)) { // We have a valid value, output this value stream->print(F("esphome_sensor_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_sensor_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",unit=\"")); stream->print(obj->get_unit_of_measurement().c_str()); stream->print(F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); - stream->print('\n'); + stream->print(F("\n")); } else { // Invalid state stream->print(F("esphome_sensor_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); @@ -104,31 +143,41 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor // Type-specific implementation #ifdef USE_BINARY_SENSOR void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_binary_sensor_value GAUGE\n")); - stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n")); + stream->print(F("#TYPE esphome_binary_sensor_value gauge\n")); + stream->print(F("#TYPE esphome_binary_sensor_failed gauge\n")); } -void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { +void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *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(F("esphome_binary_sensor_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_binary_sensor_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); - stream->print('\n'); + stream->print(F("\n")); } else { // Invalid state stream->print(F("esphome_binary_sensor_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); @@ -138,62 +187,79 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s #ifdef USE_FAN void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_fan_value GAUGE\n")); - stream->print(F("#TYPE esphome_fan_failed GAUGE\n")); - stream->print(F("#TYPE esphome_fan_speed GAUGE\n")); - stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n")); + stream->print(F("#TYPE esphome_fan_value gauge\n")); + stream->print(F("#TYPE esphome_fan_failed gauge\n")); + stream->print(F("#TYPE esphome_fan_speed gauge\n")); + stream->print(F("#TYPE esphome_fan_oscillation gauge\n")); } -void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { +void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node, + std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_fan_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_fan_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); - stream->print('\n'); + stream->print(F("\n")); // Speed if available if (obj->get_traits().supports_speed()) { stream->print(F("esphome_fan_speed{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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->speed); - stream->print('\n'); + stream->print(F("\n")); } // Oscillation if available if (obj->get_traits().supports_oscillation()) { stream->print(F("esphome_fan_oscillation{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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->oscillating); - stream->print('\n'); + stream->print(F("\n")); } } #endif #ifdef USE_LIGHT void PrometheusHandler::light_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_light_state GAUGE\n")); - stream->print(F("#TYPE esphome_light_color GAUGE\n")); - stream->print(F("#TYPE esphome_light_effect_active GAUGE\n")); + stream->print(F("#TYPE esphome_light_state gauge\n")); + stream->print(F("#TYPE esphome_light_color gauge\n")); + stream->print(F("#TYPE esphome_light_effect_active gauge\n")); } -void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) { +void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, + std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; // State stream->print(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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -206,6 +272,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat color.as_rgbw(&r, &g, &b, &w); stream->print(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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"brightness\"} ")); @@ -213,6 +282,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat stream->print(F("\n")); stream->print(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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"r\"} ")); @@ -220,6 +292,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat stream->print(F("\n")); stream->print(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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"g\"} ")); @@ -227,6 +302,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat stream->print(F("\n")); stream->print(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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"b\"} ")); @@ -234,6 +312,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat stream->print(F("\n")); stream->print(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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"w\"} ")); @@ -244,12 +325,18 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat if (effect == "None") { stream->print(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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",effect=\"None\"} 0\n")); } else { stream->print(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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",effect=\"")); @@ -261,40 +348,53 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat #ifdef USE_COVER void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_cover_value GAUGE\n")); - stream->print(F("#TYPE esphome_cover_failed GAUGE\n")); + stream->print(F("#TYPE esphome_cover_value gauge\n")); + stream->print(F("#TYPE esphome_cover_failed gauge\n")); } -void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { +void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node, + std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; if (!std::isnan(obj->position)) { // We have a valid value, output this value stream->print(F("esphome_cover_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_cover_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->position); - stream->print('\n'); + stream->print(F("\n")); if (obj->get_traits().get_supports_tilt()) { stream->print(F("esphome_cover_tilt{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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->tilt); - stream->print('\n'); + stream->print(F("\n")); } } else { // Invalid state stream->print(F("esphome_cover_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); @@ -304,53 +404,113 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob #ifdef USE_SWITCH void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_switch_value GAUGE\n")); - stream->print(F("#TYPE esphome_switch_failed GAUGE\n")); + stream->print(F("#TYPE esphome_switch_value gauge\n")); + stream->print(F("#TYPE esphome_switch_failed gauge\n")); } -void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { +void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, + std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_switch_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_switch_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); - stream->print('\n'); + stream->print(F("\n")); } #endif #ifdef USE_LOCK void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_lock_value GAUGE\n")); - stream->print(F("#TYPE esphome_lock_failed GAUGE\n")); + stream->print(F("#TYPE esphome_lock_value gauge\n")); + stream->print(F("#TYPE esphome_lock_failed gauge\n")); } -void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) { +void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node, + std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_lock_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_lock_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(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); - stream->print('\n'); + stream->print(F("\n")); +} +#endif + +// Type-specific implementation +#ifdef USE_TEXT_SENSOR +void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_text_sensor_value gauge\n")); + stream->print(F("#TYPE esphome_text_sensor_failed gauge\n")); +} +void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *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(F("esphome_text_sensor_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(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_text_sensor_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(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\",value=\"")); + stream->print(obj->state.c_str()); + stream->print(F("\"} ")); + stream->print(F("1.0")); + stream->print(F("\n")); + } else { + // Invalid state + stream->print(F("esphome_text_sensor_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(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} 1\n")); + } } #endif } // namespace prometheus } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index f416ecf246..5d08aca63a 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -1,13 +1,13 @@ #pragma once - -#ifdef USE_ARDUINO - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include #include "esphome/components/web_server_base/web_server_base.h" -#include "esphome/core/controller.h" #include "esphome/core/component.h" +#include "esphome/core/controller.h" +#include "esphome/core/entity_base.h" namespace esphome { namespace prometheus { @@ -60,54 +60,72 @@ class PrometheusHandler : public AsyncWebHandler, public Component { protected: std::string relabel_id_(EntityBase *obj); std::string relabel_name_(EntityBase *obj); + 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); #ifdef USE_SENSOR /// Return the type for prometheus void sensor_type_(AsyncResponseStream *stream); /// Return the sensor state as prometheus data point - void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj); + void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_BINARY_SENSOR /// Return the type for prometheus void binary_sensor_type_(AsyncResponseStream *stream); /// Return the sensor state as prometheus data point - void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj); + void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj, std::string &area, + std::string &node, std::string &friendly_name); #endif #ifdef USE_FAN /// Return the type for prometheus void fan_type_(AsyncResponseStream *stream); /// Return the sensor state as prometheus data point - void fan_row_(AsyncResponseStream *stream, fan::Fan *obj); + void fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_LIGHT /// Return the type for prometheus void light_type_(AsyncResponseStream *stream); /// Return the Light Values state as prometheus data point - void light_row_(AsyncResponseStream *stream, light::LightState *obj); + void light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_COVER /// Return the type for prometheus void cover_type_(AsyncResponseStream *stream); /// Return the switch Values state as prometheus data point - void cover_row_(AsyncResponseStream *stream, cover::Cover *obj); + void cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_SWITCH /// Return the type for prometheus void switch_type_(AsyncResponseStream *stream); /// Return the switch Values state as prometheus data point - void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj); + void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_LOCK /// Return the type for prometheus void lock_type_(AsyncResponseStream *stream); /// Return the lock Values state as prometheus data point - void lock_row_(AsyncResponseStream *stream, lock::Lock *obj); + void lock_row_(AsyncResponseStream *stream, lock::Lock *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); + /// Return the lock Values state as prometheus data point + void text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif web_server_base::WebServerBase *base_; @@ -118,5 +136,4 @@ class PrometheusHandler : public AsyncWebHandler, public Component { } // namespace prometheus } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index ac6d034514..796957c315 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -1,9 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.core import CORE from esphome.const import ( CONF_ID, + CONF_MODE, + CONF_SPEED, ) CODEOWNERS = ["@esphome/core"] @@ -11,8 +13,26 @@ CODEOWNERS = ["@esphome/core"] psram_ns = cg.esphome_ns.namespace("psram") PsramComponent = psram_ns.class_("PsramComponent", cg.Component) +SPIRAM_MODES = { + "quad": "CONFIG_SPIRAM_MODE_QUAD", + "octal": "CONFIG_SPIRAM_MODE_OCT", +} + +SPIRAM_SPEEDS = { + 40e6: "CONFIG_SPIRAM_SPEED_40M", + 80e6: "CONFIG_SPIRAM_SPEED_80M", + 120e6: "CONFIG_SPIRAM_SPEED_120M", +} + CONFIG_SCHEMA = cv.All( - cv.Schema({cv.GenerateID(): cv.declare_id(PsramComponent)}), cv.only_on_esp32 + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PsramComponent), + cv.Optional(CONF_MODE): cv.enum(SPIRAM_MODES, lower=True), + cv.Optional(CONF_SPEED): cv.All(cv.frequency, cv.one_of(*SPIRAM_SPEEDS)), + } + ), + cv.only_on_esp32, ) @@ -21,9 +41,20 @@ async def to_code(config): cg.add_build_flag("-DBOARD_HAS_PSRAM") if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_ESP32_SPIRAM_SUPPORT", True) + add_idf_sdkconfig_option( + f"CONFIG_{get_esp32_variant().upper()}_SPIRAM_SUPPORT", True + ) + add_idf_sdkconfig_option("CONFIG_SPIRAM", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True) + if CONF_MODE in config: + add_idf_sdkconfig_option(f"{SPIRAM_MODES[config[CONF_MODE]]}", True) + if CONF_SPEED in config: + add_idf_sdkconfig_option(f"{SPIRAM_SPEEDS[config[CONF_SPEED]]}", True) + + cg.add_define("USE_PSRAM") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 1f50360fed..bd3e4fcbef 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -53,12 +53,19 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() { #ifdef HAS_PCNT bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0; + static pcnt_channel_t next_pcnt_channel = PCNT_CHANNEL_0; this->pin = pin; this->pin->setup(); this->pcnt_unit = next_pcnt_unit; + this->pcnt_channel = next_pcnt_channel; next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1); + if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) { + next_pcnt_unit = PCNT_UNIT_0; + 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); pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS; switch (this->rising_edge_mode) { @@ -94,7 +101,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { .counter_h_lim = 0, .counter_l_lim = 0, .unit = this->pcnt_unit, - .channel = PCNT_CHANNEL_0, + .channel = this->pcnt_channel, }; esp_err_t error = pcnt_unit_config(&pcnt_config); if (error != ESP_OK) { @@ -104,7 +111,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { if (this->filter_us != 0) { uint16_t filter_val = std::min(static_cast(this->filter_us * 80u), 1023u); - ESP_LOGCONFIG(TAG, " Filter Value: %uus (val=%u)", this->filter_us, filter_val); + ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val); error = pcnt_set_filter_value(this->pcnt_unit, filter_val); if (error != ESP_OK) { ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error)); @@ -161,7 +168,7 @@ void PulseCounterSensor::dump_config() { LOG_PIN(" Pin: ", this->pin_); ESP_LOGCONFIG(TAG, " Rising Edge: %s", EDGE_MODE_TO_STRING[this->storage_.rising_edge_mode]); ESP_LOGCONFIG(TAG, " Falling Edge: %s", EDGE_MODE_TO_STRING[this->storage_.falling_edge_mode]); - ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %u µs", this->storage_.filter_us); + ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %" PRIu32 " µs", this->storage_.filter_us); LOG_UPDATE_INTERVAL(this); } @@ -177,7 +184,7 @@ void PulseCounterSensor::update() { if (this->total_sensor_ != nullptr) { current_total_ += raw; - ESP_LOGD(TAG, "'%s': Total : %i pulses", this->get_name().c_str(), current_total_); + ESP_LOGD(TAG, "'%s': Total : %" PRIu32 " pulses", this->get_name().c_str(), current_total_); this->total_sensor_->publish_state(current_total_); } this->last_time_ = now; diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index ef944f106f..fc3d8711d1 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -4,6 +4,8 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" +#include + #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #include #define HAS_PCNT @@ -53,6 +55,7 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase { pulse_counter_t read_raw_value() override; pcnt_unit_t pcnt_unit; + pcnt_channel_t pcnt_channel; }; #endif diff --git a/esphome/components/pulse_meter/pulse_filter.md b/esphome/components/pulse_meter/pulse_filter.md new file mode 100644 index 0000000000..240c479d54 --- /dev/null +++ b/esphome/components/pulse_meter/pulse_filter.md @@ -0,0 +1,61 @@ +# PULSE Filter + +The PULSE filter filters noisy pulses by ensuring that the pulse is in a steady state for at least `filter_length` before allowing the state change to occur. +It counts the pulse time from the rising edge that stayed high for at least `filter_length`, so noise before this won't be considered the start of a pulse. +It then must see a low pulse that is at least `filter_length` long before a subsequent rising edge is considered a new pulse start. + +It's operation should be the same as delayed_on_off from the Binary Sensor component. + +There are three moving parts in the algorithm that are used to determine the state of the filter. + +1. The time between interrupts, measured from the last interrupt, this is compared to filter_length to determine if the pulse has been in a steady state for long enough. +2. A latch variable that is set true when a high pulse is long enough to be considered a valid pulse and is reset when a low pulse is long enough to allow for another pulse to begin. +3. The previous and current state of the pin used to determine if the pulse is rising or falling. + +## Ghost interrupts + +Observations from the devices show that even though the interrupt should trigger on every rising or falling edge, sometimes interrupts show the same state twice in a row. +The current theory is an interprets occurs, but then the pin changing back faster than the ISR can be called and read the value, meaning it sees the same state twice in a row. +The algorithm interprets these when it sees them as two edges in a row, so will potentially reset a pulse if + +## Pulse Filter Truth table + +The following is all of the possible states of the filter along with the new inputs. +It also shows a diagram of a possible series of interrupts that would cause the filter to enter that state. +It then has the action that the filter should take and a description of what is happening. + +Diagram legend + +- `/` rising edge +- `\` falling edge +- `‾` high steady state of at least `filter_length` +- `_` low steady state of at least `filter_length` +- `¦` ghost interrupt + +| Length | Latch | From | To | Diagram | Action | Description | +| ------ | ----- | ---- | --- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------- | +| T | 1 | 0 | 0 | `‾\_¦ ` | Reset | `filter_length` low, reset the latch | +| T | 1 | 0 | 1 | `‾\_/ ` | Reset, Rising Edge | `filter_length` low, reset the latch, rising edge could be a new pulse | +| T | 1 | 1 | 0 | `‾\/‾\` | - | Already latched from a previous `filter_length` high | +| T | 1 | 1 | 1 | `‾\/‾¦` | - | Already latched from a previous `filter_length` high | +| T | 0 | 1 | 1 | `_/‾¦` | Set and Publish | `filter_length` high, set the latch and publish the pulse | +| T | 0 | 1 | 0 | `_/‾\ ` | Set and Publish | `filter_length` high, set the latch and publish the pulse | +| T | 0 | 0 | 1 | `_/\_/` | Rising Edge | Already unlatched from a previous `filter_length` low | +| T | 0 | 0 | 0 | `_/\_¦` | - | Already unlatched from a previous `filter_length` low | +| F | 1 | 0 | 0 | `‾\¦ ` | - | Low was not long enough to reset the latch | +| F | 1 | 0 | 1 | `‾\/ ` | - | Low was not long enough to reset the latch | +| F | 1 | 1 | 0 | `‾\/\ ` | - | Low was not long enough to reset the latch | +| F | 1 | 1 | 1 | `‾¦ ` | - | Ghost of 0 length definitely was not long was not long enough to reset the latch | +| F | 0 | 1 | 1 | `_/¦ ` | Rising Edge | High was not long enough to set the latch, but the second half of the ghost can be a new rising edge | +| F | 0 | 1 | 0 | `_/\ ` | - | High was not long enough to set the latch | +| F | 0 | 0 | 1 | `_/\/ ` | Rising Edge | High was not long enough to set the latch, but this may be a rising edge | +| F | 0 | 0 | 0 | `_¦ ` | - | Ghost of 0 length definitely was not long was not long enough to set the latch | + +## Startup + +On startup the filter should not consider whatever state it is in as valid so it does not count a strange pulse. +There are two possible starting configurations, either the pin is high or the pin is low. +If the pin is high, the subsequent falling edge should not count as a pulse as we never saw the rising edge. +Therefore we start in the latched state. +If the pin is low, the subsequent rising edge can be counted as the first pulse. +Therefore we start in the unlatched state. diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index d0c627313c..530425563c 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -1,4 +1,5 @@ #include "pulse_meter_sensor.h" +#include #include "esphome/core/log.h" namespace esphome { @@ -6,172 +7,156 @@ namespace pulse_meter { static const char *const TAG = "pulse_meter"; +void PulseMeterSensor::set_total_pulses(uint32_t pulses) { + this->total_pulses_ = pulses; + if (this->total_sensor_ != nullptr) { + this->total_sensor_->publish_state(this->total_pulses_); + } +} + void PulseMeterSensor::setup() { this->pin_->setup(); this->isr_pin_ = pin_->to_isr(); - this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); - this->last_detected_edge_us_ = 0; - this->last_valid_edge_us_ = 0; - this->pulse_width_us_ = 0; - this->sensor_is_high_ = this->isr_pin_.digital_read(); - this->has_valid_edge_ = false; - this->pending_state_change_ = NONE; + // Set the last processed edge to now for the first timeout + this->last_processed_edge_us_ = micros(); + + if (this->filter_mode_ == FILTER_EDGE) { + this->pin_->attach_interrupt(PulseMeterSensor::edge_intr, this, gpio::INTERRUPT_RISING_EDGE); + } else if (this->filter_mode_ == FILTER_PULSE) { + // Set the pin value to the current value to avoid a false edge + this->pulse_state_.last_pin_val_ = this->isr_pin_.digital_read(); + this->pulse_state_.latched_ = this->pulse_state_.last_pin_val_; + this->pin_->attach_interrupt(PulseMeterSensor::pulse_intr, this, gpio::INTERRUPT_ANY_EDGE); + } } -// In PULSE mode we set a flag (pending_state_change_) for every interrupt -// that constitutes a state change. In the loop() method we check if a time -// interval greater than the internal_filter time has passed without any -// interrupts. void PulseMeterSensor::loop() { - // Get a snapshot of the needed volatile sensor values, to make sure they are not - // modified by the ISR while we are in the loop() method. If they are changed - // after we the variable "now" has been set, overflow will occur in the - // subsequent arithmetic - const bool has_valid_edge = this->has_valid_edge_; - const uint32_t last_detected_edge_us = this->last_detected_edge_us_; - const uint32_t last_valid_edge_us = this->last_valid_edge_us_; - // Get the current time after the snapshot of saved times const uint32_t now = micros(); - this->handle_state_change_(now, last_detected_edge_us, last_valid_edge_us, has_valid_edge); + // Reset the count in get before we pass it back to the ISR as set + this->get_->count_ = 0; - // If we've exceeded our timeout interval without receiving any pulses, assume 0 pulses/min until - // we get at least two valid pulses. - const uint32_t time_since_valid_edge_us = now - last_detected_edge_us; - if ((has_valid_edge) && (time_since_valid_edge_us > this->timeout_us_)) { - ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); + // Swap out set and get to get the latest state from the ISR + // The ISR could interrupt on any of these lines and the results would be consistent + auto *temp = this->set_; + this->set_ = this->get_; + this->get_ = temp; - this->last_valid_edge_us_ = 0; - this->pulse_width_us_ = 0; - this->has_valid_edge_ = false; - this->last_detected_edge_us_ = 0; + // If an edge was peeked, repay the debt + if (this->peeked_edge_ && this->get_->count_ > 0) { + this->peeked_edge_ = false; + this->get_->count_--; } - // We quantize our pulse widths to 1 ms to avoid unnecessary jitter - const uint32_t pulse_width_ms = this->pulse_width_us_ / 1000; - if (this->pulse_width_dedupe_.next(pulse_width_ms)) { - if (pulse_width_ms == 0) { - // Treat 0 pulse width as 0 pulses/min (normally because we've not detected any pulses for a while) - this->publish_state(0); - } else { - // Calculate pulses/min from the pulse width in ms - this->publish_state((60.0f * 1000.0f) / pulse_width_ms); - } + // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early + if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ && + now - this->get_->last_rising_edge_us_ >= this->filter_us_) { + this->peeked_edge_ = true; + this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; + this->get_->count_++; } - if (this->total_sensor_ != nullptr) { - const uint32_t total = this->total_pulses_; - if (this->total_dedupe_.next(total)) { + // Check if we detected a pulse this loop + if (this->get_->count_ > 0) { + // Keep a running total of pulses if a total sensor is configured + if (this->total_sensor_ != nullptr) { + this->total_pulses_ += this->get_->count_; + const uint32_t total = this->total_pulses_; this->total_sensor_->publish_state(total); } + + // We need to detect at least two edges to have a valid pulse width + switch (this->meter_state_) { + case MeterState::INITIAL: + case MeterState::TIMED_OUT: { + this->meter_state_ = MeterState::RUNNING; + } break; + case MeterState::RUNNING: { + uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; + float pulse_width_us = delta_us / float(this->get_->count_); + this->publish_state((60.0f * 1000000.0f) / pulse_width_us); + } break; + } + + this->last_processed_edge_us_ = this->get_->last_detected_edge_us_; + } + // No detected edges this loop + else { + const uint32_t time_since_valid_edge_us = now - this->last_processed_edge_us_; + + switch (this->meter_state_) { + // Running and initial states can timeout + case MeterState::INITIAL: + case MeterState::RUNNING: { + if (time_since_valid_edge_us > this->timeout_us_) { + this->meter_state_ = MeterState::TIMED_OUT; + ESP_LOGD(TAG, "No pulse detected for %" PRIu32 "s, assuming 0 pulses/min", + time_since_valid_edge_us / 1000000); + this->publish_state(0.0f); + } + } break; + default: + break; + } } } -void PulseMeterSensor::set_total_pulses(uint32_t pulses) { this->total_pulses_ = pulses; } +float PulseMeterSensor::get_setup_priority() const { return setup_priority::DATA; } void PulseMeterSensor::dump_config() { LOG_SENSOR("", "Pulse Meter", this); LOG_PIN(" Pin: ", this->pin_); if (this->filter_mode_ == FILTER_EDGE) { - ESP_LOGCONFIG(TAG, " Filtering rising edges less than %u µs apart", this->filter_us_); + ESP_LOGCONFIG(TAG, " Filtering rising edges less than %" PRIu32 " µs apart", this->filter_us_); } else { - ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %u µs", this->filter_us_); + ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %" PRIu32 " µs", this->filter_us_); } - ESP_LOGCONFIG(TAG, " Assuming 0 pulses/min after not receiving a pulse for %us", this->timeout_us_ / 1000000); + ESP_LOGCONFIG(TAG, " Assuming 0 pulses/min after not receiving a pulse for %" PRIu32 "s", + this->timeout_us_ / 1000000); } -void IRAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) { +void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { + // This is an interrupt handler - we can't call any virtual method from this method + // Get the current time before we do anything else so the measurements are consistent + const uint32_t now = micros(); + auto &state = sensor->edge_state_; + auto &set = *sensor->set_; + + if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) { + state.last_sent_edge_us_ = now; + set.last_detected_edge_us_ = now; + set.last_rising_edge_us_ = now; + set.count_++; + } +} + +void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { // This is an interrupt handler - we can't call any virtual method from this method // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); const bool pin_val = sensor->isr_pin_.digital_read(); + auto &state = sensor->pulse_state_; + auto &set = *sensor->set_; - if (sensor->filter_mode_ == FILTER_EDGE) { - // We only look at rising edges - if (!pin_val) { - return; - } - // Check to see if we should filter this edge out - if ((now - sensor->last_detected_edge_us_) >= sensor->filter_us_) { - // Don't measure the first valid pulse (we need at least two pulses to measure the width) - if (sensor->has_valid_edge_) { - sensor->pulse_width_us_ = (now - sensor->last_valid_edge_us_); - } - sensor->total_pulses_++; - sensor->last_valid_edge_us_ = now; - sensor->has_valid_edge_ = true; - } - sensor->last_detected_edge_us_ = now; - } else { - // Filter Mode is PULSE - const uint32_t delta_t_us = now - sensor->last_detected_edge_us_; - // We need to check if we have missed to handle a state change in the - // loop() function. This can happen when the filter_us value is less than - // the loop() interval, which is ~50-60ms - // The section below is essentially a modified repeat of the - // handle_state_change method. Ideally i would refactor and call the - // method here as well. However functions called in ISRs need to meet - // strict criteria and I don't think the methos would meet them. - if (sensor->pending_state_change_ != NONE && (delta_t_us > sensor->filter_us_)) { - // We have missed to handle a state change in the loop function. - sensor->sensor_is_high_ = sensor->pending_state_change_ == TO_HIGH; - if (sensor->sensor_is_high_) { - // We need to handle a pulse that would have been missed by the loop function - sensor->total_pulses_++; - if (sensor->has_valid_edge_) { - sensor->pulse_width_us_ = sensor->last_detected_edge_us_ - sensor->last_valid_edge_us_; - sensor->has_valid_edge_ = true; - sensor->last_valid_edge_us_ = sensor->last_detected_edge_us_; - } - } - } // End of checking for and handling of change in state + // Filter length has passed since the last interrupt + const bool length = now - state.last_intr_ >= sensor->filter_us_; - // Ignore false edges that may be caused by bouncing and exit the ISR ASAP - if (pin_val == sensor->sensor_is_high_) { - sensor->pending_state_change_ = NONE; - return; - } - sensor->pending_state_change_ = pin_val ? TO_HIGH : TO_LOW; - sensor->last_detected_edge_us_ = now; - } -} - -void PulseMeterSensor::handle_state_change_(uint32_t now, uint32_t last_detected_edge_us, uint32_t last_valid_edge_us, - bool has_valid_edge) { - if (this->pending_state_change_ == NONE) { - return; + if (length && state.latched_ && !state.last_pin_val_) { // Long enough low edge + state.latched_ = false; + } else if (length && !state.latched_ && state.last_pin_val_) { // Long enough high edge + state.latched_ = true; + set.last_detected_edge_us_ = state.last_intr_; + set.count_++; } - const bool pin_val = this->isr_pin_.digital_read(); - if (pin_val == this->sensor_is_high_) { - // Most likely caused by high frequency bouncing. Theoretically we should - // expect interrupts of alternating state. Here we are registering an - // interrupt with no change in state. Another interrupt will likely trigger - // just after this one and have an alternate state. - this->pending_state_change_ = NONE; - return; - } + // Due to order of operations this includes + // length && latched && rising (just reset from a long low edge) + // !latched && (rising || high) (noise on the line resetting the potential rising edge) + set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_; - if ((now - last_detected_edge_us) > this->filter_us_) { - this->sensor_is_high_ = pin_val; - ESP_LOGVV(TAG, "State is now %s", pin_val ? "high" : "low"); - - // Increment with valid rising edges only - if (pin_val) { - this->total_pulses_++; - ESP_LOGVV(TAG, "Incremented pulses to %u", this->total_pulses_); - - if (has_valid_edge) { - this->pulse_width_us_ = last_detected_edge_us - last_valid_edge_us; - ESP_LOGVV(TAG, "Set pulse width to %u", this->pulse_width_us_); - } - this->has_valid_edge_ = true; - this->last_valid_edge_us_ = last_detected_edge_us; - ESP_LOGVV(TAG, "last_valid_edge_us_ is now %u", this->last_valid_edge_us_); - } - this->pending_state_change_ = NONE; - } + state.last_intr_ = now; + state.last_pin_val_ = pin_val; } } // namespace pulse_meter diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 47af6e2398..76c4a35f03 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -5,6 +5,8 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace pulse_meter { @@ -17,41 +19,64 @@ class PulseMeterSensor : public sensor::Sensor, public Component { void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_filter_us(uint32_t filter) { this->filter_us_ = filter; } - void set_filter_mode(InternalFilterMode mode) { this->filter_mode_ = mode; } void set_timeout_us(uint32_t timeout) { this->timeout_us_ = timeout; } void set_total_sensor(sensor::Sensor *sensor) { this->total_sensor_ = sensor; } + void set_filter_mode(InternalFilterMode mode) { this->filter_mode_ = mode; } void set_total_pulses(uint32_t pulses); void setup() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } + float get_setup_priority() const override; void dump_config() override; protected: - enum StateChange { TO_LOW = 0, TO_HIGH, NONE }; - - static void gpio_intr(PulseMeterSensor *sensor); - void handle_state_change_(uint32_t now, uint32_t last_detected_edge_us, uint32_t last_valid_edge_us, - bool has_valid_edge); + static void edge_intr(PulseMeterSensor *sensor); + static void pulse_intr(PulseMeterSensor *sensor); InternalGPIOPin *pin_{nullptr}; - ISRInternalGPIOPin isr_pin_; uint32_t filter_us_ = 0; uint32_t timeout_us_ = 1000000UL * 60UL * 5UL; sensor::Sensor *total_sensor_{nullptr}; InternalFilterMode filter_mode_{FILTER_EDGE}; - Deduplicator pulse_width_dedupe_; - Deduplicator total_dedupe_; + // Variables used in the loop + enum class MeterState { INITIAL, RUNNING, TIMED_OUT }; + MeterState meter_state_ = MeterState::INITIAL; + bool peeked_edge_ = false; + uint32_t total_pulses_ = 0; + uint32_t last_processed_edge_us_ = 0; - volatile uint32_t last_detected_edge_us_ = 0; - volatile uint32_t last_valid_edge_us_ = 0; - volatile uint32_t pulse_width_us_ = 0; - volatile uint32_t total_pulses_ = 0; - volatile bool sensor_is_high_ = false; - volatile bool has_valid_edge_ = false; - volatile StateChange pending_state_change_{NONE}; + // This struct (and the two pointers) are used to pass data between the ISR and loop. + // These two pointers are exchanged each loop. + // Therefore you can't use data in the pointer to loop receives to set values in the pointer to loop sends. + // As a result it's easiest if you only use these pointers to send data from the ISR to the loop. + // (except for resetting the values) + struct State { + uint32_t last_detected_edge_us_ = 0; + uint32_t last_rising_edge_us_ = 0; + uint32_t count_ = 0; + }; + State state_[2]; + volatile State *set_ = state_; + volatile State *get_ = state_ + 1; + + // Only use these variables in the ISR + ISRInternalGPIOPin isr_pin_; + + /// Filter state for edge mode + struct EdgeState { + uint32_t last_sent_edge_us_ = 0; + }; + EdgeState edge_state_{}; + + /// Filter state for pulse mode + struct PulseState { + uint32_t last_intr_ = 0; + bool latched_ = false; + bool last_pin_val_ = false; + }; + PulseState pulse_state_{}; }; } // namespace pulse_meter diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 26bc6b189b..59ffa58c21 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -19,7 +19,7 @@ from esphome.const import ( ) from esphome.core import CORE -CODEOWNERS = ["@stevebaxter", "@cstaahl"] +CODEOWNERS = ["@stevebaxter", "@cstaahl", "@TrentHouliston"] pulse_meter_ns = cg.esphome_ns.namespace("pulse_meter") diff --git a/esphome/components/pvvx_mithermometer/display/__init__.py b/esphome/components/pvvx_mithermometer/display/__init__.py index d935638933..70c568c1e3 100644 --- a/esphome/components/pvvx_mithermometer/display/__init__.py +++ b/esphome/components/pvvx_mithermometer/display/__init__.py @@ -38,7 +38,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await ble_client.register_ble_node(var, config) cg.add(var.set_disconnect_delay(config[CONF_DISCONNECT_DELAY].total_milliseconds)) diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index 384537e5d7..1856a023cc 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -13,8 +13,10 @@ void PVVXDisplay::dump_config() { ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); ESP_LOGCONFIG(TAG, " Characteristic UUID : %s", this->char_uuid_.to_string().c_str()); ESP_LOGCONFIG(TAG, " Auto clear : %s", YESNO(this->auto_clear_enabled_)); +#ifdef USE_TIME ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr)); - ESP_LOGCONFIG(TAG, " Disconnect delay : %dms", this->disconnect_delay_ms_); +#endif + ESP_LOGCONFIG(TAG, " Disconnect delay : %" PRIu32 "ms", this->disconnect_delay_ms_); LOG_UPDATE_INTERVAL(this); } @@ -22,8 +24,10 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_ble_gattc_cb_param_t *param) { switch (event) { case ESP_GATTC_OPEN_EVT: - ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); - this->delayed_disconnect_(); + if (param->open.status == ESP_GATT_OK) { + ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); + this->delayed_disconnect_(); + } break; case ESP_GATTC_DISCONNECT_EVT: ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str().c_str()); @@ -139,7 +143,11 @@ void PVVXDisplay::sync_time_() { } time.recalc_timestamp_utc(true); // calculate timestamp of local time uint8_t blk[5] = {}; +#if ESP_IDF_VERSION_MAJOR >= 5 + ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str().c_str(), time.timestamp); +#else ESP_LOGD(TAG, "[%s] Sync time with timestamp %lu.", this->parent_->address_str().c_str(), time.timestamp); +#endif 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 c7e7cc04fb..dfeb49c49d 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -4,6 +4,8 @@ #include "esphome/core/defines.h" #include "esphome/components/ble_client/ble_client.h" +#include + #ifdef USE_ESP32 #include #ifdef USE_TIME diff --git a/esphome/components/pylontech/__init__.py b/esphome/components/pylontech/__init__.py new file mode 100644 index 0000000000..197f7e7904 --- /dev/null +++ b/esphome/components/pylontech/__init__.py @@ -0,0 +1,46 @@ +import logging +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@functionpointer"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +CONF_PYLONTECH_ID = "pylontech_id" +CONF_BATTERY = "battery" + +pylontech_ns = cg.esphome_ns.namespace("pylontech") +PylontechComponent = pylontech_ns.class_( + "PylontechComponent", cg.PollingComponent, uart.UARTDevice +) +PylontechBattery = pylontech_ns.class_("PylontechBattery") + +CV_NUM_BATTERIES = cv.int_range(1, 16) + +PYLONTECH_COMPONENT_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_PYLONTECH_ID): cv.use_id(PylontechComponent), + cv.Required(CONF_BATTERY): CV_NUM_BATTERIES, + } +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PylontechComponent), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +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) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp new file mode 100644 index 0000000000..b33f4d4874 --- /dev/null +++ b/esphome/components/pylontech/pylontech.cpp @@ -0,0 +1,104 @@ +#include "pylontech.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech"; +static const int MAX_DATA_LENGTH_BYTES = 256; +static const uint8_t ASCII_LF = 0x0A; + +PylontechComponent::PylontechComponent() {} + +void PylontechComponent::dump_config() { + this->check_uart_settings(115200, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); + ESP_LOGCONFIG(TAG, "pylontech:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Connection with pylontech failed!"); + } + + for (PylontechListener *listener : this->listeners_) { + listener->dump_config(); + } + + LOG_UPDATE_INTERVAL(this); +} + +void PylontechComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up pylontech..."); + while (this->available() != 0) { + this->read(); + } +} + +void PylontechComponent::update() { this->write_str("pwr\n"); } + +void PylontechComponent::loop() { + if (this->available() > 0) { + // pylontech sends a lot of data very suddenly + // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow + uint8_t data; + int recv = 0; + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_[buffer_index_write_] += (char) data; + recv++; + if (buffer_[buffer_index_write_].back() == static_cast(ASCII_LF) || + buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { + // complete line received + buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; + } + } + } + ESP_LOGV(TAG, "received %d bytes", recv); + } else { + // only process one line per call of loop() to not block esphome for too long + if (buffer_index_read_ != buffer_index_write_) { + this->process_line_(buffer_[buffer_index_read_]); + buffer_[buffer_index_read_].clear(); + buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS; + } + } +} + +void PylontechComponent::process_line_(std::string &buffer) { + ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str()); + // clang-format off + // example line to parse: + // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St + // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // clang-format on + + PylontechListener::LineContents l{}; + char mostempr_s[6]; + const int parsed = sscanf( // NOLINT + buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %5s %*s", // NOLINT + &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT + l.curr_st, l.temp_st, &l.coulomb, mostempr_s); // NOLINT + + if (l.bat_num <= 0) { + ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + if (parsed != 14) { + ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + auto mostempr_parsed = parse_number(mostempr_s); + if (mostempr_parsed.has_value()) { + l.mostempr = mostempr_parsed.value(); + } else { + l.mostempr = -300; + ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num); + } + + for (PylontechListener *listener : this->listeners_) { + listener->on_line_read(&l); + } +} + +float PylontechComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h new file mode 100644 index 0000000000..3282cb4d9f --- /dev/null +++ b/esphome/components/pylontech/pylontech.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace pylontech { + +static const uint8_t NUM_BUFFERS = 20; +static const uint8_t TEXT_SENSOR_MAX_LEN = 8; + +class PylontechListener { + public: + struct LineContents { + int bat_num = 0, volt, curr, tempr, tlow, thigh, vlow, vhigh, coulomb, mostempr; + char base_st[TEXT_SENSOR_MAX_LEN], volt_st[TEXT_SENSOR_MAX_LEN], curr_st[TEXT_SENSOR_MAX_LEN], + temp_st[TEXT_SENSOR_MAX_LEN]; + }; + + virtual void on_line_read(LineContents *line); + virtual void dump_config(); +}; + +class PylontechComponent : public PollingComponent, public uart::UARTDevice { + public: + PylontechComponent(); + + /// Schedule data readings. + void update() override; + /// Read data once available + void loop() override; + /// Setup the sensor and test for a connection. + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + void register_listener(PylontechListener *listener) { this->listeners_.push_back(listener); } + + protected: + void process_line_(std::string &buffer); + + // ring buffer + std::string buffer_[NUM_BUFFERS]; + int buffer_index_write_ = 0; + int buffer_index_read_ = 0; + + std::vector listeners_{}; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py new file mode 100644 index 0000000000..a1477c627f --- /dev/null +++ b/esphome/components/pylontech/sensor/__init__.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_VOLTAGE, + CONF_CURRENT, + CONF_TEMPERATURE, + UNIT_VOLT, + UNIT_AMPERE, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_BATTERY, + UNIT_CELSIUS, + UNIT_PERCENT, + CONF_ID, +) + +from .. import ( + CONF_PYLONTECH_ID, + PYLONTECH_COMPONENT_SCHEMA, + CONF_BATTERY, + pylontech_ns, +) + +PylontechSensor = pylontech_ns.class_("PylontechSensor", cg.Component) + +CONF_COULOMB = "coulomb" +CONF_TEMPERATURE_LOW = "temperature_low" +CONF_TEMPERATURE_HIGH = "temperature_high" +CONF_VOLTAGE_LOW = "voltage_low" +CONF_VOLTAGE_HIGH = "voltage_high" +CONF_MOS_TEMPERATURE = "mos_temperature" + +TYPES: dict[str, cv.Schema] = { + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_TEMPERATURE_LOW: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_TEMPERATURE_HIGH: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_VOLTAGE_LOW: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_VOLTAGE_HIGH: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_COULOMB: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + ), + CONF_MOS_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), +} + +CONFIG_SCHEMA = PYLONTECH_COMPONENT_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PylontechSensor)} +).extend({cv.Optional(marker): schema for marker, schema in TYPES.items()}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PYLONTECH_ID]) + bat = cg.new_Pvariable(config[CONF_ID], config[CONF_BATTERY]) + + for marker in TYPES: + if marker_config := config.get(marker): + sens = await sensor.new_sensor(marker_config) + cg.add(getattr(bat, f"set_{marker}_sensor")(sens)) + + cg.add(paren.register_listener(bat)) diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.cpp b/esphome/components/pylontech/sensor/pylontech_sensor.cpp new file mode 100644 index 0000000000..5b5db0731e --- /dev/null +++ b/esphome/components/pylontech/sensor/pylontech_sensor.cpp @@ -0,0 +1,60 @@ +#include "pylontech_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech.sensor"; + +PylontechSensor::PylontechSensor(int8_t bat_num) { this->bat_num_ = bat_num; } + +void PylontechSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Pylontech Sensor:"); + ESP_LOGCONFIG(TAG, " Battery %d", this->bat_num_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Temperature low", this->temperature_low_sensor_); + LOG_SENSOR(" ", "Temperature high", this->temperature_high_sensor_); + LOG_SENSOR(" ", "Voltage low", this->voltage_low_sensor_); + LOG_SENSOR(" ", "Voltage high", this->voltage_high_sensor_); + LOG_SENSOR(" ", "Coulomb", this->coulomb_sensor_); + LOG_SENSOR(" ", "MOS Temperature", this->mos_temperature_sensor_); +} + +void PylontechSensor::on_line_read(PylontechListener::LineContents *line) { + if (this->bat_num_ != line->bat_num) { + return; + } + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(((float) line->volt) / 1000.0f); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(((float) line->curr) / 1000.0f); + } + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(((float) line->tempr) / 1000.0f); + } + if (this->temperature_low_sensor_ != nullptr) { + this->temperature_low_sensor_->publish_state(((float) line->tlow) / 1000.0f); + } + if (this->temperature_high_sensor_ != nullptr) { + this->temperature_high_sensor_->publish_state(((float) line->thigh) / 1000.0f); + } + if (this->voltage_low_sensor_ != nullptr) { + this->voltage_low_sensor_->publish_state(((float) line->vlow) / 1000.0f); + } + if (this->voltage_high_sensor_ != nullptr) { + this->voltage_high_sensor_->publish_state(((float) line->vhigh) / 1000.0f); + } + if (this->coulomb_sensor_ != nullptr) { + this->coulomb_sensor_->publish_state(line->coulomb); + } + if (this->mos_temperature_sensor_ != nullptr) { + this->mos_temperature_sensor_->publish_state(((float) line->mostempr) / 1000.0f); + } +} + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.h b/esphome/components/pylontech/sensor/pylontech_sensor.h new file mode 100644 index 0000000000..8986adc26c --- /dev/null +++ b/esphome/components/pylontech/sensor/pylontech_sensor.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../pylontech.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace pylontech { + +class PylontechSensor : public PylontechListener, public Component { + public: + PylontechSensor(int8_t bat_num); + void dump_config() override; + + SUB_SENSOR(voltage) + SUB_SENSOR(current) + SUB_SENSOR(temperature) + SUB_SENSOR(temperature_low) + SUB_SENSOR(temperature_high) + SUB_SENSOR(voltage_low) + SUB_SENSOR(voltage_high) + + SUB_SENSOR(coulomb) + SUB_SENSOR(mos_temperature) + + void on_line_read(LineContents *line) override; + + protected: + int8_t bat_num_; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/text_sensor/__init__.py b/esphome/components/pylontech/text_sensor/__init__.py new file mode 100644 index 0000000000..d6ccc678f8 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID + +from .. import ( + CONF_PYLONTECH_ID, + PYLONTECH_COMPONENT_SCHEMA, + CONF_BATTERY, + pylontech_ns, +) + +PylontechTextSensor = pylontech_ns.class_("PylontechTextSensor", cg.Component) + +CONF_BASE_STATE = "base_state" +CONF_VOLTAGE_STATE = "voltage_state" +CONF_CURRENT_STATE = "current_state" +CONF_TEMPERATURE_STATE = "temperature_state" + +MARKERS: list[str] = [ + CONF_BASE_STATE, + CONF_VOLTAGE_STATE, + CONF_CURRENT_STATE, + CONF_TEMPERATURE_STATE, +] + +CONFIG_SCHEMA = PYLONTECH_COMPONENT_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PylontechTextSensor)} +).extend({cv.Optional(marker): text_sensor.text_sensor_schema() for marker in MARKERS}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PYLONTECH_ID]) + bat = cg.new_Pvariable(config[CONF_ID], config[CONF_BATTERY]) + + for marker in MARKERS: + if marker_config := config.get(marker): + var = await text_sensor.new_text_sensor(marker_config) + cg.add(getattr(bat, f"set_{marker}_text_sensor")(var)) + + cg.add(paren.register_listener(bat)) diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp new file mode 100644 index 0000000000..9e894bc570 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp @@ -0,0 +1,40 @@ +#include "pylontech_text_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech.textsensor"; + +PylontechTextSensor::PylontechTextSensor(int8_t bat_num) { this->bat_num_ = bat_num; } + +void PylontechTextSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Pylontech Text Sensor:"); + ESP_LOGCONFIG(TAG, " Battery %d", this->bat_num_); + LOG_TEXT_SENSOR(" ", "Base state", this->base_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Voltage state", this->voltage_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Current state", this->current_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Temperature state", this->temperature_state_text_sensor_); +} + +void PylontechTextSensor::on_line_read(PylontechListener::LineContents *line) { + if (this->bat_num_ != line->bat_num) { + return; + } + if (this->base_state_text_sensor_ != nullptr) { + this->base_state_text_sensor_->publish_state(std::string(line->base_st)); + } + if (this->voltage_state_text_sensor_ != nullptr) { + this->voltage_state_text_sensor_->publish_state(std::string(line->volt_st)); + } + if (this->current_state_text_sensor_ != nullptr) { + this->current_state_text_sensor_->publish_state(std::string(line->curr_st)); + } + if (this->temperature_state_text_sensor_ != nullptr) { + this->temperature_state_text_sensor_->publish_state(std::string(line->temp_st)); + } +} + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h new file mode 100644 index 0000000000..a685512ed5 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../pylontech.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace pylontech { + +class PylontechTextSensor : public PylontechListener, public Component { + public: + PylontechTextSensor(int8_t bat_num); + void dump_config() override; + + SUB_TEXT_SENSOR(base_state) + SUB_TEXT_SENSOR(voltage_state) + SUB_TEXT_SENSOR(current_state) + SUB_TEXT_SENSOR(temperature_state) + + void on_line_read(LineContents *line) override; + + protected: + int8_t bat_num_; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index e5418765bd..35b66b03f2 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -1,5 +1,6 @@ #include "pzem004t.h" #include "esphome/core/log.h" +#include namespace esphome { namespace pzem004t { @@ -75,7 +76,7 @@ void PZEM004T::loop() { uint32_t energy = (uint32_t(resp[1]) << 16) | (uint32_t(resp[2]) << 8) | (uint32_t(resp[3])); if (this->energy_sensor_ != nullptr) this->energy_sensor_->publish_state(energy); - ESP_LOGD(TAG, "Got Energy %u Wh", energy); + ESP_LOGD(TAG, "Got Energy %" PRIu32 " Wh", energy); this->write_state_(DONE); break; } diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index f03b6af191..49a67d4e09 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -1,4 +1,5 @@ #include "qmc5883l.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" #include @@ -59,6 +60,10 @@ void QMC5883LComponent::setup() { this->mark_failed(); return; } + + if (this->get_update_interval() < App.get_loop_interval()) { + high_freq_.start(); + } } void QMC5883LComponent::dump_config() { ESP_LOGCONFIG(TAG, "QMC5883L:"); @@ -72,12 +77,15 @@ void QMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Y Axis", this->y_sensor_); LOG_SENSOR(" ", "Z Axis", this->z_sensor_); LOG_SENSOR(" ", "Heading", this->heading_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); } float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } void QMC5883LComponent::update() { uint8_t status = false; this->read_byte(QMC5883L_REGISTER_STATUS, &status); + // Always request X,Y,Z regardless if there are sensors for them + // to avoid https://github.com/esphome/issues/issues/5731 uint16_t raw_x, raw_y, raw_z; if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x) || !this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y) || @@ -104,7 +112,19 @@ void QMC5883LComponent::update() { const float z = int16_t(raw_z) * mg_per_bit * 0.1f; float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; - ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° status=%u", x, y, z, heading, status); + + float temp = NAN; + if (this->temperature_sensor_ != nullptr) { + uint16_t raw_temp; + if (!this->read_byte_16_(QMC5883L_REGISTER_TEMPERATURE_LSB, &raw_temp)) { + this->status_set_warning(); + return; + } + temp = int16_t(raw_temp) * 0.01f; + } + + ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading, + temp, status); if (this->x_sensor_ != nullptr) this->x_sensor_->publish_state(x); @@ -114,6 +134,8 @@ void QMC5883LComponent::update() { this->z_sensor_->publish_state(z); if (this->heading_sensor_ != nullptr) this->heading_sensor_->publish_state(heading); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temp); } bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) { diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 15ef435ce5..dd2008d453 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -40,6 +40,7 @@ class QMC5883LComponent : 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_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } protected: QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ}; @@ -49,11 +50,13 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *y_sensor_{nullptr}; sensor::Sensor *z_sensor_{nullptr}; sensor::Sensor *heading_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, } error_code_; bool read_byte_16_(uint8_t a_register, uint16_t *data); + HighFrequencyLoopRequester high_freq_; }; } // namespace qmc5883l diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index cce4d93843..341c0c3f8a 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -3,12 +3,19 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ADDRESS, + CONF_FIELD_STRENGTH_X, + CONF_FIELD_STRENGTH_Y, + CONF_FIELD_STRENGTH_Z, + CONF_HEADING, + CONF_TEMPERATURE, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, + DEVICE_CLASS_TEMPERATURE, ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA, + UNIT_CELSIUS, UNIT_DEGREES, ICON_SCREEN_ROTATION, CONF_UPDATE_INTERVAL, @@ -18,11 +25,6 @@ DEPENDENCIES = ["i2c"] qmc5883l_ns = cg.esphome_ns.namespace("qmc5883l") -CONF_FIELD_STRENGTH_X = "field_strength_x" -CONF_FIELD_STRENGTH_Y = "field_strength_y" -CONF_FIELD_STRENGTH_Z = "field_strength_z" -CONF_HEADING = "heading" - QMC5883LComponent = qmc5883l_ns.class_( "QMC5883LComponent", cg.PollingComponent, i2c.I2CDevice ) @@ -79,6 +81,12 @@ heading_schema = sensor.sensor_schema( icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, ) +temperature_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, +) CONFIG_SCHEMA = ( cv.Schema( @@ -95,6 +103,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_TEMPERATURE): temperature_schema, } ) .extend(cv.polling_component_schema("60s")) @@ -131,3 +140,6 @@ 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_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/qr_code/qr_code.cpp b/esphome/components/qr_code/qr_code.cpp index a2efbdb804..b60e60a4b0 100644 --- a/esphome/components/qr_code/qr_code.cpp +++ b/esphome/components/qr_code/qr_code.cpp @@ -1,5 +1,5 @@ #include "qr_code.h" -#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display.h" #include "esphome/core/color.h" #include "esphome/core/log.h" @@ -33,7 +33,7 @@ void QrCode::generate_qr_code() { } } -void QrCode::draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale) { +void QrCode::draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale) { ESP_LOGV(TAG, "Drawing QR code at (%d, %d)", x_offset, y_offset); if (this->needs_update_) { @@ -51,5 +51,17 @@ void QrCode::draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_of } } } + +uint8_t QrCode::get_size() { + if (this->needs_update_) { + this->generate_qr_code(); + this->needs_update_ = false; + } + + uint8_t size = qrcodegen_getSize(this->qr_); + + return size; +} + } // namespace qr_code } // namespace esphome diff --git a/esphome/components/qr_code/qr_code.h b/esphome/components/qr_code/qr_code.h index 58f3a70321..ab4c587b6d 100644 --- a/esphome/components/qr_code/qr_code.h +++ b/esphome/components/qr_code/qr_code.h @@ -9,13 +9,13 @@ namespace esphome { // forward declare DisplayBuffer namespace display { -class DisplayBuffer; +class Display; } // namespace display namespace qr_code { class QrCode : public Component { public: - void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale); + void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale); void dump_config() override; @@ -24,6 +24,8 @@ class QrCode : public Component { void generate_qr_code(); + uint8_t get_size(); + protected: std::string value_; qrcodegen_Ecc ecc_; diff --git a/esphome/components/qspi_amoled/display.py b/esphome/components/qspi_amoled/display.py new file mode 100644 index 0000000000..00773b516a --- /dev/null +++ b/esphome/components/qspi_amoled/display.py @@ -0,0 +1,3 @@ +import esphome.config_validation as cv + +CONFIG_SCHEMA = cv.invalid("The qspi_amoled component has been renamed to qspi_dbi") diff --git a/esphome/components/qspi_dbi/__init__.py b/esphome/components/qspi_dbi/__init__.py new file mode 100644 index 0000000000..c58ce8a01e --- /dev/null +++ b/esphome/components/qspi_dbi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py new file mode 100644 index 0000000000..71ae31f182 --- /dev/null +++ b/esphome/components/qspi_dbi/display.py @@ -0,0 +1,185 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, spi +import esphome.config_validation as cv +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_ORDER, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_HEIGHT, + CONF_ID, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_RESET_PIN, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_WIDTH, +) +from esphome.core import TimePeriod + +from .models import DriverChip + +DEPENDENCIES = ["spi"] + +qspi_dbi_ns = cg.esphome_ns.namespace("qspi_dbi") +QSPI_DBI = qspi_dbi_ns.class_( + "QspiDbi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice +) +ColorOrder = display.display_ns.enum("ColorMode") +Model = qspi_dbi_ns.enum("Model") + +COLOR_ORDERS = { + "RGB": ColorOrder.COLOR_ORDER_RGB, + "BGR": ColorOrder.COLOR_ORDER_BGR, +} +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + +CONF_DRAW_FROM_ORIGIN = "draw_from_origin" +DELAY_FLAG = 0xFF + + +def validate_dimension(value): + value = cv.positive_int(value) + if value % 2 != 0: + raise cv.Invalid("Width/height/offset must be divisible by 2") + return value + + +def map_sequence(value): + """ + The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred + from the length of the sequence and should not be explicit. + A delay can be inserted by specifying "- delay N" where N is in ms + """ + if isinstance(value, str) and value.lower().startswith("delay "): + value = value.lower()[6:] + delay = cv.All( + cv.positive_time_period_milliseconds, + cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), + )(value) + return [delay, DELAY_FLAG] + value = cv.Length(min=1, max=254)(value) + params = value[1:] + return [value[0], len(params)] + list(params) + + +def _validate(config): + chip = DriverChip.chips[config[CONF_MODEL]] + if not chip.initsequence: + if CONF_INIT_SEQUENCE not in config: + raise cv.Invalid(f"{chip.name} model requires init_sequence") + return config + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(QSPI_DBI), + cv.Required(CONF_MODEL): cv.one_of( + *DriverChip.chips.keys(), upper=True + ), + cv.Optional(CONF_INIT_SEQUENCE): cv.ensure_list(map_sequence), + cv.Required(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): validate_dimension, + cv.Required(CONF_HEIGHT): validate_dimension, + cv.Optional( + CONF_OFFSET_HEIGHT, default=0 + ): validate_dimension, + cv.Optional( + CONF_OFFSET_WIDTH, default=0 + ): validate_dimension, + } + ), + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + } + ), + cv.Optional(CONF_COLOR_ORDER, default="RGB"): cv.enum( + COLOR_ORDERS, upper=True + ), + cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=0xD0): cv.int_range( + 0, 0xFF, min_included=True, max_included=True + ), + cv.Optional(CONF_DRAW_FROM_ORIGIN, default=False): cv.boolean, + } + ).extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=10e6, + quad=True, + ) + ) + ), + cv.only_with_esp_idf, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await display.register_display(var, config) + await spi.register_spi_device(var, config) + + chip = DriverChip.chips[config[CONF_MODEL]] + if chip.initsequence: + cg.add(var.add_init_sequence(chip.initsequence)) + if init_sequences := config.get(CONF_INIT_SEQUENCE): + sequence = [] + for seq in init_sequences: + sequence.extend(seq) + cg.add(var.add_init_sequence(sequence)) + + cg.add(var.set_color_mode(config[CONF_COLOR_ORDER])) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) + cg.add(var.set_model(config[CONF_MODEL])) + cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN])) + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = await cg.gpio_pin_expression(enable_pin) + cg.add(var.set_enable_pin(enable)) + + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + + if transform := config.get(CONF_TRANSFORM): + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + + if CONF_DIMENSIONS in config: + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT])) + cg.add( + var.set_offsets( + dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT] + ) + ) + else: + (width, height) = dimensions + cg.add(var.set_dimensions(width, height)) + + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/qspi_dbi/models.py b/esphome/components/qspi_dbi/models.py new file mode 100644 index 0000000000..071ea72d73 --- /dev/null +++ b/esphome/components/qspi_dbi/models.py @@ -0,0 +1,64 @@ +# Commands +SW_RESET_CMD = 0x01 +SLEEP_OUT = 0x11 +INVERT_OFF = 0x20 +INVERT_ON = 0x21 +ALL_ON = 0x23 +WRAM = 0x24 +MIPI = 0x26 +DISPLAY_OFF = 0x28 +DISPLAY_ON = 0x29 +RASET = 0x2B +CASET = 0x2A +WDATA = 0x2C +TEON = 0x35 +MADCTL_CMD = 0x36 +PIXFMT = 0x3A +BRIGHTNESS = 0x51 +SWIRE1 = 0x5A +SWIRE2 = 0x5B +PAGESEL = 0xFE + + +class DriverChip: + chips = {} + + def __init__(self, name: str): + name = name.upper() + self.name = name + self.chips[name] = self + self.initsequence = [] + + def cmd(self, c, *args): + """ + Add a command sequence to the init sequence + :param c: The command (8 bit) + :param args: zero or more arguments (8 bit values) + """ + self.initsequence.extend([c, len(args)] + list(args)) + + def delay(self, ms): + self.initsequence.extend([ms, 0xFF]) + + +chip = DriverChip("RM67162") +chip.cmd(PIXFMT, 0x55) +chip.cmd(BRIGHTNESS, 0) + +chip = DriverChip("RM690B0") +chip.cmd(PAGESEL, 0x20) +chip.cmd(MIPI, 0x0A) +chip.cmd(WRAM, 0x80) +chip.cmd(SWIRE1, 0x51) +chip.cmd(SWIRE2, 0x2E) +chip.cmd(PAGESEL, 0x00) +chip.cmd(0xC2, 0x00) +chip.delay(10) +chip.cmd(TEON, 0x00) + +chip = DriverChip("AXS15231") +chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5) +chip.cmd(0xC1, 0x33) +chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + +DriverChip("Custom") diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp new file mode 100644 index 0000000000..a649a25ea6 --- /dev/null +++ b/esphome/components/qspi_dbi/qspi_dbi.cpp @@ -0,0 +1,230 @@ +#ifdef USE_ESP_IDF +#include "qspi_dbi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace qspi_dbi { + +void QspiDbi::setup() { + ESP_LOGCONFIG(TAG, "Setting up QSPI_DBI"); + this->spi_setup(); + if (this->enable_pin_ != nullptr) { + this->enable_pin_->setup(); + this->enable_pin_->digital_write(true); + } + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + } + this->set_timeout(120, [this] { this->write_command_(SLEEP_OUT); }); + this->set_timeout(240, [this] { this->write_init_sequence_(); }); + if (this->draw_from_origin_) + check_buffer_(); +} + +void QspiDbi::update() { + if (!this->setup_complete_) { + return; + } + this->do_update_(); + if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + // Start addresses and widths/heights must be divisible by 2 (CASET/RASET restriction in datasheet) + if (this->x_low_ % 2 == 1) { + this->x_low_--; + } + if (this->x_high_ % 2 == 0) { + this->x_high_++; + } + if (this->y_low_ % 2 == 1) { + this->y_low_--; + } + if (this->y_high_ % 2 == 0) { + this->y_high_++; + } + if (this->draw_from_origin_) { + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->width_ - 1; + } + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_, + this->width_ - w - this->x_low_); + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +void QspiDbi::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + if (this->is_failed()) + return; + check_buffer_(); + uint32_t pos = (y * this->width_) + x; + bool updated = false; + pos = pos * 2; + uint16_t new_color = display::ColorUtil::color_to_565(color, display::ColorOrder::COLOR_ORDER_RGB); + if (this->buffer_[pos] != static_cast(new_color >> 8)) { + this->buffer_[pos] = static_cast(new_color >> 8); + updated = true; + } + pos = pos + 1; + new_color = new_color & 0xFF; + + if (this->buffer_[pos] != new_color) { + this->buffer_[pos] = new_color; + updated = true; + } + if (updated) { + // low and high watermark may speed up drawing from buffer + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; + } +} + +void QspiDbi::reset_params_(bool ready) { + if (!ready && !this->is_ready()) + return; + this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); + // custom x/y transform and color order + uint8_t mad = this->color_mode_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB; + if (this->swap_xy_) + mad |= MADCTL_MV; + if (this->mirror_x_) + mad |= MADCTL_MX; + if (this->mirror_y_) + mad |= MADCTL_MY; + this->write_command_(MADCTL_CMD, mad); + this->write_command_(BRIGHTNESS, this->brightness_); + this->write_command_(NORON); + this->write_command_(DISPLAY_ON); +} + +void QspiDbi::write_init_sequence_() { + for (const auto &seq : this->init_sequences_) { + this->write_sequence_(seq); + } + this->reset_params_(true); + this->setup_complete_ = true; + ESP_LOGCONFIG(TAG, "QSPI_DBI setup complete"); +} + +void QspiDbi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { + ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); + uint8_t buf[4]; + x1 += this->offset_x_; + x2 += this->offset_x_; + y1 += this->offset_y_; + y2 += this->offset_y_; + put16_be(buf, y1); + put16_be(buf + 2, y2); + this->write_command_(RASET, buf, sizeof buf); + put16_be(buf, x1); + put16_be(buf + 2, x2); + this->write_command_(CASET, buf, sizeof buf); +} + +void QspiDbi::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) { + if (!this->setup_complete_ || this->is_failed()) + return; + if (w <= 0 || h <= 0) + return; + if (bitness != display::COLOR_BITNESS_565 || order != this->color_mode_ || + big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) { + return Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + } else if (this->draw_from_origin_) { + auto stride = x_offset + w + x_pad; + for (int y = 0; y != h; y++) { + memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2, + ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2); + } + ptr = this->buffer_; + w = this->width_; + h += y_start; + x_start = 0; + y_start = 0; + x_offset = 0; + y_offset = 0; + } + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); +} + +void QspiDbi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad) { + this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); + this->enable(); + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother + this->write_cmd_addr_data(8, 0x32, 24, 0x2C00, ptr, w * h * 2, 4); + } else { + auto stride = x_offset + w + x_pad; + uint16_t cmd = 0x2C00; + for (int y = 0; y != h; y++) { + this->write_cmd_addr_data(8, 0x32, 24, cmd, ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2, 4); + cmd = 0x3C00; + } + } + 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()); + this->enable(); + this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); + this->disable(); +} + +void QspiDbi::write_sequence_(const std::vector &vec) { + size_t index = 0; + while (index != vec.size()) { + if (vec.size() - index < 2) { + ESP_LOGE(TAG, "Malformed init sequence"); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence"); + return; + } + const auto *ptr = vec.data() + index; + this->write_command_(cmd, ptr, num_args); + index += num_args; + } + } +} + +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_); + 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 +} // namespace esphome +#endif diff --git a/esphome/components/qspi_dbi/qspi_dbi.h b/esphome/components/qspi_dbi/qspi_dbi.h new file mode 100644 index 0000000000..ebb65a8a05 --- /dev/null +++ b/esphome/components/qspi_dbi/qspi_dbi.h @@ -0,0 +1,173 @@ +// +// Created by Clyde Stubbs on 29/10/2023. +// +#pragma once + +#ifdef USE_ESP_IDF +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display_color_utils.h" +#include "esp_lcd_panel_ops.h" + +#include "esp_lcd_panel_rgb.h" + +namespace esphome { +namespace qspi_dbi { + +constexpr static const char *const TAG = "display.qspi_dbi"; +static const uint8_t SW_RESET_CMD = 0x01; +static const uint8_t SLEEP_OUT = 0x11; +static const uint8_t NORON = 0x13; +static const uint8_t INVERT_OFF = 0x20; +static const uint8_t INVERT_ON = 0x21; +static const uint8_t ALL_ON = 0x23; +static const uint8_t WRAM = 0x24; +static const uint8_t MIPI = 0x26; +static const uint8_t DISPLAY_ON = 0x29; +static const uint8_t RASET = 0x2B; +static const uint8_t CASET = 0x2A; +static const uint8_t WDATA = 0x2C; +static const uint8_t TEON = 0x35; +static const uint8_t MADCTL_CMD = 0x36; +static const uint8_t PIXFMT = 0x3A; +static const uint8_t BRIGHTNESS = 0x51; +static const uint8_t SWIRE1 = 0x5A; +static const uint8_t SWIRE2 = 0x5B; +static const uint8_t PAGESEL = 0xFE; + +static const uint8_t MADCTL_MY = 0x80; ///< Bit 7 Bottom to top +static const uint8_t MADCTL_MX = 0x40; ///< Bit 6 Right to left +static const uint8_t MADCTL_MV = 0x20; ///< Bit 5 Reverse Mode +static const uint8_t MADCTL_RGB = 0x00; ///< Bit 3 Red-Green-Blue pixel order +static const uint8_t MADCTL_BGR = 0x08; ///< Bit 3 Blue-Green-Red pixel order + +static const uint8_t DELAY_FLAG = 0xFF; +// store a 16 bit value in a buffer, big endian. +static inline void put16_be(uint8_t *buf, uint16_t value) { + buf[0] = value >> 8; + buf[1] = value; +} + +enum Model { + CUSTOM, + RM690B0, + RM67162, +}; + +class QspiDbi : public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_model(const char *model) { this->model_ = model; } + void update() override; + void setup() override; + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; } + void set_dimensions(uint16_t width, uint16_t height) { + this->width_ = width; + this->height_ = height; + } + void set_invert_colors(bool invert_colors) { + this->invert_colors_ = invert_colors; + this->reset_params_(); + } + void set_mirror_x(bool mirror_x) { + this->mirror_x_ = mirror_x; + this->reset_params_(); + } + void set_mirror_y(bool mirror_y) { + this->mirror_y_ = mirror_y; + this->reset_params_(); + } + void set_swap_xy(bool swap_xy) { + this->swap_xy_ = swap_xy; + this->reset_params_(); + } + void set_brightness(uint8_t brightness) { + this->brightness_ = brightness; + this->reset_params_(); + } + void set_offsets(int16_t offset_x, int16_t offset_y) { + this->offset_x_ = offset_x; + this->offset_y_ = offset_y; + } + + void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + void dump_config() override; + + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + bool can_proceed() override { return this->setup_complete_; } + void add_init_sequence(const std::vector &sequence) { this->init_sequences_.push_back(sequence); } + + protected: + void check_buffer_() { + if (this->buffer_ == nullptr) + this->init_internal_(this->width_ * this->height_ * 2); + } + void write_sequence_(const std::vector &vec); + void draw_absolute_pixel_internal(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; + void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad); + /** + * the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the + * sample code.) + * + * Immediately after enabling /CS send 4 bytes in single-dataline SPI mode: + * 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be + * sent in 1-dataline SPI. The second indicates quad mode. + * 1: 0x00 + * 2: The command (register address) byte. + * 3: 0x00 + * + * This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte. + * At the conclusion of the write, de-assert /CS. + * + * @param cmd + * @param bytes + * @param len + */ + void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len); + + void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } + void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } + void reset_params_(bool ready = false); + void write_init_sequence_(); + void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2); + + GPIOPin *reset_pin_{nullptr}; + GPIOPin *enable_pin_{nullptr}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + bool setup_complete_{}; + + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + size_t width_{}; + size_t height_{}; + int16_t offset_x_{0}; + int16_t offset_y_{0}; + bool swap_xy_{}; + bool mirror_x_{}; + bool mirror_y_{}; + bool draw_from_origin_{false}; + uint8_t brightness_{0xD0}; + const char *model_{"Unknown"}; + std::vector> init_sequences_{}; + + esp_lcd_panel_handle_t handle_{}; +}; + +} // namespace qspi_dbi +} // namespace esphome +#endif diff --git a/esphome/components/qwiic_pir/__init__.py b/esphome/components/qwiic_pir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/qwiic_pir/binary_sensor.py b/esphome/components/qwiic_pir/binary_sensor.py new file mode 100644 index 0000000000..360f8b506a --- /dev/null +++ b/esphome/components/qwiic_pir/binary_sensor.py @@ -0,0 +1,67 @@ +from esphome import core +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, binary_sensor +from esphome.const import ( + CONF_DEBOUNCE, + DEVICE_CLASS_MOTION, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@kahrendt"] + +qwiic_pir_ns = cg.esphome_ns.namespace("qwiic_pir") + +DebounceMode = qwiic_pir_ns.enum("DebounceMode") +DEBOUNCE_MODE_OPTIONS = { + "RAW": DebounceMode.RAW_DEBOUNCE_MODE, + "NATIVE": DebounceMode.NATIVE_DEBOUNCE_MODE, + "HYBRID": DebounceMode.HYBRID_DEBOUNCE_MODE, +} + +CONF_DEBOUNCE_MODE = "debounce_mode" + +QwiicPIRComponent = qwiic_pir_ns.class_( + "QwiicPIRComponent", cg.Component, i2c.I2CDevice, binary_sensor.BinarySensor +) + + +def validate_no_debounce_unless_native(config): + if CONF_DEBOUNCE in config: + if config[CONF_DEBOUNCE_MODE] != "NATIVE": + raise cv.Invalid("debounce can only be set if debounce_mode is NATIVE") + return config + + +CONFIG_SCHEMA = cv.All( + binary_sensor.binary_sensor_schema( + QwiicPIRComponent, + device_class=DEVICE_CLASS_MOTION, + ) + .extend( + { + cv.Optional(CONF_DEBOUNCE): cv.All( + cv.time_period, + cv.Range(max=core.TimePeriod(milliseconds=65535)), + ), + cv.Optional(CONF_DEBOUNCE_MODE, default="HYBRID"): cv.enum( + DEBOUNCE_MODE_OPTIONS, upper=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x12)), + validate_no_debounce_unless_native, +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if debounce_time_setting := config.get(CONF_DEBOUNCE): + cg.add(var.set_debounce_time(debounce_time_setting.total_milliseconds)) + else: + cg.add(var.set_debounce_time(1)) # default to 1 ms if not configured + cg.add(var.set_debounce_mode(config[CONF_DEBOUNCE_MODE])) diff --git a/esphome/components/qwiic_pir/qwiic_pir.cpp b/esphome/components/qwiic_pir/qwiic_pir.cpp new file mode 100644 index 0000000000..c267554c45 --- /dev/null +++ b/esphome/components/qwiic_pir/qwiic_pir.cpp @@ -0,0 +1,137 @@ +#include "qwiic_pir.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace qwiic_pir { + +static const char *const TAG = "qwiic_pir"; + +void QwiicPIRComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up Qwiic PIR..."); + + // Verify I2C communcation by reading and verifying the chip ID + uint8_t chip_id; + + if (!this->read_byte(QWIIC_PIR_CHIP_ID, &chip_id)) { + ESP_LOGE(TAG, "Failed to read the chip's ID"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + if (chip_id != QWIIC_PIR_DEVICE_ID) { + ESP_LOGE(TAG, "Unknown chip ID, is this a Qwiic PIR?"); + + this->error_code_ = ERROR_WRONG_CHIP_ID; + this->mark_failed(); + + return; + } + + if (!this->write_byte_16(QWIIC_PIR_DEBOUNCE_TIME, this->debounce_time_)) { + ESP_LOGE(TAG, "Failed to configure debounce time."); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + if (this->debounce_mode_ == NATIVE_DEBOUNCE_MODE) { + // Publish the starting raw state of the PIR sensor + // If NATIVE mode, the binary_sensor state would be unknown until a motion event + if (!this->read_byte(QWIIC_PIR_EVENT_STATUS, &this->event_register_.reg)) { + ESP_LOGE(TAG, "Failed to read initial sensor state."); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + this->publish_state(this->event_register_.raw_reading); + } +} + +void QwiicPIRComponent::loop() { + // Read Event Register + if (!this->read_byte(QWIIC_PIR_EVENT_STATUS, &this->event_register_.reg)) { + ESP_LOGW(TAG, "Failed to communicate with sensor"); + + return; + } + + if (this->debounce_mode_ == HYBRID_DEBOUNCE_MODE) { + // Use a combination of the raw sensor reading and the device's event detection to determine state + // - The device is hardcoded to use a debounce time of 1 ms in this mode + // - Any event, even if it is object_removed, implies motion was active since the last loop, so publish true + // - Use ESPHome's built-in filters for debouncing + this->publish_state(this->event_register_.raw_reading || this->event_register_.event_available); + + if (this->event_register_.event_available) { + this->clear_events_(); + } + } else if (this->debounce_mode_ == NATIVE_DEBOUNCE_MODE) { + // Uses the device's firmware to debounce the signal + // - Follows the logic of SparkFun's example implementation: + // https://github.com/sparkfun/SparkFun_Qwiic_PIR_Arduino_Library/blob/master/examples/Example2_PrintPIRStatus/Example2_PrintPIRStatus.ino + // (accessed July 2023) + // - Is unreliable at detecting an object being removed, especially at debounce rates even slightly large + if (this->event_register_.event_available) { + // If an object is detected, publish true + if (this->event_register_.object_detected) + this->publish_state(true); + + // If an object has been removed, publish false + if (this->event_register_.object_removed) + this->publish_state(false); + + this->clear_events_(); + } + } else if (this->debounce_mode_ == RAW_DEBOUNCE_MODE) { + // Publishes the raw PIR sensor reading with no further logic + // - May miss a very short motion detection if the ESP's loop time is slow + this->publish_state(this->event_register_.raw_reading); + } +} + +void QwiicPIRComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Qwiic PIR:"); + + if (this->debounce_mode_ == RAW_DEBOUNCE_MODE) { + ESP_LOGCONFIG(TAG, " Debounce Mode: RAW"); + } else if (this->debounce_mode_ == NATIVE_DEBOUNCE_MODE) { + ESP_LOGCONFIG(TAG, " Debounce Mode: NATIVE"); + ESP_LOGCONFIG(TAG, " Debounce Time: %ums", this->debounce_time_); + } else if (this->debounce_mode_ == HYBRID_DEBOUNCE_MODE) { + ESP_LOGCONFIG(TAG, " Debounce Mode: HYBRID"); + } + + switch (this->error_code_) { + case NONE: + break; + case ERROR_COMMUNICATION_FAILED: + ESP_LOGE(TAG, " Communication with Qwiic PIR failed!"); + break; + case ERROR_WRONG_CHIP_ID: + ESP_LOGE(TAG, " Qwiic PIR has wrong chip ID - please verify you are using a Qwiic PIR"); + break; + default: + ESP_LOGE(TAG, " Qwiic PIR error code %d", (int) this->error_code_); + break; + } + + LOG_I2C_DEVICE(this); + LOG_BINARY_SENSOR(" ", "Qwiic PIR Binary Sensor", this); +} + +void QwiicPIRComponent::clear_events_() { + // Clear event status register + if (!this->write_byte(QWIIC_PIR_EVENT_STATUS, 0x00)) + ESP_LOGW(TAG, "Failed to clear events on sensor"); +} + +} // namespace qwiic_pir +} // namespace esphome diff --git a/esphome/components/qwiic_pir/qwiic_pir.h b/esphome/components/qwiic_pir/qwiic_pir.h new file mode 100644 index 0000000000..d58d67734f --- /dev/null +++ b/esphome/components/qwiic_pir/qwiic_pir.h @@ -0,0 +1,70 @@ +/* + * Adds support for Qwiic PIR motion sensors that communicate over an I2C bus. + * These sensors use Sharp PIR motion sensors to detect motion. A firmware running on an ATTiny84 translates the digital + * output to I2C communications. + * ATTiny84 firmware: https://github.com/sparkfun/Qwiic_PIR (acccessed July 2023) + * SparkFun's Arduino library: https://github.com/sparkfun/SparkFun_Qwiic_PIR_Arduino_Library (accessed July 2023) + */ + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace qwiic_pir { + +// Qwiic PIR I2C Register Addresses +enum { + QWIIC_PIR_CHIP_ID = 0x00, + QWIIC_PIR_EVENT_STATUS = 0x03, + QWIIC_PIR_DEBOUNCE_TIME = 0x05, // uint16_t debounce time in milliseconds +}; + +enum DebounceMode { + RAW_DEBOUNCE_MODE, + NATIVE_DEBOUNCE_MODE, + HYBRID_DEBOUNCE_MODE, +}; + +static const uint8_t QWIIC_PIR_DEVICE_ID = 0x72; + +class QwiicPIRComponent : public Component, public i2c::I2CDevice, public binary_sensor::BinarySensor { + public: + void setup() override; + void loop() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_debounce_time(uint16_t debounce_time) { this->debounce_time_ = debounce_time; } + void set_debounce_mode(DebounceMode mode) { this->debounce_mode_ = mode; } + + protected: + uint16_t debounce_time_{}; + + DebounceMode debounce_mode_{}; + + enum ErrorCode { + NONE = 0, + ERROR_COMMUNICATION_FAILED, + ERROR_WRONG_CHIP_ID, + } error_code_{NONE}; + + union { + struct { + bool raw_reading : 1; // raw state of PIR sensor + bool event_available : 1; // a debounced object has been detected or removed + bool object_removed : 1; // a debounced object is no longer detected + bool object_detected : 1; // a debounced object has been detected + bool : 4; + }; + uint8_t reg; + } event_register_ = {.reg = 0}; + + void clear_events_(); +}; + +} // namespace qwiic_pir +} // namespace esphome diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp index b10986c9cb..a4c79db753 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.cpp +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -1,5 +1,7 @@ #include "radon_eye_listener.h" #include "esphome/core/log.h" +#include +#include #ifdef USE_ESP32 @@ -10,9 +12,14 @@ static const char *const TAG = "radon_eye_ble"; bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (not device.get_name().empty()) { - if (device.get_name().rfind("FR:R20:SN", 0) == 0) { - // This is an RD200, I think - ESP_LOGD(TAG, "Found Radon Eye RD200 device Name: %s (MAC: %s)", device.get_name().c_str(), + // 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().rfind(prefix, 0) == 0; })) { + // Device found + ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), device.address_str().c_str()); } } diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 4e74020e4c..e2146dd14e 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -397,8 +397,10 @@ RC522::StatusCode RC522::await_transceive_() { back_length_ = 0; ESP_LOGW(TAG, "Communication with the MFRC522 might be down, reset in %d", 10 - error_counter_); // todo: trigger reset? - if (error_counter_++ > 10) + if (error_counter_++ >= 10) { setup(); + error_counter_ = 0; // reset the error counter + } return STATUS_TIMEOUT; } diff --git a/esphome/components/rdm6300/rdm6300.cpp b/esphome/components/rdm6300/rdm6300.cpp index 434b9f5720..bfdd880079 100644 --- a/esphome/components/rdm6300/rdm6300.cpp +++ b/esphome/components/rdm6300/rdm6300.cpp @@ -57,7 +57,7 @@ void rdm6300::RDM6300Component::loop() { trig->process(result); if (report) { - ESP_LOGD(TAG, "Found new tag with ID %u", result); + ESP_LOGD(TAG, "Found new tag with ID %" PRIu32, result); } } } diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index 0aeabef2bc..1a1a0c0cd6 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -5,6 +5,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/uart/uart.h" +#include #include namespace esphome { diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 2ef33f3711..3d1c10a092 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import binary_sensor from esphome.const import ( + CONF_COMMAND_REPEATS, CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, @@ -17,6 +18,7 @@ from esphome.const import ( CONF_PROTOCOL, CONF_GROUP, CONF_DEVICE, + CONF_SECOND, CONF_STATE, CONF_CHANNEL, CONF_FAMILY, @@ -30,6 +32,10 @@ from esphome.const import ( CONF_MAGNITUDE, CONF_WAND_ID, CONF_LEVEL, + CONF_DELTA, + CONF_ID, + CONF_BUTTON, + CONF_CHECK, ) from esphome.core import coroutine from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -39,6 +45,7 @@ AUTO_LOAD = ["binary_sensor"] CONF_RECEIVER_ID = "receiver_id" CONF_TRANSMITTER_ID = "transmitter_id" +CONF_FIRST = "first" ns = remote_base_ns = cg.esphome_ns.namespace("remote_base") RemoteProtocol = ns.class_("RemoteProtocol") @@ -50,8 +57,9 @@ RemoteReceiverTrigger = ns.class_( "RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener ) RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper") +RemoteTransmittable = ns.class_("RemoteTransmittable") RemoteTransmitterActionBase = ns.class_( - "RemoteTransmitterActionBase", automation.Action + "RemoteTransmitterActionBase", RemoteTransmittable, automation.Action ) RemoteReceiverBase = ns.class_("RemoteReceiverBase") RemoteTransmitterBase = ns.class_("RemoteTransmitterBase") @@ -66,11 +74,30 @@ def templatize(value): return cv.Schema(ret) +REMOTE_LISTENER_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), + } +) + + +REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), + } +) + + async def register_listener(var, config): receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) cg.add(receiver.register_listener(var)) +async def register_transmittable(var, config): + transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter_)) + + def register_binary_sensor(name, type, schema): return BINARY_SENSOR_REGISTRY.register(name, type, schema) @@ -127,10 +154,9 @@ def validate_repeat(value): BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema( { - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), cv.Optional(CONF_REPEAT): validate_repeat, } -) +).extend(REMOTE_TRANSMITTABLE_SCHEMA) def register_action(name, type_, schema): @@ -141,9 +167,8 @@ def register_action(name, type_, schema): def decorator(func): async def new_func(config, action_id, template_arg, args): - transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID]) var = cg.new_Pvariable(action_id, template_arg) - cg.add(var.set_parent(transmitter)) + await register_transmittable(var, config) if CONF_REPEAT in config: conf = config[CONF_REPEAT] template_ = await cg.templatable(conf[CONF_TIMES], args, cg.uint32) @@ -240,6 +265,55 @@ async def build_dumpers(config): return dumpers +# ByronSX +( + ByronSXData, + ByronSXBinarySensor, + ByronSXTrigger, + ByronSXAction, + ByronSXDumper, +) = declare_protocol("ByronSX") +BYRONSX_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0xFF)), + cv.Optional(CONF_COMMAND, default=0x10): cv.All( + cv.hex_int, cv.one_of(1, 2, 3, 5, 6, 9, 0xD, 0xE, 0x10, int=True) + ), + } +) + + +@register_binary_sensor("byronsx", ByronSXBinarySensor, BYRONSX_SCHEMA) +def byronsx_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + ByronSXData, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("byronsx", ByronSXTrigger, ByronSXData) +def byronsx_trigger(var, config): + pass + + +@register_dumper("byronsx", ByronSXDumper) +def byronsx_dumper(var, config): + pass + + +@register_action("byronsx", ByronSXAction, BYRONSX_SCHEMA) +async def byronsx_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + # CanalSat ( CanalSatData, @@ -349,19 +423,43 @@ async def canalsatld_action(var, config, args): CoolixAction, CoolixDumper, ) = declare_protocol("Coolix") -COOLIX_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) -@register_binary_sensor("coolix", CoolixBinarySensor, COOLIX_SCHEMA) +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." + ), + } +) + +COOLIX_SENSOR_SCHEMA = cv.Any(cv.hex_int_range(0, 16777215), COOLIX_BASE_SCHEMA) + + +@register_binary_sensor("coolix", CoolixBinarySensor, COOLIX_SENSOR_SCHEMA) def coolix_binary_sensor(var, config): - cg.add( - var.set_data( - cg.StructInitializer( - CoolixData, - ("data", config[CONF_DATA]), + if isinstance(config, dict): + cg.add( + var.set_data( + cg.ArrayInitializer( + config[CONF_FIRST], + config[CONF_SECOND], + ) ) ) - ) + else: + cg.add(var.set_data(cg.ArrayInitializer(0, config))) + + +@register_action("coolix", CoolixAction, COOLIX_BASE_SCHEMA) +async def coolix_action(var, config, args): + template_ = await cg.templatable(config[CONF_FIRST], args, cg.uint32) + cg.add(var.set_first(template_)) + template_ = await cg.templatable(config[CONF_SECOND], args, cg.uint32) + cg.add(var.set_second(template_)) @register_trigger("coolix", CoolixTrigger, CoolixData) @@ -374,12 +472,6 @@ def coolix_dumper(var, config): pass -@register_action("coolix", CoolixAction, COOLIX_SCHEMA) -async def coolix_action(var, config, args): - template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) - cg.add(var.set_data(template_)) - - # Dish DishData, DishBinarySensor, DishTrigger, DishAction, DishDumper = declare_protocol( "Dish" @@ -423,6 +515,57 @@ async def dish_action(var, config, args): cg.add(var.set_command(template_)) +# Dooya +DooyaData, DooyaBinarySensor, DooyaTrigger, DooyaAction, DooyaDumper = declare_protocol( + "Dooya" +) +DOOYA_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.hex_int_range(0, 16777215), + cv.Required(CONF_CHANNEL): cv.hex_int_range(0, 255), + cv.Required(CONF_BUTTON): cv.hex_int_range(0, 15), + cv.Required(CONF_CHECK): cv.hex_int_range(0, 15), + } +) + + +@register_binary_sensor("dooya", DooyaBinarySensor, DOOYA_SCHEMA) +def dooya_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + DooyaData, + ("id", config[CONF_ID]), + ("channel", config[CONF_CHANNEL]), + ("button", config[CONF_BUTTON]), + ("check", config[CONF_CHECK]), + ) + ) + ) + + +@register_trigger("dooya", DooyaTrigger, DooyaData) +def dooya_trigger(var, config): + pass + + +@register_dumper("dooya", DooyaDumper) +def dooya_dumper(var, config): + pass + + +@register_action("dooya", DooyaAction, DOOYA_SCHEMA) +async def dooya_action(var, config, args): + template_ = await cg.templatable(config[CONF_ID], args, cg.uint32) + cg.add(var.set_id(template_)) + template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) + cg.add(var.set_channel(template_)) + template_ = await cg.templatable(config[CONF_BUTTON], args, cg.uint8) + cg.add(var.set_button(template_)) + template_ = await cg.templatable(config[CONF_CHECK], args, cg.uint8) + cg.add(var.set_check(template_)) + + # JVC JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC") JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) @@ -545,12 +688,69 @@ async def magiquest_action(var, config, args): cg.add(var.set_magnitude(template_)) +# Microchip HCS301 KeeLoq OOK +( + KeeloqData, + KeeloqBinarySensor, + KeeloqTrigger, + KeeloqAction, + KeeloqDumper, +) = declare_protocol("Keeloq") +KEELOQ_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFFFFF)), + cv.Required(CONF_CODE): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFFFFFF)), + cv.Optional(CONF_COMMAND, default=0x10): cv.All( + cv.hex_int, + cv.Range(min=0, max=0x10), + ), + cv.Optional(CONF_LEVEL, default=False): cv.boolean, + } +) + + +@register_binary_sensor("keeloq", KeeloqBinarySensor, KEELOQ_SCHEMA) +def Keeloq_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + KeeloqData, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("keeloq", KeeloqTrigger, KeeloqData) +def keeloq_trigger(var, config): + pass + + +@register_dumper("keeloq", KeeloqDumper) +def keeloq_dumper(var, config): + pass + + +@register_action("keeloq", KeeloqAction, KEELOQ_SCHEMA) +async def keeloq_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint32) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32) + cg.add(var.set_encrypted(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + template_ = await cg.templatable(config[CONF_LEVEL], args, bool) + cg.add(var.set_vlow(template_)) + + # NEC NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC") NEC_SCHEMA = cv.Schema( { cv.Required(CONF_ADDRESS): cv.hex_uint16_t, cv.Required(CONF_COMMAND): cv.hex_uint16_t, + cv.Optional(CONF_COMMAND_REPEATS, default=1): cv.uint16_t, } ) @@ -563,6 +763,7 @@ def nec_binary_sensor(var, config): NECData, ("address", config[CONF_ADDRESS]), ("command", config[CONF_COMMAND]), + ("command_repeats", config[CONF_COMMAND_REPEATS]), ) ) ) @@ -584,6 +785,8 @@ async def nec_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint16) cg.add(var.set_command(template_)) + template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint16) + cg.add(var.set_command_repeats(template_)) # Pioneer @@ -644,6 +847,7 @@ async def pioneer_action(var, config, args): PRONTO_SCHEMA = cv.Schema( { cv.Required(CONF_DATA): cv.string, + cv.Optional(CONF_DELTA, default=-1): cv.int_, } ) @@ -655,6 +859,7 @@ def pronto_binary_sensor(var, config): cg.StructInitializer( ProntoData, ("data", config[CONF_DATA]), + ("delta", config[CONF_DELTA]), ) ) ) @@ -676,6 +881,45 @@ async def pronto_action(var, config, args): cg.add(var.set_data(template_)) +# Roomba +( + RoombaData, + RoombaBinarySensor, + RoombaTrigger, + RoombaAction, + RoombaDumper, +) = declare_protocol("Roomba") +ROOMBA_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint8_t}) + + +@register_binary_sensor("roomba", RoombaBinarySensor, ROOMBA_SCHEMA) +def roomba_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + RoombaData, + ("data", config[CONF_DATA]), + ) + ) + ) + + +@register_trigger("roomba", RoombaTrigger, RoombaData) +def roomba_trigger(var, config): + pass + + +@register_dumper("roomba", RoombaDumper) +def roomba_dumper(var, config): + pass + + +@register_action("roomba", RoombaAction, ROOMBA_SCHEMA) +async def roomba_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint8) + cg.add(var.set_data(template_)) + + # Sony SonyData, SonyBinarySensor, SonyTrigger, SonyAction, SonyDumper = declare_protocol( "Sony" @@ -944,7 +1188,7 @@ RC_SWITCH_PROTOCOL_SCHEMA = cv.Any( def validate_rc_switch_code(value): - if not isinstance(value, (str, str)): + if not isinstance(value, str): raise cv.Invalid("All RCSwitch codes must be in quotes ('')") for c in value: if c not in ("0", "1"): @@ -961,7 +1205,7 @@ def validate_rc_switch_code(value): def validate_rc_switch_raw_code(value): - if not isinstance(value, (str, str)): + if not isinstance(value, str): raise cv.Invalid("All RCSwitch raw codes must be in quotes ('')") for c in value: if c not in ("0", "1", "x"): @@ -1463,19 +1707,14 @@ MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_p MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase) MIDEA_SCHEMA = cv.Schema( { - cv.Required(CONF_CODE): cv.templatable( - cv.All( - [cv.Any(cv.hex_uint8_t, cv.uint8_t)], - cv.Length(min=5, max=5), - ) - ), + cv.Required(CONF_CODE): cv.All([cv.hex_uint8_t], cv.Length(min=5, max=5)), } ) @register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA) def midea_binary_sensor(var, config): - cg.add(var.set_code(config[CONF_CODE])) + cg.add(var.set_data(config[CONF_CODE])) @register_trigger("midea", MideaTrigger, MideaData) @@ -1488,18 +1727,11 @@ def midea_dumper(var, config): pass -@register_action( - "midea", - MideaAction, - MIDEA_SCHEMA, -) +@register_action("midea", MideaAction, MIDEA_SCHEMA) async def midea_action(var, config, args): - code_ = config[CONF_CODE] - if cg.is_template(code_): - template_ = await cg.templatable(code_, args, cg.std_vector.template(cg.uint8)) - cg.add(var.set_code_template(template_)) - else: - cg.add(var.set_code_static(code_)) + vec_ = cg.std_vector.template(cg.uint8) + template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_) + cg.add(var.set_code(template_)) # AEHA @@ -1509,10 +1741,7 @@ AEHAData, AEHABinarySensor, AEHATrigger, AEHAAction, AEHADumper = declare_protoc AEHA_SCHEMA = cv.Schema( { cv.Required(CONF_ADDRESS): cv.hex_uint16_t, - cv.Required(CONF_DATA): cv.All( - [cv.Any(cv.hex_uint8_t, cv.uint8_t)], - cv.Length(min=2, max=35), - ), + cv.Required(CONF_DATA): cv.All([cv.hex_uint8_t], cv.Length(min=2, max=35)), } ) @@ -1540,8 +1769,197 @@ def aeha_dumper(var, config): pass -@register_action("aeha", AEHAAction, AEHA_SCHEMA) +@register_action( + "aeha", + AEHAAction, + AEHA_SCHEMA.extend( + { + cv.Optional(CONF_CARRIER_FREQUENCY, default="38000Hz"): cv.All( + cv.frequency, cv.int_ + ), + } + ), +) async def aeha_action(var, config, args): template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) cg.add(var.set_address(template_)) - cg.add(var.set_data(config[CONF_DATA])) + template_ = await cg.templatable( + config[CONF_DATA], args, cg.std_vector.template(cg.uint8) + ) + cg.add(var.set_data(template_)) + templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32) + cg.add(var.set_carrier_frequency(templ)) + + +# Haier +HaierData, HaierBinarySensor, HaierTrigger, HaierAction, HaierDumper = declare_protocol( + "Haier" +) +HaierAction = ns.class_("HaierAction", RemoteTransmitterActionBase) +HAIER_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.All([cv.hex_uint8_t], cv.Length(min=13, max=13)), + } +) + + +@register_binary_sensor("haier", HaierBinarySensor, HAIER_SCHEMA) +def haier_binary_sensor(var, config): + cg.add(var.set_code(config[CONF_CODE])) + + +@register_trigger("haier", HaierTrigger, HaierData) +def haier_trigger(var, config): + pass + + +@register_dumper("haier", HaierDumper) +def haier_dumper(var, config): + pass + + +@register_action("haier", HaierAction, HAIER_SCHEMA) +async def haier_action(var, config, args): + vec_ = cg.std_vector.template(cg.uint8) + template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_) + cg.add(var.set_code(template_)) + + +# ABBWelcome +( + ABBWelcomeData, + ABBWelcomeBinarySensor, + ABBWelcomeTrigger, + ABBWelcomeAction, + ABBWelcomeDumper, +) = declare_protocol("ABBWelcome") + +CONF_SOURCE_ADDRESS = "source_address" +CONF_DESTINATION_ADDRESS = "destination_address" +CONF_THREE_BYTE_ADDRESS = "three_byte_address" +CONF_MESSAGE_TYPE = "message_type" +CONF_MESSAGE_ID = "message_id" +CONF_RETRANSMISSION = "retransmission" + +ABB_WELCOME_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE_ADDRESS): cv.hex_uint32_t, + cv.Required(CONF_DESTINATION_ADDRESS): cv.hex_uint32_t, + cv.Optional(CONF_RETRANSMISSION, default=False): cv.boolean, + cv.Optional(CONF_THREE_BYTE_ADDRESS, default=False): cv.boolean, + cv.Required(CONF_MESSAGE_TYPE): cv.Any(cv.hex_uint8_t, cv.uint8_t), + cv.Optional(CONF_MESSAGE_ID): cv.Any(cv.hex_uint8_t, cv.uint8_t), + cv.Optional(CONF_DATA): cv.All( + [cv.Any(cv.hex_uint8_t, cv.uint8_t)], + cv.Length(min=0, max=7), + ), + } +) + + +@register_binary_sensor("abbwelcome", ABBWelcomeBinarySensor, ABB_WELCOME_SCHEMA) +def abbwelcome_binary_sensor(var, config): + cg.add(var.set_three_byte_address(config[CONF_THREE_BYTE_ADDRESS])) + cg.add(var.set_source_address(config[CONF_SOURCE_ADDRESS])) + cg.add(var.set_destination_address(config[CONF_DESTINATION_ADDRESS])) + cg.add(var.set_retransmission(config[CONF_RETRANSMISSION])) + cg.add(var.set_message_type(config[CONF_MESSAGE_TYPE])) + cg.add(var.set_auto_message_id(CONF_MESSAGE_ID not in config)) + if CONF_MESSAGE_ID in config: + cg.add(var.set_message_id(config[CONF_MESSAGE_ID])) + if CONF_DATA in config: + cg.add(var.set_data(config[CONF_DATA])) + cg.add(var.finalize()) + + +@register_trigger("abbwelcome", ABBWelcomeTrigger, ABBWelcomeData) +def abbwelcome_trigger(var, config): + pass + + +@register_dumper("abbwelcome", ABBWelcomeDumper) +def abbwelcome_dumper(var, config): + pass + + +@register_action("abbwelcome", ABBWelcomeAction, ABB_WELCOME_SCHEMA) +async def abbwelcome_action(var, config, args): + cg.add( + var.set_three_byte_address( + await cg.templatable(config[CONF_THREE_BYTE_ADDRESS], args, cg.bool_) + ) + ) + cg.add( + var.set_source_address( + await cg.templatable(config[CONF_SOURCE_ADDRESS], args, cg.uint16) + ) + ) + cg.add( + var.set_destination_address( + await cg.templatable(config[CONF_DESTINATION_ADDRESS], args, cg.uint16) + ) + ) + cg.add( + var.set_retransmission( + await cg.templatable(config[CONF_RETRANSMISSION], args, cg.bool_) + ) + ) + cg.add( + var.set_message_type( + await cg.templatable(config[CONF_MESSAGE_TYPE], args, cg.uint8) + ) + ) + cg.add(var.set_auto_message_id(CONF_MESSAGE_ID not in config)) + if CONF_MESSAGE_ID in config: + cg.add( + var.set_message_id( + await cg.templatable(config[CONF_MESSAGE_ID], args, cg.uint8) + ) + ) + if CONF_DATA in config: + data_ = config[CONF_DATA] + if cg.is_template(data_): + template_ = await cg.templatable( + data_, args, cg.std_vector.template(cg.uint8) + ) + cg.add(var.set_data_template(template_)) + else: + cg.add(var.set_data_static(data_)) + + +# Mirage +( + MirageData, + MirageBinarySensor, + MirageTrigger, + MirageAction, + MirageDumper, +) = declare_protocol("Mirage") + +MIRAGE_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.All([cv.hex_uint8_t], cv.Length(min=14, max=14)), + } +) + + +@register_binary_sensor("mirage", MirageBinarySensor, MIRAGE_SCHEMA) +def mirage_binary_sensor(var, config): + cg.add(var.set_code(config[CONF_CODE])) + + +@register_trigger("mirage", MirageTrigger, MirageData) +def mirage_trigger(var, config): + pass + + +@register_dumper("mirage", MirageDumper) +def mirage_dumper(var, config): + pass + + +@register_action("mirage", MirageAction, MIRAGE_SCHEMA) +async def mirage_action(var, config, args): + vec_ = cg.std_vector.template(cg.uint8) + template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_) + cg.add(var.set_code(template_)) diff --git a/esphome/components/remote_base/abbwelcome_protocol.cpp b/esphome/components/remote_base/abbwelcome_protocol.cpp new file mode 100644 index 0000000000..88f928901b --- /dev/null +++ b/esphome/components/remote_base/abbwelcome_protocol.cpp @@ -0,0 +1,123 @@ +#include "abbwelcome_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.abbwelcome"; + +static const uint32_t BIT_ONE_SPACE_US = 102; +static const uint32_t BIT_ZERO_MARK_US = 32; // 18-44 +static const uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; +static const uint16_t BYTE_SPACE_US = 210; + +uint8_t ABBWelcomeData::calc_cs_() const { + uint8_t checksum = 0; + for (uint8_t i = 0; i < this->size() - 1; i++) { + uint16_t temp = checksum ^ (this->data_[i]); + temp = temp ^ (uint16_t) (((uint32_t) temp << 0x11) >> 0x10) ^ (uint16_t) (((uint32_t) temp << 0x12) >> 0x10) ^ + (uint16_t) (((uint32_t) temp << 0x13) >> 0x10) ^ (uint16_t) (((uint32_t) temp << 0x14) >> 0x10) ^ + (uint16_t) (((uint32_t) temp << 0x15) >> 0x10) ^ (uint16_t) (((uint32_t) temp << 0x16) >> 0x10) ^ + (uint16_t) (((uint32_t) temp << 0x17) >> 0x10); + checksum = (temp & 0xfe) ^ ((temp >> 8) & 1); + } + return ~checksum; +} + +void ABBWelcomeProtocol::encode_byte_(RemoteTransmitData *dst, uint8_t data) const { + // space = bus high, mark = activate bus pulldown + dst->mark(BIT_ZERO_MARK_US); + uint32_t next_space = BIT_ZERO_SPACE_US; + for (uint8_t mask = 1 << 7; mask; mask >>= 1) { + if (data & mask) { + next_space += BIT_ONE_SPACE_US; + } else { + dst->space(next_space); + dst->mark(BIT_ZERO_MARK_US); + next_space = BIT_ZERO_SPACE_US; + } + } + next_space += BYTE_SPACE_US; + dst->space(next_space); +} + +void ABBWelcomeProtocol::encode(RemoteTransmitData *dst, const ABBWelcomeData &src) { + dst->set_carrier_frequency(0); + uint32_t reserve_count = 0; + for (size_t i = 0; i < src.size(); i++) { + reserve_count += 2 * (9 - (src[i] & 1) - ((src[i] >> 1) & 1) - ((src[i] >> 2) & 1) - ((src[i] >> 3) & 1) - + ((src[i] >> 4) & 1) - ((src[i] >> 5) & 1) - ((src[i] >> 6) & 1) - ((src[i] >> 7) & 1)); + } + dst->reserve(reserve_count); + for (size_t i = 0; i < src.size(); i++) + this->encode_byte_(dst, src[i]); + ESP_LOGD(TAG, "Transmitting: %s", src.to_string().c_str()); +} + +bool ABBWelcomeProtocol::decode_byte_(RemoteReceiveData &src, bool &done, uint8_t &data) { + if (!src.expect_mark(BIT_ZERO_MARK_US)) + return false; + uint32_t next_space = BIT_ZERO_SPACE_US; + for (uint8_t mask = 1 << 7; mask; mask >>= 1) { + // if (!src.peek_space_at_least(next_space, 0)) + // return false; + if (src.expect_space(next_space)) { + if (!src.expect_mark(BIT_ZERO_MARK_US)) + return false; + next_space = BIT_ZERO_SPACE_US; + } else { + data |= mask; + next_space += BIT_ONE_SPACE_US; + } + } + next_space += BYTE_SPACE_US; + // if (!src.peek_space_at_least(next_space, 0)) + // return false; + done = !(src.expect_space(next_space)); + return true; +} + +optional ABBWelcomeProtocol::decode(RemoteReceiveData src) { + if (src.expect_item(BIT_ZERO_MARK_US, BIT_ZERO_SPACE_US) && + src.expect_item(BIT_ZERO_MARK_US, BIT_ZERO_SPACE_US + BIT_ONE_SPACE_US) && + src.expect_item(BIT_ZERO_MARK_US, BIT_ZERO_SPACE_US + BIT_ONE_SPACE_US) && + src.expect_item(BIT_ZERO_MARK_US, BIT_ZERO_SPACE_US + BIT_ONE_SPACE_US) && + src.expect_item(BIT_ZERO_MARK_US, BIT_ZERO_SPACE_US + BIT_ONE_SPACE_US + BYTE_SPACE_US) && + src.expect_item(BIT_ZERO_MARK_US, BIT_ZERO_SPACE_US + 8 * BIT_ONE_SPACE_US + BYTE_SPACE_US)) { + ESP_LOGVV(TAG, "Received Header: 0x55FF"); + ABBWelcomeData out; + out[0] = 0x55; + out[1] = 0xff; + bool done = false; + uint8_t length = 10; + uint8_t received_bytes = 2; + for (; (received_bytes < length) && !done; received_bytes++) { + uint8_t data = 0; + if (!this->decode_byte_(src, done, data)) { + ESP_LOGW(TAG, "Received incomplete packet: %s", out.to_string(received_bytes).c_str()); + return {}; + } + if (received_bytes == 2) { + length += std::min(static_cast(data & DATA_LENGTH_MASK), MAX_DATA_LENGTH); + if (data & 0x40) { + length += 2; + } + } + ESP_LOGVV(TAG, "Received Byte: 0x%02X", data); + out[received_bytes] = data; + } + if (out.is_valid()) { + ESP_LOGI(TAG, "Received: %s", out.to_string().c_str()); + return out; + } + ESP_LOGW(TAG, "Received malformed packet: %s", out.to_string(received_bytes).c_str()); + } + return {}; +} + +void ABBWelcomeProtocol::dump(const ABBWelcomeData &data) { + ESP_LOGD(TAG, "Received ABBWelcome: %s", data.to_string().c_str()); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h new file mode 100644 index 0000000000..f2d0f5b547 --- /dev/null +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -0,0 +1,253 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "remote_base.h" +#include +#include +#include +#include + +namespace esphome { +namespace remote_base { + +static const uint8_t MAX_DATA_LENGTH = 15; +static const uint8_t DATA_LENGTH_MASK = 0x3f; + +/* +Message Format: + 2 bytes: Sync (0x55FF) + 1 bit: Retransmission flag (High means retransmission) + 1 bit: Address length flag (Low means 2 bytes, High means 3 bytes) + 2 bits: Unknown + 4 bits: Data length (in bytes) + 1 bit: Reply flag (High means this is a reply to a previous message with the same message type) + 7 bits: Message type + 2-3 bytes: Destination address + 2-3 bytes: Source address + 1 byte: Message ID (randomized, does not change for retransmissions) + 0-? bytes: Data + 1 byte: Checksum +*/ + +class ABBWelcomeData { + public: + // Make default + ABBWelcomeData() { + std::fill(std::begin(this->data_), std::end(this->data_), 0); + this->data_[0] = 0x55; + this->data_[1] = 0xff; + } + // Make from initializer_list + ABBWelcomeData(std::initializer_list data) { + std::fill(std::begin(this->data_), std::end(this->data_), 0); + std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); + } + // Make from vector + ABBWelcomeData(const std::vector &data) { + std::fill(std::begin(this->data_), std::end(this->data_), 0); + std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); + } + // Default copy constructor + ABBWelcomeData(const ABBWelcomeData &) = default; + + bool auto_message_id{false}; + + uint8_t *data() { return this->data_.data(); } + const uint8_t *data() const { return this->data_.data(); } + uint8_t size() const { + return std::min(static_cast(6 + (2 * this->get_address_length()) + (this->data_[2] & DATA_LENGTH_MASK)), + static_cast(this->data_.size())); + } + bool is_valid() const { + return this->data_[0] == 0x55 && this->data_[1] == 0xff && + ((this->data_[2] & DATA_LENGTH_MASK) <= MAX_DATA_LENGTH) && + (this->data_[this->size() - 1] == this->calc_cs_()); + } + void set_retransmission(bool retransmission) { + if (retransmission) { + this->data_[2] |= 0x80; + } else { + this->data_[2] &= 0x7f; + } + } + bool get_retransmission() const { return this->data_[2] & 0x80; } + // set_three_byte_address must be called before set_source_address, set_destination_address, set_message_id and + // set_data! + void set_three_byte_address(bool three_byte_address) { + if (three_byte_address) { + this->data_[2] |= 0x40; + } else { + this->data_[2] &= 0xbf; + } + } + uint8_t get_three_byte_address() const { return (this->data_[2] & 0x40); } + uint8_t get_address_length() const { return this->get_three_byte_address() ? 3 : 2; } + void set_message_type(uint8_t message_type) { this->data_[3] = message_type; } + uint8_t get_message_type() const { return this->data_[3]; } + void set_destination_address(uint32_t address) { + if (this->get_address_length() == 2) { + this->data_[4] = (address >> 8) & 0xff; + this->data_[5] = address & 0xff; + } else { + this->data_[4] = (address >> 16) & 0xff; + this->data_[5] = (address >> 8) & 0xff; + this->data_[6] = address & 0xff; + } + } + uint32_t get_destination_address() const { + if (this->get_address_length() == 2) { + return (this->data_[4] << 8) + this->data_[5]; + } + return (this->data_[4] << 16) + (this->data_[5] << 8) + this->data_[6]; + } + void set_source_address(uint32_t address) { + if (this->get_address_length() == 2) { + this->data_[6] = (address >> 8) & 0xff; + this->data_[7] = address & 0xff; + } else { + this->data_[7] = (address >> 16) & 0xff; + this->data_[8] = (address >> 8) & 0xff; + this->data_[9] = address & 0xff; + } + } + uint32_t get_source_address() const { + if (this->get_address_length() == 2) { + return (this->data_[6] << 8) + this->data_[7]; + } + return (this->data_[7] << 16) + (this->data_[8] << 8) + this->data_[9]; + } + void set_message_id(uint8_t message_id) { this->data_[4 + 2 * this->get_address_length()] = message_id; } + uint8_t get_message_id() const { return this->data_[4 + 2 * this->get_address_length()]; } + void set_data(std::vector data) { + uint8_t size = std::min(MAX_DATA_LENGTH, static_cast(data.size())); + this->data_[2] &= (0xff ^ DATA_LENGTH_MASK); + this->data_[2] |= (size & DATA_LENGTH_MASK); + if (size) + std::copy_n(data.begin(), size, this->data_.begin() + 5 + 2 * this->get_address_length()); + } + std::vector get_data() const { + std::vector data(this->data_.begin() + 5 + 2 * this->get_address_length(), + this->data_.begin() + 5 + 2 * this->get_address_length() + this->get_data_size()); + return data; + } + uint8_t get_data_size() const { + return std::min(MAX_DATA_LENGTH, static_cast(this->data_[2] & DATA_LENGTH_MASK)); + } + void finalize() { + if (this->auto_message_id && !this->get_retransmission() && !(this->data_[3] & 0x80)) { + this->set_message_id(static_cast(random_uint32())); + } + this->data_[0] = 0x55; + this->data_[1] = 0xff; + this->data_[this->size() - 1] = this->calc_cs_(); + } + std::string to_string(uint8_t max_print_bytes = 255) const { + std::string info; + if (this->is_valid()) { + info = str_sprintf(this->get_three_byte_address() ? "[%06" PRIX32 " %s %06" PRIX32 "] Type: %02X" + : "[%04" PRIX32 " %s %04" PRIX32 "] Type: %02X", + this->get_source_address(), this->get_retransmission() ? "»" : ">", + this->get_destination_address(), this->get_message_type()); + if (this->get_data_size()) + info += str_sprintf(", Data: %s", format_hex_pretty(this->get_data()).c_str()); + } else { + info = "[Invalid]"; + } + uint8_t print_bytes = std::min(this->size(), max_print_bytes); + if (print_bytes) + info = str_sprintf("%s %s", format_hex_pretty(this->data_.data(), print_bytes).c_str(), info.c_str()); + return info; + } + bool operator==(const ABBWelcomeData &rhs) const { + if (std::equal(this->data_.begin(), this->data_.begin() + this->size(), rhs.data_.begin())) + return true; + return (this->auto_message_id || rhs.auto_message_id) && this->is_valid() && rhs.is_valid() && + (this->get_message_type() == rhs.get_message_type()) && + (this->get_source_address() == rhs.get_source_address()) && + (this->get_destination_address() == rhs.get_destination_address()) && (this->get_data() == rhs.get_data()); + } + uint8_t &operator[](size_t idx) { return this->data_[idx]; } + const uint8_t &operator[](size_t idx) const { return this->data_[idx]; } + + protected: + std::array data_; + // Calculate checksum + uint8_t calc_cs_() const; +}; + +class ABBWelcomeProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const ABBWelcomeData &src) override; + optional decode(RemoteReceiveData src) override; + void dump(const ABBWelcomeData &data) override; + + protected: + void encode_byte_(RemoteTransmitData *dst, uint8_t data) const; + bool decode_byte_(RemoteReceiveData &src, bool &done, uint8_t &data); +}; + +class ABBWelcomeBinarySensor : public RemoteReceiverBinarySensorBase { + public: + bool matches(RemoteReceiveData src) override { + auto data = ABBWelcomeProtocol().decode(src); + return data.has_value() && data.value() == this->data_; + } + void set_source_address(const uint32_t source_address) { this->data_.set_source_address(source_address); } + void set_destination_address(const uint32_t destination_address) { + this->data_.set_destination_address(destination_address); + } + void set_retransmission(const bool retransmission) { this->data_.set_retransmission(retransmission); } + void set_three_byte_address(const bool three_byte_address) { this->data_.set_three_byte_address(three_byte_address); } + void set_message_type(const uint8_t message_type) { this->data_.set_message_type(message_type); } + void set_message_id(const uint8_t message_id) { this->data_.set_message_id(message_id); } + void set_auto_message_id(const bool auto_message_id) { this->data_.auto_message_id = auto_message_id; } + void set_data(const std::vector &data) { this->data_.set_data(data); } + void finalize() { this->data_.finalize(); } + + protected: + ABBWelcomeData data_; +}; + +using ABBWelcomeTrigger = RemoteReceiverTrigger; +using ABBWelcomeDumper = RemoteReceiverDumper; + +template class ABBWelcomeAction : public RemoteTransmitterActionBase { + TEMPLATABLE_VALUE(uint32_t, source_address) + TEMPLATABLE_VALUE(uint32_t, destination_address) + TEMPLATABLE_VALUE(bool, retransmission) + TEMPLATABLE_VALUE(bool, three_byte_address) + TEMPLATABLE_VALUE(uint8_t, message_type) + TEMPLATABLE_VALUE(uint8_t, message_id) + TEMPLATABLE_VALUE(bool, auto_message_id) + void set_data_static(std::vector data) { data_static_ = std::move(data); } + void set_data_template(std::function(Ts...)> func) { + this->data_func_ = func; + has_data_func_ = true; + } + void encode(RemoteTransmitData *dst, Ts... x) override { + ABBWelcomeData data; + data.set_three_byte_address(this->three_byte_address_.value(x...)); + data.set_source_address(this->source_address_.value(x...)); + data.set_destination_address(this->destination_address_.value(x...)); + data.set_retransmission(this->retransmission_.value(x...)); + data.set_message_type(this->message_type_.value(x...)); + data.set_message_id(this->message_id_.value(x...)); + data.auto_message_id = this->auto_message_id_.value(x...); + if (has_data_func_) { + data.set_data(this->data_func_(x...)); + } else { + data.set_data(this->data_static_); + } + data.finalize(); + ABBWelcomeProtocol().encode(dst, data); + } + + protected: + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; + bool has_data_func_{false}; +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/aeha_protocol.cpp b/esphome/components/remote_base/aeha_protocol.cpp index ee1616ed6d..04fe731817 100644 --- a/esphome/components/remote_base/aeha_protocol.cpp +++ b/esphome/components/remote_base/aeha_protocol.cpp @@ -16,7 +16,6 @@ static const uint16_t BIT_ZERO_LOW_US = BITWISE; static const uint16_t TRAILER = BITWISE; void AEHAProtocol::encode(RemoteTransmitData *dst, const AEHAData &data) { - dst->set_carrier_frequency(38000); dst->reserve(2 + 32 + (data.data.size() * 2) + 1); dst->item(HEADER_HIGH_US, HEADER_LOW_US); @@ -96,7 +95,7 @@ std::string AEHAProtocol::format_data_(const std::vector &data) { void AEHAProtocol::dump(const AEHAData &data) { auto data_str = format_data_(data.data); - ESP_LOGD(TAG, "Received AEHA: address=0x%04X, data=[%s]", data.address, data_str.c_str()); + ESP_LOGI(TAG, "Received AEHA: address=0x%04X, data=[%s]", data.address, data_str.c_str()); } } // namespace remote_base diff --git a/esphome/components/remote_base/aeha_protocol.h b/esphome/components/remote_base/aeha_protocol.h index c41f3f8df1..51718eefcb 100644 --- a/esphome/components/remote_base/aeha_protocol.h +++ b/esphome/components/remote_base/aeha_protocol.h @@ -30,12 +30,14 @@ template class AEHAAction : public RemoteTransmitterActionBase, data) + TEMPLATABLE_VALUE(uint32_t, carrier_frequency); void set_data(const std::vector &data) { data_ = data; } void encode(RemoteTransmitData *dst, Ts... x) override { AEHAData data{}; data.address = this->address_.value(x...); data.data = this->data_.value(x...); + dst->set_carrier_frequency(this->carrier_frequency_.value(x...)); AEHAProtocol().encode(dst, data); } }; diff --git a/esphome/components/remote_base/byronsx_protocol.cpp b/esphome/components/remote_base/byronsx_protocol.cpp new file mode 100644 index 0000000000..6bfa4b7ff9 --- /dev/null +++ b/esphome/components/remote_base/byronsx_protocol.cpp @@ -0,0 +1,139 @@ +#include "byronsx_protocol.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.byronsx"; + +static const uint32_t BIT_TIME_US = 333; +static const uint8_t NBITS_ADDRESS = 8; +static const uint8_t NBITS_COMMAND = 4; +static const uint8_t NBITS_START_BIT = 1; +static const uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; + +/* +ByronSX Protocol +Each transmitted packet appears to consist of thirteen bits of PWM encoded +data. Each bit period of aprox 1ms consists of a transmitter OFF period +followed by a transmitter ON period. The 'on' and 'off' periods are either +short (approx 332us) or long (664us). + +A short 'off' followed by a long 'on' represents a '1' bit. +A long 'off' followed by a short 'on' represents a '0' bit. + +A the beginning of each packet is and initial 'off' period of approx 5.6ms +followed by a short 'on'. + +The data payload consists of twelve bits which appear to be an eight bit +address floowied by a 4 bit chime number. + +SAAAAAAAACCCC + +Whese: +S = the initial short start pulse +A = The eight address bits +C - The four chime bits + +-------------------- + +I have also used 'RFLink' software (RLink Firmware Version: 1.1 Revision: 48) +to capture these packets, eg: + +20;19;Byron;ID=004f;SWITCH=02;CMD=ON;CHIME=02; + +This module transmits and interprets packets in the same way as RFLink. + +marshn + +*/ + +void ByronSXProtocol::encode(RemoteTransmitData *dst, const ByronSXData &data) { + uint32_t out_data = 0x0; + + ESP_LOGD(TAG, "Send ByronSX: address=%04x command=%03x", data.address, data.command); + + out_data = data.address; + out_data <<= NBITS_COMMAND; + out_data |= data.command; + + ESP_LOGV(TAG, "Send ByronSX: out_data %03" PRIx32, out_data); + + // Initial Mark start bit + dst->mark(1 * BIT_TIME_US); + + for (uint32_t mask = 1UL << (NBITS_DATA - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->space(2 * BIT_TIME_US); + dst->mark(1 * BIT_TIME_US); + } else { + dst->space(1 * BIT_TIME_US); + dst->mark(2 * BIT_TIME_US); + } + } + // final space at end of packet + dst->space(17 * BIT_TIME_US); +} + +optional ByronSXProtocol::decode(RemoteReceiveData src) { + ByronSXData out{ + .address = 0, + .command = 0, + }; + + if (src.size() != (NBITS_DATA + NBITS_START_BIT) * 2) { + return {}; + } + + // Skip start bit + if (!src.expect_mark(BIT_TIME_US)) { + return {}; + } + + ESP_LOGVV(TAG, + "%3" PRId32 ": %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 + " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 + " %" PRId32 " %" PRId32 " %" PRId32, + src.size(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), + src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), + src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); + + ESP_LOGVV(TAG, " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32, src.peek(20), + src.peek(21), src.peek(22), src.peek(23), src.peek(24), src.peek(25)); + + // Read data bits + uint32_t out_data = 0; + int8_t bit = NBITS_DATA; + while (--bit >= 0) { + if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { + out_data |= 1 << bit; + } else if (src.expect_space(BIT_TIME_US) && src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 0 << bit; + } else { + ESP_LOGV(TAG, "Decode ByronSX: Fail 2, %2d %08" PRIx32, bit, out_data); + return {}; + } + ESP_LOGVV(TAG, "Decode ByronSX: Data, %2d %08" PRIx32, bit, out_data); + } + + // last bit followed by a long space + if (!src.peek_space_at_least(BIT_TIME_US)) { + ESP_LOGV(TAG, "Decode ByronSX: Fail 4 "); + return {}; + } + + out.command = (uint8_t) (out_data & 0xF); + out_data >>= NBITS_COMMAND; + out.address = (uint16_t) (out_data & 0xFF); + + return out; +} + +void ByronSXProtocol::dump(const ByronSXData &data) { + ESP_LOGD(TAG, "Received ByronSX: address=0x%08X, command=0x%02x", data.address, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/byronsx_protocol.h b/esphome/components/remote_base/byronsx_protocol.h new file mode 100644 index 0000000000..5d23237ab1 --- /dev/null +++ b/esphome/components/remote_base/byronsx_protocol.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct ByronSXData { + uint8_t address; + uint8_t command; + + bool operator==(const ByronSXData &rhs) const { + // Treat 0x10 as a special, wildcard command/chime + // This allows us to match on just the address if wanted. + if (address != rhs.address) { + return false; + } + return (rhs.command == 0x10 || command == rhs.command); + } +}; + +class ByronSXProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const ByronSXData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const ByronSXData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(ByronSX) + +template class ByronSXAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint8_t, address) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) override { + ByronSXData data{}; + data.address = this->address_.value(x...); + data.command = this->command_.value(x...); + ByronSXProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp index 1ea47750fd..bee3d57fd8 100644 --- a/esphome/components/remote_base/canalsat_protocol.cpp +++ b/esphome/components/remote_base/canalsat_protocol.cpp @@ -96,10 +96,10 @@ optional CanalSatBaseProtocol::decode(RemoteReceiveData src) { void CanalSatBaseProtocol::dump(const CanalSatData &data) { if (this->tag_ == CANALSATLD_TAG) { - ESP_LOGD(this->tag_, "Received CanalSatLD: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device, + ESP_LOGI(this->tag_, "Received CanalSatLD: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device, data.address, data.command, data.repeat); } else { - ESP_LOGD(this->tag_, "Received CanalSat: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device, + ESP_LOGI(this->tag_, "Received CanalSat: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device, data.address, data.command, data.repeat); } } diff --git a/esphome/components/remote_base/coolix_protocol.cpp b/esphome/components/remote_base/coolix_protocol.cpp index 252b6f0e91..21a9f598b7 100644 --- a/esphome/components/remote_base/coolix_protocol.cpp +++ b/esphome/components/remote_base/coolix_protocol.cpp @@ -15,11 +15,21 @@ static const int32_t BIT_ZERO_SPACE_US = 1 * TICK_US; static const int32_t FOOTER_MARK_US = 1 * TICK_US; static const int32_t FOOTER_SPACE_US = 10 * TICK_US; -static void encode_data(RemoteTransmitData *dst, const CoolixData &src) { - // Break data into bytes, starting at the Most Significant - // Byte. Each byte then being sent normal, then followed inverted. +bool CoolixData::operator==(const CoolixData &other) const { + if (this->first == 0) + return this->second == other.first || this->second == other.second; + if (other.first == 0) + return other.second == this->first || other.second == this->second; + return this->first == other.first && this->second == other.second; +} + +static void encode_frame(RemoteTransmitData *dst, const uint32_t &src) { + // Append header + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + // Break data into bytes, starting at the Most Significant + // Byte. Each byte then being sent normal, then followed inverted. for (unsigned shift = 16;; shift -= 8) { - // Grab a bytes worth of data. + // Grab a bytes worth of data const uint8_t byte = src >> shift; // Normal for (uint8_t mask = 1 << 7; mask; mask >>= 1) @@ -27,27 +37,33 @@ static void encode_data(RemoteTransmitData *dst, const CoolixData &src) { // Inverted for (uint8_t mask = 1 << 7; mask; mask >>= 1) dst->item(BIT_MARK_US, (byte & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US); - // Data end - if (shift == 0) + // End of frame + if (shift == 0) { + // Append footer + dst->mark(FOOTER_MARK_US); break; + } } } void CoolixProtocol::encode(RemoteTransmitData *dst, const CoolixData &data) { dst->set_carrier_frequency(38000); - dst->reserve(2 + 2 * 48 + 2 + 2 + 2 * 48 + 1); - dst->item(HEADER_MARK_US, HEADER_SPACE_US); - encode_data(dst, data); - dst->item(FOOTER_MARK_US, FOOTER_SPACE_US); - dst->item(HEADER_MARK_US, HEADER_SPACE_US); - encode_data(dst, data); - dst->mark(FOOTER_MARK_US); + dst->reserve(100 + 100 * data.has_second()); + encode_frame(dst, data.first); + if (data.has_second()) { + dst->space(FOOTER_SPACE_US); + encode_frame(dst, data.second); + } } -static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { +static bool decode_frame(RemoteReceiveData &src, uint32_t &dst) { + // Checking for header + if (!src.expect_item(HEADER_MARK_US, HEADER_SPACE_US)) + return false; + // Reading data uint32_t data = 0; for (unsigned n = 3;; data <<= 8) { - // Read byte + // Reading byte for (uint32_t mask = 1 << 7; mask; mask >>= 1) { if (!src.expect_mark(BIT_MARK_US)) return false; @@ -57,13 +73,16 @@ static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { return false; } } - // Check for inverse byte + // Checking for inverted byte for (uint32_t mask = 1 << 7; mask; mask >>= 1) { if (!src.expect_item(BIT_MARK_US, (data & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) return false; } - // Checking the end of reading + // End of frame if (--n == 0) { + // Checking for footer + if (!src.expect_mark(FOOTER_MARK_US)) + return false; dst = data; return true; } @@ -71,15 +90,24 @@ static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { } optional CoolixProtocol::decode(RemoteReceiveData data) { - CoolixData first, second; - if (data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && decode_data(data, first) && - data.expect_item(FOOTER_MARK_US, FOOTER_SPACE_US) && data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && - decode_data(data, second) && data.expect_mark(FOOTER_MARK_US) && first == second) - return first; - return {}; + CoolixData result; + const auto size = data.size(); + if ((size != 200 && size != 100) || !decode_frame(data, result.first)) + return {}; + if (size == 100 || !data.expect_space(FOOTER_SPACE_US) || !decode_frame(data, result.second)) + result.second = 0; + return result; } -void CoolixProtocol::dump(const CoolixData &data) { ESP_LOGD(TAG, "Received Coolix: 0x%06X", data); } +void CoolixProtocol::dump(const CoolixData &data) { + if (data.is_strict()) { + ESP_LOGI(TAG, "Received Coolix: 0x%06" PRIX32, data.first); + } else if (data.has_second()) { + ESP_LOGI(TAG, "Received unstrict Coolix: [0x%06" PRIX32 ", 0x%06" PRIX32 "]", data.first, data.second); + } else { + ESP_LOGI(TAG, "Received unstrict Coolix: [0x%06" PRIX32 "]", data.first); + } +} } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/coolix_protocol.h b/esphome/components/remote_base/coolix_protocol.h index 9ce3eabb0e..b66415ff70 100644 --- a/esphome/components/remote_base/coolix_protocol.h +++ b/esphome/components/remote_base/coolix_protocol.h @@ -4,10 +4,21 @@ #include "esphome/core/helpers.h" #include "remote_base.h" +#include + namespace esphome { namespace remote_base { -using CoolixData = uint32_t; +struct CoolixData { + CoolixData() {} + CoolixData(uint32_t a) : first(a), second(a) {} + CoolixData(uint32_t a, uint32_t b) : first(a), second(b) {} + bool operator==(const CoolixData &other) const; + bool is_strict() const { return this->first == this->second; } + bool has_second() const { return this->second != 0; } + uint32_t first; + uint32_t second; +}; class CoolixProtocol : public RemoteProtocol { public: @@ -19,10 +30,10 @@ class CoolixProtocol : public RemoteProtocol { DECLARE_REMOTE_PROTOCOL(Coolix) template class CoolixAction : public RemoteTransmitterActionBase { - TEMPLATABLE_VALUE(CoolixData, data) + TEMPLATABLE_VALUE(uint32_t, first) + TEMPLATABLE_VALUE(uint32_t, second) void encode(RemoteTransmitData *dst, Ts... x) override { - CoolixData data = this->data_.value(x...); - CoolixProtocol().encode(dst, data); + CoolixProtocol().encode(dst, {this->first_.value(x...), this->second_.value(x...)}); } }; diff --git a/esphome/components/remote_base/dish_protocol.cpp b/esphome/components/remote_base/dish_protocol.cpp index 47bfdc5c58..754b6c3b12 100644 --- a/esphome/components/remote_base/dish_protocol.cpp +++ b/esphome/components/remote_base/dish_protocol.cpp @@ -87,7 +87,7 @@ optional DishProtocol::decode(RemoteReceiveData src) { } void DishProtocol::dump(const DishData &data) { - ESP_LOGD(TAG, "Received Dish: address=0x%02X, command=0x%02X", data.address, data.command); + ESP_LOGI(TAG, "Received Dish: address=0x%02X, command=0x%02X", data.address, data.command); } } // namespace remote_base diff --git a/esphome/components/remote_base/dooya_protocol.cpp b/esphome/components/remote_base/dooya_protocol.cpp new file mode 100644 index 0000000000..04c5fef8f3 --- /dev/null +++ b/esphome/components/remote_base/dooya_protocol.cpp @@ -0,0 +1,120 @@ +#include "dooya_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.dooya"; + +static const uint32_t HEADER_HIGH_US = 5000; +static const uint32_t HEADER_LOW_US = 1500; +static const uint32_t BIT_ZERO_HIGH_US = 350; +static const uint32_t BIT_ZERO_LOW_US = 750; +static const uint32_t BIT_ONE_HIGH_US = 750; +static const uint32_t BIT_ONE_LOW_US = 350; + +void DooyaProtocol::encode(RemoteTransmitData *dst, const DooyaData &data) { + dst->set_carrier_frequency(0); + dst->reserve(2 + 40 * 2u); + + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + + for (uint32_t mask = 1UL << (23); mask != 0; mask >>= 1) { + if (data.id & mask) { + dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US); + } + } + + for (uint32_t mask = 1UL << (7); mask != 0; mask >>= 1) { + if (data.channel & mask) { + dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US); + } + } + + for (uint32_t mask = 1UL << (3); mask != 0; mask >>= 1) { + if (data.button & mask) { + dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US); + } + } + + for (uint32_t mask = 1UL << (3); mask != 0; mask >>= 1) { + if (data.check & mask) { + dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US); + } + } +} +optional DooyaProtocol::decode(RemoteReceiveData src) { + DooyaData out{ + .id = 0, + .channel = 0, + .button = 0, + .check = 0, + }; + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + for (uint8_t i = 0; i < 24; i++) { + if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) { + out.id = (out.id << 1) | 1; + } else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) { + out.id = (out.id << 1) | 0; + } else { + return {}; + } + } + + for (uint8_t i = 0; i < 8; i++) { + if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) { + out.channel = (out.channel << 1) | 1; + } else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) { + out.channel = (out.channel << 1) | 0; + } else { + return {}; + } + } + + for (uint8_t i = 0; i < 4; i++) { + if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) { + out.button = (out.button << 1) | 1; + } else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) { + out.button = (out.button << 1) | 0; + } else { + return {}; + } + } + + for (uint8_t i = 0; i < 3; i++) { + if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) { + out.check = (out.check << 1) | 1; + } else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) { + out.check = (out.check << 1) | 0; + } else { + return {}; + } + } + // Last bit is not received properly but can be decoded + if (src.expect_mark(BIT_ONE_HIGH_US)) { + out.check = (out.check << 1) | 1; + } else if (src.expect_mark(BIT_ZERO_HIGH_US)) { + out.check = (out.check << 1) | 0; + } else { + return {}; + } + + return out; +} +void DooyaProtocol::dump(const DooyaData &data) { + ESP_LOGI(TAG, "Received Dooya: id=0x%08" PRIX32 ", channel=%d, button=%d, check=%d", data.id, data.channel, + data.button, data.check); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/dooya_protocol.h b/esphome/components/remote_base/dooya_protocol.h new file mode 100644 index 0000000000..9d17ad5d5e --- /dev/null +++ b/esphome/components/remote_base/dooya_protocol.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +#include + +namespace esphome { +namespace remote_base { + +struct DooyaData { + uint32_t id; + uint8_t channel; + uint8_t button; + uint8_t check; + + bool operator==(const DooyaData &rhs) const { + return id == rhs.id && channel == rhs.channel && button == rhs.button && check == rhs.check; + } +}; + +class DooyaProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const DooyaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const DooyaData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Dooya) + +template class DooyaAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, id) + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint8_t, button) + TEMPLATABLE_VALUE(uint8_t, check) + + void encode(RemoteTransmitData *dst, Ts... x) override { + DooyaData data{}; + data.id = this->id_.value(x...); + data.channel = this->channel_.value(x...); + data.button = this->button_.value(x...); + data.check = this->check_.value(x...); + DooyaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index f5eae49058..da2e985af0 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -1,6 +1,8 @@ #include "drayton_protocol.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace remote_base { @@ -13,7 +15,8 @@ static const uint8_t NBITS_SYNC = 4; static const uint8_t NBITS_ADDRESS = 16; static const uint8_t NBITS_CHANNEL = 5; static const uint8_t NBITS_COMMAND = 7; -static const uint8_t NBITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; +static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; +static const uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); static const uint8_t CMD_ON = 0x41; static const uint8_t CMD_OFF = 0x02; @@ -114,9 +117,9 @@ void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) { out_data <<= NBITS_CHANNEL; out_data |= data.channel; - ESP_LOGV(TAG, "Send Drayton: out_data %08x", out_data); + ESP_LOGV(TAG, "Send Drayton: out_data %08" PRIx32, out_data); - for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { + for (uint32_t mask = 1UL << (NDATABITS - 1); mask != 0; mask >>= 1) { if (out_data & mask) { dst->mark(BIT_TIME_US); dst->space(BIT_TIME_US); @@ -134,78 +137,102 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { .command = 0, }; - if (src.size() < 45) { - return {}; - } + while (src.size() - src.get_index() >= MIN_RX_SRC) { + ESP_LOGVV(TAG, + "Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 + " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 + " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "", + src.size() - src.get_index(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), + src.peek(5), src.peek(6), src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), + src.peek(13), src.peek(14), src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); - ESP_LOGVV(TAG, "Decode Drayton: %d, %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d", src.size(), - src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), src.peek(7), - src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), - src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); + // If first preamble item is a space, skip it + if (src.peek_space_at_least(1)) { + src.advance(1); + } - // If first preamble item is a space, skip it - if (src.peek_space_at_least(1)) { - src.advance(1); - } + // Look for sync pulse, after. If sucessful index points to space of sync symbol + while (src.size() - src.get_index() >= MIN_RX_SRC) { + ESP_LOGVV(TAG, "Decode Drayton: sync search %" PRIu32 ", %" PRId32 " %" PRId32, src.size() - src.get_index(), + src.peek(), src.peek(1)); + if (src.peek_mark(2 * BIT_TIME_US) && + (src.peek_space(2 * BIT_TIME_US, 1) || src.peek_space(3 * BIT_TIME_US, 1))) { + src.advance(1); + ESP_LOGVV(TAG, "Decode Drayton: Found SYNC, - %" PRIu32, src.get_index()); + break; + } else { + src.advance(2); + } + } - // Look for sync pulse, after. If sucessful index points to space of sync symbol - for (uint16_t preamble = 0; preamble <= NBITS_PREAMBLE * 2; preamble += 2) { - ESP_LOGVV(TAG, "Decode Drayton: preamble %d %d %d", preamble, src.peek(preamble), src.peek(preamble + 1)); - if (src.peek_mark(2 * BIT_TIME_US, preamble) && - (src.peek_space(2 * BIT_TIME_US, preamble + 1) || src.peek_space(3 * BIT_TIME_US, preamble + 1))) { - src.advance(preamble + 1); + // No point continuing if not enough samples remaining to complete a packet + if (src.size() - src.get_index() < NDATABITS) { + ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index()); break; } - } - // Read data. Index points to space of sync symbol - // Extract first bit - // Checks next bit to leave index pointing correctly - uint32_t out_data = 0; - uint8_t bit = NBITS_ADDRESS + NBITS_COMMAND + NBITS_CHANNEL - 1; - if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { - out_data |= 0 << bit; - } else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) && - (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { - out_data |= 1 << bit; - } else { - ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %d", src.get_index()); - return {}; - } - - // Before/after each bit is read the index points to the transition at the start of the bit period or, - // if there is no transition at the start of the bit period, then the transition in the middle of - // the previous bit period. - while (--bit >= 1) { - ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); - if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && - (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + // Read data. Index points to space of sync symbol + // Extract first bit + // Checks next bit to leave index pointing correctly + uint32_t out_data = 0; + uint8_t bit = NDATABITS - 1; + ESP_LOGVV(TAG, "Decode Drayton: first bit %" PRId32 " %" PRId32 ", %" PRId32, src.peek(0), src.peek(1), + src.peek(2)); + if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { out_data |= 0 << bit; - } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + } else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) && (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { out_data |= 1 << bit; } else { - ESP_LOGVV(TAG, "Decode Drayton: Fail 2, %2d %08x", bit, out_data); - return {}; + ESP_LOGV(TAG, "Decode Drayton: Fail 2, - %" PRId32 " %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), + src.peek(1)); + continue; } - } - if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { - out_data |= 0; - } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { - out_data |= 1; - } - ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); - out.channel = (uint8_t) (out_data & 0x1F); - out_data >>= NBITS_CHANNEL; - out.command = (uint8_t) (out_data & 0x7F); - out_data >>= NBITS_COMMAND; - out.address = (uint16_t) (out_data & 0xFFFF); + // Before/after each bit is read the index points to the transition at the start of the bit period or, + // if there is no transition at the start of the bit period, then the transition in the middle of + // the previous bit period. + while (--bit >= 1) { + ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data); + if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; + } else { + break; + } + } - return out; + if (bit > 0) { + ESP_LOGVV(TAG, "Decode Drayton: Fail 3, %" PRId32 " %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), + src.peek(1)); + continue; + } + + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } else { + continue; + } + + ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data); + + out.channel = (uint8_t) (out_data & 0x1F); + out_data >>= NBITS_CHANNEL; + out.command = (uint8_t) (out_data & 0x7F); + out_data >>= NBITS_COMMAND; + out.address = (uint16_t) (out_data & 0xFFFF); + + return out; + } + return {}; } void DraytonProtocol::dump(const DraytonData &data) { - ESP_LOGD(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address, + ESP_LOGI(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address, ((data.address << 1) & 0xffff), data.channel, data.command); } diff --git a/esphome/components/remote_base/drayton_protocol.h b/esphome/components/remote_base/drayton_protocol.h index f468e7b57e..75213b9186 100644 --- a/esphome/components/remote_base/drayton_protocol.h +++ b/esphome/components/remote_base/drayton_protocol.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/haier_protocol.cpp b/esphome/components/remote_base/haier_protocol.cpp new file mode 100644 index 0000000000..ec5cb5775c --- /dev/null +++ b/esphome/components/remote_base/haier_protocol.cpp @@ -0,0 +1,84 @@ +#include "haier_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.haier"; + +constexpr uint32_t HEADER_LOW_US = 3100; +constexpr uint32_t HEADER_HIGH_US = 4400; +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; + +void HaierProtocol::encode_byte_(RemoteTransmitData *dst, uint8_t item) { + for (uint8_t mask = 1 << 7; mask != 0; mask >>= 1) { + if (item & mask) { + dst->space(BIT_ONE_SPACE_US); + } else { + dst->space(BIT_ZERO_SPACE_US); + } + dst->mark(BIT_MARK_US); + } +} + +void HaierProtocol::encode(RemoteTransmitData *dst, const HaierData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(5 + ((data.data.size() + 1) * 2)); + dst->mark(HEADER_LOW_US); + dst->space(HEADER_LOW_US); + dst->mark(HEADER_LOW_US); + dst->space(HEADER_HIGH_US); + dst->mark(BIT_MARK_US); + uint8_t checksum = 0; + for (uint8_t item : data.data) { + this->encode_byte_(dst, item); + checksum += item; + } + this->encode_byte_(dst, checksum); +} + +optional HaierProtocol::decode(RemoteReceiveData src) { + if (!src.expect_item(HEADER_LOW_US, HEADER_LOW_US) || !src.expect_item(HEADER_LOW_US, HEADER_HIGH_US)) { + return {}; + } + if (!src.expect_mark(BIT_MARK_US)) { + return {}; + } + size_t size = src.size() - src.get_index() - 1; + if (size < HAIER_IR_PACKET_BIT_SIZE * 2) + return {}; + size = HAIER_IR_PACKET_BIT_SIZE * 2; + uint8_t checksum = 0; + HaierData out; + while (size > 0) { + uint8_t data = 0; + for (uint8_t mask = 0x80; mask != 0; mask >>= 1) { + if (src.expect_space(BIT_ONE_SPACE_US)) { + data |= mask; + } else if (!src.expect_space(BIT_ZERO_SPACE_US)) { + return {}; + } + if (!src.expect_mark(BIT_MARK_US)) { + return {}; + } + size -= 2; + } + if (size > 0) { + checksum += data; + out.data.push_back(data); + } else if (checksum != data) { + return {}; + } + } + return out; +} + +void HaierProtocol::dump(const HaierData &data) { + ESP_LOGI(TAG, "Received Haier: %s", format_hex_pretty(data.data).c_str()); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/haier_protocol.h b/esphome/components/remote_base/haier_protocol.h new file mode 100644 index 0000000000..7a4ee640e8 --- /dev/null +++ b/esphome/components/remote_base/haier_protocol.h @@ -0,0 +1,39 @@ +#pragma once + +#include "remote_base.h" +#include + +namespace esphome { +namespace remote_base { + +struct HaierData { + std::vector data; + + bool operator==(const HaierData &rhs) const { return data == rhs.data; } +}; + +class HaierProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const HaierData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const HaierData &data) override; + + protected: + void encode_byte_(RemoteTransmitData *dst, uint8_t item); +}; + +DECLARE_REMOTE_PROTOCOL(Haier) + +template class HaierAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(std::vector, code) + + void encode(RemoteTransmitData *dst, Ts... x) override { + HaierData data{}; + data.data = this->code_.value(x...); + HaierProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/jvc_protocol.cpp b/esphome/components/remote_base/jvc_protocol.cpp index 169b1d00bf..ca423b61e6 100644 --- a/esphome/components/remote_base/jvc_protocol.cpp +++ b/esphome/components/remote_base/jvc_protocol.cpp @@ -46,7 +46,7 @@ optional JVCProtocol::decode(RemoteReceiveData src) { } return out; } -void JVCProtocol::dump(const JVCData &data) { ESP_LOGD(TAG, "Received JVC: data=0x%04X", data.data); } +void JVCProtocol::dump(const JVCData &data) { ESP_LOGI(TAG, "Received JVC: data=0x%04" PRIX32, data.data); } } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index fc40a6a874..a17e593ad2 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -2,6 +2,8 @@ #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/keeloq_protocol.cpp b/esphome/components/remote_base/keeloq_protocol.cpp new file mode 100644 index 0000000000..72540c37f1 --- /dev/null +++ b/esphome/components/remote_base/keeloq_protocol.cpp @@ -0,0 +1,194 @@ +#include "keeloq_protocol.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.keeloq"; + +static const uint32_t BIT_TIME_US = 380; +static const uint8_t NBITS_PREAMBLE = 12; +static const uint8_t NBITS_REPEAT = 1; +static const uint8_t NBITS_VLOW = 1; +static const uint8_t NBITS_SERIAL = 28; +static const uint8_t NBITS_BUTTONS = 4; +static const uint8_t NBITS_DISC = 12; +static const uint8_t NBITS_SYNC_CNT = 16; + +static const uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; +static const uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; +static const uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; + +/* +KeeLoq Protocol + +Coded using information from datasheet for Microchip HCS301 KeeLow Code Hopping Encoder + +Encoder - Hopping code is generated at random. + +Decoder - Hopping code is ignored and not checked when received. Serial number of +transmitter and nutton command is decoded. + +*/ + +void KeeloqProtocol::encode(RemoteTransmitData *dst, const KeeloqData &data) { + uint32_t out_data = 0x0; + + ESP_LOGD(TAG, "Send Keeloq: address=%07" PRIx32 " command=%03x encrypted=%08" PRIx32, data.address, data.command, + data.encrypted); + ESP_LOGV(TAG, "Send Keeloq: data bits (%d + %d)", NBITS_ENCRYPTED_DATA, NBITS_FIXED_DATA); + + // Preamble = '01' x 12 + for (uint8_t cnt = NBITS_PREAMBLE; cnt; cnt--) { + dst->space(BIT_TIME_US); + dst->mark(BIT_TIME_US); + } + + // Header = 10 bit space + dst->space(10 * BIT_TIME_US); + + // Encrypted field + out_data = data.encrypted; + + ESP_LOGV(TAG, "Send Keeloq: Encrypted data %04" PRIx32, out_data); + + for (uint32_t mask = 1, cnt = 0; cnt < NBITS_ENCRYPTED_DATA; cnt++, mask <<= 1) { + if (out_data & mask) { + dst->mark(1 * BIT_TIME_US); + dst->space(2 * BIT_TIME_US); + } else { + dst->mark(2 * BIT_TIME_US); + dst->space(1 * BIT_TIME_US); + } + } + + // first 32 bits of fixed portion + out_data = (data.command & 0x0f); + out_data <<= NBITS_SERIAL; + out_data |= data.address; + ESP_LOGV(TAG, "Send Keeloq: Fixed data %04" PRIx32, out_data); + + for (uint32_t mask = 1, cnt = 0; cnt < (NBITS_FIXED_DATA - 2); cnt++, mask <<= 1) { + if (out_data & mask) { + dst->mark(1 * BIT_TIME_US); + dst->space(2 * BIT_TIME_US); + } else { + dst->mark(2 * BIT_TIME_US); + dst->space(1 * BIT_TIME_US); + } + } + + // low battery flag + if (data.vlow) { + dst->mark(1 * BIT_TIME_US); + dst->space(2 * BIT_TIME_US); + } else { + dst->mark(2 * BIT_TIME_US); + dst->space(1 * BIT_TIME_US); + } + + // repeat flag - always sent as a '1' + dst->mark(1 * BIT_TIME_US); + dst->space(2 * BIT_TIME_US); + + // Guard time at end of packet + dst->space(39 * BIT_TIME_US); +} + +optional KeeloqProtocol::decode(RemoteReceiveData src) { + KeeloqData out{ + .encrypted = 0, + .address = 0, + .command = 0, + .repeat = false, + .vlow = false, + + }; + + if (src.size() != (NBITS_PREAMBLE + NBITS_DATA) * 2) { + return {}; + } + + ESP_LOGVV(TAG, + "%2" PRId32 ": %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 + " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 + " %" PRId32 " %" PRId32 " %" PRId32, + src.size(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), + src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), + src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); + + // Check preamble bits + int8_t bit = NBITS_PREAMBLE - 1; + while (--bit >= 0) { + if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(BIT_TIME_US)) { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 1, %d %" PRId32, bit + 1, src.peek()); + return {}; + } + } + if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(10 * BIT_TIME_US)) { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 1, %d %" PRId32, bit + 1, src.peek()); + return {}; + } + + // Read encrypted bits + uint32_t out_data = 0; + for (bit = 0; bit < NBITS_ENCRYPTED_DATA; bit++) { + if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + out_data |= 0 << bit; + } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) { + out_data |= 1 << bit; + } else { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 2, %" PRIu32 " %" PRId32, src.get_index(), src.peek()); + return {}; + } + } + ESP_LOGVV(TAG, "Decode KeeLoq: Data, %d %08" PRIx32, bit, out_data); + out.encrypted = out_data; + + // Read Serial Number and Button Status + out_data = 0; + for (bit = 0; bit < NBITS_SERIAL + NBITS_BUTTONS; bit++) { + if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + out_data |= 0 << bit; + } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) { + out_data |= 1 << bit; + } else { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 3, %" PRIu32 " %" PRId32, src.get_index(), src.peek()); + return {}; + } + } + ESP_LOGVV(TAG, "Decode KeeLoq: Data, %2d %08" PRIx32, bit, out_data); + out.command = (out_data >> 28) & 0xf; + out.address = out_data & 0xfffffff; + + // Read Vlow bit + if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + out.vlow = false; + } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) { + out.vlow = true; + } else { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 4, %" PRId32, src.peek()); + return {}; + } + + // Read Repeat bit + if (src.expect_mark(2 * BIT_TIME_US) && src.peek_space_at_least(BIT_TIME_US)) { + out.repeat = false; + } else if (src.expect_mark(BIT_TIME_US) && src.peek_space_at_least(2 * BIT_TIME_US)) { + out.repeat = true; + } else { + ESP_LOGV(TAG, "Decode KeeLoq: Fail 5, %" PRId32, src.peek()); + return {}; + } + + return out; +} + +void KeeloqProtocol::dump(const KeeloqData &data) { + ESP_LOGD(TAG, "Received Keeloq: address=0x%08" PRIx32 ", command=0x%02x", data.address, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/keeloq_protocol.h b/esphome/components/remote_base/keeloq_protocol.h new file mode 100644 index 0000000000..47125c151b --- /dev/null +++ b/esphome/components/remote_base/keeloq_protocol.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct KeeloqData { + uint32_t encrypted; // 32 bit encrypted field + uint32_t address; // 28 bit serial number + uint8_t command; // Button Status S2-S1-S0-S3 + bool repeat; // Repeated command bit + bool vlow; // Battery status bit + + bool operator==(const KeeloqData &rhs) const { + // Treat 0x10 as a special, wildcard button press + // This allows us to match on just the address if wanted. + if (address != rhs.address) { + return false; + } + return (rhs.command == 0x10 || command == rhs.command); + } +}; + +class KeeloqProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const KeeloqData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const KeeloqData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Keeloq) + +template class KeeloqAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, address) + TEMPLATABLE_VALUE(uint32_t, encrypted) + TEMPLATABLE_VALUE(uint8_t, command) + TEMPLATABLE_VALUE(bool, vlow) + + void encode(RemoteTransmitData *dst, Ts... x) override { + KeeloqData data{}; + data.address = this->address_.value(x...); + data.encrypted = this->encrypted_.value(x...); + data.command = this->command_.value(x...); + data.vlow = this->vlow_.value(x...); + KeeloqProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/lg_protocol.cpp b/esphome/components/remote_base/lg_protocol.cpp index 8040b0f3fc..d25c59d2b1 100644 --- a/esphome/components/remote_base/lg_protocol.cpp +++ b/esphome/components/remote_base/lg_protocol.cpp @@ -51,7 +51,7 @@ optional LGProtocol::decode(RemoteReceiveData src) { return out; } void LGProtocol::dump(const LGData &data) { - ESP_LOGD(TAG, "Received LG: data=0x%08X, nbits=%d", data.data, data.nbits); + ESP_LOGI(TAG, "Received LG: data=0x%08" PRIX32 ", nbits=%d", data.data, data.nbits); } } // namespace remote_base diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index 6267560443..e0039d033d 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/magiquest_protocol.cpp b/esphome/components/remote_base/magiquest_protocol.cpp index 20b40ef201..1cec58a55f 100644 --- a/esphome/components/remote_base/magiquest_protocol.cpp +++ b/esphome/components/remote_base/magiquest_protocol.cpp @@ -76,7 +76,7 @@ optional MagiQuestProtocol::decode(RemoteReceiveData src) { return data; } void MagiQuestProtocol::dump(const MagiQuestData &data) { - ESP_LOGD(TAG, "Received MagiQuest: wand_id=0x%08X, magnitude=0x%04X", data.wand_id, data.magnitude); + ESP_LOGI(TAG, "Received MagiQuest: wand_id=0x%08" PRIX32 ", magnitude=0x%04X", data.wand_id, data.magnitude); } } // namespace remote_base diff --git a/esphome/components/remote_base/magiquest_protocol.h b/esphome/components/remote_base/magiquest_protocol.h index 909be346d0..a531a9aee1 100644 --- a/esphome/components/remote_base/magiquest_protocol.h +++ b/esphome/components/remote_base/magiquest_protocol.h @@ -2,6 +2,8 @@ #include "remote_base.h" +#include + /* Based on protocol analysis from * https://arduino-irremote.github.io/Arduino-IRremote/ir__MagiQuest_8cpp_source.html */ diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp index f619d201bc..8006fe4048 100644 --- a/esphome/components/remote_base/midea_protocol.cpp +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -70,7 +70,7 @@ optional MideaProtocol::decode(RemoteReceiveData src) { return {}; } -void MideaProtocol::dump(const MideaData &data) { ESP_LOGD(TAG, "Received Midea: %s", data.to_string().c_str()); } +void MideaProtocol::dump(const MideaData &data) { ESP_LOGI(TAG, "Received Midea: %s", data.to_string().c_str()); } } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index a7f5636b06..94fb6f3d94 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -1,11 +1,11 @@ #pragma once +#include +#include + #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "remote_base.h" -#include -#include -#include namespace esphome { namespace remote_base { @@ -22,8 +22,6 @@ class MideaData { MideaData(const std::vector &data) { std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } - // Default copy constructor - MideaData(const MideaData &) = default; uint8_t *data() { return this->data_.data(); } const uint8_t *data() const { return this->data_.data(); } @@ -69,40 +67,16 @@ class MideaProtocol : public RemoteProtocol { void dump(const MideaData &data) override; }; -class MideaBinarySensor : public RemoteReceiverBinarySensorBase { - public: - bool matches(RemoteReceiveData src) override { - auto data = MideaProtocol().decode(src); - return data.has_value() && data.value() == this->data_; - } - void set_code(const std::vector &code) { this->data_ = code; } - - protected: - MideaData data_; -}; - -using MideaTrigger = RemoteReceiverTrigger; -using MideaDumper = RemoteReceiverDumper; +DECLARE_REMOTE_PROTOCOL(Midea) template class MideaAction : public RemoteTransmitterActionBase { TEMPLATABLE_VALUE(std::vector, code) - void set_code_static(std::vector code) { code_static_ = std::move(code); } - void set_code_template(std::function(Ts...)> func) { this->code_func_ = func; } void encode(RemoteTransmitData *dst, Ts... x) override { - MideaData data; - if (!this->code_static_.empty()) { - data = MideaData(this->code_static_); - } else { - data = MideaData(this->code_func_(x...)); - } + MideaData data(this->code_.value(x...)); data.finalize(); MideaProtocol().encode(dst, data); } - - protected: - std::function(Ts...)> code_func_{}; - std::vector code_static_{}; }; } // namespace remote_base diff --git a/esphome/components/remote_base/mirage_protocol.cpp b/esphome/components/remote_base/mirage_protocol.cpp new file mode 100644 index 0000000000..10d644a1cd --- /dev/null +++ b/esphome/components/remote_base/mirage_protocol.cpp @@ -0,0 +1,84 @@ +#include "mirage_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.mirage"; + +constexpr uint32_t HEADER_MARK_US = 8360; +constexpr uint32_t HEADER_SPACE_US = 4248; +constexpr uint32_t BIT_MARK_US = 554; +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; + +void MirageProtocol::encode(RemoteTransmitData *dst, const MirageData &data) { + ESP_LOGI(TAG, "Transive Mirage: %s", format_hex_pretty(data.data).c_str()); + dst->set_carrier_frequency(38000); + dst->reserve(5 + ((data.data.size() + 1) * 2)); + dst->mark(HEADER_MARK_US); + dst->space(HEADER_SPACE_US); + dst->mark(BIT_MARK_US); + uint8_t checksum = 0; + for (uint8_t item : data.data) { + this->encode_byte_(dst, item); + checksum += (item >> 4) + (item & 0xF); + } + this->encode_byte_(dst, checksum); +} + +void MirageProtocol::encode_byte_(RemoteTransmitData *dst, uint8_t item) { + for (uint8_t b = 0; b < 8; b++) { + if (item & (1UL << b)) { + dst->space(BIT_ONE_SPACE_US); + } else { + dst->space(BIT_ZERO_SPACE_US); + } + dst->mark(BIT_MARK_US); + } +} + +optional MirageProtocol::decode(RemoteReceiveData src) { + if (!src.expect_item(HEADER_MARK_US, HEADER_SPACE_US)) { + return {}; + } + if (!src.expect_mark(BIT_MARK_US)) { + return {}; + } + size_t size = src.size() - src.get_index() - 1; + if (size < MIRAGE_IR_PACKET_BIT_SIZE * 2) + return {}; + size = MIRAGE_IR_PACKET_BIT_SIZE * 2; + uint8_t checksum = 0; + MirageData out; + while (size > 0) { + uint8_t data = 0; + for (uint8_t b = 0; b < 8; b++) { + if (src.expect_space(BIT_ONE_SPACE_US)) { + data |= (1UL << b); + } else if (!src.expect_space(BIT_ZERO_SPACE_US)) { + return {}; + } + if (!src.expect_mark(BIT_MARK_US)) { + return {}; + } + size -= 2; + } + if (size > 0) { + checksum += (data >> 4) + (data & 0xF); + out.data.push_back(data); + } else if (checksum != data) { + return {}; + } + } + return out; +} + +void MirageProtocol::dump(const MirageData &data) { + ESP_LOGI(TAG, "Received Mirage: %s", format_hex_pretty(data.data).c_str()); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/mirage_protocol.h b/esphome/components/remote_base/mirage_protocol.h new file mode 100644 index 0000000000..4257f7fa00 --- /dev/null +++ b/esphome/components/remote_base/mirage_protocol.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct MirageData { + std::vector data; + + bool operator==(const MirageData &rhs) const { return data == rhs.data; } +}; + +class MirageProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const MirageData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const MirageData &data) override; + + protected: + void encode_byte_(RemoteTransmitData *dst, uint8_t item); +}; + +DECLARE_REMOTE_PROTOCOL(Mirage) + +template class MirageAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(std::vector, code) + + void encode(RemoteTransmitData *dst, Ts... x) override { + MirageData data{}; + data.data = this->code_.value(x...); + MirageProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index ee3fec5992..6ea9a8583c 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -13,10 +13,14 @@ static const uint32_t BIT_ONE_LOW_US = 1690; static const uint32_t BIT_ZERO_LOW_US = 560; void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { - dst->reserve(68); + ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, + data.command_repeats); + + dst->reserve(2 + 32 + 32 * data.command_repeats + 2); dst->set_carrier_frequency(38000); dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint16_t mask = 1; mask; mask <<= 1) { if (data.address & mask) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); @@ -25,11 +29,13 @@ void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { } } - for (uint16_t mask = 1; mask; mask <<= 1) { - if (data.command & mask) { - dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); - } else { - dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + for (uint16_t repeats = 0; repeats < data.command_repeats; repeats++) { + for (uint16_t mask = 1; mask; mask <<= 1) { + if (data.command & mask) { + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } } } @@ -39,6 +45,7 @@ optional NECProtocol::decode(RemoteReceiveData src) { NECData data{ .address = 0, .command = 0, + .command_repeats = 1, }; if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) return {}; @@ -63,11 +70,32 @@ optional NECProtocol::decode(RemoteReceiveData src) { } } + while (src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US) || src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + uint16_t command = 0; + for (uint16_t mask = 1; mask; mask <<= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + command |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + command &= ~mask; + } else { + return {}; + } + } + + // Make sure the extra/repeated data matches original command + if (command != data.command) { + return {}; + } + + data.command_repeats += 1; + } + src.expect_mark(BIT_HIGH_US); return data; } void NECProtocol::dump(const NECData &data) { - ESP_LOGD(TAG, "Received NEC: address=0x%04X, command=0x%04X", data.address, data.command); + ESP_LOGI(TAG, "Received NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, + data.command_repeats); } } // namespace remote_base diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 593a3efe17..71e1bccba8 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -8,6 +8,7 @@ namespace remote_base { struct NECData { uint16_t address; uint16_t command; + uint16_t command_repeats; bool operator==(const NECData &rhs) const { return address == rhs.address && command == rhs.command; } }; @@ -25,11 +26,13 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); data.command = this->command_.value(x...); + data.command_repeats = this->command_repeats_.value(x...); NECProtocol().encode(dst, data); } }; diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp index a0066ea5d4..862165714e 100644 --- a/esphome/components/remote_base/nexa_protocol.cpp +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -232,7 +232,7 @@ optional NexaProtocol::decode(RemoteReceiveData src) { } void NexaProtocol::dump(const NexaData &data) { - ESP_LOGD(TAG, "Received NEXA: device=0x%04X group=%d state=%d channel=%d level=%d", data.device, data.group, + ESP_LOGI(TAG, "Received NEXA: device=0x%04" PRIX32 " group=%d state=%d channel=%d level=%d", data.device, data.group, data.state, data.channel, data.level); } diff --git a/esphome/components/remote_base/nexa_protocol.h b/esphome/components/remote_base/nexa_protocol.h index f1ce380780..4d9443ce0e 100644 --- a/esphome/components/remote_base/nexa_protocol.h +++ b/esphome/components/remote_base/nexa_protocol.h @@ -2,6 +2,8 @@ #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/panasonic_protocol.cpp b/esphome/components/remote_base/panasonic_protocol.cpp index fe1060e935..d6cf1a160d 100644 --- a/esphome/components/remote_base/panasonic_protocol.cpp +++ b/esphome/components/remote_base/panasonic_protocol.cpp @@ -67,7 +67,7 @@ optional PanasonicProtocol::decode(RemoteReceiveData src) { return out; } void PanasonicProtocol::dump(const PanasonicData &data) { - ESP_LOGD(TAG, "Received Panasonic: address=0x%04X, command=0x%08X", data.address, data.command); + ESP_LOGI(TAG, "Received Panasonic: address=0x%04X, command=0x%08" PRIX32, data.address, data.command); } } // namespace remote_base diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index eae97a8a14..c81366138a 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp index 5c10eab48d..043565282d 100644 --- a/esphome/components/remote_base/pioneer_protocol.cpp +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -146,9 +146,9 @@ optional PioneerProtocol::decode(RemoteReceiveData src) { } void PioneerProtocol::dump(const PioneerData &data) { if (data.rc_code_2 == 0) { - ESP_LOGD(TAG, "Received Pioneer: rc_code_X=0x%04X", data.rc_code_1); + ESP_LOGI(TAG, "Received Pioneer: rc_code_X=0x%04X", data.rc_code_1); } else { - ESP_LOGD(TAG, "Received Pioneer: rc_code_1=0x%04X, rc_code_2=0x%04X", data.rc_code_1, data.rc_code_2); + ESP_LOGI(TAG, "Received Pioneer: rc_code_1=0x%04X, rc_code_2=0x%04X", data.rc_code_1, data.rc_code_2); } } diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 4951b12bb1..35fd782248 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -49,13 +49,13 @@ bool ProntoData::operator==(const ProntoData &rhs) const { for (std::vector::size_type i = 0; i < data1.size() - 1; ++i) { int diff = data2[i] - data1[i]; diff *= diff; - if (diff > 9) + if (rhs.delta == -1 && diff > 9) return false; total_diff += diff; } - return total_diff <= data1.size() * 3; + return total_diff <= (rhs.delta == -1 ? data1.size() * 3 : rhs.delta); } // DO NOT EXPORT from this file @@ -187,11 +187,10 @@ std::string ProntoProtocol::dump_duration_(uint32_t duration, uint16_t timebase, return dump_number_((duration + timebase / 2) / timebase, end); } -std::string ProntoProtocol::compensate_and_dump_sequence_(std::vector *data, uint16_t timebase) { +std::string ProntoProtocol::compensate_and_dump_sequence_(const RawTimings &data, uint16_t timebase) { std::string out; - for (std::vector::size_type i = 0; i < data->size() - 1; i++) { - int32_t t_length = data->at(i); + for (int32_t t_length : data) { uint32_t t_duration; if (t_length > 0) { // Mark @@ -202,9 +201,6 @@ std::string ProntoProtocol::compensate_and_dump_sequence_(std::vector * out += dump_duration_(t_duration, timebase); } - // append minimum gap - out += dump_duration_(PRONTO_DEFAULT_GAP, timebase, true); - return out; } @@ -212,32 +208,34 @@ optional ProntoProtocol::decode(RemoteReceiveData src) { ProntoData out; uint16_t frequency = 38000U; - std::vector *data = src.get_raw_data(); + auto &data = src.get_raw_data(); std::string prontodata; prontodata += dump_number_(frequency > 0 ? LEARNED_TOKEN : LEARNED_NON_MODULATED_TOKEN); prontodata += dump_number_(to_frequency_code_(frequency)); - prontodata += dump_number_((data->size() + 1) / 2); + prontodata += dump_number_((data.size() + 1) / 2); prontodata += dump_number_(0); uint16_t timebase = to_timebase_(frequency); prontodata += compensate_and_dump_sequence_(data, timebase); out.data = prontodata; + out.delta = -1; return out; } void ProntoProtocol::dump(const ProntoData &data) { - std::string first, rest; - if (data.data.size() < 230) { - first = data.data; - } else { - first = data.data.substr(0, 229); - rest = data.data.substr(230); - } - ESP_LOGD(TAG, "Received Pronto: data=%s", first.c_str()); - if (!rest.empty()) { - ESP_LOGD(TAG, "%s", rest.c_str()); + std::string rest; + + rest = data.data; + ESP_LOGI(TAG, "Received Pronto: data="); + while (true) { + ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str()); + if (rest.size() > 230) { + rest = rest.substr(230); + } else { + break; + } } } diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h index 8c491257d3..e600834d1a 100644 --- a/esphome/components/remote_base/pronto_protocol.h +++ b/esphome/components/remote_base/pronto_protocol.h @@ -12,6 +12,7 @@ std::vector encode_pronto(const std::string &str); struct ProntoData { std::string data; + int delta; bool operator==(const ProntoData &rhs) const; }; @@ -27,7 +28,7 @@ class ProntoProtocol : public RemoteProtocol { std::string dump_digit_(uint8_t x); std::string dump_number_(uint16_t number, bool end = false); std::string dump_duration_(uint32_t duration, uint16_t timebase, bool end = false); - std::string compensate_and_dump_sequence_(std::vector *data, uint16_t timebase); + std::string compensate_and_dump_sequence_(const RawTimings &data, uint16_t timebase); public: void encode(RemoteTransmitData *dst, const ProntoData &data) override; @@ -40,10 +41,12 @@ DECLARE_REMOTE_PROTOCOL(Pronto) template class ProntoAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(std::string, data) + TEMPLATABLE_VALUE(int, delta) void encode(RemoteTransmitData *dst, Ts... x) override { ProntoData data{}; data.data = this->data_.value(x...); + data.delta = this->delta_.value(x...); ProntoProtocol().encode(dst, data); } }; diff --git a/esphome/components/remote_base/raw_protocol.cpp b/esphome/components/remote_base/raw_protocol.cpp index 3446dcdbb8..bdeb935dc4 100644 --- a/esphome/components/remote_base/raw_protocol.cpp +++ b/esphome/components/remote_base/raw_protocol.cpp @@ -17,28 +17,28 @@ bool RawDumper::dump(RemoteReceiveData src) { int written; if (i + 1 < src.size() - 1) { - written = snprintf(buffer + buffer_offset, remaining_length, "%d, ", value); + written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value); } else { - written = snprintf(buffer + buffer_offset, remaining_length, "%d", value); + written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value); } if (written < 0 || written >= int(remaining_length)) { // write failed, flush... buffer[buffer_offset] = '\0'; - ESP_LOGD(TAG, "%s", buffer); + ESP_LOGI(TAG, "%s", buffer); buffer_offset = 0; written = sprintf(buffer, " "); if (i + 1 < src.size()) { - written += sprintf(buffer + written, "%d, ", value); + written += sprintf(buffer + written, "%" PRId32 ", ", value); } else { - written += sprintf(buffer + written, "%d", value); + written += sprintf(buffer + written, "%" PRId32, value); } } buffer_offset += written; } if (buffer_offset != 0) { - ESP_LOGD(TAG, "%s", buffer); + ESP_LOGI(TAG, "%s", buffer); } return true; } diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index dc22282d1c..9b671e611f 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" +#include #include namespace esphome { @@ -31,17 +32,17 @@ class RawBinarySensor : public RemoteReceiverBinarySensorBase { size_t len_; }; -class RawTrigger : public Trigger>, public Component, public RemoteReceiverListener { +class RawTrigger : public Trigger, public Component, public RemoteReceiverListener { protected: bool on_receive(RemoteReceiveData src) override { - this->trigger(*src.get_raw_data()); + this->trigger(src.get_raw_data()); return false; } }; template class RawAction : public RemoteTransmitterActionBase { public: - void set_code_template(std::function(Ts...)> func) { this->code_func_ = func; } + void set_code_template(std::function func) { this->code_func_ = func; } void set_code_static(const int32_t *code, size_t len) { this->code_static_ = code; this->code_static_len_ = len; @@ -65,7 +66,7 @@ template class RawAction : public RemoteTransmitterActionBase(Ts...)> code_func_{}; + std::function code_func_{nullptr}; const int32_t *code_static_{nullptr}; int32_t code_static_len_{0}; }; diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index cb6eed4c6c..08f2f2eaa3 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -83,7 +83,7 @@ optional RC5Protocol::decode(RemoteReceiveData src) { return out; } void RC5Protocol::dump(const RC5Data &data) { - ESP_LOGD(TAG, "Received RC5: address=0x%02X, command=0x%02X", data.address, data.command); + ESP_LOGI(TAG, "Received RC5: address=0x%02X, command=0x%02X", data.address, data.command); } } // namespace remote_base diff --git a/esphome/components/remote_base/rc6_protocol.cpp b/esphome/components/remote_base/rc6_protocol.cpp index ad12c7c208..fcb4da11a4 100644 --- a/esphome/components/remote_base/rc6_protocol.cpp +++ b/esphome/components/remote_base/rc6_protocol.cpp @@ -173,7 +173,7 @@ optional RC6Protocol::decode(RemoteReceiveData src) { } void RC6Protocol::dump(const RC6Data &data) { - ESP_LOGD(RC6_TAG, "Received RC6: mode=0x%X, address=0x%02X, command=0x%02X, toggle=0x%X", data.mode, data.address, + ESP_LOGI(RC6_TAG, "Received RC6: mode=0x%X, address=0x%02X, command=0x%02X, toggle=0x%X", data.mode, data.address, data.command, data.toggle); } diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 5b6284a86f..cb8a077975 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -54,7 +54,7 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const { } } void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const { - dst->set_carrier_frequency(0); + dst->set_carrier_frequency(38000); this->sync(dst); for (int16_t i = len - 1; i >= 0; i--) { if (code & ((uint64_t) 1 << i)) { @@ -258,7 +258,7 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) { buffer[j] = (out_data & ((uint64_t) 1 << (out_nbits - j - 1))) ? '1' : '0'; buffer[out_nbits] = '\0'; - ESP_LOGD(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", i, buffer); + ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", i, buffer); // only send first decoded protocol return true; diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index fc465dbd5d..96cbbd1467 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -15,6 +15,8 @@ struct RCSwitchData { class RCSwitchBase { public: + using ProtocolData = RCSwitchData; + RCSwitchBase() = default; RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high, uint32_t one_low, bool inverted); @@ -213,7 +215,7 @@ class RCSwitchDumper : public RemoteReceiverDumperBase { bool dump(RemoteReceiveData src) override; }; -using RCSwitchTrigger = RemoteReceiverTrigger; +using RCSwitchTrigger = RemoteReceiverTrigger; } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 97ee027b84..fdfd0b43cc 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -1,6 +1,8 @@ #include "remote_base.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace remote_base { @@ -13,8 +15,11 @@ RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_b next_rmt_channel = rmt_channel_t(int(next_rmt_channel) + mem_block_num); } +RemoteRMTChannel::RemoteRMTChannel(rmt_channel_t channel, uint8_t mem_block_num) + : channel_(channel), mem_block_num_(mem_block_num) {} + void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) { - if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) >= RMT_CHANNEL_MAX) { + if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) > RMT_CHANNEL_MAX) { this->mem_block_num_ = int(RMT_CHANNEL_MAX) - int(this->channel_); ESP_LOGW(TAG, "Not enough RMT memory blocks available, reduced to %i blocks.", this->mem_block_num_); } @@ -24,14 +29,108 @@ void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) { } #endif +/* RemoteReceiveData */ + +bool RemoteReceiveData::peek_mark(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t lo = this->lower_bound_(length); + const int32_t hi = this->upper_bound_(length); + return value >= 0 && lo <= value && value <= hi; +} + +bool RemoteReceiveData::peek_space(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t lo = this->lower_bound_(length); + const int32_t hi = this->upper_bound_(length); + return value <= 0 && lo <= -value && -value <= hi; +} + +bool RemoteReceiveData::peek_space_at_least(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t lo = this->lower_bound_(length); + return value <= 0 && lo <= -value; +} + +bool RemoteReceiveData::expect_mark(uint32_t length) { + if (!this->peek_mark(length)) + return false; + this->advance(); + return true; +} + +bool RemoteReceiveData::expect_space(uint32_t length) { + if (!this->peek_space(length)) + return false; + this->advance(); + return true; +} + +bool RemoteReceiveData::expect_item(uint32_t mark, uint32_t space) { + if (!this->peek_item(mark, space)) + return false; + this->advance(2); + return true; +} + +bool RemoteReceiveData::expect_pulse_with_gap(uint32_t mark, uint32_t space) { + if (!this->peek_space_at_least(space, 1) || !this->peek_mark(mark)) + return false; + this->advance(2); + return true; +} + +/* RemoteReceiverBinarySensorBase */ + +bool RemoteReceiverBinarySensorBase::on_receive(RemoteReceiveData src) { + if (!this->matches(src)) + return false; + this->publish_state(true); + yield(); + this->publish_state(false); + return true; +} + +/* RemoteReceiverBase */ + +void RemoteReceiverBase::register_dumper(RemoteReceiverDumperBase *dumper) { + if (dumper->is_secondary()) { + this->secondary_dumpers_.push_back(dumper); + } else { + this->dumpers_.push_back(dumper); + } +} + +void RemoteReceiverBase::call_listeners_() { + for (auto *listener : this->listeners_) + listener->on_receive(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)); +} + +void RemoteReceiverBase::call_dumpers_() { + bool success = false; + for (auto *dumper : this->dumpers_) { + if (dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_))) + success = true; + } + if (!success) { + for (auto *dumper : this->secondary_dumpers_) + dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)); + } +} + void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); } void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - const std::vector &vec = this->temp_.get_data(); + const auto &vec = this->temp_.get_data(); char buffer[256]; uint32_t buffer_offset = 0; - buffer_offset += sprintf(buffer, "Sending times=%u wait=%ums: ", send_times, send_wait); + buffer_offset += sprintf(buffer, "Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait); for (size_t i = 0; i < vec.size(); i++) { const int32_t value = vec[i]; @@ -39,9 +138,9 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { int written; if (i + 1 < vec.size()) { - written = snprintf(buffer + buffer_offset, remaining_length, "%d, ", value); + written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value); } else { - written = snprintf(buffer + buffer_offset, remaining_length, "%d", value); + written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value); } if (written < 0 || written >= int(remaining_length)) { @@ -51,9 +150,9 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { buffer_offset = 0; written = sprintf(buffer, " "); if (i + 1 < vec.size()) { - written += sprintf(buffer + written, "%d, ", value); + written += sprintf(buffer + written, "%" PRId32 ", ", value); } else { - written += sprintf(buffer + written, "%d", value); + written += sprintf(buffer + written, "%" PRId32, value); } } diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index fdb6d45e5f..c31127735a 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -3,10 +3,10 @@ #pragma once +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/core/automation.h" -#include "esphome/components/binary_sensor/binary_sensor.h" #ifdef USE_ESP32 #include @@ -15,144 +15,90 @@ namespace esphome { namespace remote_base { +enum ToleranceMode : uint8_t { + TOLERANCE_MODE_PERCENTAGE = 0, + TOLERANCE_MODE_TIME = 1, +}; + +using RawTimings = std::vector; + class RemoteTransmitData { public: void mark(uint32_t length) { this->data_.push_back(length); } - void space(uint32_t length) { this->data_.push_back(-length); } - void item(uint32_t mark, uint32_t space) { this->mark(mark); this->space(space); } - void reserve(uint32_t len) { this->data_.reserve(len); } - void set_carrier_frequency(uint32_t carrier_frequency) { this->carrier_frequency_ = carrier_frequency; } - uint32_t get_carrier_frequency() const { return this->carrier_frequency_; } - - const std::vector &get_data() const { return this->data_; } - - void set_data(const std::vector &data) { - this->data_.clear(); - this->data_.reserve(data.size()); - for (auto dat : data) - this->data_.push_back(dat); - } - + const RawTimings &get_data() const { return this->data_; } + void set_data(const RawTimings &data) { this->data_ = data; } void reset() { this->data_.clear(); this->carrier_frequency_ = 0; } - std::vector::iterator begin() { return this->data_.begin(); } - - std::vector::iterator end() { return this->data_.end(); } - protected: - std::vector data_{}; + RawTimings data_{}; uint32_t carrier_frequency_{0}; }; class RemoteReceiveData { public: - RemoteReceiveData(std::vector *data, uint8_t tolerance) : data_(data), tolerance_(tolerance) {} + explicit RemoteReceiveData(const RawTimings &data, uint32_t tolerance, ToleranceMode tolerance_mode) + : data_(data), index_(0), tolerance_(tolerance), tolerance_mode_(tolerance_mode) {} - bool peek_mark(uint32_t length, uint32_t offset = 0) { - if (int32_t(this->index_ + offset) >= this->size()) - return false; - int32_t value = this->peek(offset); - const int32_t lo = this->lower_bound_(length); - const int32_t hi = this->upper_bound_(length); - return value >= 0 && lo <= value && value <= hi; + const RawTimings &get_raw_data() const { return this->data_; } + uint32_t get_index() const { return index_; } + int32_t operator[](uint32_t index) const { return this->data_[index]; } + int32_t size() const { return this->data_.size(); } + bool is_valid(uint32_t offset) const { return this->index_ + offset < this->data_.size(); } + int32_t peek(uint32_t offset = 0) const { return this->data_[this->index_ + offset]; } + bool peek_mark(uint32_t length, uint32_t offset = 0) const; + bool peek_space(uint32_t length, uint32_t offset = 0) const; + bool peek_space_at_least(uint32_t length, uint32_t offset = 0) const; + bool peek_item(uint32_t mark, uint32_t space, uint32_t offset = 0) const { + return this->peek_space(space, offset + 1) && this->peek_mark(mark, offset); } - bool peek_space(uint32_t length, uint32_t offset = 0) { - if (int32_t(this->index_ + offset) >= this->size()) - return false; - int32_t value = this->peek(offset); - const int32_t lo = this->lower_bound_(length); - const int32_t hi = this->upper_bound_(length); - return value <= 0 && lo <= -value && -value <= hi; - } - - bool peek_space_at_least(uint32_t length, uint32_t offset = 0) { - if (int32_t(this->index_ + offset) >= this->size()) - return false; - int32_t value = this->pos(this->index_ + offset); - const int32_t lo = this->lower_bound_(length); - return value <= 0 && lo <= -value; - } - - bool peek_item(uint32_t mark, uint32_t space, uint32_t offset = 0) { - return this->peek_mark(mark, offset) && this->peek_space(space, offset + 1); - } - - int32_t peek(uint32_t offset = 0) { return (*this)[this->index_ + offset]; } - + bool expect_mark(uint32_t length); + bool expect_space(uint32_t length); + bool expect_item(uint32_t mark, uint32_t space); + bool expect_pulse_with_gap(uint32_t mark, uint32_t space); void advance(uint32_t amount = 1) { this->index_ += amount; } - - bool expect_mark(uint32_t length) { - if (this->peek_mark(length)) { - this->advance(); - return true; - } - return false; - } - - bool expect_space(uint32_t length) { - if (this->peek_space(length)) { - this->advance(); - return true; - } - return false; - } - - bool expect_item(uint32_t mark, uint32_t space) { - if (this->peek_item(mark, space)) { - this->advance(2); - return true; - } - return false; - } - - bool expect_pulse_with_gap(uint32_t mark, uint32_t space) { - if (this->peek_mark(mark, 0) && this->peek_space_at_least(space, 1)) { - this->advance(2); - return true; - } - return false; - } - - uint32_t get_index() { return index_; } - void reset() { this->index_ = 0; } - int32_t pos(uint32_t index) const { return (*this->data_)[index]; } - - int32_t operator[](uint32_t index) const { return this->pos(index); } - - int32_t size() const { return this->data_->size(); } - - std::vector *get_raw_data() { return this->data_; } + void set_tolerance(uint32_t tolerance, ToleranceMode tolerance_mode) { + this->tolerance_ = tolerance; + this->tolerance_mode_ = tolerance_mode; + } + uint32_t get_tolerance() { return tolerance_; } + ToleranceMode get_tolerance_mode() { return this->tolerance_mode_; } protected: - int32_t lower_bound_(uint32_t length) { return int32_t(100 - this->tolerance_) * length / 100U; } - int32_t upper_bound_(uint32_t length) { return int32_t(100 + this->tolerance_) * length / 100U; } + int32_t lower_bound_(uint32_t length) const { + if (this->tolerance_mode_ == TOLERANCE_MODE_TIME) { + return int32_t(length - this->tolerance_); + } else if (this->tolerance_mode_ == TOLERANCE_MODE_PERCENTAGE) { + return int32_t(100 - this->tolerance_) * length / 100U; + } + return 0; + } + int32_t upper_bound_(uint32_t length) const { + if (this->tolerance_mode_ == TOLERANCE_MODE_TIME) { + return int32_t(length + this->tolerance_); + } else if (this->tolerance_mode_ == TOLERANCE_MODE_PERCENTAGE) { + return int32_t(100 + this->tolerance_) * length / 100U; + } + return 0; + } - uint32_t index_{0}; - std::vector *data_; - uint8_t tolerance_; -}; - -template class RemoteProtocol { - public: - virtual void encode(RemoteTransmitData *dst, const T &data) = 0; - - virtual optional decode(RemoteReceiveData src) = 0; - - virtual void dump(const T &data) = 0; + const RawTimings &data_; + uint32_t index_; + uint32_t tolerance_; + ToleranceMode tolerance_mode_; }; class RemoteComponentBase { @@ -167,6 +113,7 @@ class RemoteComponentBase { class RemoteRMTChannel { public: explicit RemoteRMTChannel(uint8_t mem_block_num = 1); + explicit RemoteRMTChannel(rmt_channel_t channel, uint8_t mem_block_num = 1); void config_rmt(rmt_config_t &rmt); void set_clock_divider(uint8_t clock_divider) { this->clock_divider_ = clock_divider; } @@ -196,7 +143,6 @@ class RemoteTransmitterBase : public RemoteComponentBase { RemoteTransmitData *get_data() { return &this->parent_->temp_; } void set_send_times(uint32_t send_times) { send_times_ = send_times; } void set_send_wait(uint32_t send_wait) { send_wait_ = send_wait; } - void perform() { this->parent_->send_(this->send_times_, this->send_wait_); } protected: @@ -209,6 +155,14 @@ class RemoteTransmitterBase : public RemoteComponentBase { this->temp_.reset(); return TransmitCall(this); } + template + void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + auto call = this->transmit(); + Protocol().encode(call.get_data(), data); + call.set_send_times(send_times); + call.set_send_wait(send_wait); + call.perform(); + } protected: void send_(uint32_t send_times, uint32_t send_wait); @@ -234,51 +188,26 @@ class RemoteReceiverBase : public RemoteComponentBase { public: RemoteReceiverBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {} void register_listener(RemoteReceiverListener *listener) { this->listeners_.push_back(listener); } - void register_dumper(RemoteReceiverDumperBase *dumper) { - if (dumper->is_secondary()) { - this->secondary_dumpers_.push_back(dumper); - } else { - this->dumpers_.push_back(dumper); - } + void register_dumper(RemoteReceiverDumperBase *dumper); + void set_tolerance(uint32_t tolerance, ToleranceMode tolerance_mode) { + this->tolerance_ = tolerance; + this->tolerance_mode_ = tolerance_mode; } - void set_tolerance(uint8_t tolerance) { tolerance_ = tolerance; } protected: - bool call_listeners_() { - bool success = false; - for (auto *listener : this->listeners_) { - auto data = RemoteReceiveData(&this->temp_, this->tolerance_); - if (listener->on_receive(data)) - success = true; - } - return success; - } - void call_dumpers_() { - bool success = false; - for (auto *dumper : this->dumpers_) { - auto data = RemoteReceiveData(&this->temp_, this->tolerance_); - if (dumper->dump(data)) - success = true; - } - if (!success) { - for (auto *dumper : this->secondary_dumpers_) { - auto data = RemoteReceiveData(&this->temp_, this->tolerance_); - dumper->dump(data); - } - } - } + void call_listeners_(); + void call_dumpers_(); void call_listeners_dumpers_() { - if (this->call_listeners_()) - return; - // If a listener handled, then do not dump + this->call_listeners_(); this->call_dumpers_(); } std::vector listeners_; std::vector dumpers_; std::vector secondary_dumpers_; - std::vector temp_; - uint8_t tolerance_{25}; + RawTimings temp_; + uint32_t tolerance_{25}; + ToleranceMode tolerance_mode_{TOLERANCE_MODE_PERCENTAGE}; }; class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitiallyOff, @@ -288,18 +217,20 @@ class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitial explicit RemoteReceiverBinarySensorBase() {} void dump_config() override; virtual bool matches(RemoteReceiveData src) = 0; - bool on_receive(RemoteReceiveData src) override { - if (this->matches(src)) { - this->publish_state(true); - yield(); - this->publish_state(false); - return true; - } - return false; - } + bool on_receive(RemoteReceiveData src) override; }; -template class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { +/* TEMPLATES */ + +template class RemoteProtocol { + public: + using ProtocolData = T; + virtual void encode(RemoteTransmitData *dst, const ProtocolData &data) = 0; + virtual optional decode(RemoteReceiveData src) = 0; + virtual void dump(const ProtocolData &data) = 0; +}; + +template class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { public: RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {} @@ -311,13 +242,14 @@ template class RemoteReceiverBinarySensor : public Remot } public: - void set_data(D data) { data_ = data; } + void set_data(typename T::ProtocolData data) { data_ = data; } protected: - D data_; + typename T::ProtocolData data_; }; -template class RemoteReceiverTrigger : public Trigger, public RemoteReceiverListener { +template +class RemoteReceiverTrigger : public Trigger, public RemoteReceiverListener { protected: bool on_receive(RemoteReceiveData src) override { auto proto = T(); @@ -330,28 +262,36 @@ template class RemoteReceiverTrigger : public Trigger } }; -template class RemoteTransmitterActionBase : public Action { +class RemoteTransmittable { public: - void set_parent(RemoteTransmitterBase *parent) { this->parent_ = parent; } + RemoteTransmittable() {} + RemoteTransmittable(RemoteTransmitterBase *transmitter) : transmitter_(transmitter) {} + void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } - TEMPLATABLE_VALUE(uint32_t, send_times); - TEMPLATABLE_VALUE(uint32_t, send_wait); + protected: + template + void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + this->transmitter_->transmit(data, send_times, send_wait); + } + RemoteTransmitterBase *transmitter_; +}; +template class RemoteTransmitterActionBase : public RemoteTransmittable, public Action { + TEMPLATABLE_VALUE(uint32_t, send_times) + TEMPLATABLE_VALUE(uint32_t, send_wait) + + protected: void play(Ts... x) override { - auto call = this->parent_->transmit(); + auto call = this->transmitter_->transmit(); this->encode(call.get_data(), x...); call.set_send_times(this->send_times_.value_or(x..., 1)); call.set_send_wait(this->send_wait_.value_or(x..., 0)); call.perform(); } - - protected: virtual void encode(RemoteTransmitData *dst, Ts... x) = 0; - - RemoteTransmitterBase *parent_{}; }; -template class RemoteReceiverDumper : public RemoteReceiverDumperBase { +template class RemoteReceiverDumper : public RemoteReceiverDumperBase { public: bool dump(RemoteReceiveData src) override { auto proto = T(); @@ -364,9 +304,9 @@ template class RemoteReceiverDumper : public RemoteRecei }; #define DECLARE_REMOTE_PROTOCOL_(prefix) \ - using prefix##BinarySensor = RemoteReceiverBinarySensor; \ - using prefix##Trigger = RemoteReceiverTrigger; \ - using prefix##Dumper = RemoteReceiverDumper; + using prefix##BinarySensor = RemoteReceiverBinarySensor; \ + using prefix##Trigger = RemoteReceiverTrigger; \ + using prefix##Dumper = RemoteReceiverDumper; #define DECLARE_REMOTE_PROTOCOL(prefix) DECLARE_REMOTE_PROTOCOL_(prefix) } // namespace remote_base diff --git a/esphome/components/remote_base/roomba_protocol.cpp b/esphome/components/remote_base/roomba_protocol.cpp new file mode 100644 index 0000000000..2d2dde114a --- /dev/null +++ b/esphome/components/remote_base/roomba_protocol.cpp @@ -0,0 +1,56 @@ +#include "roomba_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.roomba"; + +static const uint8_t NBITS = 8; +static const uint32_t BIT_ONE_HIGH_US = 3000; +static const uint32_t BIT_ONE_LOW_US = 1000; +static const uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; +static const uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; + +void RoombaProtocol::encode(RemoteTransmitData *dst, const RoombaData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(NBITS * 2u); + + for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { + if (data.data & mask) { + dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US); + } + } +} +optional RoombaProtocol::decode(RemoteReceiveData src) { + RoombaData out{.data = 0}; + + for (uint8_t i = 0; i < (NBITS - 1); i++) { + out.data <<= 1UL; + if (src.expect_item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US)) { + out.data |= 1UL; + } else if (src.expect_item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US)) { + out.data |= 0UL; + } else { + return {}; + } + } + + // not possible to measure space on last bit, check only mark + out.data <<= 1UL; + if (src.expect_mark(BIT_ONE_HIGH_US)) { + out.data |= 1UL; + } else if (src.expect_mark(BIT_ZERO_HIGH_US)) { + out.data |= 0UL; + } else { + return {}; + } + + return out; +} +void RoombaProtocol::dump(const RoombaData &data) { ESP_LOGD(TAG, "Received Roomba: data=0x%02X", data.data); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/roomba_protocol.h b/esphome/components/remote_base/roomba_protocol.h new file mode 100644 index 0000000000..f94cb7df1b --- /dev/null +++ b/esphome/components/remote_base/roomba_protocol.h @@ -0,0 +1,35 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct RoombaData { + uint8_t data; + + bool operator==(const RoombaData &rhs) const { return data == rhs.data; } +}; + +class RoombaProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const RoombaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const RoombaData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Roomba) + +template class RoombaAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint8_t, data) + + void encode(RemoteTransmitData *dst, Ts... x) override { + RoombaData data{}; + data.data = this->data_.value(x...); + RoombaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/samsung36_protocol.cpp b/esphome/components/remote_base/samsung36_protocol.cpp index 9ef8ade205..bd3eee5849 100644 --- a/esphome/components/remote_base/samsung36_protocol.cpp +++ b/esphome/components/remote_base/samsung36_protocol.cpp @@ -96,7 +96,7 @@ optional Samsung36Protocol::decode(RemoteReceiveData src) { return out; } void Samsung36Protocol::dump(const Samsung36Data &data) { - ESP_LOGD(TAG, "Received Samsung36: address=0x%04X, command=0x%08X", data.address, data.command); + ESP_LOGI(TAG, "Received Samsung36: address=0x%04X, command=0x%08" PRIX32, data.address, data.command); } } // namespace remote_base diff --git a/esphome/components/remote_base/samsung36_protocol.h b/esphome/components/remote_base/samsung36_protocol.h index 4ba6226edd..aa7fd21609 100644 --- a/esphome/components/remote_base/samsung36_protocol.h +++ b/esphome/components/remote_base/samsung36_protocol.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 20e8285a03..2d6d5531e5 100644 --- a/esphome/components/remote_base/samsung_protocol.cpp +++ b/esphome/components/remote_base/samsung_protocol.cpp @@ -58,7 +58,7 @@ optional SamsungProtocol::decode(RemoteReceiveData src) { return out; } void SamsungProtocol::dump(const SamsungData &data) { - ESP_LOGD(TAG, "Received Samsung: data=0x%" PRIX64 ", nbits=%d", data.data, data.nbits); + ESP_LOGI(TAG, "Received Samsung: data=0x%" PRIX64 ", nbits=%d", data.data, data.nbits); } } // namespace remote_base diff --git a/esphome/components/remote_base/sony_protocol.cpp b/esphome/components/remote_base/sony_protocol.cpp index 8634cf7d61..69f2b49c42 100644 --- a/esphome/components/remote_base/sony_protocol.cpp +++ b/esphome/components/remote_base/sony_protocol.cpp @@ -62,7 +62,7 @@ optional SonyProtocol::decode(RemoteReceiveData src) { return out; } void SonyProtocol::dump(const SonyData &data) { - ESP_LOGD(TAG, "Received Sony: data=0x%08X, nbits=%d", data.data, data.nbits); + ESP_LOGI(TAG, "Received Sony: data=0x%08" PRIX32 ", nbits=%d", data.data, data.nbits); } } // namespace remote_base diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index aecc8ab91c..d9e4f37d53 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/toshiba_ac_protocol.cpp b/esphome/components/remote_base/toshiba_ac_protocol.cpp index 1a19f534f8..42241eea8c 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.cpp +++ b/esphome/components/remote_base/toshiba_ac_protocol.cpp @@ -105,9 +105,9 @@ optional ToshibaAcProtocol::decode(RemoteReceiveData src) { void ToshibaAcProtocol::dump(const ToshibaAcData &data) { if (data.rc_code_2 != 0) { - ESP_LOGD(TAG, "Received Toshiba AC: rc_code_1=0x%" PRIX64 ", rc_code_2=0x%" PRIX64, data.rc_code_1, data.rc_code_2); + ESP_LOGI(TAG, "Received Toshiba AC: rc_code_1=0x%" PRIX64 ", rc_code_2=0x%" PRIX64, data.rc_code_1, data.rc_code_2); } else { - ESP_LOGD(TAG, "Received Toshiba AC: rc_code_1=0x%" PRIX64, data.rc_code_1); + ESP_LOGI(TAG, "Received Toshiba AC: rc_code_1=0x%" PRIX64, data.rc_code_1); } } diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index d59ad5c7f1..e5085bb33c 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.components import remote_base +from esphome.components import remote_base, esp32_rmt from esphome.const import ( CONF_BUFFER_SIZE, CONF_DUMP, @@ -10,16 +10,75 @@ from esphome.const import ( CONF_IDLE, CONF_PIN, CONF_TOLERANCE, + CONF_TYPE, CONF_MEMORY_BLOCKS, + CONF_RMT_CHANNEL, + CONF_VALUE, ) from esphome.core import CORE, TimePeriod +CONF_CLOCK_DIVIDER = "clock_divider" + AUTO_LOAD = ["remote_base"] remote_receiver_ns = cg.esphome_ns.namespace("remote_receiver") +remote_base_ns = cg.esphome_ns.namespace("remote_base") + +ToleranceMode = remote_base_ns.enum("ToleranceMode") + +TYPE_PERCENTAGE = "percentage" +TYPE_TIME = "time" + +TOLERANCE_MODE = { + TYPE_PERCENTAGE: ToleranceMode.TOLERANCE_MODE_PERCENTAGE, + TYPE_TIME: ToleranceMode.TOLERANCE_MODE_TIME, +} + +TOLERANCE_SCHEMA = cv.typed_schema( + { + TYPE_PERCENTAGE: cv.Schema( + {cv.Required(CONF_VALUE): cv.All(cv.percentage_int, cv.uint32_t)} + ), + TYPE_TIME: cv.Schema( + { + cv.Required(CONF_VALUE): cv.All( + cv.positive_time_period_microseconds, + cv.Range(max=TimePeriod(microseconds=4294967295)), + ) + } + ), + }, + lower=True, + enum=TOLERANCE_MODE, +) + RemoteReceiverComponent = remote_receiver_ns.class_( "RemoteReceiverComponent", remote_base.RemoteReceiverBase, cg.Component ) + +def validate_tolerance(value): + if isinstance(value, dict): + return TOLERANCE_SCHEMA(value) + + if "%" in str(value): + type_ = TYPE_PERCENTAGE + else: + try: + cv.positive_time_period_microseconds(value) + type_ = TYPE_TIME + except cv.Invalid as exc: + raise cv.Invalid( + "Tolerance must be a percentage or time. Configurations made before 2024.5.0 treated the value as a percentage." + ) from exc + + return TOLERANCE_SCHEMA( + { + CONF_VALUE: value, + CONF_TYPE: type_, + } + ) + + MULTI_CONF = True CONFIG_SCHEMA = remote_base.validate_triggers( cv.Schema( @@ -27,20 +86,27 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.GenerateID(): cv.declare_id(RemoteReceiverComponent), cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), cv.Optional(CONF_DUMP, default=[]): remote_base.validate_dumpers, - cv.Optional(CONF_TOLERANCE, default=25): cv.All( - cv.percentage_int, cv.Range(min=0) - ), + cv.Optional(CONF_TOLERANCE, default="25%"): validate_tolerance, cv.SplitDefault( - CONF_BUFFER_SIZE, esp32="10000b", esp8266="1000b" + CONF_BUFFER_SIZE, + esp32="10000b", + esp8266="1000b", + bk72xx="1000b", + rtl87xx="1000b", ): cv.validate_bytes, cv.Optional(CONF_FILTER, default="50us"): cv.All( cv.positive_time_period_microseconds, - cv.Range(max=TimePeriod(microseconds=255)), + cv.Range(max=TimePeriod(microseconds=4294967295)), + ), + cv.SplitDefault(CONF_CLOCK_DIVIDER, esp32=80): cv.All( + cv.only_on_esp32, cv.Range(min=1, max=255) + ), + cv.Optional(CONF_IDLE, default="10ms"): cv.All( + cv.positive_time_period_microseconds, + cv.Range(max=TimePeriod(microseconds=4294967295)), ), - cv.Optional( - CONF_IDLE, default="10ms" - ): cv.positive_time_period_microseconds, cv.Optional(CONF_MEMORY_BLOCKS, default=3): cv.Range(min=1, max=8), + cv.Optional(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=False), } ).extend(cv.COMPONENT_SCHEMA) ) @@ -49,7 +115,13 @@ CONFIG_SCHEMA = remote_base.validate_triggers( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: - var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS]) + if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: + var = cg.new_Pvariable( + config[CONF_ID], pin, rmt_channel, config[CONF_MEMORY_BLOCKS] + ) + else: + var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS]) + cg.add(var.set_clock_divider(config[CONF_CLOCK_DIVIDER])) else: var = cg.new_Pvariable(config[CONF_ID], pin) @@ -62,7 +134,11 @@ async def to_code(config): cg.add(var.register_listener(trigger)) await cg.register_component(var, config) - cg.add(var.set_tolerance(config[CONF_TOLERANCE])) + cg.add( + var.set_tolerance( + config[CONF_TOLERANCE][CONF_VALUE], config[CONF_TOLERANCE][CONF_TYPE] + ) + ) cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) cg.add(var.set_filter_us(config[CONF_FILTER])) cg.add(var.set_idle_us(config[CONF_IDLE])) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 50153c105d..773f8cf636 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -1,12 +1,14 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/remote_base/remote_base.h" +#include "esphome/core/component.h" + +#include namespace esphome { namespace remote_receiver { -#ifdef USE_ESP8266 +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -20,7 +22,7 @@ struct RemoteReceiverComponentStore { uint32_t buffer_read_at{0}; bool overflow{false}; uint32_t buffer_size{1000}; - uint8_t filter_us{10}; + uint32_t filter_us{10}; ISRInternalGPIOPin pin; }; #endif @@ -36,6 +38,9 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, #ifdef USE_ESP32 RemoteReceiverComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} + + RemoteReceiverComponent(InternalGPIOPin *pin, rmt_channel_t channel, uint8_t mem_block_num = 1) + : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(channel, mem_block_num) {} #else RemoteReceiverComponent(InternalGPIOPin *pin) : RemoteReceiverBase(pin) {} #endif @@ -45,7 +50,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, float get_setup_priority() const override { return setup_priority::DATA; } void set_buffer_size(uint32_t buffer_size) { this->buffer_size_ = buffer_size; } - void set_filter_us(uint8_t filter_us) { this->filter_us_ = filter_us; } + void set_filter_us(uint32_t filter_us) { this->filter_us_ = filter_us; } void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: @@ -53,15 +58,16 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void decode_rmt_(rmt_item32_t *item, size_t len); RingbufHandle_t ringbuf_; esp_err_t error_code_{ESP_OK}; + std::string error_string_{""}; #endif -#ifdef USE_ESP8266 +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) RemoteReceiverComponentStore store_; HighFrequencyLoopRequester high_freq_; #endif uint32_t buffer_size_{}; - uint8_t filter_us_{10}; + uint32_t filter_us_{10}; uint32_t idle_us_{10000}; }; diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 5a7fb3c985..91295871e2 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -20,13 +20,16 @@ void RemoteReceiverComponent::setup() { rmt.rx_config.filter_en = false; } else { rmt.rx_config.filter_en = true; - rmt.rx_config.filter_ticks_thresh = this->from_microseconds_(this->filter_us_); + rmt.rx_config.filter_ticks_thresh = static_cast( + std::min(this->from_microseconds_(this->filter_us_) * this->clock_divider_, (uint32_t) 255)); } - rmt.rx_config.idle_threshold = this->from_microseconds_(this->idle_us_); + rmt.rx_config.idle_threshold = + static_cast(std::min(this->from_microseconds_(this->idle_us_), (uint32_t) 65535)); esp_err_t error = rmt_config(&rmt); if (error != ESP_OK) { this->error_code_ = error; + this->error_string_ = "in rmt_config"; this->mark_failed(); return; } @@ -34,18 +37,25 @@ void RemoteReceiverComponent::setup() { error = rmt_driver_install(this->channel_, this->buffer_size_, 0); if (error != ESP_OK) { this->error_code_ = error; + if (error == ESP_ERR_INVALID_STATE) { + this->error_string_ = str_sprintf("RMT channel %i is already in use by another component", this->channel_); + } else { + this->error_string_ = "in rmt_driver_install"; + } this->mark_failed(); return; } error = rmt_get_ringbuf_handle(this->channel_, &this->ringbuf_); if (error != ESP_OK) { this->error_code_ = error; + this->error_string_ = "in rmt_get_ringbuf_handle"; this->mark_failed(); return; } error = rmt_rx_start(this->channel_, true); if (error != ESP_OK) { this->error_code_ = error; + this->error_string_ = "in rmt_rx_start"; this->mark_failed(); return; } @@ -60,11 +70,13 @@ void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, " Channel: %d", this->channel_); ESP_LOGCONFIG(TAG, " RMT memory blocks: %d", this->mem_block_num_); ESP_LOGCONFIG(TAG, " Clock divider: %u", this->clock_divider_); - ESP_LOGCONFIG(TAG, " Tolerance: %u%%", this->tolerance_); - ESP_LOGCONFIG(TAG, " Filter out pulses shorter than: %u us", this->filter_us_); - ESP_LOGCONFIG(TAG, " Signal is done after %u us of no changes", this->idle_us_); + ESP_LOGCONFIG(TAG, " Tolerance: %" PRIu32 "%s", this->tolerance_, + (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%"); + ESP_LOGCONFIG(TAG, " Filter out pulses shorter than: %" PRIu32 " us", this->filter_us_); + ESP_LOGCONFIG(TAG, " Signal is done after %" PRIu32 " us of no changes", this->idle_us_); if (this->is_failed()) { - ESP_LOGE(TAG, "Configuring RMT driver failed: %s", esp_err_to_name(this->error_code_)); + ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), + this->error_string_.c_str()); } } @@ -88,18 +100,23 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { this->temp_.clear(); int32_t multiplier = this->pin_->is_inverted() ? -1 : 1; size_t item_count = len / sizeof(rmt_item32_t); + uint32_t filter_ticks = this->from_microseconds_(this->filter_us_); ESP_LOGVV(TAG, "START:"); for (size_t i = 0; i < item_count; i++) { if (item[i].level0) { - ESP_LOGVV(TAG, "%u A: ON %uus (%u ticks)", i, this->to_microseconds_(item[i].duration0), item[i].duration0); + ESP_LOGVV(TAG, "%zu A: ON %" PRIu32 "us (%u ticks)", i, this->to_microseconds_(item[i].duration0), + item[i].duration0); } else { - ESP_LOGVV(TAG, "%u A: OFF %uus (%u ticks)", i, this->to_microseconds_(item[i].duration0), item[i].duration0); + ESP_LOGVV(TAG, "%zu A: OFF %" PRIu32 "us (%u ticks)", i, this->to_microseconds_(item[i].duration0), + item[i].duration0); } if (item[i].level1) { - ESP_LOGVV(TAG, "%u B: ON %uus (%u ticks)", i, this->to_microseconds_(item[i].duration1), item[i].duration1); + ESP_LOGVV(TAG, "%zu B: ON %" PRIu32 "us (%u ticks)", i, this->to_microseconds_(item[i].duration1), + item[i].duration1); } else { - ESP_LOGVV(TAG, "%u B: OFF %uus (%u ticks)", i, this->to_microseconds_(item[i].duration1), item[i].duration1); + ESP_LOGVV(TAG, "%zu B: OFF %" PRIu32 "us (%u ticks)", i, this->to_microseconds_(item[i].duration1), + item[i].duration1); } } ESP_LOGVV(TAG, "\n"); @@ -108,7 +125,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { for (size_t i = 0; i < item_count; i++) { if (item[i].duration0 == 0u) { // Do nothing - } else if (bool(item[i].level0) == prev_level) { + } else if ((bool(item[i].level0) == prev_level) || (item[i].duration0 < filter_ticks)) { prev_length += item[i].duration0; } else { if (prev_length > 0) { @@ -124,7 +141,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { if (item[i].duration1 == 0u) { // Do nothing - } else if (bool(item[i].level1) == prev_level) { + } else if ((bool(item[i].level1) == prev_level) || (item[i].duration1 < filter_ticks)) { prev_length += item[i].duration1; } else { if (prev_length > 0) { diff --git a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp index 8700fcf0bb..c92a134bd8 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp @@ -64,7 +64,8 @@ void RemoteReceiverComponent::dump_config() { "invert the signal using 'inverted: True' in the pin schema!"); } ESP_LOGCONFIG(TAG, " Buffer Size: %u", this->buffer_size_); - ESP_LOGCONFIG(TAG, " Tolerance: %u%%", this->tolerance_); + ESP_LOGCONFIG(TAG, " Tolerance: %u%s", this->tolerance_, + (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%"); ESP_LOGCONFIG(TAG, " Filter out pulses shorter than: %u us", this->filter_us_); ESP_LOGCONFIG(TAG, " Signal is done after %u us of no changes", this->idle_us_); } diff --git a/esphome/components/remote_receiver/remote_receiver_libretiny.cpp b/esphome/components/remote_receiver/remote_receiver_libretiny.cpp new file mode 100644 index 0000000000..bfc29b4211 --- /dev/null +++ b/esphome/components/remote_receiver/remote_receiver_libretiny.cpp @@ -0,0 +1,123 @@ +#include "remote_receiver.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef USE_LIBRETINY + +namespace esphome { +namespace remote_receiver { + +static const char *const TAG = "remote_receiver.libretiny"; + +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; + + // 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; +} + +void RemoteReceiverComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up Remote Receiver..."); + 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->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); +} +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, " Buffer Size: %u", this->buffer_size_); + ESP_LOGCONFIG(TAG, " Tolerance: %u%s", this->tolerance_, + (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%"); + ESP_LOGCONFIG(TAG, " Filter out pulses shorter than: %u us", this->filter_us_); + ESP_LOGCONFIG(TAG, " Signal is done after %u us of no changes", this->idle_us_); +} + +void RemoteReceiverComponent::loop() { + 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; + } + + 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; + } + + 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); + + this->call_listeners_dumpers_(); +} + +} // namespace remote_receiver +} // namespace esphome + +#endif diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index e09e4c7f55..f979939739 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -1,10 +1,14 @@ +from esphome import automation, pins import esphome.codegen as cg +from esphome.components import esp32_rmt, remote_base import esphome.config_validation as cv -from esphome import pins -from esphome.components import remote_base -from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_ID, CONF_PIN +from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_ID, CONF_PIN, CONF_RMT_CHANNEL AUTO_LOAD = ["remote_base"] + +CONF_ON_TRANSMIT = "on_transmit" +CONF_ON_COMPLETE = "on_complete" + remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter") RemoteTransmitterComponent = remote_transmitter_ns.class_( "RemoteTransmitterComponent", remote_base.RemoteTransmitterBase, cg.Component @@ -18,13 +22,29 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( cv.percentage_int, cv.Range(min=1, max=100) ), + cv.Optional(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=True), + cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), } ).extend(cv.COMPONENT_SCHEMA) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - var = cg.new_Pvariable(config[CONF_ID], pin) + if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: + var = cg.new_Pvariable(config[CONF_ID], pin, rmt_channel) + else: + var = cg.new_Pvariable(config[CONF_ID], pin) await cg.register_component(var, config) cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT])) + + if on_transmit_config := config.get(CONF_ON_TRANSMIT): + await automation.build_automation( + var.get_transmit_trigger(), [], on_transmit_config + ) + + if on_complete_config := config.get(CONF_ON_COMPLETE): + await automation.build_automation( + var.get_complete_trigger(), [], on_complete_config + ) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 560d83802e..4abe687d23 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/remote_base/remote_base.h" +#include "esphome/core/component.h" #include @@ -16,8 +16,15 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #endif { public: - explicit RemoteTransmitterComponent(InternalGPIOPin *pin) : remote_base::RemoteTransmitterBase(pin) {} +#ifdef USE_ESP32 + RemoteTransmitterComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) + : remote_base::RemoteTransmitterBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} + RemoteTransmitterComponent(InternalGPIOPin *pin, rmt_channel_t channel, uint8_t mem_block_num = 1) + : remote_base::RemoteTransmitterBase(pin), remote_base::RemoteRMTChannel(channel, mem_block_num) {} +#else + explicit RemoteTransmitterComponent(InternalGPIOPin *pin) : remote_base::RemoteTransmitterBase(pin) {} +#endif void setup() override; void dump_config() override; @@ -26,9 +33,12 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } + Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; + Trigger<> *get_complete_trigger() const { return this->complete_trigger_; }; + protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#ifdef USE_ESP8266 +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) 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); @@ -42,13 +52,17 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #ifdef USE_ESP32 void configure_rmt_(); - uint32_t current_carrier_frequency_{UINT32_MAX}; + uint32_t current_carrier_frequency_{38000}; bool initialized_{false}; std::vector rmt_temp_; esp_err_t error_code_{ESP_OK}; + std::string error_string_{""}; bool inverted_{false}; #endif - uint8_t carrier_duty_percent_{50}; + uint8_t carrier_duty_percent_; + + Trigger<> *transmit_trigger_{new Trigger<>()}; + Trigger<> *complete_trigger_{new Trigger<>()}; }; } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index c3d4d42e4f..bce2408723 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -23,7 +23,8 @@ void RemoteTransmitterComponent::dump_config() { } if (this->is_failed()) { - ESP_LOGE(TAG, "Configuring RMT driver failed: %s", esp_err_to_name(this->error_code_)); + ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), + this->error_string_.c_str()); } } @@ -56,6 +57,7 @@ void RemoteTransmitterComponent::configure_rmt_() { esp_err_t error = rmt_config(&c); if (error != ESP_OK) { this->error_code_ = error; + this->error_string_ = "in rmt_config"; this->mark_failed(); return; } @@ -64,6 +66,11 @@ void RemoteTransmitterComponent::configure_rmt_() { error = rmt_driver_install(this->channel_, 0, 0); if (error != ESP_OK) { this->error_code_ = error; + if (error == ESP_ERR_INVALID_STATE) { + this->error_string_ = str_sprintf("RMT channel %i is already in use by another component", this->channel_); + } else { + this->error_string_ = "in rmt_driver_install"; + } this->mark_failed(); return; } @@ -117,6 +124,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen ESP_LOGE(TAG, "Empty data"); return; } + this->transmit_trigger_->trigger(); for (uint32_t i = 0; i < send_times; i++) { esp_err_t error = rmt_write_items(this->channel_, this->rmt_temp_.data(), this->rmt_temp_.size(), true); if (error != ESP_OK) { @@ -128,6 +136,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) delayMicroseconds(send_wait); } + this->complete_trigger_->trigger(); } } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index 1c0eb94e61..613f00b7f5 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -76,6 +76,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); this->target_time_ = 0; + this->transmit_trigger_->trigger(); for (uint32_t i = 0; i < send_times; i++) { for (int32_t item : this->temp_.get_data()) { if (item > 0) { @@ -93,6 +94,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) this->target_time_ += send_wait; } + this->complete_trigger_->trigger(); } } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp new file mode 100644 index 0000000000..ad9265fb14 --- /dev/null +++ b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp @@ -0,0 +1,106 @@ +#include "remote_transmitter.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#ifdef USE_LIBRETINY + +namespace esphome { +namespace remote_transmitter { + +static const char *const TAG = "remote_transmitter"; + +void RemoteTransmitterComponent::setup() { + this->pin_->setup(); + this->pin_->digital_write(false); +} + +void RemoteTransmitterComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Remote Transmitter..."); + ESP_LOGCONFIG(TAG, " Carrier Duty: %u%%", this->carrier_duty_percent_); + LOG_PIN(" Pin: ", this->pin_); +} + +void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, + uint32_t *off_time_period) { + if (carrier_frequency == 0) { + *on_time_period = 0; + *off_time_period = 0; + return; + } + uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq) + period = std::max(uint32_t(1), period); + *on_time_period = (period * this->carrier_duty_percent_) / 100; + *off_time_period = period - *on_time_period; +} + +void RemoteTransmitterComponent::await_target_time_() { + const uint32_t current_time = micros(); + if (this->target_time_ == 0) { + this->target_time_ = current_time; + } else { + while (this->target_time_ > micros()) { + // busy loop that ensures micros is constantly called + } + } +} + +void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { + this->await_target_time_(); + this->pin_->digital_write(true); + + const uint32_t target = this->target_time_ + usec; + if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { + while (true) { // Modulate with carrier frequency + this->target_time_ += on_time; + if (this->target_time_ >= target) + break; + this->await_target_time_(); + this->pin_->digital_write(false); + + this->target_time_ += off_time; + if (this->target_time_ >= target) + break; + this->await_target_time_(); + this->pin_->digital_write(true); + } + } + this->target_time_ = target; +} + +void RemoteTransmitterComponent::space_(uint32_t usec) { + this->await_target_time_(); + this->pin_->digital_write(false); + this->target_time_ += usec; +} + +void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { + ESP_LOGD(TAG, "Sending remote code..."); + uint32_t on_time, off_time; + this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); + this->target_time_ = 0; + this->transmit_trigger_->trigger(); + for (uint32_t i = 0; i < send_times; i++) { + InterruptLock lock; + for (int32_t item : this->temp_.get_data()) { + if (item > 0) { + const auto length = uint32_t(item); + this->mark_(on_time, off_time, length); + } else { + const auto length = uint32_t(-item); + this->space_(length); + } + App.feed_wdt(); + } + this->await_target_time_(); // wait for duration of last pulse + this->pin_->digital_write(false); + + if (i + 1 < send_times) + this->target_time_ += send_wait; + } + this->complete_trigger_->trigger(); +} + +} // namespace remote_transmitter +} // namespace esphome + +#endif diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index b57f90b59c..8fa1f8b570 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -1,7 +1,8 @@ #pragma once -#include "esphome/core/component.h" +#include "esphome/components/resistance_sampler/resistance_sampler.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace resistance { @@ -11,7 +12,7 @@ enum ResistanceConfiguration { DOWNSTREAM, }; -class ResistanceSensor : public Component, public sensor::Sensor { +class ResistanceSensor : public Component, public sensor::Sensor, resistance_sampler::ResistanceSampler { public: void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; } diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py index 55e7ddfc81..ce4459fc6d 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -1,17 +1,24 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor +from esphome.components import sensor, resistance_sampler from esphome.const import ( + CONF_REFERENCE_VOLTAGE, CONF_SENSOR, STATE_CLASS_MEASUREMENT, UNIT_OHM, ICON_FLASH, ) -resistance_ns = cg.esphome_ns.namespace("resistance") -ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor) +AUTO_LOAD = ["resistance_sampler"] + +resistance_ns = cg.esphome_ns.namespace("resistance") +ResistanceSensor = resistance_ns.class_( + "ResistanceSensor", + cg.Component, + sensor.Sensor, + resistance_sampler.ResistanceSampler, +) -CONF_REFERENCE_VOLTAGE = "reference_voltage" CONF_CONFIGURATION = "configuration" CONF_RESISTOR = "resistor" diff --git a/esphome/components/resistance_sampler/__init__.py b/esphome/components/resistance_sampler/__init__.py new file mode 100644 index 0000000000..d2032848aa --- /dev/null +++ b/esphome/components/resistance_sampler/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +resistance_sampler_ns = cg.esphome_ns.namespace("resistance_sampler") +ResistanceSampler = resistance_sampler_ns.class_("ResistanceSampler") + +CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/resistance_sampler/resistance_sampler.h b/esphome/components/resistance_sampler/resistance_sampler.h new file mode 100644 index 0000000000..9e300bebcc --- /dev/null +++ b/esphome/components/resistance_sampler/resistance_sampler.h @@ -0,0 +1,10 @@ +#pragma once + +namespace esphome { +namespace resistance_sampler { + +/// Abstract interface to mark components that provide resistance values. +class ResistanceSampler {}; + +} // namespace resistance_sampler +} // namespace esphome diff --git a/esphome/components/restart/button/__init__.py b/esphome/components/restart/button/__init__.py index 1b2c991261..6aff8cb351 100644 --- a/esphome/components/restart/button/__init__.py +++ b/esphome/components/restart/button/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( CONF_ID, DEVICE_CLASS_RESTART, ENTITY_CATEGORY_CONFIG, + ICON_RESTART, ) restart_ns = cg.esphome_ns.namespace("restart") @@ -12,6 +13,7 @@ RestartButton = restart_ns.class_("RestartButton", button.Button, cg.Component) CONFIG_SCHEMA = button.button_schema( RestartButton, + icon=ICON_RESTART, device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG, ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index c34b3d2dc4..3b3e00a416 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -1,5 +1,6 @@ #include "rf_bridge.h" #include "esphome/core/log.h" +#include #include namespace esphome { @@ -53,8 +54,10 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { ESP_LOGD(TAG, "Learning success"); } - ESP_LOGI(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, - data.high, data.code); + ESP_LOGI(TAG, + "Received RFBridge Code: sync=0x%04" PRIX16 " low=0x%04" PRIX16 " high=0x%04" PRIX16 + " code=0x%06" PRIX32, + data.sync, data.low, data.high, data.code); this->data_callback_.call(data); break; } @@ -144,8 +147,8 @@ void RFBridgeComponent::loop() { } void RFBridgeComponent::send_code(RFBridgeData data) { - ESP_LOGD(TAG, "Sending code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, data.high, - data.code); + ESP_LOGD(TAG, "Sending code: sync=0x%04" PRIX16 " low=0x%04" PRIX16 " high=0x%04" PRIX16 " code=0x%06" PRIX32, + data.sync, data.low, data.high, data.code); this->write(RF_CODE_START); this->write(RF_CODE_RFOUT); this->write((data.sync >> 8) & 0xFF); diff --git a/esphome/components/rgbct/rgbct_light_output.h b/esphome/components/rgbct/rgbct_light_output.h index 9257d67cd1..9e23f783ae 100644 --- a/esphome/components/rgbct/rgbct_light_output.h +++ b/esphome/components/rgbct/rgbct_light_output.h @@ -23,10 +23,11 @@ class RGBCTLightOutput : public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); + } traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index 0f55775608..a2ab17b75d 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -16,10 +16,11 @@ class RGBWLightOutput : public light::LightOutput { void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + } return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index 5a86b88595..9687360059 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -20,10 +20,11 @@ class RGBWWLightOutput : public light::LightOutput { void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLD_WARM_WHITE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE}); + } traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 7440214b1c..a3631ffe27 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -226,6 +226,7 @@ void RotaryEncoderSensor::loop() { } this->store_.last_read = counter; this->publish_state(counter); + this->listeners_.call(counter); this->publish_initial_value_ = false; } } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index deba3d952d..e88ee9152a 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -92,6 +92,8 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { this->on_anticlockwise_callback_.add(std::move(callback)); } + void register_listener(std::function listener) { this->listeners_.add(std::move(listener)); } + protected: InternalGPIOPin *pin_a_; InternalGPIOPin *pin_b_; @@ -102,8 +104,9 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { RotaryEncoderSensorStore store_{}; - CallbackManager on_clockwise_callback_; - CallbackManager on_anticlockwise_callback_; + CallbackManager on_clockwise_callback_{}; + CallbackManager on_anticlockwise_callback_{}; + CallbackManager listeners_{}; }; template class RotaryEncoderSetValueAction : public Action { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 1eba0bf192..d612631a4c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -1,6 +1,5 @@ import logging import os - from string import ascii_letters, digits import esphome.codegen as cg @@ -8,16 +7,17 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_FRAMEWORK, + CONF_PLATFORM_VERSION, CONF_SOURCE, CONF_VERSION, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + PLATFORM_RP2040, ) -from esphome.core import CORE, coroutine_with_priority, EsphomeError -from esphome.helpers import mkdir_p, write_file -import esphome.platformio_api as api +from esphome.core import CORE, EsphomeError, coroutine_with_priority +from esphome.helpers import copy_file_if_changed, mkdir_p, write_file, read_file from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns @@ -26,12 +26,12 @@ from .gpio import rp2040_pin_to_code # noqa _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jesserockz"] -AUTO_LOAD = [] +AUTO_LOAD = ["preferences"] def set_core_data(config): CORE.data[KEY_RP2040] = {} - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "rp2040" + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_RP2040 CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] @@ -43,6 +43,23 @@ def set_core_data(config): return config +def get_download_types(storage_json): + return [ + { + "title": "UF2 factory format", + "description": "For copying to RP2040 over USB.", + "file": "firmware.uf2", + "download": f"{storage_json.name}.factory.uf2", + }, + { + "title": "OTA format", + "description": "For OTA updating a device.", + "file": "firmware.ota.bin", + "download": f"{storage_json.name}.ota.bin", + }, + ] + + def _format_framework_arduino_version(ver: cv.Version) -> str: # The most recent releases have not been uploaded to platformio so grabbing them directly from # the GitHub release is one path forward for now. @@ -54,6 +71,14 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" +def _parse_platform_version(value): + value = cv.string(value) + if value.startswith("http"): + return value + + return f"https://github.com/maxgerhardt/platform-raspberrypi.git#{value}" + + # NOTE: Keep this in mind when updating the recommended version: # * The new version needs to be thoroughly validated before changing the # recommended version as otherwise a bunch of devices could be bricked @@ -63,19 +88,18 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 6, 4) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 9, 4) -# The platformio/raspberrypi version to use for arduino frameworks -# - https://github.com/platformio/platform-raspberrypi/releases -# - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi -ARDUINO_PLATFORM_VERSION = cv.Version(1, 7, 0) +# The raspberrypi platform version to use for arduino frameworks +# - https://github.com/maxgerhardt/platform-raspberrypi/tags +RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.2.0-gcc12" def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(2, 6, 4), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(2, 6, 4), None), + "dev": (cv.Version(3, 9, 4), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(3, 9, 4), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -94,7 +118,8 @@ def _arduino_check_versions(value): value[CONF_SOURCE] = source or _format_framework_arduino_version(version) value[CONF_PLATFORM_VERSION] = value.get( - CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) + CONF_PLATFORM_VERSION, + _parse_platform_version(RECOMMENDED_ARDUINO_PLATFORM_VERSION), ) if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: @@ -105,17 +130,6 @@ def _arduino_check_versions(value): return value -def _parse_platform_version(value): - try: - # if platform version is a valid version constraint, prefix the default package - cv.platformio_version_constraint(value) - return f"platformio/raspberrypi@{value}" - except cv.Invalid: - return value - - -CONF_PLATFORM_VERSION = "platform_version" - ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { @@ -142,11 +156,16 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add(rp2040_ns.setup_preferences()) + # Allow LDF to properly discover dependency including those in preprocessor + # conditionals + cg.add_platformio_option("lib_ldf_mode", "chain+") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") + cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) + conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") @@ -157,7 +176,6 @@ async def to_code(config): "platform_packages", [ f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}", - "earlephilhower/tool-pioasm-rp2040-earlephilhower", ], ) @@ -194,28 +212,32 @@ def generate_pio_files() -> bool: pio_path = CORE.relative_build_path(f"src/pio/{key}.pio") mkdir_p(os.path.dirname(pio_path)) write_file(pio_path, data) - _LOGGER.info("Assembling PIO assembly code") - retval = api.run_platformio_cli( - "pkg", - "exec", - "--package", - "earlephilhower/tool-pioasm-rp2040-earlephilhower", - "--", - "pioasm", - pio_path, - pio_path + ".h", - ) includes.append(f"pio/{key}.pio.h") - if retval != 0: - raise EsphomeError("PIO assembly failed") write_file( CORE.relative_build_path("src/pio_includes.h"), "#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]), ) + + dir = os.path.dirname(__file__) + build_pio_file = os.path.join(dir, "build_pio.py.script") + copy_file_if_changed( + build_pio_file, + CORE.relative_build_path("build_pio.py"), + ) + return True # Called by writer.py -def copy_files() -> bool: - return generate_pio_files() +def copy_files(): + dir = os.path.dirname(__file__) + post_build_file = os.path.join(dir, "post_build.py.script") + copy_file_if_changed( + post_build_file, + CORE.relative_build_path("post_build.py"), + ) + if generate_pio_files(): + path = CORE.relative_src_path("esphome.h") + content = read_file(path).rstrip("\n") + write_file(path, content + '\n#include "pio_includes.h"\n') diff --git a/esphome/components/rp2040/build_pio.py.script b/esphome/components/rp2040/build_pio.py.script new file mode 100644 index 0000000000..c3e0767ed6 --- /dev/null +++ b/esphome/components/rp2040/build_pio.py.script @@ -0,0 +1,47 @@ +""" +Custom pioasm compiler script for platformio. +(c) 2022 by P.Z. + +Sourced 2023/06/23 from https://gist.github.com/hexeguitar/f4533bc697c956ac1245b6843e2ef438 + +Modified by jesserockz 2023/06/23 +""" + +from os.path import join +import glob +import sys + +import subprocess + +# pylint: disable=E0602 +Import("env") # noqa + +from SCons.Script import ARGUMENTS + + +platform = env.PioPlatform() +PROJ_SRC = env["PROJECT_SRC_DIR"] +PIO_FILES = glob.glob(join(PROJ_SRC, "**", "*.pio"), recursive=True) + +verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) + + +if PIO_FILES: + if verbose: + print("==============================================") + print("PIO ASSEMBLY COMPILER") + try: + PIOASM_DIR = platform.get_package_dir("tool-pioasm-rp2040-earlephilhower") + except: + print("tool-pioasm-rp2040-earlephilhower not supported on your system!") + sys.exit() + + PIOASM_EXE = join(PIOASM_DIR, "pioasm") + if verbose: + print("PIO files found:") + for filename in PIO_FILES: + if verbose: + print(f" {filename}") + subprocess.run([PIOASM_EXE, "-o", "c-sdk", filename, f"{filename}.h"]) + if verbose: + print("==============================================") diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 2340bed892..58514f7db5 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -1,6 +1,8 @@ +from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_ANALOG, CONF_ID, CONF_INPUT, CONF_INVERTED, @@ -12,7 +14,6 @@ from esphome.const import ( CONF_PULLUP, ) from esphome.core import CORE -from esphome import pins from . import boards from .const import KEY_BOARD, KEY_RP2040, rp2040_ns @@ -40,8 +41,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: @@ -76,25 +79,11 @@ def validate_supports(value): return value -CONF_ANALOG = "analog" - RP2040_PIN_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(RP2040GPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_ANALOG, default=False): cv.boolean, - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } + pins.gpio_base_schema( + RP2040GPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), ), validate_supports, ) diff --git a/esphome/components/rp2040/post_build.py.script b/esphome/components/rp2040/post_build.py.script new file mode 100644 index 0000000000..7dcd7e52a6 --- /dev/null +++ b/esphome/components/rp2040/post_build.py.script @@ -0,0 +1,23 @@ +import shutil + +# pylint: disable=E0602 +Import("env") # noqa + + +def rp2040_copy_factory_uf2(source, target, env): + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.uf2") + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.uf2") + + shutil.copyfile(firmware_name, new_file_name) + + +def rp2040_copy_ota_bin(source, target, env): + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin") + + shutil.copyfile(firmware_name, new_file_name) + + +# pylint: disable=E0602 +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", rp2040_copy_factory_uf2) # noqa +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", rp2040_copy_ota_bin) # noqa diff --git a/esphome/components/rp2040_pio/__init__.py b/esphome/components/rp2040_pio/__init__.py new file mode 100644 index 0000000000..af884d5ac2 --- /dev/null +++ b/esphome/components/rp2040_pio/__init__.py @@ -0,0 +1,40 @@ +import platform + +import esphome.codegen as cg + + +DEPENDENCIES = ["rp2040"] + + +PIOASM_REPO_VERSION = "1.5.0-b" +PIOASM_REPO_BASE = f"https://github.com/earlephilhower/pico-quick-toolchain/releases/download/{PIOASM_REPO_VERSION}" +PIOASM_VERSION = "pioasm-2e6142b.230216" +PIOASM_DOWNLOADS = { + "linux": { + "aarch64": f"aarch64-linux-gnu.{PIOASM_VERSION}.tar.gz", + "armv7l": f"arm-linux-gnueabihf.{PIOASM_VERSION}.tar.gz", + "x86_64": f"x86_64-linux-gnu.{PIOASM_VERSION}.tar.gz", + }, + "windows": { + "amd64": f"x86_64-w64-mingw32.{PIOASM_VERSION}.zip", + }, + "darwin": { + "x86_64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz", + "arm64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz", + }, +} + + +async def to_code(config): + # cg.add_platformio_option( + # "platform_packages", + # [ + # "earlephilhower/tool-pioasm-rp2040-earlephilhower", + # ], + # ) + file = PIOASM_DOWNLOADS[platform.system().lower()][platform.machine().lower()] + cg.add_platformio_option( + "platform_packages", + [f"earlephilhower/tool-pioasm-rp2040-earlephilhower@{PIOASM_REPO_BASE}/{file}"], + ) + cg.add_platformio_option("extra_scripts", ["pre:build_pio.py"]) diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index ce1836306f..2aaa2ceb19 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -6,14 +6,39 @@ #include "esphome/core/log.h" #include +#include +#include #include #include +#include namespace esphome { namespace rp2040_pio_led_strip { static const char *TAG = "rp2040_pio_led_strip"; +static uint8_t num_instance_[2] = {0, 0}; +static std::map chipset_offsets_ = { + {CHIPSET_WS2812, 0}, {CHIPSET_WS2812B, 0}, {CHIPSET_SK6812, 0}, {CHIPSET_SM16703, 0}, {CHIPSET_CUSTOM, 0}, +}; +static std::map conf_count_ = { + {CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false}, + {CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false}, +}; +static bool dma_chan_active_[12]; +static struct semaphore dma_write_complete_sem_[12]; + +// DMA interrupt service routine +void RP2040PIOLEDStripLightOutput::dma_write_complete_handler_() { + uint32_t channel = dma_hw->ints0; + for (uint dma_chan = 0; dma_chan < 12; ++dma_chan) { + if (RP2040PIOLEDStripLightOutput::dma_chan_active_[dma_chan] && (channel & (1u << dma_chan))) { + dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt + sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[dma_chan]); // Handle the interrupt + } + } +} + void RP2040PIOLEDStripLightOutput::setup() { ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip..."); @@ -34,24 +59,81 @@ void RP2040PIOLEDStripLightOutput::setup() { return; } + // Initialize the PIO program + // Select PIO instance to use (0 or 1) - this->pio_ = pio0; if (this->pio_ == nullptr) { ESP_LOGE(TAG, "Failed to claim PIO instance"); this->mark_failed(); return; } - // Load the assembled program into the PIO and get its location in the PIO's instruction memory - uint offset = pio_add_program(this->pio_, this->program_); + // if there are multiple strips, we can reuse the same PIO program and save space + // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO + uint offset = 0; + + if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { + ESP_LOGE(TAG, "Too many instances of PIO program"); + this->mark_failed(); + return; + } + // keep track of how many instances of the PIO program are running on each PIO + RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1]++; + + // if there are multiple strips of the same chipset, we can reuse the same PIO program and save space + if (this->conf_count_[this->chipset_]) { + offset = RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_]; + } else { + // Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it + offset = pio_add_program(this->pio_, this->program_); + RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_] = offset; + RP2040PIOLEDStripLightOutput::conf_count_[this->chipset_] = true; + } // Configure the state machine's PIO, and start it this->sm_ = pio_claim_unused_sm(this->pio_, true); if (this->sm_ < 0) { + // in theory this code should never be reached ESP_LOGE(TAG, "Failed to claim PIO state machine"); this->mark_failed(); return; } + + // Initalize the DMA channel (Note: There are 12 DMA channels and 8 state machines so we won't run out) + + this->dma_chan_ = dma_claim_unused_channel(true); + if (this->dma_chan_ < 0) { + ESP_LOGE(TAG, "Failed to claim DMA channel"); + this->mark_failed(); + return; + } + + // Mark the DMA channel as active + RP2040PIOLEDStripLightOutput::dma_chan_active_[this->dma_chan_] = true; + + this->dma_config_ = dma_channel_get_default_config(this->dma_chan_); + channel_config_set_transfer_data_size( + &this->dma_config_, + DMA_SIZE_8); // 8 bit transfers (could be 32 but the pio program would need to be changed to handle junk data) + channel_config_set_read_increment(&this->dma_config_, true); // increment the read address + channel_config_set_write_increment(&this->dma_config_, false); // don't increment the write address + channel_config_set_dreq(&this->dma_config_, + pio_get_dreq(this->pio_, this->sm_, true)); // set the DREQ to the state machine's TX FIFO + + dma_channel_configure(this->dma_chan_, &this->dma_config_, + &this->pio_->txf[this->sm_], // write to the state machine's TX FIFO + this->buf_, // read from memory + this->is_rgbw_ ? num_leds_ * 4 : num_leds_ * 3, // number of bytes to transfer + false // don't start yet + ); + + // Initialize the semaphore for this DMA channel + sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_], 1, 1); + + irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler_); // after DMA all data, raise an interrupt + dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt + irq_set_enabled(DMA_IRQ_0, true); // enable interrupt + this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_); } @@ -68,15 +150,9 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { return; } - // assemble bits in buffer to 32 bit words with ex for GBR: 0bGGGGGGGGRRRRRRRRBBBBBBBB00000000 - for (int i = 0; i < this->num_leds_; i++) { - uint8_t c1 = this->buf_[(i * 3) + 0]; - uint8_t c2 = this->buf_[(i * 3) + 1]; - uint8_t c3 = this->buf_[(i * 3) + 2]; - uint8_t w = this->is_rgbw_ ? this->buf_[(i * 4) + 3] : 0; - uint32_t color = encode_uint32(c1, c2, c3, w); - pio_sm_put_blocking(this->pio_, this->sm_, color); - } + // the bits are already in the correct order for the pio program so we can just copy the buffer using DMA + sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_]); + dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_()); } light::ESPColorView RP2040PIOLEDStripLightOutput::get_view_internal(int32_t index) const { diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h index 25ef9ca55f..7b62648974 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.h +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -9,9 +9,12 @@ #include "esphome/components/light/addressable_light.h" #include "esphome/components/light/light_output.h" +#include #include #include #include +#include +#include namespace esphome { namespace rp2040_pio_led_strip { @@ -25,6 +28,15 @@ enum RGBOrder : uint8_t { ORDER_BRG, }; +enum Chipset : uint8_t { + CHIPSET_WS2812, + CHIPSET_WS2812B, + CHIPSET_SK6812, + CHIPSET_SM16703, + CHIPSET_APA102, + CHIPSET_CUSTOM = 0xFF, +}; + inline const char *rgb_order_to_string(RGBOrder order) { switch (order) { case ORDER_RGB: @@ -69,6 +81,7 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { void set_program(const pio_program_t *program) { this->program_ = program; } void set_init_function(init_fn init) { this->init_ = init; } + void set_chipset(Chipset chipset) { this->chipset_ = chipset; }; void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } void clear_effect_data() override { for (int i = 0; i < this->size(); i++) { @@ -83,6 +96,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + static void dma_write_complete_handler_(); + uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -92,14 +107,24 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { pio_hw_t *pio_; uint sm_; + uint dma_chan_; + dma_channel_config dma_config_; RGBOrder rgb_order_{ORDER_RGB}; + Chipset chipset_{CHIPSET_CUSTOM}; uint32_t last_refresh_{0}; float max_refresh_rate_; const pio_program_t *program_; init_fn init_; + + private: + inline static int num_instance_[2]; + inline static std::map conf_count_; + inline static std::map chipset_offsets_; + inline static bool dma_chan_active_[12]; + inline static struct semaphore dma_write_complete_sem_[12]; }; } // namespace rp2040_pio_led_strip diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index 432ff6935a..8dd2549ad4 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -5,6 +5,7 @@ from esphome.components import light, rp2040 from esphome.const import ( CONF_CHIPSET, CONF_ID, + CONF_IS_RGBW, CONF_NUM_LEDS, CONF_OUTPUT_ID, CONF_PIN, @@ -67,12 +68,15 @@ static inline void rp2040_pio_led_strip_driver_{id}_init(PIO pio, uint sm, uint pio_sm_config c = rp2040_pio_led_strip_{id}_program_get_default_config(offset); sm_config_set_set_pins(&c, pin, 1); - sm_config_set_out_shift(&c, false, true, {32 if rgbw else 24}); + sm_config_set_out_shift(&c, false, true, 8); sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); - int cycles_per_bit = 69; - float div = 2.409; - sm_config_set_clkdiv(&c, div); + // target frequency is 57.5MHz + long clk = clock_get_hz(clk_sys); + long target_freq = 57500000; + int n = 2; + int f = round(((clk / target_freq) - n ) * 256); + sm_config_set_clkdiv_int_frac(&c, n, f); pio_sm_init(pio, sm, offset, &c); @@ -85,8 +89,9 @@ static inline void rp2040_pio_led_strip_driver_{id}_init(PIO pio, uint sm, uint .wrap_target awaiting_data: ; Wait for data in FIFO queue + ; out null, 24 ; discard the byte lane replication of the FIFO since we only need 8 bits (not needed????) pull block ; this will block until there is data in the FIFO queue and then it will pull it into the shift register - set y, {31 if rgbw else 23} ; set y to the number of bits to write counting 0, (23 if RGB, 31 if RGBW) + set y, 7 ; set y to the number of bits to write counting 0, (always 7 because we are doing one word at a time) mainloop: ; go through each bit in the shift register and jump to the appropriate label @@ -94,7 +99,15 @@ mainloop: out x, 1 jmp !x, writezero - jmp writeone + +writeone: + ; Write T1H and T1L bits to the output pin + set pins, 1 [{t1h}] +{nops_t1h} + set pins, 0 [{t1l}] +{nops_t1l} + jmp y--, mainloop + jmp awaiting_data writezero: ; Write T0H and T0L bits to the output pin @@ -105,14 +118,7 @@ writezero: jmp y--, mainloop jmp awaiting_data -writeone: - ; Write T1H and T1L bits to the output pin - set pins, 1 [{t1h}] -{nops_t1h} - set pins, 0 [{t1l}] -{nops_t1l} - jmp y--, mainloop - jmp awaiting_data + .wrap""" @@ -127,6 +133,7 @@ def time_to_cycles(time_us): CONF_PIO = "pio" +AUTO_LOAD = ["rp2040_pio"] CODEOWNERS = ["@Papa-DMan"] DEPENDENCIES = ["rp2040"] @@ -137,7 +144,15 @@ RP2040PIOLEDStripLightOutput = rp2040_pio_led_strip_ns.class_( RGBOrder = rp2040_pio_led_strip_ns.enum("RGBOrder") -Chipsets = rp2040_pio_led_strip_ns.enum("Chipset") +Chipset = rp2040_pio_led_strip_ns.enum("Chipset") + +CHIPSETS = { + "WS2812": Chipset.CHIPSET_WS2812, + "WS2812B": Chipset.CHIPSET_WS2812B, + "SK6812": Chipset.CHIPSET_SK6812, + "SM16703": Chipset.CHIPSET_SM16703, + "CUSTOM": Chipset.CHIPSET_CUSTOM, +} @dataclass @@ -157,14 +172,13 @@ RGB_ORDERS = { "BRG": RGBOrder.ORDER_BRG, } -CHIPSETS = { - "WS2812": LEDStripTimings(20, 43, 41, 31), - "WS2812B": LEDStripTimings(23, 46, 46, 23), - "SK6812": LEDStripTimings(17, 52, 31, 31), +CHIPSET_TIMINGS = { + "WS2812": LEDStripTimings(20, 40, 46, 34), + "WS2812B": LEDStripTimings(23, 49, 46, 26), + "SK6812": LEDStripTimings(17, 52, 34, 34), "SM16703": LEDStripTimings(17, 52, 52, 17), } -CONF_IS_RGBW = "is_rgbw" CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_LOW = "bit0_low" CONF_BIT1_HIGH = "bit1_high" @@ -191,7 +205,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), cv.Required(CONF_PIO): cv.one_of(0, 1, int=True), - cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), + cv.Optional(CONF_CHIPSET): cv.enum(CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, cv.Inclusive( CONF_BIT0_HIGH, @@ -237,7 +251,8 @@ async def to_code(config): key = f"led_strip_{id}" - if CONF_CHIPSET in config: + if chipset := config.get(CONF_CHIPSET): + cg.add(var.set_chipset(chipset)) _LOGGER.info("Generating PIO assembly code") rp2040.add_pio_file( __name__, @@ -245,13 +260,14 @@ async def to_code(config): generate_assembly_code( id, config[CONF_IS_RGBW], - CHIPSETS[config[CONF_CHIPSET]].T0H, - CHIPSETS[config[CONF_CHIPSET]].T0L, - CHIPSETS[config[CONF_CHIPSET]].T1H, - CHIPSETS[config[CONF_CHIPSET]].T1L, + CHIPSET_TIMINGS[chipset].T0H, + CHIPSET_TIMINGS[chipset].T0L, + CHIPSET_TIMINGS[chipset].T1H, + CHIPSET_TIMINGS[chipset].T1L, ), ) else: + cg.add(var.set_chipset(Chipset.CHIPSET_CUSTOM)) _LOGGER.info("Generating custom PIO assembly code") rp2040.add_pio_file( __name__, diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp index 3c5591885e..170af59905 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.cpp +++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp @@ -27,8 +27,12 @@ void RP2040PWM::setup_pwm_() { uint32_t clock = clock_get_hz(clk_sys); float divider = ceil(clock / (4096 * this->frequency_)) / 16.0f; + if (divider < 1.0f) { + divider = 1.0f; + } uint16_t wrap = clock / divider / this->frequency_ - 1; this->wrap_ = wrap; + ESP_LOGD(TAG, "divider=%.5f, wrap=%d, clock=%d", divider, wrap, clock); pwm_config_set_clkdiv(&config, divider); pwm_config_set_wrap(&config, wrap); diff --git a/esphome/components/rpi_dpi_rgb/__init__.py b/esphome/components/rpi_dpi_rgb/__init__.py new file mode 100644 index 0000000000..c58ce8a01e --- /dev/null +++ b/esphome/components/rpi_dpi_rgb/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py new file mode 100644 index 0000000000..c26143d63e --- /dev/null +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -0,0 +1,200 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import display +from esphome.components.esp32 import const, only_on_variant +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_ORDER, + CONF_DATA_PINS, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_GREEN, + CONF_HEIGHT, + CONF_HSYNC_PIN, + CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_NUMBER, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_RED, + CONF_RESET_PIN, + CONF_VSYNC_PIN, + CONF_WIDTH, +) + +DEPENDENCIES = ["esp32"] + +CONF_DE_PIN = "de_pin" +CONF_PCLK_PIN = "pclk_pin" + +CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" +CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" +CONF_HSYNC_BACK_PORCH = "hsync_back_porch" +CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" +CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" +CONF_VSYNC_BACK_PORCH = "vsync_back_porch" +CONF_PCLK_FREQUENCY = "pclk_frequency" +CONF_PCLK_INVERTED = "pclk_inverted" + +rpi_dpi_rgb_ns = cg.esphome_ns.namespace("rpi_dpi_rgb") +RPI_DPI_RGB = rpi_dpi_rgb_ns.class_("RpiDpiRgb", display.Display, cg.Component) +ColorOrder = display.display_ns.enum("ColorMode") + +COLOR_ORDERS = { + "RGB": ColorOrder.COLOR_ORDER_RGB, + "BGR": ColorOrder.COLOR_ORDER_BGR, +} +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + + +def data_pin_validate(value): + """ + It is safe to use strapping pins as RGB output data bits, as they are outputs only, + and not initialised until after boot. + """ + if not isinstance(value, dict): + try: + return DATA_PIN_SCHEMA( + {CONF_NUMBER: value, CONF_IGNORE_STRAPPING_WARNING: True} + ) + except cv.Invalid: + pass + return DATA_PIN_SCHEMA(value) + + +def data_pin_set(length): + return cv.All( + [data_pin_validate], + cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), + ) + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RPI_DPI_RGB), + cv.Required(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_, + cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_, + } + ), + ), + cv.Optional(CONF_PCLK_FREQUENCY, default="16MHz"): cv.All( + cv.frequency, cv.Range(min=4e6, max=30e6) + ), + cv.Optional(CONF_PCLK_INVERTED, default=True): cv.boolean, + cv.Required(CONF_DATA_PINS): cv.Any( + data_pin_set(16), + cv.Schema( + { + cv.Required(CONF_RED): data_pin_set(5), + cv.Required(CONF_GREEN): data_pin_set(6), + cv.Required(CONF_BLUE): data_pin_set(5), + } + ), + ), + cv.Optional(CONF_COLOR_ORDER): cv.one_of( + *COLOR_ORDERS.keys(), upper=True + ), + cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, + cv.Required(CONF_DE_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_HSYNC_PULSE_WIDTH, default=10): cv.int_, + cv.Optional(CONF_HSYNC_BACK_PORCH, default=10): cv.int_, + cv.Optional(CONF_HSYNC_FRONT_PORCH, default=20): cv.int_, + cv.Optional(CONF_VSYNC_PULSE_WIDTH, default=10): cv.int_, + cv.Optional(CONF_VSYNC_BACK_PORCH, default=10): cv.int_, + cv.Optional(CONF_VSYNC_FRONT_PORCH, default=10): cv.int_, + } + ) + ), + only_on_variant(supported=[const.VARIANT_ESP32S3]), + cv.only_with_esp_idf, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await display.register_display(var, config) + + cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) + cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) + cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH])) + cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) + cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) + cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) + cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) + index = 0 + dpins = [] + if CONF_RED in config[CONF_DATA_PINS]: + 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) + # swap bytes to match big-endian format + dpins = dpins[8:16] + dpins[0:8] + else: + dpins = config[CONF_DATA_PINS] + for pin in dpins: + data_pin = await cg.gpio_pin_expression(pin) + cg.add(var.add_data_pin(data_pin, index)) + index += 1 + + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = await cg.gpio_pin_expression(enable_pin) + cg.add(var.set_enable_pin(enable)) + + 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_DIMENSIONS in config: + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT])) + cg.add( + var.set_offsets( + dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT] + ) + ) + else: + (width, height) = dimensions + cg.add(var.set_dimensions(width, height)) + + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + pin = await cg.gpio_pin_expression(config[CONF_DE_PIN]) + cg.add(var.set_de_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN]) + cg.add(var.set_pclk_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN]) + cg.add(var.set_hsync_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN]) + cg.add(var.set_vsync_pin(pin)) diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp new file mode 100644 index 0000000000..ba09171649 --- /dev/null +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -0,0 +1,166 @@ +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "rpi_dpi_rgb.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace rpi_dpi_rgb { + +void RpiDpiRgb::setup() { + ESP_LOGCONFIG(TAG, "Setting up RPI_DPI_RGB"); + this->reset_display_(); + esp_lcd_rgb_panel_config_t config{}; + config.flags.fb_in_psram = 1; +#if ESP_IDF_VERSION_MAJOR >= 5 + config.bounce_buffer_size_px = this->width_ * 10; + config.num_fbs = 1; +#endif // ESP_IDF_VERSION_MAJOR + config.timings.h_res = this->width_; + config.timings.v_res = this->height_; + config.timings.hsync_pulse_width = this->hsync_pulse_width_; + config.timings.hsync_back_porch = this->hsync_back_porch_; + config.timings.hsync_front_porch = this->hsync_front_porch_; + config.timings.vsync_pulse_width = this->vsync_pulse_width_; + config.timings.vsync_back_porch = this->vsync_back_porch_; + config.timings.vsync_front_porch = this->vsync_front_porch_; + config.timings.flags.pclk_active_neg = this->pclk_inverted_; + config.timings.pclk_hz = this->pclk_frequency_; + config.clk_src = LCD_CLK_SRC_PLL160M; + config.psram_trans_align = 64; + size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); + for (size_t i = 0; i != data_pin_count; i++) { + config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + } + config.data_width = data_pin_count; + config.disp_gpio_num = -1; + config.hsync_gpio_num = this->hsync_pin_->get_pin(); + config.vsync_gpio_num = this->vsync_pin_->get_pin(); + config.de_gpio_num = this->de_pin_->get_pin(); + config.pclk_gpio_num = this->pclk_pin_->get_pin(); + esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); + ESP_LOGCONFIG(TAG, "RPI_DPI_RGB setup complete"); +} +void RpiDpiRgb::loop() { +#if ESP_IDF_VERSION_MAJOR >= 5 + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); +#endif // ESP_IDF_VERSION_MAJOR +} + +void RpiDpiRgb::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) { + if (w <= 0 || h <= 0) + return; + // if color mapping is required, pass the buck. + // note that endianness is not considered here - it is assumed to match! + if (bitness != display::COLOR_BITNESS_565) { + return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, + x_pad); + } + x_start += this->offset_x_; + y_start += this->offset_y_; + esp_err_t err = ESP_OK; + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y_start, x_start + w, y_start + h, ptr); + } else { + // draw line by line + auto stride = x_offset + w + x_pad; + for (int y = 0; y != h; y++) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y + y_start, x_start + w, y + y_start + 1, + ptr + ((y + y_offset) * stride + x_offset) * 2); + if (err != ESP_OK) + break; + } + } + if (err != ESP_OK) + ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); +} + +int RpiDpiRgb::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + default: + return this->get_width_internal(); + } +} + +int RpiDpiRgb::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_width_internal(); + default: + return this->get_height_internal(); + } +} + +void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; // NOLINT + + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = this->width_ - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = this->height_ - y - 1; + break; + } + auto pixel = convert_big_endian(display::ColorUtil::color_to_565(color)); + + this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true, + 0, 0, 0); + App.feed_wdt(); +} + +void RpiDpiRgb::dump_config() { + ESP_LOGCONFIG("", "RPI_DPI_RGB LCD"); + ESP_LOGCONFIG(TAG, " Height: %u", this->height_); + ESP_LOGCONFIG(TAG, " Width: %u", this->width_); + LOG_PIN(" DE Pin: ", this->de_pin_); + LOG_PIN(" Enable Pin: ", this->enable_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); + for (size_t i = 0; i != data_pin_count; i++) + ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str()); +} + +void RpiDpiRgb::reset_display_() const { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + if (this->enable_pin_ != nullptr) { + this->enable_pin_->setup(); + this->enable_pin_->digital_write(false); + } + delay(1); + this->reset_pin_->digital_write(true); + if (this->enable_pin_ != nullptr) { + delay(11); + this->enable_pin_->digital_write(true); + } + } +} + +} // namespace rpi_dpi_rgb +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h new file mode 100644 index 0000000000..7525040cd1 --- /dev/null +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h @@ -0,0 +1,97 @@ +// +// Created by Clyde Stubbs on 29/10/2023. +// +#pragma once + +// only applicable on ESP32-S3 +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/display/display.h" +#include "esp_lcd_panel_ops.h" + +#include "esp_lcd_panel_rgb.h" + +namespace esphome { +namespace rpi_dpi_rgb { + +constexpr static const char *const TAG = "rpi_dpi_rgb"; + +class RpiDpiRgb : public display::Display { + public: + void update() override { this->do_update_(); } + void setup() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + 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; + void draw_pixel_at(int x, int y, Color color) override; + + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + + void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; }; + void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; } + void set_pclk_pin(InternalGPIOPin *pclk_pin) { this->pclk_pin_ = pclk_pin; } + void set_vsync_pin(InternalGPIOPin *vsync_pin) { this->vsync_pin_ = vsync_pin; } + void set_hsync_pin(InternalGPIOPin *hsync_pin) { this->hsync_pin_ = hsync_pin; } + void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_width(uint16_t width) { this->width_ = width; } + void set_dimensions(uint16_t width, uint16_t height) { + this->width_ = width; + this->height_ = height; + } + int get_width() override; + int get_height() override; + void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } + void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } + void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } + void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; } + void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; } + void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } + void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_inverted(bool inverted) { this->pclk_inverted_ = inverted; } + void set_offsets(int16_t offset_x, int16_t offset_y) { + this->offset_x_ = offset_x; + this->offset_y_ = offset_y; + } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + void dump_config() override; + + protected: + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void reset_display_() const; + InternalGPIOPin *de_pin_{nullptr}; + InternalGPIOPin *pclk_pin_{nullptr}; + InternalGPIOPin *hsync_pin_{nullptr}; + InternalGPIOPin *vsync_pin_{nullptr}; + GPIOPin *enable_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + InternalGPIOPin *data_pins_[16] = {}; + uint16_t hsync_front_porch_ = 8; + uint16_t hsync_pulse_width_ = 4; + uint16_t hsync_back_porch_ = 8; + uint16_t vsync_front_porch_ = 8; + uint16_t vsync_pulse_width_ = 4; + uint16_t vsync_back_porch_ = 8; + uint32_t pclk_frequency_ = 16 * 1000 * 1000; + bool pclk_inverted_{true}; + + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + size_t width_{}; + size_t height_{}; + int16_t offset_x_{0}; + int16_t offset_y_{0}; + + esp_lcd_panel_handle_t handle_{}; +}; + +} // namespace rpi_dpi_rgb +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/rtl87xx/__init__.py b/esphome/components/rtl87xx/__init__.py new file mode 100644 index 0000000000..9060a7c4a6 --- /dev/null +++ b/esphome/components/rtl87xx/__init__.py @@ -0,0 +1,51 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. +# For custom pin validators, put validate_pin() or validate_usage() +# in gpio.py file in this directory. +# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA +# in schema.py file in this directory. + +from esphome import pins +from esphome.components import libretiny +from esphome.components.libretiny.const import ( + COMPONENT_RTL87XX, + KEY_COMPONENT_DATA, + KEY_LIBRETINY, + LibreTinyComponent, +) +from esphome.core import CORE + +from .boards import RTL87XX_BOARDS, RTL87XX_BOARD_PINS + +CODEOWNERS = ["@kuba2k2"] +AUTO_LOAD = ["libretiny"] + +COMPONENT_DATA = LibreTinyComponent( + name=COMPONENT_RTL87XX, + boards=RTL87XX_BOARDS, + board_pins=RTL87XX_BOARD_PINS, + pin_validation=None, + usage_validation=None, +) + + +def _set_core_data(config): + CORE.data[KEY_LIBRETINY] = {} + CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA + return config + + +CONFIG_SCHEMA = libretiny.BASE_SCHEMA + +PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA + +CONFIG_SCHEMA.prepend_extra(_set_core_data) + + +async def to_code(config): + return await libretiny.component_to_code(config) + + +@pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA) +async def pin_to_code(config): + return await libretiny.gpio.component_pin_to_code(config) diff --git a/esphome/components/rtl87xx/boards.py b/esphome/components/rtl87xx/boards.py new file mode 100644 index 0000000000..e737767a56 --- /dev/null +++ b/esphome/components/rtl87xx/boards.py @@ -0,0 +1,1404 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. + +from esphome.components.libretiny.const import FAMILY_RTL8710B, FAMILY_RTL8720C + +RTL87XX_BOARDS = { + "bw12": { + "name": "BW12", + "family": FAMILY_RTL8710B, + }, + "bw15": { + "name": "BW15", + "family": FAMILY_RTL8720C, + }, + "generic-rtl8710bn-2mb-468k": { + "name": "Generic - RTL8710BN (2M/468k)", + "family": FAMILY_RTL8710B, + }, + "generic-rtl8710bn-2mb-788k": { + "name": "Generic - RTL8710BN (2M/788k)", + "family": FAMILY_RTL8710B, + }, + "generic-rtl8710bx-4mb-980k": { + "name": "Generic - RTL8710BX (4M/980k)", + "family": FAMILY_RTL8710B, + }, + "generic-rtl8720cf-2mb-992k": { + "name": "Generic - RTL8720CF (2M/992k)", + "family": FAMILY_RTL8720C, + }, + "t102-v1.1": { + "name": "T102_V1.1", + "family": FAMILY_RTL8710B, + }, + "t103-v1.0": { + "name": "T103_V1.0", + "family": FAMILY_RTL8710B, + }, + "t112-v1.1": { + "name": "T112_V1.1", + "family": FAMILY_RTL8710B, + }, + "wr1": { + "name": "WR1 Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr1e": { + "name": "WR1E Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr2": { + "name": "WR2 Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr2e": { + "name": "WR2E Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr2l": { + "name": "WR2L Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr2le": { + "name": "WR2LE Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr3": { + "name": "WR3 Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr3e": { + "name": "WR3E Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr3l": { + "name": "WR3L Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr3le": { + "name": "WR3LE Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, + "wr3n": { + "name": "WR3N Wi-Fi Module", + "family": FAMILY_RTL8710B, + }, +} + +RTL87XX_BOARD_PINS = { + "bw12": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 29, + "WIRE0_SCL_1": 22, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 30, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 5, + "D1": 29, + "D2": 0, + "D3": 19, + "D4": 22, + "D5": 30, + "D6": 14, + "D7": 12, + "D8": 15, + "D9": 18, + "D10": 23, + "A0": 19, + }, + "bw15": { + "SPI0_CS_0": 2, + "SPI0_CS_1": 15, + "SPI0_MISO": 20, + "SPI0_MOSI_0": 4, + "SPI0_MOSI_1": 19, + "SPI0_SCK_0": 16, + "SPI0_SCK_1": 3, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 15, + "WIRE0_SCL_2": 19, + "WIRE0_SDA_0": 20, + "WIRE0_SDA_1": 16, + "WIRE0_SDA_2": 3, + "SERIAL0_RX": 13, + "SERIAL0_TX": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX_0": 2, + "SERIAL1_RX_1": 0, + "SERIAL1_TX_0": 3, + "SERIAL1_TX_1": 1, + "SERIAL2_CTS": 19, + "SERIAL2_RTS": 20, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CTS1": 4, + "CTS2": 19, + "MISO0": 20, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PA19": 19, + "PA20": 20, + "PWM1": 1, + "PWM5": 17, + "PWM6": 18, + "RTS2": 20, + "RX0": 13, + "RX2": 15, + "SCL0": 19, + "SDA0": 3, + "TX0": 14, + "TX2": 16, + "D0": 17, + "D1": 18, + "D2": 2, + "D3": 15, + "D4": 4, + "D5": 19, + "D6": 20, + "D7": 16, + "D8": 0, + "D9": 3, + "D10": 1, + "D11": 13, + "D12": 14, + }, + "generic-rtl8710bn-2mb-468k": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "FCS": 6, + "FD0": 9, + "FD1": 7, + "FD2": 8, + "FD3": 11, + "FSCK": 10, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA06": 6, + "PA6": 6, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 30, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 0, + "D1": 5, + "D2": 6, + "D3": 7, + "D4": 8, + "D5": 9, + "D6": 10, + "D7": 11, + "D8": 12, + "D9": 14, + "D10": 15, + "D11": 18, + "D12": 19, + "D13": 22, + "D14": 23, + "D15": 29, + "D16": 30, + "A0": 19, + "A1": 41, + }, + "generic-rtl8710bn-2mb-788k": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "FCS": 6, + "FD0": 9, + "FD1": 7, + "FD2": 8, + "FD3": 11, + "FSCK": 10, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA06": 6, + "PA6": 6, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 30, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 0, + "D1": 5, + "D2": 6, + "D3": 7, + "D4": 8, + "D5": 9, + "D6": 10, + "D7": 11, + "D8": 12, + "D9": 14, + "D10": 15, + "D11": 18, + "D12": 19, + "D13": 22, + "D14": 23, + "D15": 29, + "D16": 30, + "A0": 19, + "A1": 41, + }, + "generic-rtl8710bx-4mb-980k": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "FCS": 6, + "FD0": 9, + "FD1": 7, + "FD2": 8, + "FD3": 11, + "FSCK": 10, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA06": 6, + "PA6": 6, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 30, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 0, + "D1": 5, + "D2": 6, + "D3": 7, + "D4": 8, + "D5": 9, + "D6": 10, + "D7": 11, + "D8": 12, + "D9": 14, + "D10": 15, + "D11": 18, + "D12": 19, + "D13": 22, + "D14": 23, + "D15": 29, + "D16": 30, + "A0": 19, + }, + "generic-rtl8720cf-2mb-992k": { + "SPI0_CS_0": 2, + "SPI0_CS_1": 7, + "SPI0_CS_2": 15, + "SPI0_MISO_0": 10, + "SPI0_MISO_1": 20, + "SPI0_MOSI_0": 4, + "SPI0_MOSI_1": 9, + "SPI0_MOSI_2": 19, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 8, + "SPI0_SCK_2": 16, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SCL_3": 19, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 16, + "WIRE0_SDA_3": 20, + "SERIAL0_CTS": 10, + "SERIAL0_RTS": 9, + "SERIAL0_RX_0": 12, + "SERIAL0_RX_1": 13, + "SERIAL0_TX_0": 11, + "SERIAL0_TX_1": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, + "SERIAL2_CTS": 19, + "SERIAL2_RTS": 20, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CS0": 15, + "CTS0": 10, + "CTS1": 4, + "CTS2": 19, + "MOSI0": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PA19": 19, + "PA20": 20, + "PA23": 23, + "PWM0": 20, + "PWM5": 17, + "PWM6": 18, + "PWM7": 23, + "RTS0": 9, + "RTS2": 20, + "RX2": 15, + "SCK0": 16, + "TX2": 16, + "D0": 0, + "D1": 1, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 7, + "D6": 8, + "D7": 9, + "D8": 10, + "D9": 11, + "D10": 12, + "D11": 13, + "D12": 14, + "D13": 15, + "D14": 16, + "D15": 17, + "D16": 18, + "D17": 19, + "D18": 20, + "D19": 23, + }, + "t102-v1.1": { + "WIRE0_SCL": 29, + "WIRE0_SDA": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 29, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL0": 29, + "SCL1": 18, + "SDA0": 30, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 12, + "D1": 0, + "D2": 5, + "D3": 30, + "D4": 29, + "D5": 18, + "D6": 23, + "D7": 14, + "D8": 15, + }, + "t103-v1.0": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 5, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 19, + "D1": 14, + "D2": 15, + "D3": 0, + "D4": 22, + "D5": 29, + "D6": 30, + "D7": 5, + "D8": 12, + "D9": 18, + "D10": 23, + "A0": 19, + "A1": 41, + }, + "t112-v1.1": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 29, + "WIRE0_SCL_1": 22, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 30, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 29, + "D1": 19, + "D2": 15, + "D3": 14, + "D4": 0, + "D5": 5, + "D6": 18, + "D7": 12, + "D8": 23, + "D9": 22, + "D10": 30, + "A0": 19, + }, + "wr1": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 29, + "WIRE0_SCL_1": 22, + "WIRE0_SDA_0": 30, + "WIRE0_SDA_1": 19, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM4": 29, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 23, + "D1": 18, + "D2": 14, + "D3": 15, + "D4": 30, + "D5": 0, + "D6": 5, + "D7": 29, + "D8": 19, + "D9": 22, + "A0": 19, + "A1": 41, + }, + "wr1e": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 29, + "WIRE0_SCL_1": 22, + "WIRE0_SDA_0": 30, + "WIRE0_SDA_1": 19, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM3": 12, + "PWM4": 29, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 23, + "D1": 18, + "D2": 14, + "D3": 15, + "D4": 30, + "D5": 12, + "D6": 5, + "D7": 29, + "D8": 19, + "D9": 22, + "A0": 19, + "A1": 41, + }, + "wr2": { + "WIRE0_SCL": 29, + "WIRE0_SDA": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC2": 41, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 29, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL0": 29, + "SCL1": 18, + "SDA0": 30, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 12, + "D1": 0, + "D2": 5, + "D4": 18, + "D5": 23, + "D6": 14, + "D7": 15, + "D8": 30, + "D9": 29, + "A1": 41, + }, + "wr2e": { + "WIRE0_SCL": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MOSI0": 23, + "MOSI1": 23, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM3": 12, + "PWM4": 29, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL0": 29, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 12, + "D1": 19, + "D2": 5, + "D3": 18, + "D4": 23, + "D5": 14, + "D6": 15, + "D7": 30, + "D8": 29, + "A0": 19, + "A1": 41, + }, + "wr2l": { + "ADC1": 19, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA19": 19, + "PWM0": 14, + "PWM1": 15, + "PWM3": 12, + "PWM4": 5, + "SDA0": 19, + "D0": 15, + "D1": 14, + "D2": 5, + "D3": 19, + "D4": 12, + "A0": 19, + }, + "wr2le": { + "MISO0": 22, + "MISO1": 22, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA22": 22, + "PWM0": 14, + "PWM1": 15, + "PWM3": 12, + "PWM4": 5, + "PWM5": 22, + "RTS0": 22, + "SCL0": 22, + "D0": 15, + "D1": 14, + "D2": 5, + "D3": 22, + "D4": 12, + }, + "wr3": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 5, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 22, + "D1": 19, + "D2": 14, + "D3": 15, + "D4": 0, + "D5": 29, + "D6": 30, + "D7": 5, + "D8": 12, + "D9": 18, + "D10": 23, + "A0": 19, + "A1": 41, + }, + "wr3e": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 29, + "WIRE0_SCL_1": 22, + "WIRE0_SDA_0": 30, + "WIRE0_SDA_1": 19, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 5, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 29, + "D1": 14, + "D2": 15, + "D3": 22, + "D4": 0, + "D5": 30, + "D6": 19, + "D7": 5, + "D8": 12, + "D9": 18, + "D10": 23, + "A0": 19, + "A1": 41, + }, + "wr3l": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 5, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 22, + "D1": 19, + "D2": 14, + "D3": 15, + "D4": 0, + "D5": 29, + "D6": 30, + "D7": 5, + "D8": 12, + "D9": 18, + "D10": 23, + "A0": 19, + "A1": 41, + }, + "wr3le": { + "SPI0_CS": 19, + "SPI0_MISO": 22, + "SPI0_MOSI": 23, + "SPI0_SCK": 18, + "SPI1_CS": 19, + "SPI1_MISO": 22, + "SPI1_MOSI": 23, + "SPI1_SCK": 18, + "WIRE0_SCL_0": 29, + "WIRE0_SCL_1": 22, + "WIRE0_SDA_0": 30, + "WIRE0_SDA_1": 19, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_CTS": 19, + "SERIAL0_RTS": 22, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC1": 19, + "ADC2": 41, + "CS0": 19, + "CS1": 19, + "CTS0": 19, + "MISO0": 22, + "MISO1": 22, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA19": 19, + "PA22": 22, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 5, + "PWM5": 22, + "RTS0": 22, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL1": 18, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 29, + "D1": 14, + "D2": 15, + "D3": 22, + "D4": 0, + "D5": 30, + "D6": 19, + "D7": 5, + "D8": 12, + "D9": 18, + "D10": 23, + "A0": 19, + "A1": 41, + }, + "wr3n": { + "WIRE0_SCL": 29, + "WIRE0_SDA": 30, + "WIRE1_SCL": 18, + "WIRE1_SDA": 23, + "SERIAL0_RX": 18, + "SERIAL0_TX": 23, + "SERIAL2_RX": 29, + "SERIAL2_TX": 30, + "ADC2": 41, + "MOSI0": 23, + "MOSI1": 23, + "PA00": 0, + "PA0": 0, + "PA05": 5, + "PA5": 5, + "PA12": 12, + "PA14": 14, + "PA15": 15, + "PA18": 18, + "PA23": 23, + "PA29": 29, + "PA30": 30, + "PWM1": 15, + "PWM2": 0, + "PWM3": 12, + "PWM4": 5, + "RX0": 18, + "RX2": 29, + "SCK0": 18, + "SCK1": 18, + "SCL0": 29, + "SCL1": 18, + "SDA0": 30, + "SDA1": 23, + "TX0": 23, + "TX2": 30, + "D0": 29, + "D1": 14, + "D2": 15, + "D3": 0, + "D4": 30, + "D5": 5, + "D6": 12, + "D7": 18, + "D8": 23, + "A1": 41, + }, +} + +BOARDS = RTL87XX_BOARDS diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index e9453896ac..10f1313408 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -4,7 +4,16 @@ import esphome.config_validation as cv import esphome.final_validate as fv from esphome import automation from esphome.components.output import FloatOutput -from esphome.const import CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_TRIGGER_ID +from esphome.components.speaker import Speaker + +from esphome.const import ( + CONF_ID, + CONF_OUTPUT, + CONF_PLATFORM, + CONF_TRIGGER_ID, + CONF_SPEAKER, + CONF_GAIN, +) _LOGGER = logging.getLogger(__name__) @@ -24,17 +33,24 @@ IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), - cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FinishedPlaybackTrigger), - } - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), + cv.Optional(CONF_OUTPUT): cv.use_id(FloatOutput), + cv.Optional(CONF_SPEAKER): cv.use_id(Speaker), + cv.Optional(CONF_GAIN, default="0.6"): cv.percentage, + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FinishedPlaybackTrigger + ), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_OUTPUT, CONF_SPEAKER), +) def validate_parent_output_config(value): @@ -63,9 +79,9 @@ def validate_parent_output_config(value): FINAL_VALIDATE_SCHEMA = cv.Schema( { - cv.Required(CONF_OUTPUT): fv.id_declaration_match_schema( + cv.Optional(CONF_OUTPUT): fv.id_declaration_match_schema( validate_parent_output_config - ) + ), }, extra=cv.ALLOW_EXTRA, ) @@ -75,8 +91,16 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - out = await cg.get_variable(config[CONF_OUTPUT]) - cg.add(var.set_output(out)) + if CONF_OUTPUT in config: + out = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(out)) + cg.add_define("USE_OUTPUT") + + if CONF_SPEAKER in config: + out = await cg.get_variable(config[CONF_SPEAKER]) + cg.add(var.set_speaker(out)) + + cg.add(var.set_gain(config[CONF_GAIN])) for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 6274e69ba3..db4cc731e4 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -1,4 +1,5 @@ #include "rtttl.h" +#include #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -15,104 +16,215 @@ static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; -void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } +static const uint16_t I2S_SPEED = 1000; + +#undef HALF_PI +static const double HALF_PI = 1.5707963267948966192313216916398; + +inline double deg2rad(double degrees) { + static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0; + return degrees * PI_ON_180; +} + +void Rtttl::dump_config() { + ESP_LOGCONFIG(TAG, "Rtttl:"); + ESP_LOGCONFIG(TAG, " Gain: %f", gain_); +} void Rtttl::play(std::string rtttl) { - rtttl_ = std::move(rtttl); + if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { + int pos = this->rtttl_.find(':'); + auto name = this->rtttl_.substr(0, pos); + ESP_LOGW(TAG, "RTTTL Component is already playing: %s", name.c_str()); + return; + } + + this->rtttl_ = std::move(rtttl); + + this->default_duration_ = 4; + this->default_octave_ = 6; + this->note_duration_ = 0; - default_duration_ = 4; - default_octave_ = 6; int bpm = 63; uint8_t num; // Get name - position_ = rtttl_.find(':'); + this->position_ = rtttl_.find(':'); // it's somewhat documented to be up to 10 characters but let's be a bit flexible here - if (position_ == std::string::npos || position_ > 15) { + if (this->position_ == std::string::npos || this->position_ > 15) { ESP_LOGE(TAG, "Missing ':' when looking for name."); return; } - auto name = this->rtttl_.substr(0, position_); + auto name = this->rtttl_.substr(0, this->position_); ESP_LOGD(TAG, "Playing song %s", name.c_str()); // get default duration - position_ = this->rtttl_.find("d=", position_); - if (position_ == std::string::npos) { + this->position_ = this->rtttl_.find("d=", this->position_); + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing 'd='"); return; } - position_ += 2; + this->position_ += 2; num = this->get_integer_(); if (num > 0) - default_duration_ = num; + this->default_duration_ = num; // get default octave - position_ = rtttl_.find("o=", position_); - if (position_ == std::string::npos) { + this->position_ = this->rtttl_.find("o=", this->position_); + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing 'o="); return; } - position_ += 2; + this->position_ += 2; num = get_integer_(); if (num >= 3 && num <= 7) - default_octave_ = num; + this->default_octave_ = num; // get BPM - position_ = rtttl_.find("b=", position_); - if (position_ == std::string::npos) { + this->position_ = this->rtttl_.find("b=", this->position_); + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing b="); return; } - position_ += 2; + this->position_ += 2; num = get_integer_(); if (num != 0) bpm = num; - position_ = rtttl_.find(':', position_); - if (position_ == std::string::npos) { + this->position_ = this->rtttl_.find(':', this->position_); + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing second ':'"); return; } - position_++; + this->position_++; // BPM usually expresses the number of quarter notes per minute - wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) - output_freq_ = 0; - last_note_ = millis(); - note_duration_ = 1; + this->output_freq_ = 0; + this->last_note_ = millis(); + this->note_duration_ = 1; + +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + this->set_state_(State::STATE_INIT); + this->samples_sent_ = 0; + this->samples_count_ = 0; + } +#endif +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->set_state_(State::STATE_RUNNING); + } +#endif +} + +void Rtttl::stop() { +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(STATE_STOPPED); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + if (this->speaker_->is_running()) { + this->speaker_->stop(); + } + this->set_state_(STATE_STOPPING); + } +#endif + this->note_duration_ = 0; } void Rtttl::loop() { - if (note_duration_ == 0 || millis() - last_note_ < note_duration_) + if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) return; - if (!rtttl_[position_]) { - output_->set_level(0.0); - ESP_LOGD(TAG, "Playback finished"); - this->on_finished_playback_callback_.call(); - note_duration_ = 0; +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + if (this->state_ == State::STATE_STOPPING) { + if (this->speaker_->is_stopped()) { + this->set_state_(State::STATE_STOPPED); + } + } else if (this->state_ == State::STATE_INIT) { + if (this->speaker_->is_stopped()) { + this->speaker_->start(); + this->set_state_(State::STATE_STARTING); + } + } else if (this->state_ == State::STATE_STARTING) { + if (this->speaker_->is_running()) { + this->set_state_(State::STATE_RUNNING); + } + } + if (!this->speaker_->is_running()) { + return; + } + if (this->samples_sent_ != this->samples_count_) { + SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; + int x = 0; + double rem = 0.0; + + while (true) { + // Try and send out the remainder of the existing note, one per loop() + + if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note// + rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); + + int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152 + + sample[x].left = val; + sample[x].right = val; + + } else { + sample[x].left = 0; + sample[x].right = 0; + } + + if (x >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { + break; + } + this->samples_sent_++; + x++; + } + if (x > 0) { + int send = this->speaker_->play((uint8_t *) (&sample), x * 2); + if (send != x * 4) { + this->samples_sent_ -= (x - (send / 2)); + } + return; + } + } + } +#endif +#ifdef USE_OUTPUT + if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) + return; +#endif + if (!this->rtttl_[position_]) { + this->finish_(); return; } // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... - while (rtttl_[position_] == ',' || rtttl_[position_] == ' ') - position_++; + while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') + this->position_++; // first, get note duration, if available uint8_t num = this->get_integer_(); if (num) { - note_duration_ = wholenote_ / num; + this->note_duration_ = this->wholenote_ / num; } else { - note_duration_ = wholenote_ / default_duration_; // we will need to check if we are a dotted note after + this->note_duration_ = + this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after } uint8_t note; - switch (rtttl_[position_]) { + switch (this->rtttl_[this->position_]) { case 'c': note = 1; break; @@ -131,6 +243,7 @@ void Rtttl::loop() { case 'a': note = 10; break; + case 'h': case 'b': note = 12; break; @@ -138,51 +251,145 @@ void Rtttl::loop() { default: note = 0; } - position_++; + this->position_++; // now, get optional '#' sharp - if (rtttl_[position_] == '#') { + if (this->rtttl_[this->position_] == '#') { note++; - position_++; + this->position_++; } // now, get optional '.' dotted note - if (rtttl_[position_] == '.') { - note_duration_ += note_duration_ / 2; - position_++; + if (this->rtttl_[this->position_] == '.') { + this->note_duration_ += this->note_duration_ / 2; + this->position_++; } // now, get scale uint8_t scale = get_integer_(); if (scale == 0) - scale = default_octave_; + scale = this->default_octave_; + + if (scale < 4 || scale > 7) { + ESP_LOGE(TAG, "Octave out of valid range. Should be between 4 and 7. (Octave: %d)", scale); + this->finish_(); + return; + } + bool need_note_gap = false; // Now play the note if (note) { auto note_index = (scale - 4) * 12 + note; if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { - ESP_LOGE(TAG, "Note out of valid range"); + ESP_LOGE(TAG, "Note out of valid range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, + (int) sizeof(NOTES)); + this->finish_(); return; } auto freq = NOTES[note_index]; + need_note_gap = freq == this->output_freq_; - if (freq == output_freq_) { - // Add small silence gap between same note - output_->set_level(0.0); - delay(DOUBLE_NOTE_GAP_MS); - note_duration_ -= DOUBLE_NOTE_GAP_MS; - } - output_freq_ = freq; + // Add small silence gap between same note + this->output_freq_ = freq; - ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_); - output_->update_frequency(freq); - output_->set_level(0.5); + ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_); } else { - ESP_LOGVV(TAG, "waiting: %dms", note_duration_); - output_->set_level(0.0); + ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_); + this->output_freq_ = 0; } - last_note_ = millis(); +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + if (need_note_gap) { + this->output_->set_level(0.0); + delay(DOUBLE_NOTE_GAP_MS); + this->note_duration_ -= DOUBLE_NOTE_GAP_MS; + } + if (this->output_freq_ != 0) { + this->output_->update_frequency(this->output_freq_); + this->output_->set_level(this->gain_); + } else { + this->output_->set_level(0.0); + } + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + this->samples_sent_ = 0; + this->samples_gap_ = 0; + this->samples_per_wave_ = 0; + this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms); + if (need_note_gap) { + this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); + } + if (this->output_freq_ != 0) { + // make sure there is enough samples to add a full last sinus. + + uint16_t samples_wish = this->samples_count_; + this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_; + + uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1; + + this->samples_count_ = (division * this->samples_per_wave_); + this->samples_count_ = this->samples_count_ >> 10; + ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_, + division, this->samples_per_wave_); + } + // Convert from frequency in Hz to high and low samples in fixed point + } +#endif + + this->last_note_ = millis(); } + +void Rtttl::finish_() { +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + + this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); + } +#endif + this->note_duration_ = 0; + this->on_finished_playback_callback_.call(); + ESP_LOGD(TAG, "Playback finished"); +} + +static const LogString *state_to_string(State state) { + switch (state) { + case STATE_STOPPED: + return LOG_STR("STATE_STOPPED"); + case STATE_STARTING: + return LOG_STR("STATE_STARTING"); + case STATE_RUNNING: + return LOG_STR("STATE_RUNNING"); + case STATE_STOPPING: + return LOG_STR("STATE_STOPPING"); + case STATE_INIT: + return LOG_STR("STATE_INIT"); + default: + return LOG_STR("UNKNOWN"); + } +}; + +void Rtttl::set_state_(State state) { + State old_state = this->state_; + this->state_ = state; + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), + LOG_STR_ARG(state_to_string(state))); +} + } // namespace rtttl } // namespace esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index ec6fe7f98f..10c290c5fb 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -1,23 +1,57 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" + +#ifdef USE_OUTPUT #include "esphome/components/output/float_output.h" +#endif + +#ifdef USE_SPEAKER +#include "esphome/components/speaker/speaker.h" +#endif namespace esphome { namespace rtttl { +enum State : uint8_t { + STATE_STOPPED = 0, + STATE_INIT, + STATE_STARTING, + STATE_RUNNING, + STATE_STOPPING, +}; + +#ifdef USE_SPEAKER +static const size_t SAMPLE_BUFFER_SIZE = 2048; + +struct SpeakerSample { + int8_t left{0}; + int8_t right{0}; +}; +#endif + class Rtttl : public Component { public: - void set_output(output::FloatOutput *output) { output_ = output; } - void play(std::string rtttl); - void stop() { - note_duration_ = 0; - output_->set_level(0.0); +#ifdef USE_OUTPUT + void set_output(output::FloatOutput *output) { this->output_ = output; } +#endif +#ifdef USE_SPEAKER + void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } +#endif + float get_gain() { return gain_; } + void set_gain(float gain) { + if (gain < 0.1f) + gain = 0.1f; + if (gain > 1.0f) + gain = 1.0f; + this->gain_ = gain; } + void play(std::string rtttl); + void stop(); void dump_config() override; - bool is_playing() { return note_duration_ != 0; } + bool is_playing() { return this->state_ != State::STATE_STOPPED; } void loop() override; void add_on_finished_playback_callback(std::function callback) { @@ -27,14 +61,16 @@ class Rtttl : public Component { protected: inline uint8_t get_integer_() { uint8_t ret = 0; - while (isdigit(rtttl_[position_])) { - ret = (ret * 10) + (rtttl_[position_++] - '0'); + while (isdigit(this->rtttl_[this->position_])) { + ret = (ret * 10) + (this->rtttl_[this->position_++] - '0'); } return ret; } + void finish_(); + void set_state_(State state); - std::string rtttl_; - size_t position_; + std::string rtttl_{""}; + size_t position_{0}; uint16_t wholenote_; uint16_t default_duration_; uint16_t default_octave_; @@ -42,7 +78,22 @@ class Rtttl : public Component { uint16_t note_duration_; uint32_t output_freq_; + float gain_{0.6f}; + State state_{State::STATE_STOPPED}; + +#ifdef USE_OUTPUT output::FloatOutput *output_; +#endif + +#ifdef USE_SPEAKER + speaker::Speaker *speaker_{nullptr}; + int sample_rate_{16000}; + int samples_per_wave_{0}; + int samples_sent_{0}; + int samples_count_{0}; + int samples_gap_{0}; + +#endif CallbackManager on_finished_playback_callback_; }; diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index ab884bfee4..185c0e70b1 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -1,5 +1,75 @@ +from esphome.cpp_generator import RawExpression import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_DISABLED, + CONF_ID, + CONF_NUM_ATTEMPTS, + CONF_REBOOT_TIMEOUT, + CONF_SAFE_MODE, + CONF_TRIGGER_ID, + KEY_PAST_SAFE_MODE, +) +from esphome.core import CORE, coroutine_with_priority +from esphome import automation -CODEOWNERS = ["@paulmonigatti", "@jsuanet"] + +CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] + +CONF_BOOT_IS_GOOD_AFTER = "boot_is_good_after" +CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") +SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) +SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) + + +def _remove_id_if_disabled(value): + value = value.copy() + if value[CONF_DISABLED]: + value.pop(CONF_ID) + return value + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SafeModeComponent), + cv.Optional( + CONF_BOOT_IS_GOOD_AFTER, default="1min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_DISABLED, default=False): cv.boolean, + cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="5min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SafeModeTrigger), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + _remove_id_if_disabled, +) + + +@coroutine_with_priority(50.0) +async def to_code(config): + if not config[CONF_DISABLED]: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + for conf in config.get(CONF_ON_SAFE_MODE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + condition = var.should_enter_safe_mode( + config[CONF_NUM_ATTEMPTS], + config[CONF_REBOOT_TIMEOUT], + config[CONF_BOOT_IS_GOOD_AFTER], + ) + cg.add(RawExpression(f"if ({condition}) return")) + + CORE.data[CONF_SAFE_MODE] = {} + CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h new file mode 100644 index 0000000000..d1388449ee --- /dev/null +++ b/esphome/components/safe_mode/automation.h @@ -0,0 +1,17 @@ +#pragma once +#include "safe_mode.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace safe_mode { + +class SafeModeTrigger : public Trigger<> { + public: + explicit SafeModeTrigger(SafeModeComponent *parent) { + parent->add_on_safe_mode_callback([this, parent]() { trigger(); }); + } +}; + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py index 307e4e372e..5662db8f7e 100644 --- a/esphome/components/safe_mode/button/__init__.py +++ b/esphome/components/safe_mode/button/__init__.py @@ -1,18 +1,16 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import button -from esphome.components.ota import OTAComponent from esphome.const import ( - CONF_ID, - CONF_OTA, + CONF_SAFE_MODE, DEVICE_CLASS_RESTART, ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT, ) +from .. import safe_mode_ns, SafeModeComponent -DEPENDENCIES = ["ota"] +DEPENDENCIES = ["safe_mode"] -safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component) CONFIG_SCHEMA = ( @@ -22,15 +20,14 @@ CONFIG_SCHEMA = ( entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_RESTART_ALERT, ) - .extend({cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent)}) + .extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)}) .extend(cv.COMPONENT_SCHEMA) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await button.new_button(config) await cg.register_component(var, config) - await button.register_button(var, config) - ota = await cg.get_variable(config[CONF_OTA]) - cg.add(var.set_ota(ota)) + safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE]) + cg.add(var.set_safe_mode(safe_mode_component)) diff --git a/esphome/components/safe_mode/button/safe_mode_button.cpp b/esphome/components/safe_mode/button/safe_mode_button.cpp index 2b8654de46..261688807a 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.cpp +++ b/esphome/components/safe_mode/button/safe_mode_button.cpp @@ -8,11 +8,13 @@ namespace safe_mode { static const char *const TAG = "safe_mode.button"; -void SafeModeButton::set_ota(ota::OTAComponent *ota) { this->ota_ = ota; } +void SafeModeButton::set_safe_mode(SafeModeComponent *safe_mode_component) { + this->safe_mode_component_ = safe_mode_component; +} void SafeModeButton::press_action() { ESP_LOGI(TAG, "Restarting device in safe mode..."); - this->ota_->set_safe_mode_pending(true); + this->safe_mode_component_->set_safe_mode_pending(true); // Let MQTT settle a bit delay(100); // NOLINT diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h index 63e0d1755e..fea0955abb 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.h +++ b/esphome/components/safe_mode/button/safe_mode_button.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/ota/ota_component.h" #include "esphome/components/button/button.h" +#include "esphome/components/safe_mode/safe_mode.h" +#include "esphome/core/component.h" namespace esphome { namespace safe_mode { @@ -10,10 +10,10 @@ namespace safe_mode { class SafeModeButton : public button::Button, public Component { public: void dump_config() override; - void set_ota(ota::OTAComponent *ota); + void set_safe_mode(SafeModeComponent *safe_mode_component); protected: - ota::OTAComponent *ota_; + SafeModeComponent *safe_mode_component_; void press_action() override; }; diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp new file mode 100644 index 0000000000..aa1a4b6822 --- /dev/null +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -0,0 +1,131 @@ +#include "safe_mode.h" + +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include +#include + +namespace esphome { +namespace safe_mode { + +static const char *const TAG = "safe_mode"; + +void SafeModeComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Safe Mode:"); + ESP_LOGCONFIG(TAG, " Boot considered successful after %" PRIu32 " seconds", + this->safe_mode_boot_is_good_after_ / 1000); // because milliseconds + ESP_LOGCONFIG(TAG, " Invoke after %u boot attempts", this->safe_mode_num_attempts_); + ESP_LOGCONFIG(TAG, " Remain in safe mode for %" PRIu32 " seconds", + this->safe_mode_enable_time_ / 1000); // because milliseconds + + if (this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) { + auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_; + if (remaining_restarts) { + ESP_LOGW(TAG, "Last reset occurred too quickly; safe mode will be invoked in %" PRIu32 " restarts", + remaining_restarts); + } else { + ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); + } + } +} + +float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } + +void SafeModeComponent::loop() { + if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) { + // successful boot, reset counter + ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); + this->clean_rtc(); + this->boot_successful_ = true; + } +} + +void SafeModeComponent::set_safe_mode_pending(const bool &pending) { + uint32_t current_rtc = this->read_rtc_(); + + if (pending && current_rtc != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) { + ESP_LOGI(TAG, "Device will enter safe mode on next boot"); + this->write_rtc_(SafeModeComponent::ENTER_SAFE_MODE_MAGIC); + } + + if (!pending && current_rtc == SafeModeComponent::ENTER_SAFE_MODE_MAGIC) { + ESP_LOGI(TAG, "Safe mode pending has been cleared"); + this->clean_rtc(); + } +} + +bool SafeModeComponent::get_safe_mode_pending() { + return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; +} + +bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, + uint32_t boot_is_good_after) { + this->safe_mode_start_time_ = millis(); + this->safe_mode_enable_time_ = enable_time; + this->safe_mode_boot_is_good_after_ = boot_is_good_after; + this->safe_mode_num_attempts_ = num_attempts; + this->rtc_ = global_preferences->make_preference(233825507UL, false); + this->safe_mode_rtc_value_ = this->read_rtc_(); + + bool is_manual_safe_mode = this->safe_mode_rtc_value_ == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; + + if (is_manual_safe_mode) { + ESP_LOGI(TAG, "Safe mode invoked manually"); + } else { + ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts", this->safe_mode_rtc_value_); + } + + if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) { + this->clean_rtc(); + + if (!is_manual_safe_mode) { + ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode"); + } + + this->status_set_error(); + this->set_timeout(enable_time, []() { + ESP_LOGW(TAG, "Safe mode enable time has elapsed -- restarting"); + App.reboot(); + }); + + // Delay here to allow power to stabilize before Wi-Fi/Ethernet is initialised + delay(300); // NOLINT + App.setup(); + + ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); + + this->safe_mode_callback_.call(); + + return true; + } else { + // increment counter + this->write_rtc_(this->safe_mode_rtc_value_ + 1); + return false; + } +} + +void SafeModeComponent::write_rtc_(uint32_t val) { + this->rtc_.save(&val); + global_preferences->sync(); +} + +uint32_t SafeModeComponent::read_rtc_() { + uint32_t val; + if (!this->rtc_.load(&val)) + return 0; + return val; +} + +void SafeModeComponent::clean_rtc() { this->write_rtc_(0); } + +void SafeModeComponent::on_safe_shutdown() { + if (this->read_rtc_() != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) + this->clean_rtc(); +} + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h new file mode 100644 index 0000000000..37e2c3a3d6 --- /dev/null +++ b/esphome/components/safe_mode/safe_mode.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace safe_mode { + +/// SafeModeComponent provides a safe way to recover from repeated boot failures +class SafeModeComponent : public Component { + public: + bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after); + + /// Set to true if the next startup will enter safe mode + void set_safe_mode_pending(const bool &pending); + bool get_safe_mode_pending(); + + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + void clean_rtc(); + + void on_safe_shutdown() override; + + void add_on_safe_mode_callback(std::function &&callback) { + this->safe_mode_callback_.add(std::move(callback)); + } + + protected: + void write_rtc_(uint32_t val); + uint32_t read_rtc_(); + + bool boot_successful_{false}; ///< set to true after boot is considered successful + uint32_t safe_mode_boot_is_good_after_{60000}; ///< The amount of time after which the boot is considered successful + uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for + uint32_t safe_mode_rtc_value_{0}; + uint32_t safe_mode_start_time_{0}; ///< stores when safe mode was enabled + uint8_t safe_mode_num_attempts_{0}; + ESPPreferenceObject rtc_; + CallbackManager safe_mode_callback_{}; + + static const uint32_t ENTER_SAFE_MODE_MAGIC = + 0x5afe5afe; ///< a magic number to indicate that safe mode should be entered on next boot +}; + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py index a6fcdfbece..7271358149 100644 --- a/esphome/components/safe_mode/switch/__init__.py +++ b/esphome/components/safe_mode/switch/__init__.py @@ -1,26 +1,25 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import switch -from esphome.components.ota import OTAComponent from esphome.const import ( - CONF_OTA, + CONF_SAFE_MODE, ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT, ) -from .. import safe_mode_ns +from .. import safe_mode_ns, SafeModeComponent -DEPENDENCIES = ["ota"] +DEPENDENCIES = ["safe_mode"] SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component) CONFIG_SCHEMA = ( switch.switch_schema( SafeModeSwitch, - icon=ICON_RESTART_ALERT, - entity_category=ENTITY_CATEGORY_CONFIG, block_inverted=True, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, ) - .extend({cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent)}) + .extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)}) .extend(cv.COMPONENT_SCHEMA) ) @@ -29,5 +28,5 @@ async def to_code(config): var = await switch.new_switch(config) await cg.register_component(var, config) - ota = await cg.get_variable(config[CONF_OTA]) - cg.add(var.set_ota(ota)) + safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE]) + cg.add(var.set_safe_mode(safe_mode_component)) diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.cpp b/esphome/components/safe_mode/switch/safe_mode_switch.cpp index a3979eec06..13b35ed210 100644 --- a/esphome/components/safe_mode/switch/safe_mode_switch.cpp +++ b/esphome/components/safe_mode/switch/safe_mode_switch.cpp @@ -1,14 +1,16 @@ #include "safe_mode_switch.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace safe_mode { -static const char *const TAG = "safe_mode_switch"; +static const char *const TAG = "safe_mode.switch"; -void SafeModeSwitch::set_ota(ota::OTAComponent *ota) { this->ota_ = ota; } +void SafeModeSwitch::set_safe_mode(SafeModeComponent *safe_mode_component) { + this->safe_mode_component_ = safe_mode_component; +} void SafeModeSwitch::write_state(bool state) { // Acknowledge @@ -16,13 +18,14 @@ void SafeModeSwitch::write_state(bool state) { if (state) { ESP_LOGI(TAG, "Restarting device in safe mode..."); - this->ota_->set_safe_mode_pending(true); + this->safe_mode_component_->set_safe_mode_pending(true); // Let MQTT settle a bit delay(100); // NOLINT App.safe_reboot(); } } + void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); } } // namespace safe_mode diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.h b/esphome/components/safe_mode/switch/safe_mode_switch.h index 2772db3d84..24e660c803 100644 --- a/esphome/components/safe_mode/switch/safe_mode_switch.h +++ b/esphome/components/safe_mode/switch/safe_mode_switch.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/ota/ota_component.h" +#include "esphome/components/safe_mode/safe_mode.h" #include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" namespace esphome { namespace safe_mode { @@ -10,10 +10,10 @@ namespace safe_mode { class SafeModeSwitch : public switch_::Switch, public Component { public: void dump_config() override; - void set_ota(ota::OTAComponent *ota); + void set_safe_mode(SafeModeComponent *safe_mode_component); protected: - ota::OTAComponent *ota_; + SafeModeComponent *safe_mode_component_; void write_state(bool state) override; }; diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 01abca0a1f..3eeca23800 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -42,13 +42,18 @@ void SCD30Component::setup() { ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8), uint16_t(raw_firmware_version[0] & 0xFF)); - if (this->temperature_offset_ != 0) { - if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t) (temperature_offset_ * 100.0))) { - ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } + uint16_t temp_offset; + if (this->temperature_offset_ > 0) { + temp_offset = (this->temperature_offset_ * 100); + } else { + temp_offset = 0; + } + + if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, temp_offset)) { + ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; } #ifdef USE_ESP32 // According ESP32 clock stretching is typically 30ms and up to 150ms "due to diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 1ddf0f1e85..a900c51a58 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_HUMIDITY, CONF_TEMPERATURE, CONF_CO2, + CONF_TEMPERATURE_OFFSET, CONF_UPDATE_INTERVAL, CONF_VALUE, DEVICE_CLASS_CARBON_DIOXIDE, @@ -36,7 +37,6 @@ ForceRecalibrationWithReference = scd30_ns.class_( CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" -CONF_TEMPERATURE_OFFSET = "temperature_offset" CONFIG_SCHEMA = ( @@ -68,7 +68,10 @@ CONFIG_SCHEMA = ( cv.int_range(min=0, max=0xFFFF, max_included=False), ), cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION, default=0): cv.pressure, - cv.Optional(CONF_TEMPERATURE_OFFSET): cv.temperature, + cv.Optional(CONF_TEMPERATURE_OFFSET): cv.All( + cv.temperature, + cv.float_range(min=0, max=655.35), + ), cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All( cv.positive_time_period_seconds, cv.Range( diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 4c94d4257f..13027b6f88 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_CO2, CONF_HUMIDITY, CONF_TEMPERATURE, + CONF_TEMPERATURE_OFFSET, CONF_VALUE, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, @@ -52,7 +53,6 @@ CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_MEASUREMENT_MODE = "measurement_mode" -CONF_TEMPERATURE_OFFSET = "temperature_offset" CONFIG_SCHEMA = ( diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 6337d89bcd..16b1d4c54e 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_MODE, CONF_PARAMETERS +from esphome.const import CONF_ID, CONF_MODE, CONF_PARAMETERS, CONF_RESTART from esphome.core import CORE, EsphomeError CODEOWNERS = ["@esphome/core"] @@ -19,7 +19,6 @@ ParallelScript = script_ns.class_("ParallelScript", Script) CONF_SCRIPT = "script" CONF_SINGLE = "single" -CONF_RESTART = "restart" CONF_QUEUED = "queued" CONF_PARALLEL = "parallel" CONF_MAX_RUNS = "max_runs" @@ -33,6 +32,7 @@ SCRIPT_MODES = { PARAMETER_TYPE_TRANSLATIONS = { "string": "std::string", + "boolean": "bool", } @@ -88,7 +88,7 @@ def validate_parameter_name(value): raise cv.Invalid(f"Script's parameter name cannot be {CONF_ID}") -ALLOWED_PARAM_TYPE_CHARSET = set("abcdefghijklmnopqrstuvwxyz0123456789_:*&[]") +ALLOWED_PARAM_TYPE_CHARSET = set("abcdefghijklmnopqrstuvwxyz0123456789_:*&[]<>") def validate_parameter_type(value): @@ -149,6 +149,16 @@ async def to_code(config): ), ) async def script_execute_action_to_code(config, action_id, template_arg, args): + def convert(type: str): + def converter(value): + if type == "std::string": + return value + if type == "bool": + return cg.RawExpression(str(value).lower()) + return cg.RawExpression(str(value)) + + return converter + async def get_ordered_args(config, script_params): config_args = config.copy() config_args.pop(CONF_ID) @@ -160,7 +170,9 @@ async def script_execute_action_to_code(config, action_id, template_arg, args): raise EsphomeError( f"Missing parameter: '{name}' in script.execute {config[CONF_ID]}" ) - arg = await cg.templatable(config_args[name], args, type) + arg = await cg.templatable( + config_args[name], args, type, convert(str(type)) + ) script_args.append(arg) return script_args diff --git a/esphome/components/sdl/__init__.py b/esphome/components/sdl/__init__.py new file mode 100644 index 0000000000..c58ce8a01e --- /dev/null +++ b/esphome/components/sdl/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/sdl/display.py b/esphome/components/sdl/display.py new file mode 100644 index 0000000000..18dc570f88 --- /dev/null +++ b/esphome/components/sdl/display.py @@ -0,0 +1,72 @@ +import subprocess + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display +from esphome.const import ( + CONF_ID, + CONF_DIMENSIONS, + CONF_WIDTH, + CONF_HEIGHT, + CONF_LAMBDA, + PLATFORM_HOST, +) + +sdl_ns = cg.esphome_ns.namespace("sdl") +Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component) + + +CONF_SDL_OPTIONS = "sdl_options" +CONF_SDL_ID = "sdl_id" + + +def get_sdl_options(value): + if value != "": + return value + try: + return subprocess.check_output(["sdl2-config", "--cflags", "--libs"]).decode() + except Exception as e: + raise cv.Invalid("Unable to run sdl2-config - have you installed sdl2?") from e + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sdl), + cv.Optional(CONF_SDL_OPTIONS, default=""): get_sdl_options, + cv.Required(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } + ), + ), + } + ) + ), + cv.only_on(PLATFORM_HOST), +) + + +async def to_code(config): + for option in config[CONF_SDL_OPTIONS].split(): + cg.add_build_flag(option) + cg.add_build_flag("-DSDL_BYTEORDER=4321") + var = cg.new_Pvariable(config[CONF_ID]) + await display.register_display(var, config) + + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT])) + else: + (width, height) = dimensions + cg.add(var.set_dimensions(width, height)) + + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp new file mode 100644 index 0000000000..8f0821a2fa --- /dev/null +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -0,0 +1,113 @@ +#ifdef USE_HOST +#include "sdl_esphome.h" +#include "esphome/components/display/display_color_utils.h" + +namespace esphome { +namespace sdl { + +void Sdl::setup() { + ESP_LOGD(TAG, "Starting setup"); + SDL_Init(SDL_INIT_VIDEO); + this->window_ = SDL_CreateWindow(App.get_name().c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + this->width_, this->height_, SDL_WINDOW_RESIZABLE); + this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE); + SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_); + this->texture_ = + SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_); + SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND); + ESP_LOGD(TAG, "Setup Complete"); +} +void Sdl::update() { + this->do_update_(); + if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) + return; + SDL_Rect rect{this->x_low_, this->y_low_, this->x_high_ + 1 - this->x_low_, this->y_high_ + 1 - this->y_low_}; + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; + this->redraw_(rect); +} + +void Sdl::redraw_(SDL_Rect &rect) { + SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect); + SDL_RenderPresent(this->renderer_); +} + +void Sdl::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) { + SDL_Rect rect{x_start, y_start, w, h}; + if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) { + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + } else { + auto stride = x_offset + w + x_pad; + auto data = ptr + (stride * y_offset + x_offset) * 2; + SDL_UpdateTexture(this->texture_, &rect, data, stride * 2); + } + this->redraw_(rect); +} + +void Sdl::draw_pixel_at(int x, int y, Color color) { + SDL_Rect rect{x, y, 1, 1}; + auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); + SDL_UpdateTexture(this->texture_, &rect, &data, 2); + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; +} + +void Sdl::loop() { + SDL_Event e; + if (SDL_PollEvent(&e)) { + switch (e.type) { + case SDL_QUIT: + exit(0); + + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + if (e.button.button == 1) { + this->mouse_x = e.button.x; + this->mouse_y = e.button.y; + this->mouse_down = e.button.state != 0; + } + break; + + case SDL_MOUSEMOTION: + if (e.motion.state & 1) { + this->mouse_x = e.button.x; + this->mouse_y = e.button.y; + this->mouse_down = true; + } else { + this->mouse_down = false; + } + break; + + case SDL_WINDOWEVENT: + switch (e.window.event) { + case SDL_WINDOWEVENT_SIZE_CHANGED: + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_RESIZED: { + SDL_Rect rect{0, 0, this->width_, this->height_}; + this->redraw_(rect); + break; + } + default: + break; + } + break; + + default: + ESP_LOGV(TAG, "Event %d", e.type); + break; + } + } +} + +} // namespace sdl +} // namespace esphome +#endif diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h new file mode 100644 index 0000000000..4b0e59c9fe --- /dev/null +++ b/esphome/components/sdl/sdl_esphome.h @@ -0,0 +1,55 @@ +#pragma once + +#ifdef USE_HOST +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/display/display.h" +#define SDL_MAIN_HANDLED +#include "SDL.h" + +namespace esphome { +namespace sdl { + +constexpr static const char *const TAG = "sdl"; + +class Sdl : public display::Display { + public: + display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; } + void update() override; + void loop() override; + void setup() 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; + void draw_pixel_at(int x, int y, Color color) override; + void set_dimensions(uint16_t width, uint16_t height) { + this->width_ = width; + this->height_ = height; + } + int get_width() override { return this->width_; } + int get_height() override { return this->height_; } + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void dump_config() override { LOG_DISPLAY("", "SDL", this); } + + int mouse_x{}; + int mouse_y{}; + bool mouse_down{}; + + protected: + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void redraw_(SDL_Rect &rect); + int width_{}; + int height_{}; + SDL_Renderer *renderer_{}; + SDL_Window *window_{}; + SDL_Texture *texture_{}; + uint16_t x_low_{0}; + uint16_t y_low_{0}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; +}; +} // namespace sdl +} // namespace esphome + +#endif diff --git a/esphome/components/sdl/touchscreen/__init__.py b/esphome/components/sdl/touchscreen/__init__.py new file mode 100644 index 0000000000..d6c0ed1c03 --- /dev/null +++ b/esphome/components/sdl/touchscreen/__init__.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +from esphome.components import touchscreen +from ..display import Sdl, sdl_ns, CONF_SDL_ID + +SdlTouchscreen = sdl_ns.class_("SdlTouchscreen", touchscreen.Touchscreen) + + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SdlTouchscreen), + cv.GenerateID(CONF_SDL_ID): cv.use_id(Sdl), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_SDL_ID]) + await touchscreen.register_touchscreen(var, config) diff --git a/esphome/components/sdl/touchscreen/sdl_touchscreen.h b/esphome/components/sdl/touchscreen/sdl_touchscreen.h new file mode 100644 index 0000000000..a1f0fb15e3 --- /dev/null +++ b/esphome/components/sdl/touchscreen/sdl_touchscreen.h @@ -0,0 +1,26 @@ +#pragma once + +#ifdef USE_HOST +#include "../sdl_esphome.h" +#include "esphome/components/touchscreen/touchscreen.h" + +namespace esphome { +namespace sdl { + +class SdlTouchscreen : public touchscreen::Touchscreen, public Parented { + public: + void setup() override { + this->x_raw_max_ = this->display_->get_width(); + this->y_raw_max_ = this->display_->get_height(); + } + + void update_touches() override { + if (this->parent_->mouse_down) { + add_raw_touch_position_(0, this->parent_->mouse_x, this->parent_->mouse_y); + } + } +}; + +} // namespace sdl +} // namespace esphome +#endif diff --git a/esphome/components/seeed_mr24hpc1/__init__.py b/esphome/components/seeed_mr24hpc1/__init__.py new file mode 100644 index 0000000000..52b971e7e4 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/__init__.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +DEPENDENCIES = ["uart"] +# is the code owner of the relevant code base +CODEOWNERS = ["@limengdu"] +# The current component or platform can be configured or defined multiple times in the same configuration file. +MULTI_CONF = True + +# This line of code creates a new namespace called mr24hpc1_ns. +# This namespace will be used as a prefix for all classes, functions and variables associated with the mr24hpc1_ns component, ensuring that they do not conflict with the names of other components. +mr24hpc1_ns = cg.esphome_ns.namespace("seeed_mr24hpc1") +# This MR24HPC1Component class will be a periodically polled UART device +MR24HPC1Component = mr24hpc1_ns.class_( + "MR24HPC1Component", cg.Component, uart.UARTDevice +) + +CONF_MR24HPC1_ID = "mr24hpc1_id" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MR24HPC1Component), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +# A verification mode was created to verify the configuration parameters of a UART device named "seeed_mr24hpc1". +# This authentication mode requires that the device must have transmit and receive functionality, a parity mode of "NONE", and a stop bit of one. +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "seeed_mr24hpc1", + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +# The async def keyword is used to define a concurrent function. +# Concurrent functions are special functions designed to work with Python's asyncio library to support asynchronous I/O operations. +async def to_code(config): + # This line of code creates a new Pvariable (a Python object representing a C++ variable) with the variable's ID taken from the configuration. + var = cg.new_Pvariable(config[CONF_ID]) + # This line of code registers the newly created Pvariable as a component so that ESPHome can manage it at runtime. + await cg.register_component(var, config) + # This line of code registers the newly created Pvariable as a device. + await uart.register_uart_device(var, config) diff --git a/esphome/components/seeed_mr24hpc1/binary_sensor.py b/esphome/components/seeed_mr24hpc1/binary_sensor.py new file mode 100644 index 0000000000..003db9f4a3 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/binary_sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_OCCUPANCY, + CONF_HAS_TARGET, +) +from . import CONF_MR24HPC1_ID, MR24HPC1Component + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, icon="mdi:motion-sensor" + ), +} + + +async def to_code(config): + mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) + if has_target_config := config.get(CONF_HAS_TARGET): + sens = await binary_sensor.new_binary_sensor(has_target_config) + cg.add(mr24hpc1_component.set_has_target_binary_sensor(sens)) diff --git a/esphome/components/seeed_mr24hpc1/button/__init__.py b/esphome/components/seeed_mr24hpc1/button/__init__.py new file mode 100644 index 0000000000..59372e4100 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/button/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + CONF_RESTART, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART_ALERT, +) +from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns + +RestartButton = mr24hpc1_ns.class_("RestartButton", button.Button) +CustomSetEndButton = mr24hpc1_ns.class_("CustomSetEndButton", button.Button) + +CONF_CUSTOM_SET_END = "custom_set_end" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component), + cv.Optional(CONF_RESTART): button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_CUSTOM_SET_END): button.button_schema( + CustomSetEndButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:cog", + ), +} + + +async def to_code(config): + mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) + if restart_config := config.get(CONF_RESTART): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_MR24HPC1_ID]) + cg.add(mr24hpc1_component.set_restart_button(b)) + if custom_set_end_config := config.get(CONF_CUSTOM_SET_END): + b = await button.new_button(custom_set_end_config) + await cg.register_parented(b, config[CONF_MR24HPC1_ID]) + cg.add(mr24hpc1_component.set_custom_set_end_button(b)) diff --git a/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.cpp b/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.cpp new file mode 100644 index 0000000000..0ae8889247 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.cpp @@ -0,0 +1,9 @@ +#include "custom_mode_end_button.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void CustomSetEndButton::press_action() { this->parent_->set_custom_end_mode(); } + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.h b/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.h new file mode 100644 index 0000000000..a1701d8581 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class CustomSetEndButton : public button::Button, public Parented { + public: + CustomSetEndButton() = default; + + protected: + void press_action() override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/button/restart_button.cpp b/esphome/components/seeed_mr24hpc1/button/restart_button.cpp new file mode 100644 index 0000000000..6c4a070d1c --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/button/restart_button.cpp @@ -0,0 +1,9 @@ +#include "restart_button.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void RestartButton::press_action() { this->parent_->set_restart(); } + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/button/restart_button.h b/esphome/components/seeed_mr24hpc1/button/restart_button.h new file mode 100644 index 0000000000..8a2ec2087c --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/button/restart_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class RestartButton : public button::Button, public Parented { + public: + RestartButton() = default; + + protected: + void press_action() override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/__init__.py b/esphome/components/seeed_mr24hpc1/number/__init__.py new file mode 100644 index 0000000000..2055fc548c --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/__init__.py @@ -0,0 +1,132 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_SENSITIVITY, + ENTITY_CATEGORY_CONFIG, +) +from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns + +SensitivityNumber = mr24hpc1_ns.class_("SensitivityNumber", number.Number) +CustomModeNumber = mr24hpc1_ns.class_("CustomModeNumber", number.Number) +ExistenceThresholdNumber = mr24hpc1_ns.class_("ExistenceThresholdNumber", number.Number) +MotionThresholdNumber = mr24hpc1_ns.class_("MotionThresholdNumber", number.Number) +MotionTriggerTimeNumber = mr24hpc1_ns.class_("MotionTriggerTimeNumber", number.Number) +MotionToRestTimeNumber = mr24hpc1_ns.class_("MotionToRestTimeNumber", number.Number) +CustomUnmanTimeNumber = mr24hpc1_ns.class_("CustomUnmanTimeNumber", number.Number) + +CONF_CUSTOM_MODE = "custom_mode" +CONF_EXISTENCE_THRESHOLD = "existence_threshold" +CONF_MOTION_THRESHOLD = "motion_threshold" +CONF_MOTION_TRIGGER = "motion_trigger" +CONF_MOTION_TO_REST = "motion_to_rest" +CONF_CUSTOM_UNMAN_TIME = "custom_unman_time" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component), + cv.Optional(CONF_SENSITIVITY): number.number_schema( + SensitivityNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:archive-check-outline", + ), + cv.Optional(CONF_CUSTOM_MODE): number.number_schema( + CustomModeNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:cog", + ), + cv.Optional(CONF_EXISTENCE_THRESHOLD): number.number_schema( + ExistenceThresholdNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + cv.Optional(CONF_MOTION_THRESHOLD): number.number_schema( + MotionThresholdNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + cv.Optional(CONF_MOTION_TRIGGER): number.number_schema( + MotionTriggerTimeNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:camera-timer", + unit_of_measurement="ms", + ), + cv.Optional(CONF_MOTION_TO_REST): number.number_schema( + MotionToRestTimeNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:camera-timer", + unit_of_measurement="ms", + ), + cv.Optional(CONF_CUSTOM_UNMAN_TIME): number.number_schema( + CustomUnmanTimeNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:camera-timer", + unit_of_measurement="s", + ), + } +) + + +async def to_code(config): + mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) + if sensitivity_config := config.get(CONF_SENSITIVITY): + n = await number.new_number( + sensitivity_config, + min_value=0, + max_value=3, + step=1, + ) + await cg.register_parented(n, mr24hpc1_component) + cg.add(mr24hpc1_component.set_sensitivity_number(n)) + if custom_mode_config := config.get(CONF_CUSTOM_MODE): + n = await number.new_number( + custom_mode_config, + min_value=0, + max_value=4, + step=1, + ) + await cg.register_parented(n, mr24hpc1_component) + cg.add(mr24hpc1_component.set_custom_mode_number(n)) + if existence_threshold_config := config.get(CONF_EXISTENCE_THRESHOLD): + n = await number.new_number( + existence_threshold_config, + min_value=0, + max_value=250, + step=1, + ) + await cg.register_parented(n, mr24hpc1_component) + cg.add(mr24hpc1_component.set_existence_threshold_number(n)) + if motion_threshold_config := config.get(CONF_MOTION_THRESHOLD): + n = await number.new_number( + motion_threshold_config, + min_value=0, + max_value=250, + step=1, + ) + await cg.register_parented(n, mr24hpc1_component) + cg.add(mr24hpc1_component.set_motion_threshold_number(n)) + if motion_trigger_config := config.get(CONF_MOTION_TRIGGER): + n = await number.new_number( + motion_trigger_config, + min_value=0, + max_value=150, + step=1, + ) + await cg.register_parented(n, mr24hpc1_component) + cg.add(mr24hpc1_component.set_motion_trigger_number(n)) + if motion_to_rest_config := config.get(CONF_MOTION_TO_REST): + n = await number.new_number( + motion_to_rest_config, + min_value=0, + max_value=3000, + step=1, + ) + await cg.register_parented(n, mr24hpc1_component) + cg.add(mr24hpc1_component.set_motion_to_rest_number(n)) + if custom_unman_time_config := config.get(CONF_CUSTOM_UNMAN_TIME): + n = await number.new_number( + custom_unman_time_config, + min_value=0, + max_value=3600, + step=1, + ) + await cg.register_parented(n, mr24hpc1_component) + cg.add(mr24hpc1_component.set_custom_unman_time_number(n)) diff --git a/esphome/components/seeed_mr24hpc1/number/custom_mode_number.cpp b/esphome/components/seeed_mr24hpc1/number/custom_mode_number.cpp new file mode 100644 index 0000000000..0aebd8fb9f --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/custom_mode_number.cpp @@ -0,0 +1,12 @@ +#include "custom_mode_number.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void CustomModeNumber::control(float value) { + this->publish_state(value); + this->parent_->set_custom_mode(value); +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/custom_mode_number.h b/esphome/components/seeed_mr24hpc1/number/custom_mode_number.h new file mode 100644 index 0000000000..40ff3f201a --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/custom_mode_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class CustomModeNumber : public number::Number, public Parented { + public: + CustomModeNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.cpp b/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.cpp new file mode 100644 index 0000000000..12a8ff69fa --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.cpp @@ -0,0 +1,9 @@ +#include "custom_unman_time_number.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void CustomUnmanTimeNumber::control(float value) { this->parent_->set_custom_unman_time(value); } + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.h b/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.h new file mode 100644 index 0000000000..6b871c4c13 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class CustomUnmanTimeNumber : public number::Number, public Parented { + public: + CustomUnmanTimeNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.cpp b/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.cpp new file mode 100644 index 0000000000..58ef56509e --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.cpp @@ -0,0 +1,9 @@ +#include "existence_threshold_number.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void ExistenceThresholdNumber::control(float value) { this->parent_->set_existence_threshold(value); } + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.h b/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.h new file mode 100644 index 0000000000..656bad17de --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class ExistenceThresholdNumber : public number::Number, public Parented { + public: + ExistenceThresholdNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.cpp b/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.cpp new file mode 100644 index 0000000000..d252b4f01d --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.cpp @@ -0,0 +1,9 @@ +#include "motion_threshold_number.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void MotionThresholdNumber::control(float value) { this->parent_->set_motion_threshold(value); } + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.h b/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.h new file mode 100644 index 0000000000..e8ae37b96f --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class MotionThresholdNumber : public number::Number, public Parented { + public: + MotionThresholdNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.cpp b/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.cpp new file mode 100644 index 0000000000..fc7659dc54 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.cpp @@ -0,0 +1,9 @@ +#include "motion_trigger_time_number.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void MotionTriggerTimeNumber::control(float value) { this->parent_->set_motion_trigger_time(value); } + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.h b/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.h new file mode 100644 index 0000000000..996356e237 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class MotionTriggerTimeNumber : public number::Number, public Parented { + public: + MotionTriggerTimeNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.cpp b/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.cpp new file mode 100644 index 0000000000..f598e6686c --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.cpp @@ -0,0 +1,9 @@ +#include "motiontorest_time_number.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void MotionToRestTimeNumber::control(float value) { this->parent_->set_motion_to_rest_time(value); } + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.h b/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.h new file mode 100644 index 0000000000..559d23fdeb --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class MotionToRestTimeNumber : public number::Number, public Parented { + public: + MotionToRestTimeNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/sensitivity_number.cpp b/esphome/components/seeed_mr24hpc1/number/sensitivity_number.cpp new file mode 100644 index 0000000000..d903dfd818 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/sensitivity_number.cpp @@ -0,0 +1,9 @@ +#include "sensitivity_number.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void SensitivityNumber::control(float value) { this->parent_->set_sensitivity(value); } + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/number/sensitivity_number.h b/esphome/components/seeed_mr24hpc1/number/sensitivity_number.h new file mode 100644 index 0000000000..fee33521d0 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/number/sensitivity_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class SensitivityNumber : public number::Number, public Parented { + public: + SensitivityNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp new file mode 100644 index 0000000000..1cf9bd300a --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -0,0 +1,890 @@ +#include "seeed_mr24hpc1.h" + +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace seeed_mr24hpc1 { + +static const char *const TAG = "seeed_mr24hpc1"; + +// 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 MR24HPC1Component::dump_config() { + ESP_LOGCONFIG(TAG, "MR24HPC1:"); +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "Heartbeat Text Sensor", this->heartbeat_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Product Model Text Sensor", this->product_model_text_sensor_); + LOG_TEXT_SENSOR(" ", "Product ID Text Sensor", this->product_id_text_sensor_); + LOG_TEXT_SENSOR(" ", "Hardware Model Text Sensor", this->hardware_model_text_sensor_); + LOG_TEXT_SENSOR(" ", "Firware Verison Text Sensor", this->firware_version_text_sensor_); + LOG_TEXT_SENSOR(" ", "Keep Away Text Sensor", this->keep_away_text_sensor_); + LOG_TEXT_SENSOR(" ", "Motion Status Text Sensor", this->motion_status_text_sensor_); + LOG_TEXT_SENSOR(" ", "Custom Mode End Text Sensor", this->custom_mode_end_text_sensor_); +#endif +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Has Target Binary Sensor", this->has_target_binary_sensor_); +#endif +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Custom Presence Of Detection Sensor", this->custom_presence_of_detection_sensor_); + LOG_SENSOR(" ", "Movement Signs Sensor", this->movement_signs_sensor_); + LOG_SENSOR(" ", "Custom Motion Distance Sensor", this->custom_motion_distance_sensor_); + LOG_SENSOR(" ", "Custom Spatial Static Sensor", this->custom_spatial_static_value_sensor_); + LOG_SENSOR(" ", "Custom Spatial Motion Sensor", this->custom_spatial_motion_value_sensor_); + LOG_SENSOR(" ", "Custom Motion Speed Sensor", this->custom_motion_speed_sensor_); + LOG_SENSOR(" ", "Custom Mode Num Sensor", this->custom_mode_num_sensor_); +#endif +#ifdef USE_SWITCH + LOG_SWITCH(" ", "Underly Open Function Switch", this->underlying_open_function_switch_); +#endif +#ifdef USE_BUTTON + LOG_BUTTON(" ", "Restart Button", this->restart_button_); + LOG_BUTTON(" ", "Custom Set End Button", this->custom_set_end_button_); +#endif +#ifdef USE_SELECT + LOG_SELECT(" ", "Scene Mode Select", this->scene_mode_select_); + LOG_SELECT(" ", "Unman Time Select", this->unman_time_select_); + LOG_SELECT(" ", "Existence Boundary Select", this->existence_boundary_select_); + LOG_SELECT(" ", "Motion Boundary Select", this->motion_boundary_select_); +#endif +#ifdef USE_NUMBER + LOG_NUMBER(" ", "Sensitivity Number", this->sensitivity_number_); + LOG_NUMBER(" ", "Custom Mode Number", this->custom_mode_number_); + LOG_NUMBER(" ", "Existence Threshold Number", this->existence_threshold_number_); + LOG_NUMBER(" ", "Motion Threshold Number", this->motion_threshold_number_); + LOG_NUMBER(" ", "Motion Trigger Time Number", this->motion_trigger_number_); + LOG_NUMBER(" ", "Motion To Rest Time Number", this->motion_to_rest_number_); + LOG_NUMBER(" ", "Custom Unman Time Number", this->custom_unman_time_number_); +#endif +} + +// Initialisation functions +void MR24HPC1Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MR24HPC1..."); + this->check_uart_settings(115200); + + if (this->custom_mode_number_ != nullptr) { + this->custom_mode_number_->publish_state(0); // Zero out the custom mode + } + if (this->custom_mode_num_sensor_ != nullptr) { + this->custom_mode_num_sensor_->publish_state(0); + } + if (this->custom_mode_end_text_sensor_ != nullptr) { + this->custom_mode_end_text_sensor_->publish_state("Not in custom mode"); + } + this->set_custom_end_mode(); + this->poll_time_base_func_check_ = true; + this->check_dev_inf_sign_ = true; + this->sg_start_query_data_ = STANDARD_FUNCTION_QUERY_PRODUCT_MODE; + this->sg_data_len_ = 0; + this->sg_frame_len_ = 0; + this->sg_recv_data_state_ = FRAME_IDLE; + this->s_output_info_switch_flag_ = OUTPUT_SWITCH_INIT; + + memset(this->c_product_mode_, 0, PRODUCT_BUF_MAX_SIZE); + memset(this->c_product_id_, 0, PRODUCT_BUF_MAX_SIZE); + memset(this->c_firmware_version_, 0, PRODUCT_BUF_MAX_SIZE); + memset(this->c_hardware_model_, 0, PRODUCT_BUF_MAX_SIZE); + memset(this->sg_frame_prase_buf_, 0, FRAME_BUF_MAX_SIZE); + memset(this->sg_frame_buf_, 0, FRAME_BUF_MAX_SIZE); + + this->set_interval(8000, [this]() { this->update_(); }); + ESP_LOGCONFIG(TAG, "Set up MR24HPC1 complete"); +} + +// Timed polling of radar data +void MR24HPC1Component::update_() { + this->get_radar_output_information_switch(); // Query the key status every so often + this->poll_time_base_func_check_ = true; // Query the base functionality information at regular intervals +} + +// main loop +void MR24HPC1Component::loop() { + uint8_t byte; + + // Is there data on the serial port + while (this->available()) { + this->read_byte(&byte); + this->r24_split_data_frame_(byte); // split data frame + } + + if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) && + (this->sg_start_query_data_ > CUSTOM_FUNCTION_QUERY_TIME_OF_ENTER_UNMANNED) && (!this->check_dev_inf_sign_)) { + this->sg_start_query_data_ = STANDARD_FUNCTION_QUERY_SCENE_MODE; + } else if ((this->s_output_info_switch_flag_ == OUTPUT_SWITCH_ON) && + (this->sg_start_query_data_ < CUSTOM_FUNCTION_QUERY_EXISTENCE_BOUNDARY) && (!this->check_dev_inf_sign_)) { + this->sg_start_query_data_ = CUSTOM_FUNCTION_QUERY_EXISTENCE_BOUNDARY; + } else if (this->check_dev_inf_sign_ && (this->sg_start_query_data_ > STANDARD_FUNCTION_QUERY_HARDWARE_MODE)) { + // First time power up information polling + this->sg_start_query_data_ = STANDARD_FUNCTION_QUERY_PRODUCT_MODE; + } + + // Polling Functions + if (this->poll_time_base_func_check_) { + switch (this->sg_start_query_data_) { + case STANDARD_FUNCTION_QUERY_PRODUCT_MODE: + this->get_product_mode(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_PRODUCT_ID: + this->get_product_id(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_FIRMWARE_VERSION: + this->get_product_mode(); + this->get_product_id(); + this->get_firmware_version(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_HARDWARE_MODE: // Above is the equipment information + this->get_product_mode(); + this->get_product_id(); + this->get_hardware_model(); + this->sg_start_query_data_++; + this->check_dev_inf_sign_ = false; + break; + case STANDARD_FUNCTION_QUERY_SCENE_MODE: + this->get_scene_mode(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_SENSITIVITY: + this->get_sensitivity(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_UNMANNED_TIME: + this->get_unmanned_time(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_HUMAN_STATUS: + this->get_human_status(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_HUMAN_MOTION_INF: + this->get_human_motion_info(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_BODY_MOVE_PARAMETER: + this->get_body_motion_params(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_KEEPAWAY_STATUS: // The above is the basic functional information + this->get_keep_away(); + this->sg_start_query_data_++; + break; + case STANDARD_QUERY_CUSTOM_MODE: + this->get_custom_mode(); + this->sg_start_query_data_++; + break; + case STANDARD_FUNCTION_QUERY_HEARTBEAT_STATE: + this->get_heartbeat_packet(); + this->sg_start_query_data_++; + break; + case CUSTOM_FUNCTION_QUERY_EXISTENCE_BOUNDARY: + this->get_existence_boundary(); + this->sg_start_query_data_++; + break; + case CUSTOM_FUNCTION_QUERY_MOTION_BOUNDARY: + this->get_motion_boundary(); + this->sg_start_query_data_++; + break; + case CUSTOM_FUNCTION_QUERY_EXISTENCE_THRESHOLD: + this->get_existence_threshold(); + this->sg_start_query_data_++; + break; + case CUSTOM_FUNCTION_QUERY_MOTION_THRESHOLD: + this->get_motion_threshold(); + this->sg_start_query_data_++; + break; + case CUSTOM_FUNCTION_QUERY_MOTION_TRIGGER_TIME: + this->get_motion_trigger_time(); + this->sg_start_query_data_++; + break; + case CUSTOM_FUNCTION_QUERY_MOTION_TO_REST_TIME: + this->get_motion_to_rest_time(); + this->sg_start_query_data_++; + break; + case CUSTOM_FUNCTION_QUERY_TIME_OF_ENTER_UNMANNED: + this->get_custom_unman_time(); + this->sg_start_query_data_++; + if (this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) { + this->poll_time_base_func_check_ = false; // Avoiding high-speed polling that can cause the device to jam + } + break; + case UNDERLY_FUNCTION_QUERY_HUMAN_STATUS: + this->get_human_status(); + this->sg_start_query_data_++; + break; + case UNDERLY_FUNCTION_QUERY_SPATIAL_STATIC_VALUE: + this->get_spatial_static_value(); + this->sg_start_query_data_++; + break; + case UNDERLY_FUNCTION_QUERY_SPATIAL_MOTION_VALUE: + this->get_spatial_motion_value(); + this->sg_start_query_data_++; + break; + case UNDERLY_FUNCTION_QUERY_DISTANCE_OF_STATIC_OBJECT: + this->get_distance_of_static_object(); + this->sg_start_query_data_++; + break; + case UNDERLY_FUNCTION_QUERY_DISTANCE_OF_MOVING_OBJECT: + this->get_distance_of_moving_object(); + this->sg_start_query_data_++; + break; + case UNDERLY_FUNCTION_QUERY_TARGET_MOVEMENT_SPEED: + this->get_target_movement_speed(); + this->sg_start_query_data_++; + this->poll_time_base_func_check_ = false; // Avoiding high-speed polling that can cause the device to jam + break; + default: + break; + } + } +} + +// Calculate CRC check digit +static uint8_t get_frame_crc_sum(const uint8_t *data, int len) { + unsigned int crc_sum = 0; + for (int i = 0; i < len - 3; i++) { + crc_sum += data[i]; + } + return crc_sum & 0xff; +} + +// Check that the check digit is correct +static int get_frame_check_status(uint8_t *data, int len) { + uint8_t crc_sum = get_frame_crc_sum(data, len); + uint8_t verified = data[len - 3]; + return (verified == crc_sum) ? 1 : 0; +} + +// split data frame +void MR24HPC1Component::r24_split_data_frame_(uint8_t value) { + switch (this->sg_recv_data_state_) { + case FRAME_IDLE: // starting value + if (FRAME_HEADER1_VALUE == value) { + this->sg_recv_data_state_ = FRAME_HEADER2; + } + break; + case FRAME_HEADER2: + if (FRAME_HEADER2_VALUE == value) { + this->sg_frame_buf_[0] = FRAME_HEADER1_VALUE; + this->sg_frame_buf_[1] = FRAME_HEADER2_VALUE; + this->sg_recv_data_state_ = FRAME_CTL_WORD; + } else { + this->sg_recv_data_state_ = FRAME_IDLE; + ESP_LOGD(TAG, "FRAME_IDLE ERROR value:%x", value); + } + break; + case FRAME_CTL_WORD: + this->sg_frame_buf_[2] = value; + this->sg_recv_data_state_ = FRAME_CMD_WORD; + break; + case FRAME_CMD_WORD: + this->sg_frame_buf_[3] = value; + this->sg_recv_data_state_ = FRAME_DATA_LEN_H; + break; + case FRAME_DATA_LEN_H: + if (value <= 4) { + this->sg_data_len_ = value * 256; + this->sg_frame_buf_[4] = value; + this->sg_recv_data_state_ = FRAME_DATA_LEN_L; + } else { + this->sg_data_len_ = 0; + this->sg_recv_data_state_ = FRAME_IDLE; + ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value); + } + break; + case FRAME_DATA_LEN_L: + this->sg_data_len_ += value; + if (this->sg_data_len_ > 32) { + ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value); + this->sg_data_len_ = 0; + this->sg_recv_data_state_ = FRAME_IDLE; + } else { + this->sg_frame_buf_[5] = value; + this->sg_frame_len_ = 6; + this->sg_recv_data_state_ = FRAME_DATA_BYTES; + } + break; + case FRAME_DATA_BYTES: + this->sg_data_len_ -= 1; + this->sg_frame_buf_[this->sg_frame_len_++] = value; + if (this->sg_data_len_ <= 0) { + this->sg_recv_data_state_ = FRAME_DATA_CRC; + } + break; + case FRAME_DATA_CRC: + this->sg_frame_buf_[this->sg_frame_len_++] = value; + this->sg_recv_data_state_ = FRAME_TAIL1; + break; + case FRAME_TAIL1: + if (FRAME_TAIL1_VALUE == value) { + this->sg_recv_data_state_ = FRAME_TAIL2; + } else { + this->sg_recv_data_state_ = FRAME_IDLE; + this->sg_frame_len_ = 0; + this->sg_data_len_ = 0; + ESP_LOGD(TAG, "FRAME_TAIL1 ERROR value:%x", value); + } + break; + case FRAME_TAIL2: + if (FRAME_TAIL2_VALUE == value) { + this->sg_frame_buf_[this->sg_frame_len_++] = FRAME_TAIL1_VALUE; + this->sg_frame_buf_[this->sg_frame_len_++] = FRAME_TAIL2_VALUE; + memcpy(this->sg_frame_prase_buf_, this->sg_frame_buf_, this->sg_frame_len_); + if (get_frame_check_status(this->sg_frame_prase_buf_, this->sg_frame_len_)) { + this->r24_parse_data_frame_(this->sg_frame_prase_buf_, this->sg_frame_len_); + } else { + ESP_LOGD(TAG, "frame check failer!"); + } + } else { + ESP_LOGD(TAG, "FRAME_TAIL2 ERROR value:%x", value); + } + memset(this->sg_frame_prase_buf_, 0, FRAME_BUF_MAX_SIZE); + memset(this->sg_frame_buf_, 0, FRAME_BUF_MAX_SIZE); + this->sg_frame_len_ = 0; + this->sg_data_len_ = 0; + this->sg_recv_data_state_ = FRAME_IDLE; + break; + default: + this->sg_recv_data_state_ = FRAME_IDLE; + } +} + +// Parses data frames related to product information +void MR24HPC1Component::r24_frame_parse_product_information_(uint8_t *data) { + uint16_t product_len = encode_uint16(data[FRAME_COMMAND_WORD_INDEX + 1], data[FRAME_COMMAND_WORD_INDEX + 2]); + if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_PRODUCT_MODE) { + if ((this->product_model_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) { + memset(this->c_product_mode_, 0, PRODUCT_BUF_MAX_SIZE); + memcpy(this->c_product_mode_, &data[FRAME_DATA_INDEX], product_len); + this->product_model_text_sensor_->publish_state(this->c_product_mode_); + } else { + ESP_LOGD(TAG, "Reply: get product_mode error!"); + } + } else if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_PRODUCT_ID) { + if ((this->product_id_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) { + memset(this->c_product_id_, 0, PRODUCT_BUF_MAX_SIZE); + memcpy(this->c_product_id_, &data[FRAME_DATA_INDEX], product_len); + this->product_id_text_sensor_->publish_state(this->c_product_id_); + } else { + ESP_LOGD(TAG, "Reply: get productId error!"); + } + } else if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_HARDWARE_MODEL) { + if ((this->hardware_model_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) { + memset(this->c_hardware_model_, 0, PRODUCT_BUF_MAX_SIZE); + memcpy(this->c_hardware_model_, &data[FRAME_DATA_INDEX], product_len); + this->hardware_model_text_sensor_->publish_state(this->c_hardware_model_); + ESP_LOGD(TAG, "Reply: get hardware_model :%s", this->c_hardware_model_); + } else { + ESP_LOGD(TAG, "Reply: get hardwareModel error!"); + } + } else if (data[FRAME_COMMAND_WORD_INDEX] == COMMAND_FIRMWARE_VERSION) { + if ((this->firware_version_text_sensor_ != nullptr) && (product_len < PRODUCT_BUF_MAX_SIZE)) { + memset(this->c_firmware_version_, 0, PRODUCT_BUF_MAX_SIZE); + memcpy(this->c_firmware_version_, &data[FRAME_DATA_INDEX], product_len); + this->firware_version_text_sensor_->publish_state(this->c_firmware_version_); + } else { + ESP_LOGD(TAG, "Reply: get firmwareVersion error!"); + } + } +} + +// Parsing the underlying open parameters +void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *data) { + if (data[FRAME_COMMAND_WORD_INDEX] == 0x00) { + if (this->underlying_open_function_switch_ != nullptr) { + this->underlying_open_function_switch_->publish_state( + data[FRAME_DATA_INDEX]); // Underlying Open Parameter Switch Status Updates + } + if (data[FRAME_DATA_INDEX]) { + this->s_output_info_switch_flag_ = OUTPUT_SWITCH_ON; + } else { + this->s_output_info_switch_flag_ = OUTPUT_SWTICH_OFF; + } + } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x01) { + if (this->custom_spatial_static_value_sensor_ != nullptr) { + this->custom_spatial_static_value_sensor_->publish_state(data[FRAME_DATA_INDEX]); + } + if (this->custom_presence_of_detection_sensor_ != nullptr) { + this->custom_presence_of_detection_sensor_->publish_state(data[FRAME_DATA_INDEX + 1] * 0.5f); + } + if (this->custom_spatial_motion_value_sensor_ != nullptr) { + this->custom_spatial_motion_value_sensor_->publish_state(data[FRAME_DATA_INDEX + 2]); + } + if (this->custom_motion_distance_sensor_ != nullptr) { + this->custom_motion_distance_sensor_->publish_state(data[FRAME_DATA_INDEX + 3] * 0.5f); + } + if (this->custom_motion_speed_sensor_ != nullptr) { + this->custom_motion_speed_sensor_->publish_state((data[FRAME_DATA_INDEX + 4] - 10) * 0.5f); + } + } else if ((data[FRAME_COMMAND_WORD_INDEX] == 0x06) || (data[FRAME_COMMAND_WORD_INDEX] == 0x86)) { + // none:0x00 close_to:0x01 far_away:0x02 + if ((this->keep_away_text_sensor_ != nullptr) && (data[FRAME_DATA_INDEX] < 3)) { + this->keep_away_text_sensor_->publish_state(S_KEEP_AWAY_STR[data[FRAME_DATA_INDEX]]); + } + } else if ((this->movement_signs_sensor_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x07) || (data[FRAME_COMMAND_WORD_INDEX] == 0x87))) { + this->movement_signs_sensor_->publish_state(data[FRAME_DATA_INDEX]); + } else if ((this->existence_threshold_number_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x08) || (data[FRAME_COMMAND_WORD_INDEX] == 0x88))) { + this->existence_threshold_number_->publish_state(data[FRAME_DATA_INDEX]); + } else if ((this->motion_threshold_number_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x09) || (data[FRAME_COMMAND_WORD_INDEX] == 0x89))) { + this->motion_threshold_number_->publish_state(data[FRAME_DATA_INDEX]); + } else if ((this->existence_boundary_select_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x0a) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8a))) { + if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { + this->existence_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); + } + } else if ((this->motion_boundary_select_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x0b) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8b))) { + if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { + this->motion_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); + } + } else if ((this->motion_trigger_number_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x0c) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8c))) { + uint32_t motion_trigger_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1], + data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]); + this->motion_trigger_number_->publish_state(motion_trigger_time); + } else if ((this->motion_to_rest_number_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x0d) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8d))) { + uint32_t move_to_rest_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1], + data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]); + this->motion_to_rest_number_->publish_state(move_to_rest_time); + } else if ((this->custom_unman_time_number_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x0e) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8e))) { + uint32_t enter_unmanned_time = encode_uint32(data[FRAME_DATA_INDEX], data[FRAME_DATA_INDEX + 1], + data[FRAME_DATA_INDEX + 2], data[FRAME_DATA_INDEX + 3]); + float custom_unmanned_time = enter_unmanned_time / 1000.0; + this->custom_unman_time_number_->publish_state(custom_unmanned_time); + } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x80) { + if (data[FRAME_DATA_INDEX]) { + this->s_output_info_switch_flag_ = OUTPUT_SWITCH_ON; + } else { + this->s_output_info_switch_flag_ = OUTPUT_SWTICH_OFF; + } + if (this->underlying_open_function_switch_ != nullptr) { + this->underlying_open_function_switch_->publish_state(data[FRAME_DATA_INDEX]); + } + } else if ((this->custom_spatial_static_value_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x81)) { + this->custom_spatial_static_value_sensor_->publish_state(data[FRAME_DATA_INDEX]); + } else if ((this->custom_spatial_motion_value_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x82)) { + this->custom_spatial_motion_value_sensor_->publish_state(data[FRAME_DATA_INDEX]); + } else if ((this->custom_presence_of_detection_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x83)) { + this->custom_presence_of_detection_sensor_->publish_state( + S_PRESENCE_OF_DETECTION_RANGE_STR[data[FRAME_DATA_INDEX]]); + } else if ((this->custom_motion_distance_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x84)) { + this->custom_motion_distance_sensor_->publish_state(data[FRAME_DATA_INDEX] * 0.5f); + } else if ((this->custom_motion_speed_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x85)) { + this->custom_motion_speed_sensor_->publish_state((data[FRAME_DATA_INDEX] - 10) * 0.5f); + } +} + +void MR24HPC1Component::r24_parse_data_frame_(uint8_t *data, uint8_t len) { + switch (data[FRAME_CONTROL_WORD_INDEX]) { + case 0x01: { + if ((this->heartbeat_state_text_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x01)) { + this->heartbeat_state_text_sensor_->publish_state("Equipment Normal"); + } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x02) { + ESP_LOGD(TAG, "Reply: query restart packet"); + } else if (this->heartbeat_state_text_sensor_ != nullptr) { + this->heartbeat_state_text_sensor_->publish_state("Equipment Abnormal"); + } + } break; + case 0x02: { + this->r24_frame_parse_product_information_(data); + } break; + case 0x05: { + this->r24_frame_parse_work_status_(data); + } break; + case 0x08: { + this->r24_frame_parse_open_underlying_information_(data); + } break; + case 0x80: { + this->r24_frame_parse_human_information_(data); + } break; + default: + ESP_LOGD(TAG, "control word:0x%02X not found", data[FRAME_CONTROL_WORD_INDEX]); + break; + } +} + +void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { + if (data[FRAME_COMMAND_WORD_INDEX] == 0x01) { + ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); + } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x07) { + if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { + this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); + } else { + ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); + } + } else if ((this->sensitivity_number_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x08) || (data[FRAME_COMMAND_WORD_INDEX] == 0x88))) { + // 1-3 + this->sensitivity_number_->publish_state(data[FRAME_DATA_INDEX]); + } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x09) { + // 1-4 + if (this->custom_mode_num_sensor_ != nullptr) { + this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]); + } + if (this->custom_mode_number_ != nullptr) { + this->custom_mode_number_->publish_state(0); + } + if (this->custom_mode_end_text_sensor_ != nullptr) { + this->custom_mode_end_text_sensor_->publish_state("Setup in progress..."); + } + } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x81) { + ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); + } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x87) { + if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { + this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); + } else { + ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); + } + } else if ((this->custom_mode_end_text_sensor_ != nullptr) && (data[FRAME_COMMAND_WORD_INDEX] == 0x0A)) { + this->custom_mode_end_text_sensor_->publish_state("Set Success!"); + } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x89) { + if (data[FRAME_DATA_INDEX] == 0) { + if (this->custom_mode_end_text_sensor_ != nullptr) { + this->custom_mode_end_text_sensor_->publish_state("Not in custom mode"); + } + if (this->custom_mode_number_ != nullptr) { + this->custom_mode_number_->publish_state(0); + } + if (this->custom_mode_num_sensor_ != nullptr) { + this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]); + } + } else { + if (this->custom_mode_num_sensor_ != nullptr) { + this->custom_mode_num_sensor_->publish_state(data[FRAME_DATA_INDEX]); + } + } + } else { + ESP_LOGD(TAG, "[%s] No found COMMAND_WORD(%02X) in Frame", __FUNCTION__, data[FRAME_COMMAND_WORD_INDEX]); + } +} + +void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { + if ((this->has_target_binary_sensor_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x01) || (data[FRAME_COMMAND_WORD_INDEX] == 0x81))) { + this->has_target_binary_sensor_->publish_state(S_SOMEONE_EXISTS_STR[data[FRAME_DATA_INDEX]]); + } else if ((this->motion_status_text_sensor_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x02) || (data[FRAME_COMMAND_WORD_INDEX] == 0x82))) { + if (data[FRAME_DATA_INDEX] < 3) { + this->motion_status_text_sensor_->publish_state(S_MOTION_STATUS_STR[data[FRAME_DATA_INDEX]]); + } + } else if ((this->movement_signs_sensor_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x03) || (data[FRAME_COMMAND_WORD_INDEX] == 0x83))) { + this->movement_signs_sensor_->publish_state(data[FRAME_DATA_INDEX]); + } else if ((this->unman_time_select_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x0A) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8A))) { + // none:0x00 1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08 + if (data[FRAME_DATA_INDEX] < 9) { + this->unman_time_select_->publish_state(S_UNMANNED_TIME_STR[data[FRAME_DATA_INDEX]]); + } + } else if ((this->keep_away_text_sensor_ != nullptr) && + ((data[FRAME_COMMAND_WORD_INDEX] == 0x0B) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8B))) { + // none:0x00 close_to:0x01 far_away:0x02 + if (data[FRAME_DATA_INDEX] < 3) { + this->keep_away_text_sensor_->publish_state(S_KEEP_AWAY_STR[data[FRAME_DATA_INDEX]]); + } + } else { + ESP_LOGD(TAG, "[%s] No found COMMAND_WORD(%02X) in Frame", __FUNCTION__, data[FRAME_COMMAND_WORD_INDEX]); + } +} + +// Sending data frames +void MR24HPC1Component::send_query_(const uint8_t *query, size_t string_length) { + this->write_array(query, string_length); +} + +// Send Heartbeat Packet Command +void MR24HPC1Component::get_heartbeat_packet() { this->send_query_(GET_HEARTBEAT, sizeof(GET_HEARTBEAT)); } + +// Issuance of the underlying open parameter query command +void MR24HPC1Component::get_radar_output_information_switch() { + this->send_query_(GET_RADAR_OUTPUT_INFORMATION_SWITCH, sizeof(GET_RADAR_OUTPUT_INFORMATION_SWITCH)); +} + +// Issuance of product model orders +void MR24HPC1Component::get_product_mode() { this->send_query_(GET_PRODUCT_MODE, sizeof(GET_PRODUCT_MODE)); } + +// Issuing the Get Product ID command +void MR24HPC1Component::get_product_id() { this->send_query_(GET_PRODUCT_ID, sizeof(GET_PRODUCT_ID)); } + +// Issuing hardware model commands +void MR24HPC1Component::get_hardware_model() { this->send_query_(GET_HARDWARE_MODEL, sizeof(GET_HARDWARE_MODEL)); } + +// Issuing software version commands +void MR24HPC1Component::get_firmware_version() { + this->send_query_(GET_FIRMWARE_VERSION, sizeof(GET_FIRMWARE_VERSION)); +} + +void MR24HPC1Component::get_human_status() { this->send_query_(GET_HUMAN_STATUS, sizeof(GET_HUMAN_STATUS)); } + +void MR24HPC1Component::get_human_motion_info() { + this->send_query_(GET_HUMAN_MOTION_INFORMATION, sizeof(GET_HUMAN_MOTION_INFORMATION)); +} + +void MR24HPC1Component::get_body_motion_params() { + this->send_query_(GET_BODY_MOTION_PARAMETERS, sizeof(GET_BODY_MOTION_PARAMETERS)); +} + +void MR24HPC1Component::get_keep_away() { this->send_query_(GET_KEEP_AWAY, sizeof(GET_KEEP_AWAY)); } + +void MR24HPC1Component::get_scene_mode() { this->send_query_(GET_SCENE_MODE, sizeof(GET_SCENE_MODE)); } + +void MR24HPC1Component::get_sensitivity() { this->send_query_(GET_SENSITIVITY, sizeof(GET_SENSITIVITY)); } + +void MR24HPC1Component::get_unmanned_time() { this->send_query_(GET_UNMANNED_TIME, sizeof(GET_UNMANNED_TIME)); } + +void MR24HPC1Component::get_custom_mode() { this->send_query_(GET_CUSTOM_MODE, sizeof(GET_CUSTOM_MODE)); } + +void MR24HPC1Component::get_existence_boundary() { + this->send_query_(GET_EXISTENCE_BOUNDARY, sizeof(GET_EXISTENCE_BOUNDARY)); +} + +void MR24HPC1Component::get_motion_boundary() { this->send_query_(GET_MOTION_BOUNDARY, sizeof(GET_MOTION_BOUNDARY)); } + +void MR24HPC1Component::get_spatial_static_value() { + this->send_query_(GET_SPATIAL_STATIC_VALUE, sizeof(GET_SPATIAL_STATIC_VALUE)); +} + +void MR24HPC1Component::get_spatial_motion_value() { + this->send_query_(GET_SPATIAL_MOTION_VALUE, sizeof(GET_SPATIAL_MOTION_VALUE)); +} + +void MR24HPC1Component::get_distance_of_static_object() { + this->send_query_(GET_DISTANCE_OF_STATIC_OBJECT, sizeof(GET_DISTANCE_OF_STATIC_OBJECT)); +} + +void MR24HPC1Component::get_distance_of_moving_object() { + this->send_query_(GET_DISTANCE_OF_MOVING_OBJECT, sizeof(GET_DISTANCE_OF_MOVING_OBJECT)); +} + +void MR24HPC1Component::get_target_movement_speed() { + this->send_query_(GET_TARGET_MOVEMENT_SPEED, sizeof(GET_TARGET_MOVEMENT_SPEED)); +} + +void MR24HPC1Component::get_existence_threshold() { + this->send_query_(GET_EXISTENCE_THRESHOLD, sizeof(GET_EXISTENCE_THRESHOLD)); +} + +void MR24HPC1Component::get_motion_threshold() { + this->send_query_(GET_MOTION_THRESHOLD, sizeof(GET_MOTION_THRESHOLD)); +} + +void MR24HPC1Component::get_motion_trigger_time() { + this->send_query_(GET_MOTION_TRIGGER_TIME, sizeof(GET_MOTION_TRIGGER_TIME)); +} + +void MR24HPC1Component::get_motion_to_rest_time() { + this->send_query_(GET_MOTION_TO_REST_TIME, sizeof(GET_MOTION_TO_REST_TIME)); +} + +void MR24HPC1Component::get_custom_unman_time() { + this->send_query_(GET_CUSTOM_UNMAN_TIME, sizeof(GET_CUSTOM_UNMAN_TIME)); +} + +// Logic of setting: After setting, query whether the setting is successful or not! + +void MR24HPC1Component::set_underlying_open_function(bool enable) { + if (enable) { + this->send_query_(UNDERLYING_SWITCH_ON, sizeof(UNDERLYING_SWITCH_ON)); + } else { + this->send_query_(UNDERLYING_SWITCH_OFF, sizeof(UNDERLYING_SWITCH_OFF)); + } + if (this->keep_away_text_sensor_ != nullptr) { + this->keep_away_text_sensor_->publish_state(""); + } + if (this->motion_status_text_sensor_ != nullptr) { + this->motion_status_text_sensor_->publish_state(""); + } + if (this->custom_spatial_static_value_sensor_ != nullptr) { + this->custom_spatial_static_value_sensor_->publish_state(NAN); + } + if (this->custom_spatial_motion_value_sensor_ != nullptr) { + this->custom_spatial_motion_value_sensor_->publish_state(NAN); + } + if (this->custom_motion_distance_sensor_ != nullptr) { + this->custom_motion_distance_sensor_->publish_state(NAN); + } + if (this->custom_presence_of_detection_sensor_ != nullptr) { + this->custom_presence_of_detection_sensor_->publish_state(NAN); + } + if (this->custom_motion_speed_sensor_ != nullptr) { + this->custom_motion_speed_sensor_->publish_state(NAN); + } +} + +void MR24HPC1Component::set_scene_mode(uint8_t value) { + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x07, 0x00, 0x01, value, 0x00, 0x54, 0x43}; + send_data[7] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + if (this->custom_mode_number_ != nullptr) { + this->custom_mode_number_->publish_state(0); + } + if (this->custom_mode_num_sensor_ != nullptr) { + this->custom_mode_num_sensor_->publish_state(0); + } + this->get_scene_mode(); + this->get_sensitivity(); + this->get_custom_mode(); + this->get_existence_boundary(); + this->get_motion_boundary(); + this->get_existence_threshold(); + this->get_motion_threshold(); + this->get_motion_trigger_time(); + this->get_motion_to_rest_time(); + this->get_custom_unman_time(); +} + +void MR24HPC1Component::set_sensitivity(uint8_t value) { + if (value == 0x00) + return; + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x08, 0x00, 0x01, value, 0x00, 0x54, 0x43}; + send_data[7] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_scene_mode(); + this->get_sensitivity(); +} + +void MR24HPC1Component::set_restart() { + this->send_query_(SET_RESTART, sizeof(SET_RESTART)); + this->check_dev_inf_sign_ = true; +} + +void MR24HPC1Component::set_unman_time(uint8_t value) { + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x80, 0x0a, 0x00, 0x01, value, 0x00, 0x54, 0x43}; + send_data[7] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_unmanned_time(); +} + +void MR24HPC1Component::set_custom_mode(uint8_t mode) { + if (mode == 0) { + this->set_custom_end_mode(); // Equivalent to end setting + if (this->custom_mode_number_ != nullptr) { + this->custom_mode_number_->publish_state(0); + } + return; + } + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x09, 0x00, 0x01, mode, 0x00, 0x54, 0x43}; + send_data[7] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_existence_boundary(); + this->get_motion_boundary(); + this->get_existence_threshold(); + this->get_motion_threshold(); + this->get_motion_trigger_time(); + this->get_motion_to_rest_time(); + this->get_custom_unman_time(); + this->get_custom_mode(); + this->get_scene_mode(); + this->get_sensitivity(); +} + +void MR24HPC1Component::set_custom_end_mode() { + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x05, 0x0a, 0x00, 0x01, 0x0F, 0xCB, 0x54, 0x43}; + this->send_query_(send_data, send_data_len); + if (this->custom_mode_number_ != nullptr) { + this->custom_mode_number_->publish_state(0); // Clear setpoints + } + this->get_existence_boundary(); + this->get_motion_boundary(); + this->get_existence_threshold(); + this->get_motion_threshold(); + this->get_motion_trigger_time(); + this->get_motion_to_rest_time(); + this->get_custom_unman_time(); + this->get_custom_mode(); + this->get_scene_mode(); + this->get_sensitivity(); +} + +void MR24HPC1Component::set_existence_boundary(uint8_t value) { + if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) + return; // You'll have to check that you're in custom mode to set it up + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x0A, 0x00, 0x01, (uint8_t) (value + 1), 0x00, 0x54, 0x43}; + send_data[7] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_existence_boundary(); +} + +void MR24HPC1Component::set_motion_boundary(uint8_t value) { + if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) + return; // You'll have to check that you're in custom mode to set it up + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x0B, 0x00, 0x01, (uint8_t) (value + 1), 0x00, 0x54, 0x43}; + send_data[7] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_motion_boundary(); +} + +void MR24HPC1Component::set_existence_threshold(uint8_t value) { + if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) + return; // You'll have to check that you're in custom mode to set it up + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x08, 0x00, 0x01, value, 0x00, 0x54, 0x43}; + send_data[7] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_existence_threshold(); +} + +void MR24HPC1Component::set_motion_threshold(uint8_t value) { + if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) + return; // You'll have to check that you're in custom mode to set it up + uint8_t send_data_len = 10; + uint8_t send_data[10] = {0x53, 0x59, 0x08, 0x09, 0x00, 0x01, value, 0x00, 0x54, 0x43}; + send_data[7] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_motion_threshold(); +} + +void MR24HPC1Component::set_motion_trigger_time(uint8_t value) { + if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) + return; // You'll have to check that you're in custom mode to set it up + uint8_t send_data_len = 13; + uint8_t send_data[13] = {0x53, 0x59, 0x08, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, value, 0x00, 0x54, 0x43}; + send_data[10] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_motion_trigger_time(); +} + +void MR24HPC1Component::set_motion_to_rest_time(uint16_t value) { + if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) + return; // You'll have to check that you're in custom mode to set it up + uint8_t h8_num = (value >> 8) & 0xff; + uint8_t l8_num = value & 0xff; + uint8_t send_data_len = 13; + uint8_t send_data[13] = {0x53, 0x59, 0x08, 0x0D, 0x00, 0x04, 0x00, 0x00, h8_num, l8_num, 0x00, 0x54, 0x43}; + send_data[10] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_motion_to_rest_time(); +} + +void MR24HPC1Component::set_custom_unman_time(uint16_t value) { + if ((this->custom_mode_num_sensor_ != nullptr) && (this->custom_mode_num_sensor_->state == 0)) + return; // You'll have to check that you're in custom mode to set it up + uint32_t value_ms = value * 1000; + uint8_t h24_num = (value_ms >> 24) & 0xff; + uint8_t h16_num = (value_ms >> 16) & 0xff; + uint8_t h8_num = (value_ms >> 8) & 0xff; + uint8_t l8_num = value_ms & 0xff; + uint8_t send_data_len = 13; + uint8_t send_data[13] = {0x53, 0x59, 0x08, 0x0E, 0x00, 0x04, h24_num, h16_num, h8_num, l8_num, 0x00, 0x54, 0x43}; + send_data[10] = get_frame_crc_sum(send_data, send_data_len); + this->send_query_(send_data, send_data_len); + this->get_custom_unman_time(); +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.h b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.h new file mode 100644 index 0000000000..8fc61ad37c --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.h @@ -0,0 +1,217 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include "seeed_mr24hpc1_constants.h" + +#include + +namespace esphome { +namespace seeed_mr24hpc1 { + +enum FrameState { + FRAME_IDLE, + FRAME_HEADER2, + FRAME_CTL_WORD, + FRAME_CMD_WORD, + FRAME_DATA_LEN_H, + FRAME_DATA_LEN_L, + FRAME_DATA_BYTES, + FRAME_DATA_CRC, + FRAME_TAIL1, + FRAME_TAIL2, +}; + +enum PollingState { + STANDARD_FUNCTION_QUERY_PRODUCT_MODE = 0, + STANDARD_FUNCTION_QUERY_PRODUCT_ID, + STANDARD_FUNCTION_QUERY_FIRMWARE_VERSION, + STANDARD_FUNCTION_QUERY_HARDWARE_MODE, // Above is the equipment information + STANDARD_FUNCTION_QUERY_SCENE_MODE, + STANDARD_FUNCTION_QUERY_SENSITIVITY, + STANDARD_FUNCTION_QUERY_UNMANNED_TIME, + STANDARD_FUNCTION_QUERY_HUMAN_STATUS, + STANDARD_FUNCTION_QUERY_HUMAN_MOTION_INF, + STANDARD_FUNCTION_QUERY_BODY_MOVE_PARAMETER, + STANDARD_FUNCTION_QUERY_KEEPAWAY_STATUS, + STANDARD_QUERY_CUSTOM_MODE, + STANDARD_FUNCTION_QUERY_HEARTBEAT_STATE, // Above is the basic function + + CUSTOM_FUNCTION_QUERY_EXISTENCE_BOUNDARY, + CUSTOM_FUNCTION_QUERY_MOTION_BOUNDARY, + CUSTOM_FUNCTION_QUERY_EXISTENCE_THRESHOLD, + CUSTOM_FUNCTION_QUERY_MOTION_THRESHOLD, + CUSTOM_FUNCTION_QUERY_MOTION_TRIGGER_TIME, + CUSTOM_FUNCTION_QUERY_MOTION_TO_REST_TIME, + CUSTOM_FUNCTION_QUERY_TIME_OF_ENTER_UNMANNED, + + UNDERLY_FUNCTION_QUERY_HUMAN_STATUS, + UNDERLY_FUNCTION_QUERY_SPATIAL_STATIC_VALUE, + UNDERLY_FUNCTION_QUERY_SPATIAL_MOTION_VALUE, + UNDERLY_FUNCTION_QUERY_DISTANCE_OF_STATIC_OBJECT, + UNDERLY_FUNCTION_QUERY_DISTANCE_OF_MOVING_OBJECT, + UNDERLY_FUNCTION_QUERY_TARGET_MOVEMENT_SPEED, +}; + +enum OutputSwitch { + OUTPUT_SWITCH_INIT, + OUTPUT_SWITCH_ON, + OUTPUT_SWTICH_OFF, +}; + +static const char *const S_SCENE_STR[5] = {"None", "Living Room", "Bedroom", "Washroom", "Area Detection"}; +static const bool S_SOMEONE_EXISTS_STR[2] = {false, true}; +static const char *const S_MOTION_STATUS_STR[3] = {"None", "Motionless", "Active"}; +static const char *const S_KEEP_AWAY_STR[3] = {"None", "Close", "Away"}; +static const char *const S_UNMANNED_TIME_STR[9] = {"None", "10s", "30s", "1min", "2min", + "5min", "10min", "30min", "60min"}; +static const char *const S_BOUNDARY_STR[10] = {"0.5m", "1.0m", "1.5m", "2.0m", "2.5m", + "3.0m", "3.5m", "4.0m", "4.5m", "5.0m"}; // uint: m +static const float S_PRESENCE_OF_DETECTION_RANGE_STR[7] = {0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f}; // uint: m + +class MR24HPC1Component : public Component, + public uart::UARTDevice { // The class name must be the name defined by text_sensor.py +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(heartbeat_state) + SUB_TEXT_SENSOR(product_model) + SUB_TEXT_SENSOR(product_id) + SUB_TEXT_SENSOR(hardware_model) + SUB_TEXT_SENSOR(firware_version) + SUB_TEXT_SENSOR(keep_away) + SUB_TEXT_SENSOR(motion_status) + SUB_TEXT_SENSOR(custom_mode_end) +#endif +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(has_target) +#endif +#ifdef USE_SENSOR + SUB_SENSOR(custom_presence_of_detection) + SUB_SENSOR(movement_signs) + SUB_SENSOR(custom_motion_distance) + SUB_SENSOR(custom_spatial_static_value) + SUB_SENSOR(custom_spatial_motion_value) + SUB_SENSOR(custom_motion_speed) + SUB_SENSOR(custom_mode_num) +#endif +#ifdef USE_SWITCH + SUB_SWITCH(underlying_open_function) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(restart) + SUB_BUTTON(custom_set_end) +#endif +#ifdef USE_SELECT + SUB_SELECT(scene_mode) + SUB_SELECT(unman_time) + SUB_SELECT(existence_boundary) + SUB_SELECT(motion_boundary) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(sensitivity) + SUB_NUMBER(custom_mode) + SUB_NUMBER(existence_threshold) + SUB_NUMBER(motion_threshold) + SUB_NUMBER(motion_trigger) + SUB_NUMBER(motion_to_rest) + SUB_NUMBER(custom_unman_time) +#endif + + protected: + char c_product_mode_[PRODUCT_BUF_MAX_SIZE + 1]; + char c_product_id_[PRODUCT_BUF_MAX_SIZE + 1]; + char c_hardware_model_[PRODUCT_BUF_MAX_SIZE + 1]; + char c_firmware_version_[PRODUCT_BUF_MAX_SIZE + 1]; + uint8_t s_output_info_switch_flag_; + uint8_t sg_recv_data_state_; + uint8_t sg_frame_len_; + uint8_t sg_data_len_; + uint8_t sg_frame_buf_[FRAME_BUF_MAX_SIZE]; + uint8_t sg_frame_prase_buf_[FRAME_BUF_MAX_SIZE]; + int sg_start_query_data_; + bool check_dev_inf_sign_; + bool poll_time_base_func_check_; + + void update_(); + void r24_split_data_frame_(uint8_t value); + void r24_parse_data_frame_(uint8_t *data, uint8_t len); + void r24_frame_parse_open_underlying_information_(uint8_t *data); + void r24_frame_parse_work_status_(uint8_t *data); + void r24_frame_parse_product_information_(uint8_t *data); + void r24_frame_parse_human_information_(uint8_t *data); + void send_query_(const uint8_t *query, size_t string_length); + + public: + float get_setup_priority() const override { return esphome::setup_priority::LATE; } + void setup() override; + void dump_config() override; + void loop() override; + + void get_heartbeat_packet(); + void get_radar_output_information_switch(); + void get_product_mode(); + void get_product_id(); + void get_hardware_model(); + void get_firmware_version(); + void get_human_status(); + void get_human_motion_info(); + void get_body_motion_params(); + void get_keep_away(); + void get_scene_mode(); + void get_sensitivity(); + void get_unmanned_time(); + void get_custom_mode(); + void get_existence_boundary(); + void get_motion_boundary(); + void get_spatial_static_value(); + void get_spatial_motion_value(); + void get_distance_of_static_object(); + void get_distance_of_moving_object(); + void get_target_movement_speed(); + void get_existence_threshold(); + void get_motion_threshold(); + void get_motion_trigger_time(); + void get_motion_to_rest_time(); + void get_custom_unman_time(); + + void set_scene_mode(uint8_t value); + void set_underlying_open_function(bool enable); + void set_sensitivity(uint8_t value); + void set_restart(); + void set_unman_time(uint8_t value); + void set_custom_mode(uint8_t mode); + void set_custom_end_mode(); + void set_existence_boundary(uint8_t value); + void set_motion_boundary(uint8_t value); + void set_existence_threshold(uint8_t value); + void set_motion_threshold(uint8_t value); + void set_motion_trigger_time(uint8_t value); + void set_motion_to_rest_time(uint16_t value); + void set_custom_unman_time(uint16_t value); +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1_constants.h b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1_constants.h new file mode 100644 index 0000000000..dafc6c0368 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1_constants.h @@ -0,0 +1,173 @@ +#pragma once + +#include + +namespace esphome { +namespace seeed_mr24hpc1 { + +static const uint8_t FRAME_BUF_MAX_SIZE = 128; +static const uint8_t PRODUCT_BUF_MAX_SIZE = 32; + +static const uint8_t FRAME_CONTROL_WORD_INDEX = 2; +static const uint8_t FRAME_COMMAND_WORD_INDEX = 3; +static const uint8_t FRAME_DATA_INDEX = 6; + +static const uint8_t FRAME_HEADER1_VALUE = 0x53; +static const uint8_t FRAME_HEADER2_VALUE = 0x59; +static const uint8_t FRAME_TAIL1_VALUE = 0x54; +static const uint8_t FRAME_TAIL2_VALUE = 0x43; + +static const uint8_t CONTROL_MAIN = 0x01; +static const uint8_t CONTROL_PRODUCT_INFORMATION = 0x02; +static const uint8_t CONTROL_WORK = 0x05; +static const uint8_t CONTROL_UNDERLYING_FUNCTION = 0x08; +static const uint8_t CONTROL_HUMAN_INFORMATION = 0x80; + +static const uint8_t COMMAND_HEARTBEAT = 0x01; +static const uint8_t COMMAND_RESTART = 0x02; + +static const uint8_t COMMAND_PRODUCT_MODE = 0xA1; +static const uint8_t COMMAND_PRODUCT_ID = 0xA2; +static const uint8_t COMMAND_HARDWARE_MODEL = 0xA3; +static const uint8_t COMMAND_FIRMWARE_VERSION = 0xA4; + +static const uint8_t GET_HEARTBEAT[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_MAIN, COMMAND_HEARTBEAT, 0x00, 0x01, 0x0F, 0xBE, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t SET_RESTART[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_MAIN, COMMAND_RESTART, 0x00, 0x01, 0x0F, 0xBF, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; + +static const uint8_t GET_PRODUCT_MODE[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_PRODUCT_INFORMATION, COMMAND_PRODUCT_MODE, 0x00, 0x01, 0x0F, 0x5F, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_PRODUCT_ID[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_PRODUCT_INFORMATION, COMMAND_PRODUCT_ID, 0x00, 0x01, 0x0F, 0x60, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_HARDWARE_MODEL[] = { + FRAME_HEADER1_VALUE, + FRAME_HEADER2_VALUE, + CONTROL_PRODUCT_INFORMATION, + COMMAND_HARDWARE_MODEL, + 0x00, + 0x01, + 0x0F, + 0x61, + FRAME_TAIL1_VALUE, + FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_FIRMWARE_VERSION[] = { + FRAME_HEADER1_VALUE, + FRAME_HEADER2_VALUE, + CONTROL_PRODUCT_INFORMATION, + COMMAND_FIRMWARE_VERSION, + 0x00, + 0x01, + 0x0F, + 0x62, + FRAME_TAIL1_VALUE, + FRAME_TAIL2_VALUE, +}; + +static const uint8_t GET_SCENE_MODE[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_WORK, 0x87, 0x00, 0x01, 0x0F, 0x48, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_SENSITIVITY[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_WORK, 0x88, 0x00, 0x01, 0x0F, 0x49, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_CUSTOM_MODE[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_WORK, 0x89, 0x00, 0x01, 0x0F, 0x4A, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; + +static const uint8_t UNDERLYING_SWITCH_ON[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x00, 0x00, 0x01, 0x01, 0xB6, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t UNDERLYING_SWITCH_OFF[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x00, 0x00, 0x01, 0x00, 0xB5, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; + +static const uint8_t GET_RADAR_OUTPUT_INFORMATION_SWITCH[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x80, 0x00, 0x01, 0x0F, 0x44, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_SPATIAL_STATIC_VALUE[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x81, 0x00, 0x01, 0x0F, 0x45, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_SPATIAL_MOTION_VALUE[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x82, 0x00, 0x01, 0x0F, 0x46, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_DISTANCE_OF_STATIC_OBJECT[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x83, 0x00, 0x01, 0x0F, 0x47, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_DISTANCE_OF_MOVING_OBJECT[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x84, 0x00, 0x01, 0x0F, 0x48, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_TARGET_MOVEMENT_SPEED[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x85, 0x00, 0x01, 0x0F, 0x49, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_EXISTENCE_THRESHOLD[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x88, 0x00, 0x01, 0x0F, 0x4C, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_MOTION_THRESHOLD[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x89, 0x00, 0x01, 0x0F, 0x4D, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_EXISTENCE_BOUNDARY[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x8A, 0x00, 0x01, 0x0F, 0x4E, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_MOTION_BOUNDARY[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x8B, 0x00, 0x01, 0x0F, 0x4F, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_MOTION_TRIGGER_TIME[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x8C, 0x00, 0x01, 0x0F, 0x50, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_MOTION_TO_REST_TIME[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x8D, 0x00, 0x01, 0x0F, 0x51, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_CUSTOM_UNMAN_TIME[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_UNDERLYING_FUNCTION, 0x8E, 0x00, 0x01, 0x0F, 0x52, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; + +static const uint8_t GET_HUMAN_STATUS[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_HUMAN_INFORMATION, 0x81, 0x00, 0x01, 0x0F, 0xBD, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_HUMAN_MOTION_INFORMATION[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_HUMAN_INFORMATION, 0x82, 0x00, 0x01, 0x0F, 0xBE, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_BODY_MOTION_PARAMETERS[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_HUMAN_INFORMATION, 0x83, 0x00, 0x01, 0x0F, 0xBF, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_UNMANNED_TIME[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_HUMAN_INFORMATION, 0x8A, 0x00, 0x01, 0x0F, 0xC6, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; +static const uint8_t GET_KEEP_AWAY[] = { + FRAME_HEADER1_VALUE, FRAME_HEADER2_VALUE, CONTROL_HUMAN_INFORMATION, 0x8B, 0x00, 0x01, 0x0F, 0xC7, + FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/select/__init__.py b/esphome/components/seeed_mr24hpc1/select/__init__.py new file mode 100644 index 0000000000..7da83627b9 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/__init__.py @@ -0,0 +1,103 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ( + ENTITY_CATEGORY_CONFIG, +) +from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns + +SceneModeSelect = mr24hpc1_ns.class_("SceneModeSelect", select.Select) +UnmanTimeSelect = mr24hpc1_ns.class_("UnmanTimeSelect", select.Select) +ExistenceBoundarySelect = mr24hpc1_ns.class_("ExistenceBoundarySelect", select.Select) +MotionBoundarySelect = mr24hpc1_ns.class_("MotionBoundarySelect", select.Select) + +CONF_SCENE_MODE = "scene_mode" +CONF_UNMAN_TIME = "unman_time" +CONF_EXISTENCE_BOUNDARY = "existence_boundary" +CONF_MOTION_BOUNDARY = "motion_boundary" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component), + cv.Optional(CONF_SCENE_MODE): select.select_schema( + SceneModeSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:hoop-house", + ), + cv.Optional(CONF_UNMAN_TIME): select.select_schema( + UnmanTimeSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:timeline-clock", + ), + cv.Optional(CONF_EXISTENCE_BOUNDARY): select.select_schema( + ExistenceBoundarySelect, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + cv.Optional(CONF_MOTION_BOUNDARY): select.select_schema( + MotionBoundarySelect, + entity_category=ENTITY_CATEGORY_CONFIG, + ), +} + + +async def to_code(config): + mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) + if scenemode_config := config.get(CONF_SCENE_MODE): + s = await select.new_select( + scenemode_config, + options=["None", "Living Room", "Bedroom", "Washroom", "Area Detection"], + ) + await cg.register_parented(s, config[CONF_MR24HPC1_ID]) + cg.add(mr24hpc1_component.set_scene_mode_select(s)) + if unmantime_config := config.get(CONF_UNMAN_TIME): + s = await select.new_select( + unmantime_config, + options=[ + "None", + "10s", + "30s", + "1min", + "2min", + "5min", + "10min", + "30min", + "60min", + ], + ) + await cg.register_parented(s, config[CONF_MR24HPC1_ID]) + cg.add(mr24hpc1_component.set_unman_time_select(s)) + if existence_boundary_config := config.get(CONF_EXISTENCE_BOUNDARY): + s = await select.new_select( + existence_boundary_config, + options=[ + "0.5m", + "1.0m", + "1.5m", + "2.0m", + "2.5m", + "3.0m", + "3.5m", + "4.0m", + "4.5m", + "5.0m", + ], + ) + await cg.register_parented(s, config[CONF_MR24HPC1_ID]) + cg.add(mr24hpc1_component.set_existence_boundary_select(s)) + if motion_boundary_config := config.get(CONF_MOTION_BOUNDARY): + s = await select.new_select( + motion_boundary_config, + options=[ + "0.5m", + "1.0m", + "1.5m", + "2.0m", + "2.5m", + "3.0m", + "3.5m", + "4.0m", + "4.5m", + "5.0m", + ], + ) + await cg.register_parented(s, config[CONF_MR24HPC1_ID]) + cg.add(mr24hpc1_component.set_motion_boundary_select(s)) diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp new file mode 100644 index 0000000000..03c2ec4745 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp @@ -0,0 +1,15 @@ +#include "existence_boundary_select.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void ExistenceBoundarySelect::control(const std::string &value) { + this->publish_state(value); + auto index = this->index_of(value); + if (index.has_value()) { + this->parent_->set_existence_boundary(index.value()); + } +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h new file mode 100644 index 0000000000..ad770a7296 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class ExistenceBoundarySelect : public select::Select, public Parented { + public: + ExistenceBoundarySelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.cpp b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.cpp new file mode 100644 index 0000000000..619a4f0935 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.cpp @@ -0,0 +1,15 @@ +#include "motion_boundary_select.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void MotionBoundarySelect::control(const std::string &value) { + this->publish_state(value); + auto index = this->index_of(value); + if (index.has_value()) { + this->parent_->set_motion_boundary(index.value()); + } +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h new file mode 100644 index 0000000000..9058e3130b --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class MotionBoundarySelect : public select::Select, public Parented { + public: + MotionBoundarySelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.cpp b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.cpp new file mode 100644 index 0000000000..153ae603cf --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.cpp @@ -0,0 +1,15 @@ +#include "scene_mode_select.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void SceneModeSelect::control(const std::string &value) { + this->publish_state(value); + auto index = this->index_of(value); + if (index.has_value()) { + this->parent_->set_scene_mode(index.value()); + } +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h new file mode 100644 index 0000000000..95508d49b0 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class SceneModeSelect : public select::Select, public Parented { + public: + SceneModeSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/select/unman_time_select.cpp b/esphome/components/seeed_mr24hpc1/select/unman_time_select.cpp new file mode 100644 index 0000000000..a9d96c8f67 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/unman_time_select.cpp @@ -0,0 +1,15 @@ +#include "unman_time_select.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void UnmanTimeSelect::control(const std::string &value) { + this->publish_state(value); + auto index = this->index_of(value); + if (index.has_value()) { + this->parent_->set_unman_time(index.value()); + } +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h new file mode 100644 index 0000000000..7131988cda --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class UnmanTimeSelect : public select::Select, public Parented { + public: + UnmanTimeSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/sensor.py b/esphome/components/seeed_mr24hpc1/sensor.py new file mode 100644 index 0000000000..d5eb09e265 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/sensor.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_SPEED, + UNIT_METER, +) +from . import CONF_MR24HPC1_ID, MR24HPC1Component + +CONF_CUSTOM_PRESENCE_OF_DETECTION = "custom_presence_of_detection" +CONF_MOVEMENT_SIGNS = "movement_signs" +CONF_CUSTOM_MOTION_DISTANCE = "custom_motion_distance" +CONF_CUSTOM_SPATIAL_STATIC_VALUE = "custom_spatial_static_value" +CONF_CUSTOM_SPATIAL_MOTION_VALUE = "custom_spatial_motion_value" +CONF_CUSTOM_MOTION_SPEED = "custom_motion_speed" +CONF_CUSTOM_MODE_NUM = "custom_mode_num" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component), + cv.Optional(CONF_CUSTOM_PRESENCE_OF_DETECTION): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_METER, + accuracy_decimals=2, # Specify the number of decimal places + icon="mdi:signal-distance-variant", + ), + cv.Optional(CONF_MOVEMENT_SIGNS): sensor.sensor_schema( + icon="mdi:human-greeting-variant", + ), + cv.Optional(CONF_CUSTOM_MOTION_DISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + accuracy_decimals=2, + icon="mdi:signal-distance-variant", + ), + cv.Optional(CONF_CUSTOM_SPATIAL_STATIC_VALUE): sensor.sensor_schema( + device_class=DEVICE_CLASS_ENERGY, + icon="mdi:counter", + ), + cv.Optional(CONF_CUSTOM_SPATIAL_MOTION_VALUE): sensor.sensor_schema( + device_class=DEVICE_CLASS_ENERGY, + icon="mdi:counter", + ), + cv.Optional(CONF_CUSTOM_MOTION_SPEED): sensor.sensor_schema( + unit_of_measurement="m/s", + device_class=DEVICE_CLASS_SPEED, + accuracy_decimals=2, + icon="mdi:run-fast", + ), + cv.Optional(CONF_CUSTOM_MODE_NUM): sensor.sensor_schema( + icon="mdi:counter", + ), + } +) + + +async def to_code(config): + mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) + if custompresenceofdetection_config := config.get( + CONF_CUSTOM_PRESENCE_OF_DETECTION + ): + sens = await sensor.new_sensor(custompresenceofdetection_config) + cg.add(mr24hpc1_component.set_custom_presence_of_detection_sensor(sens)) + if movementsigns_config := config.get(CONF_MOVEMENT_SIGNS): + sens = await sensor.new_sensor(movementsigns_config) + cg.add(mr24hpc1_component.set_movement_signs_sensor(sens)) + if custommotiondistance_config := config.get(CONF_CUSTOM_MOTION_DISTANCE): + sens = await sensor.new_sensor(custommotiondistance_config) + cg.add(mr24hpc1_component.set_custom_motion_distance_sensor(sens)) + if customspatialstaticvalue_config := config.get(CONF_CUSTOM_SPATIAL_STATIC_VALUE): + sens = await sensor.new_sensor(customspatialstaticvalue_config) + cg.add(mr24hpc1_component.set_custom_spatial_static_value_sensor(sens)) + if customspatialmotionvalue_config := config.get(CONF_CUSTOM_SPATIAL_MOTION_VALUE): + sens = await sensor.new_sensor(customspatialmotionvalue_config) + cg.add(mr24hpc1_component.set_custom_spatial_motion_value_sensor(sens)) + if custommotionspeed_config := config.get(CONF_CUSTOM_MOTION_SPEED): + sens = await sensor.new_sensor(custommotionspeed_config) + cg.add(mr24hpc1_component.set_custom_motion_speed_sensor(sens)) + if custommodenum_config := config.get(CONF_CUSTOM_MODE_NUM): + sens = await sensor.new_sensor(custommodenum_config) + cg.add(mr24hpc1_component.set_custom_mode_num_sensor(sens)) diff --git a/esphome/components/seeed_mr24hpc1/switch/__init__.py b/esphome/components/seeed_mr24hpc1/switch/__init__.py new file mode 100644 index 0000000000..bbf5391a57 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/switch/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_SWITCH, + ENTITY_CATEGORY_CONFIG, +) +from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns + +UnderlyingOpenFuncSwitch = mr24hpc1_ns.class_( + "UnderlyOpenFunctionSwitch", switch.Switch +) + +CONF_UNDERLYING_OPEN_FUNCTION = "underlying_open_function" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component), + cv.Optional(CONF_UNDERLYING_OPEN_FUNCTION): switch.switch_schema( + UnderlyingOpenFuncSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:electric-switch", + ), +} + + +async def to_code(config): + mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) + if underlying_open_function_config := config.get(CONF_UNDERLYING_OPEN_FUNCTION): + s = await switch.new_switch(underlying_open_function_config) + await cg.register_parented(s, config[CONF_MR24HPC1_ID]) + cg.add(mr24hpc1_component.set_underlying_open_function_switch(s)) diff --git a/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.cpp b/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.cpp new file mode 100644 index 0000000000..0fcc49bc4c --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.cpp @@ -0,0 +1,12 @@ +#include "underlyFuc_switch.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +void UnderlyOpenFunctionSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_underlying_open_function(state); +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.h b/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.h new file mode 100644 index 0000000000..1baabb25ce --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../seeed_mr24hpc1.h" + +namespace esphome { +namespace seeed_mr24hpc1 { + +class UnderlyOpenFunctionSwitch : public switch_::Switch, public Parented { + public: + UnderlyOpenFunctionSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/esphome/components/seeed_mr24hpc1/text_sensor.py b/esphome/components/seeed_mr24hpc1/text_sensor.py new file mode 100644 index 0000000000..aa50f577d4 --- /dev/null +++ b/esphome/components/seeed_mr24hpc1/text_sensor.py @@ -0,0 +1,74 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC +from . import CONF_MR24HPC1_ID, MR24HPC1Component + +CONF_HEART_BEAT = "heart_beat" +CONF_PRODUCT_MODEL = "product_model" +CONF_PRODUCT_ID = "product_id" +CONF_HARDWARE_MODEL = "hardware_model" +CONF_HARDWARE_VERSION = "hardware_version" + +CONF_KEEP_AWAY = "keep_away" +CONF_MOTION_STATUS = "motion_status" + +CONF_CUSTOM_MODE_END = "custom_mode_end" + + +# The entity category for read only diagnostic values, for example RSSI, uptime or MAC Address +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component), + cv.Optional(CONF_HEART_BEAT): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:connection" + ), + cv.Optional(CONF_PRODUCT_MODEL): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:information-outline" + ), + cv.Optional(CONF_PRODUCT_ID): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:information-outline" + ), + cv.Optional(CONF_HARDWARE_MODEL): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:information-outline" + ), + cv.Optional(CONF_HARDWARE_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:information-outline" + ), + cv.Optional(CONF_KEEP_AWAY): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:walk" + ), + cv.Optional(CONF_MOTION_STATUS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:human-greeting" + ), + cv.Optional(CONF_CUSTOM_MODE_END): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:account-check" + ), +} + + +async def to_code(config): + mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) + if heartbeat_config := config.get(CONF_HEART_BEAT): + sens = await text_sensor.new_text_sensor(heartbeat_config) + cg.add(mr24hpc1_component.set_heartbeat_state_text_sensor(sens)) + if productmodel_config := config.get(CONF_PRODUCT_MODEL): + sens = await text_sensor.new_text_sensor(productmodel_config) + cg.add(mr24hpc1_component.set_product_model_text_sensor(sens)) + if productid_config := config.get(CONF_PRODUCT_ID): + sens = await text_sensor.new_text_sensor(productid_config) + cg.add(mr24hpc1_component.set_product_id_text_sensor(sens)) + if hardwaremodel_config := config.get(CONF_HARDWARE_MODEL): + sens = await text_sensor.new_text_sensor(hardwaremodel_config) + cg.add(mr24hpc1_component.set_hardware_model_text_sensor(sens)) + if firwareversion_config := config.get(CONF_HARDWARE_VERSION): + sens = await text_sensor.new_text_sensor(firwareversion_config) + cg.add(mr24hpc1_component.set_firware_version_text_sensor(sens)) + if keepaway_config := config.get(CONF_KEEP_AWAY): + sens = await text_sensor.new_text_sensor(keepaway_config) + cg.add(mr24hpc1_component.set_keep_away_text_sensor(sens)) + if motionstatus_config := config.get(CONF_MOTION_STATUS): + sens = await text_sensor.new_text_sensor(motionstatus_config) + cg.add(mr24hpc1_component.set_motion_status_text_sensor(sens)) + if custommodeend_config := config.get(CONF_CUSTOM_MODE_END): + sens = await text_sensor.new_text_sensor(custommodeend_config) + cg.add(mr24hpc1_component.set_custom_mode_end_text_sensor(sens)) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 760f7600b7..5a3271fdfd 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -1,19 +1,20 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_CYCLE, CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, + CONF_INDEX, + CONF_MODE, + CONF_MQTT_ID, CONF_ON_VALUE, + CONF_OPERATION, CONF_OPTION, CONF_TRIGGER_ID, - CONF_MQTT_ID, - CONF_CYCLE, - CONF_MODE, - CONF_OPERATION, - CONF_INDEX, + CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass @@ -47,16 +48,20 @@ SELECT_OPERATION_OPTIONS = { } -SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), - cv.GenerateID(): cv.declare_id(Select), - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SelectStateTrigger), - } - ), - } +SELECT_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), + cv.GenerateID(): cv.declare_id(Select), + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SelectStateTrigger), + } + ), + } + ) ) _UNDEF = object() @@ -95,10 +100,13 @@ async def setup_select_core_(var, config, *, options: list[str]): trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf ) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + async def register_select(var, config, *, options: list[str]): if not CORE.has_id(config[CONF_ID]): @@ -113,7 +121,7 @@ async def new_select(config, *, options: list[str]): return var -@coroutine_with_priority(40.0) +@coroutine_with_priority(100.0) async def to_code(config): cg.add_define("USE_SELECT") cg.add_global(select_ns.using) @@ -223,14 +231,14 @@ async def select_set_index_to_code(config, action_id, template_arg, args): async def select_operation_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_OPERATION in config: - op_ = await cg.templatable(config[CONF_OPERATION], args, SelectOperation) + if (operation := config.get(CONF_OPERATION)) is not None: + op_ = await cg.templatable(operation, args, SelectOperation) cg.add(var.set_operation(op_)) - if CONF_CYCLE in config: - cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) - cg.add(var.set_cycle(cycle_)) - if CONF_MODE in config: - cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[config[CONF_MODE]])) - if CONF_CYCLE in config: - cg.add(var.set_cycle(config[CONF_CYCLE])) + if (cycle := config.get(CONF_CYCLE)) is not None: + template_ = await cg.templatable(cycle, args, bool) + cg.add(var.set_cycle(template_)) + if (mode := config.get(CONF_MODE)) is not None: + cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[mode])) + if (cycle := config.get(CONF_CYCLE)) is not None: + cg.add(var.set_cycle(cycle)) return var diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index f4583b4e2e..806882ad94 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -12,7 +12,7 @@ void Select::publish_state(const std::string &state) { if (index.has_value()) { this->has_state_ = true; this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %s (index %d)", name, state.c_str(), index.value()); + ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); this->state_callback_.call(state, index.value()); } else { ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 23258d5785..8ca9a69d1c 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -17,6 +17,13 @@ namespace select { } \ } +#define SUB_SELECT(name) \ + protected: \ + select::Select *name##_select_{nullptr}; \ +\ + public: \ + void set_##name##_select(select::Select *select) { this->name##_select_ = select; } + /** Base-class for all selects. * * A select can use publish_state to send out a new value. diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 6ee41b1029..85f755645c 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -71,7 +71,7 @@ void SelectCall::perform() { return; } if (this->index_.value() >= options.size()) { - ESP_LOGW(TAG, "'%s' - Index value %d out of bounds", name, this->index_.value()); + ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, this->index_.value()); return; } target_value = options[this->index_.value()]; diff --git a/esphome/components/sen0321/__init__.py b/esphome/components/sen0321/__init__.py new file mode 100644 index 0000000000..458ffa67f9 --- /dev/null +++ b/esphome/components/sen0321/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@notjj"] diff --git a/esphome/components/sen0321/sen0321.cpp b/esphome/components/sen0321/sen0321.cpp new file mode 100644 index 0000000000..7801c8c389 --- /dev/null +++ b/esphome/components/sen0321/sen0321.cpp @@ -0,0 +1,36 @@ +#include "sen0321.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace sen0321_sensor { + +static const char *const TAG = "sen0321_sensor.sensor"; + +void Sen0321Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up sen0321..."); + if (!this->write_byte(SENSOR_MODE_REGISTER, SENSOR_MODE_AUTO)) { + ESP_LOGW(TAG, "Error setting measurement mode."); + this->mark_failed(); + }; +} + +void Sen0321Sensor::update() { this->read_data_(); } + +void Sen0321Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "DF Robot Ozone Sensor sen0321:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with sen0321 failed!"); + } + LOG_UPDATE_INTERVAL(this); +} + +void Sen0321Sensor::read_data_() { + uint8_t result[2]; + this->read_bytes(SENSOR_AUTO_READ_REG, result, (uint8_t) 2); + this->publish_state(((uint16_t) (result[0] << 8) + result[1])); +} + +} // namespace sen0321_sensor +} // namespace esphome diff --git a/esphome/components/sen0321/sen0321.h b/esphome/components/sen0321/sen0321.h new file mode 100644 index 0000000000..3bb3d5b015 --- /dev/null +++ b/esphome/components/sen0321/sen0321.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +// ref: +// https://github.com/DFRobot/DFRobot_OzoneSensor + +namespace esphome { +namespace sen0321_sensor { +// Sensor Mode +// While passive is supposedly supported, it does not appear to work reliably. +static const uint8_t SENSOR_MODE_REGISTER = 0x03; +static const uint8_t SENSOR_MODE_AUTO = 0x00; +static const uint8_t SENSOR_MODE_PASSIVE = 0x01; +static const uint8_t SET_REGISTER = 0x04; + +// Each register is 2 wide, so 0x07-0x08 for passive, or 0x09-0x0A for auto +// First register is high bits, next low. +static const uint8_t SENSOR_PASS_READ_REG = 0x07; +static const uint8_t SENSOR_AUTO_READ_REG = 0x09; + +class Sen0321Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void update() override; + void dump_config() override; + void setup() override; + + protected: + void read_data_(); +}; + +} // namespace sen0321_sensor +} // namespace esphome diff --git a/esphome/components/sen0321/sensor.py b/esphome/components/sen0321/sensor.py new file mode 100644 index 0000000000..ee613dc440 --- /dev/null +++ b/esphome/components/sen0321/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + ICON_CHEMICAL_WEAPON, + UNIT_PARTS_PER_BILLION, + STATE_CLASS_MEASUREMENT, +) + +CODEOWNERS = ["@notjj"] +DEPENDENCIES = ["i2c"] + +sen0321_sensor_ns = cg.esphome_ns.namespace("sen0321_sensor") +Sen0321Sensor = sen0321_sensor_ns.class_( + "Sen0321Sensor", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + Sen0321Sensor, + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x73)) +) + + +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/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index ddce568c97..0efc961943 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -1,6 +1,8 @@ #include "sen5x.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include namespace esphome { namespace sen5x { @@ -134,21 +136,24 @@ void SEN5XComponent::setup() { ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); if (this->voc_sensor_ && this->store_baseline_) { - // Hash with compilation time + 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 // This ensures the baseline storage is cleared after OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); + // 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)); this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0, - voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); } // Initialize storage timestamp this->seconds_since_last_store_ = 0; if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { - ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", + ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); uint16_t states[4]; @@ -196,13 +201,19 @@ void SEN5XComponent::setup() { ESP_LOGE(TAG, "Failed to read RHT Acceleration mode"); } } - if (this->voc_tuning_params_.has_value()) + if (this->voc_tuning_params_.has_value()) { this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value()); - if (this->nox_tuning_params_.has_value()) + delay(20); + } + if (this->nox_tuning_params_.has_value()) { this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); + delay(20); + } - if (this->temperature_compensation_.has_value()) + if (this->temperature_compensation_.has_value()) { this->write_temperature_compensation_(this->temperature_compensation_.value()); + delay(20); + } // Finally start sensor measurements auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY; @@ -252,7 +263,7 @@ void SEN5XComponent::dump_config() { ESP_LOGCONFIG(TAG, " Firmware version: %d", this->firmware_version_); ESP_LOGCONFIG(TAG, " Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); if (this->auto_cleaning_interval_.has_value()) { - ESP_LOGCONFIG(TAG, " Auto cleaning interval %d seconds", auto_cleaning_interval_.value()); + ESP_LOGCONFIG(TAG, " Auto cleaning interval %" PRId32 " seconds", auto_cleaning_interval_.value()); } if (this->acceleration_mode_.has_value()) { switch (this->acceleration_mode_.value()) { @@ -302,8 +313,8 @@ void SEN5XComponent::update() { this->voc_baselines_storage_.state1 = state1; if (this->pref_.save(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0, - voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); } else { ESP_LOGW(TAG, "Could not store VOC baselines"); } @@ -341,7 +352,7 @@ void SEN5XComponent::update() { float humidity = measurements[4] / 100.0; if (measurements[4] == 0xFFFF) humidity = NAN; - float temperature = measurements[5] / 200.0; + float temperature = (int16_t) measurements[5] / 200.0; if (measurements[5] == 0xFFFF) temperature = NAN; float voc = measurements[6] / 10.0; diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index f306003a82..6d90636a89 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -41,8 +41,8 @@ struct GasTuning { }; struct TemperatureCompensation { - uint16_t offset; - uint16_t normalized_offset_slope; + int16_t offset; + int16_t normalized_offset_slope; uint16_t time_constant; }; @@ -70,27 +70,33 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t std_initial, uint16_t gain_factor) { - voc_tuning_params_.value().index_offset = index_offset; - voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; - voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; - voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; - voc_tuning_params_.value().std_initial = std_initial; - voc_tuning_params_.value().gain_factor = gain_factor; + GasTuning tuning_params; + tuning_params.index_offset = index_offset; + tuning_params.learning_time_offset_hours = learning_time_offset_hours; + tuning_params.learning_time_gain_hours = learning_time_gain_hours; + tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; + tuning_params.std_initial = std_initial; + tuning_params.gain_factor = gain_factor; + voc_tuning_params_ = tuning_params; } void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t gain_factor) { - nox_tuning_params_.value().index_offset = index_offset; - nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; - nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; - nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; - nox_tuning_params_.value().std_initial = 50; - nox_tuning_params_.value().gain_factor = gain_factor; + GasTuning tuning_params; + tuning_params.index_offset = index_offset; + tuning_params.learning_time_offset_hours = learning_time_offset_hours; + tuning_params.learning_time_gain_hours = learning_time_gain_hours; + tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; + tuning_params.std_initial = 50; + tuning_params.gain_factor = gain_factor; + nox_tuning_params_ = tuning_params; } void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { - temperature_compensation_.value().offset = offset * 200; - temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100; - temperature_compensation_.value().time_constant = time_constant; + TemperatureCompensation temp_comp; + temp_comp.offset = offset * 200; + temp_comp.normalized_offset_slope = normalized_offset_slope * 10000; + temp_comp.time_constant = time_constant; + temperature_compensation_ = temp_comp; } bool start_fan_cleaning(); diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 392510e417..67bd627f7f 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -14,13 +14,12 @@ from esphome.const import ( CONF_PM_4_0, CONF_STORE_BASELINE, CONF_TEMPERATURE, + DEVICE_CLASS_AQI, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, ICON_CHEMICAL_WEAPON, ICON_RADIATOR, ICON_THERMOMETER, @@ -88,6 +87,15 @@ GAS_SENSOR = cv.Schema( } ) + +def float_previously_pct(value): + if isinstance(value, str) and "%" in value: + raise cv.Invalid( + f"The value '{value}' is a percentage. Suggested value: {float(value.strip('%')) / 100}" + ) + return value + + CONFIG_SCHEMA = ( cv.Schema( { @@ -123,13 +131,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_VOC): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, ).extend(GAS_SENSOR), cv.Optional(CONF_NOX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_NITROUS_OXIDE, + device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, ).extend(GAS_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, @@ -151,7 +159,9 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema( { cv.Optional(CONF_OFFSET, default=0): cv.float_, - cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage, + cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.All( + float_previously_pct, cv.float_ + ), cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_, } ), diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index e0504eb2b9..e58ee157f7 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -54,9 +54,9 @@ void SenseAirComponent::update() { this->status_clear_warning(); const uint8_t length = response[2]; const uint16_t status = (uint16_t(response[3]) << 8) | response[4]; - const uint16_t ppm = (uint16_t(response[length + 1]) << 8) | response[length + 2]; + const int16_t ppm = int16_t((response[length + 1] << 8) | response[length + 2]); - ESP_LOGD(TAG, "SenseAir Received CO₂=%uppm Status=0x%02X", ppm, status); + ESP_LOGD(TAG, "SenseAir Received CO₂=%dppm Status=0x%02X", ppm, status); if (this->co2_sensor_ != nullptr) this->co2_sensor_->publish_state(ppm); } diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 06b96171a7..27338b8608 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -1,21 +1,28 @@ import math -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( - CONF_DEVICE_CLASS, CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, + CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_EXPIRE_AFTER, CONF_FILTERS, + CONF_FORCE_UPDATE, CONF_FROM, CONF_ICON, CONF_ID, + CONF_IGNORE_OUT_OF_RANGE, + CONF_MAX_VALUE, + CONF_METHOD, + CONF_MIN_VALUE, + CONF_MQTT_ID, + CONF_MULTIPLE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -23,20 +30,21 @@ from esphome.const import ( CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_STATE_CLASS, + CONF_TIMEOUT, CONF_TO, CONF_TRIGGER_ID, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, - CONF_WINDOW_SIZE, - CONF_MQTT_ID, - CONF_FORCE_UPDATE, CONF_VALUE, + CONF_WEB_SERVER, + CONF_WINDOW_SIZE, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, + DEVICE_CLASS_CONDUCTIVITY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_DATA_RATE, DEVICE_CLASS_DATA_SIZE, @@ -57,6 +65,7 @@ from esphome.const import ( DEVICE_CLASS_NITROGEN_MONOXIDE, DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_OZONE, + DEVICE_CLASS_PH, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, @@ -76,10 +85,12 @@ from esphome.const import ( DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_WATER, DEVICE_CLASS_WEIGHT, DEVICE_CLASS_WIND_SPEED, + ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass @@ -94,6 +105,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, + DEVICE_CLASS_CONDUCTIVITY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_DATA_RATE, DEVICE_CLASS_DATA_SIZE, @@ -114,6 +126,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_NITROGEN_MONOXIDE, DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_OZONE, + DEVICE_CLASS_PH, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, @@ -133,6 +146,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_WATER, DEVICE_CLASS_WEIGHT, @@ -182,6 +196,15 @@ def validate_datapoint(value): return validate_datapoint({CONF_FROM: cv.float_(a), CONF_TO: cv.float_(b)}) +_SENSOR_ENTITY_CATEGORIES = { + k: v for k, v in cv.ENTITY_CATEGORIES.items() if k != ENTITY_CATEGORY_CONFIG +} + + +def sensor_entity_category(value): + return cv.enum(_SENSOR_ENTITY_CATEGORIES, lower=True)(value) + + # Base Sensor = sensor_ns.class_("Sensor", cg.EntityBase) SensorPtr = Sensor.operator("ptr") @@ -217,6 +240,7 @@ OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter) ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) +TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -224,48 +248,58 @@ OrFilter = sensor_ns.class_("OrFilter", Filter) CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter) CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter) SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) +ClampFilter = sensor_ns.class_("ClampFilter", Filter) +RoundFilter = sensor_ns.class_("RoundFilter", Filter) +RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) validate_unit_of_measurement = cv.string_strict validate_accuracy_decimals = cv.int_ validate_icon = cv.icon validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( - { - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent), - cv.GenerateID(): cv.declare_id(Sensor), - cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, - cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals, - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_STATE_CLASS): validate_state_class, - 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"), - cv.Any(None, cv.positive_time_period_milliseconds), - ), - cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorStateTrigger), - } - ), - cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorRawStateTrigger), - } - ), - cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), - cv.Optional(CONF_ABOVE): cv.templatable(cv.float_), - cv.Optional(CONF_BELOW): cv.templatable(cv.float_), - }, - cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), - ), - } +SENSOR_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent), + cv.GenerateID(): cv.declare_id(Sensor), + cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, + cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals, + 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"), + cv.Any(None, cv.positive_time_period_milliseconds), + ), + cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorStateTrigger), + } + ), + cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + SensorRawStateTrigger + ), + } + ), + cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), + cv.Optional(CONF_ABOVE): cv.templatable(cv.float_), + cv.Optional(CONF_BELOW): cv.templatable(cv.float_), + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), + ), + } + ) ) _UNDEF = object() @@ -293,7 +327,7 @@ def sensor_schema( (CONF_ACCURACY_DECIMALS, accuracy_decimals, validate_accuracy_decimals), (CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_STATE_CLASS, state_class, validate_state_class), - (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ENTITY_CATEGORY, entity_category, sensor_entity_category), ]: if default is not _UNDEF: schema[cv.Optional(key, default=default)] = validator @@ -536,6 +570,22 @@ async def heartbeat_filter_to_code(config, filter_id): return var +TIMEOUT_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, + cv.Optional(CONF_VALUE, default="nan"): cv.float_, + }, + key=CONF_TIMEOUT, +) + + +@FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) +async def timeout_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], config[CONF_VALUE]) + await cg.register_component(var, {}) + return var + + @FILTER_REGISTRY.register( "debounce", DebounceFilter, cv.positive_time_period_milliseconds ) @@ -545,30 +595,60 @@ async def debounce_filter_to_code(config, filter_id): return var -def validate_not_all_from_same(config): - if all(conf[CONF_FROM] == config[0][CONF_FROM] for conf in config): - raise cv.Invalid( - "The 'from' values of the calibrate_linear filter cannot all point " - "to the same value! Please add more values to the filter." - ) +CONF_DATAPOINTS = "datapoints" + + +def validate_calibrate_linear(config): + datapoints = config[CONF_DATAPOINTS] + if config[CONF_METHOD] == "exact": + for i in range(len(datapoints) - 1): + if datapoints[i][CONF_FROM] > datapoints[i + 1][CONF_FROM]: + raise cv.Invalid( + "The 'from' values of the calibrate_linear filter must be sorted in ascending order." + ) + for i in range(len(datapoints) - 1): + if datapoints[i][CONF_FROM] == datapoints[i + 1][CONF_FROM]: + raise cv.Invalid( + "The 'from' values of the calibrate_linear filter must not contain duplicates." + ) + elif config[CONF_METHOD] == "least_squares": + if all(conf[CONF_FROM] == datapoints[0][CONF_FROM] for conf in datapoints): + raise cv.Invalid( + "The 'from' values of the calibrate_linear filter cannot all point " + "to the same value! Please add more values to the filter." + ) return config @FILTER_REGISTRY.register( "calibrate_linear", CalibrateLinearFilter, - cv.All( - cv.ensure_list(validate_datapoint), cv.Length(min=2), validate_not_all_from_same + cv.maybe_simple_value( + { + cv.Required(CONF_DATAPOINTS): cv.All( + cv.ensure_list(validate_datapoint), cv.Length(min=2) + ), + cv.Optional(CONF_METHOD, default="least_squares"): cv.one_of( + "least_squares", "exact", lower=True + ), + }, + validate_calibrate_linear, + key=CONF_DATAPOINTS, ), ) async def calibrate_linear_filter_to_code(config, filter_id): - x = [conf[CONF_FROM] for conf in config] - y = [conf[CONF_TO] for conf in config] - k, b = fit_linear(x, y) - return cg.new_Pvariable(filter_id, k, b) + x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]] + y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]] + + linear_functions = [] + if config[CONF_METHOD] == "least_squares": + k, b = fit_linear(x, y) + linear_functions = [[k, b, float("NaN")]] + elif config[CONF_METHOD] == "exact": + linear_functions = map_linear(x, y) + return cg.new_Pvariable(filter_id, linear_functions) -CONF_DATAPOINTS = "datapoints" CONF_DEGREE = "degree" @@ -607,6 +687,72 @@ async def calibrate_polynomial_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, res) +def validate_clamp(config): + if not math.isfinite(config[CONF_MIN_VALUE]) and not math.isfinite( + config[CONF_MAX_VALUE] + ): + raise cv.Invalid("Either 'min_value' or 'max_value' must be set to a number.") + if config[CONF_MIN_VALUE] > config[CONF_MAX_VALUE]: + raise cv.Invalid("The 'min_value' must not be larger than the 'max_value'.") + return config + + +CLAMP_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_MIN_VALUE, default="NaN"): cv.float_, + cv.Optional(CONF_MAX_VALUE, default="NaN"): cv.float_, + cv.Optional(CONF_IGNORE_OUT_OF_RANGE, default=False): cv.boolean, + } + ), + validate_clamp, +) + + +@FILTER_REGISTRY.register("clamp", ClampFilter, CLAMP_SCHEMA) +async def clamp_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_MIN_VALUE], + config[CONF_MAX_VALUE], + config[CONF_IGNORE_OUT_OF_RANGE], + ) + + +@FILTER_REGISTRY.register( + "round", + RoundFilter, + cv.maybe_simple_value( + { + cv.Required(CONF_ACCURACY_DECIMALS): cv.uint8_t, + }, + key=CONF_ACCURACY_DECIMALS, + ), +) +async def round_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_ACCURACY_DECIMALS], + ) + + +@FILTER_REGISTRY.register( + "round_to_multiple_of", + RoundMultipleFilter, + cv.maybe_simple_value( + { + cv.Required(CONF_MULTIPLE): cv.positive_not_null_float, + }, + key=CONF_MULTIPLE, + ), +) +async def round_multiple_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_MULTIPLE], + ) + + async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) @@ -614,14 +760,14 @@ async def build_filters(config): async def setup_sensor_core_(var, config): await setup_entity(var, config) - if CONF_DEVICE_CLASS in config: - cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) - if CONF_STATE_CLASS in config: - cg.add(var.set_state_class(config[CONF_STATE_CLASS])) - if CONF_UNIT_OF_MEASUREMENT in config: - cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) - if CONF_ACCURACY_DECIMALS in config: - cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + if (state_class := config.get(CONF_STATE_CLASS)) is not None: + cg.add(var.set_state_class(state_class)) + if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: + cg.add(var.set_unit_of_measurement(unit_of_measurement)) + if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None: + cg.add(var.set_accuracy_decimals(accuracy_decimals)) cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) @@ -636,23 +782,26 @@ async def setup_sensor_core_(var, config): for conf in config.get(CONF_ON_VALUE_RANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) - if CONF_ABOVE in conf: - template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) + if (above := conf.get(CONF_ABOVE)) is not None: + template_ = await cg.templatable(above, [(float, "x")], float) cg.add(trigger.set_min(template_)) - if CONF_BELOW in conf: - template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) + if (below := conf.get(CONF_BELOW)) is not None: + template_ = await cg.templatable(below, [(float, "x")], float) cg.add(trigger.set_max(template_)) await automation.build_automation(trigger, [(float, "x")], conf) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) - if CONF_EXPIRE_AFTER in config: - if config[CONF_EXPIRE_AFTER] is None: + if (expire_after := config.get(CONF_EXPIRE_AFTER, _UNDEF)) is not _UNDEF: + if expire_after is None: cg.add(mqtt_.disable_expire_after()) else: - cg.add(mqtt_.set_expire_after(config[CONF_EXPIRE_AFTER])) + cg.add(mqtt_.set_expire_after(expire_after)) + + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) async def register_sensor(var, config): @@ -685,10 +834,10 @@ async def sensor_in_range_to_code(config, condition_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(condition_id, template_arg, paren) - if CONF_ABOVE in config: - cg.add(var.set_min(config[CONF_ABOVE])) - if CONF_BELOW in config: - cg.add(var.set_max(config[CONF_BELOW])) + if (above := config.get(CONF_ABOVE)) is not None: + cg.add(var.set_min(above)) + if (below := config.get(CONF_BELOW)) is not None: + cg.add(var.set_max(below)) return var @@ -718,6 +867,22 @@ def fit_linear(x, y): return k, b +def map_linear(x, y): + assert len(x) == len(y) + f = [] + for i in range(len(x) - 1): + slope = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) + bias = y[i] - (slope * x[i]) + next_x = x[i + 1] + if i == len(x) - 2: + next_x = float("NaN") + if f and f[-1][0] == slope and f[-1][1] == bias: + f[-1][2] = next_x + else: + f.append([slope, bias, next_x]) + return f + + def _mat_copy(m): return [list(row) for row in m] @@ -778,7 +943,7 @@ def _lstsq(a, b): return _mat_dot(_mat_dot(x, a_t), b) -@coroutine_with_priority(40.0) +@coroutine_with_priority(100.0) async def to_code(config): cg.add_define("USE_SENSOR") cg.add_global(sensor_ns.using) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 472649ebdc..bcf1fc8269 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,8 +1,8 @@ #include "filter.h" +#include #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "sensor.h" -#include namespace esphome { namespace sensor { @@ -79,7 +79,7 @@ SkipInitialFilter::SkipInitialFilter(size_t num_to_ignore) : num_to_ignore_(num_ optional SkipInitialFilter::new_value(float value) { if (num_to_ignore_ > 0) { num_to_ignore_--; - ESP_LOGV(TAG, "SkipInitialFilter(%p)::new_value(%f) SKIPPING, %u left", this, value, num_to_ignore_); + ESP_LOGV(TAG, "SkipInitialFilter(%p)::new_value(%f) SKIPPING, %zu left", this, value, num_to_ignore_); return {}; } @@ -252,7 +252,9 @@ ThrottleAverageFilter::ThrottleAverageFilter(uint32_t time_period) : time_period optional ThrottleAverageFilter::new_value(float value) { ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::new_value(value=%f)", this, value); - if (!std::isnan(value)) { + if (std::isnan(value)) { + this->have_nan_ = true; + } else { this->sum_ += value; this->n_++; } @@ -262,12 +264,14 @@ void ThrottleAverageFilter::setup() { this->set_interval("throttle_average", this->time_period_, [this]() { ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_); if (this->n_ == 0) { - this->output(NAN); + if (this->have_nan_) + this->output(NAN); } else { this->output(this->sum_ / this->n_); this->sum_ = 0.0f; this->n_ = 0; } + this->have_nan_ = false; }); } float ThrottleAverageFilter::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -355,11 +359,15 @@ OrFilter::OrFilter(std::vector filters) : filters_(std::move(filters)) OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} optional OrFilter::PhiNode::new_value(float value) { - this->or_parent_->output(value); + if (!this->or_parent_->has_value_) { + this->or_parent_->output(value); + this->or_parent_->has_value_ = true; + } return {}; } optional OrFilter::new_value(float value) { + this->has_value_ = false; for (Filter *filter : this->filters_) filter->input(value); @@ -373,6 +381,15 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { this->phi_.initialize(parent, nullptr); } +// TimeoutFilter +optional TimeoutFilter::new_value(float value) { + this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_); }); + return value; +} + +TimeoutFilter::TimeoutFilter(uint32_t time_period, float new_value) : time_period_(time_period), value_(new_value) {} +float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } + // DebounceFilter optional DebounceFilter::new_value(float value) { this->set_timeout("debounce", this->time_period_, [this, value]() { this->output(value); }); @@ -405,8 +422,13 @@ void HeartbeatFilter::setup() { } float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional CalibrateLinearFilter::new_value(float value) { return value * this->slope_ + this->bias_; } -CalibrateLinearFilter::CalibrateLinearFilter(float slope, float bias) : slope_(slope), bias_(bias) {} +optional CalibrateLinearFilter::new_value(float value) { + for (std::array f : this->linear_functions_) { + if (!std::isfinite(f[2]) || value < f[2]) + return (value * f[0]) + f[1]; + } + return NAN; +} optional CalibratePolynomialFilter::new_value(float value) { float res = 0.0f; @@ -418,5 +440,45 @@ optional CalibratePolynomialFilter::new_value(float value) { return res; } +ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range) + : min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {} +optional ClampFilter::new_value(float value) { + if (std::isfinite(value)) { + if (std::isfinite(this->min_) && value < this->min_) { + if (this->ignore_out_of_range_) { + return {}; + } else { + return this->min_; + } + } + + if (std::isfinite(this->max_) && value > this->max_) { + if (this->ignore_out_of_range_) { + return {}; + } else { + return this->max_; + } + } + } + return value; +} + +RoundFilter::RoundFilter(uint8_t precision) : precision_(precision) {} +optional RoundFilter::new_value(float value) { + if (std::isfinite(value)) { + float accuracy_mult = powf(10.0f, this->precision_); + return roundf(accuracy_mult * value) / accuracy_mult; + } + return value; +} + +RoundMultipleFilter::RoundMultipleFilter(float multiple) : multiple_(multiple) {} +optional RoundMultipleFilter::new_value(float value) { + if (std::isfinite(value)) { + return value - remainderf(value, this->multiple_); + } + return value; +} + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 05934a26e8..92b1d8d240 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -245,6 +245,7 @@ class ThrottleAverageFilter : public Filter, public Component { uint32_t time_period_; float sum_{0.0f}; unsigned int n_{0}; + bool have_nan_{false}; }; using lambda_filter_t = std::function(float)>; @@ -313,6 +314,20 @@ class ThrottleFilter : public Filter { uint32_t min_time_between_inputs_; }; +class TimeoutFilter : public Filter, public Component { + public: + explicit TimeoutFilter(uint32_t time_period, float new_value); + void set_value(float new_value) { this->value_ = new_value; } + + optional new_value(float value) override; + + float get_setup_priority() const override; + + protected: + uint32_t time_period_; + float value_; +}; + class DebounceFilter : public Filter, public Component { public: explicit DebounceFilter(uint32_t time_period); @@ -373,17 +388,18 @@ class OrFilter : public Filter { }; std::vector filters_; + bool has_value_{false}; PhiNode phi_; }; class CalibrateLinearFilter : public Filter { public: - CalibrateLinearFilter(float slope, float bias); + CalibrateLinearFilter(std::vector> linear_functions) + : linear_functions_(std::move(linear_functions)) {} optional new_value(float value) override; protected: - float slope_; - float bias_; + std::vector> linear_functions_; }; class CalibratePolynomialFilter : public Filter { @@ -395,5 +411,34 @@ class CalibratePolynomialFilter : public Filter { std::vector coefficients_; }; +class ClampFilter : public Filter { + public: + ClampFilter(float min, float max, bool ignore_out_of_range); + optional new_value(float value) override; + + protected: + float min_{NAN}; + float max_{NAN}; + bool ignore_out_of_range_; +}; + +class RoundFilter : public Filter { + public: + explicit RoundFilter(uint8_t precision); + optional new_value(float value) override; + + protected: + uint8_t precision_; +}; + +class RoundMultipleFilter : public Filter { + public: + explicit RoundMultipleFilter(float multiple); + optional new_value(float value) override; + + protected: + float multiple_; +}; + } // namespace sensor } // namespace esphome diff --git a/esphome/components/servo/servo.cpp b/esphome/components/servo/servo.cpp index 78fc45c679..18e8c8087e 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -1,6 +1,7 @@ #include "servo.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include namespace esphome { namespace servo { @@ -14,8 +15,24 @@ void Servo::dump_config() { ESP_LOGCONFIG(TAG, " Idle Level: %.1f%%", this->idle_level_ * 100.0f); ESP_LOGCONFIG(TAG, " Min Level: %.1f%%", this->min_level_ * 100.0f); ESP_LOGCONFIG(TAG, " Max Level: %.1f%%", this->max_level_ * 100.0f); - ESP_LOGCONFIG(TAG, " auto detach time: %d ms", this->auto_detach_time_); - ESP_LOGCONFIG(TAG, " run duration: %d ms", this->transition_length_); + ESP_LOGCONFIG(TAG, " auto detach time: %" PRIu32 " ms", this->auto_detach_time_); + ESP_LOGCONFIG(TAG, " run duration: %" PRIu32 " ms", this->transition_length_); +} + +void Servo::setup() { + float v; + if (this->restore_) { + this->rtc_ = global_preferences->make_preference(global_servo_id); + global_servo_id++; + if (this->rtc_.load(&v)) { + this->target_value_ = v; + this->internal_write(v); + this->state_ = STATE_ATTACHED; + this->start_millis_ = millis(); + return; + } + } + this->detach(); } void Servo::loop() { @@ -24,7 +41,6 @@ void Servo::loop() { if (millis() - this->start_millis_ > this->auto_detach_time_) { this->detach(); this->start_millis_ = 0; - this->state_ = STATE_DETACHED; ESP_LOGD(TAG, "Servo detached on auto_detach_time"); } } @@ -53,8 +69,11 @@ void Servo::loop() { void Servo::write(float value) { value = clamp(value, -1.0f, 1.0f); - if (this->target_value_ == value) + if ((this->state_ == STATE_DETACHED) && (this->target_value_ == value)) { this->internal_write(value); + } else { + this->save_level_(value); + } this->target_value_ = value; this->source_value_ = this->current_value_; this->state_ = STATE_ATTACHED; @@ -71,11 +90,18 @@ void Servo::internal_write(float value) { level = lerp(value, this->idle_level_, this->max_level_); } this->output_->set_level(level); - if (this->target_value_ == this->current_value_) { - this->save_level_(level); - } this->current_value_ = value; } +void Servo::detach() { + this->state_ = STATE_DETACHED; + this->output_->set_level(0.0f); +} + +void Servo::save_level_(float v) { + if (this->restore_) + this->rtc_.save(&v); +} + } // namespace servo } // namespace esphome diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index e2e3823158..13a7472ae5 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -17,22 +17,8 @@ class Servo : public Component { void loop() override; void write(float value); void internal_write(float value); - void detach() { - this->output_->set_level(0.0f); - this->save_level_(0.0f); - } - void setup() override { - float v; - if (this->restore_) { - this->rtc_ = global_preferences->make_preference(global_servo_id); - global_servo_id++; - if (this->rtc_.load(&v)) { - this->output_->set_level(v); - return; - } - } - this->detach(); - } + void detach(); + void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } void set_min_level(float min_level) { min_level_ = min_level; } @@ -42,8 +28,10 @@ class Servo : public Component { void set_auto_detach_time(uint32_t auto_detach_time) { auto_detach_time_ = auto_detach_time; } void set_transition_length(uint32_t transition_length) { transition_length_ = transition_length; } + bool has_reached_target() { return this->current_value_ == this->target_value_; } + protected: - void save_level_(float v) { this->rtc_.save(&v); } + void save_level_(float v); output::FloatOutput *output_; float min_level_ = 0.0300f; diff --git a/esphome/components/sfa30/__init__.py b/esphome/components/sfa30/__init__.py new file mode 100644 index 0000000000..28b665906f --- /dev/null +++ b/esphome/components/sfa30/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ghsensdev"] diff --git a/esphome/components/sfa30/sensor.py b/esphome/components/sfa30/sensor.py new file mode 100644 index 0000000000..428f6b874b --- /dev/null +++ b/esphome/components/sfa30/sensor.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor, sensirion_common + +from esphome.const import ( + CONF_ID, + CONF_FORMALDEHYDE, + CONF_HUMIDITY, + CONF_TEMPERATURE, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + ICON_RADIATOR, + ICON_WATER_PERCENT, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_BILLION, + UNIT_PERCENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@ghsensdev"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] + +sfa30_ns = cg.esphome_ns.namespace("sfa30") + +SFA30Component = sfa30_ns.class_( + "SFA30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SFA30Component), + cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=1, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5D)) +) + +SENSOR_MAP = { + CONF_FORMALDEHYDE: "set_formaldehyde_sensor", + CONF_HUMIDITY: "set_humidity_sensor", + CONF_TEMPERATURE: "set_temperature_sensor", +} + + +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) + + for key, funcName in SENSOR_MAP.items(): + if sensor_config := config.get(key): + sens = await sensor.new_sensor(sensor_config) + cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp new file mode 100644 index 0000000000..20d5ddad5e --- /dev/null +++ b/esphome/components/sfa30/sfa30.cpp @@ -0,0 +1,99 @@ +#include "sfa30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sfa30 { + +static const char *const TAG = "sfa30"; + +static const uint16_t SFA30_CMD_GET_DEVICE_MARKING = 0xD060; +static const uint16_t SFA30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0006; +static const uint16_t SFA30_CMD_READ_MEASUREMENT = 0x0327; + +void SFA30Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up sfa30..."); + + // Serial Number identification + uint16_t raw_device_marking[16]; + if (!this->get_register(SFA30_CMD_GET_DEVICE_MARKING, raw_device_marking, 16, 5)) { + ESP_LOGE(TAG, "Failed to read device marking"); + this->error_code_ = DEVICE_MARKING_READ_FAILED; + this->mark_failed(); + return; + } + + for (size_t i = 0; i < 16; i++) { + this->device_marking_[i * 2] = static_cast(raw_device_marking[i] >> 8); + this->device_marking_[i * 2 + 1] = static_cast(raw_device_marking[i] & 0xFF); + } + ESP_LOGD(TAG, "Device Marking: '%s'", this->device_marking_); + + if (!this->write_command(SFA30_CMD_START_CONTINUOUS_MEASUREMENTS)) { + ESP_LOGE(TAG, "Error starting measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + + ESP_LOGD(TAG, "Sensor initialized"); +} + +void SFA30Component::dump_config() { + ESP_LOGCONFIG(TAG, "sfa30:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case DEVICE_MARKING_READ_FAILED: + ESP_LOGW(TAG, "Unable to read device marking!"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement initialization failed!"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Device Marking: '%s'", this->device_marking_); + LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); +} + +void SFA30Component::update() { + if (!this->write_command(SFA30_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement!"); + this->status_set_warning(); + return; + } + + this->set_timeout(5, [this]() { + uint16_t raw_data[3]; + if (!this->read_data(raw_data, 3)) { + ESP_LOGW(TAG, "Error reading measurement data!"); + this->status_set_warning(); + return; + } + + if (this->formaldehyde_sensor_ != nullptr) { + const float formaldehyde = raw_data[0] / 5.0f; + this->formaldehyde_sensor_->publish_state(formaldehyde); + } + + if (this->humidity_sensor_ != nullptr) { + const float humidity = raw_data[1] / 100.0f; + this->humidity_sensor_->publish_state(humidity); + } + + if (this->temperature_sensor_ != nullptr) { + const float temperature = raw_data[2] / 200.0f; + this->temperature_sensor_->publish_state(temperature); + } + + this->status_clear_warning(); + }); +} + +} // namespace sfa30 +} // namespace esphome diff --git a/esphome/components/sfa30/sfa30.h b/esphome/components/sfa30/sfa30.h new file mode 100644 index 0000000000..fa2c59f624 --- /dev/null +++ b/esphome/components/sfa30/sfa30.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" + +namespace esphome { +namespace sfa30 { + +class SFA30Component : public PollingComponent, public sensirion_common::SensirionI2CDevice { + enum ErrorCode { DEVICE_MARKING_READ_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; + + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + + void set_formaldehyde_sensor(sensor::Sensor *formaldehyde) { this->formaldehyde_sensor_ = formaldehyde; } + void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } + void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } + + protected: + char device_marking_[32] = {0}; + + ErrorCode error_code_{UNKNOWN}; + + sensor::Sensor *formaldehyde_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; +}; + +} // namespace sfa30 +} // namespace esphome diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 0029e2c515..13e859cc09 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( + CONF_COMPENSATION, CONF_ID, CONF_BASELINE, CONF_ECO2, @@ -11,7 +12,7 @@ from esphome.const import ( CONF_TVOC, ICON_RADIATOR, DEVICE_CLASS_CARBON_DIOXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, @@ -30,7 +31,6 @@ SGP30Component = sgp30_ns.class_( CONF_ECO2_BASELINE = "eco2_baseline" CONF_TVOC_BASELINE = "tvoc_baseline" CONF_UPTIME = "uptime" -CONF_COMPENSATION = "compensation" CONF_HUMIDITY_SOURCE = "humidity_source" @@ -49,7 +49,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index a6572620c4..261604b992 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -73,9 +73,10 @@ void SGP30Component::setup() { return; } - // Hash with compilation time + // Hash with compilation time and serial number // This ensures the baseline storage is cleared after OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); + // 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_)); this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->baselines_storage_)) { @@ -181,9 +182,18 @@ void SGP30Component::send_env_data_() { ESP_LOGD(TAG, "External compensation data received: Temperature %0.2f°C", temperature); } - float absolute_humidity = - 216.7f * (((humidity / 100) * 6.112f * std::exp((17.62f * temperature) / (243.12f + temperature))) / - (273.15f + temperature)); + float absolute_humidity; + if (temperature < 0) { + absolute_humidity = + 216.67f * + ((humidity * 0.061121f * std::exp((23.036f - temperature / 333.7f) * (temperature / (279.82f + temperature)))) / + (273.15f + temperature)); + } else { + absolute_humidity = + 216.67f * + ((humidity * 0.061121f * std::exp((18.678f - temperature / 234.5f) * (temperature / (257.14f + temperature)))) / + (273.15f + temperature)); + } uint8_t humidity_full = uint8_t(std::floor(absolute_humidity)); uint8_t humidity_dec = uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)); ESP_LOGD(TAG, "Calculated Absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, @@ -246,7 +256,7 @@ void SGP30Component::dump_config() { } else { ESP_LOGCONFIG(TAG, " Baseline: No baseline configured"); } - ESP_LOGCONFIG(TAG, " Warm up time: %us", this->required_warm_up_time_); + ESP_LOGCONFIG(TAG, " Warm up time: %" PRIu32 "s", this->required_warm_up_time_); } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "eCO2 sensor", this->eco2_sensor_); diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index d61eee00db..9e882e6b05 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" #include "esphome/core/preferences.h" + +#include #include namespace esphome { diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index cb4231c168..ad9de6fe24 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -2,7 +2,7 @@ import esphome.config_validation as cv CODEOWNERS = ["@SenexCrenshaw"] -CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( +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" ) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 3d24f6c409..b7cec542bf 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( + CONF_COMPENSATION, CONF_ID, CONF_STORE_BASELINE, CONF_TEMPERATURE_SOURCE, @@ -23,7 +24,6 @@ SGP4xComponent = sgp4x_ns.class_( ) CONF_ALGORITHM_TUNING = "algorithm_tuning" -CONF_COMPENSATION = "compensation" CONF_GAIN_FACTOR = "gain_factor" CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" CONF_HUMIDITY_SOURCE = "humidity_source" diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 52f9adc808..bf91c90832 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -61,23 +61,24 @@ void SGP4xComponent::setup() { ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); if (this->store_baseline_) { - // Hash with compilation time + // Hash with compilation time and serial number // This ensures the baseline storage is cleared after OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); + // 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_)); this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->voc_baselines_storage_)) { this->voc_state0_ = this->voc_baselines_storage_.state0; this->voc_state1_ = this->voc_baselines_storage_.state1; - ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0, - voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); } // Initialize storage timestamp this->seconds_since_last_store_ = 0; if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { - ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", + ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); } @@ -110,7 +111,7 @@ void SGP4xComponent::setup() { number of records reported from being overwhelming. */ ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler"); - this->set_interval(1000, [this]() { this->update_gas_indices(); }); + this->set_interval(1000, [this]() { this->take_sample(); }); } void SGP4xComponent::self_test_() { @@ -145,31 +146,15 @@ void SGP4xComponent::self_test_() { }); } -/** - * @brief Combined the measured gasses, temperature, and humidity - * to calculate the VOC Index - * - * @param temperature The measured temperature in degrees C - * @param humidity The measured relative humidity in % rH - * @return int32_t The VOC Index - */ -bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) { - uint16_t voc_sraw; - uint16_t nox_sraw; - if (!measure_raw_(voc_sraw, nox_sraw)) - return false; - - this->status_clear_warning(); - - voc = voc_algorithm_.process(voc_sraw); - if (nox_sensor_) { - nox = nox_algorithm_.process(nox_sraw); - } - ESP_LOGV(TAG, "VOC = %d, NOx = %d", voc, nox); +void SGP4xComponent::update_gas_indices_() { + this->voc_index_ = this->voc_algorithm_.process(this->voc_sraw_); + if (this->nox_sensor_ != nullptr) + this->nox_index_ = this->nox_algorithm_.process(this->nox_sraw_); + ESP_LOGV(TAG, "VOC = %" PRId32 ", NOx = %" PRId32, this->voc_index_, this->nox_index_); // Store baselines after defined interval or if the difference between current and stored baseline becomes too // much if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { - voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_); + this->voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_); if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF || std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) { this->seconds_since_last_store_ = 0; @@ -177,30 +162,28 @@ bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) { this->voc_baselines_storage_.state1 = this->voc_state1_; if (this->pref_.save(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0, - voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); } else { ESP_LOGW(TAG, "Could not store VOC baselines"); } } } - return true; + if (this->samples_read_ < this->samples_to_stabilize_) { + this->samples_read_++; + ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %" PRIu32, this->samples_read_, + this->samples_to_stabilize_, this->voc_index_); + } } -/** - * @brief Return the raw gas measurement - * - * @param temperature The measured temperature in degrees C - * @param humidity The measured relative humidity in % rH - * @return uint16_t The current raw gas measurement - */ -bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) { + +void SGP4xComponent::measure_raw_() { float humidity = NAN; static uint32_t nox_conditioning_start = millis(); if (!this->self_test_complete_) { ESP_LOGD(TAG, "Self-test not yet complete"); - return false; + return; } if (this->humidity_sensor_ != nullptr) { humidity = this->humidity_sensor_->state; @@ -242,61 +225,45 @@ bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) { data[1] = tempticks; if (!this->write_command(command, data, 2)) { - this->status_set_warning(); ESP_LOGD(TAG, "write error (%d)", this->last_error_); - return false; + this->status_set_warning("measurement request failed"); + return; } - delay(measure_time_); - uint16_t raw_data[2]; - raw_data[1] = 0; - if (!this->read_data(raw_data, response_words)) { - this->status_set_warning(); - ESP_LOGD(TAG, "read error (%d)", this->last_error_); - return false; - } - voc_raw = raw_data[0]; - nox_raw = raw_data[1]; // either 0 or the measured NOx ticks - return true; + + this->set_timeout(this->measure_time_, [this, response_words]() { + uint16_t raw_data[2]; + raw_data[1] = 0; + if (!this->read_data(raw_data, response_words)) { + ESP_LOGD(TAG, "read error (%d)", this->last_error_); + this->status_set_warning("measurement read failed"); + this->voc_index_ = this->nox_index_ = UINT16_MAX; + return; + } + this->voc_sraw_ = raw_data[0]; + this->nox_sraw_ = raw_data[1]; // either 0 or the measured NOx ticks + this->status_clear_warning(); + this->update_gas_indices_(); + }); } -void SGP4xComponent::update_gas_indices() { +void SGP4xComponent::take_sample() { if (!this->self_test_complete_) return; - this->seconds_since_last_store_ += 1; - if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) { - // Set values to UINT16_MAX to indicate failure - this->voc_index_ = this->nox_index_ = UINT16_MAX; - ESP_LOGE(TAG, "measure gas indices failed"); - return; - } - if (this->samples_read_ < this->samples_to_stabilize_) { - this->samples_read_++; - ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_, - this->samples_to_stabilize_, this->voc_index_); - return; - } + this->measure_raw_(); } void SGP4xComponent::update() { if (this->samples_read_ < this->samples_to_stabilize_) { return; } - if (this->voc_sensor_) { - if (this->voc_index_ != UINT16_MAX) { - this->status_clear_warning(); + if (this->voc_sensor_ != nullptr) { + if (this->voc_index_ != UINT16_MAX) this->voc_sensor_->publish_state(this->voc_index_); - } else { - this->status_set_warning(); - } } - if (this->nox_sensor_) { - if (this->nox_index_ != UINT16_MAX) { - this->status_clear_warning(); + if (this->nox_sensor_ != nullptr) { + if (this->nox_index_ != UINT16_MAX) this->nox_sensor_->publish_state(this->nox_index_); - } else { - this->status_set_warning(); - } } } @@ -328,7 +295,7 @@ void SGP4xComponent::dump_config() { } LOG_UPDATE_INTERVAL(this); - if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { + if (this->humidity_sensor_ != nullptr || this->temperature_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, " Compensation:"); LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 3a8d8200a7..959ff12c27 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" @@ -8,8 +11,6 @@ #include #include -#include - namespace esphome { namespace sgp4x { @@ -72,7 +73,7 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se void setup() override; void update() override; - void update_gas_indices(); + void take_sample(); void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } @@ -107,8 +108,10 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se sensor::Sensor *temperature_sensor_{nullptr}; int16_t sensirion_init_sensors_(); - bool measure_gas_indices_(int32_t &voc, int32_t &nox); - bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw); + void update_gas_indices_(); + void measure_raw_(); + uint16_t voc_sraw_; + uint16_t nox_sraw_; SgpType sgp_type_{SGP40}; uint64_t serial_number_; diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index c49193d135..625784427f 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -29,7 +29,8 @@ from esphome.const import ( from esphome.core import HexInt, CORE DOMAIN = "shelly_dimmer" -DEPENDENCIES = ["sensor", "uart", "esp8266"] +AUTO_LOAD = ["sensor"] +DEPENDENCIES = ["uart", "esp8266"] shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer") ShellyDimmer = shelly_dimmer_ns.class_( @@ -57,6 +58,10 @@ KNOWN_FIRMWARE = { "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.6/shelly-dimmer-stm32_v51.6.bin", "eda483e111c914723a33f5088f1397d5c0b19333db4a88dc965636b976c16c36", ), + "51.7": ( + "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.7/shelly-dimmer-stm32_v51.7.bin", + "7a20f1c967c469917368a79bc56498009045237080408cef7190743e08031889", + ), } @@ -87,12 +92,7 @@ def get_firmware(value): url = value[CONF_URL] if CONF_SHA256 in value: # we have a hash, enable caching - path = ( - Path(CORE.config_dir) - / ".esphome" - / DOMAIN - / (value[CONF_SHA256] + "_fw_stm.bin") - ) + path = Path(CORE.data_dir) / DOMAIN / (value[CONF_SHA256] + "_fw_stm.bin") if not path.is_file(): firmware_data, dl_hash = dl(url) diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 144236bfe1..b415840bdc 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -64,46 +64,46 @@ uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) { return std::accumulate(buf, buf + len, 0); } +bool ShellyDimmer::is_running_configured_version() const { + return this->version_major_ == USE_SHD_FIRMWARE_MAJOR_VERSION && + this->version_minor_ == USE_SHD_FIRMWARE_MINOR_VERSION; +} + +void ShellyDimmer::handle_firmware() { + // Reset the STM32 and check the firmware version. + this->reset_normal_boot_(); + this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0); + ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_, + this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION); + + if (!is_running_configured_version()) { +#ifdef USE_SHD_FIRMWARE_DATA + if (!this->upgrade_firmware_()) { + ESP_LOGW(TAG, "Failed to upgrade firmware"); + this->mark_failed(); + return; + } + + this->reset_normal_boot_(); + this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0); + if (!is_running_configured_version()) { + ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect"); + this->mark_failed(); + return; + } +#else + ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update."); +#endif + } +} + void ShellyDimmer::setup() { this->pin_nrst_->setup(); this->pin_boot0_->setup(); ESP_LOGI(TAG, "Initializing Shelly Dimmer..."); - // Reset the STM32 and check the firmware version. - for (int i = 0; i < 2; i++) { - this->reset_normal_boot_(); - this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0); - ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_, - this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION); - if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION || - this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) { -#ifdef USE_SHD_FIRMWARE_DATA - // Update firmware if needed. - ESP_LOGW(TAG, "Unsupported STM32 firmware version, flashing"); - if (i > 0) { - // Upgrade was already performed but the reported version is still not right. - ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect"); - this->mark_failed(); - return; - } - - if (!this->upgrade_firmware_()) { - ESP_LOGW(TAG, "Failed to upgrade firmware"); - this->mark_failed(); - return; - } - - // Firmware upgrade completed, do the checks again. - continue; -#else - ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update."); - this->mark_failed(); - return; -#endif - } - break; - } + this->handle_firmware(); this->send_settings_(); // Do an immediate poll to refresh current state. diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.h b/esphome/components/shelly_dimmer/shelly_dimmer.h index 4701f3a32a..fd75caa797 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.h +++ b/esphome/components/shelly_dimmer/shelly_dimmer.h @@ -20,6 +20,8 @@ class ShellyDimmer : public PollingComponent, public light::LightOutput, public public: float get_setup_priority() const override { return setup_priority::LATE; } + bool is_running_configured_version() const; + void handle_firmware(); void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/sht3xd/sensor.py b/esphome/components/sht3xd/sensor.py index 8e1ef426ad..1286489b29 100644 --- a/esphome/components/sht3xd/sensor.py +++ b/esphome/components/sht3xd/sensor.py @@ -12,6 +12,10 @@ from esphome.const import ( UNIT_PERCENT, ) +CONF_HEATER_ENABLED = "heater_enabled" + +CODEOWNERS = ["@mrtoy-me"] + DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensirion_common"] @@ -24,19 +28,20 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(SHT3XDComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + 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.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - } + cv.Optional(CONF_HEATER_ENABLED, default=False): cv.boolean, + }, ) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x44)) @@ -48,6 +53,8 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + cg.add(var.set_heater_enabled(config[CONF_HEATER_ENABLED])) + if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index 4e1c9742bc..ffaf5db322 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -6,7 +6,16 @@ namespace sht3xd { static const char *const TAG = "sht3xd"; -static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER = 0x3780; +// https://sensirion.com/media/documents/E5762713/63D103C2/Sensirion_electronic_identification_code_SHT3x.pdf +// indicates two possible read serial number registers either with clock stretching enabled or disabled. +// Other SHT3XD_COMMAND registers use the clock stretching disabled register. +// To ensure compatibility, reading serial number using the register with clock stretching register enabled +// (used originally in this component) is tried first and if that fails the alternate register address +// with clock stretching disabled is read. + +static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING = 0x3780; +static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER = 0x3682; + static const uint16_t SHT3XD_COMMAND_READ_STATUS = 0xF32D; static const uint16_t SHT3XD_COMMAND_CLEAR_STATUS = 0x3041; static const uint16_t SHT3XD_COMMAND_HEATER_ENABLE = 0x306D; @@ -18,25 +27,53 @@ static const uint16_t SHT3XD_COMMAND_FETCH_DATA = 0xE000; void SHT3XDComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SHT3xD..."); uint16_t raw_serial_number[2]; - if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2)) { + if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING, raw_serial_number, 2)) { + this->error_code_ = READ_SERIAL_STRETCHED_FAILED; + if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2)) { + this->error_code_ = READ_SERIAL_FAILED; + this->mark_failed(); + return; + } + } + + this->serial_number_ = (uint32_t(raw_serial_number[0]) << 16) | uint32_t(raw_serial_number[1]); + + if (!this->write_command(heater_enabled_ ? SHT3XD_COMMAND_HEATER_ENABLE : SHT3XD_COMMAND_HEATER_DISABLE)) { + this->error_code_ = WRITE_HEATER_MODE_FAILED; this->mark_failed(); return; } - uint32_t serial_number = (uint32_t(raw_serial_number[0]) << 16) | uint32_t(raw_serial_number[1]); - ESP_LOGV(TAG, " Serial Number: 0x%08X", serial_number); } + void SHT3XDComponent::dump_config() { ESP_LOGCONFIG(TAG, "SHT3xD:"); - LOG_I2C_DEVICE(this); - if (this->is_failed()) { - ESP_LOGE(TAG, "Communication with SHT3xD failed!"); + switch (this->error_code_) { + case READ_SERIAL_FAILED: + ESP_LOGD(TAG, " Error reading serial number"); + break; + case WRITE_HEATER_MODE_FAILED: + ESP_LOGD(TAG, " Error writing heater mode"); + break; + default: + break; } + if (this->is_failed()) { + ESP_LOGE(TAG, " Communication with SHT3xD failed!"); + return; + } + ESP_LOGD(TAG, " Setup successful"); + ESP_LOGD(TAG, " Serial Number: 0x%08" PRIX32, this->serial_number_); + ESP_LOGD(TAG, " Heater Enabled: %s", this->heater_enabled_ ? "true" : "false"); + + LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } + float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; } + void SHT3XDComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 41ca3c5d6e..74f155121b 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" +#include + namespace esphome { namespace sht3xd { @@ -17,10 +19,20 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir void dump_config() override; float get_setup_priority() const override; void update() override; + void set_heater_enabled(bool heater_enabled) { heater_enabled_ = heater_enabled; } protected: + enum ErrorCode { + NONE = 0, + READ_SERIAL_STRETCHED_FAILED, + READ_SERIAL_FAILED, + WRITE_HEATER_MODE_FAILED, + } error_code_{NONE}; + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; + bool heater_enabled_{true}; + uint32_t serial_number_{0}; }; } // namespace sht3xd diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 0f9123434d..dea542ea9e 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -20,7 +20,7 @@ void SHT4XComponent::setup() { if (this->duty_cycle_ > 0.0) { uint32_t heater_interval = (uint32_t) (this->heater_time_ / this->duty_cycle_); - ESP_LOGD(TAG, "Heater interval: %i", heater_interval); + ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index 01553d5c15..46037bb0e8 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" +#include + namespace esphome { namespace sht4x { diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.cpp b/esphome/components/sigma_delta_output/sigma_delta_output.cpp new file mode 100644 index 0000000000..d386f8db1a --- /dev/null +++ b/esphome/components/sigma_delta_output/sigma_delta_output.cpp @@ -0,0 +1,57 @@ +#include "sigma_delta_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sigma_delta_output { + +static const char *const TAG = "output.sigma_delta"; + +void SigmaDeltaOutput::setup() { + if (this->pin_) + this->pin_->setup(); +} + +void SigmaDeltaOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Sigma Delta Output:"); + LOG_PIN(" Pin: ", this->pin_); + if (this->state_change_trigger_) { + ESP_LOGCONFIG(TAG, " State change automation configured"); + } + if (this->turn_on_trigger_) { + ESP_LOGCONFIG(TAG, " Turn on automation configured"); + } + if (this->turn_off_trigger_) { + ESP_LOGCONFIG(TAG, " Turn off automation configured"); + } + LOG_UPDATE_INTERVAL(this); + LOG_FLOAT_OUTPUT(this); +} + +void SigmaDeltaOutput::update() { + this->accum_ += this->state_; + const bool next_value = this->accum_ > 0; + + if (next_value) { + this->accum_ -= 1.; + } + + if (next_value != this->value_) { + this->value_ = next_value; + if (this->pin_) { + this->pin_->digital_write(next_value); + } + + if (this->state_change_trigger_) { + this->state_change_trigger_->trigger(next_value); + } + + if (next_value && this->turn_on_trigger_) { + this->turn_on_trigger_->trigger(); + } else if (!next_value && this->turn_off_trigger_) { + this->turn_off_trigger_->trigger(); + } + } +} + +} // namespace sigma_delta_output +} // namespace esphome diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.h b/esphome/components/sigma_delta_output/sigma_delta_output.h index 5a5acd2dfb..8fd1e1f761 100644 --- a/esphome/components/sigma_delta_output/sigma_delta_output.h +++ b/esphome/components/sigma_delta_output/sigma_delta_output.h @@ -1,9 +1,12 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" namespace esphome { namespace sigma_delta_output { + class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { public: Trigger<> *get_turn_on_trigger() { @@ -25,31 +28,9 @@ class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { void set_pin(GPIOPin *pin) { this->pin_ = pin; }; void write_state(float state) override { this->state_ = state; } - void update() override { - this->accum_ += this->state_; - const bool next_value = this->accum_ > 0; - - if (next_value) { - this->accum_ -= 1.; - } - - if (next_value != this->value_) { - this->value_ = next_value; - if (this->pin_) { - this->pin_->digital_write(next_value); - } - - if (this->state_change_trigger_) { - this->state_change_trigger_->trigger(next_value); - } - - if (next_value && this->turn_on_trigger_) { - this->turn_on_trigger_->trigger(); - } else if (!next_value && this->turn_off_trigger_) { - this->turn_off_trigger_->trigger(); - } - } - } + void setup() override; + void dump_config() override; + void update() override; protected: GPIOPin *pin_{nullptr}; diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 698e3cda9e..faa6cefe27 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import ( CONF_ID, + CONF_MESSAGE, CONF_TRIGGER_ID, ) from esphome.components import uart @@ -52,7 +53,6 @@ CONF_ON_INCOMING_CALL = "on_incoming_call" CONF_ON_CALL_CONNECTED = "on_call_connected" CONF_ON_CALL_DISCONNECTED = "on_call_disconnected" CONF_RECIPIENT = "recipient" -CONF_MESSAGE = "message" CONF_USSD = "ussd" CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/sm10bit_base/sm10bit_base.cpp b/esphome/components/sm10bit_base/sm10bit_base.cpp index 9c7abb48e2..d380f31c6f 100644 --- a/esphome/components/sm10bit_base/sm10bit_base.cpp +++ b/esphome/components/sm10bit_base/sm10bit_base.cpp @@ -11,6 +11,8 @@ static const uint8_t SM10BIT_ADDR_START_3CH = 0x8; static const uint8_t SM10BIT_ADDR_START_2CH = 0x10; static const uint8_t SM10BIT_ADDR_START_5CH = 0x18; +static const uint8_t SM10BIT_DELAY = 2; + // Power current values // HEX | Binary | RGB level | White level | Config value // 0x0 | 0000 | RGB 10mA | CW 5mA | 0 @@ -37,10 +39,13 @@ void Sm10BitBase::loop() { uint8_t data[12]; if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { - // Off / Sleep - data[0] = this->model_id_ + SM10BIT_ADDR_STANDBY; for (int i = 1; i < 12; i++) data[i] = 0; + // First turn all channels off + data[0] = this->model_id_ + SM10BIT_ADDR_START_5CH; + this->write_buffer_(data, 12); + // Then sleep + data[0] = this->model_id_ + SM10BIT_ADDR_STANDBY; this->write_buffer_(data, 12); } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { @@ -84,28 +89,42 @@ void Sm10BitBase::set_channel_value_(uint8_t channel, uint16_t value) { this->pwm_amounts_[channel] = value; } void Sm10BitBase::write_bit_(bool value) { - this->clock_pin_->digital_write(false); this->data_pin_->digital_write(value); + delayMicroseconds(SM10BIT_DELAY); this->clock_pin_->digital_write(true); + delayMicroseconds(SM10BIT_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(SM10BIT_DELAY); } void Sm10BitBase::write_byte_(uint8_t data) { for (uint8_t mask = 0x80; mask; mask >>= 1) { this->write_bit_(data & mask); } - this->clock_pin_->digital_write(false); - this->data_pin_->digital_write(true); + + // ack bit + this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->clock_pin_->digital_write(true); + delayMicroseconds(SM10BIT_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(SM10BIT_DELAY); + this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); } void Sm10BitBase::write_buffer_(uint8_t *buffer, uint8_t size) { this->data_pin_->digital_write(false); + delayMicroseconds(SM10BIT_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(SM10BIT_DELAY); + for (uint32_t i = 0; i < size; i++) { this->write_byte_(buffer[i]); } - this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + delayMicroseconds(SM10BIT_DELAY); this->data_pin_->digital_write(true); + delayMicroseconds(SM10BIT_DELAY); } } // namespace sm10bit_base diff --git a/esphome/components/sm2135/__init__.py b/esphome/components/sm2135/__init__.py index ce78d5337f..52128f1f24 100644 --- a/esphome/components/sm2135/__init__.py +++ b/esphome/components/sm2135/__init__.py @@ -15,6 +15,7 @@ SM2135 = sm2135_ns.class_("SM2135", cg.Component) CONF_RGB_CURRENT = "rgb_current" CONF_CW_CURRENT = "cw_current" +CONF_SEPARATE_MODES = "separate_modes" SM2135Current = sm2135_ns.enum("SM2135Current") @@ -51,6 +52,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RGB_CURRENT, "20mA"): cv.enum(DRIVE_STRENGTHS_RGB), cv.Optional(CONF_CW_CURRENT, "10mA"): cv.enum(DRIVE_STRENGTHS_CW), + cv.Optional(CONF_SEPARATE_MODES, default=True): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -66,3 +68,4 @@ async def to_code(config): cg.add(var.set_rgb_current(config[CONF_RGB_CURRENT])) cg.add(var.set_cw_current(config[CONF_CW_CURRENT])) + cg.add(var.set_separate_modes(config[CONF_SEPARATE_MODES])) diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp index f9cd4235ed..ee5948bb3a 100644 --- a/esphome/components/sm2135/sm2135.cpp +++ b/esphome/components/sm2135/sm2135.cpp @@ -97,23 +97,32 @@ void SM2135::loop() { this->write_byte_(SM2135_ADDR_MC); this->write_byte_(current_mask_); - if (this->update_channel_ == 3 || this->update_channel_ == 4) { - // No color so must be Cold/Warm + if (this->separate_modes_) { + if (this->update_channel_ == 3 || this->update_channel_ == 4) { + // No color so must be Cold/Warm - this->write_byte_(SM2135_CW); - this->sm2135_stop_(); - delay(1); - this->sm2135_start_(); - this->write_byte_(SM2135_ADDR_C); - this->write_byte_(this->pwm_amounts_[4]); // Warm - this->write_byte_(this->pwm_amounts_[3]); // Cold + this->write_byte_(SM2135_CW); + this->sm2135_stop_(); + delay(1); + this->sm2135_start_(); + this->write_byte_(SM2135_ADDR_C); + this->write_byte_(this->pwm_amounts_[3]); + this->write_byte_(this->pwm_amounts_[4]); + } else { + // Color + + this->write_byte_(SM2135_RGB); + this->write_byte_(this->pwm_amounts_[0]); + this->write_byte_(this->pwm_amounts_[1]); + this->write_byte_(this->pwm_amounts_[2]); + } } else { - // Color - this->write_byte_(SM2135_RGB); - this->write_byte_(this->pwm_amounts_[1]); // Green - this->write_byte_(this->pwm_amounts_[0]); // Red - this->write_byte_(this->pwm_amounts_[2]); // Blue + this->write_byte_(this->pwm_amounts_[0]); + this->write_byte_(this->pwm_amounts_[1]); + this->write_byte_(this->pwm_amounts_[2]); + this->write_byte_(this->pwm_amounts_[3]); + this->write_byte_(this->pwm_amounts_[4]); } this->sm2135_stop_(); diff --git a/esphome/components/sm2135/sm2135.h b/esphome/components/sm2135/sm2135.h index a557fc3287..6f207d093a 100644 --- a/esphome/components/sm2135/sm2135.h +++ b/esphome/components/sm2135/sm2135.h @@ -39,6 +39,8 @@ class SM2135 : public Component { this->current_mask_ = (this->rgb_current_ << 4) | this->cw_current_; } + void set_separate_modes(bool separate_modes) { this->separate_modes_ = separate_modes; } + void setup() override; void dump_config() override; @@ -78,6 +80,7 @@ class SM2135 : public Component { uint8_t current_mask_; SM2135Current rgb_current_; SM2135Current cw_current_; + bool separate_modes_; uint8_t update_channel_; std::vector pwm_amounts_; bool update_{true}; diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index f3b6dd95ef..8bcfb69a45 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -1,9 +1,10 @@ import re +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_TRIGGER_ID CODEOWNERS = ["@alengwenus"] @@ -16,10 +17,26 @@ 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") + +DataTrigger = sml_ns.class_( + "DataTrigger", + automation.Trigger.template( + cg.std_vector.template(cg.uint8).operator("ref"), cg.bool_ + ), +) + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Sml), + cv.Optional(CONF_ON_DATA): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger), + } + ), } ).extend(uart.UART_DEVICE_SCHEMA) @@ -28,6 +45,19 @@ 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) + for conf in config.get(CONF_ON_DATA, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + ( + cg.std_vector.template(cg.uint8).operator("ref").operator("const"), + "bytes", + ), + (cg.bool_, "valid"), + ], + conf, + ) def obis_code(value): diff --git a/esphome/components/sml/automation.h b/esphome/components/sml/automation.h new file mode 100644 index 0000000000..d51063065d --- /dev/null +++ b/esphome/components/sml/automation.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "sml.h" + +#include + +namespace esphome { +namespace sml { + +class DataTrigger : public Trigger &, bool> { + public: + explicit DataTrigger(Sml *sml) { + sml->add_on_data_callback([this](const std::vector &data, bool valid) { this->trigger(data, valid); }); + } +}; + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/constants.h b/esphome/components/sml/constants.h index 22114fd233..d6761d4bb7 100644 --- a/esphome/components/sml/constants.h +++ b/esphome/components/sml/constants.h @@ -17,32 +17,11 @@ enum SmlType : uint8_t { enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES = 0x701 }; -enum Crc16CheckResult : uint8_t { CHECK_CRC16_FAILED, CHECK_CRC16_X25_SUCCESS, CHECK_CRC16_KERMIT_SUCCESS }; - // masks with two-bit mapping 0x1b -> 0b01; 0x01 -> 0b10; 0x1a -> 0b11 -const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 1b 01 01 01 01 +const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 01 01 01 01 const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a -const uint16_t CRC16_X25_TABLE[256] = { - 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, - 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, - 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, - 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, - 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, - 0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, - 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, - 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, - 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, - 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, - 0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, - 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, - 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, - 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, - 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, - 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, - 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, - 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, - 0x3de3, 0x2c6a, 0x1ef1, 0x0f78}; +const std::vector START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01}; } // namespace sml } // namespace esphome diff --git a/esphome/components/sml/sml.cpp b/esphome/components/sml/sml.cpp index c6fe7d64cd..bac13be923 100644 --- a/esphome/components/sml/sml.cpp +++ b/esphome/components/sml/sml.cpp @@ -1,5 +1,6 @@ #include "sml.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include "sml_parser.h" namespace esphome { @@ -34,16 +35,24 @@ void Sml::loop() { case START_BYTES_DETECTED: { this->record_ = true; this->sml_data_.clear(); + // add start sequence (for callbacks) + this->sml_data_.insert(this->sml_data_.begin(), START_SEQ.begin(), START_SEQ.end()); break; }; case END_BYTES_DETECTED: { if (this->record_) { this->record_ = false; - if (!check_sml_data(this->sml_data_)) + bool valid = check_sml_data(this->sml_data_); + + // call callbacks + this->data_callbacks_.call(this->sml_data_, valid); + + if (!valid) break; - // remove footer bytes + // remove start/end sequence + this->sml_data_.erase(this->sml_data_.begin(), this->sml_data_.begin() + START_SEQ.size()); this->sml_data_.resize(this->sml_data_.size() - 8); this->process_sml_file_(this->sml_data_); } @@ -53,6 +62,10 @@ void Sml::loop() { } } +void Sml::add_on_data_callback(std::function, bool)> &&callback) { + this->data_callbacks_.add(std::move(callback)); +} + void Sml::process_sml_file_(const bytes &sml_data) { SmlFile sml_file = SmlFile(sml_data); std::vector obis_info = sml_file.get_obis_info(); @@ -99,12 +112,15 @@ bool check_sml_data(const bytes &buffer) { } uint16_t crc_received = (buffer.at(buffer.size() - 2) << 8) | buffer.at(buffer.size() - 1); - if (crc_received == calc_crc16_x25(buffer.begin(), buffer.end() - 2, 0x6e23)) { + uint16_t crc_calculated = crc16(buffer.data() + 8, buffer.size() - 10, 0x6e23, 0x8408, true, true); + crc_calculated = (crc_calculated >> 8) | (crc_calculated << 8); + if (crc_received == crc_calculated) { ESP_LOGV(TAG, "Checksum verification successful with CRC16/X25."); return true; } - if (crc_received == calc_crc16_kermit(buffer.begin(), buffer.end() - 2, 0xed50)) { + crc_calculated = crc16(buffer.data() + 8, buffer.size() - 10, 0xed50, 0x8408); + if (crc_received == crc_calculated) { ESP_LOGV(TAG, "Checksum verification successful with CRC16/KERMIT."); return true; } @@ -113,22 +129,6 @@ bool check_sml_data(const bytes &buffer) { return false; } -uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum) { - for (auto it = begin; it != end; it++) { - crcsum = (crcsum >> 8) ^ CRC16_X25_TABLE[(crcsum & 0xff) ^ *it]; - } - return crcsum; -} - -uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) { - crcsum = calc_crc16_p1021(begin, end, crcsum ^ 0xffff) ^ 0xffff; - return (crcsum >> 8) | ((crcsum & 0xff) << 8); -} - -uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) { - return calc_crc16_p1021(begin, end, crcsum); -} - uint8_t get_code(uint8_t byte) { switch (byte) { case 0x1b: diff --git a/esphome/components/sml/sml.h b/esphome/components/sml/sml.h index ac7befb043..b0c932ca95 100644 --- a/esphome/components/sml/sml.h +++ b/esphome/components/sml/sml.h @@ -3,6 +3,7 @@ #include #include #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/uart/uart.h" #include "sml_parser.h" @@ -23,6 +24,7 @@ class Sml : public Component, public uart::UARTDevice { void loop() override; void dump_config() override; std::vector sml_listeners_{}; + void add_on_data_callback(std::function, bool)> &&callback); protected: void process_sml_file_(const bytes &sml_data); @@ -35,12 +37,11 @@ class Sml : public Component, public uart::UARTDevice { bool record_ = false; uint16_t incoming_mask_ = 0; bytes sml_data_; + + CallbackManager &, bool)> data_callbacks_{}; }; bool check_sml_data(const bytes &buffer); -uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); -uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); -uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); uint8_t get_code(uint8_t byte); } // namespace sml diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index ff7da4cabd..2cc71e87fa 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -10,7 +10,7 @@ SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { this->pos_ = 0; while (this->pos_ < this->buffer_.size()) { if (this->buffer_[this->pos_] == 0x00) - break; // fill byte detected -> no more messages + break; // EndOfSmlMsg SmlNode message = SmlNode(); if (!this->setup_node(&message)) @@ -20,38 +20,66 @@ SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { } bool SmlFile::setup_node(SmlNode *node) { - uint8_t type = this->buffer_[this->pos_] >> 4; // type including overlength info - uint8_t length = this->buffer_[this->pos_] & 0x0f; // length including TL bytes - bool is_list = (type & 0x07) == SML_LIST; - bool has_extended_length = type & 0x08; // we have a long list/value (>15 entries) - uint8_t parse_length = length; - if (has_extended_length) { - length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); - parse_length = length - 1; + // If the TL field is 0x00, this is the end of the message + // (see 6.3.1 of SML protocol definition) + if (this->buffer_[this->pos_] == 0x00) { + // Increment past this byte and signal that the message is done this->pos_ += 1; + return true; } - if (this->pos_ + parse_length >= this->buffer_.size()) + // Extract data from initial TL field + uint8_t type = (this->buffer_[this->pos_] >> 4) & 0x07; // type without overlength info + bool overlength = (this->buffer_[this->pos_] >> 4) & 0x08; // overlength information + uint8_t length = this->buffer_[this->pos_] & 0x0f; // length (including TL bytes) + + // Check if we need additional length bytes + if (overlength) { + // Shift the current length to the higher nibble + // and add the lower nibble of the next byte to the length + length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); + // We are basically done with the first TL field now, + // so increment past that, we now point to the second TL field + this->pos_ += 1; + // Decrement the length for value fields (not lists), + // since the byte we just handled is counted as part of the field + // in case of values but not for lists + if (type != SML_LIST) + length -= 1; + + // Technically, this is not enough, the standard allows for more than two length fields. + // However I don't think it is very common to have more than 255 entries in a list + } + + // We are done with the last TL field(s), so advance the position + this->pos_ += 1; + // and decrement the length for non-list fields + if (type != SML_LIST) + length -= 1; + + // Check if the buffer length is long enough + if (this->pos_ + length > this->buffer_.size()) return false; - node->type = type & 0x07; + node->type = type; node->nodes.clear(); node->value_bytes.clear(); - if (this->buffer_[this->pos_] == 0x00) { // end of message - this->pos_ += 1; - } else if (is_list) { // list - this->pos_ += 1; - node->nodes.reserve(parse_length); - for (size_t i = 0; i != parse_length; i++) { + + if (type == SML_LIST) { + node->nodes.reserve(length); + for (size_t i = 0; i != length; i++) { SmlNode child_node = SmlNode(); if (!this->setup_node(&child_node)) return false; node->nodes.emplace_back(child_node); } - } else { // value - node->value_bytes = - bytes(this->buffer_.begin() + this->pos_ + 1, this->buffer_.begin() + this->pos_ + parse_length); - this->pos_ += parse_length; + } else { + // Value starts at the current position + // Value ends "length" bytes later, + // (since the TL field is counted but already subtracted from length) + node->value_bytes = bytes(this->buffer_.begin() + this->pos_, this->buffer_.begin() + this->pos_ + length); + // Increment the pointer past all consumed bytes + this->pos_ += length; } return true; } @@ -95,19 +123,15 @@ int64_t bytes_to_int(const bytes &buffer) { uint64_t tmp = bytes_to_uint(buffer); int64_t val; - switch (buffer.size()) { - case 1: // int8 - val = (int8_t) tmp; - break; - case 2: // int16 - val = (int16_t) tmp; - break; - case 4: // int32 - val = (int32_t) tmp; - break; - default: // int64 - val = (int64_t) tmp; + // sign extension for abbreviations of leading ones (e.g. 3 byte transmissions, see 6.2.2 of SML protocol definition) + // see https://stackoverflow.com/questions/42534749/signed-extension-from-24-bit-to-32-bit-in-c + if (buffer.size() < 8) { + const int bits = buffer.size() * 8; + const uint64_t m = 1ull << (bits - 1); + tmp = (tmp ^ m) - m; } + + val = (int64_t) tmp; return val; } diff --git a/esphome/components/sn74hc165/__init__.py b/esphome/components/sn74hc165/__init__.py index 85d0220a88..0f2abd3678 100644 --- a/esphome/components/sn74hc165/__init__.py +++ b/esphome/components/sn74hc165/__init__.py @@ -77,7 +77,15 @@ SN74HC165_PIN_SCHEMA = cv.All( ) -@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC165, SN74HC165_PIN_SCHEMA) +def sn74hc165_pin_final_validate(pin_config, parent_config): + max_pins = parent_config[CONF_SR_COUNT] * 8 + if pin_config[CONF_NUMBER] >= max_pins: + raise cv.Invalid(f"Pin number must be less than {max_pins}") + + +@pins.PIN_SCHEMA_REGISTRY.register( + CONF_SN74HC165, SN74HC165_PIN_SCHEMA, sn74hc165_pin_final_validate +) async def sn74hc165_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_SN74HC165]) diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index 92b6d8d0e5..2fd49f6824 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -1,81 +1,117 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins +from esphome.components import spi from esphome.const import ( CONF_ID, - CONF_MODE, CONF_NUMBER, CONF_INVERTED, CONF_DATA_PIN, CONF_CLOCK_PIN, + CONF_OE_PIN, CONF_OUTPUT, + CONF_TYPE, ) -DEPENDENCIES = [] MULTI_CONF = True sn74hc595_ns = cg.esphome_ns.namespace("sn74hc595") SN74HC595Component = sn74hc595_ns.class_("SN74HC595Component", cg.Component) +SN74HC595GPIOComponent = sn74hc595_ns.class_( + "SN74HC595GPIOComponent", SN74HC595Component +) +SN74HC595SPIComponent = sn74hc595_ns.class_( + "SN74HC595SPIComponent", SN74HC595Component, spi.SPIDevice +) + SN74HC595GPIOPin = sn74hc595_ns.class_( "SN74HC595GPIOPin", cg.GPIOPin, cg.Parented.template(SN74HC595Component) ) CONF_SN74HC595 = "sn74hc595" CONF_LATCH_PIN = "latch_pin" -CONF_OE_PIN = "oe_pin" CONF_SR_COUNT = "sr_count" -CONFIG_SCHEMA = cv.Schema( + +TYPE_GPIO = "gpio" +TYPE_SPI = "spi" + +_COMMON_SCHEMA = cv.Schema( { - cv.Required(CONF_ID): cv.declare_id(SN74HC595Component), - cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_LATCH_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_SR_COUNT, default=1): cv.int_range(min=1, max=256), } -).extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.typed_schema( + { + TYPE_GPIO: _COMMON_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(SN74HC595GPIOComponent), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + } + ).extend(cv.COMPONENT_SCHEMA), + TYPE_SPI: _COMMON_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(SN74HC595SPIComponent), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=False)), + }, + default_type=TYPE_GPIO, +) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - data_pin = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) - cg.add(var.set_data_pin(data_pin)) - clock_pin = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) - cg.add(var.set_clock_pin(clock_pin)) + if config[CONF_TYPE] == TYPE_GPIO: + data_pin = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data_pin)) + clock_pin = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock_pin)) + else: + await spi.register_spi_device(var, config) + latch_pin = await cg.gpio_pin_expression(config[CONF_LATCH_PIN]) cg.add(var.set_latch_pin(latch_pin)) - if CONF_OE_PIN in config: - oe_pin = await cg.gpio_pin_expression(config[CONF_OE_PIN]) + if oe_pin := config.get(CONF_OE_PIN): + oe_pin = await cg.gpio_pin_expression(oe_pin) cg.add(var.set_oe_pin(oe_pin)) cg.add(var.set_sr_count(config[CONF_SR_COUNT])) def _validate_output_mode(value): - if value is not True: + if value.get(CONF_OUTPUT) is not True: raise cv.Invalid("Only output mode is supported") return value -SN74HC595_PIN_SCHEMA = cv.All( +SN74HC595_PIN_SCHEMA = pins.gpio_base_schema( + SN74HC595GPIOPin, + cv.int_range(min=0, max=2047), + modes=[CONF_OUTPUT], + mode_validator=_validate_output_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(SN74HC595GPIOPin), cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=2048, max_included=False), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_OUTPUT, default=True): cv.All( - cv.boolean, _validate_output_mode - ), - }, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) -@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC595, SN74HC595_PIN_SCHEMA) +def sn74hc595_pin_final_validate(pin_config, parent_config): + max_pins = parent_config[CONF_SR_COUNT] * 8 + if pin_config[CONF_NUMBER] >= max_pins: + raise cv.Invalid(f"Pin number must be less than {max_pins}") + + +@pins.PIN_SCHEMA_REGISTRY.register( + CONF_SN74HC595, SN74HC595_PIN_SCHEMA, sn74hc595_pin_final_validate +) async def sn74hc595_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_SN74HC595]) diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index 1895b1d5a6..8a37c3bece 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -6,26 +6,39 @@ namespace sn74hc595 { static const char *const TAG = "sn74hc595"; -void SN74HC595Component::setup() { +void SN74HC595Component::pre_setup_() { ESP_LOGCONFIG(TAG, "Setting up SN74HC595..."); if (this->have_oe_pin_) { // disable output this->oe_pin_->setup(); this->oe_pin_->digital_write(true); } - - // initialize output pins - this->clock_pin_->setup(); - this->data_pin_->setup(); +} +void SN74HC595Component::post_setup_() { this->latch_pin_->setup(); - this->clock_pin_->digital_write(false); - this->data_pin_->digital_write(false); this->latch_pin_->digital_write(false); // send state to shift register - this->write_gpio_(); + this->write_gpio(); } +void SN74HC595GPIOComponent::setup() { + this->pre_setup_(); + this->clock_pin_->setup(); + this->data_pin_->setup(); + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(false); + this->post_setup_(); +} + +#ifdef USE_SPI +void SN74HC595SPIComponent::setup() { + this->pre_setup_(); + this->spi_setup(); + this->post_setup_(); +} +#endif + void SN74HC595Component::dump_config() { ESP_LOGCONFIG(TAG, "SN74HC595:"); } void SN74HC595Component::digital_write_(uint16_t pin, bool value) { @@ -34,17 +47,38 @@ void SN74HC595Component::digital_write_(uint16_t pin, bool value) { (this->sr_count_ * 8) - 1); return; } - this->output_bits_[pin] = value; - this->write_gpio_(); + if (value) { + this->output_bytes_[pin / 8] |= (1 << (pin % 8)); + } else { + this->output_bytes_[pin / 8] &= ~(1 << (pin % 8)); + } + this->write_gpio(); } -void SN74HC595Component::write_gpio_() { - for (auto bit = this->output_bits_.rbegin(); bit != this->output_bits_.rend(); bit++) { - this->data_pin_->digital_write(*bit); - this->clock_pin_->digital_write(true); - this->clock_pin_->digital_write(false); +void SN74HC595GPIOComponent::write_gpio() { + for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { + for (int8_t i = 7; i >= 0; i--) { + bool bit = (*byte >> i) & 1; + this->data_pin_->digital_write(bit); + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); + } } + SN74HC595Component::write_gpio(); +} +#ifdef USE_SPI +void SN74HC595SPIComponent::write_gpio() { + for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { + this->enable(); + this->transfer_byte(*byte); + this->disable(); + } + SN74HC595Component::write_gpio(); +} +#endif + +void SN74HC595Component::write_gpio() { // pulse latch to activate new values this->latch_pin_->digital_write(true); this->latch_pin_->digital_write(false); @@ -60,11 +94,7 @@ 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 { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via SN74HC595", pin_); - return buffer; -} +std::string SN74HC595GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC595", 18, pin_); } } // namespace sn74hc595 } // namespace esphome diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h index 64bf06d881..cb9d7bf140 100644 --- a/esphome/components/sn74hc595/sn74hc595.h +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -1,9 +1,14 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#ifdef USE_SPI +#include "esphome/components/spi/spi.h" +#endif + #include namespace esphome { @@ -13,34 +18,33 @@ class SN74HC595Component : public Component { public: SN74HC595Component() = default; - void setup() override; + void setup() override = 0; float get_setup_priority() const override; void dump_config() override; - void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } - void set_clock_pin(GPIOPin *pin) { clock_pin_ = pin; } - void set_latch_pin(GPIOPin *pin) { latch_pin_ = pin; } + void set_latch_pin(GPIOPin *pin) { this->latch_pin_ = pin; } void set_oe_pin(GPIOPin *pin) { - oe_pin_ = pin; - have_oe_pin_ = true; + this->oe_pin_ = pin; + this->have_oe_pin_ = true; } void set_sr_count(uint8_t count) { - sr_count_ = count; - this->output_bits_.resize(count * 8); + this->sr_count_ = count; + this->output_bytes_.resize(count); } protected: friend class SN74HC595GPIOPin; void digital_write_(uint16_t pin, bool value); - void write_gpio_(); + virtual void write_gpio(); + + void pre_setup_(); + void post_setup_(); - GPIOPin *data_pin_; - GPIOPin *clock_pin_; GPIOPin *latch_pin_; GPIOPin *oe_pin_; uint8_t sr_count_; bool have_oe_pin_{false}; - std::vector output_bits_; + std::vector output_bytes_; }; /// Helper class to expose a SC74HC595 pin as an internal output GPIO pin. @@ -60,5 +64,31 @@ class SN74HC595GPIOPin : public GPIOPin, public Parented { bool inverted_; }; +class SN74HC595GPIOComponent : public SN74HC595Component { + public: + void setup() override; + void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } + void set_clock_pin(GPIOPin *pin) { clock_pin_ = pin; } + + protected: + void write_gpio() override; + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; +}; + +#ifdef USE_SPI +class SN74HC595SPIComponent : public SN74HC595Component, + public spi::SPIDevice { + public: + void setup() override; + + protected: + void write_gpio() override; +}; + +#endif + } // namespace sn74hc595 } // namespace esphome diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 3af21a9b23..4ded98d483 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -1,16 +1,11 @@ #include "sntp_component.h" #include "esphome/core/log.h" -#ifdef USE_ESP32 -#include "lwip/apps/sntp.h" #ifdef USE_ESP_IDF #include "esp_sntp.h" -#endif -#endif -#ifdef USE_ESP8266 +#elif USE_ESP8266 #include "sntp.h" -#endif -#ifdef USE_RP2040 +#else #include "lwip/apps/sntp.h" #endif @@ -26,14 +21,14 @@ static const char *const TAG = "sntp"; void SNTPComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SNTP..."); -#ifdef USE_ESP32 - if (sntp_enabled()) { - sntp_stop(); +#if defined(USE_ESP_IDF) + if (esp_sntp_enabled()) { + esp_sntp_stop(); } - sntp_setoperatingmode(SNTP_OPMODE_POLL); -#endif -#ifdef USE_ESP8266 + esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); +#else sntp_stop(); + sntp_setoperatingmode(SNTP_OPMODE_POLL); #endif sntp_setservername(0, strdup(this->server_1_.c_str())); @@ -44,7 +39,7 @@ void SNTPComponent::setup() { sntp_setservername(2, strdup(this->server_3_.c_str())); } #ifdef USE_ESP_IDF - sntp_set_sync_interval(this->get_update_interval()); + esp_sntp_set_sync_interval(this->get_update_interval()); #endif sntp_init(); @@ -57,7 +52,7 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); } void SNTPComponent::update() { -#ifndef USE_ESP_IDF +#if !defined(USE_ESP_IDF) // force resync if (sntp_enabled()) { sntp_stop(); diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index 4c70a6b09f..987dd23a19 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -22,7 +22,7 @@ class SNTPComponent : public time::RealTimeClock { this->server_2_ = server_2; this->server_3_ = server_3; } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } void update() override; void loop() override; diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index b1362f5421..7cc82e3dff 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -2,24 +2,41 @@ from esphome.components import time as time_ import esphome.config_validation as cv import esphome.codegen as cg from esphome.core import CORE -from esphome.const import CONF_ID, CONF_SERVERS - +from esphome.const import ( + CONF_ID, + CONF_SERVERS, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, + PLATFORM_RTL87XX, + PLATFORM_BK72XX, +) DEPENDENCIES = ["network"] sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) - DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] -CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(SNTPComponent), - cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All( - cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3) - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SNTPComponent), + cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All( + cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3) + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, + PLATFORM_BK72XX, + PLATFORM_RTL87XX, + ] + ), +) async def to_code(config): diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 1757ec4668..19952aeece 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -5,6 +5,7 @@ CODEOWNERS = ["@esphome/core"] CONF_IMPLEMENTATION = "implementation" IMPLEMENTATION_LWIP_TCP = "lwip_tcp" +IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets" IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" CONFIG_SCHEMA = cv.Schema( @@ -14,9 +15,15 @@ CONFIG_SCHEMA = cv.Schema( esp8266=IMPLEMENTATION_LWIP_TCP, esp32=IMPLEMENTATION_BSD_SOCKETS, rp2040=IMPLEMENTATION_LWIP_TCP, + bk72xx=IMPLEMENTATION_LWIP_SOCKETS, + rtl87xx=IMPLEMENTATION_LWIP_SOCKETS, host=IMPLEMENTATION_BSD_SOCKETS, ): cv.one_of( - IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_" + IMPLEMENTATION_LWIP_TCP, + IMPLEMENTATION_LWIP_SOCKETS, + IMPLEMENTATION_BSD_SOCKETS, + lower=True, + space="_", ), } ) @@ -26,5 +33,7 @@ async def to_code(config): impl = config[CONF_IMPLEMENTATION] if impl == IMPLEMENTATION_LWIP_TCP: cg.add_define("USE_SOCKET_IMPL_LWIP_TCP") + elif impl == IMPLEMENTATION_LWIP_SOCKETS: + cg.add_define("USE_SOCKET_IMPL_LWIP_SOCKETS") elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 2dea4af277..f07f5c8f81 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -86,26 +86,15 @@ class BSDSocketImpl : public Socket { } int listen(int backlog) override { return ::listen(fd_, backlog); } ssize_t read(void *buf, size_t len) override { return ::read(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); +#else + return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); +#endif + } ssize_t readv(const struct iovec *iov, int iovcnt) override { -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 4 - // esp-idf v3 doesn't have readv, emulate it - ssize_t ret = 0; - for (int i = 0; i < iovcnt; i++) { - ssize_t err = this->read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); - if (err == -1) { - if (ret != 0) { - // if we already read some don't return an error - break; - } - return err; - } - ret += err; - if (err != iov[i].iov_len) - break; - } - return ret; -#elif defined(USE_ESP32) - // ESP-IDF v4 only has symbol lwip_readv +#if defined(USE_ESP32) return ::lwip_readv(fd_, iov, iovcnt); #else return ::readv(fd_, iov, iovcnt); @@ -114,26 +103,7 @@ class BSDSocketImpl : public Socket { 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 writev(const struct iovec *iov, int iovcnt) override { -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 4 - // esp-idf v3 doesn't have writev, emulate it - ssize_t ret = 0; - for (int i = 0; i < iovcnt; i++) { - ssize_t err = - this->send(reinterpret_cast(iov[i].iov_base), iov[i].iov_len, i == iovcnt - 1 ? 0 : MSG_MORE); - if (err == -1) { - if (ret != 0) { - // if we already wrote some don't return an error - break; - } - return err; - } - ret += err; - if (err != iov[i].iov_len) - break; - } - return ret; -#elif defined(USE_ESP32) - // ESP-IDF v4 only has symbol lwip_writev +#if defined(USE_ESP32) return ::lwip_writev(fd_, iov, iovcnt); #else return ::writev(fd_, iov, iovcnt); diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 1922885ac0..032892072d 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -120,6 +120,35 @@ struct iovec { #endif // USE_SOCKET_IMPL_LWIP_TCP +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS + +// standard lwIP's compatibility macros will interfere +// with Socket class function names - disable the macros +// and use real function names instead +#undef LWIP_COMPAT_SOCKETS +#define LWIP_COMPAT_SOCKETS 0 + +#include "lwip/sockets.h" +#include + +#ifdef USE_ARDUINO +// arduino-esp32 declares a global var called INADDR_NONE which is replaced +// by the define +#ifdef INADDR_NONE +#undef INADDR_NONE +#endif +// not defined for ESP32 +using socklen_t = uint32_t; + +#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL) +#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL) +#else // !USE_ESP32 +#define ESPHOME_INADDR_ANY INADDR_ANY +#define ESPHOME_INADDR_NONE INADDR_NONE +#endif + +#endif // USE_SOCKET_IMPL_LWIP_SOCKETS + #ifdef USE_SOCKET_IMPL_BSD_SOCKETS #include diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index bd59b81caa..1d998902ff 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -469,7 +469,8 @@ class LWIPRawImpl : public Socket { } 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); - return 0; + errno = ENOSYS; + return -1; } int setblocking(bool blocking) override { if (pcb_ == nullptr) { diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp new file mode 100644 index 0000000000..eaf6ac2c6f --- /dev/null +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -0,0 +1,115 @@ +#include "socket.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS + +#include + +namespace esphome { +namespace 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 { + public: + LwIPSocketImpl(int fd) : fd_(fd) {} + ~LwIPSocketImpl() override { + if (!closed_) { + close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) + } + } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { + int fd = lwip_accept(fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd); + } + int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(fd_, addr, addrlen); } + int close() override { + int ret = lwip_close(fd_); + closed_ = true; + return ret; + } + int shutdown(int how) override { return lwip_shutdown(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 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 getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + return lwip_getsockopt(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); + } + 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); } + 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); + } + int setblocking(bool blocking) override { + int fl = lwip_fcntl(fd_, F_GETFL, 0); + if (blocking) { + fl &= ~O_NONBLOCK; + } else { + fl |= O_NONBLOCK; + } + lwip_fcntl(fd_, F_SETFL, fl); + return 0; + } + + protected: + int fd_; + bool closed_ = false; +}; + +std::unique_ptr socket(int domain, int type, int protocol) { + int ret = lwip_socket(domain, type, protocol); + if (ret == -1) + return nullptr; + return std::unique_ptr{new LwIPSocketImpl(ret)}; +} + +} // namespace socket +} // namespace esphome + +#endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 4c78397873..e260fce05e 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -1,4 +1,5 @@ #include "socket.h" +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) #include #include #include @@ -10,33 +11,31 @@ namespace socket { Socket::~Socket() {} std::unique_ptr socket_ip(int type, int protocol) { -#if LWIP_IPV6 +#if USE_NETWORK_IPV6 return socket(AF_INET6, type, protocol); #else return socket(AF_INET, type, protocol); -#endif +#endif /* USE_NETWORK_IPV6 */ } socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { -#if LWIP_IPV6 - if (addrlen < sizeof(sockaddr_in6)) { - errno = EINVAL; - return 0; - } - auto *server = reinterpret_cast(addr); - memset(server, 0, sizeof(sockaddr_in6)); - server->sin6_family = AF_INET6; - server->sin6_port = htons(port); +#if USE_NETWORK_IPV6 + if (ip_address.find(':') != std::string::npos) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return 0; + } + auto *server = reinterpret_cast(addr); + memset(server, 0, sizeof(sockaddr_in6)); + server->sin6_family = AF_INET6; + server->sin6_port = htons(port); - if (ip_address.find('.') != std::string::npos) { - server->sin6_addr.un.u32_addr[3] = inet_addr(ip_address.c_str()); - } else { ip6_addr_t ip6; inet6_aton(ip_address.c_str(), &ip6); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); + return sizeof(sockaddr_in6); } - return sizeof(sockaddr_in6); -#else +#endif /* USE_NETWORK_IPV6 */ if (addrlen < sizeof(sockaddr_in)) { errno = EINVAL; return 0; @@ -47,11 +46,10 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri server->sin_addr.s_addr = inet_addr(ip_address.c_str()); server->sin_port = htons(port); return sizeof(sockaddr_in); -#endif } socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { -#if LWIP_IPV6 +#if USE_NETWORK_IPV6 if (addrlen < sizeof(sockaddr_in6)) { errno = EINVAL; return 0; @@ -60,7 +58,7 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po memset(server, 0, sizeof(sockaddr_in6)); server->sin6_family = AF_INET6; server->sin6_port = htons(port); - server->sin6_addr = in6addr_any; + server->sin6_addr = IN6ADDR_ANY_INIT; return sizeof(sockaddr_in6); #else if (addrlen < sizeof(sockaddr_in)) { @@ -73,7 +71,8 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po server->sin_addr.s_addr = ESPHOME_INADDR_ANY; server->sin_port = htons(port); return sizeof(sockaddr_in); -#endif +#endif /* USE_NETWORK_IPV6 */ } } // namespace socket } // namespace esphome +#endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index c9b8be88a0..cefdb51e0d 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -5,6 +5,7 @@ #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 { @@ -31,6 +32,9 @@ class Socket { 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; @@ -54,3 +58,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po } // namespace socket } // namespace esphome +#endif diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index 6ae80296fd..e70ec7b70d 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -128,7 +128,8 @@ bool SonoffD1Output::read_ack_(const uint8_t *cmd, const size_t len) { // Expected acknowledgement from rf chip uint8_t ref_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00}; uint8_t buffer[sizeof(ref_buffer)] = {0}; - uint32_t pos = 0, buf_len = sizeof(ref_buffer); + uint32_t pos = 0; + size_t buf_len = sizeof(ref_buffer); // Update the reference checksum this->populate_checksum_(ref_buffer, sizeof(ref_buffer)); diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 79d5df8c5a..7a668dc2f3 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -1,17 +1,18 @@ from esphome import automation -import esphome.config_validation as cv -import esphome.codegen as cg - from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_DATA +import esphome.codegen as cg +from esphome.components import audio_dac +import esphome.config_validation as cv +from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME from esphome.core import CORE from esphome.coroutine import coroutine_with_priority - -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] IS_PLATFORM_COMPONENT = True +CONF_AUDIO_DAC = "audio_dac" + speaker_ns = cg.esphome_ns.namespace("speaker") Speaker = speaker_ns.class_("Speaker") @@ -22,12 +23,28 @@ PlayAction = speaker_ns.class_( StopAction = speaker_ns.class_( "StopAction", automation.Action, cg.Parented.template(Speaker) ) +FinishAction = speaker_ns.class_( + "FinishAction", automation.Action, cg.Parented.template(Speaker) +) +VolumeSetAction = speaker_ns.class_( + "VolumeSetAction", automation.Action, cg.Parented.template(Speaker) +) +MuteOnAction = speaker_ns.class_( + "MuteOnAction", automation.Action, cg.Parented.template(Speaker) +) +MuteOffAction = speaker_ns.class_( + "MuteOffAction", automation.Action, cg.Parented.template(Speaker) +) + IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition) +IsStoppedCondition = speaker_ns.class_("IsStoppedCondition", automation.Condition) async def setup_speaker_core_(var, config): - pass + if audio_dac_config := config.get(CONF_AUDIO_DAC): + aud_dac = await cg.get_variable(audio_dac_config) + cg.add(var.set_audio_dac(aud_dac)) async def register_speaker(var, config): @@ -36,8 +53,11 @@ async def register_speaker(var, config): await setup_speaker_core_(var, config) -SPEAKER_SCHEMA = cv.Schema({}) - +SPEAKER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_AUDIO_DAC): cv.use_id(audio_dac.AudioDac), + } +) SPEAKER_AUTOMATION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Speaker)}) @@ -75,11 +95,46 @@ async def speaker_play_action(config, action_id, template_arg, args): automation.register_action("speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA)( speaker_action ) +automation.register_action("speaker.finish", FinishAction, SPEAKER_AUTOMATION_SCHEMA)( + speaker_action +) automation.register_condition( "speaker.is_playing", IsPlayingCondition, SPEAKER_AUTOMATION_SCHEMA )(speaker_action) +automation.register_condition( + "speaker.is_stopped", IsStoppedCondition, SPEAKER_AUTOMATION_SCHEMA +)(speaker_action) + + +@automation.register_action( + "speaker.volume_set", + VolumeSetAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Speaker), + cv.Required(CONF_VOLUME): cv.templatable(cv.percentage), + }, + key=CONF_VOLUME, + ), +) +async def speaker_volume_set_action(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + volume = await cg.templatable(config[CONF_VOLUME], args, float) + cg.add(var.set_volume(volume)) + return var + + +@automation.register_action( + "speaker.mute_off", MuteOffAction, SPEAKER_AUTOMATION_SCHEMA +) +@automation.register_action("speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA) +async def speaker_mute_action_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) + @coroutine_with_priority(100.0) async def to_code(config): diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index e28991a0d1..c083796eea 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -34,15 +34,50 @@ template class PlayAction : public Action, public Parente std::vector data_static_{}; }; +template class VolumeSetAction : public Action, public Parented { + TEMPLATABLE_VALUE(float, volume) + void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); } +}; + +template class MuteOnAction : public Action { + public: + explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {} + + void play(Ts... x) override { this->speaker_->set_mute_state(true); } + + protected: + Speaker *speaker_; +}; + +template class MuteOffAction : public Action { + public: + explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {} + + void play(Ts... x) override { this->speaker_->set_mute_state(false); } + + protected: + Speaker *speaker_; +}; + template class StopAction : public Action, public Parented { public: void play(Ts... x) override { this->parent_->stop(); } }; +template class FinishAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->finish(); } +}; + template class IsPlayingCondition : public Condition, public Parented { public: bool check(Ts... x) override { return this->parent_->is_running(); } }; +template class IsStoppedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_stopped(); } +}; + } // namespace speaker } // namespace esphome diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 5dfabfa40e..96843e2d5a 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -1,5 +1,20 @@ #pragma once +#include +#include +#include + +#ifdef USE_ESP32 +#include +#endif + +#include "esphome/core/defines.h" + +#include "esphome/components/audio/audio.h" +#ifdef USE_AUDIO_DAC +#include "esphome/components/audio_dac/audio_dac.h" +#endif + namespace esphome { namespace speaker { @@ -12,15 +27,83 @@ enum State : uint8_t { class Speaker { public: - virtual bool play(const uint8_t *data, size_t length) = 0; - virtual bool play(const std::vector &data) { return this->play(data.data(), data.size()); } +#ifdef USE_ESP32 + /// @brief Plays the provided audio data. + /// If the speaker component doesn't implement this method, it falls back to the play method without this parameter. + /// @param data Audio data in the format specified by ``set_audio_stream_info`` method. + /// @param length The length of the audio data in bytes. + /// @param ticks_to_wait The FreeRTOS ticks to wait before writing as much data as possible to the ring buffer. + /// @return The number of bytes that were actually written to the speaker's internal buffer. + virtual size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { + return this->play(data, length); + }; +#endif + /// @brief Plays the provided audio data. + /// If the audio stream is not the default defined in "esphome/core/audio.h" and the speaker component implements it, + /// then this should be called after calling ``set_audio_stream_info``. + /// @param data Audio data in the format specified by ``set_audio_stream_info`` method. + /// @param length The length of the audio data in bytes. + /// @return The number of bytes that were actually written to the speaker's internal buffer. + virtual size_t play(const uint8_t *data, size_t length) = 0; + + size_t play(const std::vector &data) { return this->play(data.data(), data.size()); } + + virtual void start() = 0; virtual void stop() = 0; + // In compare between *STOP()* and *FINISH()*; *FINISH()* will stop after emptying the play buffer, + // while *STOP()* will break directly. + // When finish() is not implemented on the platform component it should just do a normal stop. + virtual void finish() { this->stop(); } + + virtual bool has_buffered_data() const = 0; bool is_running() const { return this->state_ == STATE_RUNNING; } + bool is_stopped() const { return this->state_ == STATE_STOPPED; } + + // Volume control is handled by a configured audio dac component. Individual speaker components can + // override and implement in software if an audio dac isn't available. + virtual void set_volume(float volume) { + this->volume_ = volume; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + this->audio_dac_->set_volume(volume); + } +#endif + }; + float get_volume() { return this->volume_; } + + virtual void set_mute_state(bool mute_state) { + this->mute_state_ = mute_state; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_) { + if (mute_state) { + this->audio_dac_->set_mute_on(); + } else { + this->audio_dac_->set_mute_off(); + } + } +#endif + } + bool get_mute_state() { return this->mute_state_; } + +#ifdef USE_AUDIO_DAC + void set_audio_dac(audio_dac::AudioDac *audio_dac) { this->audio_dac_ = audio_dac; } +#endif + + void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info) { + this->audio_stream_info_ = audio_stream_info; + } protected: State state_{STATE_STOPPED}; + audio::AudioStreamInfo audio_stream_info_; + float volume_{1.0f}; + bool mute_state_{false}; + +#ifdef USE_AUDIO_DAC + audio_dac::AudioDac *audio_dac_{nullptr}; +#endif }; } // namespace speaker diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 978e68d1e9..5fbf011669 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,14 +1,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output +from esphome.components.fan import validate_preset_modes from esphome.const import ( + CONF_PRESET_MODES, + CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, - CONF_DIRECTION_OUTPUT, CONF_OUTPUT_ID, CONF_SPEED, CONF_SPEED_COUNT, ) + from .. import speed_ns SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) @@ -23,16 +26,19 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( "Configuring individual speeds is deprecated." ), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ).extend(cv.COMPONENT_SCHEMA) async def to_code(config): - output_ = await cg.get_variable(config[CONF_OUTPUT]) - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], output_, config[CONF_SPEED_COUNT]) + var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_SPEED_COUNT]) await cg.register_component(var, config) await fan.register_fan(var, config) + output_ = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(output_)) + if CONF_OSCILLATION_OUTPUT in config: oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) @@ -40,3 +46,6 @@ async def to_code(config): if CONF_DIRECTION_OUTPUT in config: direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT]) cg.add(var.set_direction(direction_output)) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3a65f2c365..57bd795416 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -12,11 +12,14 @@ void SpeedFan::setup() { restore->apply(*this); this->write_state_(); } + + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } + void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } -fan::FanTraits SpeedFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); -} + void SpeedFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); @@ -26,14 +29,15 @@ void SpeedFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); this->write_state_(); this->publish_state(); } + void SpeedFan::write_state_() { float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; this->output_->set_level(speed); - if (this->oscillating_ != nullptr) this->oscillating_->set_state(this->oscillating); if (this->direction_ != nullptr) diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 1fad53813a..6537bce3f6 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -10,12 +12,14 @@ namespace speed { class SpeedFan : public Component, public fan::Fan { public: - SpeedFan(output::FloatOutput *output, int speed_count) : output_(output), speed_count_(speed_count) {} + SpeedFan(int speed_count) : speed_count_(speed_count) {} void setup() override; void dump_config() override; + void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - fan::FanTraits get_traits() override; + void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -25,6 +29,8 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; + fan::FanTraits traits_; + std::set preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index e0fc9efb42..52afbf365e 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,65 +1,370 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -import esphome.final_validate as fv +import re + from esphome import pins +import esphome.codegen as cg +from esphome.components.esp32.const import ( + KEY_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +import esphome.config_validation as cv from esphome.const import ( CONF_CLK_PIN, + CONF_CS_PIN, + CONF_DATA_PINS, + CONF_DATA_RATE, CONF_ID, + CONF_INVERTED, CONF_MISO_PIN, CONF_MOSI_PIN, + CONF_NUMBER, CONF_SPI_ID, - CONF_CS_PIN, + KEY_CORE, + KEY_TARGET_PLATFORM, + KEY_VARIANT, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, ) -from esphome.core import coroutine_with_priority, CORE +from esphome.core import CORE, coroutine_with_priority +import esphome.final_validate as fv -CODEOWNERS = ["@esphome/core"] +CODEOWNERS = ["@esphome/core", "@clydebarrow"] spi_ns = cg.esphome_ns.namespace("spi") SPIComponent = spi_ns.class_("SPIComponent", cg.Component) +QuadSPIComponent = spi_ns.class_("QuadSPIComponent", cg.Component) SPIDevice = spi_ns.class_("SPIDevice") -MULTI_CONF = True -CONF_FORCE_SW = "force_sw" +SPIDataRate = spi_ns.enum("SPIDataRate") +SPIMode = spi_ns.enum("SPIMode") -CONFIG_SCHEMA = cv.All( +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, +} +SPI_DATA_RATE_SCHEMA = cv.All(cv.frequency, cv.enum(SPI_DATA_RATE_OPTIONS)) + +SPI_MODE_OPTIONS = { + "MODE0": SPIMode.MODE0, + "MODE1": SPIMode.MODE1, + "MODE2": SPIMode.MODE2, + "MODE3": SPIMode.MODE3, + 0: SPIMode.MODE0, + 1: SPIMode.MODE1, + 2: SPIMode.MODE2, + 3: SPIMode.MODE3, + "0": SPIMode.MODE0, + "1": SPIMode.MODE1, + "2": SPIMode.MODE2, + "3": SPIMode.MODE3, +} + +CONF_SPI_MODE = "spi_mode" +CONF_FORCE_SW = "force_sw" +CONF_INTERFACE = "interface" +CONF_INTERFACE_INDEX = "interface_index" +TYPE_SINGLE = "single" +TYPE_QUAD = "quad" + +# RP2040 SPI pin assignments are complicated; +# refer to GPIO function select table in https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + +RP_SPI_PINSETS = [ + { + CONF_MISO_PIN: [0, 4, 16, 20, -1], + CONF_CLK_PIN: [2, 6, 18, 22], + CONF_MOSI_PIN: [3, 7, 19, 23, -1], + }, + { + CONF_MISO_PIN: [8, 12, 24, 28, -1], + CONF_CLK_PIN: [10, 14, 26], + CONF_MOSI_PIN: [11, 15, 27, -1], + }, +] + + +def get_target_platform(): + return ( + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] + if KEY_TARGET_PLATFORM in CORE.data[KEY_CORE] + else "" + ) + + +def get_target_variant(): + return ( + CORE.data[KEY_ESP32][KEY_VARIANT] if KEY_VARIANT in CORE.data[KEY_ESP32] else "" + ) + + +# Get a list of available hardware interfaces based on target and variant. +# The returned value is a list of lists of names +def get_hw_interface_list(): + target_platform = get_target_platform() + if target_platform == PLATFORM_ESP8266: + return [["spi", "hspi"]] + if target_platform == PLATFORM_ESP32: + if get_target_variant() in [ + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + ]: + return [["spi", "spi2"]] + return [["spi", "spi2"], ["spi3"]] + if target_platform == PLATFORM_RP2040: + return [["spi"], ["spi1"]] + return [] + + +# Given an SPI name, return the index of it in the available list +def get_spi_index(name): + for i, ilist in enumerate(get_hw_interface_list()): + if name in ilist: + return i + # Should never get to here. + raise cv.Invalid(f"{name} is not an available SPI") + + +# Check that pins are suitable for HW spi +# \param spi the config data for the spi instance +# \param index the selected hw interface number, -1 if not yet known +# TODO verify that the pins are internal +def validate_hw_pins(spi, index=-1): + clk_pin = spi[CONF_CLK_PIN] + if clk_pin[CONF_INVERTED]: + return False + clk_pin_no = clk_pin[CONF_NUMBER] + sdo_pin_no = -1 + sdi_pin_no = -1 + if CONF_MOSI_PIN in spi: + sdo_pin = spi[CONF_MOSI_PIN] + if sdo_pin[CONF_INVERTED]: + return False + sdo_pin_no = sdo_pin[CONF_NUMBER] + if CONF_MISO_PIN in spi: + sdi_pin = spi[CONF_MISO_PIN] + if sdi_pin[CONF_INVERTED]: + return False + sdi_pin_no = sdi_pin[CONF_NUMBER] + + target_platform = get_target_platform() + if target_platform == PLATFORM_ESP8266: + if clk_pin_no == 6: + return sdo_pin_no in (-1, 8) and sdi_pin_no in (-1, 7) + if clk_pin_no == 14: + return sdo_pin_no in (-1, 13) and sdi_pin_no in (-1, 12) + return False + + if target_platform == PLATFORM_ESP32: + return clk_pin_no >= 0 + + if target_platform == PLATFORM_RP2040: + pin_set = ( + list(filter(lambda s: clk_pin_no in s[CONF_CLK_PIN], RP_SPI_PINSETS))[0] + if index == -1 + else RP_SPI_PINSETS[index] + ) + if pin_set is None: + return False + if sdo_pin_no not in pin_set[CONF_MOSI_PIN]: + return False + if sdi_pin_no not in pin_set[CONF_MISO_PIN]: + return False + return True + return False + + +def get_hw_spi(config, available): + """Get an available hardware spi interface suitable for this config""" + matching = list(filter(lambda idx: validate_hw_pins(config, idx), available)) + if len(matching) != 0: + return matching[0] + return None + + +def validate_spi_config(config): + available = list(range(len(get_hw_interface_list()))) + for spi in config: + interface = spi[CONF_INTERFACE] + if interface == "software": + pass + elif interface == "any": + if not validate_hw_pins(spi): + spi[CONF_INTERFACE] = "software" + elif interface == "hardware": + index = get_hw_spi(spi, available) + if index is None: + raise cv.Invalid("No suitable hardware interface available") + spi[CONF_INTERFACE_INDEX] = index + available.remove(index) + else: + # Must be a specific name + index = spi[CONF_INTERFACE_INDEX] = get_spi_index(interface) + if index not in available: + raise cv.Invalid( + f"interface '{interface}' not available here (may be already assigned)" + ) + available.remove(index) + + # Second time around: + # Any specific names and any 'hardware' requests will have already been filled, + # so just need to assign remaining hardware to 'any' requests. + for spi in config: + if spi[CONF_INTERFACE] == "any": + index = get_hw_spi(spi, available) + if index is not None: + spi[CONF_INTERFACE_INDEX] = index + available.remove(index) + if CONF_INTERFACE_INDEX in spi and not validate_hw_pins( + spi, spi[CONF_INTERFACE_INDEX] + ): + raise cv.Invalid("Invalid pin selections for hardware SPI interface") + if CONF_DATA_PINS in spi and CONF_INTERFACE_INDEX not in spi: + raise cv.Invalid("Quad mode requires a hardware interface") + + return 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: + return ["SPI2_HOST", "SPI3_HOST"][index] + # Arduino code follows + platform = get_target_platform() + if platform == PLATFORM_RP2040: + return ["&SPI", "&SPI1"][index] + if index == 0: + return "&SPI" + # Following code can't apply to C2, H2 or 8266 since they have only one SPI + if get_target_variant() in (VARIANT_ESP32S3, VARIANT_ESP32S2): + return "new SPIClass(FSPI)" + return "new SPIClass(HSPI)" + + +SPI_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(SPIComponent), cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_FORCE_SW, default=False): cv.boolean, + cv.Optional(CONF_FORCE_SW): cv.invalid( + "force_sw is deprecated - use interface: software" + ), + cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( + *sum(get_hw_interface_list(), ["software", "hardware", "any"]), + lower=True, + ), + cv.Optional(CONF_DATA_PINS): cv.invalid( + "'data_pins' should be used with 'type: quad' only" + ), } ), cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), +) + +SPI_QUAD_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(QuadSPIComponent), + cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DATA_PINS): cv.All( + cv.ensure_list(pins.internal_gpio_output_pin_number), + cv.Length(min=4, max=4), + ), + cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( + *sum(get_hw_interface_list(), ["hardware"]), + lower=True, + ), + cv.Optional(CONF_MISO_PIN): cv.invalid( + "'miso_pin' should not be used with quad SPI" + ), + cv.Optional(CONF_MOSI_PIN): cv.invalid( + "'mosi_pin' should not be used with quad SPI" + ), + } + ), + cv.only_on([PLATFORM_ESP32]), + cv.only_with_esp_idf, +) + +CONFIG_SCHEMA = cv.All( + cv.ensure_list( + cv.typed_schema( + { + TYPE_SINGLE: SPI_SCHEMA, + TYPE_QUAD: SPI_QUAD_SCHEMA, + }, + default_type=TYPE_SINGLE, + ) + ), + validate_spi_config, ) @coroutine_with_priority(1.0) -async def to_code(config): +async def to_code(configs): + cg.add_define("USE_SPI") cg.add_global(spi_ns.using) - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - - clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) - cg.add(var.set_clk(clk)) - cg.add(var.set_force_sw(config[CONF_FORCE_SW])) - if CONF_MISO_PIN in config: - miso = await cg.gpio_pin_expression(config[CONF_MISO_PIN]) - cg.add(var.set_miso(miso)) - if CONF_MOSI_PIN in config: - mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN]) - cg.add(var.set_mosi(mosi)) - if CORE.using_arduino: cg.add_library("SPI", None) + for spi in configs: + var = cg.new_Pvariable(spi[CONF_ID]) + await cg.register_component(var, spi) + clk = await cg.gpio_pin_expression(spi[CONF_CLK_PIN]) + cg.add(var.set_clk(clk)) + if miso := spi.get(CONF_MISO_PIN): + cg.add(var.set_miso(await cg.gpio_pin_expression(miso))) + if mosi := spi.get(CONF_MOSI_PIN): + cg.add(var.set_mosi(await cg.gpio_pin_expression(mosi))) + if data_pins := spi.get(CONF_DATA_PINS): + cg.add(var.set_data_pins(data_pins)) + if (index := spi.get(CONF_INTERFACE_INDEX)) is not None: + interface = get_spi_interface(index) + cg.add(var.set_interface(cg.RawExpression(interface))) + cg.add( + var.set_interface_name( + re.sub(r"\W", "", interface.replace("new SPIClass", "")) + ) + ) -def spi_device_schema(cs_pin_required=True): +def spi_device_schema( + cs_pin_required=True, + default_data_rate=cv.UNDEFINED, + default_mode=cv.UNDEFINED, + quad=False, +): """Create a schema for an SPI device. :param cs_pin_required: If true, make the CS_PIN required in the config. + :param default_data_rate: Optional data_rate to use as default + :param default_mode Optional. The default SPI mode to use. + :param quad If set, will require an SPI component configured as quad data bits. :return: The SPI device schema, `extend` this in your config schema. """ schema = { - cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + cv.GenerateID(CONF_SPI_ID): cv.use_id( + QuadSPIComponent if quad else SPIComponent + ), + 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 + ), } if cs_pin_required: schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema @@ -74,6 +379,10 @@ async def register_spi_device(var, config): if CONF_CS_PIN in config: pin = await cg.gpio_pin_expression(config[CONF_CS_PIN]) cg.add(var.set_cs_pin(pin)) + if CONF_DATA_RATE in config: + cg.add(var.set_data_rate(config[CONF_DATA_RATE])) + if CONF_SPI_MODE in config: + cg.add(var.set_mode(config[CONF_SPI_MODE])) def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: bool): diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 141bfb9448..f9435b0424 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -1,243 +1,118 @@ #include "spi.h" #include "esphome/core/log.h" -#include "esphome/core/helpers.h" #include "esphome/core/application.h" namespace esphome { namespace spi { -static const char *const TAG = "spi"; +const char *const TAG = "spi"; -void IRAM_ATTR HOT SPIComponent::disable() { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - this->hw_spi_->endTransaction(); - } -#endif // USE_SPI_ARDUINO_BACKEND - if (this->active_cs_) { - this->active_cs_->digital_write(true); - this->active_cs_ = nullptr; +bool SPIDelegate::is_ready() { return true; } + +GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, + GPIOPin *cs_pin) { + if (this->devices_.count(device) != 0) { + ESP_LOGE(TAG, "SPI device already registered"); + return this->devices_[device]; } + SPIDelegate *delegate = this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin); // NOLINT + this->devices_[device] = delegate; + return delegate; } + +void SPIComponent::unregister_device(SPIClient *device) { + if (this->devices_.count(device) == 0) { + esph_log_e(TAG, "SPI device not registered"); + return; + } + delete this->devices_[device]; // NOLINT + this->devices_.erase(device); +} + void SPIComponent::setup() { - ESP_LOGCONFIG(TAG, "Setting up SPI bus..."); - this->clk_->setup(); - this->clk_->digital_write(true); + ESP_LOGD(TAG, "Setting up SPI bus..."); -#ifdef USE_SPI_ARDUINO_BACKEND - bool use_hw_spi = !this->force_sw_; - const bool has_miso = this->miso_ != nullptr; - const bool has_mosi = this->mosi_ != nullptr; - int8_t clk_pin = -1, miso_pin = -1, mosi_pin = -1; - - if (!this->clk_->is_internal()) - use_hw_spi = false; - if (has_miso && !miso_->is_internal()) - use_hw_spi = false; - if (has_mosi && !mosi_->is_internal()) - use_hw_spi = false; - if (use_hw_spi) { - auto *clk_internal = (InternalGPIOPin *) clk_; - auto *miso_internal = (InternalGPIOPin *) miso_; - auto *mosi_internal = (InternalGPIOPin *) mosi_; - - if (clk_internal->is_inverted()) - use_hw_spi = false; - if (has_miso && miso_internal->is_inverted()) - use_hw_spi = false; - if (has_mosi && mosi_internal->is_inverted()) - use_hw_spi = false; - - if (use_hw_spi) { - clk_pin = clk_internal->get_pin(); - miso_pin = has_miso ? miso_internal->get_pin() : -1; - mosi_pin = has_mosi ? mosi_internal->get_pin() : -1; - } - } -#ifdef USE_ESP8266 - if (!(clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) && - !(clk_pin == 14 && (!has_miso || miso_pin == 12) && (!has_mosi || mosi_pin == 13))) - use_hw_spi = false; - - if (use_hw_spi) { - this->hw_spi_ = &SPI; - this->hw_spi_->pins(clk_pin, miso_pin, mosi_pin, 0); - this->hw_spi_->begin(); + if (this->sdo_pin_ == nullptr) + this->sdo_pin_ = NullPin::NULL_PIN; + if (this->sdi_pin_ == nullptr) + this->sdi_pin_ = NullPin::NULL_PIN; + if (this->clk_pin_ == nullptr) { + ESP_LOGE(TAG, "No clock pin for SPI"); + this->mark_failed(); return; } -#endif // USE_ESP8266 -#ifdef USE_ESP32 - static uint8_t spi_bus_num = 0; - if (spi_bus_num >= 2) { - use_hw_spi = false; - } - if (use_hw_spi) { - if (spi_bus_num == 0) { - this->hw_spi_ = &SPI; - } else { -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - this->hw_spi_ = new SPIClass(FSPI); // NOLINT(cppcoreguidelines-owning-memory) -#else - this->hw_spi_ = new SPIClass(HSPI); // NOLINT(cppcoreguidelines-owning-memory) -#endif // USE_ESP32_VARIANT + if (this->using_hw_) { + this->spi_bus_ = + SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_, this->data_pins_); + if (this->spi_bus_ == nullptr) { + ESP_LOGE(TAG, "Unable to allocate SPI interface"); + this->mark_failed(); } - spi_bus_num++; - this->hw_spi_->begin(clk_pin, miso_pin, mosi_pin); - return; - } -#endif // USE_ESP32 -#endif // USE_SPI_ARDUINO_BACKEND - - if (this->miso_ != nullptr) { - this->miso_->setup(); - } - if (this->mosi_ != nullptr) { - this->mosi_->setup(); - this->mosi_->digital_write(false); + } else { + this->spi_bus_ = new SPIBus(this->clk_pin_, this->sdo_pin_, this->sdi_pin_); // NOLINT + this->clk_pin_->setup(); + this->clk_pin_->digital_write(true); + this->sdo_pin_->setup(); + this->sdi_pin_->setup(); } } + void SPIComponent::dump_config() { ESP_LOGCONFIG(TAG, "SPI bus:"); - LOG_PIN(" CLK Pin: ", this->clk_); - LOG_PIN(" MISO Pin: ", this->miso_); - LOG_PIN(" MOSI Pin: ", this->mosi_); -#ifdef USE_SPI_ARDUINO_BACKEND - ESP_LOGCONFIG(TAG, " Using HW SPI: %s", YESNO(this->hw_spi_ != nullptr)); -#endif // USE_SPI_ARDUINO_BACKEND -} -float SPIComponent::get_setup_priority() const { return setup_priority::BUS; } - -void SPIComponent::cycle_clock_(bool value) { - uint32_t start = arch_get_cpu_cycle_count(); - while (start - arch_get_cpu_cycle_count() < this->wait_cycle_) - ; - this->clk_->digital_write(value); - start += this->wait_cycle_; - while (start - arch_get_cpu_cycle_count() < this->wait_cycle_) - ; + 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]); + } + if (this->spi_bus_->is_hw()) { + ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_); + } else { + ESP_LOGCONFIG(TAG, " Using software SPI"); + } } -// NOLINTNEXTLINE -#ifndef CLANG_TIDY -#pragma GCC optimize("unroll-loops") -// NOLINTNEXTLINE -#pragma GCC optimize("O2") -#endif // CLANG_TIDY +uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); } -template -uint8_t HOT SPIComponent::transfer_(uint8_t data) { +void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); } + +uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { // Clock starts out at idle level - this->clk_->digital_write(CLOCK_POLARITY); + this->clk_pin_->digital_write(clock_polarity_); uint8_t out_data = 0; - for (uint8_t i = 0; i < 8; i++) { + for (uint8_t i = 0; i != num_bits; i++) { uint8_t shift; - if (BIT_ORDER == BIT_ORDER_MSB_FIRST) { - shift = 7 - i; + if (bit_order_ == BIT_ORDER_MSB_FIRST) { + shift = num_bits - 1 - i; } else { shift = i; } - if (CLOCK_PHASE == CLOCK_PHASE_LEADING) { + if (clock_phase_ == CLOCK_PHASE_LEADING) { // sampling on leading edge - if (WRITE) { - this->mosi_->digital_write(data & (1 << shift)); - } - - // SAMPLE! - this->cycle_clock_(!CLOCK_POLARITY); - - if (READ) { - out_data |= uint8_t(this->miso_->digital_read()) << shift; - } - - this->cycle_clock_(CLOCK_POLARITY); + this->sdo_pin_->digital_write(data & (1 << shift)); + this->cycle_clock_(); + out_data |= uint16_t(this->sdi_pin_->digital_read()) << shift; + this->clk_pin_->digital_write(!this->clock_polarity_); + this->cycle_clock_(); + this->clk_pin_->digital_write(this->clock_polarity_); } else { // sampling on trailing edge - this->cycle_clock_(!CLOCK_POLARITY); - - if (WRITE) { - this->mosi_->digital_write(data & (1 << shift)); - } - - // SAMPLE! - this->cycle_clock_(CLOCK_POLARITY); - - if (READ) { - out_data |= uint8_t(this->miso_->digital_read()) << shift; - } + this->cycle_clock_(); + this->clk_pin_->digital_write(!this->clock_polarity_); + this->sdo_pin_->digital_write(data & (1 << shift)); + this->cycle_clock_(); + out_data |= uint16_t(this->sdi_pin_->digital_read()) << shift; + this->clk_pin_->digital_write(this->clock_polarity_); } } - App.feed_wdt(); - return out_data; } -// Generate with (py3): -// -// from itertools import product -// bit_orders = ['BIT_ORDER_LSB_FIRST', 'BIT_ORDER_MSB_FIRST'] -// clock_pols = ['CLOCK_POLARITY_LOW', 'CLOCK_POLARITY_HIGH'] -// clock_phases = ['CLOCK_PHASE_LEADING', 'CLOCK_PHASE_TRAILING'] -// reads = [False, True] -// writes = [False, True] -// cpp_bool = {False: 'false', True: 'true'} -// for b, cpol, cph, r, w in product(bit_orders, clock_pols, clock_phases, reads, writes): -// if not r and not w: -// continue -// print(f"template uint8_t SPIComponent::transfer_<{b}, {cpol}, {cph}, {cpp_bool[r]}, {cpp_bool[w]}>(uint8_t -// data);") - -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); - } // namespace spi } // namespace esphome diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index bacdad723b..4cd8d3383c 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -1,17 +1,36 @@ #pragma once +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include +#include #include #ifdef USE_ARDUINO -#define USE_SPI_ARDUINO_BACKEND -#endif -#ifdef USE_SPI_ARDUINO_BACKEND #include + +#ifdef USE_RP2040 +using SPIInterface = SPIClassRP2040 *; +#else +using SPIInterface = SPIClass *; #endif +#endif + +#ifdef USE_ESP_IDF + +#include "driver/spi_master.h" + +using SPIInterface = spi_host_device_t; + +#endif // USE_ESP_IDF + +/** + * Implementation of SPI Controller mode. + */ namespace esphome { namespace spi { @@ -48,10 +67,19 @@ enum SPIClockPhase { /// The data is sampled on a trailing clock edge. (CPHA=1) CLOCK_PHASE_TRAILING, }; -/** The SPI clock signal data rate. This defines for what duration the clock signal is HIGH/LOW. - * So effectively the rate of bytes can be calculated using + +/** + * Modes mapping to clock phase and polarity. * - * effective_byte_rate = spi_data_rate / 16 + */ + +enum SPIMode { + MODE0 = 0, + MODE1 = 1, + MODE2 = 2, + MODE3 = 3, +}; +/** The SPI clock signal frequency, which determines the transfer bit rate/second. * * Implementations can use the pre-defined constants here, or use an integer in the template definition * to manually use a specific data rate. @@ -63,276 +91,386 @@ enum SPIDataRate : uint32_t { DATA_RATE_1MHZ = 1000000, DATA_RATE_2MHZ = 2000000, DATA_RATE_4MHZ = 4000000, + DATA_RATE_5MHZ = 5000000, DATA_RATE_8MHZ = 8000000, DATA_RATE_10MHZ = 10000000, DATA_RATE_20MHZ = 20000000, DATA_RATE_40MHZ = 40000000, + DATA_RATE_80MHZ = 80000000, }; +/** + * A pin to replace those that don't exist. + */ +class NullPin : public GPIOPin { + friend class SPIComponent; + + friend class SPIDelegate; + + friend class Utility; + + public: + void setup() override {} + + 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 { return std::string(); } + + protected: + static GPIOPin *const NULL_PIN; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + // https://bugs.llvm.org/show_bug.cgi?id=48040 +}; + +class Utility { + public: + static int get_pin_no(GPIOPin *pin) { + if (pin == nullptr || !pin->is_internal()) + return -1; + if (((InternalGPIOPin *) pin)->is_inverted()) + return -1; + return ((InternalGPIOPin *) pin)->get_pin(); + } + + static SPIMode get_mode(SPIClockPolarity polarity, SPIClockPhase phase) { + if (polarity == CLOCK_POLARITY_HIGH) { + return phase == CLOCK_PHASE_LEADING ? MODE2 : MODE3; + } + return phase == CLOCK_PHASE_LEADING ? MODE0 : MODE1; + } + + static SPIClockPhase get_phase(SPIMode mode) { + switch (mode) { + case MODE0: + case MODE2: + return CLOCK_PHASE_LEADING; + default: + return CLOCK_PHASE_TRAILING; + } + } + + static SPIClockPolarity get_polarity(SPIMode mode) { + switch (mode) { + case MODE0: + case MODE1: + return CLOCK_POLARITY_LOW; + default: + return CLOCK_POLARITY_HIGH; + } + } +}; + +// represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is +// a thin wrapper over SPIClass. +class SPIDelegate { + friend class SPIClient; + + public: + SPIDelegate() = default; + + SPIDelegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) + : bit_order_(bit_order), data_rate_(data_rate), mode_(mode), cs_pin_(cs_pin) { + if (this->cs_pin_ == nullptr) + this->cs_pin_ = NullPin::NULL_PIN; + this->cs_pin_->setup(); + this->cs_pin_->digital_write(true); + } + + virtual ~SPIDelegate(){}; + + // enable CS if configured. + virtual void begin_transaction() { this->cs_pin_->digital_write(false); } + + // end the transaction + virtual void end_transaction() { this->cs_pin_->digital_write(true); } + + // transfer one byte, return the byte that was read. + virtual uint8_t transfer(uint8_t data) = 0; + + // transfer a buffer, replace the contents with read data + virtual void transfer(uint8_t *ptr, size_t length) { this->transfer(ptr, ptr, length); } + + virtual void transfer(const uint8_t *txbuf, uint8_t *rxbuf, size_t length) { + for (size_t i = 0; i != length; i++) + rxbuf[i] = this->transfer(txbuf[i]); + } + + /** + * write a variable length data item, up to 16 bits. + * @param data The data to send. Should be LSB-aligned (i.e. top bits will be discarded.) + * @param num_bits The number of bits to send + */ + virtual void write(uint16_t data, size_t num_bits) { + esph_log_e("spi_device", "variable length write not implemented"); + } + + virtual void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, + const uint8_t *data, size_t length, uint8_t bus_width) { + esph_log_e("spi_device", "write_cmd_addr_data not implemented"); + } + // write 16 bits + virtual void write16(uint16_t data) { + if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { + uint16_t buffer; + buffer = (data >> 8) | (data << 8); + this->write_array(reinterpret_cast(&buffer), 2); + } else { + this->write_array(reinterpret_cast(&data), 2); + } + } + + virtual void write_array16(const uint16_t *data, size_t length) { + for (size_t i = 0; i != length; i++) { + this->write16(data[i]); + } + } + + // write the contents of a buffer, ignore read data (buffer is unchanged.) + virtual void write_array(const uint8_t *ptr, size_t length) { + for (size_t i = 0; i != length; i++) + this->transfer(ptr[i]); + } + + // read into a buffer, write nulls + virtual void read_array(uint8_t *ptr, size_t length) { + for (size_t i = 0; i != length; i++) + ptr[i] = this->transfer(0); + } + + // check if device is ready + virtual bool is_ready(); + + protected: + SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST}; + uint32_t data_rate_{1000000}; + SPIMode mode_{MODE0}; + GPIOPin *cs_pin_{NullPin::NULL_PIN}; +}; + +/** + * An implementation of SPI that relies only on software toggling of pins. + * + */ +class SPIDelegateBitBash : public SPIDelegate { + public: + SPIDelegateBitBash(uint32_t clock, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, GPIOPin *clk_pin, + GPIOPin *sdo_pin, GPIOPin *sdi_pin) + : SPIDelegate(clock, bit_order, mode, cs_pin), clk_pin_(clk_pin), sdo_pin_(sdo_pin), sdi_pin_(sdi_pin) { + // this calculation is pretty meaningless except at very low bit rates. + this->wait_cycle_ = uint32_t(arch_get_cpu_freq_hz()) / this->data_rate_ / 2ULL; + this->clock_polarity_ = Utility::get_polarity(this->mode_); + this->clock_phase_ = Utility::get_phase(this->mode_); + } + + uint8_t transfer(uint8_t data) override; + + void write(uint16_t data, size_t num_bits) override; + + void write16(uint16_t data) override { this->write(data, 16); }; + + protected: + GPIOPin *clk_pin_; + GPIOPin *sdo_pin_; + GPIOPin *sdi_pin_; + uint32_t last_transition_{0}; + uint32_t wait_cycle_; + SPIClockPolarity clock_polarity_; + SPIClockPhase clock_phase_; + + void HOT cycle_clock_() { + while (this->last_transition_ - arch_get_cpu_cycle_count() < this->wait_cycle_) + continue; + this->last_transition_ += this->wait_cycle_; + } + uint16_t transfer_(uint16_t data, size_t num_bits); +}; + +class SPIBus { + public: + SPIBus() = default; + + SPIBus(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) : clk_pin_(clk), sdo_pin_(sdo), sdi_pin_(sdi) {} + + virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) { + return new SPIDelegateBitBash(data_rate, bit_order, mode, cs_pin, this->clk_pin_, this->sdo_pin_, this->sdi_pin_); + } + + virtual bool is_hw() { return false; } + + protected: + GPIOPin *clk_pin_{}; + GPIOPin *sdo_pin_{}; + GPIOPin *sdi_pin_{}; +}; + +class SPIClient; + class SPIComponent : public Component { public: - void set_clk(GPIOPin *clk) { clk_ = clk; } - void set_miso(GPIOPin *miso) { miso_ = miso; } - void set_mosi(GPIOPin *mosi) { mosi_ = mosi; } - void set_force_sw(bool force_sw) { force_sw_ = force_sw; } + SPIDelegate *register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, + GPIOPin *cs_pin); + void unregister_device(SPIClient *device); + + void set_clk(GPIOPin *clk) { this->clk_pin_ = clk; } + + void set_miso(GPIOPin *sdi) { this->sdi_pin_ = sdi; } + + void set_mosi(GPIOPin *sdo) { this->sdo_pin_ = sdo; } + void set_data_pins(std::vector pins) { this->data_pins_ = std::move(pins); } + + void set_interface(SPIInterface interface) { + this->interface_ = interface; + this->using_hw_ = true; + } + + void set_interface_name(const char *name) { this->interface_name_ = name; } + + float get_setup_priority() const override { return setup_priority::BUS; } void setup() override; - void dump_config() override; - template uint8_t read_byte() { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - return this->hw_spi_->transfer(0x00); - } -#endif // USE_SPI_ARDUINO_BACKEND - return this->transfer_(0x00); - } - - template - void read_array(uint8_t *data, size_t length) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - this->hw_spi_->transfer(data, length); - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - for (size_t i = 0; i < length; i++) { - data[i] = this->read_byte(); - } - } - - template - void write_byte(uint8_t data) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { -#ifdef USE_RP2040 - this->hw_spi_->transfer(data); -#else - this->hw_spi_->write(data); -#endif - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - this->transfer_(data); - } - - template - void write_byte16(const uint16_t data) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { -#ifdef USE_RP2040 - this->hw_spi_->transfer16(data); -#else - this->hw_spi_->write16(data); -#endif - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - - this->write_byte(data >> 8); - this->write_byte(data); - } - - template - void write_array16(const uint16_t *data, size_t length) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - for (size_t i = 0; i < length; i++) { -#ifdef USE_RP2040 - this->hw_spi_->transfer16(data[i]); -#else - this->hw_spi_->write16(data[i]); -#endif - } - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - for (size_t i = 0; i < length; i++) { - this->write_byte16(data[i]); - } - } - - template - void write_array(const uint8_t *data, size_t length) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - auto *data_c = const_cast(data); -#ifdef USE_RP2040 - this->hw_spi_->transfer(data_c, length); -#else - this->hw_spi_->writeBytes(data_c, length); -#endif - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - for (size_t i = 0; i < length; i++) { - this->write_byte(data[i]); - } - } - - template - uint8_t transfer_byte(uint8_t data) { - if (this->miso_ != nullptr) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - return this->hw_spi_->transfer(data); - } else { -#endif // USE_SPI_ARDUINO_BACKEND - return this->transfer_(data); -#ifdef USE_SPI_ARDUINO_BACKEND - } -#endif // USE_SPI_ARDUINO_BACKEND - } - this->write_byte(data); - return 0; - } - - template - void transfer_array(uint8_t *data, size_t length) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - if (this->miso_ != nullptr) { - this->hw_spi_->transfer(data, length); - } else { -#ifdef USE_RP2040 - this->hw_spi_->transfer(data, length); -#else - this->hw_spi_->writeBytes(data, length); -#endif - } - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - - if (this->miso_ != nullptr) { - for (size_t i = 0; i < length; i++) { - data[i] = this->transfer_byte(data[i]); - } - } else { - this->write_array(data, length); - } - } - - template - void enable(GPIOPin *cs) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - uint8_t data_mode = SPI_MODE0; - if (!CLOCK_POLARITY && CLOCK_PHASE) { - data_mode = SPI_MODE1; - } else if (CLOCK_POLARITY && !CLOCK_PHASE) { - data_mode = SPI_MODE2; - } else if (CLOCK_POLARITY && CLOCK_PHASE) { - data_mode = SPI_MODE3; - } -#ifdef USE_RP2040 - SPISettings settings(DATA_RATE, static_cast(BIT_ORDER), data_mode); -#else - SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); -#endif - this->hw_spi_->beginTransaction(settings); - } else { -#endif // USE_SPI_ARDUINO_BACKEND - this->clk_->digital_write(CLOCK_POLARITY); - uint32_t cpu_freq_hz = arch_get_cpu_freq_hz(); - this->wait_cycle_ = uint32_t(cpu_freq_hz) / DATA_RATE / 2ULL; -#ifdef USE_SPI_ARDUINO_BACKEND - } -#endif // USE_SPI_ARDUINO_BACKEND - - if (cs != nullptr) { - this->active_cs_ = cs; - this->active_cs_->digital_write(false); - } - } - - void disable(); - - float get_setup_priority() const override; - protected: - inline void cycle_clock_(bool value); + GPIOPin *clk_pin_{nullptr}; + GPIOPin *sdi_pin_{nullptr}; + GPIOPin *sdo_pin_{nullptr}; + std::vector data_pins_{}; - template - uint8_t transfer_(uint8_t data); + SPIInterface interface_{}; + bool using_hw_{false}; + const char *interface_name_{nullptr}; + SPIBus *spi_bus_{}; + std::map devices_; - GPIOPin *clk_; - GPIOPin *miso_{nullptr}; - GPIOPin *mosi_{nullptr}; - GPIOPin *active_cs_{nullptr}; - bool force_sw_{false}; -#ifdef USE_SPI_ARDUINO_BACKEND - SPIClass *hw_spi_{nullptr}; -#endif // USE_SPI_ARDUINO_BACKEND - uint32_t wait_cycle_; + static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins); }; -template -class SPIDevice { +using QuadSPIComponent = SPIComponent; +/** + * Base class for SPIDevice, un-templated. + */ +class SPIClient { public: - SPIDevice() = default; - SPIDevice(SPIComponent *parent, GPIOPin *cs) : parent_(parent), cs_(cs) {} + SPIClient(SPIBitOrder bit_order, SPIMode mode, uint32_t data_rate) + : bit_order_(bit_order), mode_(mode), data_rate_(data_rate) {} - void set_spi_parent(SPIComponent *parent) { parent_ = parent; } - void set_cs_pin(GPIOPin *cs) { cs_ = cs; } - - void spi_setup() { - if (this->cs_) { - this->cs_->setup(); - this->cs_->digital_write(true); - } + virtual void spi_setup() { + esph_log_d("spi_device", "mode %u, data_rate %ukHz", (unsigned) this->mode_, (unsigned) (this->data_rate_ / 1000)); + this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_); } - void enable() { this->parent_->template enable(this->cs_); } - - void disable() { this->parent_->disable(); } - - uint8_t read_byte() { return this->parent_->template read_byte(); } - - void read_array(uint8_t *data, size_t length) { - return this->parent_->template read_array(data, length); + virtual void spi_teardown() { + this->parent_->unregister_device(this); + this->delegate_ = nullptr; } - template std::array read_array() { - std::array data; - this->read_array(data.data(), N); - return data; + bool spi_is_ready() { return this->delegate_->is_ready(); } + + protected: + SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST}; + SPIMode mode_{MODE0}; + uint32_t data_rate_{1000000}; + SPIComponent *parent_{nullptr}; + GPIOPin *cs_{nullptr}; + SPIDelegate *delegate_{nullptr}; +}; + +/** + * The SPIDevice is what components using the SPI will create. + * + * @tparam BIT_ORDER + * @tparam CLOCK_POLARITY + * @tparam CLOCK_PHASE + * @tparam DATA_RATE + */ +template +class SPIDevice : public SPIClient { + public: + SPIDevice() : SPIClient(BIT_ORDER, Utility::get_mode(CLOCK_POLARITY, CLOCK_PHASE), DATA_RATE) {} + + SPIDevice(SPIComponent *parent, GPIOPin *cs_pin) { + this->set_spi_parent(parent); + this->set_cs_pin(cs_pin); } - void write_byte(uint8_t data) { - return this->parent_->template write_byte(data); + void spi_setup() override { SPIClient::spi_setup(); } + + void spi_teardown() override { SPIClient::spi_teardown(); } + + void set_spi_parent(SPIComponent *parent) { this->parent_ = parent; } + + void set_cs_pin(GPIOPin *cs) { this->cs_ = cs; } + + void set_data_rate(uint32_t data_rate) { this->data_rate_ = data_rate; } + + void set_bit_order(SPIBitOrder order) { this->bit_order_ = order; } + + void set_mode(SPIMode mode) { this->mode_ = mode; } + + uint8_t read_byte() { return this->delegate_->transfer(0); } + + void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); } + + /** + * Write a single data item, up to 32 bits. + * @param data The data + * @param num_bits The number of bits to write. The lower num_bits of data will be sent. + */ + void write(uint16_t data, size_t num_bits) { this->delegate_->write(data, num_bits); }; + + /* Write command, address and data. Command and address will be written as single-bit SPI, + * data phase can be multiple bit (currently only 1 or 4) + * @param cmd_bits Number of bits to write in the command phase + * @param cmd The command value to write + * @param addr_bits Number of bits to write in addr phase + * @param address Address data + * @param data Plain data bytes + * @param length Number of data bytes + * @param bus_width The number of data lines to use for the data phase. + */ + void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data, + size_t length, uint8_t bus_width = 1) { + this->delegate_->write_cmd_addr_data(cmd_bits, cmd, addr_bits, address, data, length, bus_width); } - void write_byte16(uint16_t data) { - return this->parent_->template write_byte16(data); - } + void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); } - void write_array16(const uint16_t *data, size_t length) { - this->parent_->template write_array16(data, length); - } + /** + * Write the array data, replace with received data. + * @param data + * @param length + */ + void transfer_array(uint8_t *data, size_t length) { this->delegate_->transfer(data, length); } - void write_array(const uint8_t *data, size_t length) { - this->parent_->template write_array(data, length); - } + uint8_t transfer_byte(uint8_t data) { return this->delegate_->transfer(data); } + + /** Write 16 bit data. The driver will byte-swap if required. + */ + void write_byte16(uint16_t data) { this->delegate_->write16(data); } + + /** + * Write an array of data as 16 bit values, byte-swapping if required. Use of this should be avoided as + * it is horribly slow. + * @param data + * @param length + */ + void write_array16(const uint16_t *data, size_t length) { this->delegate_->write_array16(data, length); } + + void enable() { this->delegate_->begin_transaction(); } + + void disable() { this->delegate_->end_transaction(); } + + void write_array(const uint8_t *data, size_t length) { this->delegate_->write_array(data, length); } template void write_array(const std::array &data) { this->write_array(data.data(), N); } void write_array(const std::vector &data) { this->write_array(data.data(), data.size()); } - uint8_t transfer_byte(uint8_t data) { - return this->parent_->template transfer_byte(data); - } - - void transfer_array(uint8_t *data, size_t length) { - this->parent_->template transfer_array(data, length); - } - template void transfer_array(std::array &data) { this->transfer_array(data.data(), N); } - - protected: - SPIComponent *parent_{nullptr}; - GPIOPin *cs_{nullptr}; }; } // namespace spi diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp new file mode 100644 index 0000000000..f7fe523a33 --- /dev/null +++ b/esphome/components/spi/spi_arduino.cpp @@ -0,0 +1,95 @@ +#include "spi.h" +#include + +namespace esphome { +namespace spi { + +#ifdef USE_ARDUINO + +static const char *const TAG = "spi-esp-arduino"; +class SPIDelegateHw : public SPIDelegate { + public: + SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) + : SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel) {} + + void begin_transaction() override { +#ifdef USE_RP2040 + SPISettings const settings(this->data_rate_, static_cast(this->bit_order_), this->mode_); +#elif defined(ESP8266) + // Arduino ESP8266 library has mangled values for SPI modes :-( + auto mode = (this->mode_ & 0x01) + ((this->mode_ & 0x02) << 3); + ESP_LOGVV(TAG, "8266 mangled SPI mode 0x%X", mode); + SPISettings const settings(this->data_rate_, this->bit_order_, mode); +#else + SPISettings const settings(this->data_rate_, this->bit_order_, this->mode_); +#endif + this->channel_->beginTransaction(settings); + SPIDelegate::begin_transaction(); + } + + void transfer(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); } + + void end_transaction() override { + this->channel_->endTransaction(); + SPIDelegate::end_transaction(); + } + + uint8_t transfer(uint8_t data) override { return this->channel_->transfer(data); } + + void write16(uint16_t data) override { this->channel_->transfer16(data); } + +#ifdef USE_RP2040 + void write_array(const uint8_t *ptr, size_t length) override { + // avoid overwriting the supplied buffer + uint8_t *rxbuf = new uint8_t[length]; // NOLINT(cppcoreguidelines-owning-memory) + memcpy(rxbuf, ptr, length); + this->channel_->transfer((void *) rxbuf, length); + delete[] rxbuf; // NOLINT(cppcoreguidelines-owning-memory) + } +#else + void write_array(const uint8_t *ptr, size_t length) override { this->channel_->writeBytes(ptr, length); } +#endif + + void read_array(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); } + + protected: + SPIInterface channel_{}; +}; + +class SPIBusHw : public SPIBus { + public: + SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) { +#ifdef USE_ESP8266 + channel->pins(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1); + channel->begin(); +#endif // USE_ESP8266 +#ifdef USE_ESP32 + channel->begin(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1); +#endif +#ifdef USE_RP2040 + if (Utility::get_pin_no(sdi) != -1) + channel->setRX(Utility::get_pin_no(sdi)); + if (Utility::get_pin_no(sdo) != -1) + channel->setTX(Utility::get_pin_no(sdo)); + channel->setSCK(Utility::get_pin_no(clk)); + channel->begin(); +#endif + } + + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { + return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin); + } + + protected: + SPIInterface channel_{}; + bool is_hw() override { return true; } +}; + +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins) { + return new SPIBusHw(clk, sdo, sdi, interface); +} + +#endif // USE_ARDUINO +} // namespace spi +} // namespace esphome diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp new file mode 100644 index 0000000000..55680f72d3 --- /dev/null +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -0,0 +1,244 @@ +#include "spi.h" +#include + +namespace esphome { +namespace spi { + +#ifdef USE_ESP_IDF +static const char *const TAG = "spi-esp-idf"; +static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API. + +class SPIDelegateHw : public SPIDelegate { + public: + SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool write_only) + : SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel), write_only_(write_only) { + spi_device_interface_config_t config = {}; + config.mode = static_cast(mode); + config.clock_speed_hz = static_cast(data_rate); + config.spics_io_num = -1; + config.flags = 0; + config.queue_size = 1; + config.pre_cb = nullptr; + config.post_cb = nullptr; + if (bit_order == BIT_ORDER_LSB_FIRST) + config.flags |= SPI_DEVICE_BIT_LSBFIRST; + if (write_only) + config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; + esp_err_t const err = spi_bus_add_device(channel, &config, &this->handle_); + if (err != ESP_OK) + ESP_LOGE(TAG, "Add device failed - err %X", err); + } + + bool is_ready() override { return this->handle_ != nullptr; } + + void begin_transaction() override { + if (this->is_ready()) { + if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK) + ESP_LOGE(TAG, "Failed to acquire SPI bus"); + SPIDelegate::begin_transaction(); + } else { + ESP_LOGW(TAG, "spi_setup called before initialisation"); + } + } + + void end_transaction() override { + if (this->is_ready()) { + SPIDelegate::end_transaction(); + spi_device_release_bus(this->handle_); + } + } + + ~SPIDelegateHw() override { + esp_err_t const err = spi_bus_remove_device(this->handle_); + if (err != ESP_OK) + ESP_LOGE(TAG, "Remove device failed - err %X", err); + } + + // do a transfer. either txbuf or rxbuf (but not both) may be null. + // transfers above the maximum size will be split. + // TODO - make use of the queue for interrupt transfers to provide a (short) pipeline of blocks + // when splitting is required. + void transfer(const uint8_t *txbuf, uint8_t *rxbuf, size_t length) override { + if (rxbuf != nullptr && this->write_only_) { + ESP_LOGE(TAG, "Attempted read from write-only channel"); + return; + } + spi_transaction_t desc = {}; + desc.flags = 0; + while (length != 0) { + size_t const partial = std::min(length, MAX_TRANSFER_SIZE); + desc.length = partial * 8; + desc.rxlength = this->write_only_ ? 0 : partial * 8; + desc.tx_buffer = txbuf; + desc.rx_buffer = rxbuf; + // polling is used as it has about 10% less overhead than queuing an interrupt transfer + esp_err_t err = spi_device_polling_start(this->handle_, &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Transmit failed - err %X", err); + break; + } + length -= partial; + if (txbuf != nullptr) + txbuf += partial; + if (rxbuf != nullptr) + rxbuf += partial; + } + } + + void write(uint16_t data, size_t num_bits) override { + spi_transaction_ext_t desc = {}; + desc.command_bits = num_bits; + desc.base.flags = SPI_TRANS_VARIABLE_CMD; + desc.base.cmd = data; + esp_err_t err = spi_device_polling_start(this->handle_, (spi_transaction_t *) &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } + + if (err != ESP_OK) { + ESP_LOGE(TAG, "Transmit failed - err %X", err); + } + } + + /** + * Write command, address and data + * @param cmd_bits Number of bits to write in the command phase + * @param cmd The command value to write + * @param addr_bits Number of bits to write in addr phase + * @param address Address data + * @param data Remaining data bytes + * @param length Number of data bytes + * @param bus_width The number of data lines to use + */ + void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data, + size_t length, uint8_t bus_width) override { + spi_transaction_ext_t desc = {}; + if (length == 0 && cmd_bits == 0 && addr_bits == 0) { + esph_log_w(TAG, "Nothing to transfer"); + return; + } + desc.base.flags = SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_DUMMY; + if (bus_width == 4) { + desc.base.flags |= SPI_TRANS_MODE_QIO; + } else if (bus_width == 8) { + desc.base.flags |= SPI_TRANS_MODE_OCT; + } + desc.command_bits = cmd_bits; + desc.address_bits = addr_bits; + desc.dummy_bits = 0; + desc.base.rxlength = 0; + desc.base.cmd = cmd; + desc.base.addr = address; + do { + size_t chunk_size = std::min(length, MAX_TRANSFER_SIZE); + if (data != nullptr && chunk_size != 0) { + desc.base.length = chunk_size * 8; + desc.base.tx_buffer = data; + length -= chunk_size; + data += chunk_size; + } else { + length = 0; + desc.base.length = 0; + } + esp_err_t err = spi_device_polling_start(this->handle_, (spi_transaction_t *) &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Transmit failed - err %X", err); + return; + } + // if more data is to be sent, skip the command and address phases. + desc.command_bits = 0; + desc.address_bits = 0; + } while (length != 0); + } + + void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); } + + uint8_t transfer(uint8_t data) override { + uint8_t rxbuf; + this->transfer(&data, &rxbuf, 1); + return rxbuf; + } + + void write16(uint16_t data) override { this->write(data, 16); } + + void write_array(const uint8_t *ptr, size_t length) override { this->transfer(ptr, nullptr, length); } + + void write_array16(const uint16_t *data, size_t length) override { + if (this->bit_order_ == BIT_ORDER_LSB_FIRST) { + this->write_array((uint8_t *) data, length * 2); + } else { + uint16_t buffer[MAX_TRANSFER_SIZE / 2]; + while (length != 0) { + size_t const partial = std::min(length, MAX_TRANSFER_SIZE / 2); + for (size_t i = 0; i != partial; i++) { + buffer[i] = SPI_SWAP_DATA_TX(*data++, 16); + } + this->write_array((const uint8_t *) buffer, partial * 2); + length -= partial; + } + } + } + + void read_array(uint8_t *ptr, size_t length) override { this->transfer(nullptr, ptr, length); } + + protected: + SPIInterface channel_{}; + spi_device_handle_t handle_{}; + bool write_only_{false}; +}; + +class SPIBusHw : public SPIBus { + public: + SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel, std::vector data_pins) + : SPIBus(clk, sdo, sdi), channel_(channel) { + spi_bus_config_t buscfg = {}; + buscfg.sclk_io_num = Utility::get_pin_no(clk); + buscfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_SCLK; + if (data_pins.empty()) { + buscfg.mosi_io_num = Utility::get_pin_no(sdo); + buscfg.miso_io_num = Utility::get_pin_no(sdi); + buscfg.quadwp_io_num = -1; + buscfg.quadhd_io_num = -1; + } else { + buscfg.data0_io_num = data_pins[0]; + buscfg.data1_io_num = data_pins[1]; + buscfg.data2_io_num = data_pins[2]; + buscfg.data3_io_num = data_pins[3]; + buscfg.data4_io_num = -1; + buscfg.data5_io_num = -1; + buscfg.data6_io_num = -1; + buscfg.data7_io_num = -1; + buscfg.flags |= SPICOMMON_BUSFLAG_QUAD; + } + buscfg.max_transfer_sz = MAX_TRANSFER_SIZE; + auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO); + if (err != ESP_OK) + ESP_LOGE(TAG, "Bus init failed - err %X", err); + } + + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { + return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin, + Utility::get_pin_no(this->sdi_pin_) == -1); + } + + protected: + SPIInterface channel_{}; + + bool is_hw() override { return true; } +}; + +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins) { + return new SPIBusHw(clk, sdo, sdi, interface, data_pins); +} + +#endif +} // namespace spi +} // namespace esphome diff --git a/esphome/components/spi_device/__init__.py b/esphome/components/spi_device/__init__.py new file mode 100644 index 0000000000..2f23d8a011 --- /dev/null +++ b/esphome/components/spi_device/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +from esphome.components import spi +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MODE + +DEPENDENCIES = ["spi"] +CODEOWNERS = ["@clydebarrow"] + +MULTI_CONF = True +spi_device_ns = cg.esphome_ns.namespace("spi_device") + +spi_device = spi_device_ns.class_("SPIDeviceComponent", cg.Component, spi.SPIDevice) + +BitOrder = spi.spi_ns.enum("SPIBitOrder") +ORDERS = { + "msb_first": BitOrder.BIT_ORDER_MSB_FIRST, + "lsb_first": BitOrder.BIT_ORDER_LSB_FIRST, +} +CONF_BIT_ORDER = "bit_order" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(spi_device), + cv.Optional(CONF_BIT_ORDER, default="msb_first"): cv.enum(ORDERS, lower=True), + cv.Optional(CONF_MODE): cv.invalid( + "The 'mode' option has been renamed to 'spi_mode'." + ), + } +).extend(spi.spi_device_schema(False, "1MHz")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_bit_order(config[CONF_BIT_ORDER])) + await spi.register_spi_device(var, config) diff --git a/esphome/components/spi_device/spi_device.cpp b/esphome/components/spi_device/spi_device.cpp new file mode 100644 index 0000000000..1f579cb802 --- /dev/null +++ b/esphome/components/spi_device/spi_device.cpp @@ -0,0 +1,31 @@ +#include "spi_device.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace spi_device { + +static const char *const TAG = "spi_device"; + +void SPIDeviceComponent::setup() { + ESP_LOGD(TAG, "Setting up SPIDevice..."); + this->spi_setup(); + ESP_LOGCONFIG(TAG, "SPIDevice started!"); +} + +void SPIDeviceComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SPIDevice"); + 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 { + ESP_LOGCONFIG(TAG, " Data rate: %" PRId32 "MHz", this->data_rate_ / 1000000); + } +} + +float SPIDeviceComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace spi_device +} // namespace esphome diff --git a/esphome/components/spi_device/spi_device.h b/esphome/components/spi_device/spi_device.h new file mode 100644 index 0000000000..d8aef440a7 --- /dev/null +++ b/esphome/components/spi_device/spi_device.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace spi_device { + +class SPIDeviceComponent : public Component, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: +}; + +} // namespace spi_device +} // namespace esphome diff --git a/esphome/components/spi_led_strip/__init__.py b/esphome/components/spi_led_strip/__init__.py new file mode 100644 index 0000000000..850a1f6e02 --- /dev/null +++ b/esphome/components/spi_led_strip/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["spi"] diff --git a/esphome/components/spi_led_strip/light.py b/esphome/components/spi_led_strip/light.py new file mode 100644 index 0000000000..78642935de --- /dev/null +++ b/esphome/components/spi_led_strip/light.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light +from esphome.components import spi +from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS + +spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip") +SpiLedStrip = spi_led_strip_ns.class_( + "SpiLedStrip", light.AddressableLight, spi.SPIDevice +) + +CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpiLedStrip), + cv.Optional(CONF_NUM_LEDS, default=1): cv.positive_not_null_int, + } +).extend(spi.spi_device_schema(False, "1MHz")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) + await light.register_light(var, config) + await spi.register_spi_device(var, config) + await cg.register_component(var, config) diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h new file mode 100644 index 0000000000..1b317cdd69 --- /dev/null +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/light/addressable_light.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace spi_led_strip { + +static const char *const TAG = "spi_led_strip"; +class SpiLedStrip : public light::AddressableLight, + public spi::SPIDevice { + public: + void setup() override { this->spi_setup(); } + + int32_t size() const override { return this->num_leds_; } + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + void set_num_leds(uint16_t num_leds) { + this->num_leds_ = num_leds; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_size_ = num_leds * 4 + 8; + this->buf_ = allocator.allocate(this->buffer_size_); + if (this->buf_ == nullptr) { + esph_log_e(TAG, "Failed to allocate buffer of size %u", this->buffer_size_); + this->mark_failed(); + return; + } + + this->effect_data_ = allocator.allocate(num_leds); + if (this->effect_data_ == nullptr) { + esph_log_e(TAG, "Failed to allocate effect data of size %u", num_leds); + this->mark_failed(); + return; + } + memset(this->buf_, 0xFF, this->buffer_size_); + memset(this->buf_, 0, 4); + } + + void dump_config() override { + esph_log_config(TAG, "SPI LED Strip:"); + esph_log_config(TAG, " 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 { + esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000)); + } + } + + void write_state(light::LightState *state) override { + if (this->is_failed()) + return; + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { + char strbuf[49]; + size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3); + memset(strbuf, 0, sizeof(strbuf)); + for (size_t i = 0; i != len; i++) { + sprintf(strbuf + i * 3, "%02X ", this->buf_[i]); + } + esph_log_v(TAG, "write_state: buf = %s", strbuf); + } + this->enable(); + this->write_array(this->buf_, this->buffer_size_); + this->disable(); + } + + void clear_effect_data() override { + for (int i = 0; i < this->size(); i++) + this->effect_data_[i] = 0; + } + + protected: + light::ESPColorView get_view_internal(int32_t index) const override { + size_t pos = index * 4 + 5; + return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr, + this->effect_data_ + index, &this->correction_}; + } + + size_t buffer_size_{}; + uint8_t *effect_data_{nullptr}; + uint8_t *buf_{nullptr}; + uint16_t num_leds_; +}; + +} // namespace spi_led_strip +} // namespace esphome diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index e1d855778a..e5bcf681f4 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -19,6 +19,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, UNIT_MINUTE, UNIT_SECOND, + CONF_SET_ACTION, ) AUTO_LOAD = ["number", "switch"] @@ -46,7 +47,6 @@ CONF_QUEUE_ENABLE_SWITCH = "queue_enable_switch" CONF_REPEAT_NUMBER = "repeat_number" CONF_REVERSE_SWITCH = "reverse_switch" CONF_RUN_DURATION_NUMBER = "run_duration_number" -CONF_SET_ACTION = "set_action" CONF_STANDBY_SWITCH = "standby_switch" CONF_VALVE_NUMBER = "valve_number" CONF_VALVE_OPEN_DELAY = "valve_open_delay" @@ -571,18 +571,12 @@ async def sprinkler_simple_action_to_code(config, action_id, template_arg, args) async def to_code(config): for sprinkler_controller in config: - var = cg.new_Pvariable(sprinkler_controller[CONF_ID]) - - if CONF_NAME in sprinkler_controller: - cg.add(var.set_name(sprinkler_controller[CONF_NAME])) + if len(sprinkler_controller[CONF_VALVES]) > 1: + name = sprinkler_controller[CONF_MAIN_SWITCH][CONF_NAME] else: - if len(sprinkler_controller[CONF_VALVES]) > 1: - name = sprinkler_controller[CONF_MAIN_SWITCH][CONF_NAME] - else: - name = sprinkler_controller[CONF_VALVES][0][CONF_VALVE_SWITCH][ - CONF_NAME - ] - cg.add(var.set_name(name)) + name = sprinkler_controller[CONF_VALVES][0][CONF_VALVE_SWITCH][CONF_NAME] + name = sprinkler_controller.get(CONF_NAME, name) + var = cg.new_Pvariable(sprinkler_controller[CONF_ID], name) await cg.register_component(var, sprinkler_controller) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 095884997c..59565251c3 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -4,6 +4,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #include namespace esphome { @@ -386,12 +387,17 @@ SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this SprinklerValveRunRequestOrigin SprinklerValveRunRequest::request_is_from() { return this->origin_; } -void Sprinkler::setup() { +Sprinkler::Sprinkler() {} +Sprinkler::Sprinkler(const std::string &name) { + // The `name` is needed to set timers up, hence non-default constructor + // replaces `set_name()` method previously existed + this->name_ = name; this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); - this->all_valves_off_(true); } +void Sprinkler::setup() { this->all_valves_off_(true); } + void Sprinkler::loop() { for (auto &p : this->pump_) { p.loop(); @@ -641,7 +647,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == min_str) { + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -723,7 +729,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == min_str) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); @@ -870,7 +876,7 @@ void Sprinkler::queue_valve(optional valve_number, optional ru if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) { SprinklerQueueItem item{valve_number.value(), run_duration.value()}; this->queued_valves_.insert(this->queued_valves_.begin(), item); - ESP_LOGD(TAG, "Valve %u placed into queue with run duration of %u seconds", valve_number.value_or(0), + ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0), run_duration.value_or(0)); } } @@ -949,15 +955,23 @@ void Sprinkler::pause() { this->paused_valve_ = this->active_valve(); this->resume_duration_ = this->time_remaining_active_valve(); this->shutdown(false); - ESP_LOGD(TAG, "Paused valve %u with %u seconds remaining", this->paused_valve_.value_or(0), + ESP_LOGD(TAG, "Paused valve %zu with %" PRIu32 " seconds remaining", this->paused_valve_.value_or(0), this->resume_duration_.value_or(0)); } void Sprinkler::resume() { + if (this->standby()) { + ESP_LOGD(TAG, "resume called but standby is enabled; no action taken"); + return; + } + if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { - ESP_LOGD(TAG, "Resuming valve %u with %u seconds remaining", this->paused_valve_.value_or(0), - this->resume_duration_.value_or(0)); - this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + // Resume only if valve has not been completed yet + if (!this->valve_cycle_complete_(this->paused_valve_.value())) { + ESP_LOGD(TAG, "Resuming valve %zu with %" PRIu32 " seconds remaining", this->paused_valve_.value_or(0), + this->resume_duration_.value_or(0)); + this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + } this->reset_resume(); } else { ESP_LOGD(TAG, "No valve to resume!"); @@ -1376,7 +1390,8 @@ void Sprinkler::load_next_valve_run_request_(const optional first_valve) this->next_req_.set_run_duration( this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); } else if ((this->repeat_count_++ < this->repeat().value_or(0))) { - ESP_LOGD(TAG, "Repeating - starting cycle %u of %u", this->repeat_count_ + 1, this->repeat().value_or(0) + 1); + ESP_LOGD(TAG, "Repeating - starting cycle %" PRIu32 " of %" PRIu32, this->repeat_count_ + 1, + this->repeat().value_or(0) + 1); // if there are repeats remaining and no more valves were left in the cycle, start a new cycle this->prep_full_cycle_(); if (this->next_valve_number_in_cycle_().has_value()) { // this should always succeed here, but just in case... @@ -1407,7 +1422,7 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up 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 %u for %u seconds, cycle %u of %u", + 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); req->set_valve_operator(&vo); @@ -1632,7 +1647,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { this->timer_[timer_index].start_time = millis(); this->timer_[timer_index].active = true; } - ESP_LOGVV(TAG, "Timer %u started for %u sec", static_cast(timer_index), + ESP_LOGVV(TAG, "Timer %zu started for %" PRIu32 " sec", static_cast(timer_index), this->timer_duration_(timer_index) / 1000); } @@ -1671,48 +1686,48 @@ void Sprinkler::sm_timer_callback_() { void Sprinkler::dump_config() { ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_.c_str()); if (this->manual_selection_delay_.has_value()) { - ESP_LOGCONFIG(TAG, " Manual Selection Delay: %u seconds", this->manual_selection_delay_.value_or(0)); + ESP_LOGCONFIG(TAG, " Manual Selection Delay: %" PRIu32 " seconds", this->manual_selection_delay_.value_or(0)); } if (this->repeat().has_value()) { - ESP_LOGCONFIG(TAG, " Repeat Cycles: %u times", this->repeat().value_or(0)); + ESP_LOGCONFIG(TAG, " Repeat Cycles: %" PRIu32 " times", this->repeat().value_or(0)); } if (this->start_delay_) { if (this->start_delay_is_valve_delay_) { - ESP_LOGCONFIG(TAG, " Pump Start Valve Delay: %u seconds", this->start_delay_); + ESP_LOGCONFIG(TAG, " Pump Start Valve Delay: %" PRIu32 " seconds", this->start_delay_); } else { - ESP_LOGCONFIG(TAG, " Pump Start Pump Delay: %u seconds", this->start_delay_); + ESP_LOGCONFIG(TAG, " Pump Start Pump Delay: %" PRIu32 " seconds", this->start_delay_); } } if (this->stop_delay_) { if (this->stop_delay_is_valve_delay_) { - ESP_LOGCONFIG(TAG, " Pump Stop Valve Delay: %u seconds", this->stop_delay_); + ESP_LOGCONFIG(TAG, " Pump Stop Valve Delay: %" PRIu32 " seconds", this->stop_delay_); } else { - ESP_LOGCONFIG(TAG, " Pump Stop Pump Delay: %u seconds", this->stop_delay_); + ESP_LOGCONFIG(TAG, " Pump Stop Pump Delay: %" PRIu32 " seconds", this->stop_delay_); } } if (this->switching_delay_.has_value()) { if (this->valve_overlap_) { - ESP_LOGCONFIG(TAG, " Valve Overlap: %u seconds", this->switching_delay_.value_or(0)); + ESP_LOGCONFIG(TAG, " Valve Overlap: %" PRIu32 " seconds", this->switching_delay_.value_or(0)); } else { - ESP_LOGCONFIG(TAG, " Valve Open Delay: %u seconds", this->switching_delay_.value_or(0)); + ESP_LOGCONFIG(TAG, " Valve Open Delay: %" PRIu32 " seconds", this->switching_delay_.value_or(0)); ESP_LOGCONFIG(TAG, " Pump Switch Off During Valve Open Delay: %s", YESNO(this->pump_switch_off_during_valve_open_delay_)); } } for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { - ESP_LOGCONFIG(TAG, " Valve %u:", valve_number); + ESP_LOGCONFIG(TAG, " Valve %zu:", valve_number); ESP_LOGCONFIG(TAG, " Name: %s", this->valve_name(valve_number)); - ESP_LOGCONFIG(TAG, " Run Duration: %u seconds", this->valve_run_duration(valve_number)); + ESP_LOGCONFIG(TAG, " Run Duration: %" PRIu32 " seconds", this->valve_run_duration(valve_number)); if (this->valve_[valve_number].valve_switch.pulse_duration()) { - ESP_LOGCONFIG(TAG, " Pulse Duration: %u milliseconds", + 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: %u", this->pump_.size()); + ESP_LOGCONFIG(TAG, " Total number of pumps: %zu", this->pump_.size()); } if (!this->valve_.empty()) { - ESP_LOGCONFIG(TAG, " Total number of valves: %u", this->valve_.size()); + ESP_LOGCONFIG(TAG, " Total number of valves: %zu", this->valve_.size()); } } diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index ae7554d3af..c4a8b8aeb8 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -11,7 +11,7 @@ namespace esphome { namespace sprinkler { -const std::string min_str = "min"; +const std::string MIN_STR = "min"; enum SprinklerState : uint8_t { // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! @@ -204,12 +204,12 @@ class SprinklerValveRunRequest { class Sprinkler : public Component { public: + Sprinkler(); + Sprinkler(const std::string &name); void setup() override; void loop() override; void dump_config() override; - void set_name(const std::string &name) { this->name_ = name; } - /// add a valve to the controller void add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw = nullptr); diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index ff8d5a3594..0f01bab514 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -20,7 +20,7 @@ from esphome.const import ( DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, - UNIT_COUNTS_PER_CUBIC_METER, + UNIT_COUNTS_PER_CUBIC_CENTIMETER, UNIT_MICROMETER, ICON_CHEMICAL_WEAPON, ICON_COUNTER, @@ -73,31 +73,31 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( - unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_CENTIMETER, icon=ICON_COUNTER, accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( - unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_CENTIMETER, icon=ICON_COUNTER, accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( - unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_CENTIMETER, icon=ICON_COUNTER, accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_4_0): sensor.sensor_schema( - unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_CENTIMETER, icon=ICON_COUNTER, accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( - unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_CENTIMETER, icon=ICON_COUNTER, accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 48143b9e1a..1fe74dfcb5 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -33,6 +33,7 @@ MODELS = { "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, "SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48, "SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64, + "SH1107_128X128": SSD1306Model.SH1107_MODEL_128_128, "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, "SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64, } @@ -63,15 +64,16 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, cv.Optional(CONF_FLIP_X, default=True): cv.boolean, cv.Optional(CONF_FLIP_Y, default=True): cv.boolean, - cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=-32, max=32), - cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=-32, max=32), + # Offsets determine shifts of memory location to LCD rows/columns, + # and this family of controllers supports up to 128x128 screens + cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=0, max=128), + cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=128), cv.Optional(CONF_INVERT, default=False): cv.boolean, } ).extend(cv.polling_component_schema("1s")) async def setup_ssd1306(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) @@ -96,6 +98,6 @@ async def setup_ssd1306(var, config): cg.add(var.init_invert(config[CONF_INVERT])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 730e1c8f35..90b805a79f 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -35,16 +35,31 @@ static const uint8_t SSD1306_COMMAND_INVERSE_DISPLAY = 0xA7; static const uint8_t SSD1305_COMMAND_SET_BRIGHTNESS = 0x82; static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8; +static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC; +static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD; + void SSD1306::setup() { this->init_internal_(this->get_buffer_length_()); + // SH1107 resources + // + // Datasheet v2.3: + // www.displayfuture.com/Display/datasheet/controller/SH1107.pdf + // Adafruit C++ driver: + // github.com/adafruit/Adafruit_SH110x + // Adafruit CircuitPython driver: + // github.com/adafruit/Adafruit_CircuitPython_DisplayIO_SH1107 + // Turn off display during initialization (0xAE) this->command(SSD1306_COMMAND_DISPLAY_OFF); - // Set oscillator frequency to 4'b1000 with no clock division (0xD5) - this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); - // Oscillator frequency <= 4'b1000, no clock division - this->command(0x80); + // If SH1107, use POR defaults (0x50) = divider 1, frequency +0% + if (!this->is_sh1107_()) { + // Set oscillator frequency to 4'b1000 with no clock division (0xD5) + this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); + // Oscillator frequency <= 4'b1000, no clock division + this->command(0x80); + } // Enable low power display mode for SSD1305 (0xD8) if (this->is_ssd1305_()) { @@ -60,11 +75,26 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y); this->command(0x00 + this->offset_y_); - // Set start line at line 0 (0x40) - this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + if (this->is_sh1107_()) { + // Set start line at line 0 (0xDC) + this->command(SH1107_COMMAND_SET_START_LINE); + this->command(0x00); + } else { + // Set start line at line 0 (0x40) + this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + } - // SSD1305 does not have charge pump - if (!this->is_ssd1305_()) { + if (this->is_ssd1305_()) { + // SSD1305 does not have charge pump + } else if (this->is_sh1107_()) { + // Enable charge pump (0xAD) + this->command(SH1107_COMMAND_CHARGE_PUMP); + if (this->external_vcc_) { + this->command(0x8A); + } else { + this->command(0x8B); + } + } else { // Enable charge pump (0x8D) this->command(SSD1306_COMMAND_CHARGE_PUMP); if (this->external_vcc_) { @@ -76,34 +106,41 @@ void SSD1306::setup() { // Set addressing mode to horizontal (0x20) this->command(SSD1306_COMMAND_MEMORY_MODE); - this->command(0x00); - + if (!this->is_sh1107_()) { + // SH1107 memory mode is a 1 byte command + this->command(0x00); + } // X flip mode (0xA0, 0xA1) this->command(SSD1306_COMMAND_SEGRE_MAP | this->flip_x_); // Y flip mode (0xC0, 0xC8) this->command(SSD1306_COMMAND_COM_SCAN_INC | (this->flip_y_ << 3)); - // Set pin configuration (0xDA) - this->command(SSD1306_COMMAND_SET_COM_PINS); - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - this->command(0x02); - break; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1306_MODEL_64_48: - case SSD1306_MODEL_64_32: - case SH1106_MODEL_64_48: - case SH1107_MODEL_128_64: - case SSD1305_MODEL_128_32: - case SSD1305_MODEL_128_64: - case SSD1306_MODEL_72_40: - this->command(0x12); - break; + if (!this->is_sh1107_()) { + // Set pin configuration (0xDA) + this->command(SSD1306_COMMAND_SET_COM_PINS); + switch (this->model_) { + case SSD1306_MODEL_128_32: + case SH1106_MODEL_128_32: + case SSD1306_MODEL_96_16: + case SH1106_MODEL_96_16: + this->command(0x02); + break; + case SSD1306_MODEL_128_64: + case SH1106_MODEL_128_64: + case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: + case SH1106_MODEL_64_48: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: + case SSD1306_MODEL_72_40: + this->command(0x12); + break; + case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: + // Not used, but prevents build warning + break; + } } // Pre-charge period (0xD9) @@ -117,7 +154,9 @@ void SSD1306::setup() { // Set V_COM (0xDB) this->command(SSD1306_COMMAND_SET_VCOM_DETECT); switch (this->model_) { + case SH1106_MODEL_128_64: case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: this->command(0x35); break; case SSD1306_MODEL_72_40: @@ -132,7 +171,7 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME); // Inverse display mode (0xA6, 0xA7) - this->command(SSD1306_COMMAND_NORMAL_DISPLAY | this->invert_); + this->set_invert(this->invert_); // Disable scrolling mode (0x2E) this->command(SSD1306_COMMAND_DEACTIVATE_SCROLL); @@ -149,7 +188,7 @@ void SSD1306::setup() { this->turn_on(); } void SSD1306::display() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { this->write_display_data(); return; } @@ -183,6 +222,7 @@ bool SSD1306::is_sh1106_() const { return this->model_ == SH1106_MODEL_96_16 || this->model_ == SH1106_MODEL_128_32 || this->model_ == SH1106_MODEL_128_64; } +bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; } bool SSD1306::is_ssd1305_() const { return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64; } @@ -190,6 +230,13 @@ void SSD1306::update() { this->do_update_(); this->display(); } + +void SSD1306::set_invert(bool invert) { + this->invert_ = invert; + // Inverse display mode (0xA6, 0xA7) + this->command(SSD1306_COMMAND_NORMAL_DISPLAY | this->invert_); +} +float SSD1306::get_contrast() { return this->contrast_; }; void SSD1306::set_contrast(float contrast) { // validation this->contrast_ = clamp(contrast, 0.0F, 1.0F); @@ -197,6 +244,7 @@ void SSD1306::set_contrast(float contrast) { this->command(SSD1306_COMMAND_SET_CONTRAST); this->command(int(SSD1306_MAX_CONTRAST * (this->contrast_))); } +float SSD1306::get_brightness() { return this->brightness_; }; void SSD1306::set_brightness(float brightness) { // validation if (!this->is_ssd1305_()) @@ -218,6 +266,7 @@ void SSD1306::turn_off() { int SSD1306::get_height_internal() { switch (this->model_) { case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: return 128; case SSD1306_MODEL_128_32: case SSD1306_MODEL_64_32: @@ -248,6 +297,7 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_128_64: case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_64: + case SH1107_MODEL_128_128: return 128; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 7402ae3af2..14ec309ae0 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -19,11 +19,12 @@ enum SSD1306Model { SH1106_MODEL_96_16, SH1106_MODEL_64_48, SH1107_MODEL_128_64, + SH1107_MODEL_128_128, SSD1305_MODEL_128_32, SSD1305_MODEL_128_64, }; -class SSD1306 : public PollingComponent, public display::DisplayBuffer { +class SSD1306 : public display::DisplayBuffer { public: void setup() override; @@ -35,7 +36,9 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } void init_contrast(float contrast) { this->contrast_ = contrast; } + float get_contrast(); void set_contrast(float contrast); + float get_brightness(); void init_brightness(float brightness) { this->brightness_ = brightness; } void set_brightness(float brightness); void init_flip_x(bool flip_x) { this->flip_x_ = flip_x; } @@ -43,6 +46,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void init_offset_x(uint8_t offset_x) { this->offset_x_ = offset_x; } void init_offset_y(uint8_t offset_y) { this->offset_y_ = offset_y; } void init_invert(bool invert) { this->invert_ = invert; } + void set_invert(bool invert); bool is_on(); void turn_on(); void turn_off(); @@ -57,6 +61,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void init_reset_(); bool is_sh1106_() const; + bool is_sh1107_() const; bool is_ssd1305_() const; void draw_absolute_pixel_internal(int x, int y, Color color) override; diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 96734eb618..ed7cf102ee 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -38,13 +38,19 @@ void I2CSSD1306::dump_config() { } void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); } void HOT I2CSSD1306::write_display_data() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { uint32_t i = 0; for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) { this->command(0xB0 + page); // row - this->command(0x02); // lower column - this->command(0x10); // higher column - + if (this->is_sh1106_()) { + this->command(0x02); // lower column - 0x02 is historical SH1106 value + } else { + // Other SH1107 drivers use 0x00 + // Column values dont change and it seems they can be set only once, + // but we follow SH1106 implementation and resend them + this->command(0x00); + } + this->command(0x10); // higher column for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) { uint8_t data[16]; for (uint8_t &j : data) diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index 0af1168bde..4af41073d4 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -1,8 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1306_base from esphome.components.ssd1306_base import _validate +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES AUTO_LOAD = ["ssd1306_base"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( _validate, ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1306_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 7f025d77cd..0a0debfd65 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -36,10 +36,14 @@ void SPISSD1306::command(uint8_t value) { this->disable(); } void HOT SPISSD1306::write_display_data() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { this->command(0xB0 + y); - this->command(0x02); + if (this->is_sh1106_()) { + this->command(0x02); + } else { + this->command(0x00); + } this->command(0x10); this->dc_pin_->digital_write(true); for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) { diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py index 434caf4e35..471c874986 100644 --- a/esphome/components/ssd1322_base/__init__.py +++ b/esphome/components/ssd1322_base/__init__.py @@ -33,7 +33,6 @@ SSD1322_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1322(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) @@ -46,6 +45,6 @@ async def setup_ssd1322(var, config): cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1322_base/ssd1322_base.h b/esphome/components/ssd1322_base/ssd1322_base.h index d672b298d6..9f4d39976c 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.h +++ b/esphome/components/ssd1322_base/ssd1322_base.h @@ -11,7 +11,7 @@ enum SSD1322Model { SSD1322_MODEL_256_64 = 0, }; -class SSD1322 : public PollingComponent, public display::DisplayBuffer { +class SSD1322 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1322_spi/display.py b/esphome/components/ssd1322_spi/display.py index 88b3a53355..849e71abee 100644 --- a/esphome/components/ssd1322_spi/display.py +++ b/esphome/components/ssd1322_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1322_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1322_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.cpp b/esphome/components/ssd1322_spi/ssd1322_spi.cpp index 50c46c4d02..a841c5606e 100644 --- a/esphome/components/ssd1322_spi/ssd1322_spi.cpp +++ b/esphome/components/ssd1322_spi/ssd1322_spi.cpp @@ -21,8 +21,7 @@ void SPISSD1322::setup() { void SPISSD1322::dump_config() { LOG_DISPLAY("", "SPI SSD1322", this); ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - if (this->cs_) - LOG_PIN(" CS Pin: ", this->cs_); + 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_); diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 68be287d2a..e66cfbc684 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -37,7 +37,6 @@ SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1325(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) @@ -50,6 +49,6 @@ async def setup_ssd1325(var, config): cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index 8ba6a56c8b..ae033e582b 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -15,7 +15,7 @@ enum SSD1325Model { SSD1327_MODEL_128_128, }; -class SSD1325 : public PollingComponent, public display::DisplayBuffer { +class SSD1325 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index a86dc751d5..e18db33c68 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1325_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1325_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index 98f48b8538..8a95bfeae3 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -21,8 +21,7 @@ void SPISSD1325::setup() { void SPISSD1325::dump_config() { LOG_DISPLAY("", "SPI SSD1325", this); ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - if (this->cs_) - LOG_PIN(" CS Pin: ", this->cs_); + 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_); diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py index eada66a6e3..7f2259cf32 100644 --- a/esphome/components/ssd1327_base/__init__.py +++ b/esphome/components/ssd1327_base/__init__.py @@ -26,7 +26,6 @@ SSD1327_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1327(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) @@ -37,6 +36,6 @@ async def setup_ssd1327(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1327_base/ssd1327_base.h b/esphome/components/ssd1327_base/ssd1327_base.h index 5639beb828..207023a3d3 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.h +++ b/esphome/components/ssd1327_base/ssd1327_base.h @@ -11,7 +11,7 @@ enum SSD1327Model { SSD1327_MODEL_128_128 = 0, }; -class SSD1327 : public PollingComponent, public display::DisplayBuffer { +class SSD1327 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1327_spi/display.py b/esphome/components/ssd1327_spi/display.py index 138e85eecd..b622c098ec 100644 --- a/esphome/components/ssd1327_spi/display.py +++ b/esphome/components/ssd1327_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1327_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1327_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.cpp b/esphome/components/ssd1327_spi/ssd1327_spi.cpp index 1dd2b73e66..c6ae377119 100644 --- a/esphome/components/ssd1327_spi/ssd1327_spi.cpp +++ b/esphome/components/ssd1327_spi/ssd1327_spi.cpp @@ -21,8 +21,7 @@ void SPISSD1327::setup() { void SPISSD1327::dump_config() { LOG_DISPLAY("", "SPI SSD1327", this); ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - if (this->cs_) - LOG_PIN(" CS Pin: ", this->cs_); + 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_); diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py index 067f55a252..80162979fc 100644 --- a/esphome/components/ssd1331_base/__init__.py +++ b/esphome/components/ssd1331_base/__init__.py @@ -18,7 +18,6 @@ SSD1331_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1331(var, config): - await cg.register_component(var, config) await display.register_display(var, config) if CONF_RESET_PIN in config: @@ -28,6 +27,6 @@ async def setup_ssd1331(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1331_base/ssd1331_base.h b/esphome/components/ssd1331_base/ssd1331_base.h index be5713f208..719bfc1f8b 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.h +++ b/esphome/components/ssd1331_base/ssd1331_base.h @@ -7,7 +7,7 @@ namespace esphome { namespace ssd1331_base { -class SSD1331 : public PollingComponent, public display::DisplayBuffer { +class SSD1331 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1331_spi/display.py b/esphome/components/ssd1331_spi/display.py index c32ac60578..50895b3175 100644 --- a/esphome/components/ssd1331_spi/display.py +++ b/esphome/components/ssd1331_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1331_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1331_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.cpp b/esphome/components/ssd1331_spi/ssd1331_spi.cpp index ff42c74b9f..88116f6c00 100644 --- a/esphome/components/ssd1331_spi/ssd1331_spi.cpp +++ b/esphome/components/ssd1331_spi/ssd1331_spi.cpp @@ -20,8 +20,7 @@ void SPISSD1331::setup() { } void SPISSD1331::dump_config() { LOG_DISPLAY("", "SPI SSD1331", this); - if (this->cs_) - LOG_PIN(" CS Pin: ", this->cs_); + 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_); diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py index 555d6c5e2e..150d89afed 100644 --- a/esphome/components/ssd1351_base/__init__.py +++ b/esphome/components/ssd1351_base/__init__.py @@ -27,7 +27,6 @@ SSD1351_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1351(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) @@ -38,6 +37,6 @@ async def setup_ssd1351(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp index 036a6a0e82..38ea05a0b8 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.cpp +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -112,6 +112,9 @@ void SSD1351::set_brightness(float brightness) { } else { this->brightness_ = brightness; } + if (!this->is_ready()) { + return; // Component is not yet setup skip the command + } // now write the new brightness level to the display this->command(SSD1351_CONTRASTMASTER); this->data(int(SSD1351_MAX_CONTRAST * (this->brightness_))); diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h index 2f1e0237cd..62777a60a0 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.h +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -12,7 +12,7 @@ enum SSD1351Model { SSD1351_MODEL_128_128, }; -class SSD1351 : public PollingComponent, public display::DisplayBuffer { +class SSD1351 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py index 3f3409226c..bd7033c3d4 100644 --- a/esphome/components/ssd1351_spi/display.py +++ b/esphome/components/ssd1351_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1351_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1351_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.cpp b/esphome/components/ssd1351_spi/ssd1351_spi.cpp index 9599c6e644..b71b8f4f88 100644 --- a/esphome/components/ssd1351_spi/ssd1351_spi.cpp +++ b/esphome/components/ssd1351_spi/ssd1351_spi.cpp @@ -21,8 +21,7 @@ void SPISSD1351::setup() { void SPISSD1351::dump_config() { LOG_DISPLAY("", "SPI SSD1351", this); ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - if (this->cs_) - LOG_PIN(" CS Pin: ", this->cs_); + 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_); diff --git a/esphome/components/st7567_base/__init__.py b/esphome/components/st7567_base/__init__.py new file mode 100644 index 0000000000..7ce50fd99f --- /dev/null +++ b/esphome/components/st7567_base/__init__.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import ( + CONF_LAMBDA, + CONF_RESET_PIN, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_TRANSFORM, + CONF_INVERT_COLORS, +) + +CODEOWNERS = ["@latonita"] + +st7567_base_ns = cg.esphome_ns.namespace("st7567_base") +ST7567 = st7567_base_ns.class_("ST7567", cg.PollingComponent, display.DisplayBuffer) +ST7567Model = st7567_base_ns.enum("ST7567Model") + +# todo in future: reuse following constants from const.py when they are released + + +ST7567_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), + } +).extend(cv.polling_component_schema("1s")) + + +async def setup_st7567(var, config): + await display.register_display(var, config) + + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + cg.add(var.init_invert_colors(config[CONF_INVERT_COLORS])) + + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.init_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.init_mirror_y(transform[CONF_MIRROR_Y])) + + 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_)) diff --git a/esphome/components/st7567_base/st7567_base.cpp b/esphome/components/st7567_base/st7567_base.cpp new file mode 100644 index 0000000000..b22a7d7fd5 --- /dev/null +++ b/esphome/components/st7567_base/st7567_base.cpp @@ -0,0 +1,152 @@ +#include "st7567_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace st7567_base { + +static const char *const TAG = "st7567"; + +void ST7567::setup() { + this->init_internal_(this->get_buffer_length_()); + this->display_init_(); +} + +void ST7567::display_init_() { + ESP_LOGD(TAG, "Initializing ST7567 display..."); + this->display_init_registers_(); + this->clear(); + this->write_display_data(); + this->command(ST7567_DISPLAY_ON); +} + +void ST7567::display_init_registers_() { + this->command(ST7567_BIAS_9); + this->command(this->mirror_x_ ? ST7567_SEG_REVERSE : ST7567_SEG_NORMAL); + this->command(this->mirror_y_ ? ST7567_COM_NORMAL : ST7567_COM_REMAP); + this->command(ST7567_POWER_CTL | 0x4); + this->command(ST7567_POWER_CTL | 0x6); + this->command(ST7567_POWER_CTL | 0x7); + + this->set_brightness(this->brightness_); + this->set_contrast(this->contrast_); + + this->command(ST7567_INVERT_OFF | this->invert_colors_); + + this->command(ST7567_BOOSTER_ON); + this->command(ST7567_REGULATOR_ON); + this->command(ST7567_POWER_ON); + + this->command(ST7567_SCAN_START_LINE); + this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_); +} + +void ST7567::display_sw_refresh_() { + ESP_LOGD(TAG, "Performing refresh sequence..."); + this->command(ST7567_SW_REFRESH); + this->display_init_registers_(); +} + +void ST7567::request_refresh() { + // as per datasheet: It is recommended to use the refresh sequence regularly in a specified interval. + this->refresh_requested_ = true; +} + +void ST7567::update() { + this->do_update_(); + if (this->refresh_requested_) { + this->refresh_requested_ = false; + this->display_sw_refresh_(); + } + this->write_display_data(); +} + +void ST7567::set_all_pixels_on(bool enable) { + this->all_pixels_on_ = enable; + this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_); +} + +void ST7567::set_invert_colors(bool invert_colors) { + this->invert_colors_ = invert_colors; + this->command(ST7567_INVERT_OFF | this->invert_colors_); +} + +void ST7567::set_contrast(uint8_t val) { + this->contrast_ = val & 0b111111; + // 0..63, 26 is normal + + // two byte command + // first byte 0x81 + // second byte 0-63 + + this->command(ST7567_SET_EV_CMD); + this->command(this->contrast_); +} + +void ST7567::set_brightness(uint8_t val) { + this->brightness_ = val & 0b111; + // 0..7, 5 normal + + //********Adjust display brightness******** + // 0x20-0x27 is the internal Rb/Ra resistance + // adjustment setting of V5 voltage RR=4.5V + + this->command(ST7567_RESISTOR_RATIO | this->brightness_); +} + +bool ST7567::is_on() { return this->is_on_; } + +void ST7567::turn_on() { + this->command(ST7567_DISPLAY_ON); + this->is_on_ = true; +} + +void ST7567::turn_off() { + this->command(ST7567_DISPLAY_OFF); + this->is_on_ = false; +} + +void ST7567::set_scroll(uint8_t line) { this->start_line_ = line % this->get_height_internal(); } + +int ST7567::get_width_internal() { return 128; } + +int ST7567::get_height_internal() { return 64; } + +// 128x64, but memory size 132x64, line starts from 0, but if mirrored then it starts from 131, not 127 +size_t ST7567::get_buffer_length_() { + return size_t(this->get_width_internal() + 4) * size_t(this->get_height_internal()) / 8u; +} + +void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + + uint16_t pos = x + (y / 8) * this->get_width_internal(); + uint8_t subpos = y & 0x07; + if (color.is_on()) { + this->buffer_[pos] |= (1 << subpos); + } else { + this->buffer_[pos] &= ~(1 << subpos); + } +} + +void ST7567::fill(Color color) { memset(buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } + +void ST7567::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +const char *ST7567::model_str_() { return "ST7567 128x64"; } + +} // namespace st7567_base +} // namespace esphome diff --git a/esphome/components/st7567_base/st7567_base.h b/esphome/components/st7567_base/st7567_base.h new file mode 100644 index 0000000000..e3053673d1 --- /dev/null +++ b/esphome/components/st7567_base/st7567_base.h @@ -0,0 +1,100 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7567_base { + +static const uint8_t ST7567_BOOSTER_ON = 0x2C; // internal power supply on +static const uint8_t ST7567_REGULATOR_ON = 0x2E; // internal power supply on +static const uint8_t ST7567_POWER_ON = 0x2F; // internal power supply on + +static const uint8_t ST7567_DISPLAY_ON = 0xAF; // Display ON. Normal Display Mode. +static const uint8_t ST7567_DISPLAY_OFF = 0xAE; // Display OFF. All SEGs/COMs output with VSS +static const uint8_t ST7567_SET_START_LINE = 0x40; +static const uint8_t ST7567_POWER_CTL = 0x28; +static const uint8_t ST7567_SEG_NORMAL = 0xA0; // +static const uint8_t ST7567_SEG_REVERSE = 0xA1; // mirror X axis (horizontal) +static const uint8_t ST7567_COM_NORMAL = 0xC0; // +static const uint8_t ST7567_COM_REMAP = 0xC8; // mirror Y axis (vertical) +static const uint8_t ST7567_PIXELS_NORMAL = 0xA4; // display ram content +static const uint8_t ST7567_PIXELS_ALL_ON = 0xA5; // all pixels on +static const uint8_t ST7567_INVERT_OFF = 0xA6; // normal pixels +static const uint8_t ST7567_INVERT_ON = 0xA7; // inverted pixels +static const uint8_t ST7567_SCAN_START_LINE = 0x40; // scrolling = 0x40 + (0..63) +static const uint8_t ST7567_COL_ADDR_H = 0x10; // x pos (0..95) 4 MSB +static const uint8_t ST7567_COL_ADDR_L = 0x00; // x pos (0..95) 4 LSB +static const uint8_t ST7567_PAGE_ADDR = 0xB0; // y pos, 8.5 rows (0..8) +static const uint8_t ST7567_BIAS_9 = 0xA2; +static const uint8_t ST7567_CONTRAST = 0x80; // 0x80 + (0..31) +static const uint8_t ST7567_SET_EV_CMD = 0x81; +static const uint8_t ST7567_SET_EV_PARAM = 0x00; +static const uint8_t ST7567_RESISTOR_RATIO = 0x20; +static const uint8_t ST7567_SW_REFRESH = 0xE2; + +class ST7567 : public display::DisplayBuffer { + public: + void setup() override; + + void update() override; + + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } + void init_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } + void init_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + + void set_invert_colors(bool invert_colors); // inversion of screen colors + void set_contrast(uint8_t val); // 0..63, 27-30 normal + void set_brightness(uint8_t val); // 0..7, 5 normal + void set_all_pixels_on(bool enable); // turn on all pixels, this doesn't affect RAM + void set_scroll(uint8_t line); // set display start line: for screen scrolling w/o affecting RAM + + bool is_on(); + void turn_on(); + void turn_off(); + + void request_refresh(); // from datasheet: It is recommended to use the refresh sequence regularly in a specified + // interval. + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + + void init_reset_(); + void display_init_(); + void display_init_registers_(); + void display_sw_refresh_(); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + + int get_offset_x_() { return mirror_x_ ? 4 : 0; }; + + const char *model_str_(); + + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + // float contrast_{1.0}; + // float brightness_{1.0}; + uint8_t contrast_{27}; + uint8_t brightness_{5}; + bool mirror_x_{true}; + bool mirror_y_{true}; + bool invert_colors_{false}; + bool all_pixels_on_{false}; + uint8_t start_line_{0}; + bool refresh_requested_{false}; +}; + +} // namespace st7567_base +} // namespace esphome diff --git a/esphome/components/st7567_i2c/__init__.py b/esphome/components/st7567_i2c/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/st7567_i2c/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/st7567_i2c/display.py b/esphome/components/st7567_i2c/display.py new file mode 100644 index 0000000000..fa92d652d7 --- /dev/null +++ b/esphome/components/st7567_i2c/display.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import st7567_base, i2c +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@latonita"] + +AUTO_LOAD = ["st7567_base"] +DEPENDENCIES = ["i2c"] + +st7567_i2c = cg.esphome_ns.namespace("st7567_i2c") +I2CST7567 = st7567_i2c.class_("I2CST7567", st7567_base.ST7567, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + st7567_base.ST7567_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2CST7567), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x3F)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await st7567_base.setup_st7567(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp new file mode 100644 index 0000000000..05173d1be5 --- /dev/null +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -0,0 +1,60 @@ +#include "st7567_i2c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7567_i2c { + +static const char *const TAG = "st7567_i2c"; + +void I2CST7567::setup() { + ESP_LOGCONFIG(TAG, "Setting up I2C ST7567 display..."); + this->init_reset_(); + + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + ST7567::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, " 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); + + if (this->error_code_ == COMMUNICATION_FAILED) { + ESP_LOGE(TAG, "Communication with I2C ST7567 failed!"); + } +} + +void I2CST7567::command(uint8_t value) { this->write_byte(0x00, value); } + +void HOT I2CST7567::write_display_data() { + // ST7567A has built-in RAM with 132x65 bit capacity which stores the display data. + // but only first 128 pixels from each line are shown on screen + // if screen got flipped horizontally then it shows last 128 pixels, + // so we need to write x coordinate starting from column 4, not column 0 + this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_); + for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { + this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page + this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address + this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address + + static const size_t BLOCK_SIZE = 64; + for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) { + this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x], + this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x, + true); + } + } +} + +} // namespace st7567_i2c +} // namespace esphome diff --git a/esphome/components/st7567_i2c/st7567_i2c.h b/esphome/components/st7567_i2c/st7567_i2c.h new file mode 100644 index 0000000000..f760a54945 --- /dev/null +++ b/esphome/components/st7567_i2c/st7567_i2c.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/st7567_base/st7567_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace st7567_i2c { + +class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + protected: + void command(uint8_t value) override; + void write_display_data() override; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; +}; + +} // namespace st7567_i2c +} // namespace esphome diff --git a/esphome/components/st7567_spi/__init__.py b/esphome/components/st7567_spi/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/st7567_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/st7567_spi/display.py b/esphome/components/st7567_spi/display.py new file mode 100644 index 0000000000..305aa35024 --- /dev/null +++ b/esphome/components/st7567_spi/display.py @@ -0,0 +1,38 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import spi, st7567_base +import esphome.config_validation as cv +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ["@latonita"] + +AUTO_LOAD = ["st7567_base"] +DEPENDENCIES = ["spi"] + +st7567_spi = cg.esphome_ns.namespace("st7567_spi") +SPIST7567 = st7567_spi.class_("SPIST7567", st7567_base.ST7567, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + st7567_base.ST7567_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SPIST7567), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "st7567_spi", require_miso=False, require_mosi=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await st7567_base.setup_st7567(var, config) + await spi.register_spi_device(var, config) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/st7567_spi/st7567_spi.cpp b/esphome/components/st7567_spi/st7567_spi.cpp new file mode 100644 index 0000000000..25698d0dd0 --- /dev/null +++ b/esphome/components/st7567_spi/st7567_spi.cpp @@ -0,0 +1,66 @@ +#include "st7567_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7567_spi { + +static const char *const TAG = "st7567_spi"; + +void SPIST7567::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI ST7567 display..."); + this->spi_setup(); + this->dc_pin_->setup(); + if (this->cs_) + this->cs_->setup(); + + this->init_reset_(); + ST7567::setup(); +} + +void SPIST7567::dump_config() { + LOG_DISPLAY("", "SPI ST7567", 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, " 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); +} + +void SPIST7567::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +void HOT SPIST7567::write_display_data() { + // ST7567A has built-in RAM with 132x65 bit capacity which stores the display data. + // but only first 128 pixels from each line are shown on screen + // if screen got flipped horizontally then it shows last 128 pixels, + // so we need to write x coordinate starting from column 4, not column 0 + this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_); + for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { + this->dc_pin_->digital_write(false); + this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page + this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address + this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address + this->dc_pin_->digital_write(true); + + this->enable(); + this->write_array(&this->buffer_[y * this->get_width_internal()], this->get_width_internal()); + this->disable(); + } +} + +} // namespace st7567_spi +} // namespace esphome diff --git a/esphome/components/st7567_spi/st7567_spi.h b/esphome/components/st7567_spi/st7567_spi.h new file mode 100644 index 0000000000..e8d1ebe0cc --- /dev/null +++ b/esphome/components/st7567_spi/st7567_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/st7567_base/st7567_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace st7567_spi { + +class SPIST7567 : public st7567_base::ST7567, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace st7567_spi +} // namespace esphome diff --git a/esphome/components/st7701s/__init__.py b/esphome/components/st7701s/__init__.py new file mode 100644 index 0000000000..c58ce8a01e --- /dev/null +++ b/esphome/components/st7701s/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py new file mode 100644 index 0000000000..c6ad43c14c --- /dev/null +++ b/esphome/components/st7701s/display.py @@ -0,0 +1,255 @@ +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.rpi_dpi_rgb.display import ( + CONF_PCLK_FREQUENCY, + CONF_PCLK_INVERTED, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_ORDER, + CONF_DATA_PINS, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_GREEN, + CONF_HEIGHT, + CONF_HSYNC_PIN, + CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_NUMBER, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_RED, + CONF_RESET_PIN, + CONF_TRANSFORM, + CONF_VSYNC_PIN, + CONF_WIDTH, +) +from esphome.core import TimePeriod + +from .init_sequences import ST7701S_INITS, cmd + +CONF_DE_PIN = "de_pin" +CONF_PCLK_PIN = "pclk_pin" + +CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" +CONF_HSYNC_BACK_PORCH = "hsync_back_porch" +CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" +CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" +CONF_VSYNC_BACK_PORCH = "vsync_back_porch" +CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" + +DEPENDENCIES = ["spi", "esp32"] + +st7701s_ns = cg.esphome_ns.namespace("st7701s") +ST7701S = st7701s_ns.class_("ST7701S", display.Display, cg.Component, spi.SPIDevice) +ColorOrder = display.display_ns.enum("ColorMode") +ST7701S_DELAY_FLAG = 0xFF + +COLOR_ORDERS = { + "RGB": ColorOrder.COLOR_ORDER_RGB, + "BGR": ColorOrder.COLOR_ORDER_BGR, +} +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + + +def data_pin_validate(value): + """ + It is safe to use strapping pins as RGB output data bits, as they are outputs only, + and not initialised until after boot. + """ + if not isinstance(value, dict): + try: + return DATA_PIN_SCHEMA( + {CONF_NUMBER: value, CONF_IGNORE_STRAPPING_WARNING: True} + ) + except cv.Invalid: + pass + return DATA_PIN_SCHEMA(value) + + +def data_pin_set(length): + return cv.All( + [data_pin_validate], + cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), + ) + + +def map_sequence(value): + """ + An initialisation sequence can be selected from one of the pre-defined sequences in init_sequences.py, + or can be a literal array of data bytes. + The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred + from the length of the sequence and should not be explicit. + A delay can be inserted by specifying "- delay N" where N is in ms + """ + if isinstance(value, str) and value.lower().startswith("delay "): + value = value.lower()[6:] + delay = cv.All( + cv.positive_time_period_milliseconds, + cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), + )(value) + return [delay, ST7701S_DELAY_FLAG] + if not isinstance(value, list): + value = cv.int_(value) + value = cv.one_of(*ST7701S_INITS)(value) + return ST7701S_INITS[value] + value = cv.Length(min=1, max=254)(value) + return cmd(*value) + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ST7701S), + cv.Required(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_, + cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_, + } + ), + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), + cv.Required(CONF_DATA_PINS): cv.Any( + data_pin_set(16), + cv.Schema( + { + cv.Required(CONF_RED): data_pin_set(5), + cv.Required(CONF_GREEN): data_pin_set(6), + cv.Required(CONF_BLUE): data_pin_set(5), + } + ), + ), + cv.Optional(CONF_INIT_SEQUENCE, default=1): cv.ensure_list( + map_sequence + ), + cv.Optional(CONF_COLOR_ORDER): cv.one_of( + *COLOR_ORDERS.keys(), upper=True + ), + cv.Optional(CONF_PCLK_FREQUENCY, default="16MHz"): cv.All( + cv.frequency, cv.Range(min=4e6, max=30e6) + ), + cv.Optional(CONF_PCLK_INVERTED, default=True): cv.boolean, + cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, + cv.Required(CONF_DE_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_HSYNC_PULSE_WIDTH, default=10): cv.int_, + cv.Optional(CONF_HSYNC_BACK_PORCH, default=10): cv.int_, + cv.Optional(CONF_HSYNC_FRONT_PORCH, default=20): cv.int_, + cv.Optional(CONF_VSYNC_PULSE_WIDTH, default=10): cv.int_, + cv.Optional(CONF_VSYNC_BACK_PORCH, default=10): cv.int_, + cv.Optional(CONF_VSYNC_FRONT_PORCH, default=10): cv.int_, + } + ).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, +) + +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "st7701s", require_miso=False, require_mosi=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await display.register_display(var, config) + await spi.register_spi_device(var, config) + + sequence = [] + for seq in config[CONF_INIT_SEQUENCE]: + sequence.extend(seq) + cg.add(var.set_init_sequence(sequence)) + cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) + cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) + cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH])) + cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) + cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) + cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) + cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) + index = 0 + dpins = [] + if CONF_RED in config[CONF_DATA_PINS]: + 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) + # swap bytes to match big-endian format + dpins = dpins[8:16] + dpins[0:8] + else: + dpins = config[CONF_DATA_PINS] + for pin in dpins: + data_pin = await cg.gpio_pin_expression(pin) + cg.add(var.add_data_pin(data_pin, index)) + index += 1 + + if dc_pin := config.get(CONF_DC_PIN): + dc = await cg.gpio_pin_expression(dc_pin) + cg.add(var.set_dc_pin(dc)) + + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + + if transform := config.get(CONF_TRANSFORM): + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + + if CONF_DIMENSIONS in config: + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT])) + cg.add( + var.set_offsets( + dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT] + ) + ) + else: + (width, height) = dimensions + cg.add(var.set_dimensions(width, height)) + + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + pin = await cg.gpio_pin_expression(config[CONF_DE_PIN]) + cg.add(var.set_de_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN]) + cg.add(var.set_pclk_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN]) + cg.add(var.set_hsync_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN]) + cg.add(var.set_vsync_pin(pin)) diff --git a/esphome/components/st7701s/init_sequences.py b/esphome/components/st7701s/init_sequences.py new file mode 100644 index 0000000000..4786731c78 --- /dev/null +++ b/esphome/components/st7701s/init_sequences.py @@ -0,0 +1,363 @@ +# These are initialisation sequences for ST7701S displays. The contents are somewhat arcane. + + +def cmd(c, *args): + """ + Create a command sequence + :param c: The command (8 bit) + :param args: zero or more arguments (8 bit values) + :return: a list with the command, the argument count and the arguments + """ + return [c, len(args)] + list(args) + + +ST7701S_1_INIT = ( + cmd(0x01) + + cmd(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10) + + cmd(0xC0, 0x3B, 0x00) + + cmd(0xC1, 0x0D, 0x02) + + cmd(0xC2, 0x31, 0x05) + + cmd(0xCD, 0x08) + + cmd( + 0xB0, + 0x00, + 0x11, + 0x18, + 0x0E, + 0x11, + 0x06, + 0x07, + 0x08, + 0x07, + 0x22, + 0x04, + 0x12, + 0x0F, + 0xAA, + 0x31, + 0x18, + ) + + cmd( + 0xB1, + 0x00, + 0x11, + 0x19, + 0x0E, + 0x12, + 0x07, + 0x08, + 0x08, + 0x08, + 0x22, + 0x04, + 0x11, + 0x11, + 0xA9, + 0x32, + 0x18, + ) + + cmd(0xFF, 0x77, 0x01, 0x00, 0x00, 0x11) + + cmd(0xB0, 0x60) + + cmd(0xB1, 0x32) + + cmd(0xB2, 0x07) + + cmd(0xB3, 0x80) + + cmd(0xB5, 0x49) + + cmd(0xB7, 0x85) + + cmd(0xB8, 0x21) + + cmd(0xC1, 0x78) + + cmd(0xC2, 0x78) + + cmd(0xE0, 0x00, 0x1B, 0x02) + + cmd(0xE1, 0x08, 0xA0, 0x00, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x44, 0x44) + + cmd(0xE2, 0x11, 0x11, 0x44, 0x44, 0xED, 0xA0, 0x00, 0x00, 0xEC, 0xA0, 0x00, 0x00) + + cmd(0xE3, 0x00, 0x00, 0x11, 0x11) + + cmd(0xE4, 0x44, 0x44) + + cmd( + 0xE5, + 0x0A, + 0xE9, + 0xD8, + 0xA0, + 0x0C, + 0xEB, + 0xD8, + 0xA0, + 0x0E, + 0xED, + 0xD8, + 0xA0, + 0x10, + 0xEF, + 0xD8, + 0xA0, + ) + + cmd(0xE6, 0x00, 0x00, 0x11, 0x11) + + cmd(0xE7, 0x44, 0x44) + + cmd( + 0xE8, + 0x09, + 0xE8, + 0xD8, + 0xA0, + 0x0B, + 0xEA, + 0xD8, + 0xA0, + 0x0D, + 0xEC, + 0xD8, + 0xA0, + 0x0F, + 0xEE, + 0xD8, + 0xA0, + ) + + cmd(0xEB, 0x02, 0x00, 0xE4, 0xE4, 0x88, 0x00, 0x40) + + cmd(0xEC, 0x3C, 0x00) + + cmd( + 0xED, + 0xAB, + 0x89, + 0x76, + 0x54, + 0x02, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x20, + 0x45, + 0x67, + 0x98, + 0xBA, + ) + + cmd(0xFF, 0x77, 0x01, 0x00, 0x00, 0x13) + + cmd(0xE5, 0xE4) + + cmd(0x3A, 0x60) +) + +# This is untested +ST7701S_7_INIT = ( + cmd( + 0xFF, + 0x77, + 0x01, + 0x00, + 0x00, + 0x10, + ) + + cmd(0xC0, 0x3B, 0x00) + + cmd(0xC1, 0x0B, 0x02) + + cmd(0xC2, 0x07, 0x02) + + cmd(0xCC, 0x10) + + cmd(0xCD, 0x08) + + cmd( + 0xB0, + 0x00, + 0x11, + 0x16, + 0x0E, + 0x11, + 0x06, + 0x05, + 0x09, + 0x08, + 0x21, + 0x06, + 0x13, + 0x10, + 0x29, + 0x31, + 0x18, + ) + + cmd( + 0xB1, + 0x00, + 0x11, + 0x16, + 0x0E, + 0x11, + 0x07, + 0x05, + 0x09, + 0x09, + 0x21, + 0x05, + 0x13, + 0x11, + 0x2A, + 0x31, + 0x18, + ) + + cmd( + 0xFF, + 0x77, + 0x01, + 0x00, + 0x00, + 0x11, + ) + + cmd(0xB0, 0x6D) + + cmd(0xB1, 0x37) + + cmd(0xB2, 0x81) + + cmd(0xB3, 0x80) + + cmd(0xB5, 0x43) + + cmd(0xB7, 0x85) + + cmd(0xB8, 0x20) + + cmd(0xC1, 0x78) + + cmd(0xC2, 0x78) + + cmd(0xD0, 0x88) + + cmd( + 0xE0, + 3, + 0x00, + 0x00, + 0x02, + ) + + cmd( + 0xE1, + 0x03, + 0xA0, + 0x00, + 0x00, + 0x04, + 0xA0, + 0x00, + 0x00, + 0x00, + 0x20, + 0x20, + ) + + cmd( + 0xE2, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ) + + cmd( + 0xE3, + 0x00, + 0x00, + 0x11, + 0x00, + ) + + cmd(0xE4, 0x22, 0x00) + + cmd( + 0xE5, + 0x05, + 0xEC, + 0xA0, + 0xA0, + 0x07, + 0xEE, + 0xA0, + 0xA0, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ) + + cmd( + 0xE6, + 0x00, + 0x00, + 0x11, + 0x00, + ) + + cmd(0xE7, 0x22, 0x00) + + cmd( + 0xE8, + 0x06, + 0xED, + 0xA0, + 0xA0, + 0x08, + 0xEF, + 0xA0, + 0xA0, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ) + + cmd( + 0xEB, + 0x00, + 0x00, + 0x40, + 0x40, + 0x00, + 0x00, + 0x00, + ) + + cmd( + 0xED, + 0xFF, + 0xFF, + 0xFF, + 0xBA, + 0x0A, + 0xBF, + 0x45, + 0xFF, + 0xFF, + 0x54, + 0xFB, + 0xA0, + 0xAB, + 0xFF, + 0xFF, + 0xFF, + ) + + cmd( + 0xEF, + 0x10, + 0x0D, + 0x04, + 0x08, + 0x3F, + 0x1F, + ) + + cmd( + 0xFF, + 0x77, + 0x01, + 0x00, + 0x00, + 0x13, + ) + + cmd(0xEF, 0x08) + + cmd( + 0xFF, + 0x77, + 0x01, + 0x00, + 0x00, + 0x00, + ) + + cmd(0x3A, 0x66) +) + +ST7701S_INITS = { + 1: ST7701S_1_INIT, + # 7: ST7701S_7_INIT, +} diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp new file mode 100644 index 0000000000..403bff789d --- /dev/null +++ b/esphome/components/st7701s/st7701s.cpp @@ -0,0 +1,198 @@ +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "st7701s.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7701s { + +void ST7701S::setup() { + esph_log_config(TAG, "Setting up ST7701S"); + this->spi_setup(); + this->write_init_sequence_(); + + esp_lcd_rgb_panel_config_t config{}; + config.flags.fb_in_psram = 1; +#if ESP_IDF_VERSION_MAJOR >= 5 + config.bounce_buffer_size_px = this->width_ * 10; + config.num_fbs = 1; +#endif // ESP_IDF_VERSION_MAJOR + config.timings.h_res = this->width_; + config.timings.v_res = this->height_; + config.timings.hsync_pulse_width = this->hsync_pulse_width_; + config.timings.hsync_back_porch = this->hsync_back_porch_; + config.timings.hsync_front_porch = this->hsync_front_porch_; + config.timings.vsync_pulse_width = this->vsync_pulse_width_; + config.timings.vsync_back_porch = this->vsync_back_porch_; + config.timings.vsync_front_porch = this->vsync_front_porch_; + config.timings.flags.pclk_active_neg = this->pclk_inverted_; + config.timings.pclk_hz = this->pclk_frequency_; + config.clk_src = LCD_CLK_SRC_PLL160M; + config.psram_trans_align = 64; + size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); + for (size_t i = 0; i != data_pin_count; i++) { + config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + } + config.data_width = data_pin_count; + config.disp_gpio_num = -1; + config.hsync_gpio_num = this->hsync_pin_->get_pin(); + config.vsync_gpio_num = this->vsync_pin_->get_pin(); + config.de_gpio_num = this->de_pin_->get_pin(); + config.pclk_gpio_num = this->pclk_pin_->get_pin(); + esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); + ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); + if (err != ESP_OK) { + esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); + } + esph_log_config(TAG, "ST7701S setup complete"); +} + +void ST7701S::loop() { +#if ESP_IDF_VERSION_MAJOR >= 5 + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); +#endif // ESP_IDF_VERSION_MAJOR +} + +void ST7701S::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) { + if (w <= 0 || h <= 0) + return; + // if color mapping is required, pass the buck. + // note that endianness is not considered here - it is assumed to match! + if (bitness != display::COLOR_BITNESS_565) { + return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, + x_pad); + } + x_start += this->offset_x_; + y_start += this->offset_y_; + esp_err_t err; + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y_start, x_start + w, y_start + h, ptr); + } else { + // draw line by line + auto stride = x_offset + w + x_pad; + for (int y = 0; y != h; y++) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y + y_start, x_start + w, y + y_start + 1, + ptr + ((y + y_offset) * stride + x_offset) * 2); + if (err != ESP_OK) + break; + } + } + if (err != ESP_OK) + esph_log_e(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); +} + +void ST7701S::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; // NOLINT + + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = this->width_ - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = this->height_ - y - 1; + break; + } + auto pixel = convert_big_endian(display::ColorUtil::color_to_565(color)); + + this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true, + 0, 0, 0); + App.feed_wdt(); +} + +void ST7701S::write_command_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value, 9); + } else { + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + } + this->disable(); +} + +void ST7701S::write_data_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value | 0x100, 9); + } else { + this->dc_pin_->digital_write(true); + this->write_byte(value); + } + this->disable(); +} + +/** + * this relies upon the init sequence being well-formed, which is guaranteed by the Python init code. + */ + +void ST7701S::write_sequence_(uint8_t cmd, size_t len, const uint8_t *bytes) { + this->write_command_(cmd); + while (len-- != 0) + this->write_data_(*bytes++); +} + +void ST7701S::write_init_sequence_() { + for (size_t i = 0; i != this->init_sequence_.size();) { + uint8_t cmd = this->init_sequence_[i++]; + size_t len = this->init_sequence_[i++]; + if (len == ST7701S_DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + this->write_sequence_(cmd, len, &this->init_sequence_[i]); + i += len; + ESP_LOGV(TAG, "Command %X, %d bytes", cmd, len); + if (cmd == SW_RESET_CMD) + delay(6); + } + } + // st7701 does not appear to support axis swapping + this->write_sequence_(CMD2_BKSEL, sizeof(CMD2_BK0), CMD2_BK0); + this->write_command_(SDIR_CMD); // this is in the BK0 command set + this->write_data_(this->mirror_x_ ? 0x04 : 0x00); + uint8_t val = this->color_mode_ == display::COLOR_ORDER_BGR ? 0x08 : 0x00; + if (this->mirror_y_) + val |= 0x10; + this->write_command_(MADCTL_CMD); + this->write_data_(val); + ESP_LOGD(TAG, "write MADCTL %X", val); + this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); + // can't avoid this inline delay due to the need to complete setup before anything else tries to draw. + delay(120); // NOLINT + this->write_command_(SLEEP_OUT); + this->write_command_(DISPLAY_ON); + this->spi_teardown(); // SPI not needed after this + delay(10); +} + +void ST7701S::dump_config() { + ESP_LOGCONFIG("", "ST7701S RGB LCD"); + ESP_LOGCONFIG(TAG, " Height: %u", this->height_); + ESP_LOGCONFIG(TAG, " Width: %u", this->width_); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" DE Pin: ", this->de_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); + for (size_t i = 0; i != data_pin_count; i++) + ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str()); + ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); +} + +} // namespace st7701s +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/st7701s/st7701s.h b/esphome/components/st7701s/st7701s.h new file mode 100644 index 0000000000..a1e3c2e54a --- /dev/null +++ b/esphome/components/st7701s/st7701s.h @@ -0,0 +1,118 @@ +// +// Created by Clyde Stubbs on 29/10/2023. +// +#pragma once + +// only applicable on ESP32-S3 +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display.h" +#include "esp_lcd_panel_ops.h" + +#include "esp_lcd_panel_rgb.h" + +namespace esphome { +namespace st7701s { + +constexpr static const char *const TAG = "display.st7701s"; +const uint8_t SW_RESET_CMD = 0x01; +const uint8_t SLEEP_OUT = 0x11; +const uint8_t SDIR_CMD = 0xC7; +const uint8_t MADCTL_CMD = 0x36; +const uint8_t INVERT_OFF = 0x20; +const uint8_t INVERT_ON = 0x21; +const uint8_t DISPLAY_ON = 0x29; +const uint8_t CMD2_BKSEL = 0xFF; +const uint8_t CMD2_BK0[5] = {0x77, 0x01, 0x00, 0x00, 0x10}; +const uint8_t ST7701S_DELAY_FLAG = 0xFF; + +class ST7701S : public display::Display, + public spi::SPIDevice { + public: + void update() override { this->do_update_(); } + void setup() override; + void complete_setup_(); + void loop() 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; + + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + + void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; }; + void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; } + void set_pclk_pin(InternalGPIOPin *pclk_pin) { this->pclk_pin_ = pclk_pin; } + void set_vsync_pin(InternalGPIOPin *vsync_pin) { this->vsync_pin_ = vsync_pin; } + void set_hsync_pin(InternalGPIOPin *hsync_pin) { this->hsync_pin_ = hsync_pin; } + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_width(uint16_t width) { this->width_ = width; } + void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_inverted(bool inverted) { this->pclk_inverted_ = inverted; } + void set_dimensions(uint16_t width, uint16_t height) { + this->width_ = width; + this->height_ = height; + } + int get_width() override { return this->width_; } + int get_height() override { return this->height_; } + void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } + void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } + void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } + void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; } + void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; } + void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } + void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } + void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } + void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } + void set_offsets(int16_t offset_x, int16_t offset_y) { + this->offset_x_ = offset_x; + this->offset_y_ = offset_y; + } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void dump_config() override; + void draw_pixel_at(int x, int y, Color color) override; + + // this will be horribly slow. + protected: + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_sequence_(uint8_t cmd, size_t len, const uint8_t *bytes); + void write_init_sequence_(); + + InternalGPIOPin *de_pin_{nullptr}; + InternalGPIOPin *pclk_pin_{nullptr}; + InternalGPIOPin *hsync_pin_{nullptr}; + InternalGPIOPin *vsync_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + GPIOPin *dc_pin_{nullptr}; + InternalGPIOPin *data_pins_[16] = {}; + uint16_t hsync_pulse_width_ = 10; + uint16_t hsync_back_porch_ = 10; + uint16_t hsync_front_porch_ = 20; + uint16_t vsync_pulse_width_ = 10; + uint16_t vsync_back_porch_ = 10; + uint16_t vsync_front_porch_ = 10; + std::vector init_sequence_; + uint32_t pclk_frequency_ = 16 * 1000 * 1000; + bool pclk_inverted_{true}; + + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + size_t width_{}; + size_t height_{}; + int16_t offset_x_{0}; + int16_t offset_y_{0}; + bool mirror_x_{}; + bool mirror_y_{}; + + esp_lcd_panel_handle_t handle_{}; +}; + +} // namespace st7701s +} // namespace esphome +#endif diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index ae31f604a5..2761214315 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -1,16 +1,17 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.components import spi -from esphome.components import display +import esphome.codegen as cg +from esphome.components import display, spi +import esphome.config_validation as cv from esphome.const import ( CONF_DC_PIN, CONF_ID, + CONF_INVERT_COLORS, CONF_LAMBDA, CONF_MODEL, - CONF_RESET_PIN, CONF_PAGES, + CONF_RESET_PIN, ) + from . import st7735_ns CODEOWNERS = ["@SenexCrenshaw"] @@ -23,7 +24,6 @@ CONF_ROW_START = "row_start" CONF_COL_START = "col_start" CONF_EIGHT_BIT_COLOR = "eight_bit_color" CONF_USE_BGR = "use_bgr" -CONF_INVERT_COLORS = "invert_colors" SPIST7735 = st7735_ns.class_( "ST7735", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice @@ -68,8 +68,12 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "st7735", require_miso=False, require_mosi=True +) + + async def setup_st7735(var, config): - await cg.register_component(var, config) await display.register_display(var, config) if CONF_RESET_PIN in config: @@ -77,7 +81,7 @@ async def setup_st7735(var, config): cg.add(var.set_reset_pin(reset)) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 3baa9b083a..37fe673962 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -32,8 +32,7 @@ enum ST7735Model { ST7735_INITR_18REDTAB = INITR_18REDTAB }; -class ST7735 : public PollingComponent, - public display::DisplayBuffer, +class ST7735 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index d18e305cc2..8259eacf2d 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -1,22 +1,26 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.components import display, spi +import esphome.codegen as cg +from esphome.components import display, power_supply, spi +import esphome.config_validation as cv from esphome.const import ( CONF_BACKLIGHT_PIN, + CONF_CS_PIN, CONF_DC_PIN, CONF_HEIGHT, CONF_ID, CONF_LAMBDA, CONF_MODEL, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_POWER_SUPPLY, CONF_RESET_PIN, + CONF_ROTATION, CONF_WIDTH, ) + from . import st7789v_ns CONF_EIGHTBITCOLOR = "eightbitcolor" -CONF_OFFSET_HEIGHT = "offset_height" -CONF_OFFSET_WIDTH = "offset_width" CODEOWNERS = ["@kbx81"] @@ -25,39 +29,119 @@ DEPENDENCIES = ["spi"] ST7789V = st7789v_ns.class_( "ST7789V", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer ) -ST7789VRef = ST7789V.operator("ref") -ST7789VModel = st7789v_ns.enum("ST7789VModel") + +MODEL_PRESETS = "model_presets" +REQUIRE_PS = "require_ps" + + +def model_spec(require_ps=False, presets=None): + if presets is None: + presets = {} + return {MODEL_PRESETS: presets, REQUIRE_PS: require_ps} + MODELS = { - "TTGO_TDISPLAY_135X240": ST7789VModel.ST7789V_MODEL_TTGO_TDISPLAY_135_240, - "ADAFRUIT_FUNHOUSE_240X240": ST7789VModel.ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240, - "ADAFRUIT_RR_280X240": ST7789VModel.ST7789V_MODEL_ADAFRUIT_RR_280_240, - "CUSTOM": ST7789VModel.ST7789V_MODEL_CUSTOM, + "TTGO_TDISPLAY_135X240": model_spec( + presets={ + CONF_HEIGHT: 240, + CONF_WIDTH: 135, + CONF_OFFSET_HEIGHT: 52, + CONF_OFFSET_WIDTH: 40, + CONF_CS_PIN: "GPIO5", + CONF_DC_PIN: "GPIO16", + CONF_RESET_PIN: "GPIO23", + CONF_BACKLIGHT_PIN: "GPIO4", + } + ), + "ADAFRUIT_FUNHOUSE_240X240": model_spec( + presets={ + CONF_HEIGHT: 240, + CONF_WIDTH: 240, + CONF_OFFSET_HEIGHT: 0, + CONF_OFFSET_WIDTH: 0, + CONF_CS_PIN: "GPIO40", + CONF_DC_PIN: "GPIO39", + CONF_RESET_PIN: "GPIO41", + } + ), + "ADAFRUIT_RR_280X240": model_spec( + presets={ + CONF_HEIGHT: 280, + CONF_WIDTH: 240, + CONF_OFFSET_HEIGHT: 0, + CONF_OFFSET_WIDTH: 20, + } + ), + "ADAFRUIT_S2_TFT_FEATHER_240X135": model_spec( + require_ps=True, + presets={ + CONF_HEIGHT: 240, + CONF_WIDTH: 135, + CONF_OFFSET_HEIGHT: 52, + CONF_OFFSET_WIDTH: 40, + CONF_CS_PIN: "GPIO7", + CONF_DC_PIN: "GPIO39", + CONF_RESET_PIN: "GPIO40", + CONF_BACKLIGHT_PIN: "GPIO45", + }, + ), + "LILYGO_T-EMBED_170X320": model_spec( + presets={ + CONF_HEIGHT: 320, + CONF_WIDTH: 170, + CONF_OFFSET_HEIGHT: 35, + CONF_OFFSET_WIDTH: 0, + CONF_ROTATION: 270, + CONF_CS_PIN: "GPIO10", + CONF_DC_PIN: "GPIO13", + CONF_RESET_PIN: "GPIO9", + CONF_BACKLIGHT_PIN: "GPIO15", + } + ), + "WAVESHARE_1.47IN_172X320": model_spec( + presets={ + CONF_HEIGHT: 320, + CONF_WIDTH: 172, + CONF_OFFSET_HEIGHT: 34, + CONF_OFFSET_WIDTH: 0, + CONF_ROTATION: 90, + CONF_CS_PIN: "GPIO21", + CONF_DC_PIN: "GPIO22", + CONF_RESET_PIN: "GPIO23", + CONF_BACKLIGHT_PIN: "GPIO4", + } + ), + "CUSTOM": model_spec(), } -ST7789V_MODEL = cv.enum(MODELS, upper=True, space="_") - def validate_st7789v(config): - if config[CONF_MODEL].upper() == "CUSTOM" and ( - CONF_HEIGHT not in config - or CONF_WIDTH not in config - or CONF_OFFSET_HEIGHT not in config - or CONF_OFFSET_WIDTH not in config - ): + model_data = MODELS[config[CONF_MODEL]] + presets = model_data[MODEL_PRESETS] + for key, value in presets.items(): + if key not in config: + if key.endswith("pin"): + # All pins are output. + value = pins.gpio_output_pin_schema(value) + config[key] = value + + if model_data[REQUIRE_PS] and CONF_POWER_SUPPLY not in config: raise cv.Invalid( - f'{CONF_HEIGHT}, {CONF_WIDTH}, {CONF_OFFSET_HEIGHT} and {CONF_OFFSET_WIDTH} must be specified when {CONF_MODEL} is "CUSTOM"' + f'{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is {config[CONF_MODEL]}"' ) - if config[CONF_MODEL].upper() != "CUSTOM" and ( - CONF_HEIGHT in config - or CONF_WIDTH in config - or CONF_OFFSET_HEIGHT in config - or CONF_OFFSET_WIDTH in config + if ( + CONF_OFFSET_WIDTH not in config + or CONF_OFFSET_HEIGHT not in config + or CONF_HEIGHT not in config + or CONF_WIDTH not in config ): raise cv.Invalid( - f'Do not specify {CONF_HEIGHT}, {CONF_WIDTH}, {CONF_OFFSET_HEIGHT} or {CONF_OFFSET_WIDTH} when using {CONF_MODEL} that is not "CUSTOM"' + f"{CONF_HEIGHT}, {CONF_WIDTH}, {CONF_OFFSET_HEIGHT} and {CONF_OFFSET_WIDTH} must all be specified" ) + if CONF_DC_PIN not in config or CONF_RESET_PIN not in config: + raise cv.Invalid(f"both {CONF_DC_PIN} and {CONF_RESET_PIN} must be specified") + return config @@ -65,10 +149,14 @@ CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(ST7789V), - cv.Required(CONF_MODEL): ST7789V_MODEL, - cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_MODEL): cv.one_of(*MODELS.keys(), upper=True, space="_"), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BACKLIGHT_PIN): cv.Any( + cv.boolean, + pins.gpio_output_pin_schema, + ), + cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, cv.Optional(CONF_HEIGHT): cv.int_, cv.Optional(CONF_WIDTH): cv.int_, @@ -81,20 +169,22 @@ CONFIG_SCHEMA = cv.All( validate_st7789v, ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "st7789v", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) - cg.add(var.set_model(config[CONF_MODEL])) + cg.add(var.set_model_str(config[CONF_MODEL])) - if config[CONF_MODEL].upper() == "CUSTOM": - cg.add(var.set_height(config[CONF_HEIGHT])) - cg.add(var.set_width(config[CONF_WIDTH])) - cg.add(var.set_offset_height(config[CONF_OFFSET_HEIGHT])) - cg.add(var.set_offset_width(config[CONF_OFFSET_WIDTH])) + cg.add(var.set_height(config[CONF_HEIGHT])) + cg.add(var.set_width(config[CONF_WIDTH])) + cg.add(var.set_offset_height(config[CONF_OFFSET_HEIGHT])) + cg.add(var.set_offset_width(config[CONF_OFFSET_WIDTH])) cg.add(var.set_eightbitcolor(config[CONF_EIGHTBITCOLOR])) @@ -104,12 +194,16 @@ async def to_code(config): reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) - if CONF_BACKLIGHT_PIN in config: + if CONF_BACKLIGHT_PIN in config and config[CONF_BACKLIGHT_PIN]: bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) cg.add(var.set_backlight_pin(bl)) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) + + if CONF_POWER_SUPPLY in config: + ps = await cg.get_variable(config[CONF_POWER_SUPPLY]) + cg.add(var.set_power_supply(ps)) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 8a4fcfb179..74c7a4e9e3 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -5,9 +5,14 @@ namespace esphome { namespace st7789v { static const char *const TAG = "st7789v"; +static const size_t TEMP_BUFFER_SIZE = 128; void ST7789V::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI ST7789V..."); +#ifdef USE_POWER_SUPPLY + this->power_.request(); + // the PowerSupply component takes care of post turn-on delay +#endif this->spi_setup(); this->dc_pin_->setup(); // OUTPUT @@ -117,17 +122,21 @@ void ST7789V::setup() { void ST7789V::dump_config() { LOG_DISPLAY("", "SPI ST7789V", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - if (this->model_ == ST7789V_MODEL_CUSTOM) { - ESP_LOGCONFIG(TAG, " Height Offset: %u", this->offset_height_); - ESP_LOGCONFIG(TAG, " Width Offset: %u", this->offset_width_); - } + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_); + ESP_LOGCONFIG(TAG, " Height: %u", this->height_); + ESP_LOGCONFIG(TAG, " Width: %u", this->width_); + ESP_LOGCONFIG(TAG, " Height Offset: %u", this->offset_height_); + ESP_LOGCONFIG(TAG, " Width Offset: %u", this->offset_width_); ESP_LOGCONFIG(TAG, " 8-bit color mode: %s", YESNO(this->eightbitcolor_)); 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 } float ST7789V::get_setup_priority() const { return setup_priority::PROCESSOR; } @@ -137,35 +146,7 @@ void ST7789V::update() { this->write_display_data(); } -void ST7789V::set_model(ST7789VModel model) { - this->model_ = model; - - switch (this->model_) { - case ST7789V_MODEL_TTGO_TDISPLAY_135_240: - this->height_ = 240; - this->width_ = 135; - this->offset_height_ = 52; - this->offset_width_ = 40; - break; - - case ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240: - this->height_ = 240; - this->width_ = 240; - this->offset_height_ = 0; - this->offset_width_ = 0; - break; - - case ST7789V_MODEL_ADAFRUIT_RR_280_240: - this->height_ = 280; - this->width_ = 240; - this->offset_height_ = 0; - this->offset_width_ = 20; - break; - - default: - break; - } -} +void ST7789V::set_model_str(const char *model_str) { this->model_str_ = model_str; } void ST7789V::write_display_data() { uint16_t x1 = this->offset_height_; @@ -191,15 +172,23 @@ void ST7789V::write_display_data() { this->dc_pin_->digital_write(true); if (this->eightbitcolor_) { + uint8_t temp_buffer[TEMP_BUFFER_SIZE]; + size_t temp_index = 0; for (int line = 0; line < this->get_buffer_length_(); line = line + this->get_width_internal()) { for (int index = 0; index < this->get_width_internal(); ++index) { auto color = display::ColorUtil::color_to_565( display::ColorUtil::to_color(this->buffer_[index + line], display::ColorOrder::COLOR_ORDER_RGB, display::ColorBitness::COLOR_BITNESS_332, true)); - this->write_byte((color >> 8) & 0xff); - this->write_byte(color & 0xff); + temp_buffer[temp_index++] = (uint8_t) (color >> 8); + temp_buffer[temp_index++] = (uint8_t) color; + if (temp_index == TEMP_BUFFER_SIZE) { + this->write_array(temp_buffer, TEMP_BUFFER_SIZE); + temp_index = 0; + } } } + if (temp_index != 0) + this->write_array(temp_buffer, temp_index); } else { this->write_array(this->buffer_, this->get_buffer_length_()); } @@ -214,9 +203,10 @@ void ST7789V::init_reset_() { delay(1); // Trigger Reset this->reset_pin_->digital_write(false); - delay(10); + delay(1); // Wake up this->reset_pin_->digital_write(true); + delay(5); } } @@ -315,18 +305,5 @@ void HOT ST7789V::draw_absolute_pixel_internal(int x, int y, Color color) { } } -const char *ST7789V::model_str_() { - switch (this->model_) { - case ST7789V_MODEL_TTGO_TDISPLAY_135_240: - return "TTGO T-Display 135x240"; - case ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240: - return "Adafruit Funhouse 240x240"; - case ST7789V_MODEL_ADAFRUIT_RR_280_240: - return "Adafruit Round-Rectangular 280x240"; - default: - return "Custom"; - } -} - } // namespace st7789v } // namespace esphome diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 96e97c9d78..29ea315979 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -3,17 +3,13 @@ #include "esphome/core/component.h" #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" +#ifdef USE_POWER_SUPPLY +#include "esphome/components/power_supply/power_supply.h" +#endif namespace esphome { namespace st7789v { -enum ST7789VModel { - ST7789V_MODEL_TTGO_TDISPLAY_135_240, - ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240, - ST7789V_MODEL_ADAFRUIT_RR_280_240, - ST7789V_MODEL_CUSTOM -}; - static const uint8_t ST7789_NOP = 0x00; // No Operation static const uint8_t ST7789_SWRESET = 0x01; // Software Reset static const uint8_t ST7789_RDDID = 0x04; // Read Display ID @@ -111,15 +107,17 @@ static const uint8_t ST7789_MADCTL_GS = 0x01; static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; -class ST7789V : public PollingComponent, - public display::DisplayBuffer, - public spi::SPIDevice { +class ST7789V : public display::DisplayBuffer, + public spi::SPIDevice { public: - void set_model(ST7789VModel model); + void set_model_str(const char *model_str); void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } +#ifdef USE_POWER_SUPPLY + void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } +#endif void set_eightbitcolor(bool eightbitcolor) { this->eightbitcolor_ = eightbitcolor; } void set_height(uint32_t height) { this->height_ = height; } @@ -139,10 +137,12 @@ class ST7789V : public PollingComponent, display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } protected: - ST7789VModel model_{ST7789V_MODEL_TTGO_TDISPLAY_135_240}; GPIOPin *dc_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; GPIOPin *backlight_pin_{nullptr}; +#ifdef USE_POWER_SUPPLY + power_supply::PowerSupplyRequester power_; +#endif bool eightbitcolor_{false}; uint16_t height_{0}; @@ -165,7 +165,7 @@ class ST7789V : public PollingComponent, void draw_absolute_pixel_internal(int x, int y, Color color) override; - const char *model_str_(); + const char *model_str_; }; } // namespace st7789v diff --git a/esphome/components/st7920/display.py b/esphome/components/st7920/display.py index 9b544fa644..1267e2ad63 100644 --- a/esphome/components/st7920/display.py +++ b/esphome/components/st7920/display.py @@ -28,7 +28,6 @@ CONFIG_SCHEMA = ( 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) if CONF_LAMBDA in config: diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index c00b7cf5e0..c9fdad454d 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -11,8 +11,7 @@ class ST7920; using st7920_writer_t = std::function; -class ST7920 : public PollingComponent, - public display::DisplayBuffer, +class ST7920 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/statsd/__init__.py b/esphome/components/statsd/__init__.py new file mode 100644 index 0000000000..3623338aec --- /dev/null +++ b/esphome/components/statsd/__init__.py @@ -0,0 +1,65 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor +from esphome.const import ( + CONF_ID, + CONF_PORT, + CONF_NAME, + CONF_SENSORS, + CONF_BINARY_SENSORS, +) + +AUTO_LOAD = ["socket"] +CODEOWNERS = ["@Links2004"] +DEPENDENCIES = ["network"] + +CONF_HOST = "host" +CONF_PREFIX = "prefix" + +statsd_component_ns = cg.esphome_ns.namespace("statsd") +StatsdComponent = statsd_component_ns.class_("StatsdComponent", cg.PollingComponent) + +CONFIG_SENSORS_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_NAME): cv.string_strict, + } +) + +CONFIG_BINARY_SENSORS_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_NAME): cv.string_strict, + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(StatsdComponent), + cv.Required(CONF_HOST): cv.string_strict, + cv.Optional(CONF_PORT, default=8125): cv.port, + cv.Optional(CONF_PREFIX, default=""): cv.string_strict, + cv.Optional(CONF_SENSORS): cv.ensure_list(CONFIG_SENSORS_SCHEMA), + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list(CONFIG_BINARY_SENSORS_SCHEMA), + } +).extend(cv.polling_component_schema("10s")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add( + var.configure( + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_PREFIX), + ) + ) + + for sensor_cfg in config.get(CONF_SENSORS, []): + s = await cg.get_variable(sensor_cfg[CONF_ID]) + cg.add(var.register_sensor(sensor_cfg[CONF_NAME], s)) + + for sensor_cfg in config.get(CONF_BINARY_SENSORS, []): + s = await cg.get_variable(sensor_cfg[CONF_ID]) + cg.add(var.register_binary_sensor(sensor_cfg[CONF_NAME], s)) diff --git a/esphome/components/statsd/statsd.cpp b/esphome/components/statsd/statsd.cpp new file mode 100644 index 0000000000..b7fad19332 --- /dev/null +++ b/esphome/components/statsd/statsd.cpp @@ -0,0 +1,158 @@ +#include "esphome/core/log.h" + +#include "statsd.h" + +#ifdef USE_NETWORK +namespace esphome { +namespace statsd { + +// send UDP packet if we reach 1Kb packed size +// this is needed since statsD does not support fragmented UDP packets +static const uint16_t SEND_THRESHOLD = 1024; + +static const char *const TAG = "statsD"; + +void StatsdComponent::setup() { +#ifndef USE_ESP8266 + this->sock_ = esphome::socket::socket(AF_INET, SOCK_DGRAM, 0); + + struct sockaddr_in source; + source.sin_family = AF_INET; + source.sin_addr.s_addr = htonl(INADDR_ANY); + source.sin_port = htons(this->port_); + this->sock_->bind((struct sockaddr *) &source, sizeof(source)); + + this->destination_.sin_family = AF_INET; + this->destination_.sin_port = htons(this->port_); + this->destination_.sin_addr.s_addr = inet_addr(this->host_); +#endif +} + +StatsdComponent::~StatsdComponent() { +#ifndef USE_ESP8266 + if (!this->sock_) { + return; + } + this->sock_->close(); +#endif +} + +void StatsdComponent::dump_config() { + ESP_LOGCONFIG(TAG, "statsD:"); + ESP_LOGCONFIG(TAG, " host: %s", this->host_); + ESP_LOGCONFIG(TAG, " port: %d", this->port_); + if (this->prefix_) { + ESP_LOGCONFIG(TAG, " prefix: %s", this->prefix_); + } + + ESP_LOGCONFIG(TAG, " metrics:"); + for (sensors_t s : this->sensors_) { + ESP_LOGCONFIG(TAG, " - name: %s", s.name); + ESP_LOGCONFIG(TAG, " type: %d", s.type); + } +} + +float StatsdComponent::get_setup_priority() const { return esphome::setup_priority::AFTER_WIFI; } + +#ifdef USE_SENSOR +void StatsdComponent::register_sensor(const char *name, esphome::sensor::Sensor *sensor) { + sensors_t s; + s.name = name; + s.sensor = sensor; + s.type = TYPE_SENSOR; + this->sensors_.push_back(s); +} +#endif + +#ifdef USE_BINARY_SENSOR +void StatsdComponent::register_binary_sensor(const char *name, esphome::binary_sensor::BinarySensor *binary_sensor) { + sensors_t s; + s.name = name; + s.binary_sensor = binary_sensor; + s.type = TYPE_BINARY_SENSOR; + this->sensors_.push_back(s); +} +#endif + +void StatsdComponent::update() { + std::string out; + out.reserve(SEND_THRESHOLD); + + for (sensors_t s : this->sensors_) { + double val = 0; + switch (s.type) { +#ifdef USE_SENSOR + case TYPE_SENSOR: + if (!s.sensor->has_state()) { + continue; + } + val = s.sensor->state; + break; +#endif +#ifdef USE_BINARY_SENSOR + case TYPE_BINARY_SENSOR: + if (!s.binary_sensor->has_state()) { + continue; + } + // map bool to double + if (s.binary_sensor->state) { + val = 1; + } + break; +#endif + default: + ESP_LOGE(TAG, "type not known, name: %s type: %d", s.name, s.type); + continue; + } + + // statsD gauge: + // https://github.com/statsd/statsd/blob/master/docs/metric_types.md + // This implies you can't explicitly set a gauge to a negative number without first setting it to zero. + if (val < 0) { + if (this->prefix_) { + out.append(str_sprintf("%s.", this->prefix_)); + } + out.append(str_sprintf("%s:0|g\n", s.name)); + } + if (this->prefix_) { + out.append(str_sprintf("%s.", this->prefix_)); + } + out.append(str_sprintf("%s:%f|g\n", s.name, val)); + + if (out.length() > SEND_THRESHOLD) { + this->send_(&out); + out.clear(); + } + } + + this->send_(&out); +} + +void StatsdComponent::send_(std::string *out) { + if (out->empty()) { + return; + } +#ifdef USE_ESP8266 + IPAddress ip; + ip.fromString(this->host_); + + this->sock_.beginPacket(ip, this->port_); + this->sock_.write((const uint8_t *) out->c_str(), out->length()); + this->sock_.endPacket(); + +#else + if (!this->sock_) { + return; + } + + int n_bytes = this->sock_->sendto(out->c_str(), out->length(), 0, reinterpret_cast(&this->destination_), + sizeof(this->destination_)); + if (n_bytes != out->length()) { + ESP_LOGE(TAG, "Failed to send UDP packed (%d of %d)", n_bytes, out->length()); + } +#endif +} + +} // namespace statsd +} // namespace esphome +#endif diff --git a/esphome/components/statsd/statsd.h b/esphome/components/statsd/statsd.h new file mode 100644 index 0000000000..34f84cbe00 --- /dev/null +++ b/esphome/components/statsd/statsd.h @@ -0,0 +1,88 @@ +#pragma once + +#include + +#include "esphome/core/defines.h" +#ifdef USE_NETWORK +#include "esphome/core/component.h" +#include "esphome/components/socket/socket.h" +#include "esphome/components/network/ip_address.h" + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +#ifdef USE_ESP8266 +#include "WiFiUdp.h" +#include "IPAddress.h" +#endif + +namespace esphome { +namespace statsd { + +using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR }; + +using sensors_t = struct { + const char *name; + sensor_type_t type; + union { +#ifdef USE_SENSOR + esphome::sensor::Sensor *sensor; +#endif +#ifdef USE_BINARY_SENSOR + esphome::binary_sensor::BinarySensor *binary_sensor; +#endif + }; +}; + +class StatsdComponent : public PollingComponent { + public: + ~StatsdComponent(); + + void setup() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override; + + void configure(const char *host, uint16_t port, const char *prefix) { + this->host_ = host; + this->port_ = port; + this->prefix_ = prefix; + } + +#ifdef USE_SENSOR + void register_sensor(const char *name, esphome::sensor::Sensor *sensor); +#endif + +#ifdef USE_BINARY_SENSOR + void register_binary_sensor(const char *name, esphome::binary_sensor::BinarySensor *binary_sensor); +#endif + + private: + const char *host_; + const char *prefix_; + uint16_t port_; + + std::vector sensors_; + +#ifdef USE_ESP8266 + WiFiUDP sock_; +#else + std::unique_ptr sock_; + struct sockaddr_in destination_; +#endif + + void send_(std::string *out); +}; + +} // namespace statsd +} // namespace esphome +#endif diff --git a/esphome/components/status_led/light/status_led_light.cpp b/esphome/components/status_led/light/status_led_light.cpp index b47d1f5bd0..549024c4df 100644 --- a/esphome/components/status_led/light/status_led_light.cpp +++ b/esphome/components/status_led/light/status_led_light.cpp @@ -1,6 +1,7 @@ #include "status_led_light.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include namespace esphome { namespace status_led { @@ -11,7 +12,7 @@ void StatusLEDLightOutput::loop() { uint32_t new_state = App.get_app_state() & STATUS_LED_MASK; if (new_state != this->last_app_state_) { - ESP_LOGV(TAG, "New app state 0x%08X", new_state); + ESP_LOGV(TAG, "New app state 0x%08" PRIX32, new_state); } if ((new_state & STATUS_LED_ERROR) != 0u) { diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index 5af808b6e7..a533bc1d87 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -30,7 +30,7 @@ void STS3XComponent::setup() { return; } uint32_t serial_number = (uint32_t(raw_serial_number[0]) << 16); - ESP_LOGV(TAG, " Serial Number: 0x%08X", serial_number); + ESP_LOGV(TAG, " Serial Number: 0x%08" PRIX32, serial_number); } void STS3XComponent::dump_config() { ESP_LOGCONFIG(TAG, "STS3x:"); diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h index 261033efad..8f806a3471 100644 --- a/esphome/components/sts3x/sts3x.h +++ b/esphome/components/sts3x/sts3x.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" +#include + namespace esphome { namespace sts3x { diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index ef368015b1..fa52200d46 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -4,7 +4,7 @@ import esphome.config_validation as cv from esphome import core from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS from esphome.yaml_util import ESPHomeDataBase, make_data_base -from esphome.config_helpers import merge_config +from esphome.config_helpers import merge_config, Extend, Remove CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) @@ -105,7 +105,7 @@ def _substitute_item(substitutions, item, path, ignore_missing): sub = _expand_substitutions(substitutions, item, path, ignore_missing) if sub != item: return sub - elif isinstance(item, core.Lambda): + elif isinstance(item, (core.Lambda, Extend, Remove)): sub = _expand_substitutions(substitutions, item.value, path, ignore_missing) if sub != item: item.value = sub @@ -116,7 +116,7 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return - substitutions = config[CONF_SUBSTITUTIONS] + substitutions = config.get(CONF_SUBSTITUTIONS) if substitutions is None: substitutions = command_line_substitutions elif command_line_substitutions: diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 5f9179682a..2fd9394a5e 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -37,7 +37,7 @@ num_t EquatorialCoordinate::declination_rad() const { return radians(declination num_t HorizontalCoordinate::elevation_rad() const { return radians(elevation); } num_t HorizontalCoordinate::azimuth_rad() const { return radians(azimuth); } -num_t julian_day(time::ESPTime moment) { +num_t julian_day(ESPTime moment) { // p. 59 // UT -> JD, TT -> JDE int y = moment.year; @@ -54,7 +54,7 @@ num_t julian_day(time::ESPTime moment) { int b = 2 - a + a / 4; return ((int) (365.25 * (y + 4716))) + ((int) (30.6001 * (m + 1))) + d + b - 1524.5; } -num_t delta_t(time::ESPTime moment) { +num_t delta_t(ESPTime moment) { // approximation for 2005-2050 from NASA (https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html) int t = moment.year - 2000; return 62.92 + t * (0.32217 + t * 0.005589); @@ -199,7 +199,7 @@ struct SunAtLocation { // see chapter 12, p. 87 num_t jd = moment.jd(); // eq 12.1, p.87; jd for 0h UT of this date - time::ESPTime moment_0h = moment.dt; + ESPTime moment_0h = moment.dt; moment_0h.hour = moment_0h.minute = moment_0h.second = 0; num_t jd0 = Moment{moment_0h}.jd(); num_t t = (jd0 - 2451545) / 36525; @@ -227,9 +227,9 @@ struct SunAtLocation { return HorizontalCoordinate{degrees(elevation_rad), degrees(azimuth_rad) + 180}; } - optional sunrise(time::ESPTime date, num_t zenith) const { return event(true, date, zenith); } - optional sunset(time::ESPTime date, num_t zenith) const { return event(false, date, zenith); } - optional event(bool rise, time::ESPTime date, num_t zenith) const { + optional sunrise(ESPTime date, num_t zenith) const { return event(true, date, zenith); } + optional sunset(ESPTime date, num_t zenith) const { return event(false, date, zenith); } + optional event(bool rise, ESPTime date, num_t zenith) const { // couldn't get the method described in chapter 15 to work, // so instead this is based on the algorithm in time4j // https://github.com/MenoData/Time4J/blob/master/base/src/main/java/net/time4j/calendar/astro/StdSolarCalculator.java @@ -244,7 +244,7 @@ struct SunAtLocation { new_h = *x; } while (std::abs(new_h - old_h) >= 15); time_t new_timestamp = m.timestamp + (time_t) new_h; - return time::ESPTime::from_epoch_local(new_timestamp); + return ESPTime::from_epoch_local(new_timestamp); } protected: @@ -263,14 +263,14 @@ struct SunAtLocation { return hour_angle; } - time::ESPTime local_event_(time::ESPTime date, int hour) const { + ESPTime local_event_(ESPTime date, int hour) const { // input date should be in UTC, and hour/minute/second fields 0 num_t added_d = hour / 24.0 - location.longitude / 360; num_t jd = julian_day(date) + added_d; num_t eot = SunAtTime(jd).equation_of_time() * 240; time_t new_timestamp = (time_t) (date.timestamp + added_d * 86400 - eot); - return time::ESPTime::from_epoch_utc(new_timestamp); + return ESPTime::from_epoch_utc(new_timestamp); } }; @@ -287,7 +287,7 @@ HorizontalCoordinate Sun::calc_coords_() { */ return sun.true_coordinate(m); } -optional Sun::calc_event_(time::ESPTime date, bool rising, double zenith) { +optional Sun::calc_event_(ESPTime date, bool rising, double zenith) { SunAtLocation sun{location_}; if (!date.is_valid()) return {}; @@ -301,24 +301,20 @@ optional Sun::calc_event_(time::ESPTime date, bool rising, double // We're calculating *next* sunrise/sunset, but calculated event // is today, so try again tomorrow time_t new_timestamp = today.timestamp + 24 * 60 * 60; - today = time::ESPTime::from_epoch_utc(new_timestamp); + today = ESPTime::from_epoch_utc(new_timestamp); it = sun.event(rising, today, zenith); } return it; } -optional Sun::calc_event_(bool rising, double zenith) { +optional Sun::calc_event_(bool rising, double zenith) { auto it = Sun::calc_event_(this->time_->utcnow(), rising, zenith); return it; } -optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } -optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } -optional Sun::sunrise(time::ESPTime date, double elevation) { - return this->calc_event_(date, true, 90 - elevation); -} -optional Sun::sunset(time::ESPTime date, double elevation) { - return this->calc_event_(date, false, 90 - elevation); -} +optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } +optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } +optional Sun::sunrise(ESPTime date, double elevation) { return this->calc_event_(date, true, 90 - elevation); } +optional Sun::sunset(ESPTime date, double elevation) { return this->calc_event_(date, false, 90 - elevation); } double Sun::elevation() { return this->calc_coords_().elevation; } double Sun::azimuth() { return this->calc_coords_().azimuth; } diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 9547b2f280..de4801a655 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -1,8 +1,10 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "esphome/core/automation.h" +#include "esphome/core/time.h" + #include "esphome/components/time/real_time_clock.h" namespace esphome { @@ -26,7 +28,7 @@ struct GeoLocation { }; struct Moment { - time::ESPTime dt; + ESPTime dt; num_t jd() const; num_t jde() const; @@ -57,18 +59,18 @@ class Sun { void set_latitude(double latitude) { location_.latitude = latitude; } void set_longitude(double longitude) { location_.longitude = longitude; } - optional sunrise(double elevation); - optional sunset(double elevation); - optional sunrise(time::ESPTime date, double elevation); - optional sunset(time::ESPTime date, double elevation); + optional sunrise(double elevation); + optional sunset(double elevation); + optional sunrise(ESPTime date, double elevation); + optional sunset(ESPTime date, double elevation); double elevation(); double azimuth(); protected: internal::HorizontalCoordinate calc_coords_(); - optional calc_event_(bool rising, double zenith); - optional calc_event_(time::ESPTime date, bool rising, double zenith); + optional calc_event_(bool rising, double zenith); + optional calc_event_(ESPTime date, bool rising, double zenith); time::RealTimeClock *time_; internal::GeoLocation location_; diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.h b/esphome/components/sun/text_sensor/sun_text_sensor.h index ad01d64ff1..ce7d21fb86 100644 --- a/esphome/components/sun/text_sensor/sun_text_sensor.h +++ b/esphome/components/sun/text_sensor/sun_text_sensor.h @@ -1,6 +1,8 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/time.h" + #include "esphome/components/sun/sun.h" #include "esphome/components/text_sensor/text_sensor.h" @@ -15,7 +17,7 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent { void set_format(const std::string &format) { format_ = format; } void update() override { - optional res; + optional res; if (this->sunrise_) { res = this->parent_->sunrise(this->elevation_); } else { diff --git a/esphome/components/sun_gtil2/__init__.py b/esphome/components/sun_gtil2/__init__.py new file mode 100644 index 0000000000..f4d46fade7 --- /dev/null +++ b/esphome/components/sun_gtil2/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@Mat931"] +MULTI_CONF = True +DEPENDENCIES = ["uart"] + +CONF_SUN_GTIL2_ID = "sun_gtil2_id" + +sun_gtil2_ns = cg.esphome_ns.namespace("sun_gtil2") + +SunGTIL2Component = sun_gtil2_ns.class_("SunGTIL2", cg.Component, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SunGTIL2Component), + } +).extend(uart.UART_DEVICE_SCHEMA) + + +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) diff --git a/esphome/components/sun_gtil2/sensor.py b/esphome/components/sun_gtil2/sensor.py new file mode 100644 index 0000000000..6d1be9c740 --- /dev/null +++ b/esphome/components/sun_gtil2/sensor.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ICON_FLASH, + UNIT_VOLT, + ICON_THERMOMETER, + UNIT_WATT, + UNIT_CELSIUS, + CONF_TEMPERATURE, +) +from . import SunGTIL2Component, CONF_SUN_GTIL2_ID + +CONF_AC_VOLTAGE = "ac_voltage" +CONF_DC_VOLTAGE = "dc_voltage" +CONF_AC_POWER = "ac_power" +CONF_DC_POWER = "dc_power" +CONF_LIMITER_POWER = "limiter_power" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_SUN_GTIL2_ID): cv.use_id(SunGTIL2Component), + cv.Optional(CONF_AC_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + icon=ICON_FLASH, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + cv.Optional(CONF_DC_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + icon=ICON_FLASH, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + cv.Optional(CONF_AC_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_FLASH, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + cv.Optional(CONF_DC_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_FLASH, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + cv.Optional(CONF_LIMITER_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_FLASH, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + } + ).extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_SUN_GTIL2_ID]) + if ac_voltage_config := config.get(CONF_AC_VOLTAGE): + sens = await sensor.new_sensor(ac_voltage_config) + cg.add(hub.set_ac_voltage(sens)) + if dc_voltage_config := config.get(CONF_DC_VOLTAGE): + sens = await sensor.new_sensor(dc_voltage_config) + cg.add(hub.set_dc_voltage(sens)) + if ac_power_config := config.get(CONF_AC_POWER): + sens = await sensor.new_sensor(ac_power_config) + cg.add(hub.set_ac_power(sens)) + if dc_power_config := config.get(CONF_DC_POWER): + sens = await sensor.new_sensor(dc_power_config) + cg.add(hub.set_dc_power(sens)) + if limiter_power_config := config.get(CONF_LIMITER_POWER): + sens = await sensor.new_sensor(limiter_power_config) + cg.add(hub.set_limiter_power(sens)) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(hub.set_temperature(sens)) diff --git a/esphome/components/sun_gtil2/sun_gtil2.cpp b/esphome/components/sun_gtil2/sun_gtil2.cpp new file mode 100644 index 0000000000..1653f937dd --- /dev/null +++ b/esphome/components/sun_gtil2/sun_gtil2.cpp @@ -0,0 +1,135 @@ +#include "sun_gtil2.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sun_gtil2 { + +static const char *const TAG = "sun_gtil2"; + +static const double NTC_A = 0.0011591051055979914; +static const double NTC_B = 0.00022878183547845582; +static const double NTC_C = 1.0396291358342124e-07; +static const float PULLUP_RESISTANCE = 10000.0f; +static const uint16_t ADC_MAX = 1023; // ADC of the inverter controller, not the ESP + +struct SunGTIL2Message { + uint16_t sync; + uint8_t ac_waveform[277]; + uint8_t frequency; + uint16_t ac_voltage; + uint16_t ac_power; + uint16_t dc_voltage; + uint8_t state; + uint8_t unknown1; + uint8_t unknown2; + uint8_t unknown3; + uint8_t limiter_mode; + uint8_t unknown4; + uint16_t temperature; + uint32_t limiter_power; + uint16_t dc_power; + char serial_number[10]; + uint8_t unknown5; + uint8_t end[39]; +} __attribute__((packed)); + +static const uint16_t MESSAGE_SIZE = sizeof(SunGTIL2Message); + +static_assert(MESSAGE_SIZE == 350, "Expected the message size to be 350 bytes"); + +void SunGTIL2::setup() { this->rx_message_.reserve(MESSAGE_SIZE); } + +void SunGTIL2::loop() { + while (this->available()) { + uint8_t c; + this->read_byte(&c); + this->handle_char_(c); + } +} + +std::string SunGTIL2::state_to_string_(uint8_t state) { + switch (state) { + case 0x02: + return "Starting voltage too low"; + case 0x07: + return "Working"; + default: + return str_sprintf("Unknown (0x%02x)", state); + } +} + +float SunGTIL2::calculate_temperature_(uint16_t adc_value) { + if (adc_value >= ADC_MAX || adc_value == 0) { + return NAN; + } + + float ntc_resistance = PULLUP_RESISTANCE / ((static_cast(ADC_MAX) / adc_value) - 1.0f); + double lr = log(double(ntc_resistance)); + double v = NTC_A + NTC_B * lr + NTC_C * lr * lr * lr; + return float(1.0 / v - 273.15); +} + +void SunGTIL2::handle_char_(uint8_t c) { + if (this->rx_message_.size() > 1 || c == 0x07) { + this->rx_message_.push_back(c); + } else if (!this->rx_message_.empty()) { + this->rx_message_.clear(); + } + if (this->rx_message_.size() < MESSAGE_SIZE) { + return; + } + + SunGTIL2Message msg; + memcpy(&msg, this->rx_message_.data(), MESSAGE_SIZE); + this->rx_message_.clear(); + + if (!((msg.end[0] == 0) && (msg.end[38] == 0x08))) + return; + + ESP_LOGVV(TAG, "Frequency raw value: %02x", msg.frequency); + ESP_LOGVV(TAG, "Unknown values: %02x %02x %02x %02x %02x", msg.unknown1, msg.unknown2, msg.unknown3, msg.unknown4, + msg.unknown5); + +#ifdef USE_SENSOR + if (this->ac_voltage_ != nullptr) + this->ac_voltage_->publish_state(__builtin_bswap16(msg.ac_voltage) / 10.0f); + if (this->dc_voltage_ != nullptr) + this->dc_voltage_->publish_state(__builtin_bswap16(msg.dc_voltage) / 8.0f); + if (this->ac_power_ != nullptr) + this->ac_power_->publish_state(__builtin_bswap16(msg.ac_power) / 10.0f); + if (this->dc_power_ != nullptr) + this->dc_power_->publish_state(__builtin_bswap16(msg.dc_power) / 10.0f); + if (this->limiter_power_ != nullptr) + this->limiter_power_->publish_state(static_cast(__builtin_bswap32(msg.limiter_power)) / 10.0f); + if (this->temperature_ != nullptr) + this->temperature_->publish_state(calculate_temperature_(__builtin_bswap16(msg.temperature))); +#endif +#ifdef USE_TEXT_SENSOR + if (this->state_ != nullptr) { + this->state_->publish_state(this->state_to_string_(msg.state)); + } + if (this->serial_number_ != nullptr) { + std::string serial_number; + serial_number.assign(msg.serial_number, 10); + this->serial_number_->publish_state(serial_number); + } +#endif +} + +void SunGTIL2::dump_config() { +#ifdef USE_SENSOR + LOG_SENSOR("", "AC Voltage", this->ac_voltage_); + LOG_SENSOR("", "DC Voltage", this->dc_voltage_); + LOG_SENSOR("", "AC Power", this->ac_power_); + LOG_SENSOR("", "DC Power", this->dc_power_); + LOG_SENSOR("", "Limiter Power", this->limiter_power_); + LOG_SENSOR("", "Temperature", this->temperature_); +#endif +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR("", "State", this->state_); + LOG_TEXT_SENSOR("", "Serial Number", this->serial_number_); +#endif +} + +} // namespace sun_gtil2 +} // namespace esphome diff --git a/esphome/components/sun_gtil2/sun_gtil2.h b/esphome/components/sun_gtil2/sun_gtil2.h new file mode 100644 index 0000000000..0c29ae695d --- /dev/null +++ b/esphome/components/sun_gtil2/sun_gtil2.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace sun_gtil2 { + +class SunGTIL2 : public Component, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::LATE; } + void setup() override; + void loop() override; + void dump_config() override; + +#ifdef USE_SENSOR + void set_ac_voltage(sensor::Sensor *sensor) { ac_voltage_ = sensor; } + void set_dc_voltage(sensor::Sensor *sensor) { dc_voltage_ = sensor; } + void set_ac_power(sensor::Sensor *sensor) { ac_power_ = sensor; } + void set_dc_power(sensor::Sensor *sensor) { dc_power_ = sensor; } + void set_limiter_power(sensor::Sensor *sensor) { limiter_power_ = sensor; } + void set_temperature(sensor::Sensor *sensor) { temperature_ = sensor; } +#endif +#ifdef USE_TEXT_SENSOR + void set_state(text_sensor::TextSensor *text_sensor) { state_ = text_sensor; } + void set_serial_number(text_sensor::TextSensor *text_sensor) { serial_number_ = text_sensor; } +#endif + + protected: + std::string state_to_string_(uint8_t state); +#ifdef USE_SENSOR + sensor::Sensor *ac_voltage_{nullptr}; + sensor::Sensor *dc_voltage_{nullptr}; + sensor::Sensor *ac_power_{nullptr}; + sensor::Sensor *dc_power_{nullptr}; + sensor::Sensor *limiter_power_{nullptr}; + sensor::Sensor *temperature_{nullptr}; +#endif +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *state_{nullptr}; + text_sensor::TextSensor *serial_number_{nullptr}; +#endif + + float calculate_temperature_(uint16_t adc_value); + void handle_char_(uint8_t c); + std::vector rx_message_; +}; + +} // namespace sun_gtil2 +} // namespace esphome diff --git a/esphome/components/sun_gtil2/text_sensor.py b/esphome/components/sun_gtil2/text_sensor.py new file mode 100644 index 0000000000..d9d3e3ca66 --- /dev/null +++ b/esphome/components/sun_gtil2/text_sensor.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_STATE +from . import SunGTIL2Component, CONF_SUN_GTIL2_ID + +CONF_SERIAL_NUMBER = "serial_number" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_SUN_GTIL2_ID): cv.use_id(SunGTIL2Component), + cv.Optional(CONF_STATE): text_sensor.text_sensor_schema( + text_sensor.TextSensor + ), + cv.Optional(CONF_SERIAL_NUMBER): text_sensor.text_sensor_schema( + text_sensor.TextSensor + ), + } + ).extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_SUN_GTIL2_ID]) + if state_config := config.get(CONF_STATE): + sens = await text_sensor.new_text_sensor(state_config) + cg.add(hub.set_state(sens)) + if serial_number_config := config.get(CONF_SERIAL_NUMBER): + sens = await text_sensor.new_text_sensor(serial_number_config) + cg.add(hub.set_serial_number(sens)) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 21cbe3dfe4..0f159f69ec 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -1,8 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition, maybe_simple_id -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, @@ -14,6 +14,7 @@ from esphome.const import ( CONF_ON_TURN_ON, CONF_RESTORE_MODE, CONF_TRIGGER_ID, + CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH, @@ -64,22 +65,26 @@ SwitchTurnOffTrigger = switch_ns.class_( validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True) -_SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( - { - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), - cv.Optional(CONF_INVERTED): cv.boolean, - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), - } - ), - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - } +_SWITCH_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), + cv.Optional(CONF_INVERTED): cv.boolean, + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), + } + ), + cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), + } + ), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + } + ) ) _UNDEF = object() @@ -138,8 +143,8 @@ SWITCH_SCHEMA = switch_schema() # for compatibility async def setup_switch_core_(var, config): await setup_entity(var, config) - if CONF_INVERTED in config: - cg.add(var.set_inverted(config[CONF_INVERTED])) + if (inverted := config.get(CONF_INVERTED)) is not None: + cg.add(var.set_inverted(inverted)) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -147,12 +152,15 @@ async def setup_switch_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) - if CONF_DEVICE_CLASS in config: - cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 9daac4ee23..b5395a2c83 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -8,6 +8,13 @@ namespace esphome { namespace switch_ { +#define SUB_SWITCH(name) \ + protected: \ + switch_::Switch *name##_switch_{nullptr}; \ +\ + public: \ + void set_##name##_switch(switch_::Switch *s) { this->name##_switch_ = s; } + // bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled const int RESTORE_MODE_ON_MASK = 0x01; const int RESTORE_MODE_PERSISTENT_MASK = 0x02; diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index faef940125..e4f79dc2bc 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, + CONF_OPEN_DRAIN, ) CONF_KEYPAD = "keypad" @@ -79,6 +80,8 @@ def validate_mode(value): raise cv.Invalid("Pulldown only available with input") if value[CONF_PULLUP] and value[CONF_PULLDOWN]: raise cv.Invalid("Can only have one of pullup or pulldown") + if value[CONF_OPEN_DRAIN] and not value[CONF_OUTPUT]: + raise cv.Invalid("Open drain available only with output") return value @@ -94,6 +97,7 @@ SX1509_PIN_SCHEMA = cv.All( cv.Optional(CONF_PULLUP, default=False): cv.boolean, cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, }, validate_mode, ), diff --git a/esphome/components/sx1509/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py index bbf0e5d0bc..280b5ad90c 100644 --- a/esphome/components/sx1509/binary_sensor/__init__.py +++ b/esphome/components/sx1509/binary_sensor/__init__.py @@ -1,12 +1,10 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor +from esphome.const import CONF_ROW, CONF_COL from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID -CONF_ROW = "row" -CONF_COL = "col" - DEPENDENCIES = ["sx1509"] SX1509BinarySensor = sx1509_ns.class_("SX1509BinarySensor", binary_sensor.BinarySensor) @@ -14,8 +12,8 @@ SX1509BinarySensor = sx1509_ns.class_("SX1509BinarySensor", binary_sensor.Binary CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(SX1509BinarySensor).extend( { cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), - cv.Required(CONF_ROW): cv.int_range(min=0, max=4), - cv.Required(CONF_COL): cv.int_range(min=0, max=4), + cv.Required(CONF_ROW): cv.int_range(min=0, max=7), + cv.Required(CONF_COL): cv.int_range(min=0, max=7), } ) diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index d0a84b99ff..855a90bacd 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -68,42 +68,81 @@ void SX1509Component::digital_write(uint8_t pin, bool bit_value) { uint16_t temp_reg_data = 0; this->read_byte_16(REG_DATA_B, &temp_reg_data); if (bit_value) { - temp_reg_data |= (1 << pin); + output_state_ |= (1 << pin); // set bit in shadow register } else { - temp_reg_data &= ~(1 << pin); + output_state_ &= ~(1 << pin); // reset bit shadow register + } + for (uint16_t b = 0x8000; b; b >>= 1) { + if ((~ddr_mask_) & b) { // transfer bits of outputs, but don't mess with inputs + if (output_state_ & b) { + temp_reg_data |= b; + } else { + temp_reg_data &= ~b; + } + } } this->write_byte_16(REG_DATA_B, temp_reg_data); } } void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) { + ESP_LOGI(TAG, "Configuring pin %u with flags %x", pin, flags); + + uint16_t temp_word = 0; + this->read_byte_16(REG_DIR_B, &this->ddr_mask_); - if (flags == gpio::FLAG_OUTPUT) { + if (flags & gpio::FLAG_OUTPUT) { + // Always disable input buffer + this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word); + temp_word |= (1 << pin); + this->write_byte_16(REG_INPUT_DISABLE_B, temp_word); + + if (flags & gpio::FLAG_OPEN_DRAIN) { + // Pullup must be disabled for open drain mode + this->read_byte_16(REG_PULL_UP_B, &temp_word); + temp_word &= ~(1 << pin); + this->write_byte_16(REG_PULL_UP_B, temp_word); + this->read_byte_16(REG_OPEN_DRAIN_B, &temp_word); + temp_word |= (1 << pin); + this->write_byte_16(REG_OPEN_DRAIN_B, temp_word); + ESP_LOGD(TAG, "Open drain output mode set for %u", pin); + } else { + ESP_LOGD(TAG, "Output Mode for %u", pin); + } + + // Set direction to output this->ddr_mask_ &= ~(1 << pin); + this->write_byte_16(REG_DIR_B, this->ddr_mask_); } else { - this->ddr_mask_ |= (1 << pin); + ESP_LOGD(TAG, "Input Mode for %u", pin); - uint16_t temp_pullup; - this->read_byte_16(REG_PULL_UP_B, &temp_pullup); - uint16_t temp_pulldown; - this->read_byte_16(REG_PULL_DOWN_B, &temp_pulldown); + // Always enable input buffer + this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word); + temp_word &= ~(1 << pin); + this->write_byte_16(REG_INPUT_DISABLE_B, temp_word); + // Pullup + this->read_byte_16(REG_PULL_UP_B, &temp_word); if (flags & gpio::FLAG_PULLUP) { - temp_pullup |= (1 << pin); + temp_word |= (1 << pin); } else { - temp_pullup &= ~(1 << pin); + temp_word &= ~(1 << pin); } + this->write_byte_16(REG_PULL_UP_B, temp_word); + // Pulldown + this->read_byte_16(REG_PULL_DOWN_B, &temp_word); if (flags & gpio::FLAG_PULLDOWN) { - temp_pulldown |= (1 << pin); + temp_word |= (1 << pin); } else { - temp_pulldown &= ~(1 << pin); + temp_word &= ~(1 << pin); } + this->write_byte_16(REG_PULL_DOWN_B, temp_word); - this->write_byte_16(REG_PULL_UP_B, temp_pullup); - this->write_byte_16(REG_PULL_DOWN_B, temp_pulldown); + // Set direction to input + this->ddr_mask_ |= (1 << pin); + this->write_byte_16(REG_DIR_B, this->ddr_mask_); } - this->write_byte_16(REG_DIR_B, this->ddr_mask_); } void SX1509Component::setup_led_driver(uint8_t pin) { @@ -134,6 +173,7 @@ void SX1509Component::setup_led_driver(uint8_t pin) { this->read_byte_16(REG_DATA_B, &temp_word); temp_word &= ~(1 << pin); + output_state_ &= ~(1 << pin); this->write_byte_16(REG_DATA_B, temp_word); } diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index 8e3b41e233..9e4f31aab0 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -61,6 +61,7 @@ class SX1509Component : public Component, public i2c::I2CDevice { uint16_t ddr_mask_ = 0x00; uint16_t input_mask_ = 0x00; uint16_t port_mask_ = 0x00; + uint16_t output_state_ = 0x00; bool has_keypad_ = false; uint8_t rows_ = 0; uint8_t cols_ = 0; diff --git a/esphome/components/tc74/__init__.py b/esphome/components/tc74/__init__.py new file mode 100644 index 0000000000..c1407c7cad --- /dev/null +++ b/esphome/components/tc74/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@sethgirvan"] diff --git a/esphome/components/tc74/sensor.py b/esphome/components/tc74/sensor.py new file mode 100644 index 0000000000..18fc2d9a42 --- /dev/null +++ b/esphome/components/tc74/sensor.py @@ -0,0 +1,32 @@ +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, +) + +CODEOWNERS = ["@sethgirvan"] +DEPENDENCIES = ["i2c"] + +tc74_ns = cg.esphome_ns.namespace("tc74") +TC74Component = tc74_ns.class_("TC74Component", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + TC74Component, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +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/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp new file mode 100644 index 0000000000..2acb71f921 --- /dev/null +++ b/esphome/components/tc74/tc74.cpp @@ -0,0 +1,68 @@ +// Based on the TC74 datasheet https://ww1.microchip.com/downloads/en/DeviceDoc/21462D.pdf + +#include "tc74.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tc74 { + +static const char *const TAG = "tc74"; + +static const uint8_t TC74_REGISTER_TEMPERATURE = 0x00; +static const uint8_t TC74_REGISTER_CONFIGURATION = 0x01; +static const uint8_t TC74_DATA_READY_MASK = 0x40; + +// It is possible the "Data Ready" bit will not be set if the TC74 has not been powered on for at least 250ms, so it not +// being set does not constitute a failure. +void TC74Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TC74..."); + uint8_t config_reg; + if (this->read_register(TC74_REGISTER_CONFIGURATION, &config_reg, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + this->data_ready_ = config_reg & TC74_DATA_READY_MASK; +} + +void TC74Component::update() { this->read_temperature_(); } + +void TC74Component::dump_config() { + LOG_SENSOR("", "TC74", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Connection with TC74 failed!"); + } + LOG_UPDATE_INTERVAL(this); +} + +void TC74Component::read_temperature_() { + if (!this->data_ready_) { + uint8_t config_reg; + if (this->read_register(TC74_REGISTER_CONFIGURATION, &config_reg, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + if (config_reg & TC74_DATA_READY_MASK) { + this->data_ready_ = true; + } else { + ESP_LOGD(TAG, "TC74 not ready"); + return; + } + } + + uint8_t temperature_reg; + if (this->read_register(TC74_REGISTER_TEMPERATURE, &temperature_reg, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + ESP_LOGD(TAG, "Got Temperature=%d °C", temperature_reg); + this->publish_state(temperature_reg); + this->status_clear_warning(); +} + +float TC74Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace tc74 +} // namespace esphome diff --git a/esphome/components/tc74/tc74.h b/esphome/components/tc74/tc74.h new file mode 100644 index 0000000000..5d70c05420 --- /dev/null +++ b/esphome/components/tc74/tc74.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tc74 { + +class TC74Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + /// Setup the sensor and check connection. + void setup() override; + void dump_config() override; + /// Update the sensor value (temperature). + void update() override; + + float get_setup_priority() const override; + + protected: + /// Internal method to read the temperature from the component after it has been scheduled. + void read_temperature_(); + + bool data_ready_ = false; +}; + +} // namespace tc74 +} // namespace esphome diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index caa3dd0655..770fd5e47c 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -7,23 +7,27 @@ namespace tca9548a { static const char *const TAG = "tca9548a"; i2c::ErrorCode TCA9548AChannel::readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) { - auto err = parent_->switch_to_channel(channel_); + auto err = this->parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - return parent_->bus_->readv(address, buffers, cnt); + err = this->parent_->bus_->readv(address, buffers, cnt); + this->parent_->disable_all_channels(); + return err; } i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) { - auto err = parent_->switch_to_channel(channel_); + auto err = this->parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - return parent_->bus_->writev(address, buffers, cnt, stop); + err = this->parent_->bus_->writev(address, buffers, cnt, stop); + this->parent_->disable_all_channels(); + return err; } void TCA9548AComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up TCA9548A..."); uint8_t status = 0; if (this->read(&status, 1) != i2c::ERROR_OK) { - ESP_LOGI(TAG, "TCA9548A failed"); + ESP_LOGE(TAG, "TCA9548A failed"); this->mark_failed(); return; } @@ -37,15 +41,16 @@ void TCA9548AComponent::dump_config() { i2c::ErrorCode TCA9548AComponent::switch_to_channel(uint8_t channel) { if (this->is_failed()) return i2c::ERROR_NOT_INITIALIZED; - if (current_channel_ == channel) - return i2c::ERROR_OK; uint8_t channel_val = 1 << channel; - auto err = this->write(&channel_val, 1); - if (err == i2c::ERROR_OK) { - current_channel_ = channel; + return this->write(&channel_val, 1); +} + +void TCA9548AComponent::disable_all_channels() { + if (this->write(&TCA9548A_DISABLE_CHANNELS_COMMAND, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to disable all channels."); + this->status_set_error(); // couldn't disable channels, set error status } - return err; } } // namespace tca9548a diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 02553f8cd0..08f1674d11 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -6,6 +6,8 @@ namespace esphome { namespace tca9548a { +static const uint8_t TCA9548A_DISABLE_CHANNELS_COMMAND = 0x00; + class TCA9548AComponent; class TCA9548AChannel : public i2c::I2CBus { public: @@ -28,10 +30,10 @@ class TCA9548AComponent : public Component, public i2c::I2CDevice { void update(); i2c::ErrorCode switch_to_channel(uint8_t channel); + void disable_all_channels(); protected: friend class TCA9548AChannel; - uint8_t current_channel_ = 255; }; } // namespace tca9548a } // namespace esphome diff --git a/esphome/components/tca9555/__init__.py b/esphome/components/tca9555/__init__.py new file mode 100644 index 0000000000..db0451d4e6 --- /dev/null +++ b/esphome/components/tca9555/__init__.py @@ -0,0 +1,72 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, +) + +CODEOWNERS = ["@mobrembski"] + +AUTO_LOAD = ["gpio_expander"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +tca9555_ns = cg.esphome_ns.namespace("tca9555") + +TCA9555Component = tca9555_ns.class_("TCA9555Component", cg.Component, i2c.I2CDevice) +TCA9555GPIOPin = tca9555_ns.class_("TCA9555GPIOPin", cg.GPIOPin) + +CONF_TCA9555 = "tca9555" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(TCA9555Component), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x21)) +) + + +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) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + return value + + +TCA9555_PIN_SCHEMA = pins.gpio_base_schema( + TCA9555GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertable=True, +).extend( + { + cv.Required(CONF_TCA9555): cv.use_id(TCA9555Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_TCA9555, TCA9555_PIN_SCHEMA) +async def tca9555_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_TCA9555]) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp new file mode 100644 index 0000000000..cf0894427f --- /dev/null +++ b/esphome/components/tca9555/tca9555.cpp @@ -0,0 +1,140 @@ +#include "tca9555.h" +#include "esphome/core/log.h" + +static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00; +static const uint8_t TCA9555_INPUT_PORT_REGISTER_1 = 0x01; +static const uint8_t TCA9555_OUTPUT_PORT_REGISTER_0 = 0x02; +static const uint8_t TCA9555_OUTPUT_PORT_REGISTER_1 = 0x03; +static const uint8_t TCA9555_POLARITY_REGISTER_0 = 0x04; +static const uint8_t TCA9555_POLARITY_REGISTER_1 = 0x05; +static const uint8_t TCA9555_CONFIGURATION_PORT_0 = 0x06; +static const uint8_t TCA9555_CONFIGURATION_PORT_1 = 0x07; + +namespace esphome { +namespace tca9555 { + +static const char *const TAG = "tca9555"; + +void TCA9555Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TCA9555..."); + if (!this->read_gpio_modes_()) { + this->mark_failed(); + return; + } + if (!this->read_gpio_outputs_()) { + this->mark_failed(); + return; + } +} +void TCA9555Component::dump_config() { + ESP_LOGCONFIG(TAG, "TCA9555:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TCA9555 failed!"); + } +} +void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + // Set mode mask bit + this->mode_mask_ |= 1 << pin; + } else if (flags == gpio::FLAG_OUTPUT) { + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + } + // Write GPIO to enable input mode + this->write_gpio_modes_(); +} +void TCA9555Component::loop() { this->reset_pin_cache_(); } + +bool TCA9555Component::read_gpio_outputs_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + if (!this->read_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { + this->status_set_warning("Failed to read output register"); + return false; + } + this->output_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); + this->status_clear_warning(); + return true; +} + +bool TCA9555Component::read_gpio_modes_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + bool success = this->read_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2); + if (!success) { + this->status_set_warning("Failed to read mode register"); + return false; + } + this->mode_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); + + this->status_clear_warning(); + return true; +} +bool TCA9555Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) + return false; + bool success; + uint8_t data[2]; + success = this->read_bytes(TCA9555_INPUT_PORT_REGISTER_0, data, 2); + this->input_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); + + if (!success) { + this->status_set_warning("Failed to read input register"); + return false; + } + + this->status_clear_warning(); + return true; +} + +void TCA9555Component::digital_write_hw(uint8_t pin, bool value) { + if (this->is_failed()) + return; + + if (value) { + this->output_mask_ |= (1 << pin); + } else { + this->output_mask_ &= ~(1 << pin); + } + + uint8_t data[2]; + data[0] = this->output_mask_; + data[1] = this->output_mask_ >> 8; + if (!this->write_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { + this->status_set_warning("Failed to write output register"); + return; + } + + this->status_clear_warning(); +} + +bool TCA9555Component::write_gpio_modes_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + + data[0] = this->mode_mask_; + data[1] = this->mode_mask_ >> 8; + if (!this->write_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2)) { + this->status_set_warning("Failed to write mode register"); + return false; + } + this->status_clear_warning(); + return true; +} + +bool TCA9555Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +float TCA9555Component::get_setup_priority() const { return setup_priority::IO; } + +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_); } + +} // namespace tca9555 +} // namespace esphome diff --git a/esphome/components/tca9555/tca9555.h b/esphome/components/tca9555/tca9555.h new file mode 100644 index 0000000000..ea464db043 --- /dev/null +++ b/esphome/components/tca9555/tca9555.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tca9555 { + +class TCA9555Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + TCA9555Component() = default; + + /// Check i2c availability and setup masks + void setup() override; + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + + void dump_config() override; + + void loop() override; + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + + /// Mask for the pin mode - 1 means output, 0 means input + uint16_t mode_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint16_t output_mask_{0x00}; + /// The state read in digital_read_hw - 1 means HIGH, 0 means LOW + uint16_t input_mask_{0x00}; + + bool read_gpio_modes_(); + bool write_gpio_modes_(); + bool read_gpio_outputs_(); +}; + +/// Helper class to expose a TCA9555 pin as an internal input GPIO pin. +class TCA9555GPIOPin : public GPIOPin, public Parented { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + 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; } + + protected: + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace tca9555 +} // namespace esphome diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 88c59eb761..0830004b5a 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -1,6 +1,8 @@ #include "tcs34725.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include +#include "esphome/core/helpers.h" namespace esphome { namespace tcs34725 { @@ -13,10 +15,7 @@ static const uint8_t TCS34725_REGISTER_ID = TCS34725_COMMAND_BIT | 0x12; static const uint8_t TCS34725_REGISTER_ATIME = TCS34725_COMMAND_BIT | 0x01; static const uint8_t TCS34725_REGISTER_CONTROL = TCS34725_COMMAND_BIT | 0x0F; static const uint8_t TCS34725_REGISTER_ENABLE = TCS34725_COMMAND_BIT | 0x00; -static const uint8_t TCS34725_REGISTER_CDATAL = TCS34725_COMMAND_BIT | 0x14; -static const uint8_t TCS34725_REGISTER_RDATAL = TCS34725_COMMAND_BIT | 0x16; -static const uint8_t TCS34725_REGISTER_GDATAL = TCS34725_COMMAND_BIT | 0x18; -static const uint8_t TCS34725_REGISTER_BDATAL = TCS34725_COMMAND_BIT | 0x1A; +static const uint8_t TCS34725_REGISTER_CRGBDATAL = TCS34725_COMMAND_BIT | 0x14; void TCS34725Component::setup() { ESP_LOGCONFIG(TAG, "Setting up TCS34725..."); @@ -74,20 +73,21 @@ float TCS34725Component::get_setup_priority() const { return setup_priority::DAT * @return Color temperature in degrees Kelvin */ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c) { - float r2, g2, b2; /* RGB values minus IR component */ - float sat; /* Digital saturation level */ - float ir; /* Inferred IR content */ + float sat; /* Digital saturation level */ - this->illuminance_ = 0; // Assign 0 value before calculation - this->color_temperature_ = 0; + this->illuminance_ = NAN; + this->color_temperature_ = NAN; - const float ga = this->glass_attenuation_; // Glass Attenuation Factor - static const float DF = 310.f; // Device Factor - static const float R_COEF = 0.136f; // - static const float G_COEF = 1.f; // used in lux computation - static const float B_COEF = -0.444f; // - static const float CT_COEF = 3810.f; // Color Temperature Coefficient - static const float CT_OFFSET = 1391.f; // Color Temperatuer Offset + const float ga = this->glass_attenuation_; // Glass Attenuation Factor + static const float DF = 310.f; // Device Factor + static const float R_COEF = 0.136f; // + static const float G_COEF = 1.f; // used in lux computation + static const float B_COEF = -0.444f; // + static const float CT_COEF = 3810.f; // Color Temperature Coefficient + static const float CT_OFFSET = 1391.f; // Color Temperatuer Offset + static const float MAX_ILLUMINANCE = 100000.0f; // Cap illuminance at 100,000 lux + static const float MAX_COLOR_TEMPERATURE = 15000.0f; // Maximum expected color temperature in Kelvin + static const float MIN_COLOR_TEMPERATURE = 1000.0f; // Maximum reasonable color temperature in Kelvin if (c == 0) { return; @@ -138,69 +138,66 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u if (c >= sat) { if (this->integration_time_auto_) { ESP_LOGI(TAG, "Saturation too high, sample discarded, autogain ongoing"); + return; } else { - ESP_LOGW( - TAG, - "Saturation too high, sample with saturation %.1f and clear %d treat values carefully or use grey filter", - sat, c); - } - } - - /* AMS RGB sensors have no IR channel, so the IR content must be */ - /* calculated indirectly. */ - ir = ((r + g + b) > c) ? (r + g + b - c) / 2 : 0; - - /* Remove the IR component from the raw RGB values */ - r2 = r - ir; - g2 = g - ir; - b2 = b - ir; - - // discarding super low values? not recemmonded, and avoided by using auto gain. - if (r2 == 0) { - // legacy code - if (!this->integration_time_auto_) { ESP_LOGW(TAG, - "No light detected on red channel, switch to auto gain or adjust timing, values will be unreliable"); + "Saturation too high, sample with saturation %.1f and clear %d lux/color temperature cannot reliably " + "calculated, reduce integration/gain or use a grey filter.", + sat, c); return; } } // Lux Calculation (DN40 3.2) - float g1 = R_COEF * r2 + G_COEF * g2 + B_COEF * b2; + float g1 = R_COEF * (float) r + G_COEF * (float) g + B_COEF * (float) b; float cpl = (this->integration_time_ * this->gain_) / (ga * DF); - this->illuminance_ = g1 / cpl; + + this->illuminance_ = std::max(g1 / cpl, 0.0f); + + if (this->illuminance_ > MAX_ILLUMINANCE) { + ESP_LOGW(TAG, "Calculated illuminance greater than limit (%f), setting to NAN", this->illuminance_); + this->illuminance_ = NAN; + return; + } + + if (r == 0) { + ESP_LOGW(TAG, "Red channel is zero, cannot compute color temperature"); + return; + } // Color Temperature Calculation (DN40) /* A simple method of measuring color temp is to use the ratio of blue */ - /* to red light, taking IR cancellation into account. */ - this->color_temperature_ = (CT_COEF * b2) / /** Color temp coefficient. */ - r2 + - CT_OFFSET; /** Color temp offset. */ + /* to red light. */ + + this->color_temperature_ = (CT_COEF * (float) b) / (float) r + CT_OFFSET; + + // Ensure the color temperature stays within reasonable bounds + if (this->color_temperature_ < MIN_COLOR_TEMPERATURE) { + ESP_LOGW(TAG, "Calculated color temperature value too low (%f), setting to NAN", this->color_temperature_); + this->color_temperature_ = NAN; + } else if (this->color_temperature_ > MAX_COLOR_TEMPERATURE) { + ESP_LOGW(TAG, "Calculated color temperature value too high (%f), setting to NAN", this->color_temperature_); + this->color_temperature_ = NAN; + } } void TCS34725Component::update() { - uint16_t raw_c; - uint16_t raw_r; - uint16_t raw_g; - uint16_t raw_b; + uint8_t data[8]; // Buffer to hold the 8 bytes (2 bytes for each of the 4 channels) - if (this->read_data_register_(TCS34725_REGISTER_CDATAL, raw_c) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - if (this->read_data_register_(TCS34725_REGISTER_RDATAL, raw_r) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - if (this->read_data_register_(TCS34725_REGISTER_GDATAL, raw_g) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - if (this->read_data_register_(TCS34725_REGISTER_BDATAL, raw_b) != i2c::ERROR_OK) { + // Perform burst + if (this->read_register(TCS34725_REGISTER_CRGBDATAL, data, 8) != i2c::ERROR_OK) { this->status_set_warning(); + ESP_LOGW(TAG, "Error reading TCS34725 sensor data"); return; } + + // Extract the data + uint16_t raw_c = encode_uint16(data[1], data[0]); // Clear channel + uint16_t raw_r = encode_uint16(data[3], data[2]); // Red channel + uint16_t raw_g = encode_uint16(data[5], data[4]); // Green channel + uint16_t raw_b = encode_uint16(data[7], data[6]); // Blue channel + ESP_LOGV(TAG, "Raw values clear=%d red=%d green=%d blue=%d", raw_c, raw_r, raw_g, raw_b); float channel_c; @@ -211,7 +208,7 @@ void TCS34725Component::update() { if (raw_c == 0) { channel_c = channel_r = channel_g = channel_b = 0.0f; } else { - float max_count = this->integration_time_ * 1024.0f / 2.4; + float max_count = this->integration_time_ <= 153.6f ? this->integration_time_ * 1024.0f / 2.4f : 65535.0f; float sum = raw_c; channel_r = raw_r / sum * 100.0f; channel_g = raw_g / sum * 100.0f; @@ -254,7 +251,8 @@ void TCS34725Component::update() { // change integration time an gain to achieve maximum resolution an dynamic range // calculate optimal integration time to achieve 70% satuaration float integration_time_ideal; - integration_time_ideal = 60 / ((float) raw_c / 655.35) * this->integration_time_; + + integration_time_ideal = 60 / ((float) std::max((uint16_t) 1, raw_c) / 655.35f) * this->integration_time_; uint8_t gain_reg_val_new = this->gain_reg_; // increase gain if less than 20% of white channel used and high integration time diff --git a/esphome/components/tem3200/__init__.py b/esphome/components/tem3200/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tem3200/sensor.py b/esphome/components/tem3200/sensor.py new file mode 100644 index 0000000000..5cd27433d0 --- /dev/null +++ b/esphome/components/tem3200/sensor.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor + +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@bakerkj"] +DEPENDENCIES = ["i2c"] + +tem3200_ns = cg.esphome_ns.namespace("tem3200") + +TEM3200Component = tem3200_ns.class_( + "TEM3200Component", cg.PollingComponent, i2c.I2CDevice +) + +CONF_RAW_PRESSURE = "raw_pressure" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TEM3200Component), + 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_RAW_PRESSURE): sensor.sensor_schema( + accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +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) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if raw_pressure_config := config.get(CONF_RAW_PRESSURE): + sens = await sensor.new_sensor(raw_pressure_config) + cg.add(var.set_raw_pressure_sensor(sens)) diff --git a/esphome/components/tem3200/tem3200.cpp b/esphome/components/tem3200/tem3200.cpp new file mode 100644 index 0000000000..d8ce48af90 --- /dev/null +++ b/esphome/components/tem3200/tem3200.cpp @@ -0,0 +1,151 @@ +#include "tem3200.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tem3200 { + +static const char *const TAG = "tem3200"; + +enum ErrorCode { + NONE = 0, + RESERVED = 1, + STALE = 2, + FAULT = 3, +}; + +void TEM3200Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TEM3200..."); + + uint8_t status(NONE); + uint16_t raw_temperature(0); + uint16_t raw_pressure(0); + + i2c::ErrorCode err = this->read_(status, raw_temperature, raw_pressure); + if (err != i2c::ERROR_OK) { + ESP_LOGCONFIG(TAG, " I2C Communication Failed..."); + this->mark_failed(); + return; + } + + switch (status) { + case RESERVED: + ESP_LOGE(TAG, "Invalid RESERVED Device Status"); + this->mark_failed(); + return; + case FAULT: + ESP_LOGE(TAG, "FAULT condition in the SSC or sensing element"); + this->mark_failed(); + return; + case STALE: + ESP_LOGE(TAG, "STALE data. Data has not been updated since last fetch"); + this->status_set_warning(); + break; + } + ESP_LOGCONFIG(TAG, " Success..."); +} + +void TEM3200Component::dump_config() { + ESP_LOGCONFIG(TAG, "TEM3200:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Raw Pressure", this->raw_pressure_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); +} + +float TEM3200Component::get_setup_priority() const { return setup_priority::DATA; } + +i2c::ErrorCode TEM3200Component::read_(uint8_t &status, uint16_t &raw_temperature, uint16_t &raw_pressure) { + uint8_t response[4] = {0x00, 0x00, 0x00, 0x00}; + + // initiate data read + i2c::ErrorCode err = this->read(response, 4); + if (err != i2c::ERROR_OK) { + return err; + } + + // extract top 2 bits of first byte for status + status = (ErrorCode) (response[0] & 0xc0) >> 6; + if (status == RESERVED || status == FAULT) { + return i2c::ERROR_OK; + } + + // if data is stale; reread + if (status == STALE) { + // wait for measurement 2ms + delay(2); + + err = this->read(response, 4); + if (err != i2c::ERROR_OK) { + return err; + } + } + + // extract top 2 bits of first byte for status + status = (ErrorCode) (response[0] & 0xc0) >> 6; + if (status == RESERVED || status == FAULT) { + return i2c::ERROR_OK; + } + + // extract top 6 bits of first byte and all bits of second byte for pressure + raw_pressure = (((response[0] & 0x3f)) << 8 | response[1]); + + // extract all bytes of 3rd byte and top 3 bits of fourth byte for temperature + raw_temperature = ((response[2] << 3) | (response[3] & 0xe0) >> 5); + + return i2c::ERROR_OK; +} + +inline float convert_temperature(uint16_t raw_temperature) { + const float temperature_bits_span = 2048; + const float temperature_max = 150; + const float temperature_min = -50; + const float temperature_span = temperature_max - temperature_min; + + float temperature = (raw_temperature * temperature_span / temperature_bits_span) + temperature_min; + + return temperature; +} + +void TEM3200Component::update() { + uint8_t status(NONE); + uint16_t raw_temperature(0); + uint16_t raw_pressure(0); + i2c::ErrorCode err = this->read_(status, raw_temperature, raw_pressure); + + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "I2C Communication Failed"); + this->status_set_warning(); + return; + } + + switch (status) { + case RESERVED: + ESP_LOGE(TAG, "Failed: Device return RESERVED status"); + this->status_set_warning(); + return; + case FAULT: + ESP_LOGE(TAG, "Failed: FAULT condition in the SSC or sensing element"); + this->mark_failed(); + return; + case STALE: + ESP_LOGE(TAG, "Warning: STALE data. Data has not been updated since last fetch"); + this->status_set_warning(); + return; + } + + float temperature = convert_temperature(raw_temperature); + + ESP_LOGD(TAG, "Got raw pressure=%d, temperature=%.1f°C", raw_pressure, temperature); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->raw_pressure_sensor_ != nullptr) + this->raw_pressure_sensor_->publish_state(raw_pressure); + + this->status_clear_warning(); +} + +} // namespace tem3200 +} // namespace esphome diff --git a/esphome/components/tem3200/tem3200.h b/esphome/components/tem3200/tem3200.h new file mode 100644 index 0000000000..c84a2aba21 --- /dev/null +++ b/esphome/components/tem3200/tem3200.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tem3200 { + +/// This class implements support for the tem3200 pressure and temperature i2c sensors. +class TEM3200Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_raw_pressure_sensor(sensor::Sensor *raw_pressure_sensor) { + this->raw_pressure_sensor_ = raw_pressure_sensor; + } + + float get_setup_priority() const override; + void setup() override; + void dump_config() override; + void update() override; + + protected: + i2c::ErrorCode read_(uint8_t &status, uint16_t &raw_temperature, uint16_t &raw_pressure); + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *raw_pressure_sensor_{nullptr}; +}; + +} // namespace tem3200 +} // namespace esphome diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py new file mode 100644 index 0000000000..3555f2fafd --- /dev/null +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -0,0 +1,159 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ( + binary_sensor, + alarm_control_panel, +) +from esphome.const import ( + CONF_ID, + CONF_BINARY_SENSORS, + CONF_INPUT, + CONF_RESTORE_MODE, +) +from .. import template_ns + +CODEOWNERS = ["@grahambrown11", "@hwstar"] + +CONF_CODES = "codes" +CONF_BYPASS_ARMED_HOME = "bypass_armed_home" +CONF_BYPASS_ARMED_NIGHT = "bypass_armed_night" +CONF_CHIME = "chime" +CONF_TRIGGER_MODE = "trigger_mode" +CONF_REQUIRES_CODE_TO_ARM = "requires_code_to_arm" +CONF_ARMING_HOME_TIME = "arming_home_time" +CONF_ARMING_NIGHT_TIME = "arming_night_time" +CONF_ARMING_AWAY_TIME = "arming_away_time" +CONF_PENDING_TIME = "pending_time" +CONF_TRIGGER_TIME = "trigger_time" + + +FLAG_NORMAL = "normal" +FLAG_BYPASS_ARMED_HOME = "bypass_armed_home" +FLAG_BYPASS_ARMED_NIGHT = "bypass_armed_night" +FLAG_CHIME = "chime" + +BinarySensorFlags = { + FLAG_NORMAL: 1 << 0, + FLAG_BYPASS_ARMED_HOME: 1 << 1, + FLAG_BYPASS_ARMED_NIGHT: 1 << 2, + FLAG_CHIME: 1 << 3, +} + + +TemplateAlarmControlPanel = template_ns.class_( + "TemplateAlarmControlPanel", alarm_control_panel.AlarmControlPanel, cg.Component +) + +TemplateAlarmControlPanelRestoreMode = template_ns.enum( + "TemplateAlarmControlPanelRestoreMode" +) +RESTORE_MODES = { + "ALWAYS_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_ALWAYS_DISARMED, + "RESTORE_DEFAULT_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, +} + +AlarmSensorType = template_ns.enum("AlarmSensorType") + +ALARM_SENSOR_TYPES = { + "DELAYED": AlarmSensorType.ALARM_SENSOR_TYPE_DELAYED, + "INSTANT": AlarmSensorType.ALARM_SENSOR_TYPE_INSTANT, + "DELAYED_FOLLOWER": AlarmSensorType.ALARM_SENSOR_TYPE_DELAYED_FOLLOWER, +} + + +def validate_config(config): + if config.get(CONF_REQUIRES_CODE_TO_ARM, False) and not config.get(CONF_CODES, []): + raise cv.Invalid( + f"{CONF_REQUIRES_CODE_TO_ARM} cannot be True when there are no codes." + ) + return config + + +TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_INPUT): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_BYPASS_ARMED_HOME, default=False): cv.boolean, + cv.Optional(CONF_BYPASS_ARMED_NIGHT, default=False): cv.boolean, + cv.Optional(CONF_CHIME, default=False): cv.boolean, + cv.Optional(CONF_TRIGGER_MODE, default="DELAYED"): cv.enum( + ALARM_SENSOR_TYPES, upper=True, space="_" + ), + }, + key=CONF_INPUT, +) + +TEMPLATE_ALARM_CONTROL_PANEL_SCHEMA = ( + alarm_control_panel.ALARM_CONTROL_PANEL_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateAlarmControlPanel), + cv.Optional(CONF_CODES): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_REQUIRES_CODE_TO_ARM): cv.boolean, + cv.Optional(CONF_ARMING_HOME_TIME): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ARMING_NIGHT_TIME): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_ARMING_AWAY_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_PENDING_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TRIGGER_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( + TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA + ), + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_DISARMED"): cv.enum( + RESTORE_MODES, upper=True + ), + } + ).extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.All( + TEMPLATE_ALARM_CONTROL_PANEL_SCHEMA, + validate_config, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await alarm_control_panel.register_alarm_control_panel(var, config) + if CONF_CODES in config: + for acode in config[CONF_CODES]: + cg.add(var.add_code(acode)) + if CONF_REQUIRES_CODE_TO_ARM in config: + cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM])) + + cg.add(var.set_arming_away_time(config[CONF_ARMING_AWAY_TIME])) + cg.add(var.set_pending_time(config[CONF_PENDING_TIME])) + cg.add(var.set_trigger_time(config[CONF_TRIGGER_TIME])) + + supports_arm_home = False + if CONF_ARMING_HOME_TIME in config: + cg.add(var.set_arming_home_time(config[CONF_ARMING_HOME_TIME])) + supports_arm_home = True + + supports_arm_night = False + if CONF_ARMING_NIGHT_TIME in 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, []): + bs = await cg.get_variable(sensor[CONF_INPUT]) + + flags = BinarySensorFlags[FLAG_NORMAL] + if sensor[CONF_BYPASS_ARMED_HOME]: + flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_HOME] + supports_arm_home = True + if sensor[CONF_BYPASS_ARMED_NIGHT]: + flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_NIGHT] + supports_arm_night = True + if sensor[CONF_CHIME]: + flags |= BinarySensorFlags[FLAG_CHIME] + cg.add(var.add_sensor(bs, flags, sensor[CONF_TRIGGER_MODE])) + + cg.add(var.set_supports_arm_home(supports_arm_home)) + cg.add(var.set_supports_arm_night(supports_arm_night)) + + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) 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 new file mode 100644 index 0000000000..99843417fa --- /dev/null +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -0,0 +1,266 @@ + +#include "template_alarm_control_panel.h" +#include +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +using namespace esphome::alarm_control_panel; + +static const char *const TAG = "template.alarm_control_panel"; + +TemplateAlarmControlPanel::TemplateAlarmControlPanel(){}; + +#ifdef USE_BINARY_SENSOR +void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags, AlarmSensorType type) { + // 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; + this->sensor_data_.push_back(sd); + this->sensor_map_[sensor].store_index = this->next_store_index_++; +}; +#endif + +void TemplateAlarmControlPanel::dump_config() { + ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:"); + ESP_LOGCONFIG(TAG, " Current State: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_))); + ESP_LOGCONFIG(TAG, " Number of Codes: %u", this->codes_.size()); + if (!this->codes_.empty()) + ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->requires_code_to_arm_)); + ESP_LOGCONFIG(TAG, " Arming Away Time: %" PRIu32 "s", (this->arming_away_time_ / 1000)); + if (this->arming_home_time_ != 0) + ESP_LOGCONFIG(TAG, " Arming Home Time: %" PRIu32 "s", (this->arming_home_time_ / 1000)); + if (this->arming_night_time_ != 0) + ESP_LOGCONFIG(TAG, " Arming Night Time: %" PRIu32 "s", (this->arming_night_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Pending Time: %" PRIu32 "s", (this->pending_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Trigger Time: %" PRIu32 "s", (this->trigger_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->get_supported_features()); +#ifdef USE_BINARY_SENSOR + for (auto sensor_info : this->sensor_map_) { + ESP_LOGCONFIG(TAG, " Binary Sensor:"); + ESP_LOGCONFIG(TAG, " Name: %s", sensor_info.first->get_name().c_str()); + ESP_LOGCONFIG(TAG, " Armed home bypass: %s", + TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)); + ESP_LOGCONFIG(TAG, " Armed night bypass: %s", + TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)); + ESP_LOGCONFIG(TAG, " Chime mode: %s", TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)); + const char *sensor_type; + switch (sensor_info.second.type) { + case ALARM_SENSOR_TYPE_INSTANT: + sensor_type = "instant"; + break; + case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: + sensor_type = "delayed_follower"; + break; + case ALARM_SENSOR_TYPE_DELAYED: + default: + sensor_type = "delayed"; + } + ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type); + } +#endif +} + +void TemplateAlarmControlPanel::setup() { + ESP_LOGCONFIG(TAG, "Setting up Template AlarmControlPanel '%s'...", this->name_.c_str()); + switch (this->restore_mode_) { + case ALARM_CONTROL_PANEL_ALWAYS_DISARMED: + this->current_state_ = ACP_STATE_DISARMED; + break; + case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: { + uint8_t value; + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (this->pref_.load(&value)) { + this->current_state_ = static_cast(value); + } else { + this->current_state_ = ACP_STATE_DISARMED; + } + break; + } + } + this->desired_state_ = this->current_state_; +} + +void TemplateAlarmControlPanel::loop() { + // change from ARMING to ARMED_x after the arming_time_ has passed + if (this->current_state_ == ACP_STATE_ARMING) { + auto delay = this->arming_away_time_; + if (this->desired_state_ == ACP_STATE_ARMED_HOME) { + delay = this->arming_home_time_; + } + if (this->desired_state_ == ACP_STATE_ARMED_NIGHT) { + delay = this->arming_night_time_; + } + if ((millis() - this->last_update_) > delay) { + this->publish_state(this->desired_state_); + } + return; + } + // change from PENDING to TRIGGERED after the delay_time_ has passed + if (this->current_state_ == ACP_STATE_PENDING && (millis() - this->last_update_) > this->pending_time_) { + this->publish_state(ACP_STATE_TRIGGERED); + return; + } + auto future_state = this->current_state_; + // reset triggered if all clear + if (this->current_state_ == ACP_STATE_TRIGGERED && this->trigger_time_ > 0 && + (millis() - this->last_update_) > this->trigger_time_) { + future_state = this->desired_state_; + } + + bool delayed_sensor_not_ready = false; + bool instant_sensor_not_ready = false; + +#ifdef USE_BINARY_SENSOR + // Test all of the sensors in the list regardless of the alarm panel state + for (auto sensor_info : this->sensor_map_) { + // Check for chime zones + if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) { + // Look for the transition from closed to open + if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) { + // Must be disarmed to chime + if (this->current_state_ == ACP_STATE_DISARMED) { + this->chime_callback_.call(); + } + } + // Record the sensor state change + this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state; + } + // Check for triggered sensors + if (sensor_info.first->state) { // Sensor triggered? + // Skip if bypass armed home + if (this->current_state_ == ACP_STATE_ARMED_HOME && + (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { + continue; + } + // Skip if bypass armed night + if (this->current_state_ == ACP_STATE_ARMED_NIGHT && + (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { + continue; + } + + // If sensor type is of type instant + if (sensor_info.second.type == ALARM_SENSOR_TYPE_INSTANT) { + instant_sensor_not_ready = true; + break; + } + // If sensor type is of type interior follower + if (sensor_info.second.type == ALARM_SENSOR_TYPE_DELAYED_FOLLOWER) { + // Look to see if we are in the pending state + if (this->current_state_ == ACP_STATE_PENDING) { + delayed_sensor_not_ready = true; + } else { + instant_sensor_not_ready = true; + } + } + // If sensor type is of type delayed + if (sensor_info.second.type == ALARM_SENSOR_TYPE_DELAYED) { + delayed_sensor_not_ready = true; + break; + } + } + } + // Update all sensors not ready flag + this->sensors_ready_ = ((!instant_sensor_not_ready) && (!delayed_sensor_not_ready)); + + // Call the ready state change callback if there was a change + if (this->sensors_ready_ != this->sensors_ready_last_) { + this->ready_callback_.call(); + this->sensors_ready_last_ = this->sensors_ready_; + } + +#endif + if (this->is_state_armed(future_state) && (!this->sensors_ready_)) { + // Instant sensors + if (instant_sensor_not_ready) { + this->publish_state(ACP_STATE_TRIGGERED); + } else if (delayed_sensor_not_ready) { + // Delayed sensors + if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) { + this->publish_state(ACP_STATE_PENDING); + } else { + this->publish_state(ACP_STATE_TRIGGERED); + } + } + } else if (future_state != this->current_state_) { + this->publish_state(future_state); + } +} + +bool TemplateAlarmControlPanel::is_code_valid_(optional code) { + if (!this->codes_.empty()) { + if (code.has_value()) { + ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str()); + return (std::count(this->codes_.begin(), this->codes_.end(), code.value()) == 1); + } + ESP_LOGD(TAG, "No code provided"); + return false; + } + return true; +} + +uint32_t TemplateAlarmControlPanel::get_supported_features() const { + uint32_t features = ACP_FEAT_ARM_AWAY | ACP_FEAT_TRIGGER; + if (this->supports_arm_home_) { + features |= ACP_FEAT_ARM_HOME; + } + if (this->supports_arm_night_) { + features |= ACP_FEAT_ARM_NIGHT; + } + return features; +} + +bool TemplateAlarmControlPanel::get_requires_code() const { return !this->codes_.empty(); } + +void TemplateAlarmControlPanel::arm_(optional code, alarm_control_panel::AlarmControlPanelState state, + uint32_t delay) { + if (this->current_state_ != ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot arm when not disarmed"); + return; + } + if (this->requires_code_to_arm_ && !this->is_code_valid_(std::move(code))) { + ESP_LOGW(TAG, "Not arming code doesn't match"); + return; + } + this->desired_state_ = state; + if (delay > 0) { + this->publish_state(ACP_STATE_ARMING); + } else { + this->publish_state(state); + } +} + +void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { + if (call.get_state()) { + if (call.get_state() == ACP_STATE_ARMED_AWAY) { + this->arm_(call.get_code(), ACP_STATE_ARMED_AWAY, this->arming_away_time_); + } else if (call.get_state() == ACP_STATE_ARMED_HOME) { + this->arm_(call.get_code(), ACP_STATE_ARMED_HOME, this->arming_home_time_); + } else if (call.get_state() == ACP_STATE_ARMED_NIGHT) { + this->arm_(call.get_code(), ACP_STATE_ARMED_NIGHT, this->arming_night_time_); + } else if (call.get_state() == ACP_STATE_DISARMED) { + if (!this->is_code_valid_(call.get_code())) { + ESP_LOGW(TAG, "Not disarming code doesn't match"); + return; + } + this->desired_state_ = ACP_STATE_DISARMED; + this->publish_state(ACP_STATE_DISARMED); + } else if (call.get_state() == ACP_STATE_TRIGGERED) { + this->publish_state(ACP_STATE_TRIGGERED); + } else if (call.get_state() == ACP_STATE_PENDING) { + this->publish_state(ACP_STATE_PENDING); + } else { + ESP_LOGE(TAG, "State not yet implemented: %s", + LOG_STR_ARG(alarm_control_panel_state_to_string(*call.get_state()))); + } + } +} + +} // namespace template_ +} // namespace esphome 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 new file mode 100644 index 0000000000..9ae69a0422 --- /dev/null +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -0,0 +1,155 @@ +#pragma once + +#include +#include + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" + +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +namespace esphome { +namespace template_ { + +#ifdef USE_BINARY_SENSOR +enum BinarySensorFlags : uint16_t { + BINARY_SENSOR_MODE_NORMAL = 1 << 0, + BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1, + BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT = 1 << 2, + BINARY_SENSOR_MODE_CHIME = 1 << 3, +}; + +enum AlarmSensorType : uint16_t { + ALARM_SENSOR_TYPE_DELAYED = 0, + ALARM_SENSOR_TYPE_INSTANT, + ALARM_SENSOR_TYPE_DELAYED_FOLLOWER +}; + +#endif + +enum TemplateAlarmControlPanelRestoreMode { + ALARM_CONTROL_PANEL_ALWAYS_DISARMED, + ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, +}; + +struct SensorDataStore { + bool last_chime_state; +}; + +struct SensorInfo { + uint16_t flags; + AlarmSensorType type; + uint8_t store_index; +}; + +class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { + public: + TemplateAlarmControlPanel(); + void dump_config() override; + void setup() override; + void loop() override; + uint32_t get_supported_features() const override; + bool get_requires_code() const override; + bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } + bool get_all_sensors_ready() { return this->sensors_ready_; }; + void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + +#ifdef USE_BINARY_SENSOR + /** Add a binary_sensor to the alarm_panel. + * + * @param sensor The BinarySensor instance. + * @param ignore_when_home if this should be ignored when armed_home mode + */ + void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0, + AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); +#endif + + /** add a code + * + * @param code The code + */ + void add_code(const std::string &code) { this->codes_.push_back(code); } + + /** set requires a code to arm + * + * @param code_to_arm The requires code to arm + */ + void set_requires_code_to_arm(bool code_to_arm) { this->requires_code_to_arm_ = code_to_arm; } + + /** set the delay before arming away + * + * @param time The milliseconds + */ + void set_arming_away_time(uint32_t time) { this->arming_away_time_ = time; } + + /** set the delay before arming home + * + * @param time The milliseconds + */ + void set_arming_home_time(uint32_t time) { this->arming_home_time_ = time; } + + /** set the delay before arming night + * + * @param time The milliseconds + */ + void set_arming_night_time(uint32_t time) { this->arming_night_time_ = time; } + + /** set the delay before triggering + * + * @param time The milliseconds + */ + void set_pending_time(uint32_t time) { this->pending_time_ = time; } + + /** set the delay before resetting after triggered + * + * @param time The milliseconds + */ + void set_trigger_time(uint32_t time) { this->trigger_time_ = time; } + + void set_supports_arm_home(bool supports_arm_home) { supports_arm_home_ = supports_arm_home; } + + void set_supports_arm_night(bool supports_arm_night) { supports_arm_night_ = supports_arm_night; } + + protected: + void control(const alarm_control_panel::AlarmControlPanelCall &call) override; +#ifdef USE_BINARY_SENSOR + // This maps a binary sensor to its type and attribute bits + std::map sensor_map_; + +#endif + TemplateAlarmControlPanelRestoreMode restore_mode_{}; + + // the arming away delay + uint32_t arming_away_time_; + // the arming home delay + uint32_t arming_home_time_{0}; + // the arming night delay + uint32_t arming_night_time_{0}; + // the trigger delay + uint32_t pending_time_; + // the time in trigger + 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; + bool sensors_ready_last_ = 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 diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index 4ce89503de..c93876380d 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -1,8 +1,10 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import binary_sensor -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE +import esphome.config_validation as cv +from esphome.const import CONF_CONDITION, CONF_ID, CONF_LAMBDA, CONF_STATE +from esphome.cpp_generator import LambdaExpression + from .. import template_ns TemplateBinarySensor = template_ns.class_( @@ -13,7 +15,10 @@ CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(TemplateBinarySensor) .extend( { - cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Exclusive(CONF_LAMBDA, CONF_CONDITION): cv.returning_lambda, + cv.Exclusive( + CONF_CONDITION, CONF_CONDITION + ): automation.validate_potentially_and_condition, } ) .extend(cv.COMPONENT_SCHEMA) @@ -24,9 +29,17 @@ async def to_code(config): var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - if CONF_LAMBDA in config: + if lamb := config.get(CONF_LAMBDA): template_ = await cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.optional.template(bool) + lamb, [], return_type=cg.optional.template(bool) + ) + cg.add(var.set_template(template_)) + if condition := config.get(CONF_CONDITION): + condition = await automation.build_condition( + condition, cg.TemplateArguments(), [] + ) + template_ = LambdaExpression( + f"return {condition.check()};", [], return_type=cg.optional.template(bool) ) cg.add(var.set_template(template_)) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 66ff4be4c4..5ce8894a8a 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,11 +6,21 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::loop() { - if (!this->f_.has_value()) +void TemplateBinarySensor::setup() { + if (!this->publish_initial_state_) return; - auto s = (*this->f_)(); + if (this->f_ != nullptr) { + this->publish_initial_state(this->f_().value_or(false)); + } else { + this->publish_initial_state(false); + } +} +void TemplateBinarySensor::loop() { + if (this->f_ == nullptr) + return; + + auto s = this->f_(); if (s.has_value()) { this->publish_state(*s); } diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index a28929b122..5e5624d82e 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -10,13 +10,14 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso public: void set_template(std::function()> &&f) { this->f_ = f; } + void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - optional()>> f_{}; + std::function()> f_{nullptr}; }; } // namespace template_ diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 8844ddd6ab..43d0be99b4 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -31,6 +31,7 @@ RESTORE_MODES = { } CONF_HAS_POSITION = "has_position" +CONF_TOGGLE_ACTION = "toggle_action" CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( { @@ -44,6 +45,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( RESTORE_MODES, upper=True @@ -74,6 +76,11 @@ async def to_code(config): var.get_stop_trigger(), [], config[CONF_STOP_ACTION] ) cg.add(var.set_has_stop(True)) + if CONF_TOGGLE_ACTION in config: + await automation.build_automation( + var.get_toggle_trigger(), [], config[CONF_TOGGLE_ACTION] + ) + cg.add(var.set_has_toggle(True)) if CONF_TILT_ACTION in config: await automation.build_automation( var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index b16e439943..2d6c3087ae 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -12,6 +12,7 @@ TemplateCover::TemplateCover() : open_trigger_(new Trigger<>()), close_trigger_(new Trigger<>), stop_trigger_(new Trigger<>()), + toggle_trigger_(new Trigger<>()), position_trigger_(new Trigger()), tilt_trigger_(new Trigger()) {} void TemplateCover::setup() { @@ -68,6 +69,7 @@ float TemplateCover::get_setup_priority() const { return setup_priority::HARDWAR Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } +Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } void TemplateCover::control(const CoverCall &call) { if (call.get_stop()) { @@ -76,6 +78,12 @@ void TemplateCover::control(const CoverCall &call) { this->prev_command_trigger_ = this->stop_trigger_; this->publish_state(); } + if (call.get_toggle().has_value()) { + this->stop_prev_trigger_(); + this->toggle_trigger_->trigger(); + this->prev_command_trigger_ = this->toggle_trigger_; + this->publish_state(); + } if (call.get_position().has_value()) { auto pos = *call.get_position(); this->stop_prev_trigger_(); @@ -110,6 +118,7 @@ CoverTraits TemplateCover::get_traits() { auto traits = CoverTraits(); traits.set_is_assumed_state(this->assumed_state_); traits.set_supports_stop(this->has_stop_); + traits.set_supports_toggle(this->has_toggle_); traits.set_supports_position(this->has_position_); traits.set_supports_tilt(this->has_tilt_); return traits; @@ -118,6 +127,7 @@ Trigger *TemplateCover::get_position_trigger() const { return this->posit Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } +void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 4ff5caf1db..958c94b0a6 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -21,6 +21,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; + Trigger<> *get_toggle_trigger() const; Trigger *get_position_trigger() const; Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); @@ -29,6 +30,7 @@ class TemplateCover : public cover::Cover, public Component { void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); + void set_has_toggle(bool has_toggle); void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } void setup() override; @@ -50,7 +52,9 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *open_trigger_; Trigger<> *close_trigger_; bool has_stop_{false}; + bool has_toggle_{false}; Trigger<> *stop_trigger_; + Trigger<> *toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; Trigger *position_trigger_; bool has_position_{false}; diff --git a/esphome/components/template/datetime/__init__.py b/esphome/components/template/datetime/__init__.py new file mode 100644 index 0000000000..746ab8a6f3 --- /dev/null +++ b/esphome/components/template/datetime/__init__.py @@ -0,0 +1,151 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import datetime +from esphome.const import ( + CONF_INITIAL_VALUE, + CONF_LAMBDA, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, + CONF_SET_ACTION, + CONF_DAY, + CONF_HOUR, + CONF_MINUTE, + CONF_MONTH, + CONF_SECOND, + CONF_TYPE, + CONF_YEAR, +) + +from .. import template_ns + +CODEOWNERS = ["@rfdarter"] + + +TemplateDate = template_ns.class_( + "TemplateDate", datetime.DateEntity, cg.PollingComponent +) + +TemplateTime = template_ns.class_( + "TemplateTime", datetime.TimeEntity, cg.PollingComponent +) + +TemplateDateTime = template_ns.class_( + "TemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent +) + + +def validate(config): + config = config.copy() + if CONF_LAMBDA in config: + if config[CONF_OPTIMISTIC]: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_VALUE in config: + raise cv.Invalid("initial_value cannot be used with lambda") + if CONF_RESTORE_VALUE in config: + raise cv.Invalid("restore_value cannot be used with lambda") + else: + if CONF_RESTORE_VALUE not in config: + config[CONF_RESTORE_VALUE] = False + + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the date and time being set." + ) + return config + + +_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } +).extend(cv.polling_component_schema("60s")) + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "DATE": datetime.date_schema(TemplateDate) + .extend(_BASE_SCHEMA) + .extend( + { + cv.Optional(CONF_INITIAL_VALUE): cv.date_time( + date=True, time=False + ), + } + ), + "TIME": datetime.time_schema(TemplateTime) + .extend(_BASE_SCHEMA) + .extend( + { + cv.Optional(CONF_INITIAL_VALUE): cv.date_time( + date=False, time=True + ), + } + ), + "DATETIME": datetime.datetime_schema(TemplateDateTime) + .extend(_BASE_SCHEMA) + .extend( + { + cv.Optional(CONF_INITIAL_VALUE): cv.date_time(date=True, time=True), + } + ), + }, + upper=True, + ), + validate, +) + + +async def to_code(config): + var = await datetime.new_datetime(config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.ESPTime) + ) + cg.add(var.set_template(template_)) + + else: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + if initial_value := config.get(CONF_INITIAL_VALUE): + if config[CONF_TYPE] == "DATE": + date_struct = cg.StructInitializer( + cg.ESPTime, + ("day_of_month", initial_value[CONF_DAY]), + ("month", initial_value[CONF_MONTH]), + ("year", initial_value[CONF_YEAR]), + ) + cg.add(var.set_initial_value(date_struct)) + elif config[CONF_TYPE] == "TIME": + time_struct = cg.StructInitializer( + cg.ESPTime, + ("second", initial_value[CONF_SECOND]), + ("minute", initial_value[CONF_MINUTE]), + ("hour", initial_value[CONF_HOUR]), + ) + cg.add(var.set_initial_value(time_struct)) + elif config[CONF_TYPE] == "DATETIME": + datetime_struct = cg.StructInitializer( + cg.ESPTime, + ("second", initial_value[CONF_SECOND]), + ("minute", initial_value[CONF_MINUTE]), + ("hour", initial_value[CONF_HOUR]), + ("day_of_month", initial_value[CONF_DAY]), + ("month", initial_value[CONF_MONTH]), + ("year", initial_value[CONF_YEAR]), + ) + cg.add(var.set_initial_value(datetime_struct)) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), + [(cg.ESPTime, "x")], + config[CONF_SET_ACTION], + ) + + await cg.register_component(var, config) diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp new file mode 100644 index 0000000000..01e15e532e --- /dev/null +++ b/esphome/components/template/datetime/template_date.cpp @@ -0,0 +1,111 @@ +#include "template_date.h" + +#ifdef USE_DATETIME_DATE + +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.date"; + +void TemplateDate::setup() { + if (this->f_.has_value()) + return; + + ESPTime state{}; + + if (!this->restore_value_) { + state = this->initial_value_; + } else { + datetime::DateEntityRestoreState temp; + this->pref_ = + global_preferences->make_preference(194434030U ^ this->get_object_id_hash()); + if (this->pref_.load(&temp)) { + temp.apply(this); + return; + } else { + // set to inital value if loading from pref failed + state = this->initial_value_; + } + } + + this->year_ = state.year; + this->month_ = state.month; + this->day_ = state.day_of_month; + this->publish_state(); +} + +void TemplateDate::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->publish_state(); +} + +void TemplateDate::control(const datetime::DateCall &call) { + bool has_year = call.get_year().has_value(); + bool has_month = call.get_month().has_value(); + bool has_day = call.get_day().has_value(); + + ESPTime value = {}; + if (has_year) + value.year = *call.get_year(); + + if (has_month) + value.month = *call.get_month(); + + if (has_day) + value.day_of_month = *call.get_day(); + + this->set_trigger_->trigger(value); + + if (this->optimistic_) { + if (has_year) + this->year_ = *call.get_year(); + if (has_month) + this->month_ = *call.get_month(); + if (has_day) + this->day_ = *call.get_day(); + this->publish_state(); + } + + if (this->restore_value_) { + datetime::DateEntityRestoreState temp = {}; + if (has_year) { + temp.year = *call.get_year(); + } else { + temp.year = this->year_; + } + if (has_month) { + temp.month = *call.get_month(); + } else { + temp.month = this->month_; + } + if (has_day) { + temp.day = *call.get_day(); + } else { + temp.day = this->day_; + } + + this->pref_.save(&temp); + } +} + +void TemplateDate::dump_config() { + LOG_DATETIME_DATE("", "Template Date", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_DATE diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h new file mode 100644 index 0000000000..185c7ed49d --- /dev/null +++ b/esphome/components/template/datetime/template_date.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_DATE + +#include "esphome/components/datetime/date_entity.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/core/time.h" + +namespace esphome { +namespace template_ { + +class TemplateDate : public datetime::DateEntity, public PollingComponent { + public: + void set_template(std::function()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return this->set_trigger_; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + + void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(const datetime::DateCall &call) override; + + bool optimistic_{false}; + ESPTime initial_value_{}; + bool restore_value_{false}; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_DATE diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp new file mode 100644 index 0000000000..3ab74e197f --- /dev/null +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -0,0 +1,150 @@ +#include "template_datetime.h" + +#ifdef USE_DATETIME_DATETIME + +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.datetime"; + +void TemplateDateTime::setup() { + if (this->f_.has_value()) + return; + + ESPTime state{}; + + if (!this->restore_value_) { + state = this->initial_value_; + } else { + datetime::DateTimeEntityRestoreState temp; + this->pref_ = global_preferences->make_preference(194434090U ^ + this->get_object_id_hash()); + if (this->pref_.load(&temp)) { + temp.apply(this); + return; + } else { + // set to inital value if loading from pref failed + state = this->initial_value_; + } + } + + this->year_ = state.year; + this->month_ = state.month; + this->day_ = state.day_of_month; + this->hour_ = state.hour; + this->minute_ = state.minute; + this->second_ = state.second; + this->publish_state(); +} + +void TemplateDateTime::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); +} + +void TemplateDateTime::control(const datetime::DateTimeCall &call) { + bool has_year = call.get_year().has_value(); + bool has_month = call.get_month().has_value(); + bool has_day = call.get_day().has_value(); + bool has_hour = call.get_hour().has_value(); + bool has_minute = call.get_minute().has_value(); + bool has_second = call.get_second().has_value(); + + ESPTime value = {}; + if (has_year) + value.year = *call.get_year(); + + if (has_month) + value.month = *call.get_month(); + + if (has_day) + value.day_of_month = *call.get_day(); + + if (has_hour) + value.hour = *call.get_hour(); + + if (has_minute) + value.minute = *call.get_minute(); + + if (has_second) + value.second = *call.get_second(); + + this->set_trigger_->trigger(value); + + if (this->optimistic_) { + if (has_year) + this->year_ = *call.get_year(); + if (has_month) + this->month_ = *call.get_month(); + if (has_day) + this->day_ = *call.get_day(); + if (has_hour) + this->hour_ = *call.get_hour(); + if (has_minute) + this->minute_ = *call.get_minute(); + if (has_second) + this->second_ = *call.get_second(); + this->publish_state(); + } + + if (this->restore_value_) { + datetime::DateTimeEntityRestoreState temp = {}; + if (has_year) { + temp.year = *call.get_year(); + } else { + temp.year = this->year_; + } + if (has_month) { + temp.month = *call.get_month(); + } else { + temp.month = this->month_; + } + if (has_day) { + temp.day = *call.get_day(); + } else { + temp.day = this->day_; + } + if (has_hour) { + temp.hour = *call.get_hour(); + } else { + temp.hour = this->hour_; + } + if (has_minute) { + temp.minute = *call.get_minute(); + } else { + temp.minute = this->minute_; + } + if (has_second) { + temp.second = *call.get_second(); + } else { + temp.second = this->second_; + } + + this->pref_.save(&temp); + } +} + +void TemplateDateTime::dump_config() { + LOG_DATETIME_DATETIME("", "Template DateTime", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_DATETIME diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h new file mode 100644 index 0000000000..ef80ded89a --- /dev/null +++ b/esphome/components/template/datetime/template_datetime.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_DATETIME + +#include "esphome/components/datetime/datetime_entity.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/core/time.h" + +namespace esphome { +namespace template_ { + +class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { + public: + void set_template(std::function()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return this->set_trigger_; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + + void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(const datetime::DateTimeCall &call) override; + + bool optimistic_{false}; + ESPTime initial_value_{}; + bool restore_value_{false}; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_DATETIME diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp new file mode 100644 index 0000000000..0e4d734d16 --- /dev/null +++ b/esphome/components/template/datetime/template_time.cpp @@ -0,0 +1,111 @@ +#include "template_time.h" + +#ifdef USE_DATETIME_TIME + +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.time"; + +void TemplateTime::setup() { + if (this->f_.has_value()) + return; + + ESPTime state{}; + + if (!this->restore_value_) { + state = this->initial_value_; + } else { + datetime::TimeEntityRestoreState temp; + this->pref_ = + global_preferences->make_preference(194434060U ^ this->get_object_id_hash()); + if (this->pref_.load(&temp)) { + temp.apply(this); + return; + } else { + // set to inital value if loading from pref failed + state = this->initial_value_; + } + } + + this->hour_ = state.hour; + this->minute_ = state.minute; + this->second_ = state.second; + this->publish_state(); +} + +void TemplateTime::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); +} + +void TemplateTime::control(const datetime::TimeCall &call) { + bool has_hour = call.get_hour().has_value(); + bool has_minute = call.get_minute().has_value(); + bool has_second = call.get_second().has_value(); + + ESPTime value = {}; + if (has_hour) + value.hour = *call.get_hour(); + + if (has_minute) + value.minute = *call.get_minute(); + + if (has_second) + value.second = *call.get_second(); + + this->set_trigger_->trigger(value); + + if (this->optimistic_) { + if (has_hour) + this->hour_ = *call.get_hour(); + if (has_minute) + this->minute_ = *call.get_minute(); + if (has_second) + this->second_ = *call.get_second(); + this->publish_state(); + } + + if (this->restore_value_) { + datetime::TimeEntityRestoreState temp = {}; + if (has_hour) { + temp.hour = *call.get_hour(); + } else { + temp.hour = this->hour_; + } + if (has_minute) { + temp.minute = *call.get_minute(); + } else { + temp.minute = this->minute_; + } + if (has_second) { + temp.second = *call.get_second(); + } else { + temp.second = this->second_; + } + + this->pref_.save(&temp); + } +} + +void TemplateTime::dump_config() { + LOG_DATETIME_TIME("", "Template Time", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_TIME diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h new file mode 100644 index 0000000000..4a7c0098ec --- /dev/null +++ b/esphome/components/template/datetime/template_time.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_DATETIME_TIME + +#include "esphome/components/datetime/time_entity.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/core/time.h" + +namespace esphome { +namespace template_ { + +class TemplateTime : public datetime::TimeEntity, public PollingComponent { + public: + void set_template(std::function()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return this->set_trigger_; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + + void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(const datetime::TimeCall &call) override; + + bool optimistic_{false}; + ESPTime initial_value_{}; + bool restore_value_{false}; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome + +#endif // USE_DATETIME_TIME diff --git a/esphome/components/template/event/__init__.py b/esphome/components/template/event/__init__.py new file mode 100644 index 0000000000..2a948cfdfd --- /dev/null +++ b/esphome/components/template/event/__init__.py @@ -0,0 +1,24 @@ +import esphome.config_validation as cv + +from esphome.components import event + +import esphome.codegen as cg + +from esphome.const import CONF_EVENT_TYPES + +from .. import template_ns + +CODEOWNERS = ["@nohat"] + +TemplateEvent = template_ns.class_("TemplateEvent", event.Event, cg.Component) + +CONFIG_SCHEMA = event.event_schema(TemplateEvent).extend( + { + cv.Required(CONF_EVENT_TYPES): cv.ensure_list(cv.string_strict), + } +) + + +async def to_code(config): + var = await event.new_event(config, event_types=config[CONF_EVENT_TYPES]) + await cg.register_component(var, config) diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h new file mode 100644 index 0000000000..251ae9299b --- /dev/null +++ b/esphome/components/template/event/template_event.h @@ -0,0 +1,12 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/event/event.h" + +namespace esphome { +namespace template_ { + +class TemplateEvent : public Component, public event::Event {}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/fan/__init__.py b/esphome/components/template/fan/__init__.py new file mode 100644 index 0000000000..348ebd281f --- /dev/null +++ b/esphome/components/template/fan/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import fan +from esphome.components.fan import validate_preset_modes +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_PRESET_MODES, + CONF_SPEED_COUNT, +) + +from .. import template_ns + +CODEOWNERS = ["@ssieb"] + +TemplateFan = template_ns.class_("TemplateFan", cg.Component, fan.Fan) + +CONF_HAS_DIRECTION = "has_direction" +CONF_HAS_OSCILLATING = "has_oscillating" + +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TemplateFan), + cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, + cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, + cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await cg.register_component(var, config) + await fan.register_fan(var, config) + + cg.add(var.set_has_direction(config[CONF_HAS_DIRECTION])) + cg.add(var.set_has_oscillating(config[CONF_HAS_OSCILLATING])) + + if CONF_SPEED_COUNT in config: + cg.add(var.set_speed_count(config[CONF_SPEED_COUNT])) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp new file mode 100644 index 0000000000..5f4a2ae8f7 --- /dev/null +++ b/esphome/components/template/fan/template_fan.cpp @@ -0,0 +1,38 @@ +#include "template_fan.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.fan"; + +void TemplateFan::setup() { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(*this); + } + + // Construct traits + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); +} + +void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } + +void TemplateFan::control(const fan::FanCall &call) { + if (call.get_state().has_value()) + this->state = *call.get_state(); + if (call.get_speed().has_value() && (this->speed_count_ > 0)) + this->speed = *call.get_speed(); + if (call.get_oscillating().has_value() && this->has_oscillating_) + this->oscillating = *call.get_oscillating(); + if (call.get_direction().has_value() && this->has_direction_) + this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); + + this->publish_state(); +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h new file mode 100644 index 0000000000..7f5305ca48 --- /dev/null +++ b/esphome/components/template/fan/template_fan.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/fan/fan.h" + +namespace esphome { +namespace template_ { + +class TemplateFan : public Component, public fan::Fan { + public: + TemplateFan() {} + void setup() override; + void dump_config() override; + void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } + void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } + void set_speed_count(int count) { this->speed_count_ = count; } + void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } + + protected: + void control(const fan::FanCall &call) override; + + bool has_oscillating_{false}; + bool has_direction_{false}; + int speed_count_{0}; + fan::FanTraits traits_; + std::set preset_modes_{}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index b9a507c7e9..c6ed25adda 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OPTIMISTIC, CONF_RESTORE_VALUE, CONF_STEP, + CONF_SET_ACTION, ) from .. import template_ns @@ -18,8 +19,6 @@ TemplateNumber = template_ns.class_( "TemplateNumber", number.Number, cg.PollingComponent ) -CONF_SET_ACTION = "set_action" - def validate_min_max(config): if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index d116cbb8ae..75dbd4f5c5 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_OPTIONS, CONF_OPTIMISTIC, CONF_RESTORE_VALUE, + CONF_SET_ACTION, ) from .. import template_ns @@ -16,8 +17,6 @@ TemplateSelect = template_ns.class_( "TemplateSelect", select.Select, cg.PollingComponent ) -CONF_SET_ACTION = "set_action" - def validate(config): if CONF_LAMBDA in config: diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index e002c4e3d8..a221cbaa60 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -43,7 +43,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( single=True ), - cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, + 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), @@ -70,7 +72,6 @@ async def to_code(config): ) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) - cg.add(var.set_restore_state(config[CONF_RESTORE_STATE])) @automation.register_action( diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5db346b99f..fa236f6364 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -36,13 +36,10 @@ void TemplateSwitch::write_state(bool state) { void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } void TemplateSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } -float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } +float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void TemplateSwitch::setup() { - if (!this->restore_state_) - return; - optional initial_state = this->get_initial_state_with_restore_mode(); if (initial_state.has_value()) { @@ -57,10 +54,8 @@ void TemplateSwitch::setup() { } void TemplateSwitch::dump_config() { LOG_SWITCH("", "Template Switch", this); - ESP_LOGCONFIG(TAG, " Restore State: %s", YESNO(this->restore_state_)); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); } -void TemplateSwitch::set_restore_state(bool restore_state) { this->restore_state_ = restore_state; } void TemplateSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index ef9b567451..bfe9ac25d6 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -15,7 +15,6 @@ class TemplateSwitch : public switch_::Switch, public Component { void dump_config() override; void set_state_lambda(std::function()> &&f); - void set_restore_state(bool restore_state); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -35,7 +34,6 @@ class TemplateSwitch : public switch_::Switch, public Component { Trigger<> *turn_on_trigger_; Trigger<> *turn_off_trigger_; Trigger<> *prev_trigger_{nullptr}; - bool restore_state_{false}; }; } // namespace template_ diff --git a/esphome/components/template/text/__init__.py b/esphome/components/template/text/__init__.py new file mode 100644 index 0000000000..f73b240197 --- /dev/null +++ b/esphome/components/template/text/__init__.py @@ -0,0 +1,92 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text +from esphome.const import ( + CONF_INITIAL_VALUE, + CONF_LAMBDA, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, + CONF_MAX_LENGTH, + CONF_MIN_LENGTH, + CONF_PATTERN, + CONF_SET_ACTION, +) +from .. import template_ns + +TemplateText = template_ns.class_("TemplateText", text.Text, cg.PollingComponent) + +TextSaverBase = template_ns.class_("TemplateTextSaverBase") +TextSaverTemplate = template_ns.class_("TextSaver", TextSaverBase) + +CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length" + + +def validate(config): + if CONF_LAMBDA in config: + if config[CONF_OPTIMISTIC]: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_VALUE in config: + raise cv.Invalid("initial_value cannot be used with lambda") + if config[CONF_RESTORE_VALUE]: + raise cv.Invalid("restore_value cannot be used with lambda") + elif CONF_INITIAL_VALUE not in config: + config[CONF_INITIAL_VALUE] = "" + + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the text input being set." + ) + + with cv.prepend_path(CONF_MIN_LENGTH): + if config[CONF_MIN_LENGTH] > config[CONF_MAX_LENGTH]: + raise cv.Invalid("min_length must be less than or equal to max_length") + return config + + +CONFIG_SCHEMA = cv.All( + text.TEXT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateText), + cv.Optional(CONF_MIN_LENGTH, default=0): cv.int_range(min=0, max=255), + cv.Optional(CONF_MAX_LENGTH, default=255): cv.int_range(min=0, max=255), + cv.Optional(CONF_PATTERN): cv.string, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, + } + ).extend(cv.polling_component_schema("60s")), + validate, +) + + +async def to_code(config): + var = await text.new_text( + config, + min_length=config[CONF_MIN_LENGTH], + max_length=config[CONF_MAX_LENGTH], + pattern=config.get(CONF_PATTERN), + ) + await cg.register_component(var, config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) + ) + cg.add(var.set_template(template_)) + + else: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + if initial_value_config := config.get(CONF_INITIAL_VALUE): + cg.add(var.set_initial_value(initial_value_config)) + if config[CONF_RESTORE_VALUE]: + args = cg.TemplateArguments(config[CONF_MAX_LENGTH]) + saver = TextSaverTemplate.template(args).new() + cg.add(var.set_value_saver(saver)) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION] + ) diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp new file mode 100644 index 0000000000..fb0208e198 --- /dev/null +++ b/esphome/components/template/text/template_text.cpp @@ -0,0 +1,64 @@ +#include "template_text.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.text"; + +void TemplateText::setup() { + if (!(this->f_ == nullptr)) { + if (this->f_.has_value()) + return; + } + + std::string value; + ESP_LOGD(TAG, "Setting up Template Text Input"); + value = this->initial_value_; + if (!this->pref_) { + ESP_LOGD(TAG, "State from initial: %s", value.c_str()); + } else { + uint32_t key = this->get_object_id_hash(); + key += this->traits.get_min_length() << 2; + key += this->traits.get_max_length() << 4; + key += fnv1_hash(this->traits.get_pattern()) << 6; + this->pref_->setup(key, value); + } + if (!value.empty()) + this->publish_state(value); +} + +void TemplateText::update() { + if (this->f_ == nullptr) + return; + + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->publish_state(*val); +} + +void TemplateText::control(const std::string &value) { + this->set_trigger_->trigger(value); + + if (this->optimistic_) + this->publish_state(value); + + if (this->pref_) { + if (!this->pref_->save(value)) { + ESP_LOGW(TAG, "Text value too long to save"); + } + } +} +void TemplateText::dump_config() { + LOG_TEXT("", "Template Text Input", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h new file mode 100644 index 0000000000..bcfc54a2ba --- /dev/null +++ b/esphome/components/template/text/template_text.h @@ -0,0 +1,87 @@ +#pragma once + +#include "esphome/components/text/text.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace template_ { + +// We keep this separate so we don't have to template and duplicate +// the text input for each different size flash allocation. +class TemplateTextSaverBase { + public: + virtual bool save(const std::string &value) { return true; } + + virtual void setup(uint32_t id, std::string &value) {} + + protected: + ESPPreferenceObject pref_; + std::string prev_; +}; + +template class TextSaver : public TemplateTextSaverBase { + public: + bool save(const std::string &value) override { + int diff = value.compare(this->prev_); + if (diff != 0) { + // If string is bigger than the allocation, do not save it. + // We don't need to waste ram setting prev_value either. + int size = value.size(); + if (size <= SZ) { + // Make it into a length prefixed thing + unsigned char temp[SZ + 1]; + memcpy(temp + 1, value.c_str(), size); + // SZ should be pre checked at the schema level, it can't go past the char range. + temp[0] = ((unsigned char) size); + this->pref_.save(&temp); + this->prev_.assign(value); + return true; + } + } + return false; + } + + // Make the preference object. Fill the provided location with the saved data + // If it is available, else leave it alone + void setup(uint32_t id, std::string &value) override { + this->pref_ = global_preferences->make_preference(id); + + char temp[SZ + 1]; + bool hasdata = this->pref_.load(&temp); + + if (hasdata) { + value.assign(temp + 1, (size_t) temp[0]); + } + + this->prev_.assign(value); + } +}; + +class TemplateText : public text::Text, public PollingComponent { + public: + void set_template(std::function()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return this->set_trigger_; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_initial_value(const std::string &initial_value) { this->initial_value_ = initial_value; } + void set_value_saver(TemplateTextSaverBase *restore_value_saver) { this->pref_ = restore_value_saver; } + + protected: + void control(const std::string &value) override; + bool optimistic_ = false; + std::string initial_value_; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_{nullptr}; + + TemplateTextSaverBase *pref_ = nullptr; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/valve/__init__.py b/esphome/components/template/valve/__init__.py new file mode 100644 index 0000000000..89d776dfdd --- /dev/null +++ b/esphome/components/template/valve/__init__.py @@ -0,0 +1,118 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import valve +from esphome.const import ( + CONF_ASSUMED_STATE, + CONF_CLOSE_ACTION, + CONF_CURRENT_OPERATION, + CONF_ID, + CONF_LAMBDA, + CONF_OPEN_ACTION, + CONF_OPTIMISTIC, + CONF_POSITION, + CONF_POSITION_ACTION, + CONF_RESTORE_MODE, + CONF_STATE, + CONF_STOP_ACTION, +) +from .. import template_ns + +TemplateValve = template_ns.class_("TemplateValve", valve.Valve, cg.Component) + +TemplateValveRestoreMode = template_ns.enum("TemplateValveRestoreMode") +RESTORE_MODES = { + "NO_RESTORE": TemplateValveRestoreMode.VALVE_NO_RESTORE, + "RESTORE": TemplateValveRestoreMode.VALVE_RESTORE, + "RESTORE_AND_CALL": TemplateValveRestoreMode.VALVE_RESTORE_AND_CALL, +} + +CONF_HAS_POSITION = "has_position" +CONF_TOGGLE_ACTION = "toggle_action" + +CONFIG_SCHEMA = valve.VALVE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateValve), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, + cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = await valve.new_valve(config) + await cg.register_component(var, config) + if lambda_config := config.get(CONF_LAMBDA): + template_ = await cg.process_lambda( + lambda_config, [], return_type=cg.optional.template(float) + ) + cg.add(var.set_state_lambda(template_)) + if open_action_config := config.get(CONF_OPEN_ACTION): + await automation.build_automation( + var.get_open_trigger(), [], open_action_config + ) + if close_action_config := config.get(CONF_CLOSE_ACTION): + await automation.build_automation( + var.get_close_trigger(), [], close_action_config + ) + if stop_action_config := config.get(CONF_STOP_ACTION): + await automation.build_automation( + var.get_stop_trigger(), [], stop_action_config + ) + cg.add(var.set_has_stop(True)) + if toggle_action_config := config.get(CONF_TOGGLE_ACTION): + await automation.build_automation( + var.get_toggle_trigger(), [], toggle_action_config + ) + cg.add(var.set_has_toggle(True)) + if position_action_config := config.get(CONF_POSITION_ACTION): + await automation.build_automation( + var.get_position_trigger(), [(float, "pos")], position_action_config + ) + cg.add(var.set_has_position(True)) + else: + cg.add(var.set_has_position(config[CONF_HAS_POSITION])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + + +@automation.register_action( + "valve.template.publish", + valve.ValvePublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(valve.Valve), + cv.Exclusive(CONF_STATE, "pos"): cv.templatable(valve.validate_valve_state), + cv.Exclusive(CONF_POSITION, "pos"): cv.templatable(cv.percentage), + cv.Optional(CONF_CURRENT_OPERATION): cv.templatable( + valve.validate_valve_operation + ), + } + ), +) +async def valve_template_publish_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if state_config := config.get(CONF_STATE): + template_ = await cg.templatable(state_config, args, float) + cg.add(var.set_position(template_)) + if (position_config := config.get(CONF_POSITION)) is not None: + template_ = await cg.templatable(position_config, args, float) + cg.add(var.set_position(template_)) + if current_operation_config := config.get(CONF_CURRENT_OPERATION): + template_ = await cg.templatable( + current_operation_config, args, valve.ValveOperation + ) + cg.add(var.set_current_operation(template_)) + return var diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp new file mode 100644 index 0000000000..f943e19da9 --- /dev/null +++ b/esphome/components/template/valve/template_valve.cpp @@ -0,0 +1,131 @@ +#include "template_valve.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +using namespace esphome::valve; + +static const char *const TAG = "template.valve"; + +TemplateValve::TemplateValve() + : open_trigger_(new Trigger<>()), + close_trigger_(new Trigger<>), + stop_trigger_(new Trigger<>()), + toggle_trigger_(new Trigger<>()), + position_trigger_(new Trigger()) {} + +void TemplateValve::setup() { + ESP_LOGCONFIG(TAG, "Setting up template valve '%s'...", this->name_.c_str()); + switch (this->restore_mode_) { + case VALVE_NO_RESTORE: + break; + case VALVE_RESTORE: { + auto restore = this->restore_state_(); + if (restore.has_value()) + restore->apply(this); + break; + } + case VALVE_RESTORE_AND_CALL: { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } + break; + } + } +} + +void TemplateValve::loop() { + bool changed = false; + + if (this->state_f_.has_value()) { + auto s = (*this->state_f_)(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; + } + } + } + + if (changed) + this->publish_state(); +} + +void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } +void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } +void TemplateValve::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } +float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } + +Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } +Trigger<> *TemplateValve::get_close_trigger() const { return this->close_trigger_; } +Trigger<> *TemplateValve::get_stop_trigger() const { return this->stop_trigger_; } +Trigger<> *TemplateValve::get_toggle_trigger() const { return this->toggle_trigger_; } + +void TemplateValve::dump_config() { + LOG_VALVE("", "Template Valve", this); + ESP_LOGCONFIG(TAG, " Has position: %s", YESNO(this->has_position_)); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} + +void TemplateValve::control(const ValveCall &call) { + if (call.get_stop()) { + this->stop_prev_trigger_(); + this->stop_trigger_->trigger(); + this->prev_command_trigger_ = this->stop_trigger_; + this->publish_state(); + } + if (call.get_toggle().has_value()) { + this->stop_prev_trigger_(); + this->toggle_trigger_->trigger(); + this->prev_command_trigger_ = this->toggle_trigger_; + this->publish_state(); + } + if (call.get_position().has_value()) { + auto pos = *call.get_position(); + this->stop_prev_trigger_(); + + if (pos == VALVE_OPEN) { + this->open_trigger_->trigger(); + this->prev_command_trigger_ = this->open_trigger_; + } else if (pos == VALVE_CLOSED) { + this->close_trigger_->trigger(); + this->prev_command_trigger_ = this->close_trigger_; + } else { + this->position_trigger_->trigger(pos); + } + + if (this->optimistic_) { + this->position = pos; + } + } + + this->publish_state(); +} + +ValveTraits TemplateValve::get_traits() { + auto traits = ValveTraits(); + traits.set_is_assumed_state(this->assumed_state_); + traits.set_supports_stop(this->has_stop_); + traits.set_supports_toggle(this->has_toggle_); + traits.set_supports_position(this->has_position_); + return traits; +} + +Trigger *TemplateValve::get_position_trigger() const { return this->position_trigger_; } + +void TemplateValve::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } +void TemplateValve::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } +void TemplateValve::set_has_position(bool has_position) { this->has_position_ = has_position; } + +void TemplateValve::stop_prev_trigger_() { + if (this->prev_command_trigger_ != nullptr) { + this->prev_command_trigger_->stop_action(); + this->prev_command_trigger_ = nullptr; + } +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h new file mode 100644 index 0000000000..5e3fb6aff3 --- /dev/null +++ b/esphome/components/template/valve/template_valve.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/valve/valve.h" + +namespace esphome { +namespace template_ { + +enum TemplateValveRestoreMode { + VALVE_NO_RESTORE, + VALVE_RESTORE, + VALVE_RESTORE_AND_CALL, +}; + +class TemplateValve : public valve::Valve, public Component { + public: + TemplateValve(); + + void set_state_lambda(std::function()> &&f); + Trigger<> *get_open_trigger() const; + Trigger<> *get_close_trigger() const; + Trigger<> *get_stop_trigger() const; + Trigger<> *get_toggle_trigger() const; + Trigger *get_position_trigger() const; + void set_optimistic(bool optimistic); + void set_assumed_state(bool assumed_state); + void set_has_stop(bool has_stop); + void set_has_position(bool has_position); + void set_has_toggle(bool has_toggle); + void set_restore_mode(TemplateValveRestoreMode restore_mode) { restore_mode_ = restore_mode; } + + void setup() override; + void loop() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: + void control(const valve::ValveCall &call) override; + valve::ValveTraits get_traits() override; + void stop_prev_trigger_(); + + TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; + optional()>> state_f_; + bool assumed_state_{false}; + bool optimistic_{false}; + Trigger<> *open_trigger_; + Trigger<> *close_trigger_; + bool has_stop_{false}; + bool has_toggle_{false}; + Trigger<> *stop_trigger_; + Trigger<> *toggle_trigger_; + Trigger<> *prev_command_trigger_{nullptr}; + Trigger *position_trigger_; + bool has_position_{false}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py new file mode 100644 index 0000000000..20e5a645d1 --- /dev/null +++ b/esphome/components/text/__init__.py @@ -0,0 +1,146 @@ +from typing import Optional + +from esphome import automation +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODE, + CONF_MQTT_ID, + CONF_ON_VALUE, + CONF_TRIGGER_ID, + CONF_VALUE, + CONF_WEB_SERVER, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@mauritskorse"] +IS_PLATFORM_COMPONENT = True + +text_ns = cg.esphome_ns.namespace("text") +Text = text_ns.class_("Text", cg.EntityBase) +TextPtr = Text.operator("ptr") + +# Triggers +TextStateTrigger = text_ns.class_( + "TextStateTrigger", automation.Trigger.template(cg.std_string) +) + +# Actions +TextSetAction = text_ns.class_("TextSetAction", automation.Action) + +# Conditions +TextMode = text_ns.enum("TextMode") + +TEXT_MODES = { + "TEXT": TextMode.TEXT_MODE_TEXT, + "PASSWORD": TextMode.TEXT_MODE_PASSWORD, # to be implemented for keys, passwords, etc. +} + +TEXT_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextComponent), + cv.GenerateID(): cv.declare_id(Text), + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TextStateTrigger), + } + ), + cv.Required(CONF_MODE): cv.enum(TEXT_MODES, upper=True), + } + ) +) + + +async def setup_text_core_( + var, + config, + *, + min_length: Optional[int], + max_length: Optional[int], + pattern: Optional[str], +): + await setup_entity(var, config) + + cg.add(var.traits.set_min_length(min_length)) + cg.add(var.traits.set_max_length(max_length)) + if pattern is not None: + cg.add(var.traits.set_pattern(pattern)) + + cg.add(var.traits.set_mode(config[CONF_MODE])) + + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) + await mqtt.register_mqtt_component(mqtt_, config) + + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + + +async def register_text( + var, + config, + *, + min_length: Optional[int] = 0, + max_length: Optional[int] = 255, + pattern: Optional[str] = None, +): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_text(var)) + await setup_text_core_( + var, config, min_length=min_length, max_length=max_length, pattern=pattern + ) + + +async def new_text( + config, + *, + min_length: Optional[int] = 0, + max_length: Optional[int] = 255, + pattern: Optional[str] = None, +): + var = cg.new_Pvariable(config[CONF_ID]) + await register_text( + var, config, min_length=min_length, max_length=max_length, pattern=pattern + ) + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_TEXT") + cg.add_global(text_ns.using) + + +OPERATION_BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Text), + } +) + + +@automation.register_action( + "text.set", + TextSetAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), + } + ), +) +async def text_set_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.std_string) + cg.add(var.set_value(template_)) + return var diff --git a/esphome/components/text/automation.h b/esphome/components/text/automation.h new file mode 100644 index 0000000000..f20a4f433b --- /dev/null +++ b/esphome/components/text/automation.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "text.h" + +namespace esphome { +namespace text { + +class TextStateTrigger : public Trigger { + public: + explicit TextStateTrigger(Text *parent) { + parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); }); + } +}; + +template class TextSetAction : public Action { + public: + explicit TextSetAction(Text *text) : text_(text) {} + TEMPLATABLE_VALUE(std::string, value) + + void play(Ts... x) override { + auto call = this->text_->make_call(); + call.set_value(this->value_.value(x...)); + call.perform(); + } + + protected: + Text *text_; +}; + +} // namespace text +} // namespace esphome diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp new file mode 100644 index 0000000000..8f0242e747 --- /dev/null +++ b/esphome/components/text/text.cpp @@ -0,0 +1,26 @@ +#include "text.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace text { + +static const char *const TAG = "text"; + +void Text::publish_state(const std::string &state) { + this->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()); + + } else { + ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); + } + this->state_callback_.call(state); +} + +void Text::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} + +} // namespace text +} // namespace esphome diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h new file mode 100644 index 0000000000..f71dde69ba --- /dev/null +++ b/esphome/components/text/text.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" +#include "text_call.h" +#include "text_traits.h" + +namespace esphome { +namespace text { + +#define LOG_TEXT(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +/** Base-class for all text inputs. + * + * A text input can use publish_state to send out a new value. + */ +class Text : public EntityBase { + public: + std::string state; + TextTraits traits; + + void publish_state(const std::string &state); + + /// Return whether this text input has gotten a full state yet. + bool has_state() const { return has_state_; } + + /// 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); + + protected: + friend class TextCall; + + /** Set the value of the text input, this is a virtual method that each text input integration must implement. + * + * This method is called by the TextCall. + * + * @param value The value as validated by the TextCall. + */ + virtual void control(const std::string &value) = 0; + + CallbackManager state_callback_; + bool has_state_{false}; +}; + +} // namespace text +} // namespace esphome diff --git a/esphome/components/text/text_call.cpp b/esphome/components/text/text_call.cpp new file mode 100644 index 0000000000..0d0a1d228d --- /dev/null +++ b/esphome/components/text/text_call.cpp @@ -0,0 +1,56 @@ +#include "text_call.h" +#include "esphome/core/log.h" +#include "text.h" + +namespace esphome { +namespace text { + +static const char *const TAG = "text"; + +TextCall &TextCall::set_value(const std::string &value) { + this->value_ = value; + return *this; +} + +void TextCall::validate_() { + const auto *name = this->parent_->get_name().c_str(); + + if (!this->value_.has_value()) { + ESP_LOGW(TAG, "'%s' - No value set for TextCall", name); + return; + } + + int sz = this->value_.value().size(); + + if (sz > this->parent_->traits.get_max_length()) { + ESP_LOGW(TAG, "'%s' - Value set for TextCall is too long", name); + this->value_.reset(); + return; + } + + if (sz < this->parent_->traits.get_min_length()) { + ESP_LOGW(TAG, "'%s' - Value set for TextCall is too short", name); + this->value_.reset(); + return; + } +} + +void TextCall::perform() { + this->validate_(); + if (!this->value_.has_value()) { + ESP_LOGW(TAG, "'%s' - No value set for TextCall", this->parent_->get_name().c_str()); + return; + } + std::string target_value = this->value_.value(); + + if (this->parent_->traits.get_mode() == TEXT_MODE_PASSWORD) { + ESP_LOGD(TAG, "'%s' - Setting password value: " LOG_SECRET("'%s'"), this->parent_->get_name().c_str(), + target_value.c_str()); + } else { + ESP_LOGD(TAG, "'%s' - Setting text value: %s", this->parent_->get_name().c_str(), target_value.c_str()); + } + this->parent_->control(target_value); +} + +} // namespace text +} // namespace esphome diff --git a/esphome/components/text/text_call.h b/esphome/components/text/text_call.h new file mode 100644 index 0000000000..9f75a25c6b --- /dev/null +++ b/esphome/components/text/text_call.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "text_traits.h" + +namespace esphome { +namespace text { + +class Text; + +class TextCall { + public: + explicit TextCall(Text *parent) : parent_(parent) {} + void perform(); + + TextCall &set_value(const std::string &value); + + protected: + Text *const parent_; + optional value_; + void validate_(); +}; + +} // namespace text +} // namespace esphome diff --git a/esphome/components/text/text_traits.h b/esphome/components/text/text_traits.h new file mode 100644 index 0000000000..952afa70c7 --- /dev/null +++ b/esphome/components/text/text_traits.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace text { + +enum TextMode : uint8_t { + TEXT_MODE_TEXT = 0, + TEXT_MODE_PASSWORD = 1, +}; + +class TextTraits { + public: + // Set/get the number value boundaries. + void set_min_length(int min_length) { this->min_length_ = min_length; } + int get_min_length() const { return this->min_length_; } + void set_max_length(int max_length) { this->max_length_ = max_length; } + 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_; } + + // Set/get the frontend mode. + void set_mode(TextMode mode) { this->mode_ = mode; } + TextMode get_mode() const { return this->mode_; } + + protected: + int min_length_; + int max_length_; + std::string pattern_; + TextMode mode_{TEXT_MODE_TEXT}; +}; + +} // namespace text +} // namespace esphome diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 3ed6b72d8f..12993d9ffc 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -1,25 +1,36 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import mqtt +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_FILTERS, + CONF_FROM, CONF_ICON, CONF_ID, - CONF_ON_VALUE, - CONF_ON_RAW_VALUE, - CONF_TRIGGER_ID, CONF_MQTT_ID, + CONF_ON_RAW_VALUE, + CONF_ON_VALUE, CONF_STATE, - CONF_FROM, CONF_TO, + CONF_TRIGGER_ID, + CONF_WEB_SERVER, + DEVICE_CLASS_DATE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TIMESTAMP, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity from esphome.util import Registry +DEVICE_CLASSES = [ + DEVICE_CLASS_DATE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TIMESTAMP, +] + IS_PLATFORM_COMPONENT = True @@ -112,24 +123,33 @@ async def map_filter_to_code(config, filter_id): ) -TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( - { - cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), - cv.GenerateID(): cv.declare_id(TextSensor), - cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TextSensorStateTrigger), - } - ), - cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - TextSensorStateRawTrigger - ), - } - ), - } +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") + +TEXT_SENSOR_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), + cv.GenerateID(): cv.declare_id(TextSensor), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + TextSensorStateTrigger + ), + } + ), + cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + TextSensorStateRawTrigger + ), + } + ), + } + ) ) _UNDEF = object() @@ -140,12 +160,21 @@ def text_sensor_schema( *, icon: str = _UNDEF, entity_category: str = _UNDEF, + device_class: str = _UNDEF, ) -> cv.Schema: schema = TEXT_SENSOR_SCHEMA if class_ is not _UNDEF: schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) if icon is not _UNDEF: schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + if device_class is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } + ) if entity_category is not _UNDEF: schema = schema.extend( { @@ -164,6 +193,9 @@ async def build_filters(config): async def setup_text_sensor_core_(var, config): await setup_entity(var, config) + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) @@ -176,10 +208,13 @@ async def setup_text_sensor_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - if CONF_MQTT_ID in config: - mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: + mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + async def register_text_sensor(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 4e36532945..2de9010b88 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -28,7 +28,7 @@ class Filter { * @param value The new value. * @return An optional string, the new value that should be pushed out. */ - virtual optional new_value(std::string value); + virtual optional new_value(std::string value) = 0; /// Initialize this filter, please note this can be called more than once. virtual void initialize(TextSensor *parent, Filter *next); diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 996af02f7e..bd72ea70e3 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -13,6 +13,9 @@ namespace text_sensor { #define LOG_TEXT_SENSOR(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + } \ if (!(obj)->get_icon().empty()) { \ ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ } \ @@ -28,7 +31,7 @@ namespace text_sensor { public: \ void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; } -class TextSensor : public EntityBase { +class TextSensor : public EntityBase, public EntityBase_DeviceClass { public: /// Getter-syntax for .state. std::string get_state() const; diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 9a57f6a337..a529bbd474 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import climate, sensor +import esphome.config_validation as cv from esphome.const import ( CONF_AUTO_MODE, CONF_AWAY_CONFIG, @@ -15,15 +15,15 @@ from esphome.const import ( CONF_DRY_ACTION, CONF_DRY_MODE, CONF_FAN_MODE, - CONF_FAN_MODE_ON_ACTION, - CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, + CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_FOCUS_ACTION, + CONF_FAN_MODE_HIGH_ACTION, CONF_FAN_MODE_LOW_ACTION, CONF_FAN_MODE_MEDIUM_ACTION, - CONF_FAN_MODE_HIGH_ACTION, CONF_FAN_MODE_MIDDLE_ACTION, - CONF_FAN_MODE_FOCUS_ACTION, - CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_OFF_ACTION, + CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_QUIET_ACTION, CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, @@ -35,6 +35,7 @@ from esphome.const import ( CONF_HEAT_DEADBAND, CONF_HEAT_MODE, CONF_HEAT_OVERRUN, + CONF_HUMIDITY_SENSOR, CONF_ID, CONF_IDLE_ACTION, CONF_MAX_COOLING_RUN_TIME, @@ -49,8 +50,8 @@ from esphome.const import ( CONF_MIN_HEATING_RUN_TIME, CONF_MIN_IDLE_TIME, CONF_MIN_TEMPERATURE, - CONF_NAME, CONF_MODE, + CONF_NAME, CONF_OFF_MODE, CONF_PRESET, CONF_SENSOR, @@ -519,6 +520,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ThermostatClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), cv.Optional( @@ -591,11 +593,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Optional( CONF_SET_POINT_MINIMUM_DIFFERENTIAL, default=0.5 - ): cv.temperature, - cv.Optional(CONF_COOL_DEADBAND, default=0.5): cv.temperature, - cv.Optional(CONF_COOL_OVERRUN, default=0.5): cv.temperature, - cv.Optional(CONF_HEAT_DEADBAND, default=0.5): cv.temperature, - cv.Optional(CONF_HEAT_OVERRUN, default=0.5): cv.temperature, + ): cv.temperature_delta, + cv.Optional(CONF_COOL_DEADBAND, default=0.5): cv.temperature_delta, + cv.Optional(CONF_COOL_OVERRUN, default=0.5): cv.temperature_delta, + cv.Optional(CONF_HEAT_DEADBAND, default=0.5): cv.temperature_delta, + cv.Optional(CONF_HEAT_OVERRUN, default=0.5): cv.temperature_delta, cv.Optional(CONF_MAX_COOLING_RUN_TIME): cv.positive_time_period_seconds, cv.Optional(CONF_MAX_HEATING_RUN_TIME): cv.positive_time_period_seconds, cv.Optional(CONF_MIN_COOLING_OFF_TIME): cv.positive_time_period_seconds, @@ -608,8 +610,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MIN_HEATING_OFF_TIME): cv.positive_time_period_seconds, cv.Optional(CONF_MIN_HEATING_RUN_TIME): cv.positive_time_period_seconds, cv.Required(CONF_MIN_IDLE_TIME): cv.positive_time_period_seconds, - cv.Optional(CONF_SUPPLEMENTAL_COOLING_DELTA): cv.temperature, - cv.Optional(CONF_SUPPLEMENTAL_HEATING_DELTA): cv.temperature, + cv.Optional(CONF_SUPPLEMENTAL_COOLING_DELTA): cv.temperature_delta, + cv.Optional(CONF_SUPPLEMENTAL_HEATING_DELTA): cv.temperature_delta, cv.Optional( CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, default=False ): cv.boolean, @@ -658,6 +660,10 @@ async def to_code(config): ) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) @@ -886,7 +892,7 @@ async def to_code(config): if name.upper() in climate.CLIMATE_PRESETS: standard_preset = climate.CLIMATE_PRESETS[name.upper()] - if two_points_available is True: + if two_points_available: preset_target_config = ThermostatClimateTargetTempConfig( preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], @@ -899,6 +905,8 @@ async def to_code(config): preset_target_config = ThermostatClimateTargetTempConfig( preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] ) + else: + preset_target_config = None preset_target_variable = cg.new_variable( preset_config[CONF_ID], preset_target_config diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 51da663a0c..f7b3410df9 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -26,6 +26,15 @@ void ThermostatClimate::setup() { }); this->current_temperature = this->sensor_->state; + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + auto use_default_preset = true; if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) { @@ -53,6 +62,15 @@ void ThermostatClimate::setup() { this->publish_state(); } +void ThermostatClimate::loop() { + for (auto &timer : this->timer_) { + if (timer.active && (timer.started + timer.time < millis())) { + timer.active = false; + timer.func(); + } + } +} + float ThermostatClimate::cool_deadband() { return this->cooling_deadband_; } float ThermostatClimate::cool_overrun() { return this->cooling_overrun_; } float ThermostatClimate::heat_deadband() { return this->heating_deadband_; } @@ -216,6 +234,9 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); + if (supports_auto_) traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); if (supports_heat_cool_) @@ -426,6 +447,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } + this->cooling_max_runtime_exceeded_ = false; trig = this->cool_action_trigger_; ESP_LOGVV(TAG, "Switching to COOLING action"); action_ready = true; @@ -439,6 +461,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } + this->heating_max_runtime_exceeded_ = false; trig = this->heat_action_trigger_; ESP_LOGVV(TAG, "Switching to HEATING action"); action_ready = true; @@ -479,8 +502,9 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } this->action = action; this->prev_action_trigger_ = trig; - assert(trig != nullptr); - trig->trigger(); + if (trig != nullptr) { + trig->trigger(); + } // if enabled, call the fan_only action with cooling/heating actions if (trig_fan != nullptr) { ESP_LOGVV(TAG, "Calling FAN_ONLY action with HEATING/COOLING action"); @@ -541,7 +565,6 @@ void ThermostatClimate::trigger_supplemental_action_() { } if (trig != nullptr) { - assert(trig != nullptr); trig->trigger(); } } @@ -611,8 +634,9 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo this->prev_fan_mode_trigger_ = nullptr; } this->start_timer_(thermostat::TIMER_FAN_MODE); - assert(trig != nullptr); - trig->trigger(); + if (trig != nullptr) { + trig->trigger(); + } this->prev_fan_mode_ = fan_mode; this->prev_fan_mode_trigger_ = trig; } @@ -655,8 +679,9 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ mode = climate::CLIMATE_MODE_HEAT_COOL; // trig = this->auto_mode_trigger_; } - assert(trig != nullptr); - trig->trigger(); + if (trig != nullptr) { + trig->trigger(); + } this->mode = mode; this->prev_mode_ = mode; this->prev_mode_trigger_ = trig; @@ -695,8 +720,9 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo swing_mode = climate::CLIMATE_SWING_OFF; // trig = this->swing_mode_off_trigger_; } - assert(trig != nullptr); - trig->trigger(); + if (trig != nullptr) { + trig->trigger(); + } this->swing_mode = swing_mode; this->prev_swing_mode_ = swing_mode; this->prev_swing_mode_trigger_ = trig; @@ -739,15 +765,15 @@ bool ThermostatClimate::heating_action_ready_() { void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { if (this->timer_duration_(timer_index) > 0) { - this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), - this->timer_cbf_(timer_index)); + this->timer_[timer_index].started = millis(); this->timer_[timer_index].active = true; } } bool ThermostatClimate::cancel_timer_(ThermostatClimateTimerIndex timer_index) { + auto ret = this->timer_[timer_index].active; this->timer_[timer_index].active = false; - return this->cancel_timeout(this->timer_[timer_index].name); + return ret; } bool ThermostatClimate::timer_active_(ThermostatClimateTimerIndex timer_index) { @@ -764,7 +790,6 @@ std::function ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex void ThermostatClimate::cooling_max_run_time_timer_callback_() { ESP_LOGVV(TAG, "cooling_max_run_time timer expired"); - this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].active = false; this->cooling_max_runtime_exceeded_ = true; this->trigger_supplemental_action_(); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); @@ -772,21 +797,18 @@ void ThermostatClimate::cooling_max_run_time_timer_callback_() { void ThermostatClimate::cooling_off_timer_callback_() { ESP_LOGVV(TAG, "cooling_off timer expired"); - this->timer_[thermostat::TIMER_COOLING_OFF].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } void ThermostatClimate::cooling_on_timer_callback_() { ESP_LOGVV(TAG, "cooling_on timer expired"); - this->timer_[thermostat::TIMER_COOLING_ON].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } void ThermostatClimate::fan_mode_timer_callback_() { ESP_LOGVV(TAG, "fan_mode timer expired"); - this->timer_[thermostat::TIMER_FAN_MODE].active = false; this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON)); if (this->supports_fan_only_action_uses_fan_mode_timer_) this->switch_to_action_(this->compute_action_()); @@ -794,19 +816,16 @@ void ThermostatClimate::fan_mode_timer_callback_() { void ThermostatClimate::fanning_off_timer_callback_() { ESP_LOGVV(TAG, "fanning_off timer expired"); - this->timer_[thermostat::TIMER_FANNING_OFF].active = false; this->switch_to_action_(this->compute_action_()); } void ThermostatClimate::fanning_on_timer_callback_() { ESP_LOGVV(TAG, "fanning_on timer expired"); - this->timer_[thermostat::TIMER_FANNING_ON].active = false; this->switch_to_action_(this->compute_action_()); } void ThermostatClimate::heating_max_run_time_timer_callback_() { ESP_LOGVV(TAG, "heating_max_run_time timer expired"); - this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].active = false; this->heating_max_runtime_exceeded_ = true; this->trigger_supplemental_action_(); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); @@ -814,21 +833,18 @@ void ThermostatClimate::heating_max_run_time_timer_callback_() { void ThermostatClimate::heating_off_timer_callback_() { ESP_LOGVV(TAG, "heating_off timer expired"); - this->timer_[thermostat::TIMER_HEATING_OFF].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } void ThermostatClimate::heating_on_timer_callback_() { ESP_LOGVV(TAG, "heating_on timer expired"); - this->timer_[thermostat::TIMER_HEATING_ON].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } void ThermostatClimate::idle_on_timer_callback_() { ESP_LOGVV(TAG, "idle_on timer expired"); - this->timer_[thermostat::TIMER_IDLE_ON].active = false; this->switch_to_action_(this->compute_action_()); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } @@ -854,8 +870,9 @@ void ThermostatClimate::check_temperature_change_trigger_() { } // trigger the action Trigger<> *trig = this->temperature_change_trigger_; - assert(trig != nullptr); - trig->trigger(); + if (trig != nullptr) { + trig->trigger(); + } } bool ThermostatClimate::cooling_required_() { @@ -985,8 +1002,10 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - assert(trig != nullptr); - trig->trigger(); + this->preset = preset; + if (trig != nullptr) { + trig->trigger(); + } this->refresh(); ESP_LOGI(TAG, "Preset %s applied", LOG_STR_ARG(climate::climate_preset_to_string(preset))); @@ -1009,8 +1028,10 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) this->custom_preset.value() != custom_preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - assert(trig != nullptr); - trig->trigger(); + this->custom_preset = custom_preset; + if (trig != nullptr) { + trig->trigger(); + } this->refresh(); ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); @@ -1166,6 +1187,9 @@ void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { + this->humidity_sensor_ = humidity_sensor; +} void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; @@ -1281,11 +1305,13 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Overrun: %.1f°C", this->cooling_overrun_); if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { ESP_LOGCONFIG(TAG, " Supplemental Delta: %.1f°C", this->supplemental_cool_delta_); - ESP_LOGCONFIG(TAG, " Maximum Run Time: %us", + ESP_LOGCONFIG(TAG, " Maximum Run Time: %" PRIu32 "s", this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000); } - ESP_LOGCONFIG(TAG, " Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000); - ESP_LOGCONFIG(TAG, " Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); + ESP_LOGCONFIG(TAG, " Minimum Off Time: %" PRIu32 "s", + this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Minimum Run Time: %" PRIu32 "s", + this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); } if (this->supports_heat_) { ESP_LOGCONFIG(TAG, " Heating Parameters:"); @@ -1293,24 +1319,28 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Overrun: %.1f°C", this->heating_overrun_); if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { ESP_LOGCONFIG(TAG, " Supplemental Delta: %.1f°C", this->supplemental_heat_delta_); - ESP_LOGCONFIG(TAG, " Maximum Run Time: %us", + ESP_LOGCONFIG(TAG, " Maximum Run Time: %" PRIu32 "s", this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000); } - ESP_LOGCONFIG(TAG, " Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000); - ESP_LOGCONFIG(TAG, " Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); + ESP_LOGCONFIG(TAG, " Minimum Off Time: %" PRIu32 "s", + this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Minimum Run Time: %" PRIu32 "s", + this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); } if (this->supports_fan_only_) { - ESP_LOGCONFIG(TAG, " Fanning Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000); - ESP_LOGCONFIG(TAG, " Fanning Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); + ESP_LOGCONFIG(TAG, " Fanning Minimum Off Time: %" PRIu32 "s", + this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Fanning Minimum Run Time: %" PRIu32 "s", + this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); } if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ || this->supports_fan_mode_quiet_) { - ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %us", + ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %" PRIu32 "s", this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); } - ESP_LOGCONFIG(TAG, " Minimum Idle Time: %us", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); + ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); ESP_LOGCONFIG(TAG, " Supports HEAT/COOL: %s", YESNO(this->supports_heat_cool_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 677b4ad324..50510cf070 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -1,10 +1,12 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" +#include #include #include @@ -26,9 +28,9 @@ enum ThermostatClimateTimerIndex : size_t { enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; struct ThermostatClimateTimer { - const std::string name; bool active; uint32_t time; + uint32_t started; std::function func; }; @@ -59,6 +61,7 @@ class ThermostatClimate : public climate::Climate, public Component { ThermostatClimate(); void setup() override; void dump_config() override; + void loop() override; void set_default_preset(const std::string &custom_preset); void set_default_preset(climate::ClimatePreset preset); @@ -81,6 +84,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_heating_minimum_run_time_in_sec(uint32_t time); void set_idle_minimum_time_in_sec(uint32_t time); void set_sensor(sensor::Sensor *sensor); + void set_humidity_sensor(sensor::Sensor *humidity_sensor); void set_use_startup_delay(bool use_startup_delay); void set_supports_auto(bool supports_auto); void set_supports_heat_cool(bool supports_heat_cool); @@ -238,6 +242,8 @@ class ThermostatClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; /// Whether the controller supports auto/cooling/drying/fanning/heating. /// @@ -438,16 +444,17 @@ class ThermostatClimate : public climate::Climate, public Component { /// Climate action timers std::vector timer_{ - {"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, - {"cool_off", false, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)}, - {"cool_on", false, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)}, - {"fan_mode", false, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)}, - {"fan_off", false, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)}, - {"fan_on", false, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)}, - {"heat_run", false, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)}, - {"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, - {"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, - {"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}}; + {false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, + {false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}, + }; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) std::map preset_config_{}; diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index b2be11611d..6a3368ca73 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,32 +1,32 @@ -import logging from importlib import resources +import logging from typing import Optional import tzlocal +from esphome import automation +from esphome.automation import Condition import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation from esphome.const import ( - CONF_ID, + CONF_AT, CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, + CONF_HOUR, CONF_HOURS, + CONF_ID, + CONF_MINUTE, CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_ON_TIME_SYNC, + CONF_SECOND, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, - CONF_AT, - CONF_SECOND, - CONF_HOUR, - CONF_MINUTE, ) from esphome.core import coroutine_with_priority -from esphome.automation import Condition _LOGGER = logging.getLogger(__name__) @@ -37,7 +37,6 @@ time_ns = cg.esphome_ns.namespace("time") RealTimeClock = time_ns.class_("RealTimeClock", cg.PollingComponent) CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Component) SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component) -ESPTime = time_ns.struct("ESPTime") TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition) @@ -50,7 +49,7 @@ def _load_tzdata(iana_key: str) -> Optional[bytes]: package = "tzdata.zoneinfo." + package_loc.replace("/", ".") try: - return resources.read_binary(package, resource) + return (resources.files(package) / resource).read_bytes() except (FileNotFoundError, ModuleNotFoundError): return None diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index af2b6c720c..f7c1916ffe 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -1,5 +1,7 @@ #include "automation.h" + #include "esphome/core/log.h" + #include namespace esphome { diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index e97413e420..b5c8291533 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -1,7 +1,9 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/time.h" + #include "real_time_clock.h" #include diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index de76676a4d..2b9a95c6bd 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -1,6 +1,10 @@ #include "real_time_clock.h" #include "esphome/core/log.h" +#ifdef USE_HOST +#include +#else #include "lwip/opt.h" +#endif #ifdef USE_ESP8266 #include "sys/time.h" #endif @@ -9,6 +13,8 @@ #endif #include +#include + namespace esphome { namespace time { @@ -24,8 +30,8 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; - ESP_LOGVV(TAG, "Got epoch %u", epoch); - timezone tz = {0, 0}; + ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); + struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); if (ret == EINVAL) { // Some ESP8266 frameworks abort when timezone parameter is not NULL @@ -52,171 +58,5 @@ void RealTimeClock::apply_timezone_() { tzset(); } -size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { - struct tm c_tm = this->to_c_tm(); - return ::strftime(buffer, buffer_len, format, &c_tm); -} -ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { - ESPTime res{}; - res.second = uint8_t(c_tm->tm_sec); - res.minute = uint8_t(c_tm->tm_min); - res.hour = uint8_t(c_tm->tm_hour); - res.day_of_week = uint8_t(c_tm->tm_wday + 1); - res.day_of_month = uint8_t(c_tm->tm_mday); - res.day_of_year = uint16_t(c_tm->tm_yday + 1); - res.month = uint8_t(c_tm->tm_mon + 1); - res.year = uint16_t(c_tm->tm_year + 1900); - res.is_dst = bool(c_tm->tm_isdst); - res.timestamp = c_time; - return res; -} -struct tm ESPTime::to_c_tm() { - struct tm c_tm {}; - c_tm.tm_sec = this->second; - c_tm.tm_min = this->minute; - c_tm.tm_hour = this->hour; - c_tm.tm_mday = this->day_of_month; - c_tm.tm_mon = this->month - 1; - c_tm.tm_year = this->year - 1900; - c_tm.tm_wday = this->day_of_week - 1; - c_tm.tm_yday = this->day_of_year - 1; - c_tm.tm_isdst = this->is_dst; - return c_tm; -} -std::string ESPTime::strftime(const std::string &format) { - std::string timestr; - timestr.resize(format.size() * 4); - struct tm c_tm = this->to_c_tm(); - size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - while (len == 0) { - timestr.resize(timestr.size() * 2); - len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - } - timestr.resize(len); - return timestr; -} - -template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) { - current++; - if (current >= end) { - current = begin; - return true; - } - return false; -} - -static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } - -static uint8_t days_in_month(uint8_t month, uint16_t year) { - static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - uint8_t days = DAYS_IN_MONTH[month]; - if (month == 2 && is_leap_year(year)) - return 29; - return days; -} - -void ESPTime::increment_second() { - this->timestamp++; - if (!increment_time_value(this->second, 0, 60)) - return; - - // second roll-over, increment minute - if (!increment_time_value(this->minute, 0, 60)) - return; - - // minute roll-over, increment hour - if (!increment_time_value(this->hour, 0, 24)) - return; - - // hour roll-over, increment day - increment_time_value(this->day_of_week, 1, 8); - - if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { - // day of month roll-over, increment month - increment_time_value(this->month, 1, 13); - } - - uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; - if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { - // day of year roll-over, increment year - this->year++; - } -} -void ESPTime::increment_day() { - this->timestamp += 86400; - - // increment day - increment_time_value(this->day_of_week, 1, 8); - - if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { - // day of month roll-over, increment month - increment_time_value(this->month, 1, 13); - } - - uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; - if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { - // day of year roll-over, increment year - this->year++; - } -} -void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { - time_t res = 0; - - if (!this->fields_in_range()) { - this->timestamp = -1; - return; - } - - for (int i = 1970; i < this->year; i++) - res += is_leap_year(i) ? 366 : 365; - - if (use_day_of_year) { - res += this->day_of_year - 1; - } else { - for (int i = 1; i < this->month; i++) - res += days_in_month(i, this->year); - - res += this->day_of_month - 1; - } - - res *= 24; - res += this->hour; - res *= 60; - res += this->minute; - res *= 60; - res += this->second; - this->timestamp = res; -} - -int32_t ESPTime::timezone_offset() { - int32_t offset = 0; - time_t now = ::time(nullptr); - auto local = ESPTime::from_epoch_local(now); - auto utc = ESPTime::from_epoch_utc(now); - bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; - - if (utc.minute > local.minute) { - local.minute += 60; - local.hour -= 1; - } - offset += (local.minute - utc.minute) * 60; - - if (negative) { - offset -= (utc.hour - local.hour) * 3600; - } else { - if (utc.hour > local.hour) { - local.hour += 24; - } - offset += (local.hour - utc.hour) * 3600; - } - return offset; -} - -bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } -bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } -bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } -bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; } -bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; } - } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 7f4afee306..a17168ae6f 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -1,106 +1,15 @@ #pragma once +#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include -#include -#include +#include "esphome/core/time.h" namespace esphome { namespace time { -/// A more user-friendly version of struct tm from time.h -struct ESPTime { - /** seconds after the minute [0-60] - * @note second is generally 0-59; the extra range is to accommodate leap seconds. - */ - uint8_t second; - /// minutes after the hour [0-59] - uint8_t minute; - /// hours since midnight [0-23] - uint8_t hour; - /// day of the week; sunday=1 [1-7] - uint8_t day_of_week; - /// day of the month [1-31] - uint8_t day_of_month; - /// day of the year [1-366] - uint16_t day_of_year; - /// month; january=1 [1-12] - uint8_t month; - /// year - uint16_t year; - /// daylight saving time flag - bool is_dst; - /// unix epoch time (seconds since UTC Midnight January 1, 1970) - time_t timestamp; - - /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. - * Up to buffer_len bytes are written. - * - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime - */ - size_t strftime(char *buffer, size_t buffer_len, const char *format); - - /** Convert this ESPTime struct to a string as specified by the format argument. - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime - * - * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some - * microcontrollers. - */ - std::string strftime(const std::string &format); - - /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) - bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } - - /// Check if all time fields of this ESPTime are in range. - bool fields_in_range() const { - return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && - this->day_of_year < 367 && this->month > 0 && this->month < 13; - } - - /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance. - static ESPTime from_c_tm(struct tm *c_tm, time_t c_time); - - /** Convert an UTC epoch timestamp to a local time ESPTime instance. - * - * @param epoch Seconds since 1st January 1970. In UTC. - * @return The generated ESPTime - */ - static ESPTime from_epoch_local(time_t epoch) { - struct tm *c_tm = ::localtime(&epoch); - return ESPTime::from_c_tm(c_tm, epoch); - } - /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. - * - * @param epoch Seconds since 1st January 1970. In UTC. - * @return The generated ESPTime - */ - static ESPTime from_epoch_utc(time_t epoch) { - struct tm *c_tm = ::gmtime(&epoch); - return ESPTime::from_c_tm(c_tm, epoch); - } - - /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). - void recalc_timestamp_utc(bool use_day_of_year = true); - - /// Convert this ESPTime instance back to a tm struct. - struct tm to_c_tm(); - - static int32_t timezone_offset(); - - /// Increment this clock instance by one second. - void increment_second(); - /// Increment this clock instance by one day. - void increment_day(); - bool operator<(ESPTime other); - bool operator<=(ESPTime other); - bool operator==(ESPTime other); - bool operator>=(ESPTime other); - bool operator>(ESPTime other); -}; - /// The RealTimeClock class exposes common timekeeping functions via the device's local real-time clock. /// /// \note diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 50376224a9..e1936d5ee1 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -96,6 +96,9 @@ void TimeBasedCover::control(const CoverCall &call) { } } else { auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; + if (this->manual_control_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) { + this->position = pos == COVER_CLOSED ? COVER_OPEN : COVER_CLOSED; + } this->target_position_ = pos; this->start_direction_(op); } diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index b7a826d237..42cf66c2ab 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -23,6 +23,7 @@ class TimeBasedCover : public cover::Cover, public Component { void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } void set_manual_control(bool value) { this->manual_control_ = value; } void set_assumed_state(bool value) { this->assumed_state_ = value; } + cover::CoverOperation get_last_operation() const { return this->last_operation_; } protected: void control(const cover::CoverCall &call) override; diff --git a/esphome/components/tlc5947/__init__.py b/esphome/components/tlc5947/__init__.py index 84380bdace..528d690fab 100644 --- a/esphome/components/tlc5947/__init__.py +++ b/esphome/components/tlc5947/__init__.py @@ -9,12 +9,11 @@ from esphome.const import ( CONF_DATA_PIN, CONF_ID, CONF_NUM_CHIPS, + CONF_OE_PIN, ) CONF_LAT_PIN = "lat_pin" -CONF_OE_PIN = "oe_pin" -AUTO_LOAD = ["output"] CODEOWNERS = ["@rnauber"] tlc5947_ns = cg.esphome_ns.namespace("tlc5947") diff --git a/esphome/components/tlc5947/output.py b/esphome/components/tlc5947/output.py deleted file mode 100644 index ece47fa63d..0000000000 --- a/esphome/components/tlc5947/output.py +++ /dev/null @@ -1,28 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import output -from esphome.const import CONF_CHANNEL, CONF_ID -from . import TLC5947 - -DEPENDENCIES = ["tlc5947"] -CODEOWNERS = ["@rnauber"] - -Channel = TLC5947.class_("Channel", output.FloatOutput) - -CONF_TLC5947_ID = "tlc5947_id" -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( - { - cv.GenerateID(CONF_TLC5947_ID): cv.use_id(TLC5947), - cv.Required(CONF_ID): cv.declare_id(Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), - } -).extend(cv.COMPONENT_SCHEMA) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await output.register_output(var, config) - - parent = await cg.get_variable(config[CONF_TLC5947_ID]) - cg.add(var.set_parent(parent)) - cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/tlc5947/output/__init__.py b/esphome/components/tlc5947/output/__init__.py new file mode 100644 index 0000000000..1b5dff1854 --- /dev/null +++ b/esphome/components/tlc5947/output/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from .. import TLC5947, tlc5947_ns + +DEPENDENCIES = ["tlc5947"] + +TLC5947Channel = tlc5947_ns.class_( + "TLC5947Channel", output.FloatOutput, cg.Parented.template(TLC5947) +) + +CONF_TLC5947_ID = "tlc5947_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_TLC5947_ID): cv.use_id(TLC5947), + cv.Required(CONF_ID): cv.declare_id(TLC5947Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + await cg.register_parented(var, config[CONF_TLC5947_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/tlc5947/output/tlc5947_output.cpp b/esphome/components/tlc5947/output/tlc5947_output.cpp new file mode 100644 index 0000000000..9630fb8c1e --- /dev/null +++ b/esphome/components/tlc5947/output/tlc5947_output.cpp @@ -0,0 +1,12 @@ +#include "tlc5947_output.h" + +namespace esphome { +namespace tlc5947 { + +void TLC5947Channel::write_state(float state) { + auto amount = static_cast(state * 0xfff); + this->parent_->set_channel_value(this->channel_, amount); +} + +} // namespace tlc5947 +} // namespace esphome diff --git a/esphome/components/tlc5947/output/tlc5947_output.h b/esphome/components/tlc5947/output/tlc5947_output.h new file mode 100644 index 0000000000..5b2c51020c --- /dev/null +++ b/esphome/components/tlc5947/output/tlc5947_output.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/helpers.h" + +#include "esphome/components/output/float_output.h" + +#include "../tlc5947.h" + +namespace esphome { +namespace tlc5947 { + +class TLC5947Channel : public output::FloatOutput, public Parented { + public: + void set_channel(uint8_t channel) { this->channel_ = channel; } + + protected: + void write_state(float state) override; + uint8_t channel_; +}; + +} // namespace tlc5947 +} // namespace esphome diff --git a/esphome/components/tlc5947/tlc5947.cpp b/esphome/components/tlc5947/tlc5947.cpp index a7e08c8341..5a5c0c17c0 100644 --- a/esphome/components/tlc5947/tlc5947.cpp +++ b/esphome/components/tlc5947/tlc5947.cpp @@ -27,8 +27,7 @@ void TLC5947::dump_config() { LOG_PIN(" Data Pin: ", this->data_pin_); LOG_PIN(" Clock Pin: ", this->clock_pin_); LOG_PIN(" LAT Pin: ", this->lat_pin_); - if (this->outenable_pin_ != nullptr) - LOG_PIN(" OE Pin: ", this->outenable_pin_); + LOG_PIN(" OE Pin: ", this->outenable_pin_); ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); } @@ -61,5 +60,14 @@ void TLC5947::loop() { this->update_ = false; } +void TLC5947::set_channel_value(uint16_t channel, uint16_t value) { + if (channel >= this->num_chips_ * N_CHANNELS_PER_CHIP) + return; + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + } + this->pwm_amounts_[channel] = value; +} + } // namespace tlc5947 } // namespace esphome diff --git a/esphome/components/tlc5947/tlc5947.h b/esphome/components/tlc5947/tlc5947.h index 0eb7f10604..95d76408c9 100644 --- a/esphome/components/tlc5947/tlc5947.h +++ b/esphome/components/tlc5947/tlc5947.h @@ -2,18 +2,16 @@ // TLC5947 24-Channel, 12-Bit PWM LED Driver // https://www.ti.com/lit/ds/symlink/tlc5947.pdf +#include +#include "esphome/components/output/float_output.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/components/output/float_output.h" -#include namespace esphome { namespace tlc5947 { class TLC5947 : public Component { public: - class Channel; - const uint8_t N_CHANNELS_PER_CHIP = 24; void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } @@ -31,31 +29,9 @@ class TLC5947 : public Component { /// Send new values if they were updated. void loop() override; - class Channel : public output::FloatOutput { - public: - void set_parent(TLC5947 *parent) { parent_ = parent; } - void set_channel(uint8_t channel) { channel_ = channel; } - - protected: - void write_state(float state) override { - auto amount = static_cast(state * 0xfff); - this->parent_->set_channel_value_(this->channel_, amount); - } - - TLC5947 *parent_; - uint8_t channel_; - }; + void set_channel_value(uint16_t channel, uint16_t value); protected: - void set_channel_value_(uint16_t channel, uint16_t value) { - if (channel >= this->num_chips_ * N_CHANNELS_PER_CHIP) - return; - if (this->pwm_amounts_[channel] != value) { - this->update_ = true; - } - this->pwm_amounts_[channel] = value; - } - GPIOPin *data_pin_; GPIOPin *clock_pin_; GPIOPin *lat_pin_; diff --git a/esphome/components/tlc5971/__init__.py b/esphome/components/tlc5971/__init__.py new file mode 100644 index 0000000000..0ff2a5d176 --- /dev/null +++ b/esphome/components/tlc5971/__init__.py @@ -0,0 +1,40 @@ +# this component is for the "TLC5971 12-Channel, 12-Bit PWM LED Driver" [https://www.ti.com/lit/ds/symlink/tlc5971.pdf], +# which is used e.g. on [https://www.adafruit.com/product/1455]. The code is based on the TLC5947 component by @rnauber. + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, + CONF_NUM_CHIPS, +) + + +CODEOWNERS = ["@IJIJI"] + +tlc5971_ns = cg.esphome_ns.namespace("tlc5971") +TLC5971 = tlc5971_ns.class_("TLC5971", cg.Component) + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TLC5971), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=85), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) diff --git a/esphome/components/tlc5971/output/__init__.py b/esphome/components/tlc5971/output/__init__.py new file mode 100644 index 0000000000..9fe7b18294 --- /dev/null +++ b/esphome/components/tlc5971/output/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from .. import TLC5971, tlc5971_ns + +DEPENDENCIES = ["tlc5971"] + +TLC5971Channel = tlc5971_ns.class_( + "TLC5971Channel", output.FloatOutput, cg.Parented.template(TLC5971) +) + +CONF_TLC5971_ID = "tlc5971_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_TLC5971_ID): cv.use_id(TLC5971), + cv.Required(CONF_ID): cv.declare_id(TLC5971Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + await cg.register_parented(var, config[CONF_TLC5971_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/tlc5971/output/tlc5971_output.cpp b/esphome/components/tlc5971/output/tlc5971_output.cpp new file mode 100644 index 0000000000..b437889072 --- /dev/null +++ b/esphome/components/tlc5971/output/tlc5971_output.cpp @@ -0,0 +1,12 @@ +#include "tlc5971_output.h" + +namespace esphome { +namespace tlc5971 { + +void TLC5971Channel::write_state(float state) { + auto amount = static_cast(state * 0xffff); + this->parent_->set_channel_value(this->channel_, amount); +} + +} // namespace tlc5971 +} // namespace esphome diff --git a/esphome/components/tlc5971/output/tlc5971_output.h b/esphome/components/tlc5971/output/tlc5971_output.h new file mode 100644 index 0000000000..944ee19b2d --- /dev/null +++ b/esphome/components/tlc5971/output/tlc5971_output.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/helpers.h" + +#include "esphome/components/output/float_output.h" + +#include "../tlc5971.h" + +namespace esphome { +namespace tlc5971 { + +class TLC5971Channel : public output::FloatOutput, public Parented { + public: + void set_channel(uint8_t channel) { this->channel_ = channel; } + + protected: + void write_state(float state) override; + uint8_t channel_; +}; + +} // namespace tlc5971 +} // namespace esphome diff --git a/esphome/components/tlc5971/tlc5971.cpp b/esphome/components/tlc5971/tlc5971.cpp new file mode 100644 index 0000000000..ebcc3af361 --- /dev/null +++ b/esphome/components/tlc5971/tlc5971.cpp @@ -0,0 +1,101 @@ +#include "tlc5971.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tlc5971 { + +static const char *const TAG = "tlc5971"; + +void TLC5971::setup() { + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + + this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0); + + ESP_LOGCONFIG(TAG, "Done setting up TLC5971 output component."); +} +void TLC5971::dump_config() { + ESP_LOGCONFIG(TAG, "TLC5971:"); + 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() { + if (!this->update_) + return; + + uint32_t command; + + // Magic word for write + command = 0x25; + + command <<= 5; + // OUTTMG = 1, EXTGCK = 0, TMGRST = 1, DSPRPT = 1, BLANK = 0 -> 0x16 + command |= 0x16; + + command <<= 7; + command |= 0x7F; // default 100% brigthness + + command <<= 7; + command |= 0x7F; // default 100% brigthness + + command <<= 7; + command |= 0x7F; // default 100% brigthness + + for (uint8_t n = 0; n < num_chips_; n++) { + this->transfer_(command >> 24); + this->transfer_(command >> 16); + this->transfer_(command >> 8); + this->transfer_(command); + + // 12 channels per TLC59711 + for (int8_t c = 11; c >= 0; c--) { + // 16 bits per channel, send MSB first + this->transfer_(pwm_amounts_.at(n * 12 + c) >> 8); + this->transfer_(pwm_amounts_.at(n * 12 + c)); + } + } + + this->update_ = false; +} + +void TLC5971::transfer_(uint8_t send) { + uint8_t startbit = 0x80; + + bool towrite, lastmosi = !(send & startbit); + uint8_t bitdelay_us = (1000000 / 1000000) / 2; + + for (uint8_t b = startbit; b != 0; b = b >> 1) { + if (bitdelay_us) { + delayMicroseconds(bitdelay_us); + } + + towrite = send & b; + if ((lastmosi != towrite)) { + this->data_pin_->digital_write(towrite); + lastmosi = towrite; + } + + this->clock_pin_->digital_write(true); + + if (bitdelay_us) { + delayMicroseconds(bitdelay_us); + } + + this->clock_pin_->digital_write(false); + } +} +void TLC5971::set_channel_value(uint16_t channel, uint16_t value) { + if (channel >= this->num_chips_ * N_CHANNELS_PER_CHIP) + return; + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + } + this->pwm_amounts_[channel] = value; +} + +} // namespace tlc5971 +} // namespace esphome diff --git a/esphome/components/tlc5971/tlc5971.h b/esphome/components/tlc5971/tlc5971.h new file mode 100644 index 0000000000..6b0daf10d1 --- /dev/null +++ b/esphome/components/tlc5971/tlc5971.h @@ -0,0 +1,43 @@ +#pragma once +// TLC5971 12-Channel, 16-Bit PWM LED Driver +// https://www.ti.com/lit/ds/symlink/tlc5971.pdf + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" +#include + +namespace esphome { +namespace tlc5971 { + +class TLC5971 : public Component { + public: + const uint8_t N_CHANNELS_PER_CHIP = 12; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_num_chips(uint8_t num_chips) { num_chips_ = num_chips; } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + void set_channel_value(uint16_t channel, uint16_t value); + + protected: + void transfer_(uint8_t send); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t num_chips_; + + std::vector pwm_amounts_; + bool update_{true}; +}; +} // namespace tlc5971 +} // namespace esphome diff --git a/esphome/components/tm1621/display.py b/esphome/components/tm1621/display.py index edbc5f6928..a82b680f62 100644 --- a/esphome/components/tm1621/display.py +++ b/esphome/components/tm1621/display.py @@ -28,7 +28,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) cs = await cg.gpio_pin_expression(config[CONF_CS_PIN]) diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py index 609c62fd10..dcbc64332a 100644 --- a/esphome/components/tm1637/display.py +++ b/esphome/components/tm1637/display.py @@ -34,7 +34,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 5b8cbc6004..2f2d4b707a 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -225,7 +225,7 @@ void TM1637Display::display() { // Write display CTRL CMND + brightness this->start_(); - this->send_byte_(TM1637_CMD_CTRL + ((this->intensity_ & 0x7) | 0x08)); + this->send_byte_(TM1637_CMD_CTRL + ((this->intensity_ & 0x7) | (this->on_ ? 0x08 : 0x00))); this->stop_(); } bool TM1637Display::send_byte_(uint8_t b) { @@ -300,6 +300,7 @@ uint8_t TM1637Display::read_byte_() { uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { // ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); uint8_t pos = start_pos; + bool use_dot = false; for (; *str != '\0'; str++) { uint8_t data = TM1637_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') @@ -312,14 +313,14 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { // XABCDEFG, but TM1637 is // XGFEDCBA if (this->inverted_) { // XABCDEFG > XGCBAFED - data = ((data & 0x80) ? 0x80 : 0) | // no move X - ((data & 0x40) ? 0x8 : 0) | // A - ((data & 0x20) ? 0x10 : 0) | // B - ((data & 0x10) ? 0x20 : 0) | // C - ((data & 0x8) ? 0x1 : 0) | // D - ((data & 0x4) ? 0x2 : 0) | // E - ((data & 0x2) ? 0x4 : 0) | // F - ((data & 0x1) ? 0x40 : 0); // G + data = ((data & 0x80) || use_dot ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x8 : 0) | // A + ((data & 0x20) ? 0x10 : 0) | // B + ((data & 0x10) ? 0x20 : 0) | // C + ((data & 0x8) ? 0x1 : 0) | // D + ((data & 0x4) ? 0x2 : 0) | // E + ((data & 0x2) ? 0x4 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G } else { // XABCDEFG > XGFEDCBA data = ((data & 0x80) ? 0x80 : 0) | // no move X @@ -331,18 +332,18 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { ((data & 0x2) ? 0x20 : 0) | // F ((data & 0x1) ? 0x40 : 0); // G } - if (*str == '.') { - if (pos != start_pos) - pos--; - this->buffer_[pos] |= 0b10000000; + use_dot = *str == '.'; + if (use_dot) { + if ((!this->inverted_) && (pos != start_pos)) { + this->buffer_[pos - 1] |= 0b10000000; + } } else { if (pos >= 6) { ESP_LOGE(TAG, "String is too long for the display!"); break; } - this->buffer_[pos] = data; + this->buffer_[pos++] = data; } - pos++; } return pos - start_pos; } @@ -368,16 +369,14 @@ uint8_t TM1637Display::printf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t TM1637Display::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t TM1637Display::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t TM1637Display::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t TM1637Display::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } } // namespace tm1637 } // namespace esphome diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 2fb572bc55..d44680c623 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -3,13 +3,10 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/time.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -52,6 +49,7 @@ class TM1637Display : public PollingComponent { void set_intensity(uint8_t intensity) { this->intensity_ = intensity; } void set_inverted(bool inverted) { this->inverted_ = inverted; } void set_length(uint8_t length) { this->length_ = length; } + void set_on(bool on) { this->on_ = on; } void display(); @@ -61,12 +59,10 @@ class TM1637Display : public PollingComponent { void add_tm1637_key(TM1637Key *tm1637_key) { this->tm1637_keys_.push_back(tm1637_key); } #endif -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); protected: void bit_delay_(); @@ -81,6 +77,7 @@ class TM1637Display : public PollingComponent { uint8_t intensity_; uint8_t length_; bool inverted_; + bool on_{true}; optional writer_{}; uint8_t buffer_[6] = {0}; #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/tm1638/binary_sensor/__init__.py b/esphome/components/tm1638/binary_sensor/__init__.py index 6623228555..de6ea35e54 100644 --- a/esphome/components/tm1638/binary_sensor/__init__.py +++ b/esphome/components/tm1638/binary_sensor/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor +import esphome.config_validation as cv from esphome.const import CONF_KEY -from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns TM1638Key = tm1638_ns.class_("TM1638Key", binary_sensor.BinarySensor) diff --git a/esphome/components/tm1638/display.py b/esphome/components/tm1638/display.py index 6339983674..14b70be94d 100644 --- a/esphome/components/tm1638/display.py +++ b/esphome/components/tm1638/display.py @@ -1,13 +1,13 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import display +import esphome.config_validation as cv from esphome.const import ( + CONF_CLK_PIN, + CONF_DIO_PIN, CONF_ID, CONF_INTENSITY, CONF_LAMBDA, - CONF_CLK_PIN, - CONF_DIO_PIN, CONF_STB_PIN, ) @@ -33,7 +33,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) @@ -52,4 +51,4 @@ async def to_code(config): config[CONF_LAMBDA], [(TM1638ComponentRef, "it")], return_type=cg.void ) - cg.add(var.set_writer(lambda_)) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1638/output/__init__.py b/esphome/components/tm1638/output/__init__.py index 2d982e409d..b16b08d504 100644 --- a/esphome/components/tm1638/output/__init__.py +++ b/esphome/components/tm1638/output/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import output +import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_LED -from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns TM1638OutputLed = tm1638_ns.class_("TM1638OutputLed", output.BinaryOutput, cg.Component) diff --git a/esphome/components/tm1638/switch/__init__.py b/esphome/components/tm1638/switch/__init__.py index ed6aa91d03..8832cf8b92 100644 --- a/esphome/components/tm1638/switch/__init__.py +++ b/esphome/components/tm1638/switch/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import switch +import esphome.config_validation as cv from esphome.const import CONF_LED -from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns TM1638SwitchLed = tm1638_ns.class_("TM1638SwitchLed", switch.Switch, cg.Component) diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index 24cb4122bf..a15b006046 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -211,16 +211,14 @@ uint8_t TM1638Component::printf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t TM1638Component::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t TM1638Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t TM1638Component::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t TM1638Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } //////////////// SPI //////////////// diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h index 2e1ac6fad3..add72cfdf3 100644 --- a/esphome/components/tm1638/tm1638.h +++ b/esphome/components/tm1638/tm1638.h @@ -1,16 +1,13 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/automation.h" #include "esphome/core/hal.h" +#include "esphome/core/time.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace tm1638 { @@ -52,12 +49,10 @@ class TM1638Component : public PollingComponent { void loop() override; uint8_t get_keys(); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); void set_led(int led_pos, bool led_on_off); diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index a6b2189eb6..4ef8842571 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( CODEOWNERS = ["@freekode"] tm1651_ns = cg.esphome_ns.namespace("tm1651") +TM1651Brightness = tm1651_ns.enum("TM1651Brightness") TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component) SetLevelPercentAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) @@ -24,9 +25,9 @@ TurnOffAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) CONF_LEVEL_PERCENT = "level_percent" TM1651_BRIGHTNESS_OPTIONS = { - 1: TM1651Display.TM1651_BRIGHTNESS_LOW, - 2: TM1651Display.TM1651_BRIGHTNESS_MEDIUM, - 3: TM1651Display.TM1651_BRIGHTNESS_HIGH, + 1: TM1651Brightness.TM1651_BRIGHTNESS_LOW, + 2: TM1651Brightness.TM1651_BRIGHTNESS_MEDIUM, + 3: TM1651Brightness.TM1651_BRIGHTNESS_HIGH, } CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp index c6bb1bc025..89807f5565 100644 --- a/esphome/components/tm1651/tm1651.cpp +++ b/esphome/components/tm1651/tm1651.cpp @@ -12,9 +12,9 @@ static const char *const TAG = "tm1651.display"; static const uint8_t MAX_INPUT_LEVEL_PERCENT = 100; static const uint8_t TM1651_MAX_LEVEL = 7; -static const uint8_t TM1651_BRIGHTNESS_LOW = 0; -static const uint8_t TM1651_BRIGHTNESS_MEDIUM = 2; -static const uint8_t TM1651_BRIGHTNESS_HIGH = 7; +static const uint8_t TM1651_BRIGHTNESS_LOW_HW = 0; +static const uint8_t TM1651_BRIGHTNESS_MEDIUM_HW = 2; +static const uint8_t TM1651_BRIGHTNESS_HIGH_HW = 7; void TM1651Display::setup() { ESP_LOGCONFIG(TAG, "Setting up TM1651..."); @@ -78,14 +78,14 @@ uint8_t TM1651Display::calculate_level_(uint8_t new_level) { uint8_t TM1651Display::calculate_brightness_(uint8_t new_brightness) { if (new_brightness <= 1) { - return TM1651_BRIGHTNESS_LOW; + return TM1651_BRIGHTNESS_LOW_HW; } else if (new_brightness == 2) { - return TM1651_BRIGHTNESS_MEDIUM; + return TM1651_BRIGHTNESS_MEDIUM_HW; } else if (new_brightness >= 3) { - return TM1651_BRIGHTNESS_HIGH; + return TM1651_BRIGHTNESS_HIGH_HW; } - return TM1651_BRIGHTNESS_LOW; + return TM1651_BRIGHTNESS_LOW_HW; } } // namespace tm1651 diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index eb65ed186d..fe7b7d9c6f 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -13,6 +13,12 @@ namespace esphome { namespace tm1651 { +enum TM1651Brightness : uint8_t { + TM1651_BRIGHTNESS_LOW = 1, + TM1651_BRIGHTNESS_MEDIUM = 2, + TM1651_BRIGHTNESS_HIGH = 3, +}; + class TM1651Display : public Component { public: void set_clk_pin(InternalGPIOPin *pin) { clk_pin_ = pin; } @@ -24,6 +30,7 @@ class TM1651Display : public Component { void set_level_percent(uint8_t new_level); void set_level(uint8_t new_level); void set_brightness(uint8_t new_brightness); + void set_brightness(TM1651Brightness new_brightness) { this->set_brightness(static_cast(new_brightness)); } void turn_on(); void turn_off(); diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py index 57d0afd5a1..2cb1a6d1f5 100644 --- a/esphome/components/tmp102/sensor.py +++ b/esphome/components/tmp102/sensor.py @@ -7,6 +7,7 @@ reading temperatures to a resolution of 0.0625°C. https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf """ + import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index f6bb9a05c0..f35fbf5d4b 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -28,24 +28,24 @@ void TMP102Component::dump_config() { } void TMP102Component::update() { - uint16_t raw_temperature; if (this->write(&TMP102_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; } - delay(50); // NOLINT - if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_temperature = i2c::i2ctohs(raw_temperature); + this->set_timeout("read_temp", 50, [this]() { + int16_t raw_temperature; + if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_temperature = i2c::i2ctohs(raw_temperature); + raw_temperature = raw_temperature >> 4; + float temperature = raw_temperature * TMP102_CONVERSION_FACTOR; + ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature); - raw_temperature = raw_temperature >> 4; - float temperature = raw_temperature * TMP102_CONVERSION_FACTOR; - ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature); - - this->publish_state(temperature); - this->status_clear_warning(); + this->publish_state(temperature); + this->status_clear_warning(); + }); } float TMP102Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py index fb97258bc1..82d099cf12 100644 --- a/esphome/components/tmp117/sensor.py +++ b/esphome/components/tmp117/sensor.py @@ -30,37 +30,37 @@ CONFIG_SCHEMA = cv.All( def determine_config_register(polling_period): - if polling_period >= 16.0: + if polling_period >= 16000: # 64 averaged conversions, max conversion time # 0000 00 111 11 00000 # 0000 0011 1110 0000 return 0x03E0 - if polling_period >= 8.0: + if polling_period >= 8000: # 64 averaged conversions, high conversion time # 0000 00 110 11 00000 # 0000 0011 0110 0000 return 0x0360 - if polling_period >= 4.0: + if polling_period >= 4000: # 64 averaged conversions, mid conversion time # 0000 00 101 11 00000 # 0000 0010 1110 0000 return 0x02E0 - if polling_period >= 1.0: + if polling_period >= 1000: # 64 averaged conversions, min conversion time # 0000 00 000 11 00000 # 0000 0000 0110 0000 return 0x0060 - if polling_period >= 0.5: + if polling_period >= 500: # 32 averaged conversions, min conversion time # 0000 00 000 10 00000 # 0000 0000 0100 0000 return 0x0040 - if polling_period >= 0.25: + if polling_period >= 250: # 8 averaged conversions, mid conversion time # 0000 00 010 01 00000 # 0000 0001 0010 0000 return 0x0120 - if polling_period >= 0.125: + if polling_period >= 125: # 8 averaged conversions, min conversion time # 0000 00 000 01 00000 # 0000 0000 0010 0000 @@ -76,5 +76,5 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - update_period = config[CONF_UPDATE_INTERVAL].total_seconds + update_period = config[CONF_UPDATE_INTERVAL].total_milliseconds cg.add(var.set_config(determine_config_register(update_period))) diff --git a/esphome/components/tof10120/tof10120_sensor.cpp b/esphome/components/tof10120/tof10120_sensor.cpp index 5cd086938e..32cd604be9 100644 --- a/esphome/components/tof10120/tof10120_sensor.cpp +++ b/esphome/components/tof10120/tof10120_sensor.cpp @@ -1,6 +1,7 @@ #include "tof10120_sensor.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include // Very basic support for TOF10120 distance sensor @@ -44,7 +45,7 @@ void TOF10120Sensor::update() { } uint32_t distance_mm = (data[0] << 8) | data[1]; - ESP_LOGI(TAG, "Data read: %dmm", distance_mm); + ESP_LOGI(TAG, "Data read: %" PRIu32 "mm", distance_mm); if (distance_mm == TOF10120_OUT_OF_RANGE_VALUE) { ESP_LOGW(TAG, "Distance measurement out of range"); diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index a4bdc8cafd..01a271a34e 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -1,46 +1,156 @@ -import esphome.config_validation as cv -import esphome.codegen as cg - -from esphome.components import display from esphome import automation -from esphome.const import CONF_ON_TOUCH +import esphome.codegen as cg +from esphome.components import display +import esphome.config_validation as cv +from esphome.const import ( + CONF_CALIBRATION, + CONF_DISPLAY, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_ON_RELEASE, + CONF_ON_TOUCH, + CONF_ON_UPDATE, + CONF_SWAP_XY, + CONF_TRANSFORM, +) from esphome.core import coroutine_with_priority -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@nielsnl68"] DEPENDENCIES = ["display"] IS_PLATFORM_COMPONENT = True touchscreen_ns = cg.esphome_ns.namespace("touchscreen") -Touchscreen = touchscreen_ns.class_("Touchscreen") +Touchscreen = touchscreen_ns.class_("Touchscreen", cg.PollingComponent) TouchRotation = touchscreen_ns.enum("TouchRotation") TouchPoint = touchscreen_ns.struct("TouchPoint") +TouchPoints_t = cg.std_vector.template(TouchPoint) +TouchPoints_t_const_ref = TouchPoints_t.operator("ref").operator("const") TouchListener = touchscreen_ns.class_("TouchListener") -CONF_DISPLAY = "display" CONF_TOUCHSCREEN_ID = "touchscreen_id" +CONF_REPORT_INTERVAL = "report_interval" # not used yet: +CONF_TOUCH_TIMEOUT = "touch_timeout" -TOUCHSCREEN_SCHEMA = cv.Schema( - { - cv.GenerateID(CONF_DISPLAY): cv.use_id(display.DisplayBuffer), - cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), - } +CONF_X_MIN = "x_min" +CONF_X_MAX = "x_max" +CONF_Y_MIN = "y_min" +CONF_Y_MAX = "y_max" + + +def validate_calibration(calibration_config): + x_min = calibration_config[CONF_X_MIN] + x_max = calibration_config[CONF_X_MAX] + y_min = calibration_config[CONF_Y_MIN] + y_max = calibration_config[CONF_Y_MAX] + if x_max < x_min: + raise cv.Invalid( + "x_min must be smaller than x_max. To mirror the direction use the 'transform' options" + ) + if y_max < y_min: + raise cv.Invalid( + "y_min must be smaller than y_max. To mirror the direction use the 'transform' options" + ) + x_delta = x_max - x_min + y_delta = y_max - y_min + if x_delta < 10 or y_delta < 10: + raise cv.Invalid("Calibration value range must be greater than 10") + return calibration_config + + +CALIBRATION_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095), + cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095), + cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095), + cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095), + } + ), + validate_calibration, ) +def touchscreen_schema(default_touch_timeout=cv.UNDEFINED, calibration_required=False): + calibration = ( + cv.Required(CONF_CALIBRATION) + if calibration_required + else cv.Optional(CONF_CALIBRATION) + ) + return cv.Schema( + { + cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), + cv.Optional(CONF_TOUCH_TIMEOUT, default=default_touch_timeout): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(milliseconds=65535)), + ), + calibration: CALIBRATION_SCHEMA, + cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), + } + ).extend(cv.polling_component_schema("50ms")) + + +TOUCHSCREEN_SCHEMA = touchscreen_schema(cv.UNDEFINED) + + async def register_touchscreen(var, config): + await cg.register_component(var, config) + disp = await cg.get_variable(config[CONF_DISPLAY]) cg.add(var.set_display(disp)) + if CONF_TOUCH_TIMEOUT in config: + cg.add(var.set_touch_timeout(config[CONF_TOUCH_TIMEOUT])) + + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + + if CONF_CALIBRATION in config: + calibration_config = config[CONF_CALIBRATION] + cg.add( + var.set_calibration( + calibration_config[CONF_X_MIN], + calibration_config[CONF_X_MAX], + calibration_config[CONF_Y_MIN], + calibration_config[CONF_Y_MAX], + ) + ) + if CONF_ON_TOUCH in config: await automation.build_automation( var.get_touch_trigger(), - [(TouchPoint, "touch")], + [(TouchPoint, "touch"), (TouchPoints_t_const_ref, "touches")], config[CONF_ON_TOUCH], ) + if CONF_ON_UPDATE in config: + await automation.build_automation( + var.get_update_trigger(), + [(TouchPoints_t_const_ref, "touches")], + config[CONF_ON_UPDATE], + ) + + if CONF_ON_RELEASE in config: + await automation.build_automation( + var.get_release_trigger(), + [], + config[CONF_ON_RELEASE], + ) + @coroutine_with_priority(100.0) async def to_code(config): diff --git a/esphome/components/touchscreen/binary_sensor/__init__.py b/esphome/components/touchscreen/binary_sensor/__init__.py index 800bc4c2a9..45fefbf814 100644 --- a/esphome/components/touchscreen/binary_sensor/__init__.py +++ b/esphome/components/touchscreen/binary_sensor/__init__.py @@ -1,10 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv - from esphome.components import binary_sensor, display -from esphome.const import CONF_PAGE_ID +import esphome.config_validation as cv +from esphome.const import CONF_PAGE_ID, CONF_PAGES -from .. import touchscreen_ns, CONF_TOUCHSCREEN_ID, Touchscreen, TouchListener +from .. import CONF_TOUCHSCREEN_ID, TouchListener, Touchscreen, touchscreen_ns DEPENDENCIES = ["touchscreen"] @@ -22,7 +21,7 @@ CONF_Y_MIN = "y_min" CONF_Y_MAX = "y_max" -def validate_coords(config): +def _validate_coords(config): if ( config[CONF_X_MAX] < config[CONF_X_MIN] or config[CONF_Y_MAX] < config[CONF_Y_MIN] @@ -33,6 +32,15 @@ def validate_coords(config): return config +def _set_pages(config: dict) -> dict: + if CONF_PAGES in config or CONF_PAGE_ID not in config: + return config + + config = config.copy() + config[CONF_PAGES] = [config.pop(CONF_PAGE_ID)] + return config + + CONFIG_SCHEMA = cv.All( binary_sensor.binary_sensor_schema(TouchscreenBinarySensor) .extend( @@ -42,11 +50,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_X_MAX): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=2000), - cv.Optional(CONF_PAGE_ID): cv.use_id(display.DisplayPage), + cv.Exclusive(CONF_PAGE_ID, group_of_exclusion=CONF_PAGES): cv.use_id( + display.DisplayPage + ), + cv.Exclusive(CONF_PAGES, group_of_exclusion=CONF_PAGES): cv.ensure_list( + cv.use_id(display.DisplayPage) + ), } ) .extend(cv.COMPONENT_SCHEMA), - validate_coords, + _validate_coords, + _set_pages, ) @@ -64,6 +78,6 @@ async def to_code(config): ) ) - if CONF_PAGE_ID in config: - page = await cg.get_variable(config[CONF_PAGE_ID]) - cg.add(var.set_page(page)) + for page_id in config.get(CONF_PAGES, []): + page = await cg.get_variable(page_id) + cg.add(var.add_page(page)) diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 583392cce3..6cd12d4d0d 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -3,17 +3,22 @@ namespace esphome { namespace touchscreen { +void TouchscreenBinarySensor::setup() { + this->parent_->register_listener(this); + this->publish_initial_state(false); +} + void TouchscreenBinarySensor::touch(TouchPoint tp) { bool touched = (tp.x >= this->x_min_ && tp.x <= this->x_max_ && tp.y >= this->y_min_ && tp.y <= this->y_max_); - if (this->page_ != nullptr) { - touched &= this->page_ == this->parent_->get_display()->get_active_page(); + if (!this->pages_.empty()) { + auto *current_page = this->parent_->get_display()->get_active_page(); + touched &= std::find(this->pages_.begin(), this->pages_.end(), current_page) != this->pages_.end(); } - if (touched) { this->publish_state(true); } else { - release(); + this->release(); } } diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index 701468aa1e..862f41064c 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -6,6 +6,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace touchscreen { @@ -14,7 +16,7 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, public TouchListener, public Parented { public: - void setup() override { this->parent_->register_listener(this); } + void setup() override; /// Set the touch screen area where the button will detect the touch. void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { @@ -30,14 +32,14 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, int16_t get_width() { return this->x_max_ - this->x_min_; } int16_t get_height() { return this->y_max_ - this->y_min_; } - void set_page(display::DisplayPage *page) { this->page_ = page; } + void add_page(display::DisplayPage *page) { this->pages_.push_back(page); } void touch(TouchPoint tp) override; void release() override; protected: int16_t x_min_, x_max_, y_min_, y_max_; - display::DisplayPage *page_{nullptr}; + std::vector pages_{}; }; } // namespace touchscreen diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 9b337fc02c..dfe723aedf 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -7,11 +7,158 @@ namespace touchscreen { static const char *const TAG = "touchscreen"; -void Touchscreen::send_touch_(TouchPoint tp) { - ESP_LOGV(TAG, "Touch (x=%d, y=%d)", tp.x, tp.y); - this->touch_trigger_.trigger(tp); - for (auto *listener : this->touch_listeners_) - listener->touch(tp); +void TouchscreenInterrupt::gpio_intr(TouchscreenInterrupt *store) { store->touched = true; } + +void Touchscreen::attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type) { + irq_pin->attach_interrupt(TouchscreenInterrupt::gpio_intr, &this->store_, type); + this->store_.init = true; + this->store_.touched = false; + ESP_LOGD(TAG, "Attach Touch Interupt"); +} + +void Touchscreen::call_setup() { + if (this->display_ != nullptr) { + this->display_width_ = this->display_->get_width(); + this->display_height_ = this->display_->get_height(); + } + PollingComponent::call_setup(); +} + +void Touchscreen::update() { + if (!this->store_.init) { + this->store_.touched = true; + } else { + // no need to poll if we have interrupts. + ESP_LOGW(TAG, "Touch Polling Stopped. You can safely remove the 'update_interval:' variable from the YAML file."); + this->stop_poller(); + } +} + +void Touchscreen::loop() { + if (this->store_.touched) { + ESP_LOGVV(TAG, "<< Do Touch loop >>"); + this->first_touch_ = this->touches_.empty(); + this->need_update_ = false; + this->is_touched_ = false; + this->skip_update_ = false; + for (auto &tp : this->touches_) { + if (tp.second.state == STATE_PRESSED || tp.second.state == STATE_UPDATED) { + tp.second.state |= STATE_RELEASING; + } else { + tp.second.state = STATE_RELEASED; + } + tp.second.x_prev = tp.second.x; + tp.second.y_prev = tp.second.y; + } + this->update_touches(); + if (this->skip_update_) { + for (auto &tp : this->touches_) { + tp.second.state &= ~STATE_RELEASING; + } + } else { + this->store_.touched = false; + this->defer([this]() { this->send_touches_(); }); + if (this->touch_timeout_ > 0) { + // Simulate a touch after touch_timeout_> ms. This will reset any existing timeout operation. + // This is to detect touch release. + if (this->is_touched_) { + this->set_timeout(TAG, this->touch_timeout_, [this]() { this->store_.touched = true; }); + } else { + this->cancel_timeout(TAG); + } + } + } + } +} + +void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { + TouchPoint tp; + uint16_t x, y; + if (this->touches_.count(id) == 0) { + tp.state = STATE_PRESSED; + tp.id = id; + } else { + tp = this->touches_[id]; + tp.state = STATE_UPDATED; + tp.y_prev = tp.y; + tp.x_prev = tp.x; + } + tp.x_raw = x_raw; + tp.y_raw = y_raw; + tp.z_raw = z_raw; + if (this->x_raw_max_ != this->x_raw_min_ and this->y_raw_max_ != this->y_raw_min_) { + x = this->normalize_(x_raw, this->x_raw_min_, this->x_raw_max_, this->invert_x_); + y = this->normalize_(y_raw, this->y_raw_min_, this->y_raw_max_, this->invert_y_); + + if (this->swap_x_y_) { + std::swap(x, y); + } + + tp.x = (uint16_t) ((int) x * this->display_width_ / 0x1000); + tp.y = (uint16_t) ((int) y * this->display_height_ / 0x1000); + } else { + tp.state |= STATE_CALIBRATE; + } + if (tp.state == STATE_PRESSED) { + tp.x_org = tp.x; + tp.y_org = tp.y; + } + + this->touches_[id] = tp; + + this->is_touched_ = true; + if ((tp.x != tp.x_prev) || (tp.y != tp.y_prev)) { + this->need_update_ = true; + } +} + +void Touchscreen::send_touches_() { + TouchPoints_t touches; + ESP_LOGV(TAG, "Touch status: is_touched=%d, was_touched=%d", this->is_touched_, this->was_touched_); + for (auto tp : this->touches_) { + ESP_LOGV(TAG, "Touch status: %d/%d: raw:(%4d,%4d,%4d) calc:(%3d,%4d)", tp.second.id, tp.second.state, + tp.second.x_raw, tp.second.y_raw, tp.second.z_raw, tp.second.x, tp.second.y); + touches.push_back(tp.second); + } + if (this->need_update_ || (!this->is_touched_ && this->was_touched_)) { + this->update_trigger_.trigger(touches); + for (auto *listener : this->touch_listeners_) { + listener->update(touches); + } + } + if (!this->is_touched_) { + if (this->was_touched_) { + this->release_trigger_.trigger(); + for (auto *listener : this->touch_listeners_) + listener->release(); + this->touches_.clear(); + } + } else { + if (this->first_touch_) { + TouchPoint tp = this->touches_.begin()->second; + this->touch_trigger_.trigger(tp, touches); + for (auto *listener : this->touch_listeners_) { + listener->touch(tp); + } + } + } + this->was_touched_ = this->is_touched_; +} + +int16_t Touchscreen::normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted) { + int16_t ret; + + if (val <= min_val) { + ret = 0; + } else if (val >= max_val) { + ret = 0xfff; + } else { + ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); + } + + ret = (inverted) ? 0xfff - ret : ret; + + return ret; } } // namespace touchscreen diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 0597759894..8016323d49 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -1,58 +1,118 @@ #pragma once -#include "esphome/components/display/display_buffer.h" +#include "esphome/core/defines.h" +#include "esphome/components/display/display.h" + #include "esphome/core/automation.h" #include "esphome/core/hal.h" #include +#include namespace esphome { namespace touchscreen { +static const uint8_t STATE_RELEASED = 0x00; +static const uint8_t STATE_PRESSED = 0x01; +static const uint8_t STATE_UPDATED = 0x02; +static const uint8_t STATE_RELEASING = 0x04; +static const uint8_t STATE_CALIBRATE = 0x07; + struct TouchPoint { - uint16_t x; - uint16_t y; uint8_t id; - uint8_t state; + int16_t x_raw{0}, y_raw{0}, z_raw{0}; + uint16_t x_prev{0}, y_prev{0}; + uint16_t x_org{0}, y_org{0}; + uint16_t x{0}, y{0}; + int8_t state{0}; +}; + +using TouchPoints_t = std::vector; + +struct TouchscreenInterrupt { + volatile bool touched{true}; + bool init{false}; + static void gpio_intr(TouchscreenInterrupt *store); }; class TouchListener { public: - virtual void touch(TouchPoint tp) = 0; + virtual void touch(TouchPoint tp) {} + virtual void update(const TouchPoints_t &tpoints) {} virtual void release() {} }; -enum TouchRotation { - ROTATE_0_DEGREES = 0, - ROTATE_90_DEGREES = 90, - ROTATE_180_DEGREES = 180, - ROTATE_270_DEGREES = 270, -}; - -class Touchscreen { +class Touchscreen : public PollingComponent { public: - void set_display(display::DisplayBuffer *display) { - this->display_ = display; - this->display_width_ = display->get_width_internal(); - this->display_height_ = display->get_height_internal(); - this->rotation_ = static_cast(display->get_rotation()); - } - display::DisplayBuffer *get_display() const { return this->display_; } + void set_display(display::Display *display) { this->display_ = display; } + display::Display *get_display() const { return this->display_; } - Trigger *get_touch_trigger() { return &this->touch_trigger_; } + void set_touch_timeout(uint16_t val) { this->touch_timeout_ = val; } + void set_mirror_x(bool invert_x) { this->invert_x_ = invert_x; } + void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; } + void set_swap_xy(bool swap) { this->swap_x_y_ = swap; } + + void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { + this->x_raw_min_ = x_min; + this->x_raw_max_ = x_max; + this->y_raw_min_ = y_min; + this->y_raw_max_ = y_max; + } + + Trigger *get_touch_trigger() { return &this->touch_trigger_; } + Trigger *get_update_trigger() { return &this->update_trigger_; } + Trigger<> *get_release_trigger() { return &this->release_trigger_; } void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); } + optional get_touch() { return this->touches_.begin()->second; } + + TouchPoints_t get_touches() { + TouchPoints_t touches; + for (auto i : this->touches_) { + touches.push_back(i.second); + } + return touches; + } + + void update() override; + void loop() override; + void call_setup() override; + protected: /// Call this function to send touch points to the `on_touch` listener and the binary_sensors. - void send_touch_(TouchPoint tp); - uint16_t display_width_; - uint16_t display_height_; - display::DisplayBuffer *display_; - TouchRotation rotation_; - Trigger touch_trigger_; + void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type); + + void add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); + + virtual void update_touches() = 0; + + void send_touches_(); + + int16_t normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted = false); + + display::Display *display_{nullptr}; + + int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0}; + int16_t display_width_{0}, display_height_{0}; + + uint16_t touch_timeout_{0}; + bool invert_x_{false}, invert_y_{false}, swap_x_y_{false}; + + Trigger touch_trigger_; + Trigger update_trigger_; + Trigger<> release_trigger_; std::vector touch_listeners_; + + std::map touches_; + TouchscreenInterrupt store_; + + bool first_touch_{true}; + bool need_update_{false}; + bool is_touched_{false}; + bool was_touched_{false}; + bool skip_update_{false}; }; } // namespace touchscreen diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 5086a38408..977048364c 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -142,8 +142,8 @@ void TSL2591Component::process_update_() { uint16_t full = this->get_illuminance(TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, combined); float lux = this->get_calculated_lux(full, infrared); uint16_t actual_gain = this->get_actual_gain(); - ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f. Actual gain: %d.", combined, - full, infrared, visible, lux, actual_gain); + ESP_LOGD(TAG, "Got illuminance: combined 0x%" PRIX32 ", full %d, IR %d, vis %d. Calc lux: %f. Actual gain: %d.", + combined, full, infrared, visible, lux, actual_gain); if (this->full_spectrum_sensor_ != nullptr) { this->full_spectrum_sensor_->publish_state(full); } diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index d7c5230276..fa302b14b0 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include + namespace esphome { namespace tsl2591 { diff --git a/esphome/components/tt21100/__init__.py b/esphome/components/tt21100/__init__.py new file mode 100644 index 0000000000..a309d34beb --- /dev/null +++ b/esphome/components/tt21100/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@kroimon"] + +tt21100_ns = cg.esphome_ns.namespace("tt21100") diff --git a/esphome/components/tt21100/binary_sensor/__init__.py b/esphome/components/tt21100/binary_sensor/__init__.py new file mode 100644 index 0000000000..d5423a01b2 --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_INDEX + +from .. import tt21100_ns +from ..touchscreen import TT21100Touchscreen, TT21100ButtonListener + +CONF_TT21100_ID = "tt21100_id" + +TT21100Button = tt21100_ns.class_( + "TT21100Button", + binary_sensor.BinarySensor, + cg.Component, + TT21100ButtonListener, + cg.Parented.template(TT21100Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TT21100Button).extend( + { + cv.GenerateID(CONF_TT21100_ID): cv.use_id(TT21100Touchscreen), + cv.Required(CONF_INDEX): cv.int_range(min=0, max=3), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_TT21100_ID]) + cg.add(var.set_index(config[CONF_INDEX])) diff --git a/esphome/components/tt21100/binary_sensor/tt21100_button.cpp b/esphome/components/tt21100/binary_sensor/tt21100_button.cpp new file mode 100644 index 0000000000..2d5ac22a83 --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/tt21100_button.cpp @@ -0,0 +1,27 @@ +#include "tt21100_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tt21100 { + +static const char *const TAG = "tt21100.binary_sensor"; + +void TT21100Button::setup() { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); +} + +void TT21100Button::dump_config() { + LOG_BINARY_SENSOR("", "TT21100 Button", this); + ESP_LOGCONFIG(TAG, " Index: %u", this->index_); +} + +void TT21100Button::update_button(uint8_t index, uint16_t state) { + if (index != this->index_) + return; + + this->publish_state(state > 0); +} + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/binary_sensor/tt21100_button.h b/esphome/components/tt21100/binary_sensor/tt21100_button.h new file mode 100644 index 0000000000..90b55bb75a --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/tt21100_button.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/tt21100/touchscreen/tt21100.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tt21100 { + +class TT21100Button : public binary_sensor::BinarySensor, + public Component, + public TT21100ButtonListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_index(uint8_t index) { this->index_ = index; } + + void update_button(uint8_t index, uint16_t state) override; + + protected: + uint8_t index_; +}; + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/touchscreen/__init__.py b/esphome/components/tt21100/touchscreen/__init__.py new file mode 100644 index 0000000000..510ca2df3a --- /dev/null +++ b/esphome/components/tt21100/touchscreen/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +from .. import tt21100_ns + +DEPENDENCIES = ["i2c"] + +TT21100Touchscreen = tt21100_ns.class_( + "TT21100Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) +TT21100ButtonListener = tt21100_ns.class_("TT21100ButtonListener") + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TT21100Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ).extend(i2c.i2c_device_schema(0x24)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_INTERRUPT_PIN in config: + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) + + if CONF_RESET_PIN in config: + rts_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(rts_pin)) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp new file mode 100644 index 0000000000..2bea72a59e --- /dev/null +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -0,0 +1,145 @@ +#include "tt21100.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tt21100 { + +static const char *const TAG = "tt21100"; + +static const uint8_t MAX_BUTTONS = 4; +static const uint8_t MAX_TOUCH_POINTS = 5; +static const uint8_t MAX_DATA_LEN = (7 + MAX_TOUCH_POINTS * 10); // 7 Header + (Points * 10 data bytes) + +struct TT21100ButtonReport { + uint16_t length; // Always 14 (0x000E) + uint8_t report_id; // Always 0x03 + uint16_t timestamp; // Number in units of 100 us + uint8_t btn_value; // Only use bit 0..3 + uint16_t btn_signal[MAX_BUTTONS]; +} __attribute__((packed)); + +struct TT21100TouchRecord { + uint8_t : 5; + uint8_t touch_type : 3; + uint8_t tip : 1; + uint8_t event_id : 2; + uint8_t touch_id : 5; + uint16_t x; + uint16_t y; + uint8_t pressure; + uint16_t major_axis_length; + uint8_t orientation; +} __attribute__((packed)); + +struct TT21100TouchReport { + uint16_t length; + uint8_t report_id; + uint16_t timestamp; + uint8_t : 2; + uint8_t large_object : 1; + uint8_t record_num : 5; + uint8_t report_counter : 2; + uint8_t : 3; + uint8_t noise_effect : 3; + TT21100TouchRecord touch_record[MAX_TOUCH_POINTS]; +} __attribute__((packed)); + +float TT21100Touchscreen::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } + +void TT21100Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up TT21100 Touchscreen..."); + + // Register interrupt pin + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + + // Perform reset if necessary + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_(); + } + + // Update display dimensions if they were updated during display setup + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->x_raw_max_ = this->display_->get_native_height(); + } + } + + // Trigger initial read to activate the interrupt + this->store_.touched = true; +} + +void TT21100Touchscreen::update_touches() { + // Read report length + uint16_t data_len; + this->read((uint8_t *) &data_len, sizeof(data_len)); + + // Read report data + uint8_t data[MAX_DATA_LEN]; + if (data_len > 0 && data_len < sizeof(data)) { + this->read(data, data_len); + + if (data_len == 14) { + // Button event + auto *report = (TT21100ButtonReport *) data; + + ESP_LOGV(TAG, "Button report: Len=%d, ID=%d, Time=%5u, Value=[%u], Signal=[%04X][%04X][%04X][%04X]", + report->length, report->report_id, report->timestamp, report->btn_value, report->btn_signal[0], + report->btn_signal[1], report->btn_signal[2], report->btn_signal[3]); + + for (uint8_t i = 0; i < 4; i++) { + for (auto *listener : this->button_listeners_) + listener->update_button(i, report->btn_signal[i]); + } + + } else if (data_len >= 7) { + // Touch point event + auto *report = (TT21100TouchReport *) data; + + ESP_LOGV(TAG, + "Touch report: Len=%d, ID=%d, Time=%5u, LargeObject=%u, RecordNum=%u, RecordCounter=%u, NoiseEffect=%u", + report->length, report->report_id, report->timestamp, report->large_object, report->record_num, + report->report_counter, report->noise_effect); + + uint8_t touch_count = (data_len - (sizeof(*report) - sizeof(report->touch_record))) / sizeof(TT21100TouchRecord); + + for (int i = 0; i < touch_count; i++) { + auto *touch = &report->touch_record[i]; + + ESP_LOGV(TAG, + "Touch %d: Type=%u, Tip=%u, EventId=%u, TouchId=%u, X=%u, Y=%u, Pressure=%u, MajorAxisLen=%u, " + "Orientation=%u", + i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, + touch->pressure, touch->major_axis_length, touch->orientation); + + this->add_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); + } + } + } +} + +void TT21100Touchscreen::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void TT21100Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "TT21100 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/touchscreen/tt21100.h b/esphome/components/tt21100/touchscreen/tt21100.h new file mode 100644 index 0000000000..5d1b2efe3c --- /dev/null +++ b/esphome/components/tt21100/touchscreen/tt21100.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +#include + +namespace esphome { +namespace tt21100 { + +using namespace touchscreen; + +class TT21100ButtonListener { + public: + virtual void update_button(uint8_t index, uint16_t state) = 0; +}; + +class TT21100Touchscreen : public Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + void register_button_listener(TT21100ButtonListener *listener) { this->button_listeners_.push_back(listener); } + + protected: + void reset_(); + + void update_touches() override; + + InternalGPIOPin *interrupt_pin_; + GPIOPin *reset_pin_{nullptr}; + + std::vector button_listeners_; +}; + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 2eaaa2a625..0738f9b6a4 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -15,6 +15,7 @@ CONF_DATAPOINT_TYPE = "datapoint_type" CONF_STATUS_PIN = "status_pin" tuya_ns = cg.esphome_ns.namespace("tuya") +TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True) Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) DPTYPE_ANY = "any" diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 199c2eabeb..363e7c764b 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -7,15 +7,22 @@ from esphome.const import ( CONF_SWITCH_DATAPOINT, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, + CONF_PRESET, + CONF_SWING_MODE, + CONF_FAN_MODE, + CONF_TEMPERATURE, ) from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ["tuya"] CODEOWNERS = ["@jesserockz"] -CONF_ACTIVE_STATE_DATAPOINT = "active_state_datapoint" -CONF_ACTIVE_STATE_HEATING_VALUE = "active_state_heating_value" -CONF_ACTIVE_STATE_COOLING_VALUE = "active_state_cooling_value" +CONF_ACTIVE_STATE = "active_state" +CONF_DATAPOINT = "datapoint" +CONF_HEATING_VALUE = "heating_value" +CONF_COOLING_VALUE = "cooling_value" +CONF_DRYING_VALUE = "drying_value" +CONF_FANONLY_VALUE = "fanonly_value" CONF_HEATING_STATE_PIN = "heating_state_pin" CONF_COOLING_STATE_PIN = "cooling_state_pin" CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" @@ -23,9 +30,17 @@ CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier" CONF_CURRENT_TEMPERATURE_MULTIPLIER = "current_temperature_multiplier" CONF_TARGET_TEMPERATURE_MULTIPLIER = "target_temperature_multiplier" -CONF_ECO_DATAPOINT = "eco_datapoint" -CONF_ECO_TEMPERATURE = "eco_temperature" +CONF_ECO = "eco" +CONF_SLEEP = "sleep" +CONF_SLEEP_DATAPOINT = "sleep_datapoint" CONF_REPORTS_FAHRENHEIT = "reports_fahrenheit" +CONF_VERTICAL_DATAPOINT = "vertical_datapoint" +CONF_HORIZONTAL_DATAPOINT = "horizontal_datapoint" +CONF_LOW_VALUE = "low_value" +CONF_MEDIUM_VALUE = "medium_value" +CONF_MIDDLE_VALUE = "middle_value" +CONF_HIGH_VALUE = "high_value" +CONF_AUTO_VALUE = "auto_value" TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component) @@ -67,30 +82,73 @@ def validate_temperature_multipliers(value): return value -def validate_active_state_values(value): - if CONF_ACTIVE_STATE_DATAPOINT not in value: - if CONF_ACTIVE_STATE_COOLING_VALUE in value: - raise cv.Invalid( - f"{CONF_ACTIVE_STATE_DATAPOINT} required if using " - f"{CONF_ACTIVE_STATE_COOLING_VALUE}" - ) - else: - if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value: - raise cv.Invalid( - f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using " - f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling" - ) +def validate_cooling_values(value): + if CONF_SUPPORTS_COOL in value: + cooling_supported = value[CONF_SUPPORTS_COOL] + if not cooling_supported and CONF_ACTIVE_STATE in value: + active_state_config = value[CONF_ACTIVE_STATE] + if ( + CONF_COOLING_VALUE in active_state_config + or CONF_COOLING_STATE_PIN in value + ): + raise cv.Invalid( + f"Device does not support cooling, but {CONF_COOLING_VALUE} or {CONF_COOLING_STATE_PIN} specified." + f" Please add '{CONF_SUPPORTS_COOL}: true' to your configuration." + ) + elif cooling_supported and CONF_ACTIVE_STATE in value: + active_state_config = value[CONF_ACTIVE_STATE] + if ( + CONF_COOLING_VALUE not in active_state_config + and CONF_COOLING_STATE_PIN not in value + ): + raise cv.Invalid( + f"Either {CONF_ACTIVE_STATE} {CONF_COOLING_VALUE} or {CONF_COOLING_STATE_PIN} is required if" + f" {CONF_SUPPORTS_COOL}: true' is in your configuration." + ) return value -def validate_eco_values(value): - if CONF_ECO_TEMPERATURE in value and CONF_ECO_DATAPOINT not in value: - raise cv.Invalid( - f"{CONF_ECO_DATAPOINT} required if using {CONF_ECO_TEMPERATURE}" - ) - return value +ACTIVE_STATES = cv.Schema( + { + cv.Required(CONF_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_HEATING_VALUE, default=1): cv.uint8_t, + cv.Optional(CONF_COOLING_VALUE): cv.uint8_t, + cv.Optional(CONF_DRYING_VALUE): cv.uint8_t, + cv.Optional(CONF_FANONLY_VALUE): cv.uint8_t, + }, +) +PRESETS = cv.Schema( + { + cv.Optional(CONF_ECO): { + cv.Required(CONF_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_TEMPERATURE): cv.temperature, + }, + cv.Optional(CONF_SLEEP): { + cv.Required(CONF_DATAPOINT): cv.uint8_t, + }, + }, +) + +FAN_MODES = cv.Schema( + { + cv.Required(CONF_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_AUTO_VALUE): cv.uint8_t, + cv.Optional(CONF_LOW_VALUE): cv.uint8_t, + cv.Optional(CONF_MEDIUM_VALUE): cv.uint8_t, + cv.Optional(CONF_MIDDLE_VALUE): cv.uint8_t, + cv.Optional(CONF_HIGH_VALUE): cv.uint8_t, + } +) + +SWING_MODES = cv.Schema( + { + cv.Optional(CONF_VERTICAL_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_HORIZONTAL_DATAPOINT): cv.uint8_t, + }, +) + CONFIG_SCHEMA = cv.All( climate.CLIMATE_SCHEMA.extend( { @@ -99,9 +157,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_ACTIVE_STATE_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_ACTIVE_STATE_HEATING_VALUE, default=1): cv.uint8_t, - cv.Optional(CONF_ACTIVE_STATE_COOLING_VALUE): cv.uint8_t, + cv.Optional(CONF_ACTIVE_STATE): ACTIVE_STATES, cv.Optional(CONF_HEATING_STATE_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_COOLING_STATE_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t, @@ -109,17 +165,30 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TEMPERATURE_MULTIPLIER): cv.positive_float, cv.Optional(CONF_CURRENT_TEMPERATURE_MULTIPLIER): cv.positive_float, cv.Optional(CONF_TARGET_TEMPERATURE_MULTIPLIER): cv.positive_float, - cv.Optional(CONF_ECO_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_ECO_TEMPERATURE): cv.temperature, cv.Optional(CONF_REPORTS_FAHRENHEIT, default=False): cv.boolean, + cv.Optional(CONF_PRESET): PRESETS, + cv.Optional(CONF_FAN_MODE): FAN_MODES, + cv.Optional(CONF_SWING_MODE): SWING_MODES, + cv.Optional("active_state_datapoint"): cv.invalid( + "'active_state_datapoint' has been moved inside of the 'active_state' config block as 'datapoint'" + ), + cv.Optional("active_state_heating_value"): cv.invalid( + "'active_state_heating_value' has been moved inside of the 'active_state' config block as 'heating_value'" + ), + cv.Optional("active_state_cooling_value"): cv.invalid( + "'active_state_cooling_value' has been moved inside of the 'active_state' config block as 'cooling_value'" + ), + cv.Optional("eco_datapoint"): cv.invalid( + "'eco_datapoint' has been moved inside of the 'eco' config block under 'preset' as 'datapoint'" + ), + cv.Optional("eco_temperature"): cv.invalid( + "'eco_temperature' has been moved inside of the 'eco' config block under 'preset' as 'temperature'" + ), } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT), validate_temperature_multipliers, - validate_active_state_values, - cv.has_at_most_one_key(CONF_ACTIVE_STATE_DATAPOINT, CONF_HEATING_STATE_PIN), - cv.has_at_most_one_key(CONF_ACTIVE_STATE_DATAPOINT, CONF_COOLING_STATE_PIN), - validate_eco_values, + validate_cooling_values, ) @@ -133,61 +202,73 @@ async def to_code(config): cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) - if CONF_SWITCH_DATAPOINT in config: - cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) - if CONF_ACTIVE_STATE_DATAPOINT in config: - cg.add(var.set_active_state_id(config[CONF_ACTIVE_STATE_DATAPOINT])) - if CONF_ACTIVE_STATE_HEATING_VALUE in config: - cg.add( - var.set_active_state_heating_value( - config[CONF_ACTIVE_STATE_HEATING_VALUE] - ) - ) - if CONF_ACTIVE_STATE_COOLING_VALUE in config: - cg.add( - var.set_active_state_cooling_value( - config[CONF_ACTIVE_STATE_COOLING_VALUE] - ) - ) + if switch_datapoint := config.get(CONF_SWITCH_DATAPOINT): + cg.add(var.set_switch_id(switch_datapoint)) + + if heating_state_pin_config := config.get(CONF_HEATING_STATE_PIN): + heating_state_pin = await cg.gpio_pin_expression(heating_state_pin_config) + cg.add(var.set_heating_state_pin(heating_state_pin)) + if cooling_state_pin_config := config.get(CONF_COOLING_STATE_PIN): + cooling_state_pin = await cg.gpio_pin_expression(cooling_state_pin_config) + cg.add(var.set_cooling_state_pin(cooling_state_pin)) + if active_state_config := config.get(CONF_ACTIVE_STATE): + cg.add(var.set_active_state_id(active_state_config.get(CONF_DATAPOINT))) + if (heating_value := active_state_config.get(CONF_HEATING_VALUE)) is not None: + cg.add(var.set_active_state_heating_value(heating_value)) + if (cooling_value := active_state_config.get(CONF_COOLING_VALUE)) is not None: + cg.add(var.set_active_state_cooling_value(cooling_value)) + if (drying_value := active_state_config.get(CONF_DRYING_VALUE)) is not None: + cg.add(var.set_active_state_drying_value(drying_value)) + if (fanonly_value := active_state_config.get(CONF_FANONLY_VALUE)) is not None: + cg.add(var.set_active_state_fanonly_value(fanonly_value)) + + if target_temperature_datapoint := config.get(CONF_TARGET_TEMPERATURE_DATAPOINT): + cg.add(var.set_target_temperature_id(target_temperature_datapoint)) + if current_temperature_datapoint := config.get(CONF_CURRENT_TEMPERATURE_DATAPOINT): + cg.add(var.set_current_temperature_id(current_temperature_datapoint)) + + if temperature_multiplier := config.get(CONF_TEMPERATURE_MULTIPLIER): + cg.add(var.set_target_temperature_multiplier(temperature_multiplier)) + cg.add(var.set_current_temperature_multiplier(temperature_multiplier)) else: - if CONF_HEATING_STATE_PIN in config: - heating_state_pin = await cg.gpio_pin_expression( - config[CONF_HEATING_STATE_PIN] + if current_temperature_multiplier := config.get( + CONF_CURRENT_TEMPERATURE_MULTIPLIER + ): + cg.add( + var.set_current_temperature_multiplier(current_temperature_multiplier) ) - cg.add(var.set_heating_state_pin(heating_state_pin)) - if CONF_COOLING_STATE_PIN in config: - cooling_state_pin = await cg.gpio_pin_expression( - config[CONF_COOLING_STATE_PIN] - ) - cg.add(var.set_cooling_state_pin(cooling_state_pin)) - if CONF_TARGET_TEMPERATURE_DATAPOINT in config: - cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT])) - if CONF_CURRENT_TEMPERATURE_DATAPOINT in config: - cg.add( - var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT]) - ) - if CONF_TEMPERATURE_MULTIPLIER in config: - cg.add( - var.set_target_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER]) - ) - cg.add( - var.set_current_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER]) - ) - else: - cg.add( - var.set_current_temperature_multiplier( - config[CONF_CURRENT_TEMPERATURE_MULTIPLIER] - ) - ) - cg.add( - var.set_target_temperature_multiplier( - config[CONF_TARGET_TEMPERATURE_MULTIPLIER] - ) - ) - if CONF_ECO_DATAPOINT in config: - cg.add(var.set_eco_id(config[CONF_ECO_DATAPOINT])) - if CONF_ECO_TEMPERATURE in config: - cg.add(var.set_eco_temperature(config[CONF_ECO_TEMPERATURE])) + if target_temperature_multiplier := config.get( + CONF_TARGET_TEMPERATURE_MULTIPLIER + ): + cg.add(var.set_target_temperature_multiplier(target_temperature_multiplier)) if config[CONF_REPORTS_FAHRENHEIT]: cg.add(var.set_reports_fahrenheit()) + + if preset_config := config.get(CONF_PRESET, {}): + if eco_config := preset_config.get(CONF_ECO, {}): + cg.add(var.set_eco_id(eco_config.get(CONF_DATAPOINT))) + if eco_temperature := eco_config.get(CONF_TEMPERATURE): + cg.add(var.set_eco_temperature(eco_temperature)) + if sleep_config := preset_config.get(CONF_SLEEP, {}): + cg.add(var.set_sleep_id(sleep_config.get(CONF_DATAPOINT))) + + if swing_mode_config := config.get(CONF_SWING_MODE): + if swing_vertical_datapoint := swing_mode_config.get(CONF_VERTICAL_DATAPOINT): + cg.add(var.set_swing_vertical_id(swing_vertical_datapoint)) + if swing_horizontal_datapoint := swing_mode_config.get( + CONF_HORIZONTAL_DATAPOINT + ): + cg.add(var.set_swing_horizontal_id(swing_horizontal_datapoint)) + if fan_mode_config := config.get(CONF_FAN_MODE): + cg.add(var.set_fan_speed_id(fan_mode_config.get(CONF_DATAPOINT))) + if (fan_auto_value := fan_mode_config.get(CONF_AUTO_VALUE)) is not None: + cg.add(var.set_fan_speed_auto_value(fan_auto_value)) + if (fan_low_value := fan_mode_config.get(CONF_LOW_VALUE)) is not None: + cg.add(var.set_fan_speed_low_value(fan_low_value)) + if (fan_medium_value := fan_mode_config.get(CONF_MEDIUM_VALUE)) is not None: + cg.add(var.set_fan_speed_medium_value(fan_medium_value)) + if (fan_middle_value := fan_mode_config.get(CONF_MIDDLE_VALUE)) is not None: + cg.add(var.set_fan_speed_middle_value(fan_middle_value)) + if (fan_high_value := fan_mode_config.get(CONF_HIGH_VALUE)) is not None: + cg.add(var.set_fan_speed_high_value(fan_high_value)) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 687764e30f..7827a4e3ab 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -24,6 +24,14 @@ void TuyaClimate::setup() { this->publish_state(); }); } + if (this->heating_state_pin_ != nullptr) { + this->heating_state_pin_->setup(); + this->heating_state_ = this->heating_state_pin_->digital_read(); + } + if (this->cooling_state_pin_ != nullptr) { + this->cooling_state_pin_->setup(); + this->cooling_state_ = this->cooling_state_pin_->digital_read(); + } if (this->active_state_id_.has_value()) { this->parent_->register_listener(*this->active_state_id_, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum); @@ -31,15 +39,6 @@ void TuyaClimate::setup() { this->compute_state_(); this->publish_state(); }); - } else { - if (this->heating_state_pin_ != nullptr) { - this->heating_state_pin_->setup(); - this->heating_state_ = this->heating_state_pin_->digital_read(); - } - if (this->cooling_state_pin_ != nullptr) { - this->cooling_state_pin_->setup(); - this->cooling_state_ = this->cooling_state_pin_->digital_read(); - } } if (this->target_temperature_id_.has_value()) { this->parent_->register_listener(*this->target_temperature_id_, [this](const TuyaDatapoint &datapoint) { @@ -75,12 +74,44 @@ void TuyaClimate::setup() { this->publish_state(); }); } + if (this->sleep_id_.has_value()) { + this->parent_->register_listener(*this->sleep_id_, [this](const TuyaDatapoint &datapoint) { + this->sleep_ = datapoint.value_bool; + ESP_LOGV(TAG, "MCU reported sleep is: %s", ONOFF(this->sleep_)); + this->compute_preset_(); + this->compute_target_temperature_(); + this->publish_state(); + }); + } + if (this->swing_vertical_id_.has_value()) { + this->parent_->register_listener(*this->swing_vertical_id_, [this](const TuyaDatapoint &datapoint) { + this->swing_vertical_ = datapoint.value_bool; + ESP_LOGV(TAG, "MCU reported vertical swing is: %s", ONOFF(datapoint.value_bool)); + this->compute_swingmode_(); + this->publish_state(); + }); + } + + if (this->swing_horizontal_id_.has_value()) { + this->parent_->register_listener(*this->swing_horizontal_id_, [this](const TuyaDatapoint &datapoint) { + this->swing_horizontal_ = datapoint.value_bool; + ESP_LOGV(TAG, "MCU reported horizontal swing is: %s", ONOFF(datapoint.value_bool)); + this->compute_swingmode_(); + this->publish_state(); + }); + } + + if (this->fan_speed_id_.has_value()) { + this->parent_->register_listener(*this->fan_speed_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported Fan Speed Mode is: %u", datapoint.value_enum); + this->fan_state_ = datapoint.value_enum; + this->compute_fanmode_(); + this->publish_state(); + }); + } } void TuyaClimate::loop() { - if (this->active_state_id_.has_value()) - return; - bool state_changed = false; if (this->heating_state_pin_ != nullptr) { bool heating_state = this->heating_state_pin_->digital_read(); @@ -110,8 +141,26 @@ void TuyaClimate::control(const climate::ClimateCall &call) { const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF; ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state)); this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state); + const climate::ClimateMode new_mode = *call.get_mode(); + + if (this->active_state_id_.has_value()) { + if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) { + this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_heating_value_); + } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) { + this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_cooling_value_); + } else if (new_mode == climate::CLIMATE_MODE_DRY && this->active_state_drying_value_.has_value()) { + this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_drying_value_); + } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY && this->active_state_fanonly_value_.has_value()) { + this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_fanonly_value_); + } + } else { + ESP_LOGW(TAG, "Active state (mode) datapoint not configured"); + } } + control_swing_mode_(call); + control_fan_mode_(call); + if (call.get_target_temperature().has_value()) { float target_temperature = *call.get_target_temperature(); if (this->reports_fahrenheit_) @@ -129,6 +178,106 @@ void TuyaClimate::control(const climate::ClimateCall &call) { ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco)); this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); } + if (this->sleep_id_.has_value()) { + const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP; + ESP_LOGV(TAG, "Setting sleep: %s", ONOFF(sleep)); + this->parent_->set_boolean_datapoint_value(*this->sleep_id_, sleep); + } + } +} + +void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) { + bool vertical_swing_changed = false; + bool horizontal_swing_changed = false; + + if (call.get_swing_mode().has_value()) { + const auto swing_mode = *call.get_swing_mode(); + + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + if (swing_vertical_ || swing_horizontal_) { + this->swing_vertical_ = false; + this->swing_horizontal_ = false; + vertical_swing_changed = true; + horizontal_swing_changed = true; + } + break; + + case climate::CLIMATE_SWING_BOTH: + if (!swing_vertical_ || !swing_horizontal_) { + this->swing_vertical_ = true; + this->swing_horizontal_ = true; + vertical_swing_changed = true; + horizontal_swing_changed = true; + } + break; + + case climate::CLIMATE_SWING_VERTICAL: + if (!swing_vertical_ || swing_horizontal_) { + this->swing_vertical_ = true; + this->swing_horizontal_ = false; + vertical_swing_changed = true; + horizontal_swing_changed = true; + } + break; + + case climate::CLIMATE_SWING_HORIZONTAL: + if (swing_vertical_ || !swing_horizontal_) { + this->swing_vertical_ = false; + this->swing_horizontal_ = true; + vertical_swing_changed = true; + horizontal_swing_changed = true; + } + break; + + default: + break; + } + } + + if (vertical_swing_changed && this->swing_vertical_id_.has_value()) { + ESP_LOGV(TAG, "Setting vertical swing: %s", ONOFF(swing_vertical_)); + this->parent_->set_boolean_datapoint_value(*this->swing_vertical_id_, swing_vertical_); + } + + if (horizontal_swing_changed && this->swing_horizontal_id_.has_value()) { + ESP_LOGV(TAG, "Setting horizontal swing: %s", ONOFF(swing_horizontal_)); + this->parent_->set_boolean_datapoint_value(*this->swing_horizontal_id_, swing_horizontal_); + } + + // Publish the state after updating the swing mode + this->publish_state(); +} + +void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) { + if (call.get_fan_mode().has_value()) { + climate::ClimateFanMode fan_mode = *call.get_fan_mode(); + + uint8_t tuya_fan_speed; + switch (fan_mode) { + case climate::CLIMATE_FAN_LOW: + tuya_fan_speed = *fan_speed_low_value_; + break; + case climate::CLIMATE_FAN_MEDIUM: + tuya_fan_speed = *fan_speed_medium_value_; + break; + case climate::CLIMATE_FAN_MIDDLE: + tuya_fan_speed = *fan_speed_middle_value_; + break; + case climate::CLIMATE_FAN_HIGH: + tuya_fan_speed = *fan_speed_high_value_; + break; + case climate::CLIMATE_FAN_AUTO: + tuya_fan_speed = *fan_speed_auto_value_; + break; + default: + tuya_fan_speed = 0; + break; + } + + if (this->fan_speed_id_.has_value()) { + this->parent_->set_enum_datapoint_value(*this->fan_speed_id_, tuya_fan_speed); + } } } @@ -140,10 +289,46 @@ climate::ClimateTraits TuyaClimate::traits() { traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); if (supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (this->active_state_drying_value_.has_value()) + traits.add_supported_mode(climate::CLIMATE_MODE_DRY); + if (this->active_state_fanonly_value_.has_value()) + traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); if (this->eco_id_.has_value()) { - traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); } + if (this->sleep_id_.has_value()) { + traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP); + } + if (this->sleep_id_.has_value() || this->eco_id_.has_value()) { + traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); + } + if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { + std::set supported_swing_modes = { + climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL}; + traits.set_supported_swing_modes(std::move(supported_swing_modes)); + } else if (this->swing_vertical_id_.has_value()) { + std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_VERTICAL}; + traits.set_supported_swing_modes(std::move(supported_swing_modes)); + } else if (this->swing_horizontal_id_.has_value()) { + std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_HORIZONTAL}; + traits.set_supported_swing_modes(std::move(supported_swing_modes)); + } + + if (fan_speed_id_) { + if (fan_speed_low_value_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_LOW); + if (fan_speed_medium_value_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_MEDIUM); + if (fan_speed_middle_value_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); + if (fan_speed_high_value_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_HIGH); + if (fan_speed_auto_value_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); + } return traits; } @@ -166,16 +351,56 @@ void TuyaClimate::dump_config() { if (this->eco_id_.has_value()) { ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *this->eco_id_); } + if (this->sleep_id_.has_value()) { + ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *this->sleep_id_); + } + if (this->swing_vertical_id_.has_value()) { + ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *this->swing_vertical_id_); + } + if (this->swing_horizontal_id_.has_value()) { + ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *this->swing_horizontal_id_); + } } void TuyaClimate::compute_preset_() { if (this->eco_) { this->preset = climate::CLIMATE_PRESET_ECO; + } else if (this->sleep_) { + this->preset = climate::CLIMATE_PRESET_SLEEP; } else { this->preset = climate::CLIMATE_PRESET_NONE; } } +void TuyaClimate::compute_swingmode_() { + if (this->swing_vertical_ && this->swing_horizontal_) { + this->swing_mode = climate::CLIMATE_SWING_BOTH; + } else if (this->swing_vertical_) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else if (this->swing_horizontal_) { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } +} + +void TuyaClimate::compute_fanmode_() { + if (this->fan_speed_id_.has_value()) { + // Use state from MCU datapoint + if (this->fan_speed_auto_value_.has_value() && this->fan_state_ == this->fan_speed_auto_value_) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + } else if (this->fan_speed_high_value_.has_value() && this->fan_state_ == this->fan_speed_high_value_) { + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } else if (this->fan_speed_medium_value_.has_value() && this->fan_state_ == this->fan_speed_medium_value_) { + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + } else if (this->fan_speed_middle_value_.has_value() && this->fan_state_ == this->fan_speed_middle_value_) { + this->fan_mode = climate::CLIMATE_FAN_MIDDLE; + } else if (this->fan_speed_low_value_.has_value() && this->fan_state_ == this->fan_speed_low_value_) { + this->fan_mode = climate::CLIMATE_FAN_LOW; + } + } +} + void TuyaClimate::compute_target_temperature_() { if (this->eco_ && this->eco_temperature_.has_value()) { this->target_temperature = *this->eco_temperature_; @@ -197,21 +422,49 @@ void TuyaClimate::compute_state_() { } climate::ClimateAction target_action = climate::CLIMATE_ACTION_IDLE; - if (this->active_state_id_.has_value()) { + if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) { + // Use state from input pins + if (this->heating_state_) { + target_action = climate::CLIMATE_ACTION_HEATING; + this->mode = climate::CLIMATE_MODE_HEAT; + } else if (this->cooling_state_) { + target_action = climate::CLIMATE_ACTION_COOLING; + this->mode = climate::CLIMATE_MODE_COOL; + } + if (this->active_state_id_.has_value()) { + // Both are available, use MCU datapoint as mode + if (this->supports_heat_ && this->active_state_heating_value_.has_value() && + this->active_state_ == this->active_state_heating_value_) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() && + this->active_state_ == this->active_state_cooling_value_) { + this->mode = climate::CLIMATE_MODE_COOL; + } else if (this->active_state_drying_value_.has_value() && + this->active_state_ == this->active_state_drying_value_) { + this->mode = climate::CLIMATE_MODE_DRY; + } else if (this->active_state_fanonly_value_.has_value() && + this->active_state_ == this->active_state_fanonly_value_) { + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + } + } + } else if (this->active_state_id_.has_value()) { // Use state from MCU datapoint if (this->supports_heat_ && this->active_state_heating_value_.has_value() && this->active_state_ == this->active_state_heating_value_) { target_action = climate::CLIMATE_ACTION_HEATING; + this->mode = climate::CLIMATE_MODE_HEAT; } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() && this->active_state_ == this->active_state_cooling_value_) { target_action = climate::CLIMATE_ACTION_COOLING; - } - } else if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) { - // Use state from input pins - if (this->heating_state_) { - target_action = climate::CLIMATE_ACTION_HEATING; - } else if (this->cooling_state_) { - target_action = climate::CLIMATE_ACTION_COOLING; + this->mode = climate::CLIMATE_MODE_COOL; + } else if (this->active_state_drying_value_.has_value() && + this->active_state_ == this->active_state_drying_value_) { + target_action = climate::CLIMATE_ACTION_DRYING; + this->mode = climate::CLIMATE_MODE_DRY; + } else if (this->active_state_fanonly_value_.has_value() && + this->active_state_ == this->active_state_fanonly_value_) { + target_action = climate::CLIMATE_ACTION_FAN; + this->mode = climate::CLIMATE_MODE_FAN_ONLY; } } else { // Fallback to active state calc based on temp and hysteresis @@ -219,8 +472,10 @@ void TuyaClimate::compute_state_() { if (std::abs(temp_diff) > this->hysteresis_) { if (this->supports_heat_ && temp_diff > 0) { target_action = climate::CLIMATE_ACTION_HEATING; + this->mode = climate::CLIMATE_MODE_HEAT; } else if (this->supports_cool_ && temp_diff < 0) { target_action = climate::CLIMATE_ACTION_COOLING; + this->mode = climate::CLIMATE_MODE_COOL; } } } diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index 7c18625c4e..d6258c21e1 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -18,8 +18,22 @@ class TuyaClimate : public climate::Climate, public Component { void set_active_state_id(uint8_t state_id) { this->active_state_id_ = state_id; } void set_active_state_heating_value(uint8_t value) { this->active_state_heating_value_ = value; } void set_active_state_cooling_value(uint8_t value) { this->active_state_cooling_value_ = value; } + void set_active_state_drying_value(uint8_t value) { this->active_state_drying_value_ = value; } + void set_active_state_fanonly_value(uint8_t value) { this->active_state_fanonly_value_ = value; } void set_heating_state_pin(GPIOPin *pin) { this->heating_state_pin_ = pin; } void set_cooling_state_pin(GPIOPin *pin) { this->cooling_state_pin_ = pin; } + void set_swing_vertical_id(uint8_t swing_vertical_id) { this->swing_vertical_id_ = swing_vertical_id; } + void set_swing_horizontal_id(uint8_t swing_horizontal_id) { this->swing_horizontal_id_ = swing_horizontal_id; } + void set_fan_speed_id(uint8_t fan_speed_id) { this->fan_speed_id_ = fan_speed_id; } + void set_fan_speed_low_value(uint8_t fan_speed_low_value) { this->fan_speed_low_value_ = fan_speed_low_value; } + void set_fan_speed_medium_value(uint8_t fan_speed_medium_value) { + this->fan_speed_medium_value_ = fan_speed_medium_value; + } + void set_fan_speed_middle_value(uint8_t fan_speed_middle_value) { + this->fan_speed_middle_value_ = fan_speed_middle_value; + } + void set_fan_speed_high_value(uint8_t fan_speed_high_value) { this->fan_speed_high_value_ = fan_speed_high_value; } + void set_fan_speed_auto_value(uint8_t fan_speed_auto_value) { this->fan_speed_auto_value_ = fan_speed_auto_value; } void set_target_temperature_id(uint8_t target_temperature_id) { this->target_temperature_id_ = target_temperature_id; } @@ -34,6 +48,7 @@ class TuyaClimate : public climate::Climate, public Component { } void set_eco_id(uint8_t eco_id) { this->eco_id_ = eco_id; } void set_eco_temperature(float eco_temperature) { this->eco_temperature_ = eco_temperature; } + void set_sleep_id(uint8_t sleep_id) { this->sleep_id_ = sleep_id; } void set_reports_fahrenheit() { this->reports_fahrenheit_ = true; } @@ -43,6 +58,12 @@ class TuyaClimate : public climate::Climate, public Component { /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; + /// Override control to change settings of swing mode. + void control_swing_mode_(const climate::ClimateCall &call); + + /// Override control to change settings of fan mode. + void control_fan_mode_(const climate::ClimateCall &call); + /// Return the traits of this controller. climate::ClimateTraits traits() override; @@ -55,6 +76,12 @@ class TuyaClimate : public climate::Climate, public Component { /// Re-compute the state of this climate controller. void compute_state_(); + /// Re-Compute the swing mode of this climate controller. + void compute_swingmode_(); + + /// Re-Compute the fan mode of this climate controller. + void compute_fanmode_(); + /// Switch the climate device to the given climate mode. void switch_to_action_(climate::ClimateAction action); @@ -65,6 +92,8 @@ class TuyaClimate : public climate::Climate, public Component { optional active_state_id_{}; optional active_state_heating_value_{}; optional active_state_cooling_value_{}; + optional active_state_drying_value_{}; + optional active_state_fanonly_value_{}; GPIOPin *heating_state_pin_{nullptr}; GPIOPin *cooling_state_pin_{nullptr}; optional target_temperature_id_{}; @@ -73,12 +102,25 @@ class TuyaClimate : public climate::Climate, public Component { float target_temperature_multiplier_{1.0f}; float hysteresis_{1.0f}; optional eco_id_{}; + optional sleep_id_{}; optional eco_temperature_{}; uint8_t active_state_; + uint8_t fan_state_; + optional swing_vertical_id_{}; + optional swing_horizontal_id_{}; + optional fan_speed_id_{}; + optional fan_speed_low_value_{}; + optional fan_speed_medium_value_{}; + optional fan_speed_middle_value_{}; + optional fan_speed_high_value_{}; + optional fan_speed_auto_value_{}; + bool swing_vertical_{false}; + bool swing_horizontal_{false}; bool heating_state_{false}; bool cooling_state_{false}; float manual_temperature_; bool eco_; + bool sleep_; bool reports_fahrenheit_{false}; }; diff --git a/esphome/components/tuya/cover/__init__.py b/esphome/components/tuya/cover/__init__.py index f886c7030f..2dd66f814d 100644 --- a/esphome/components/tuya/cover/__init__.py +++ b/esphome/components/tuya/cover/__init__.py @@ -16,6 +16,7 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint" CONF_POSITION_DATAPOINT = "position_datapoint" CONF_POSITION_REPORT_DATAPOINT = "position_report_datapoint" CONF_INVERT_POSITION = "invert_position" +CONF_INVERT_POSITION_REPORT = "invert_position_report" TuyaCover = tuya_ns.class_("TuyaCover", cover.Cover, cg.Component) @@ -47,6 +48,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + cv.Optional(CONF_INVERT_POSITION_REPORT, default=False): cv.boolean, cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( RESTORE_MODES, upper=True ), @@ -71,6 +73,7 @@ async def to_code(config): cg.add(var.set_min_value(config[CONF_MIN_VALUE])) cg.add(var.set_max_value(config[CONF_MAX_VALUE])) cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) + cg.add(var.set_invert_position_report(config[CONF_INVERT_POSITION_REPORT])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) paren = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index fcb961f45e..14bf937cf7 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -51,7 +51,7 @@ void TuyaCover::setup() { return; } auto pos = float(datapoint.value_uint - this->min_value_) / this->value_range_; - this->position = 1.0f - pos; + this->position = this->invert_position_report_ ? pos : 1.0f - pos; this->publish_state(); }); } @@ -62,7 +62,7 @@ void TuyaCover::control(const cover::CoverCall &call) { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_STOP); } else { auto pos = this->position; - pos = 1.0f - pos; + pos = this->invert_position_report_ ? pos : 1.0f - pos; auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; @@ -78,7 +78,7 @@ void TuyaCover::control(const cover::CoverCall &call) { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_CLOSE); } } else { - pos = 1.0f - pos; + pos = this->invert_position_report_ ? pos : 1.0f - pos; auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; @@ -112,6 +112,9 @@ void TuyaCover::dump_config() { ESP_LOGCONFIG(TAG, " Configured as Inverted, but direction_datapoint isn't configured"); } } + if (this->invert_position_report_) { + ESP_LOGCONFIG(TAG, " Position Reporting Inverted"); + } if (this->control_id_.has_value()) { ESP_LOGCONFIG(TAG, " Control has datapoint ID %u", *this->control_id_); } diff --git a/esphome/components/tuya/cover/tuya_cover.h b/esphome/components/tuya/cover/tuya_cover.h index 87c72b0e66..bb5a00bc59 100644 --- a/esphome/components/tuya/cover/tuya_cover.h +++ b/esphome/components/tuya/cover/tuya_cover.h @@ -25,6 +25,7 @@ class TuyaCover : public cover::Cover, public Component { void set_min_value(uint32_t min_value) { min_value_ = min_value; } void set_max_value(uint32_t max_value) { max_value_ = max_value; } void set_invert_position(bool invert_position) { invert_position_ = invert_position; } + void set_invert_position_report(bool invert_position_report) { invert_position_report_ = invert_position_report; } void set_restore_mode(TuyaCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } protected: @@ -42,6 +43,7 @@ class TuyaCover : public cover::Cover, public Component { uint32_t max_value_; uint32_t value_range_; bool invert_position_; + bool invert_position_report_; }; } // namespace tuya diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 1b03ea50fa..8a613d0bae 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -9,13 +9,20 @@ static const char *const TAG = "tuya.fan"; void TuyaFan::setup() { if (this->speed_id_.has_value()) { this->parent_->register_listener(*this->speed_id_, [this](const TuyaDatapoint &datapoint) { - ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); - if (datapoint.value_enum >= this->speed_count_) { - ESP_LOGE(TAG, "Speed has invalid value %d", datapoint.value_enum); - } else { - this->speed = datapoint.value_enum + 1; + if (datapoint.type == TuyaDatapointType::ENUM) { + ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); + if (datapoint.value_enum >= this->speed_count_) { + ESP_LOGE(TAG, "Speed has invalid value %d", datapoint.value_enum); + } else { + this->speed = datapoint.value_enum + 1; + this->publish_state(); + } + } else if (datapoint.type == TuyaDatapointType::INTEGER) { + ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_int); + this->speed = datapoint.value_int; this->publish_state(); } + this->speed_type_ = datapoint.type; }); } if (this->switch_id_.has_value()) { @@ -27,9 +34,13 @@ void TuyaFan::setup() { } if (this->oscillation_id_.has_value()) { this->parent_->register_listener(*this->oscillation_id_, [this](const TuyaDatapoint &datapoint) { + // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both + // scenarios ESP_LOGV(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); this->oscillating = datapoint.value_bool; this->publish_state(); + + this->oscillation_type_ = datapoint.type; }); } if (this->direction_id_.has_value()) { @@ -73,14 +84,22 @@ void TuyaFan::control(const fan::FanCall &call) { this->parent_->set_boolean_datapoint_value(*this->switch_id_, *call.get_state()); } if (this->oscillation_id_.has_value() && call.get_oscillating().has_value()) { - this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); + if (this->oscillation_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); + } else if (this->speed_type_ == TuyaDatapointType::BOOLEAN) { + this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); + } } if (this->direction_id_.has_value() && call.get_direction().has_value()) { bool enable = *call.get_direction() == fan::FanDirection::REVERSE; this->parent_->set_enum_datapoint_value(*this->direction_id_, enable); } if (this->speed_id_.has_value() && call.get_speed().has_value()) { - this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); + if (this->speed_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); + } else if (this->speed_type_ == TuyaDatapointType::INTEGER) { + this->parent_->set_integer_datapoint_value(*this->speed_id_, *call.get_speed()); + } } } diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 4aba1e1c07..527efa8246 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -28,6 +28,8 @@ class TuyaFan : public Component, public fan::Fan { optional oscillation_id_{}; optional direction_id_{}; int speed_count_{}; + TuyaDatapointType speed_type_{}; + TuyaDatapointType oscillation_type_{}; }; } // namespace tuya diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 7b7a974de2..66931767b2 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -168,7 +168,7 @@ void TuyaLight::write_state(light::LightState *state) { if (brightness > 0.0f || !color_interlock_) { if (this->color_temperature_id_.has_value()) { - uint32_t color_temp_int = static_cast(color_temperature * this->color_temperature_max_value_); + uint32_t color_temp_int = static_cast(roundf(color_temperature * this->color_temperature_max_value_)); if (this->color_temperature_invert_) { color_temp_int = this->color_temperature_max_value_ - color_temp_int; } diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py index 42ac9fcfbe..c00ea08d23 100644 --- a/esphome/components/tuya/number/__init__.py +++ b/esphome/components/tuya/number/__init__.py @@ -6,19 +6,39 @@ from esphome.const import ( CONF_NUMBER_DATAPOINT, CONF_MAX_VALUE, CONF_MIN_VALUE, + CONF_MULTIPLY, CONF_STEP, + CONF_INITIAL_VALUE, + CONF_RESTORE_VALUE, ) -from .. import tuya_ns, CONF_TUYA_ID, Tuya +from .. import tuya_ns, CONF_TUYA_ID, Tuya, TuyaDatapointType DEPENDENCIES = ["tuya"] CODEOWNERS = ["@frankiboy1"] +CONF_DATAPOINT_HIDDEN = "datapoint_hidden" +CONF_DATAPOINT_TYPE = "datapoint_type" + TuyaNumber = tuya_ns.class_("TuyaNumber", number.Number, cg.Component) +DATAPOINT_TYPES = { + "int": TuyaDatapointType.INTEGER, + "uint": TuyaDatapointType.INTEGER, + "enum": TuyaDatapointType.ENUM, +} + def validate_min_max(config): - if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + max_value = config[CONF_MAX_VALUE] + min_value = config[CONF_MIN_VALUE] + if max_value <= min_value: raise cv.Invalid("max_value must be greater than min_value") + if hidden_config := config.get(CONF_DATAPOINT_HIDDEN): + if (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None: + if (initial_value > max_value) or (initial_value < min_value): + raise cv.Invalid( + f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" + ) return config @@ -31,6 +51,18 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_DATAPOINT_HIDDEN): cv.All( + cv.Schema( + { + cv.Required(CONF_DATAPOINT_TYPE): cv.enum( + DATAPOINT_TYPES, lower=True + ), + cv.Optional(CONF_INITIAL_VALUE): cv.float_, + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, + } + ) + ), } ) .extend(cv.COMPONENT_SCHEMA), @@ -49,7 +81,15 @@ async def to_code(config): step=config[CONF_STEP], ) - paren = await cg.get_variable(config[CONF_TUYA_ID]) - cg.add(var.set_tuya_parent(paren)) + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + parent = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(parent)) cg.add(var.set_number_id(config[CONF_NUMBER_DATAPOINT])) + if hidden_config := config.get(CONF_DATAPOINT_HIDDEN): + cg.add(var.set_datapoint_type(hidden_config[CONF_DATAPOINT_TYPE])) + if ( + hidden_init_value := hidden_config.get(CONF_INITIAL_VALUE, None) + ) is not None: + cg.add(var.set_datapoint_initial_value(hidden_init_value)) + cg.add(var.set_restore_value(hidden_config[CONF_RESTORE_VALUE])) diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index 5c7cafbf7a..68a7f8f2a7 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -7,31 +7,88 @@ namespace tuya { static const char *const TAG = "tuya.number"; void TuyaNumber::setup() { + if (this->restore_value_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + } + this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { if (datapoint.type == TuyaDatapointType::INTEGER) { ESP_LOGV(TAG, "MCU reported number %u is: %d", datapoint.id, datapoint.value_int); - this->publish_state(datapoint.value_int); + float value = datapoint.value_int / multiply_by_; + this->publish_state(value); + if (this->restore_value_) + this->pref_.save(&value); } else if (datapoint.type == TuyaDatapointType::ENUM) { ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum); - this->publish_state(datapoint.value_enum); + float value = datapoint.value_enum; + this->publish_state(value); + if (this->restore_value_) + this->pref_.save(&value); + } else { + ESP_LOGW(TAG, "Reported type (%d) is not a number!", static_cast(datapoint.type)); + return; + } + + if ((this->type_) && (this->type_ != datapoint.type)) { + ESP_LOGW(TAG, "Reported type (%d) different than previously set (%d)!", static_cast(datapoint.type), + static_cast(*this->type_)); } this->type_ = datapoint.type; }); + + this->parent_->add_on_initialized_callback([this] { + if (this->type_) { + float value; + if (!this->restore_value_) { + if (this->initial_value_) { + value = *this->initial_value_; + } else { + return; + } + } else { + if (!this->pref_.load(&value)) { + if (this->initial_value_) { + value = *this->initial_value_; + } else { + value = this->traits.get_min_value(); + ESP_LOGW(TAG, "Failed to restore and there is no initial value defined. Setting min_value (%f)", value); + } + } + } + + this->control(value); + } + }); } void TuyaNumber::control(float value) { ESP_LOGV(TAG, "Setting number %u: %f", this->number_id_, value); if (this->type_ == TuyaDatapointType::INTEGER) { - this->parent_->set_integer_datapoint_value(this->number_id_, value); + int integer_value = lround(value * multiply_by_); + this->parent_->set_integer_datapoint_value(this->number_id_, integer_value); } else if (this->type_ == TuyaDatapointType::ENUM) { this->parent_->set_enum_datapoint_value(this->number_id_, value); } this->publish_state(value); + + if (this->restore_value_) + this->pref_.save(&value); } void TuyaNumber::dump_config() { LOG_NUMBER("", "Tuya Number", this); ESP_LOGCONFIG(TAG, " Number has datapoint ID %u", this->number_id_); + if (this->type_) { + ESP_LOGCONFIG(TAG, " Datapoint type is %d", static_cast(*this->type_)); + } else { + ESP_LOGCONFIG(TAG, " Datapoint type is unknown"); + } + + if (this->initial_value_) { + ESP_LOGCONFIG(TAG, " Initial Value: %f", *this->initial_value_); + } + + ESP_LOGCONFIG(TAG, " Restore Value: %s", YESNO(this->restore_value_)); } } // namespace tuya diff --git a/esphome/components/tuya/number/tuya_number.h b/esphome/components/tuya/number/tuya_number.h index 7cca9fc646..53137d6f66 100644 --- a/esphome/components/tuya/number/tuya_number.h +++ b/esphome/components/tuya/number/tuya_number.h @@ -1,8 +1,10 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/tuya/tuya.h" #include "esphome/components/number/number.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "esphome/core/preferences.h" namespace esphome { namespace tuya { @@ -12,6 +14,10 @@ class TuyaNumber : public number::Number, public Component { void setup() override; void dump_config() override; void set_number_id(uint8_t number_id) { this->number_id_ = number_id; } + void set_write_multiply(float factor) { multiply_by_ = factor; } + void set_datapoint_type(TuyaDatapointType type) { type_ = type; } + void set_datapoint_initial_value(float value) { this->initial_value_ = value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } @@ -20,7 +26,12 @@ class TuyaNumber : public number::Number, public Component { Tuya *parent_; uint8_t number_id_{0}; - TuyaDatapointType type_{}; + float multiply_by_{1.0}; + optional type_{}; + optional initial_value_{}; + bool restore_value_{false}; + + ESPPreferenceObject pref_; }; } // namespace tuya diff --git a/esphome/components/tuya/sensor/tuya_sensor.cpp b/esphome/components/tuya/sensor/tuya_sensor.cpp index 1e39c1bc35..673471a6ce 100644 --- a/esphome/components/tuya/sensor/tuya_sensor.cpp +++ b/esphome/components/tuya/sensor/tuya_sensor.cpp @@ -1,5 +1,6 @@ #include "esphome/core/log.h" #include "tuya_sensor.h" +#include namespace esphome { namespace tuya { @@ -18,7 +19,7 @@ void TuyaSensor::setup() { ESP_LOGV(TAG, "MCU reported sensor %u is: %u", datapoint.id, datapoint.value_enum); this->publish_state(datapoint.value_enum); } else if (datapoint.type == TuyaDatapointType::BITMASK) { - ESP_LOGV(TAG, "MCU reported sensor %u is: %x", datapoint.id, datapoint.value_bitmask); + ESP_LOGV(TAG, "MCU reported sensor %u is: %" PRIx32, datapoint.id, datapoint.value_bitmask); this->publish_state(datapoint.value_bitmask); } }); diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index 602595e89d..fbe511811f 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -1,5 +1,5 @@ -#include "esphome/core/log.h" #include "tuya_text_sensor.h" +#include "esphome/core/log.h" namespace esphome { namespace tuya { @@ -19,6 +19,12 @@ void TuyaTextSensor::setup() { this->publish_state(data); 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); + break; + } default: ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, (uint8_t) datapoint.type); break; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 040b9b7ed5..1443d10254 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -1,9 +1,9 @@ #include "tuya.h" #include "esphome/components/network/util.h" +#include "esphome/core/gpio.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -#include "esphome/core/gpio.h" #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" @@ -23,8 +23,8 @@ static const int MAX_RETRIES = 5; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); - if (this->status_pin_.has_value()) { - this->status_pin_.value()->digital_write(false); + if (this->status_pin_ != nullptr) { + this->status_pin_->digital_write(false); } } @@ -61,7 +61,7 @@ void Tuya::dump_config() { } else if (info.type == TuyaDatapointType::ENUM) { ESP_LOGCONFIG(TAG, " Datapoint %u: enum (value: %d)", info.id, info.value_enum); } else if (info.type == TuyaDatapointType::BITMASK) { - ESP_LOGCONFIG(TAG, " Datapoint %u: bitmask (value: %x)", info.id, info.value_bitmask); + ESP_LOGCONFIG(TAG, " Datapoint %u: bitmask (value: %" PRIx32 ")", info.id, info.value_bitmask); } else { ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id); } @@ -70,9 +70,7 @@ void Tuya::dump_config() { ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_, this->reset_pin_reported_); } - if (this->status_pin_.has_value()) { - LOG_PIN(" Status Pin: ", this->status_pin_.value()); - } + LOG_PIN(" Status Pin: ", this->status_pin_); ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); } @@ -194,7 +192,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->init_state_ = TuyaInitState::INIT_DATAPOINT; this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); bool is_pin_equals = - this->status_pin_.has_value() && this->status_pin_.value()->get_pin() == this->status_pin_reported_; + this->status_pin_ != nullptr && this->status_pin_->get_pin() == this->status_pin_reported_; // Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send if (is_pin_equals) { ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_); @@ -225,13 +223,19 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff break; case TuyaCommandType::DATAPOINT_DELIVER: break; - case TuyaCommandType::DATAPOINT_REPORT: + case TuyaCommandType::DATAPOINT_REPORT_ASYNC: + case TuyaCommandType::DATAPOINT_REPORT_SYNC: if (this->init_state_ == TuyaInitState::INIT_DATAPOINT) { this->init_state_ = TuyaInitState::INIT_DONE; this->set_timeout("datapoint_dump", 1000, [this] { this->dump_config(); }); this->initialized_callback_.call(); } this->handle_datapoints_(buffer, len); + + if (command_type == TuyaCommandType::DATAPOINT_REPORT_SYNC) { + this->send_command_( + TuyaCommand{.cmd = TuyaCommandType::DATAPOINT_REPORT_ACK, .payload = std::vector{0x01}}); + } break; case TuyaCommandType::DATAPOINT_QUERY: break; @@ -244,16 +248,19 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff break; case TuyaCommandType::LOCAL_TIME_QUERY: #ifdef USE_TIME - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time_(); - auto *time_id = *this->time_id_; - time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); - } else { + + if (!this->time_sync_callback_registered_) { + // tuya mcu supports time, so we let them know when our time changed + this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); }); + this->time_sync_callback_registered_ = true; + } + } else +#endif + { ESP_LOGW(TAG, "LOCAL_TIME_QUERY is not handled because time is not configured"); } -#else - ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled"); -#endif break; case TuyaCommandType::VACUUM_MAP_UPLOAD: this->send_command_( @@ -268,6 +275,30 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff ESP_LOGV(TAG, "Network status requested, reported as %i", wifi_status); break; } + case TuyaCommandType::EXTENDED_SERVICES: { + uint8_t subcommand = buffer[0]; + switch ((TuyaExtendedServicesCommandType) subcommand) { + case TuyaExtendedServicesCommandType::RESET_NOTIFICATION: { + this->send_command_( + TuyaCommand{.cmd = TuyaCommandType::EXTENDED_SERVICES, + .payload = std::vector{ + static_cast(TuyaExtendedServicesCommandType::RESET_NOTIFICATION), 0x00}}); + ESP_LOGV(TAG, "Reset status notification enabled"); + break; + } + case TuyaExtendedServicesCommandType::MODULE_RESET: { + ESP_LOGE(TAG, "EXTENDED_SERVICES::MODULE_RESET is not handled"); + break; + } + case TuyaExtendedServicesCommandType::UPDATE_IN_PROGRESS: { + ESP_LOGE(TAG, "EXTENDED_SERVICES::UPDATE_IN_PROGRESS is not handled"); + break; + } + default: + ESP_LOGE(TAG, "Invalid extended services subcommand (0x%02X) received", subcommand); + } + break; + } default: ESP_LOGE(TAG, "Invalid command (0x%02X) received", command); } @@ -338,7 +369,7 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { ESP_LOGW(TAG, "Datapoint %u has bad bitmask len %zu", datapoint.id, data_size); return; } - ESP_LOGD(TAG, "Datapoint %u update to %#08X", datapoint.id, datapoint.value_bitmask); + ESP_LOGD(TAG, "Datapoint %u update to %#08" PRIX32, datapoint.id, datapoint.value_bitmask); break; default: ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, static_cast(datapoint.type)); @@ -398,7 +429,7 @@ void Tuya::send_raw_command_(TuyaCommand command) { break; case TuyaCommandType::DATAPOINT_DELIVER: case TuyaCommandType::DATAPOINT_QUERY: - this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT; + this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT_ASYNC; break; default: break; @@ -459,7 +490,7 @@ void Tuya::send_empty_command_(TuyaCommandType command) { void Tuya::set_status_pin_() { bool is_network_ready = network::is_connected() && remote_is_connected(); - this->status_pin_.value()->digital_write(is_network_ready); + this->status_pin_->digital_write(is_network_ready); } uint8_t Tuya::get_wifi_status_code_() { @@ -507,8 +538,7 @@ void Tuya::send_wifi_status_() { #ifdef USE_TIME void Tuya::send_local_time_() { std::vector payload; - auto *time_id = *this->time_id_; - time::ESPTime now = time_id->now(); + ESPTime now = this->time_id_->now(); if (now.is_valid()) { uint8_t year = now.year - 2000; uint8_t month = now.month; @@ -590,7 +620,7 @@ optional Tuya::get_datapoint_(uint8_t datapoint_id) { void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, const uint32_t value, uint8_t length, bool forced) { - ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value); + ESP_LOGD(TAG, "Setting datapoint %u to %" PRIu32, datapoint_id, value); 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/tuya/tuya.h b/esphome/components/tuya/tuya.h index 8d6153482f..76431ddfe4 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -7,10 +10,9 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif -#include - namespace esphome { namespace tuya { @@ -51,13 +53,22 @@ enum class TuyaCommandType : uint8_t { WIFI_RESET = 0x04, WIFI_SELECT = 0x05, DATAPOINT_DELIVER = 0x06, - DATAPOINT_REPORT = 0x07, + DATAPOINT_REPORT_ASYNC = 0x07, DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, LOCAL_TIME_QUERY = 0x1C, + DATAPOINT_REPORT_SYNC = 0x22, + DATAPOINT_REPORT_ACK = 0x23, WIFI_RSSI = 0x24, VACUUM_MAP_UPLOAD = 0x28, GET_NETWORK_STATUS = 0x2B, + EXTENDED_SERVICES = 0x34, +}; + +enum class TuyaExtendedServicesCommandType : uint8_t { + RESET_NOTIFICATION = 0x04, + MODULE_RESET = 0x05, + UPDATE_IN_PROGRESS = 0x0A, }; enum class TuyaInitState : uint8_t { @@ -128,13 +139,14 @@ class Tuya : public Component, public uart::UARTDevice { #ifdef USE_TIME void send_local_time_(); - optional time_id_{}; + time::RealTimeClock *time_id_{nullptr}; + bool time_sync_callback_registered_{false}; #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; bool init_failed_{false}; int init_retries_{0}; uint8_t protocol_version_ = -1; - optional status_pin_{}; + InternalGPIOPin *status_pin_{nullptr}; int status_pin_reported_ = -1; int reset_pin_reported_ = -1; uint32_t last_command_timestamp_ = 0; diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index ed60a9f880..0738a127e1 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,5 +1,5 @@ from typing import Optional - +import re import esphome.codegen as cg import esphome.config_validation as cv import esphome.final_validate as fv @@ -11,6 +11,7 @@ from esphome.const import ( CONF_NUMBER, CONF_RX_PIN, CONF_TX_PIN, + CONF_PORT, CONF_UART_ID, CONF_DATA, CONF_RX_BUFFER_SIZE, @@ -27,6 +28,7 @@ from esphome.const import ( CONF_DUMMY_RECEIVER, CONF_DUMMY_RECEIVER_ID, CONF_LAMBDA, + PLATFORM_HOST, ) from esphome.core import CORE @@ -42,12 +44,58 @@ ESP8266UartComponent = uart_ns.class_( "ESP8266UartComponent", UARTComponent, cg.Component ) RP2040UartComponent = uart_ns.class_("RP2040UartComponent", UARTComponent, cg.Component) +LibreTinyUARTComponent = uart_ns.class_( + "LibreTinyUARTComponent", UARTComponent, cg.Component +) +HostUartComponent = uart_ns.class_("HostUartComponent", UARTComponent, cg.Component) + +NATIVE_UART_CLASSES = ( + str(IDFUARTComponent), + str(ESP32ArduinoUARTComponent), + str(ESP8266UartComponent), + str(RP2040UartComponent), + str(LibreTinyUARTComponent), +) + +HOST_BAUD_RATES = [ + 50, + 75, + 110, + 134, + 150, + 200, + 300, + 600, + 1200, + 1800, + 2400, + 4800, + 9600, + 19200, + 38400, + 57600, + 115200, + 230400, + 460800, + 500000, + 576000, + 921600, + 1000000, + 1152000, + 1500000, + 2000000, + 2500000, + 3000000, + 3500000, + 4000000, +] UARTDevice = uart_ns.class_("UARTDevice") UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action) UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action) UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component) MULTI_CONF = True +MULTI_CONF_NO_DEFAULT = True def validate_raw_data(value): @@ -72,16 +120,31 @@ def validate_rx_pin(value): def validate_invert_esp32(config): if ( CORE.is_esp32 + and CORE.using_arduino and CONF_TX_PIN in config and CONF_RX_PIN in config and config[CONF_TX_PIN][CONF_INVERTED] != config[CONF_RX_PIN][CONF_INVERTED] ): raise cv.Invalid( - "Different invert values for TX and RX pin are not (yet) supported for ESP32." + "Different invert values for TX and RX pin are not supported for ESP32 when using Arduino." ) return config +def validate_host_config(config): + if CORE.is_host: + if CONF_TX_PIN in config or CONF_RX_PIN in config: + raise cv.Invalid( + "TX and RX pins are not supported for UART on host platform." + ) + if config[CONF_BAUD_RATE] not in HOST_BAUD_RATES: + raise cv.Invalid( + f"Host platform doesn't support baud rate {config[CONF_BAUD_RATE]}", + path=[CONF_BAUD_RATE], + ) + return config + + def _uart_declare_type(value): if CORE.is_esp8266: return cv.declare_id(ESP8266UartComponent)(value) @@ -92,6 +155,10 @@ def _uart_declare_type(value): return cv.declare_id(IDFUARTComponent)(value) if CORE.is_rp2040: return cv.declare_id(RP2040UartComponent)(value) + if CORE.is_libretiny: + return cv.declare_id(LibreTinyUARTComponent)(value) + if CORE.is_host: + return cv.declare_id(HostUartComponent)(value) raise NotImplementedError @@ -134,6 +201,12 @@ def maybe_empty_debug(value): return DEBUG_SCHEMA(value) +def validate_port(value): + if not re.match(r"^/(?:[^/]+/)[^/]+$", value): + raise cv.Invalid("Port must be a valid device path") + return value + + DEBUG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UARTDebugger), @@ -166,6 +239,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_PORT): cv.All(validate_port, cv.only_on(PLATFORM_HOST)), cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), @@ -178,8 +252,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA), - cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN), + cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT), validate_invert_esp32, + validate_host_config, ) @@ -221,6 +296,8 @@ async def to_code(config): if CONF_RX_PIN in config: rx_pin = await cg.gpio_pin_expression(config[CONF_RX_PIN]) cg.add(var.set_rx_pin(rx_pin)) + if CONF_PORT in config: + cg.add(var.set_name(config[CONF_PORT])) cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) @@ -243,16 +320,18 @@ KEY_UART_DEVICES = "uart_devices" def final_validate_device_schema( name: str, *, + uart_bus: str = CONF_UART_ID, baud_rate: Optional[int] = None, require_tx: bool = False, require_rx: bool = False, + data_bits: Optional[int] = None, parity: Optional[str] = None, stop_bits: Optional[int] = None, ): def validate_baud_rate(value): if value != baud_rate: raise cv.Invalid( - f"Component {name} requires baud rate {baud_rate} for the uart bus" + f"Component {name} requires baud rate {baud_rate} for the uart referenced by {uart_bus}" ) return value @@ -268,42 +347,52 @@ def final_validate_device_schema( return validator + def validate_data_bits(value): + if value != data_bits: + raise cv.Invalid( + f"Component {name} requires {data_bits} data bits for the uart referenced by {uart_bus}" + ) + return value + def validate_parity(value): if value != parity: raise cv.Invalid( - f"Component {name} requires parity {parity} for the uart bus" + f"Component {name} requires parity {parity} for the uart referenced by {uart_bus}" ) return value def validate_stop_bits(value): if value != stop_bits: raise cv.Invalid( - f"Component {name} requires stop bits {stop_bits} for the uart bus" + f"Component {name} requires {stop_bits} stop bits for the uart referenced by {uart_bus}" ) return value def validate_hub(hub_config): hub_schema = {} uart_id = hub_config[CONF_ID] + uart_id_type_str = str(uart_id.type) devices = fv.full_config.get().data.setdefault(KEY_UART_DEVICES, {}) device = devices.setdefault(uart_id, {}) - if require_tx: + if require_tx and uart_id_type_str in NATIVE_UART_CLASSES: hub_schema[ cv.Required( CONF_TX_PIN, - msg=f"Component {name} requires this uart bus to declare a tx_pin", + msg=f"Component {name} requires uart referenced by {uart_bus} to declare a tx_pin", ) ] = validate_pin(CONF_TX_PIN, device) - if require_rx: + if require_rx and uart_id_type_str in NATIVE_UART_CLASSES: hub_schema[ cv.Required( CONF_RX_PIN, - msg=f"Component {name} requires this uart bus to declare a rx_pin", + msg=f"Component {name} requires uart referenced by {uart_bus} to declare a rx_pin", ) ] = validate_pin(CONF_RX_PIN, device) if baud_rate is not None: hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate + if data_bits is not None: + hub_schema[cv.Required(CONF_DATA_BITS)] = validate_data_bits if parity is not None: hub_schema[cv.Required(CONF_PARITY)] = validate_parity if stop_bits is not None: @@ -311,7 +400,7 @@ def final_validate_device_schema( return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config) return cv.Schema( - {cv.Required(CONF_UART_ID): fv.id_declaration_match_schema(validate_hub)}, + {cv.Required(uart_bus): fv.id_declaration_match_schema(validate_hub)}, extra=cv.ALLOW_EXTRA, ) diff --git a/esphome/components/uart/button/__init__.py b/esphome/components/uart/button/__init__.py new file mode 100644 index 0000000000..05909516a0 --- /dev/null +++ b/esphome/components/uart/button/__init__.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button, uart +from esphome.const import CONF_DATA +from esphome.core import HexInt +from .. import uart_ns, validate_raw_data + +CODEOWNERS = ["@ssieb"] + +DEPENDENCIES = ["uart"] + +UARTButton = uart_ns.class_("UARTButton", button.Button, uart.UARTDevice, cg.Component) + + +CONFIG_SCHEMA = ( + button.button_schema(UARTButton) + .extend( + { + cv.Required(CONF_DATA): validate_raw_data, + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await button.new_button(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + data = config[CONF_DATA] + if isinstance(data, bytes): + data = [HexInt(x) for x in data] + cg.add(var.set_data(data)) diff --git a/esphome/components/uart/button/uart_button.cpp b/esphome/components/uart/button/uart_button.cpp new file mode 100644 index 0000000000..4db164c400 --- /dev/null +++ b/esphome/components/uart/button/uart_button.cpp @@ -0,0 +1,17 @@ +#include "uart_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart.button"; + +void UARTButton::press_action() { + ESP_LOGD(TAG, "'%s': Sending data...", this->get_name().c_str()); + this->write_array(this->data_.data(), this->data_.size()); +} + +void UARTButton::dump_config() { LOG_BUTTON("", "UART Button", this); } + +} // namespace uart +} // namespace esphome diff --git a/esphome/components/uart/button/uart_button.h b/esphome/components/uart/button/uart_button.h new file mode 100644 index 0000000000..2d600b199a --- /dev/null +++ b/esphome/components/uart/button/uart_button.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/button/button.h" + +#include + +namespace esphome { +namespace uart { + +class UARTButton : public button::Button, public UARTDevice, public Component { + public: + void set_data(const std::vector &data) { this->data_ = data; } + + void dump_config() override; + + protected: + void press_action() override; + std::vector data_; +}; + +} // namespace uart +} // namespace esphome diff --git a/esphome/components/uart/switch/__init__.py b/esphome/components/uart/switch/__init__.py index 60f5ddaf0d..8853a61ae3 100644 --- a/esphome/components/uart/switch/__init__.py +++ b/esphome/components/uart/switch/__init__.py @@ -9,14 +9,24 @@ DEPENDENCIES = ["uart"] UARTSwitch = uart_ns.class_("UARTSwitch", switch.Switch, uart.UARTDevice, cg.Component) +CONF_TURN_OFF = "turn_off" +CONF_TURN_ON = "turn_on" CONFIG_SCHEMA = ( switch.switch_schema(UARTSwitch, block_inverted=True) .extend( { - cv.Required(CONF_DATA): validate_raw_data, + cv.Required(CONF_DATA): cv.Any( + validate_raw_data, + cv.Schema( + { + cv.Optional(CONF_TURN_OFF): validate_raw_data, + cv.Optional(CONF_TURN_ON): validate_raw_data, + } + ), + ), cv.Optional(CONF_SEND_EVERY): cv.positive_time_period_milliseconds, - } + }, ) .extend(uart.UART_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA) @@ -29,9 +39,20 @@ async def to_code(config): await uart.register_uart_device(var, config) data = config[CONF_DATA] - if isinstance(data, bytes): - data = [HexInt(x) for x in data] - cg.add(var.set_data(data)) - + if isinstance(data, dict): + if data_on := data.get(CONF_TURN_ON): + if isinstance(data_on, bytes): + data_on = [HexInt(x) for x in data_on] + cg.add(var.set_data_on(data_on)) + if data_off := data.get(CONF_TURN_OFF): + if isinstance(data_off, bytes): + data_off = [HexInt(x) for x in data_off] + cg.add(var.set_data_off(data_off)) + else: + data = config[CONF_DATA] + if isinstance(data, bytes): + data = [HexInt(x) for x in data] + cg.add(var.set_data_on(data)) + cg.add(var.set_single_state(True)) if CONF_SEND_EVERY in config: cg.add(var.set_send_every(config[CONF_SEND_EVERY])) diff --git a/esphome/components/uart/switch/uart_switch.cpp b/esphome/components/uart/switch/uart_switch.cpp index ffed08c731..1edb54641b 100644 --- a/esphome/components/uart/switch/uart_switch.cpp +++ b/esphome/components/uart/switch/uart_switch.cpp @@ -7,28 +7,41 @@ namespace uart { static const char *const TAG = "uart.switch"; void UARTSwitch::loop() { - if (this->state && this->send_every_) { + if (this->send_every_) { const uint32_t now = millis(); if (now - this->last_transmission_ > this->send_every_) { - this->write_command_(); + this->write_command_(this->state); this->last_transmission_ = now; } } } -void UARTSwitch::write_command_() { - ESP_LOGD(TAG, "'%s': Sending data...", this->get_name().c_str()); - this->write_array(this->data_.data(), this->data_.size()); +void UARTSwitch::write_command_(bool state) { + if (state && !this->data_on_.empty()) { + ESP_LOGD(TAG, "'%s': Sending on data...", this->get_name().c_str()); + this->write_array(this->data_on_.data(), this->data_on_.size()); + } + if (!state && !this->data_off_.empty()) { + ESP_LOGD(TAG, "'%s': Sending off data...", this->get_name().c_str()); + this->write_array(this->data_off_.data(), this->data_off_.size()); + } } void UARTSwitch::write_state(bool state) { + if (!this->single_state_) { + this->publish_state(state); + this->write_command_(state); + this->last_transmission_ = millis(); + return; + } + if (!state) { this->publish_state(false); return; } this->publish_state(true); - this->write_command_(); + this->write_command_(true); if (this->send_every_ == 0) { this->publish_state(false); @@ -36,10 +49,11 @@ void UARTSwitch::write_state(bool state) { this->last_transmission_ = millis(); } } + void UARTSwitch::dump_config() { LOG_SWITCH("", "UART Switch", this); if (this->send_every_) { - ESP_LOGCONFIG(TAG, " Send Every: %u", this->send_every_); + ESP_LOGCONFIG(TAG, " Send Every: %" PRIu32, this->send_every_); } } diff --git a/esphome/components/uart/switch/uart_switch.h b/esphome/components/uart/switch/uart_switch.h index 4f24d76d0c..4ef5b6da4b 100644 --- a/esphome/components/uart/switch/uart_switch.h +++ b/esphome/components/uart/switch/uart_switch.h @@ -4,6 +4,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/components/switch/switch.h" +#include #include namespace esphome { @@ -13,15 +14,19 @@ class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { public: void loop() override; - void set_data(const std::vector &data) { data_ = data; } + void set_data_on(const std::vector &data) { this->data_on_ = data; } + void set_data_off(const std::vector &data) { this->data_off_ = data; } void set_send_every(uint32_t send_every) { this->send_every_ = send_every; } + void set_single_state(bool single) { this->single_state_ = single; } void dump_config() override; protected: - void write_command_(); + void write_command_(bool state); void write_state(bool state) override; - std::vector data_; + std::vector data_on_; + std::vector data_off_; + bool single_state_{false}; uint32_t send_every_; uint32_t last_transmission_; }; diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 22a22e2772..9834462ff9 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -3,6 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include namespace esphome { namespace uart { @@ -12,8 +13,8 @@ static const char *const TAG = "uart"; void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, uint8_t data_bits) { if (this->parent_->get_baud_rate() != baud_rate) { - ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, - this->parent_->get_baud_rate()); + ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %" PRIu32 " but you have %" PRIu32 "!", + baud_rate, this->parent_->get_baud_rate()); } if (this->parent_->get_stop_bits() != stop_bits) { ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits, diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 42702cf5b8..a57910c1a1 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -31,38 +31,124 @@ const LogString *parity_to_str(UARTParityOptions parity); class UARTComponent { public: + // Writes an array of bytes to the UART bus. + // @param data A vector of bytes to be written. void write_array(const std::vector &data) { this->write_array(&data[0], data.size()); } + + // Writes a single byte to the UART bus. + // @param data The byte to be written. void write_byte(uint8_t data) { this->write_array(&data, 1); }; + + // Writes a null-terminated string to the UART bus. + // @param str Pointer to the null-terminated string. void write_str(const char *str) { const auto *data = reinterpret_cast(str); this->write_array(data, strlen(str)); }; + // Pure virtual method to write an array of bytes to the UART bus. + // @param data Pointer to the array of bytes. + // @param len Length of the array. virtual void write_array(const uint8_t *data, size_t len) = 0; + // Reads a single byte from the UART bus. + // @param data Pointer to the byte where the read data will be stored. + // @return True if a byte was successfully read, false otherwise. bool read_byte(uint8_t *data) { return this->read_array(data, 1); }; + + // Pure virtual method to peek the next byte in the UART buffer without removing it. + // @param data Pointer to the byte where the peeked data will be stored. + // @return True if a byte is available to peek, false otherwise. virtual bool peek_byte(uint8_t *data) = 0; + + // Pure virtual method to read an array of bytes from the UART bus. + // @param data Pointer to the array where the read data will be stored. + // @param len Number of bytes to read. + // @return True if the specified number of bytes were successfully read, false otherwise. virtual bool read_array(uint8_t *data, size_t len) = 0; - /// Return available number of bytes. + // Pure virtual method to return the number of bytes available for reading. + // @return Number of available bytes. virtual int available() = 0; - /// Block until all bytes have been written to the UART bus. + + // Pure virtual method to block until all bytes have been written to the UART bus. virtual void flush() = 0; + // Sets the TX (transmit) pin for the UART bus. + // @param tx_pin Pointer to the internal GPIO pin used for transmission. void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; } + + // Sets the RX (receive) pin for the UART bus. + // @param rx_pin Pointer to the internal GPIO pin used for reception. void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; } + + // Sets the size of the RX buffer. + // @param rx_buffer_size Size of the RX buffer in bytes. void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } + + // Gets the size of the RX buffer. + // @return Size of the RX buffer in bytes. size_t get_rx_buffer_size() { return this->rx_buffer_size_; } + // Sets the number of stop bits used in UART communication. + // @param stop_bits Number of stop bits. void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } + + // Gets the number of stop bits used in UART communication. + // @return Number of stop bits. uint8_t get_stop_bits() const { return this->stop_bits_; } + + // Set the number of data bits used in UART communication. + // @param data_bits Number of data bits. void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } + + // Get the number of data bits used in UART communication. + // @return Number of data bits. uint8_t get_data_bits() const { return this->data_bits_; } + + // Set the parity used in UART communication. + // @param parity Parity option. void set_parity(UARTParityOptions parity) { this->parity_ = parity; } + + // Get the parity used in UART communication. + // @return Parity option. UARTParityOptions get_parity() const { return this->parity_; } + + // Set the baud rate for UART communication. + // @param baud_rate Baud rate in bits per second. void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } + + // Get the baud rate for UART communication. + // @return Baud rate in bits per second. uint32_t get_baud_rate() const { return baud_rate_; } +#if defined(USE_ESP8266) || defined(USE_ESP32) + /** + * Load the UART settings. + * @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly. + * + * Example: + * ```cpp + * id(uart1).load_settings(false); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + virtual void load_settings(bool dump_config){}; + + /** + * Load the UART settings. + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + virtual void load_settings(){}; +#endif // USE_ESP8266 || USE_ESP32 + #ifdef USE_UART_DEBUGGER void add_debug_callback(std::function &&callback) { this->debug_callback_.add(std::move(callback)); diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 7306dd2f31..793c1d52f4 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -88,14 +88,32 @@ void ESP32ArduinoUARTComponent::setup() { #endif static uint8_t next_uart_num = 0; if (is_default_tx && is_default_rx && next_uart_num == 0) { +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial0; +#else this->hw_serial_ = &Serial; +#endif next_uart_num++; } else { #ifdef USE_LOGGER - // The logger doesn't use this UART component, instead it targets the UARTs - // directly (i.e. Serial/Serial0, Serial1, and Serial2). If the logger is - // enabled, skip the UART that it is configured to use. - if (logger::global_logger->get_baud_rate() > 0 && logger::global_logger->get_uart() == next_uart_num) { + bool logger_uses_hardware_uart = true; + +#ifdef USE_LOGGER_USB_CDC + if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_CDC) { + // this is not a hardware UART, ignore it + logger_uses_hardware_uart = false; + } +#endif // USE_LOGGER_USB_CDC + +#ifdef USE_LOGGER_USB_SERIAL_JTAG + if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_SERIAL_JTAG) { + // this is not a hardware UART, ignore it + logger_uses_hardware_uart = false; + } +#endif // USE_LOGGER_USB_SERIAL_JTAG + + if (logger_uses_hardware_uart && logger::global_logger->get_baud_rate() > 0 && + logger::global_logger->get_uart() == next_uart_num) { next_uart_num++; } #endif // USE_LOGGER @@ -109,6 +127,11 @@ void ESP32ArduinoUARTComponent::setup() { this->number_ = next_uart_num; this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory) } + + this->load_settings(false); +} + +void ESP32ArduinoUARTComponent::load_settings(bool dump_config) { 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; bool invert = false; @@ -118,6 +141,10 @@ void ESP32ArduinoUARTComponent::setup() { invert = true; this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert); + if (dump_config) { + ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->number_); + this->dump_config(); + } } void ESP32ArduinoUARTComponent::dump_config() { diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h index 02dfd0531e..de17d9718b 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -32,6 +32,21 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component { HardwareSerial *get_hw_serial() { return this->hw_serial_; } uint8_t get_hw_serial_number() { return this->number_; } + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } + protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 529108f439..fa8dc3fb17 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -98,10 +98,26 @@ void ESP8266UartComponent::setup() { } } +void ESP8266UartComponent::load_settings(bool dump_config) { + ESP_LOGCONFIG(TAG, "Loading UART bus settings..."); + if (this->hw_serial_ != nullptr) { + SerialConfig config = static_cast(get_config()); + this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); + } else { + this->sw_serial_->setup(this->tx_pin_, this->rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, + this->parity_, this->rx_buffer_size_); + } + if (dump_config) { + ESP_LOGCONFIG(TAG, "UART bus was reloaded."); + this->dump_config(); + } +} + void ESP8266UartComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus:"); - LOG_PIN(" TX Pin: ", tx_pin_); - LOG_PIN(" RX Pin: ", rx_pin_); + LOG_PIN(" TX Pin: ", this->tx_pin_); + LOG_PIN(" RX Pin: ", this->rx_pin_); if (this->rx_pin_ != nullptr) { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT } diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index eed14f3265..749dd4c61e 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -63,6 +63,21 @@ class ESP8266UartComponent : public UARTComponent, public Component { uint32_t get_config(); + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } + protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 1560409772..6999dfb619 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -47,7 +48,11 @@ uart_config_t IDFUARTComponent::get_config_() { uart_config.parity = parity; uart_config.stop_bits = this->stop_bits_ == 1 ? UART_STOP_BITS_1 : UART_STOP_BITS_2; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + uart_config.source_clk = UART_SCLK_DEFAULT; +#else uart_config.source_clk = UART_SCLK_APB; +#endif uart_config.rx_flow_ctrl_thresh = 122; return uart_config; @@ -55,16 +60,36 @@ uart_config_t IDFUARTComponent::get_config_() { void IDFUARTComponent::setup() { static uint8_t next_uart_num = 0; + #ifdef USE_LOGGER - if (logger::global_logger->get_uart_num() == next_uart_num) + bool logger_uses_hardware_uart = true; + +#ifdef USE_LOGGER_USB_CDC + if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_CDC) { + // this is not a hardware UART, ignore it + logger_uses_hardware_uart = false; + } +#endif // USE_LOGGER_USB_CDC + +#ifdef USE_LOGGER_USB_SERIAL_JTAG + if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_SERIAL_JTAG) { + // this is not a hardware UART, ignore it + logger_uses_hardware_uart = false; + } +#endif // USE_LOGGER_USB_SERIAL_JTAG + + if (logger_uses_hardware_uart && logger::global_logger->get_baud_rate() > 0 && + logger::global_logger->get_uart_num() == next_uart_num) { next_uart_num++; -#endif + } +#endif // USE_LOGGER + if (next_uart_num >= UART_NUM_MAX) { ESP_LOGW(TAG, "Maximum number of UART components created already."); this->mark_failed(); return; } - this->uart_num_ = next_uart_num++; + this->uart_num_ = static_cast(next_uart_num++); ESP_LOGCONFIG(TAG, "Setting up UART %u...", this->uart_num_); this->lock_ = xSemaphoreCreateMutex(); @@ -79,6 +104,29 @@ void IDFUARTComponent::setup() { 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; + + uint32_t invert = 0; + if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) + invert |= UART_SIGNAL_TXD_INV; + if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) + invert |= UART_SIGNAL_RXD_INV; + + err = uart_set_line_inverse(this->uart_num_, invert); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + err = uart_set_pin(this->uart_num_, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will block task until all data have been sent out.*/ @@ -91,41 +139,30 @@ void IDFUARTComponent::setup() { 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; - - err = uart_set_pin(this->uart_num_, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - - uint32_t invert = 0; - if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) - invert |= UART_SIGNAL_TXD_INV; - if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) - invert |= UART_SIGNAL_RXD_INV; - - err = uart_set_line_inverse(this->uart_num_, invert); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - xSemaphoreGive(this->lock_); } +void IDFUARTComponent::load_settings(bool dump_config) { + uart_config_t uart_config = this->get_config_(); + esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } else if (dump_config) { + ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); + this->dump_config(); + } +} + void IDFUARTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "UART Bus:"); - ESP_LOGCONFIG(TAG, " Number: %u", this->uart_num_); + ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); LOG_PIN(" TX Pin: ", tx_pin_); LOG_PIN(" RX Pin: ", rx_pin_); if (this->rx_pin_ != nullptr) { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); } - ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Baud Rate: %" PRIu32 " baud", this->baud_rate_); ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_))); ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); @@ -150,7 +187,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { if (this->has_peek_) { *data = this->peek_byte_; } else { - int len = uart_read_bytes(this->uart_num_, data, 1, 20 / portTICK_RATE_MS); + int len = uart_read_bytes(this->uart_num_, data, 1, 20 / portTICK_PERIOD_MS); if (len == 0) { *data = 0; } else { @@ -174,7 +211,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; } if (length_to_read > 0) - uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_RATE_MS); + uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index fdaa4da9a7..215641ebe2 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -26,6 +26,21 @@ class IDFUARTComponent : public UARTComponent, public Component { uint8_t get_hw_serial_number() { return this->uart_num_; } QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } + protected: void check_logger_conflict() override; uart_port_t uart_num_; diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp new file mode 100644 index 0000000000..40d3e91ab2 --- /dev/null +++ b/esphome/components/uart/uart_component_host.cpp @@ -0,0 +1,299 @@ +#ifdef USE_HOST +#include "uart_component_host.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#if !(defined(__linux__) || defined(__APPLE__)) +#error This HostUartComponent implementation is not supported on this host OS +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +namespace { + +speed_t get_baud(int baud) { +#ifdef __APPLE__ + return baud; +#else + switch (baud) { + case 50: + return B50; + case 75: + return B75; + case 110: + return B110; + case 134: + return B134; + case 150: + return B150; + case 200: + return B200; + case 300: + return B300; + case 600: + return B600; + case 1200: + return B1200; + case 1800: + return B1800; + case 2400: + return B2400; + case 4800: + return B4800; + case 9600: + return B9600; + case 19200: + return B19200; + case 38400: + return B38400; + case 57600: + return B57600; + case 115200: + return B115200; + case 230400: + return B230400; + case 460800: + return B460800; + case 500000: + return B500000; + case 576000: + return B576000; + case 921600: + return B921600; + case 1000000: + return B1000000; + case 1152000: + return B1152000; + case 1500000: + return B1500000; + case 2000000: + return B2000000; + case 2500000: + return B2500000; + case 3000000: + return B3000000; + case 3500000: + return B3500000; + case 4000000: + return B4000000; + default: + return B0; + } +#endif +} + +} // namespace + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart.host"; + +HostUartComponent::~HostUartComponent() { + if (this->file_descriptor_ != -1) { + close(this->file_descriptor_); + this->file_descriptor_ = -1; + } +} + +void HostUartComponent::setup() { + ESP_LOGCONFIG(TAG, "Opening UART port..."); + speed_t baud = get_baud(this->baud_rate_); + if (baud == B0) { + ESP_LOGE(TAG, "Unsupported baud rate: %d", this->baud_rate_); + this->mark_failed(); + return; + } + this->file_descriptor_ = ::open(this->port_name_.c_str(), O_RDWR | O_NOCTTY | O_NDELAY); + if (this->file_descriptor_ == -1) { + this->update_error_(strerror(errno)); + this->mark_failed(); + return; + } + fcntl(this->file_descriptor_, F_SETFL, 0); + struct termios options; + tcgetattr(this->file_descriptor_, &options); + options.c_cflag &= ~CRTSCTS; + options.c_cflag |= CREAD | CLOCAL; + options.c_lflag &= ~ICANON; + options.c_lflag &= ~ECHO; + options.c_lflag &= ~ECHOE; + options.c_lflag &= ~ECHONL; + options.c_lflag &= ~ISIG; + options.c_iflag &= ~(IXON | IXOFF | IXANY); + options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); + options.c_oflag &= ~OPOST; + options.c_oflag &= ~ONLCR; + // Set data bits + options.c_cflag &= ~CSIZE; // Mask the character size bits + switch (this->data_bits_) { + case 5: + options.c_cflag |= CS5; + break; + case 6: + options.c_cflag |= CS6; + break; + case 7: + options.c_cflag |= CS7; + break; + case 8: + default: + options.c_cflag |= CS8; + break; + } + // Set parity + switch (this->parity_) { + case UART_CONFIG_PARITY_NONE: + options.c_cflag &= ~PARENB; + break; + case UART_CONFIG_PARITY_EVEN: + options.c_cflag |= PARENB; + options.c_cflag &= ~PARODD; + break; + case UART_CONFIG_PARITY_ODD: + options.c_cflag |= PARENB; + options.c_cflag |= PARODD; + break; + }; + // Set stop bits + if (this->stop_bits_ == 2) { + options.c_cflag |= CSTOPB; + } else { + options.c_cflag &= ~CSTOPB; + } + cfsetispeed(&options, baud); + cfsetospeed(&options, baud); + tcsetattr(this->file_descriptor_, TCSANOW, &options); +} + +void HostUartComponent::dump_config() { + ESP_LOGCONFIG(TAG, "UART:"); + ESP_LOGCONFIG(TAG, " Port: %s", this->port_name_.c_str()); + if (this->file_descriptor_ == -1) { + ESP_LOGCONFIG(TAG, " Port status: Not opened"); + if (!this->first_error_.empty()) { + ESP_LOGCONFIG(TAG, " Error: %s", this->first_error_.c_str()); + } + return; + } + ESP_LOGCONFIG(TAG, " Port status: opened"); + ESP_LOGCONFIG(TAG, " Baud Rate: %d", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Data Bits: %d", this->data_bits_); + ESP_LOGCONFIG(TAG, " Parity: %s", + this->parity_ == UART_CONFIG_PARITY_NONE ? "None" + : this->parity_ == UART_CONFIG_PARITY_EVEN ? "Even" + : "Odd"); + ESP_LOGCONFIG(TAG, " Stop Bits: %d", this->stop_bits_); + this->check_logger_conflict(); +} + +void HostUartComponent::write_array(const uint8_t *data, size_t len) { + if (this->file_descriptor_ == -1) { + return; + } + size_t written = ::write(this->file_descriptor_, data, len); + if (written != len) { + this->update_error_(strerror(errno)); + return; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); + } +#endif + return; +} + +bool HostUartComponent::peek_byte(uint8_t *data) { + if (this->file_descriptor_ == -1) { + return false; + } + if (!this->has_peek_) { + if (!this->check_read_timeout_()) { + return false; + } + if (::read(this->file_descriptor_, &this->peek_byte_, 1) != 1) { + this->update_error_(strerror(errno)); + return false; + } + this->has_peek_ = true; + } + *data = this->peek_byte_; + return true; +} + +bool HostUartComponent::read_array(uint8_t *data, size_t len) { + if ((this->file_descriptor_ == -1) || (len == 0)) { + return false; + } + if (!this->check_read_timeout_(len)) + return false; + uint8_t *data_ptr = data; + size_t length_to_read = len; + if (this->has_peek_) { + length_to_read--; + *data_ptr = this->peek_byte_; + data_ptr++; + this->has_peek_ = false; + } + if (length_to_read > 0) { + int sz = ::read(this->file_descriptor_, data_ptr, length_to_read); + if (sz == -1) { + this->update_error_(strerror(errno)); + return false; + } + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); + } +#endif + return true; +} + +int HostUartComponent::available() { + if (this->file_descriptor_ == -1) { + return 0; + } + int available; + int res = ioctl(this->file_descriptor_, FIONREAD, &available); + if (res == -1) { + this->update_error_(strerror(errno)); + return 0; + } + if (this->has_peek_) + available++; + return available; +}; + +void HostUartComponent::flush() { + if (this->file_descriptor_ == -1) { + return; + } + tcflush(this->file_descriptor_, TCIOFLUSH); + ESP_LOGV(TAG, " Flushing..."); +} + +void HostUartComponent::update_error_(const std::string &error) { + if (this->first_error_.empty()) { + this->first_error_ = error; + } + ESP_LOGE(TAG, "Port error: %s", error.c_str()); +} + +} // namespace uart +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h new file mode 100644 index 0000000000..c1f1dd0d2c --- /dev/null +++ b/esphome/components/uart/uart_component_host.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_HOST + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "uart_component.h" + +namespace esphome { +namespace uart { + +class HostUartComponent : public UARTComponent, public Component { + public: + virtual ~HostUartComponent(); + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + 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; + void set_name(std::string port_name) { port_name_ = port_name; }; + + protected: + void update_error_(const std::string &error); + void check_logger_conflict() override {} + std::string port_name_; + std::string first_error_{""}; + int file_descriptor_ = -1; + bool has_peek_{false}; + uint8_t peek_byte_; +}; + +} // namespace uart +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp new file mode 100644 index 0000000000..c5e299e9d1 --- /dev/null +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -0,0 +1,168 @@ +#ifdef USE_LIBRETINY + +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "uart_component_libretiny.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +#if LT_ARD_HAS_SOFTSERIAL +#include +#endif + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart.lt"; + +static const char *UART_TYPE[] = { + "hardware", + "software", +}; + +uint16_t LibreTinyUARTComponent::get_config() { + uint16_t config = 0; + + switch (this->parity_) { + case UART_CONFIG_PARITY_NONE: + config |= SERIAL_PARITY_NONE; + break; + case UART_CONFIG_PARITY_EVEN: + config |= SERIAL_PARITY_EVEN; + break; + case UART_CONFIG_PARITY_ODD: + config |= SERIAL_PARITY_ODD; + break; + } + + config |= (this->data_bits_ - 4) << 8; + config |= 0x10 + (this->stop_bits_ - 1) * 0x20; + + return config; +} + +void LibreTinyUARTComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up UART..."); + + int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); + int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); + bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); + bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted(); + + if (false) + return; +#if LT_HW_UART0 + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) { + this->serial_ = &Serial0; + this->hardware_idx_ = 0; + } +#endif +#if LT_HW_UART1 + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX)) { + this->serial_ = &Serial1; + this->hardware_idx_ = 1; + } +#endif +#if LT_HW_UART2 + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX)) { + this->serial_ = &Serial2; + this->hardware_idx_ = 2; + } +#endif + else { +#if LT_ARD_HAS_SOFTSERIAL + this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted); +#else + this->serial_ = &Serial; + ESP_LOGE(TAG, " SoftwareSerial is not implemented for this chip. Only hardware pins are supported:"); +#if LT_HW_UART0 + ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL0_TX, PIN_SERIAL0_RX); +#endif +#if LT_HW_UART1 + ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL1_TX, PIN_SERIAL1_RX); +#endif +#if LT_HW_UART2 + ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL2_TX, PIN_SERIAL2_RX); +#endif + this->mark_failed(); + return; +#endif + } + + this->serial_->begin(this->baud_rate_, get_config()); +} + +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]); + if (!is_software) { + ESP_LOGCONFIG(TAG, " Port number: %d", this->hardware_idx_); + } + LOG_PIN(" TX Pin: ", tx_pin_); + LOG_PIN(" RX Pin: ", rx_pin_); + if (this->rx_pin_ != nullptr) { + ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); + } + ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); + ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_))); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); + this->check_logger_conflict(); +} + +void LibreTinyUARTComponent::write_array(const uint8_t *data, size_t len) { + this->serial_->write(data, len); +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); + } +#endif +} + +bool LibreTinyUARTComponent::peek_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + *data = this->serial_->peek(); + return true; +} + +bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { + if (!this->check_read_timeout_(len)) + return false; + this->serial_->readBytes(data, len); +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); + } +#endif + return true; +} + +int LibreTinyUARTComponent::available() { return this->serial_->available(); } +void LibreTinyUARTComponent::flush() { + ESP_LOGVV(TAG, " Flushing..."); + this->serial_->flush(); +} + +void LibreTinyUARTComponent::check_logger_conflict() { +#ifdef USE_LOGGER + if (this->hardware_idx_ == -1 || logger::global_logger->get_baud_rate() == 0) { + return; + } + + if (this->serial_ == logger::global_logger->get_hw_serial()) { + ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " + "disable logging over the serial port by setting logger->baud_rate to 0."); + } +#endif +} + +} // namespace uart +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h new file mode 100644 index 0000000000..00982fd297 --- /dev/null +++ b/esphome/components/uart/uart_component_libretiny.h @@ -0,0 +1,43 @@ +#pragma once + +#ifdef USE_LIBRETINY + +#include +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "uart_component.h" + +namespace esphome { +namespace uart { + +class LibreTinyUARTComponent : public UARTComponent, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + 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; + + uint16_t get_config(); + + HardwareSerial *get_hw_serial() { return this->serial_; } + int8_t get_hw_serial_number() { return this->hardware_idx_; } + + protected: + void check_logger_conflict() override; + + HardwareSerial *serial_{nullptr}; + int8_t hardware_idx_{-1}; +}; + +} // namespace uart +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py new file mode 100644 index 0000000000..ca15be2a80 --- /dev/null +++ b/esphome/components/udp/__init__.py @@ -0,0 +1,158 @@ +import hashlib + +import esphome.codegen as cg +from esphome.components.api import CONF_ENCRYPTION +from esphome.components.binary_sensor import BinarySensor +from esphome.components.sensor import Sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BINARY_SENSORS, + CONF_ID, + CONF_INTERNAL, + CONF_KEY, + CONF_NAME, + CONF_PORT, + CONF_SENSORS, +) +from esphome.cpp_generator import MockObjClass + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["network"] +AUTO_LOAD = ["socket"] +MULTI_CONF = True + +udp_ns = cg.esphome_ns.namespace("udp") +UDPComponent = udp_ns.class_("UDPComponent", cg.PollingComponent) + +CONF_BROADCAST = "broadcast" +CONF_BROADCAST_ID = "broadcast_id" +CONF_ADDRESSES = "addresses" +CONF_PROVIDER = "provider" +CONF_PROVIDERS = "providers" +CONF_REMOTE_ID = "remote_id" +CONF_UDP_ID = "udp_id" +CONF_PING_PONG_ENABLE = "ping_pong_enable" +CONF_PING_PONG_RECYCLE_TIME = "ping_pong_recycle_time" +CONF_ROLLING_CODE_ENABLE = "rolling_code_enable" + + +def sensor_validation(cls: MockObjClass): + return cv.maybe_simple_value( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(cls), + cv.Optional(CONF_BROADCAST_ID): cv.validate_id_name, + } + ), + key=CONF_ID, + ) + + +ENCRYPTION_SCHEMA = { + cv.Optional(CONF_ENCRYPTION): cv.maybe_simple_value( + cv.Schema( + { + cv.Required(CONF_KEY): cv.string, + } + ), + key=CONF_KEY, + ) +} + +PROVIDER_SCHEMA = cv.Schema( + { + cv.Required(CONF_NAME): cv.valid_name, + } +).extend(ENCRYPTION_SCHEMA) + + +def validate_(config): + if CONF_ENCRYPTION in config: + if CONF_SENSORS not in config and CONF_BINARY_SENSORS not in config: + raise cv.Invalid("No sensors or binary sensors to encrypt") + elif config[CONF_ROLLING_CODE_ENABLE]: + raise cv.Invalid("Rolling code requires an encryption key") + if config[CONF_PING_PONG_ENABLE]: + if not any(CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or ()): + raise cv.Invalid("Ping-pong requires at least one encrypted provider") + return config + + +CONFIG_SCHEMA = cv.All( + cv.polling_component_schema("15s") + .extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.port, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4 + ), + cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean, + cv.Optional( + CONF_PING_PONG_RECYCLE_TIME, default="600s" + ): cv.positive_time_period_seconds, + cv.Optional(CONF_SENSORS): cv.ensure_list(sensor_validation(Sensor)), + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( + sensor_validation(BinarySensor) + ), + cv.Optional(CONF_PROVIDERS): cv.ensure_list(PROVIDER_SCHEMA), + }, + ) + .extend(ENCRYPTION_SCHEMA), + validate_, +) + +SENSOR_SCHEMA = cv.Schema( + { + cv.Optional(CONF_REMOTE_ID): cv.string_strict, + cv.Required(CONF_PROVIDER): cv.valid_name, + cv.GenerateID(CONF_UDP_ID): cv.use_id(UDPComponent), + } +) + + +def require_internal_with_name(config): + if CONF_NAME in config and CONF_INTERNAL not in config: + raise cv.Invalid("Must provide internal: config when using name:") + return config + + +def hash_encryption_key(config: dict): + return list(hashlib.sha256(config[CONF_KEY].encode()).digest()) + + +async def to_code(config): + cg.add_define("USE_UDP") + cg.add_global(udp_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_port(config[CONF_PORT])) + cg.add(var.set_rolling_code_enable(config[CONF_ROLLING_CODE_ENABLE])) + cg.add(var.set_ping_pong_enable(config[CONF_PING_PONG_ENABLE])) + cg.add( + var.set_ping_pong_recycle_time( + config[CONF_PING_PONG_RECYCLE_TIME].total_seconds + ) + ) + for sens_conf in config.get(CONF_SENSORS, ()): + 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, ()): + 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)) + for address in config[CONF_ADDRESSES]: + cg.add(var.add_address(str(address))) + + if encryption := config.get(CONF_ENCRYPTION): + cg.add(var.set_encryption_key(hash_encryption_key(encryption))) + + for provider in config.get(CONF_PROVIDERS, ()): + name = provider[CONF_NAME] + cg.add(var.add_provider(name)) + if encryption := provider.get(CONF_ENCRYPTION): + cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) diff --git a/esphome/components/udp/binary_sensor.py b/esphome/components/udp/binary_sensor.py new file mode 100644 index 0000000000..d90e495527 --- /dev/null +++ b/esphome/components/udp/binary_sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +from esphome.config_validation import All, has_at_least_one_key +from esphome.const import CONF_ID + +from . import ( + CONF_PROVIDER, + CONF_REMOTE_ID, + CONF_UDP_ID, + SENSOR_SCHEMA, + require_internal_with_name, +) + +DEPENDENCIES = ["udp"] + +CONFIG_SCHEMA = All( + binary_sensor.binary_sensor_schema().extend(SENSOR_SCHEMA), + has_at_least_one_key(CONF_ID, CONF_REMOTE_ID), + require_internal_with_name, +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + comp = await cg.get_variable(config[CONF_UDP_ID]) + remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) + cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/udp/sensor.py b/esphome/components/udp/sensor.py new file mode 100644 index 0000000000..860c277c44 --- /dev/null +++ b/esphome/components/udp/sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components.sensor import new_sensor, sensor_schema +from esphome.config_validation import All, has_at_least_one_key +from esphome.const import CONF_ID + +from . import ( + CONF_PROVIDER, + CONF_REMOTE_ID, + CONF_UDP_ID, + SENSOR_SCHEMA, + require_internal_with_name, +) + +DEPENDENCIES = ["udp"] + +CONFIG_SCHEMA = All( + sensor_schema().extend(SENSOR_SCHEMA), + has_at_least_one_key(CONF_ID, CONF_REMOTE_ID), + require_internal_with_name, +) + + +async def to_code(config): + var = await new_sensor(config) + comp = await cg.get_variable(config[CONF_UDP_ID]) + remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) + cg.add(comp.add_remote_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp new file mode 100644 index 0000000000..a1c8889997 --- /dev/null +++ b/esphome/components/udp/udp_component.cpp @@ -0,0 +1,619 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/network/util.h" +#include "udp_component.h" + +namespace esphome { +namespace udp { + +/** + * Structure of a data packet; everything is little-endian + * + * --- In clear text --- + * MAGIC_NUMBER: 16 bits + * host name length: 1 byte + * host name: (length) bytes + * padding: 0 or more null bytes to a 4 byte boundary + * + * --- Encrypted (if key set) ---- + * DATA_KEY: 1 byte: OR ROLLING_CODE_KEY: + * Rolling code (if enabled): 8 bytes + * Ping keys: if any + * repeat: + * PING_KEY: 1 byte + * ping code: 4 bytes + * Sensors: + * repeat: + * SENSOR_KEY: 1 byte + * float value: 4 bytes + * name length: 1 byte + * name + * Binary Sensors: + * repeat: + * BINARY_SENSOR_KEY: 1 byte + * bool value: 1 bytes + * name length: 1 byte + * name + * + * Padded to a 4 byte boundary with nulls + * + * Structure of a ping request packet: + * --- In clear text --- + * MAGIC_PING: 16 bits + * host name length: 1 byte + * host name: (length) bytes + * Ping key (4 bytes) + * + */ +static const char *const TAG = "udp"; + +/** + * XXTEA implementation, using 256 bit key. + */ + +static const uint32_t DELTA = 0x9e3779b9; +#define MX ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p ^ e) & 7] ^ z))) + +/** + * Encrypt a block of data in-place + */ + +static void xxtea_encrypt(uint32_t *v, size_t n, const uint32_t *k) { + uint32_t z, y, sum, e; + size_t p; + size_t q = 6 + 52 / n; + sum = 0; + z = v[n - 1]; + while (q-- != 0) { + sum += DELTA; + e = (sum >> 2); + for (p = 0; p != n - 1; p++) { + y = v[p + 1]; + z = v[p] += MX; + } + y = v[0]; + z = v[n - 1] += MX; + } +} + +static void xxtea_decrypt(uint32_t *v, size_t n, const uint32_t *k) { + uint32_t z, y, sum, e; + size_t p; + size_t q = 6 + 52 / n; + sum = q * DELTA; + y = v[0]; + while (q-- != 0) { + e = (sum >> 2); + for (p = n - 1; p != 0; p--) { + z = v[p - 1]; + y = v[p] -= MX; + } + z = v[n - 1]; + y = v[0] -= MX; + sum -= DELTA; + } +} + +inline static size_t round4(size_t value) { return (value + 3) & ~3; } + +union FuData { + uint32_t u32; + float f32; +}; + +static const size_t MAX_PACKET_SIZE = 508; +static const uint16_t MAGIC_NUMBER = 0x4553; +static const uint16_t MAGIC_PING = 0x5048; +static const uint32_t PREF_HASH = 0x45535043; +enum DataKey { + ZERO_FILL_KEY, + DATA_KEY, + SENSOR_KEY, + BINARY_SENSOR_KEY, + PING_KEY, + ROLLING_CODE_KEY, +}; + +static const size_t MAX_PING_KEYS = 4; + +static inline void add(std::vector &vec, uint32_t data) { + vec.push_back(data & 0xFF); + vec.push_back((data >> 8) & 0xFF); + vec.push_back((data >> 16) & 0xFF); + vec.push_back((data >> 24) & 0xFF); +} + +static inline uint32_t get_uint32(uint8_t *&buf) { + uint32_t data = *buf++; + data += *buf++ << 8; + data += *buf++ << 16; + data += *buf++ << 24; + return data; +} + +static inline uint16_t get_uint16(uint8_t *&buf) { + uint16_t data = *buf++; + data += *buf++ << 8; + return data; +} + +static inline void add(std::vector &vec, uint8_t data) { vec.push_back(data); } +static inline void add(std::vector &vec, uint16_t data) { + vec.push_back((uint8_t) data); + vec.push_back((uint8_t) (data >> 8)); +} +static inline void add(std::vector &vec, DataKey data) { vec.push_back(data); } +static void add(std::vector &vec, const char *str) { + auto len = strlen(str); + vec.push_back(len); + for (size_t i = 0; i != len; i++) { + vec.push_back(*str++); + } +} + +void UDPComponent::setup() { + this->name_ = App.get_name().c_str(); + if (strlen(this->name_) > 255) { + this->mark_failed(); + this->status_set_error("Device name exceeds 255 chars"); + return; + } + this->resend_ping_key_ = this->ping_pong_enable_; + // restore the upper 32 bits of the rolling code, increment and save. + this->pref_ = global_preferences->make_preference(PREF_HASH, true); + this->pref_.load(&this->rolling_code_[1]); + this->rolling_code_[1]++; + this->pref_.save(&this->rolling_code_[1]); + this->ping_key_ = random_uint32(); + ESP_LOGV(TAG, "Rolling code incremented, upper part now %u", (unsigned) this->rolling_code_[1]); +#ifdef USE_SENSOR + for (auto &sensor : this->sensors_) { + sensor.sensor->add_on_state_callback([this, &sensor](float x) { + this->updated_ = true; + sensor.updated = true; + }); + } +#endif +#ifdef USE_BINARY_SENSOR + for (auto &sensor : this->binary_sensors_) { + sensor.sensor->add_on_state_callback([this, &sensor](bool value) { + this->updated_ = true; + sensor.updated = true; + }); + } +#endif + this->should_send_ = this->ping_pong_enable_; +#ifdef USE_SENSOR + this->should_send_ |= !this->sensors_.empty(); +#endif +#ifdef USE_BINARY_SENSOR + this->should_send_ |= !this->binary_sensors_.empty(); +#endif + this->should_listen_ = !this->providers_.empty() || this->is_encrypted_(); + // initialise the header. This is invariant. + add(this->header_, MAGIC_NUMBER); + add(this->header_, this->name_); + // pad to a multiple of 4 bytes + while (this->header_.size() & 0x3) + this->header_.push_back(0); +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + for (const auto &address : this->addresses_) { + struct sockaddr saddr {}; + socket::set_sockaddr(&saddr, sizeof(saddr), address, this->port_); + this->sockaddrs_.push_back(saddr); + } + // set up broadcast socket + if (this->should_send_) { + this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (this->broadcast_socket_ == nullptr) { + this->mark_failed(); + this->status_set_error("Could not create socket"); + return; + } + int enable = 1; + auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + this->status_set_warning("Socket unable to set reuseaddr"); + // we can still continue + } + err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); + if (err != 0) { + this->status_set_warning("Socket unable to set broadcast"); + } + } + // create listening socket if we either want to subscribe to providers, or need to listen + // for ping key broadcasts. + if (this->should_listen_) { + this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (this->listen_socket_ == nullptr) { + 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->mark_failed(); + this->status_set_error("Unable to set nonblocking"); + return; + } + int enable = 1; + err = this->listen_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); + if (err != 0) { + this->status_set_warning("Socket unable to set reuseaddr"); + // we can still continue + } + struct sockaddr_in server {}; + + socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); + if (sl == 0) { + ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno); + this->mark_failed(); + this->status_set_error("Unable to set sockaddr"); + return; + } + + err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); + this->status_set_error("Unable to bind socket"); + return; + } + } +#endif +#ifdef USE_SOCKET_IMPL_LWIP_TCP + // 8266 and RP2040 `Duino + for (const auto &address : this->addresses_) { + auto ipaddr = IPAddress(); + ipaddr.fromString(address.c_str()); + this->ipaddrs_.push_back(ipaddr); + } + if (this->should_listen_) + this->udp_client_.begin(this->port_); +#endif +} + +void UDPComponent::init_data_() { + this->data_.clear(); + if (this->rolling_code_enable_) { + add(this->data_, ROLLING_CODE_KEY); + add(this->data_, this->rolling_code_[0]); + add(this->data_, this->rolling_code_[1]); + this->increment_code_(); + } else { + add(this->data_, DATA_KEY); + } + for (auto pkey : this->ping_keys_) { + add(this->data_, PING_KEY); + add(this->data_, pkey.second); + } +} + +void UDPComponent::flush_() { + if (!network::is_connected() || this->data_.empty()) + return; + uint32_t buffer[MAX_PACKET_SIZE / 4]; + memset(buffer, 0, sizeof buffer); + // len must be a multiple of 4 + auto header_len = round4(this->header_.size()) / 4; + auto len = round4(data_.size()) / 4; + memcpy(buffer, this->header_.data(), this->header_.size()); + memcpy(buffer + header_len, this->data_.data(), this->data_.size()); + if (this->is_encrypted_()) { + xxtea_encrypt(buffer + header_len, len, (uint32_t *) this->encryption_key_.data()); + } + auto total_len = (header_len + len) * 4; + this->send_packet_(buffer, total_len); +} + +void UDPComponent::add_binary_data_(uint8_t key, const char *id, bool data) { + auto len = 1 + 1 + 1 + strlen(id); + if (len + this->header_.size() + this->data_.size() > MAX_PACKET_SIZE) { + this->flush_(); + } + add(this->data_, key); + add(this->data_, (uint8_t) data); + add(this->data_, id); +} +void UDPComponent::add_data_(uint8_t key, const char *id, float data) { + FuData udata{.f32 = data}; + this->add_data_(key, id, udata.u32); +} + +void UDPComponent::add_data_(uint8_t key, const char *id, uint32_t data) { + auto len = 4 + 1 + 1 + strlen(id); + if (len + this->header_.size() + this->data_.size() > MAX_PACKET_SIZE) { + this->flush_(); + } + add(this->data_, key); + add(this->data_, data); + add(this->data_, id); +} +void UDPComponent::send_data_(bool all) { + if (!this->should_send_ || !network::is_connected()) + return; + this->init_data_(); +#ifdef USE_SENSOR + for (auto &sensor : this->sensors_) { + if (all || sensor.updated) { + sensor.updated = false; + this->add_data_(SENSOR_KEY, sensor.id, sensor.sensor->get_state()); + } + } +#endif +#ifdef USE_BINARY_SENSOR + for (auto &sensor : this->binary_sensors_) { + if (all || sensor.updated) { + sensor.updated = false; + this->add_binary_data_(BINARY_SENSOR_KEY, sensor.id, sensor.sensor->state); + } + } +#endif + this->flush_(); + this->updated_ = false; + this->resend_data_ = false; +} + +void UDPComponent::update() { + this->updated_ = true; + this->resend_data_ = this->should_send_; + auto now = millis() / 1000; + if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { + this->resend_ping_key_ = this->ping_pong_enable_; + this->last_key_time_ = now; + } +} + +void UDPComponent::loop() { + uint8_t buf[MAX_PACKET_SIZE]; + if (this->should_listen_) { + for (;;) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + auto len = this->listen_socket_->read(buf, sizeof(buf)); +#endif +#ifdef USE_SOCKET_IMPL_LWIP_TCP + auto len = this->udp_client_.parsePacket(); + if (len > 0) + len = this->udp_client_.read(buf, sizeof(buf)); +#endif + if (len > 0) { + this->process_(buf, len); + continue; + } + break; + } + } + if (this->resend_ping_key_) + this->send_ping_pong_request_(); + if (this->updated_) { + this->send_data_(this->resend_data_); + } +} + +void UDPComponent::add_key_(const char *name, uint32_t key) { + if (!this->is_encrypted_()) + return; + if (this->ping_keys_.count(name) == 0 && this->ping_keys_.size() == MAX_PING_KEYS) { + ESP_LOGW(TAG, "Ping key from %s discarded", name); + return; + } + this->ping_keys_[name] = key; + this->resend_data_ = true; + ESP_LOGV(TAG, "Ping key from %s now %X", name, (unsigned) key); +} + +void UDPComponent::process_ping_request_(const char *name, uint8_t *ptr, size_t len) { + if (len != 4) { + ESP_LOGW(TAG, "Bad ping request"); + return; + } + auto key = get_uint32(ptr); + this->add_key_(name, key); + ESP_LOGV(TAG, "Updated ping key for %s to %08X", name, (unsigned) key); +} + +static bool process_rolling_code(Provider &provider, uint8_t *&buf, const uint8_t *end) { + if (end - buf < 8) + return false; + auto code0 = get_uint32(buf); + auto code1 = get_uint32(buf); + if (code1 < provider.last_code[1] || (code1 == provider.last_code[1] && code0 <= provider.last_code[0])) { + ESP_LOGW(TAG, "Rolling code for %s %08lX:%08lX is old", provider.name, (unsigned long) code1, + (unsigned long) code0); + return false; + } + provider.last_code[0] = code0; + provider.last_code[1] = code1; + return true; +} + +/** + * Process a received packet + */ +void UDPComponent::process_(uint8_t *buf, const size_t len) { + auto ping_key_seen = !this->ping_pong_enable_; + if (len < 8) { + return ESP_LOGV(TAG, "Bad length %zu", len); + } + char namebuf[256]{}; + uint8_t byte; + uint8_t *start_ptr = buf; + const uint8_t *end = buf + len; + FuData rdata{}; + auto magic = get_uint16(buf); + if (magic != MAGIC_NUMBER && magic != MAGIC_PING) + return ESP_LOGV(TAG, "Bad magic %X", magic); + + auto hlen = *buf++; + if (hlen > len - 3) { + return ESP_LOGV(TAG, "Bad hostname length %u > %zu", hlen, len - 3); + } + memcpy(namebuf, buf, hlen); + if (strcmp(this->name_, namebuf) == 0) { + return ESP_LOGV(TAG, "Ignoring our own data"); + } + buf += hlen; + if (magic == MAGIC_PING) + return this->process_ping_request_(namebuf, buf, end - buf); + if (round4(len) != len) { + return ESP_LOGW(TAG, "Bad length %zu", len); + } + hlen = round4(hlen + 3); + buf = start_ptr + hlen; + if (buf == end) { + return ESP_LOGV(TAG, "No data after header"); + } + + if (this->providers_.count(namebuf) == 0) { + return ESP_LOGVV(TAG, "Unknown hostname %s", namebuf); + } + auto &provider = this->providers_[namebuf]; + // if encryption not used with this host, ping check is pointless since it would be easily spoofed. + if (provider.encryption_key.empty()) + ping_key_seen = true; + + ESP_LOGV(TAG, "Found hostname %s", namebuf); +#ifdef USE_SENSOR + auto &sensors = this->remote_sensors_[namebuf]; +#endif +#ifdef USE_BINARY_SENSOR + auto &binary_sensors = this->remote_binary_sensors_[namebuf]; +#endif + + if (!provider.encryption_key.empty()) { + xxtea_decrypt((uint32_t *) buf, (end - buf) / 4, (uint32_t *) provider.encryption_key.data()); + } + byte = *buf++; + if (byte == ROLLING_CODE_KEY) { + if (!process_rolling_code(provider, buf, end)) + return; + } else if (byte != DATA_KEY) { + return ESP_LOGV(TAG, "Expected rolling_key or data_key, got %X", byte); + } + while (buf < end) { + byte = *buf++; + if (byte == ZERO_FILL_KEY) + continue; + if (byte == PING_KEY) { + if (end - buf < 4) { + return ESP_LOGV(TAG, "PING_KEY requires 4 more bytes"); + } + auto key = get_uint32(buf); + if (key == this->ping_key_) { + ping_key_seen = true; + ESP_LOGV(TAG, "Found good ping key %X", (unsigned) key); + } else { + ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key); + } + continue; + } + if (!ping_key_seen) { + ESP_LOGW(TAG, "Ping key not seen"); + this->resend_ping_key_ = true; + break; + } + if (byte == BINARY_SENSOR_KEY) { + if (end - buf < 3) { + return ESP_LOGV(TAG, "Binary sensor key requires at least 3 more bytes"); + } + rdata.u32 = *buf++; + } else if (byte == SENSOR_KEY) { + if (end - buf < 6) { + return ESP_LOGV(TAG, "Sensor key requires at least 6 more bytes"); + } + rdata.u32 = get_uint32(buf); + } else { + return ESP_LOGW(TAG, "Unknown key byte %X", byte); + } + + hlen = *buf++; + if (end - buf < hlen) { + return ESP_LOGV(TAG, "Name length of %u not available", hlen); + } + memset(namebuf, 0, sizeof namebuf); + memcpy(namebuf, buf, hlen); + ESP_LOGV(TAG, "Found sensor key %d, id %s, data %lX", byte, namebuf, (unsigned long) rdata.u32); + buf += hlen; +#ifdef USE_SENSOR + if (byte == SENSOR_KEY && sensors.count(namebuf) != 0) + sensors[namebuf]->publish_state(rdata.f32); +#endif +#ifdef USE_BINARY_SENSOR + if (byte == BINARY_SENSOR_KEY && binary_sensors.count(namebuf) != 0) + binary_sensors[namebuf]->publish_state(rdata.u32 != 0); +#endif + } +} + +void UDPComponent::dump_config() { + ESP_LOGCONFIG(TAG, "UDP:"); + ESP_LOGCONFIG(TAG, " Port: %u", this->port_); + ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(this->is_encrypted_())); + ESP_LOGCONFIG(TAG, " Ping-pong: %s", YESNO(this->ping_pong_enable_)); + for (const auto &address : this->addresses_) + ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); +#ifdef USE_SENSOR + for (auto sensor : this->sensors_) + ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); +#endif +#ifdef USE_BINARY_SENSOR + for (auto sensor : this->binary_sensors_) + ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); +#endif + for (const auto &host : this->providers_) { + ESP_LOGCONFIG(TAG, " Remote host: %s", host.first.c_str()); + ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(!host.second.encryption_key.empty())); +#ifdef USE_SENSOR + for (const auto &sensor : this->remote_sensors_[host.first.c_str()]) + ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.first.c_str()); +#endif +#ifdef USE_BINARY_SENSOR + for (const auto &sensor : this->remote_binary_sensors_[host.first.c_str()]) + ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.first.c_str()); +#endif + } +} +void UDPComponent::increment_code_() { + if (this->rolling_code_enable_) { + if (++this->rolling_code_[0] == 0) { + this->rolling_code_[1]++; + this->pref_.save(&this->rolling_code_[1]); + } + } +} +void UDPComponent::send_packet_(void *data, size_t len) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + for (const auto &saddr : this->sockaddrs_) { + auto result = this->broadcast_socket_->sendto(data, len, 0, &saddr, sizeof(saddr)); + if (result < 0) + ESP_LOGW(TAG, "sendto() error %d", errno); + } +#endif +#ifdef USE_SOCKET_IMPL_LWIP_TCP + auto iface = IPAddress(0, 0, 0, 0); + for (const auto &saddr : this->ipaddrs_) { + if (this->udp_client_.beginPacketMulticast(saddr, this->port_, iface, 128) != 0) { + this->udp_client_.write((const uint8_t *) data, len); + auto result = this->udp_client_.endPacket(); + if (result == 0) + ESP_LOGW(TAG, "udp.write() error"); + } + } +#endif +} + +void UDPComponent::send_ping_pong_request_() { + if (!this->ping_pong_enable_ || !network::is_connected()) + return; + this->ping_key_ = random_uint32(); + this->ping_header_.clear(); + add(this->ping_header_, MAGIC_PING); + add(this->ping_header_, this->name_); + add(this->ping_header_, this->ping_key_); + this->send_packet_(this->ping_header_.data(), this->ping_header_.size()); + this->resend_ping_key_ = false; + ESP_LOGV(TAG, "Sent new ping request %08X", (unsigned) this->ping_key_); +} +} // namespace udp +} // namespace esphome diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h new file mode 100644 index 0000000000..b4e11cf652 --- /dev/null +++ b/esphome/components/udp/udp_component.h @@ -0,0 +1,160 @@ +#pragma once + +#include "esphome/core/component.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +#include "esphome/components/socket/socket.h" +#endif +#ifdef USE_SOCKET_IMPL_LWIP_TCP +#include +#endif +#include +#include + +namespace esphome { +namespace udp { + +struct Provider { + std::vector encryption_key; + const char *name; + uint32_t last_code[2]; +}; + +#ifdef USE_SENSOR +struct Sensor { + sensor::Sensor *sensor; + const char *id; + bool updated; +}; +#endif +#ifdef USE_BINARY_SENSOR +struct BinarySensor { + binary_sensor::BinarySensor *sensor; + const char *id; + bool updated; +}; +#endif + +class UDPComponent : public PollingComponent { + public: + void setup() override; + void loop() override; + void update() override; + void dump_config() override; + +#ifdef USE_SENSOR + void add_sensor(const char *id, sensor::Sensor *sensor) { + Sensor st{sensor, id, true}; + this->sensors_.push_back(st); + } + void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { + this->add_provider(hostname); + this->remote_sensors_[hostname][remote_id] = sensor; + } +#endif +#ifdef USE_BINARY_SENSOR + void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { + BinarySensor st{sensor, id, true}; + this->binary_sensors_.push_back(st); + } + + void add_remote_binary_sensor(const char *hostname, const char *remote_id, binary_sensor::BinarySensor *sensor) { + this->add_provider(hostname); + this->remote_binary_sensors_[hostname][remote_id] = sensor; + } +#endif + void add_address(const char *addr) { this->addresses_.emplace_back(addr); } + void set_port(uint16_t port) { this->port_ = port; } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void add_provider(const char *hostname) { + if (this->providers_.count(hostname) == 0) { + Provider provider; + provider.encryption_key = std::vector{}; + provider.last_code[0] = 0; + provider.last_code[1] = 0; + provider.name = hostname; + this->providers_[hostname] = provider; +#ifdef USE_SENSOR + this->remote_sensors_[hostname] = std::map(); +#endif +#ifdef USE_BINARY_SENSOR + this->remote_binary_sensors_[hostname] = std::map(); +#endif + } + } + + 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; } + void set_ping_pong_recycle_time(uint32_t recycle_time) { this->ping_pong_recyle_time_ = recycle_time; } + void set_provider_encryption(const char *name, std::vector key) { + this->providers_[name].encryption_key = std::move(key); + } + + protected: + void send_data_(bool all); + void process_(uint8_t *buf, size_t len); + void flush_(); + void add_data_(uint8_t key, const char *id, float data); + void add_data_(uint8_t key, const char *id, uint32_t data); + void increment_code_(); + void add_binary_data_(uint8_t key, const char *id, bool data); + void init_data_(); + + bool updated_{}; + uint16_t port_{18511}; + uint32_t ping_key_{}; + uint32_t rolling_code_[2]{}; + bool rolling_code_enable_{}; + bool ping_pong_enable_{}; + uint32_t ping_pong_recyle_time_{}; + uint32_t last_key_time_{}; + bool resend_ping_key_{}; + bool resend_data_{}; + bool should_send_{}; + const char *name_{}; + bool should_listen_{}; + ESPPreferenceObject pref_; + +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + std::unique_ptr broadcast_socket_ = nullptr; + std::unique_ptr listen_socket_ = nullptr; + std::vector sockaddrs_{}; +#endif +#ifdef USE_SOCKET_IMPL_LWIP_TCP + std::vector ipaddrs_{}; + WiFiUDP udp_client_{}; +#endif + std::vector encryption_key_{}; + std::vector addresses_{}; + +#ifdef USE_SENSOR + std::vector sensors_{}; + std::map> remote_sensors_{}; +#endif +#ifdef USE_BINARY_SENSOR + std::vector binary_sensors_{}; + std::map> remote_binary_sensors_{}; +#endif + + std::map providers_{}; + std::vector ping_header_{}; + std::vector header_{}; + std::vector data_{}; + std::map ping_keys_{}; + void add_key_(const char *name, uint32_t key); + void send_ping_pong_request_(); + void send_packet_(void *data, size_t len); + void process_ping_request_(const char *name, uint8_t *ptr, size_t len); + + inline bool is_encrypted_() { return !this->encryption_key_.empty(); } +}; + +} // namespace udp +} // namespace esphome diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index 9f47f9f6b9..604e78d6f8 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -30,14 +30,14 @@ void UltrasonicSensorComponent::update() { ; const uint32_t pulse_end = micros(); - ESP_LOGV(TAG, "Echo took %uµs", pulse_end - pulse_start); + 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); - ESP_LOGD(TAG, "'%s' - Got distance: %.2f m", this->name_.c_str(), result); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); this->publish_state(result); } } @@ -45,8 +45,8 @@ 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: %u µs", this->pulse_time_us_); - ESP_LOGCONFIG(TAG, " Timeout: %u µs", this->timeout_us_); + ESP_LOGCONFIG(TAG, " Pulse time: %" PRIu32 " µs", this->pulse_time_us_); + ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " µs", this->timeout_us_); LOG_UPDATE_INTERVAL(this); } float UltrasonicSensorComponent::us_to_m(uint32_t us) { diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index e0d71b99ef..1a255d6122 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -4,6 +4,8 @@ #include "esphome/core/gpio.h" #include "esphome/components/sensor/sensor.h" +#include + namespace esphome { namespace ultrasonic { diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py new file mode 100644 index 0000000000..4729d954ee --- /dev/null +++ b/esphome/components/update/__init__.py @@ -0,0 +1,132 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_FORCE_UPDATE, + CONF_ID, + CONF_MQTT_ID, + CONF_WEB_SERVER, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_FIRMWARE, + ENTITY_CATEGORY_CONFIG, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@jesserockz"] +IS_PLATFORM_COMPONENT = True + +update_ns = cg.esphome_ns.namespace("update") +UpdateEntity = update_ns.class_("UpdateEntity", cg.EntityBase) + +UpdateInfo = update_ns.struct("UpdateInfo") + +PerformAction = update_ns.class_( + "PerformAction", automation.Action, cg.Parented.template(UpdateEntity) +) +IsAvailableCondition = update_ns.class_( + "IsAvailableCondition", automation.Condition, cg.Parented.template(UpdateEntity) +) + +DEVICE_CLASSES = [ + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_FIRMWARE, +] + +CONF_ON_UPDATE_AVAILABLE = "on_update_available" + +UPDATE_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTUpdateComponent), + cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), + cv.Optional(CONF_ON_UPDATE_AVAILABLE): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + } + ) +) + + +async def setup_update_core_(var, config): + await setup_entity(var, config) + + if device_class_config := config.get(CONF_DEVICE_CLASS): + cg.add(var.set_device_class(device_class_config)) + + if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE): + await automation.build_automation( + var.get_update_available_trigger(), + [(UpdateInfo.operator("ref").operator("const"), "x")], + on_update_available, + ) + + if mqtt_id_config := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id_config, var) + await mqtt.register_mqtt_component(mqtt_, config) + + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + + +async def register_update(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_update(var)) + await setup_update_core_(var, config) + + +async def new_update(config): + var = cg.new_Pvariable(config[CONF_ID]) + await register_update(var, config) + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_UPDATE") + cg.add_global(update_ns.using) + + +@automation.register_action( + "update.perform", + PerformAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean), + } + ), +) +async def update_perform_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]) + + force = await cg.templatable(config[CONF_FORCE_UPDATE], args, cg.bool_) + cg.add(var.set_force(force)) + return var + + +@automation.register_condition( + "update.is_available", + IsAvailableCondition, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + } + ), +) +async def update_is_available_condition_to_code( + config, condition_id, template_arg, args +): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h new file mode 100644 index 0000000000..df50f86a0c --- /dev/null +++ b/esphome/components/update/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "update_entity.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace update { + +template class PerformAction : public Action, public Parented { + TEMPLATABLE_VALUE(bool, force) + + public: + void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); } +}; + +template class IsAvailableCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } +}; + +} // namespace update +} // namespace esphome diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp new file mode 100644 index 0000000000..ed9a0480d8 --- /dev/null +++ b/esphome/components/update/update_entity.cpp @@ -0,0 +1,38 @@ +#include "update_entity.h" + +#include "esphome/core/log.h" + +namespace esphome { +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()); + + if (!this->update_info_.md5.empty()) { + ESP_LOGD(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); + } + if (!this->update_info_.firmware_url.empty()) { + ESP_LOGD(TAG, " Firmware URL: %s", this->update_info_.firmware_url.c_str()); + } + + ESP_LOGD(TAG, " Title: %s", this->update_info_.title.c_str()); + if (!this->update_info_.summary.empty()) { + ESP_LOGD(TAG, " Summary: %s", this->update_info_.summary.c_str()); + } + if (!this->update_info_.release_url.empty()) { + ESP_LOGD(TAG, " Release URL: %s", this->update_info_.release_url.c_str()); + } + + if (this->update_info_.has_progress) { + ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress); + } + + this->has_state_ = true; + this->state_callback_.call(); +} + +} // namespace update +} // namespace esphome diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h new file mode 100644 index 0000000000..cc269e288f --- /dev/null +++ b/esphome/components/update/update_entity.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" + +namespace esphome { +namespace update { + +struct UpdateInfo { + std::string latest_version; + std::string current_version; + std::string title; + std::string summary; + std::string release_url; + std::string firmware_url; + std::string md5; + bool has_progress{false}; + float progress; +}; + +enum UpdateState : uint8_t { + UPDATE_STATE_UNKNOWN, + UPDATE_STATE_NO_UPDATE, + UPDATE_STATE_AVAILABLE, + UPDATE_STATE_INSTALLING, +}; + +class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { + public: + bool has_state() const { return this->has_state_; } + + void publish_state(); + + void perform() { this->perform(false); } + virtual void perform(bool force) = 0; + virtual void check() = 0; + + const UpdateInfo &update_info = update_info_; + const UpdateState &state = state_; + + void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + + protected: + UpdateState state_{UPDATE_STATE_UNKNOWN}; + UpdateInfo update_info_; + bool has_state_{false}; + + CallbackManager state_callback_{}; +}; + +} // namespace update +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/__init__.py b/esphome/components/uponor_smatrix/__init__.py new file mode 100644 index 0000000000..35c4c4cecd --- /dev/null +++ b/esphome/components/uponor_smatrix/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, time +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_TIME_ID, +) + +CODEOWNERS = ["@kroimon"] + +DEPENDENCIES = ["uart"] + +MULTI_CONF = True + +uponor_smatrix_ns = cg.esphome_ns.namespace("uponor_smatrix") +UponorSmatrixComponent = uponor_smatrix_ns.class_( + "UponorSmatrixComponent", cg.Component, uart.UARTDevice +) +UponorSmatrixDevice = uponor_smatrix_ns.class_( + "UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent) +) + +CONF_UPONOR_SMATRIX_ID = "uponor_smatrix_id" +CONF_TIME_DEVICE_ADDRESS = "time_device_address" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(UponorSmatrixComponent), + cv.Optional(CONF_ADDRESS): cv.hex_uint16_t, + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional(CONF_TIME_DEVICE_ADDRESS): cv.hex_uint16_t, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "uponor_smatrix", + baud_rate=19200, + require_tx=True, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + +# A schema to use for all Uponor Smatrix devices +UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent), + cv.Required(CONF_ADDRESS): cv.hex_uint16_t, + } +) + + +async def to_code(config): + cg.add_global(uponor_smatrix_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if address := config.get(CONF_ADDRESS): + cg.add(var.set_system_address(address)) + if time_id := config.get(CONF_TIME_ID): + time_ = await cg.get_variable(time_id) + cg.add(var.set_time_id(time_)) + if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS): + cg.add(var.set_time_device_address(time_device_address)) + + +async def register_uponor_smatrix_device(var, config): + parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_device_address(config[CONF_ADDRESS])) + cg.add(parent.register_device(var)) diff --git a/esphome/components/uponor_smatrix/climate/__init__.py b/esphome/components/uponor_smatrix/climate/__init__.py new file mode 100644 index 0000000000..0becec2624 --- /dev/null +++ b/esphome/components/uponor_smatrix/climate/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate +from esphome.const import CONF_ID + +from .. import ( + uponor_smatrix_ns, + UponorSmatrixDevice, + UPONOR_SMATRIX_DEVICE_SCHEMA, + register_uponor_smatrix_device, +) + +DEPENDENCIES = ["uponor_smatrix"] + +UponorSmatrixClimate = uponor_smatrix_ns.class_( + "UponorSmatrixClimate", + climate.Climate, + cg.Component, + UponorSmatrixDevice, +) + +CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UponorSmatrixClimate), + } +).extend(UPONOR_SMATRIX_DEVICE_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await climate.register_climate(var, config) + await register_uponor_smatrix_device(var, config) diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp new file mode 100644 index 0000000000..5afc628db3 --- /dev/null +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -0,0 +1,101 @@ +#include "uponor_smatrix_climate.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uponor_smatrix { + +static const char *const TAG = "uponor_smatrix.climate"; + +void UponorSmatrixClimate::dump_config() { + LOG_CLIMATE("", "Uponor Smatrix Climate", this); + ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_); +} + +void UponorSmatrixClimate::loop() { + const uint32_t now = millis(); + + // Publish state after all update packets are processed + if (this->last_data_ != 0 && (now - this->last_data_ > 100) && this->target_temperature_raw_ != 0) { + float temp = raw_to_celsius((this->preset == climate::CLIMATE_PRESET_ECO) + ? (this->target_temperature_raw_ - this->eco_setback_value_raw_) + : this->target_temperature_raw_); + float step = this->get_traits().get_visual_target_temperature_step(); + this->target_temperature = roundf(temp / step) * step; + this->publish_state(); + this->last_data_ = 0; + } +} + +climate::ClimateTraits UponorSmatrixClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_current_humidity(true); + traits.set_supported_modes({climate::CLIMATE_MODE_HEAT}); + traits.set_supports_action(true); + traits.set_supported_presets({climate::CLIMATE_PRESET_ECO}); + traits.set_visual_min_temperature(this->min_temperature_); + traits.set_visual_max_temperature(this->max_temperature_); + traits.set_visual_current_temperature_step(0.1f); + traits.set_visual_target_temperature_step(0.5f); + return traits; +} + +void UponorSmatrixClimate::control(const climate::ClimateCall &call) { + if (call.get_target_temperature().has_value()) { + uint16_t temp = celsius_to_raw(*call.get_target_temperature()); + if (this->preset == climate::CLIMATE_PRESET_ECO) { + // During ECO mode, the thermostat automatically substracts the setback value from the setpoint, + // so we need to add it here first + temp += this->eco_setback_value_raw_; + } + + // For unknown reasons, we need to send a null setpoint first for the thermostat to react + UponorSmatrixData data[] = {{UPONOR_ID_TARGET_TEMP, 0}, {UPONOR_ID_TARGET_TEMP, temp}}; + this->send(data, sizeof(data) / sizeof(data[0])); + } +} + +void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t data_len) { + for (int i = 0; i < data_len; i++) { + switch (data[i].id) { + case UPONOR_ID_TARGET_TEMP_MIN: + this->min_temperature_ = raw_to_celsius(data[i].value); + break; + case UPONOR_ID_TARGET_TEMP_MAX: + this->max_temperature_ = raw_to_celsius(data[i].value); + break; + case UPONOR_ID_TARGET_TEMP: + // Ignore invalid values here as they are used by the controller to explicitely request the setpoint from a + // thermostat + if (data[i].value != UPONOR_INVALID_VALUE) + this->target_temperature_raw_ = data[i].value; + break; + case UPONOR_ID_ECO_SETBACK: + this->eco_setback_value_raw_ = data[i].value; + break; + case UPONOR_ID_DEMAND: + if (data[i].value & 0x1000) { + this->mode = climate::CLIMATE_MODE_COOL; + this->action = (data[i].value & 0x0040) ? climate::CLIMATE_ACTION_COOLING : climate::CLIMATE_ACTION_IDLE; + } else { + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = (data[i].value & 0x0040) ? climate::CLIMATE_ACTION_HEATING : climate::CLIMATE_ACTION_IDLE; + } + break; + case UPONOR_ID_MODE1: + this->set_preset_((data[i].value & 0x0008) ? climate::CLIMATE_PRESET_ECO : climate::CLIMATE_PRESET_NONE); + break; + case UPONOR_ID_ROOM_TEMP: + this->current_temperature = raw_to_celsius(data[i].value); + break; + case UPONOR_ID_HUMIDITY: + this->current_humidity = data[i].value & 0x00FF; + } + } + + this->last_data_ = millis(); +} + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h new file mode 100644 index 0000000000..b8458045c6 --- /dev/null +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/climate/climate.h" +#include "esphome/components/uponor_smatrix/uponor_smatrix.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uponor_smatrix { + +class UponorSmatrixClimate : public climate::Climate, public Component, public UponorSmatrixDevice { + public: + void dump_config() override; + void loop() override; + + protected: + climate::ClimateTraits traits() override; + void control(const climate::ClimateCall &call) override; + void on_device_data(const UponorSmatrixData *data, size_t data_len) override; + + uint32_t last_data_; + float min_temperature_{5.0f}; + float max_temperature_{35.0f}; + uint16_t eco_setback_value_raw_{0x0048}; + uint16_t target_temperature_raw_; +}; + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/sensor/__init__.py b/esphome/components/uponor_smatrix/sensor/__init__.py new file mode 100644 index 0000000000..89097aef18 --- /dev/null +++ b/esphome/components/uponor_smatrix/sensor/__init__.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_EXTERNAL_TEMPERATURE, + CONF_HUMIDITY, + CONF_TEMPERATURE, + CONF_ID, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +from .. import ( + uponor_smatrix_ns, + UponorSmatrixDevice, + UPONOR_SMATRIX_DEVICE_SCHEMA, + register_uponor_smatrix_device, +) + +DEPENDENCIES = ["uponor_smatrix"] + +UponorSmatrixSensor = uponor_smatrix_ns.class_( + "UponorSmatrixSensor", + sensor.Sensor, + cg.Component, + UponorSmatrixDevice, +) + +CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UponorSmatrixSensor), + 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, + ), + } +).extend(UPONOR_SMATRIX_DEVICE_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await register_uponor_smatrix_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(external_temperature_config) + cg.add(var.set_external_temperature_sensor(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp new file mode 100644 index 0000000000..2fd2a36efc --- /dev/null +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp @@ -0,0 +1,37 @@ +#include "uponor_smatrix_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uponor_smatrix { + +static const char *const TAG = "uponor_smatrix.sensor"; + +void UponorSmatrixSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Uponor Smatrix Sensor"); + ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t data_len) { + for (int i = 0; i < data_len; i++) { + switch (data[i].id) { + case UPONOR_ID_ROOM_TEMP: + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(raw_to_celsius(data[i].value)); + break; + case UPONOR_ID_EXTERNAL_TEMP: + if (this->external_temperature_sensor_ != nullptr) + this->external_temperature_sensor_->publish_state(raw_to_celsius(data[i].value)); + break; + case UPONOR_ID_HUMIDITY: + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(data[i].value & 0x00FF); + break; + } + } +} + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h new file mode 100644 index 0000000000..5e38117a21 --- /dev/null +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uponor_smatrix/uponor_smatrix.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uponor_smatrix { + +class UponorSmatrixSensor : public sensor::Sensor, public Component, public UponorSmatrixDevice { + SUB_SENSOR(temperature) + SUB_SENSOR(external_temperature) + SUB_SENSOR(humidity) + + public: + void dump_config() override; + + protected: + void on_device_data(const UponorSmatrixData *data, size_t data_len) override; +}; + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp new file mode 100644 index 0000000000..e058de2852 --- /dev/null +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -0,0 +1,227 @@ +#include "uponor_smatrix.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uponor_smatrix { + +static const char *const TAG = "uponor_smatrix"; + +void UponorSmatrixComponent::setup() { +#ifdef USE_TIME + if (this->time_id_ != nullptr) { + this->time_id_->add_on_time_sync_callback([this] { this->send_time(); }); + } +#endif +} + +void UponorSmatrixComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Uponor Smatrix"); + ESP_LOGCONFIG(TAG, " System address: 0x%04X", this->address_); +#ifdef USE_TIME + if (this->time_id_ != nullptr) { + ESP_LOGCONFIG(TAG, " Time synchronization: YES"); + ESP_LOGCONFIG(TAG, " Time master device address: 0x%04X", this->time_device_address_); + } +#endif + + this->check_uart_settings(19200); + + if (!this->unknown_devices_.empty()) { + ESP_LOGCONFIG(TAG, " Detected unknown device addresses:"); + for (auto device_address : this->unknown_devices_) { + ESP_LOGCONFIG(TAG, " 0x%04X", device_address); + } + } +} + +void UponorSmatrixComponent::loop() { + const uint32_t now = millis(); + + // Discard stale data + if (!this->rx_buffer_.empty() && (now - this->last_rx_ > 50)) { + ESP_LOGD(TAG, "Discarding %d bytes of unparsed data", this->rx_buffer_.size()); + this->rx_buffer_.clear(); + } + + // Read incoming data + while (this->available()) { + // The controller polls devices every 10 seconds in some units or continuously in others with around 200 ms between + // devices. Remember timestamps so we can send our own packets when the bus is expected to be silent. + this->last_rx_ = now; + + uint8_t byte; + this->read_byte(&byte); + if (this->parse_byte_(byte)) { + this->rx_buffer_.clear(); + } + } + + // Send packets during bus silence + if (this->rx_buffer_.empty() && (now - this->last_rx_ > 50) && (now - this->last_rx_ < 100) && + (now - this->last_tx_ > 200)) { +#ifdef USE_TIME + // Only build time packet when bus is silent and queue is empty to make sure we can send it right away + if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_()) + this->send_time_requested_ = false; +#endif + // Send the next packet in the queue + if (!this->tx_queue_.empty()) { + auto packet = std::move(this->tx_queue_.front()); + this->tx_queue_.pop(); + + this->write_array(packet); + this->flush(); + + this->last_tx_ = now; + } + } +} + +bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { + this->rx_buffer_.push_back(byte); + const uint8_t *packet = this->rx_buffer_.data(); + size_t packet_len = this->rx_buffer_.size(); + + if (packet_len < 7) { + // Minimum packet size is 7 bytes, wait for more + return false; + } + + uint16_t system_address = encode_uint16(packet[0], packet[1]); + uint16_t device_address = encode_uint16(packet[2], packet[3]); + uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]); + + uint16_t computed_crc = crc16(packet, packet_len - 2); + if (crc != computed_crc) { + // CRC did not match, more data might be coming + return false; + } + + ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address, + format_hex(&packet[4], packet_len - 6).c_str(), crc); + + // Detect or check system address + if (this->address_ == 0) { + ESP_LOGI(TAG, "Using detected system address 0x%04X", system_address); + this->address_ = system_address; + } else if (this->address_ != system_address) { + // This should never happen except if the system address was set or detected incorrectly, so warn the user. + ESP_LOGW(TAG, "Received packet from unknown system address 0x%04X", system_address); + return true; + } + + // Handle packet + size_t data_len = (packet_len - 6) / 3; + if (data_len == 0) { + if (packet[4] == UPONOR_ID_REQUEST) + ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address); + return true; + } + + // Decode packet payload data for easy access + UponorSmatrixData data[data_len]; + for (int i = 0; i < data_len; i++) { + data[i].id = packet[(i * 3) + 4]; + data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]); + } + +#ifdef USE_TIME + // Detect device that acts as time master if not set explicitely + if (this->time_device_address_ == 0 && data_len >= 2) { + // The first thermostat paired to the controller will act as the time master. Time can only be manually adjusted at + // this first thermostat. To synchronize time, we need to know its address, so we search for packets coming from a + // thermostat sending both room temperature and time information. + bool found_temperature = false; + bool found_time = false; + for (int i = 0; i < data_len; i++) { + if (data[i].id == UPONOR_ID_ROOM_TEMP) + found_temperature = true; + if (data[i].id == UPONOR_ID_DATETIME1) + found_time = true; + if (found_temperature && found_time) { + ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address); + this->time_device_address_ = device_address; + break; + } + } + } +#endif + + // Forward data to device components + bool found = false; + for (auto *device : this->devices_) { + if (device->address_ == device_address) { + found = true; + device->on_device_data(data, data_len); + } + } + + // Log unknown device addresses + if (!found && !this->unknown_devices_.count(device_address)) { + ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address); + this->unknown_devices_.insert(device_address); + } + + // Return true to reset buffer + return true; +} + +bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) { + if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0) + return false; + + // Assemble packet for send queue. All fields are big-endian except for the little-endian checksum. + std::vector packet; + packet.reserve(6 + 3 * data_len); + + packet.push_back(this->address_ >> 8); + packet.push_back(this->address_ >> 0); + packet.push_back(device_address >> 8); + packet.push_back(device_address >> 0); + + for (int i = 0; i < data_len; i++) { + packet.push_back(data[i].id); + packet.push_back(data[i].value >> 8); + packet.push_back(data[i].value >> 0); + } + + auto crc = crc16(packet.data(), packet.size()); + packet.push_back(crc >> 0); + packet.push_back(crc >> 8); + + this->tx_queue_.push(packet); + return true; +} + +#ifdef USE_TIME +bool UponorSmatrixComponent::do_send_time_() { + if (this->time_device_address_ == 0 || this->time_id_ == nullptr) + return false; + + ESPTime now = this->time_id_->now(); + if (!now.is_valid()) + return false; + + uint8_t year = now.year - 2000; + uint8_t month = now.month; + // ESPHome days are [1-7] starting with Sunday, Uponor days are [0-6] starting with Monday + uint8_t day_of_week = (now.day_of_week == 1) ? 6 : (now.day_of_week - 2); + uint8_t day_of_month = now.day_of_month; + uint8_t hour = now.hour; + uint8_t minute = now.minute; + uint8_t second = now.second; + + uint16_t time1 = (year & 0x7F) << 7 | (month & 0x0F) << 3 | (day_of_week & 0x07); + uint16_t time2 = (day_of_month & 0x1F) << 11 | (hour & 0x1F) << 6 | (minute & 0x3F); + uint16_t time3 = second; + + ESP_LOGI(TAG, "Sending local time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, + now.minute, now.second); + + UponorSmatrixData data[] = {{UPONOR_ID_DATETIME1, time1}, {UPONOR_ID_DATETIME2, time2}, {UPONOR_ID_DATETIME3, time3}}; + return this->send(this->time_device_address_, data, sizeof(data) / sizeof(data[0])); +} +#endif + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h new file mode 100644 index 0000000000..e3e19a12fc --- /dev/null +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -0,0 +1,129 @@ +#pragma once + +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/core/defines.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" +#endif + +#include +#include +#include + +namespace esphome { +namespace uponor_smatrix { + +/// Date/Time Part 1 (year, month, day of week) +static const uint8_t UPONOR_ID_DATETIME1 = 0x08; +/// Date/Time Part 2 (day of month, hour, minute) +static const uint8_t UPONOR_ID_DATETIME2 = 0x09; +/// Date/Time Part 3 (seconds) +static const uint8_t UPONOR_ID_DATETIME3 = 0x0A; +/// Unknown (observed values: 0x0342, 0x0024) +static const uint8_t UPONOR_ID_UNKNOWN1 = 0x0C; +/// Outdoor Temperature? (sent by controller) +static const uint8_t UPONOR_ID_OUTDOOR_TEMP = 0x2D; +/// Unknown (observed values: 0x8000) +static const uint8_t UPONOR_ID_UNKNOWN2 = 0x35; +/// Room Temperature Setpoint Minimum +static const uint8_t UPONOR_ID_TARGET_TEMP_MIN = 0x37; +/// Room Temperature Setpoint Maximum +static const uint8_t UPONOR_ID_TARGET_TEMP_MAX = 0x38; +/// Room Temperature Setpoint +static const uint8_t UPONOR_ID_TARGET_TEMP = 0x3B; +/// Room Temperature Setpoint Setback for ECO Mode +static const uint8_t UPONOR_ID_ECO_SETBACK = 0x3C; +/// Heating/Cooling Demand +static const uint8_t UPONOR_ID_DEMAND = 0x3D; +/// Thermostat Operating Mode 1 (ECO state, program schedule state) +static const uint8_t UPONOR_ID_MODE1 = 0x3E; +/// Thermostat Operating Mode 2 (sensor configuration, heating/cooling allowed) +static const uint8_t UPONOR_ID_MODE2 = 0x3F; +/// Current Room Temperature +static const uint8_t UPONOR_ID_ROOM_TEMP = 0x40; +/// Current External (Floor/Outdoor) Sensor Temperature +static const uint8_t UPONOR_ID_EXTERNAL_TEMP = 0x41; +/// Current Room Humidity +static const uint8_t UPONOR_ID_HUMIDITY = 0x42; +/// Data Request (sent by controller) +static const uint8_t UPONOR_ID_REQUEST = 0xFF; + +/// Indicating an invalid/missing value +static const uint16_t UPONOR_INVALID_VALUE = 0x7FFF; + +struct UponorSmatrixData { + uint8_t id; + uint16_t value; +}; + +class UponorSmatrixDevice; + +class UponorSmatrixComponent : public uart::UARTDevice, public Component { + public: + UponorSmatrixComponent() = default; + + void setup() override; + void dump_config() override; + void loop() override; + + void set_system_address(uint16_t address) { this->address_ = address; } + void register_device(UponorSmatrixDevice *device) { this->devices_.push_back(device); } + + bool send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len); + +#ifdef USE_TIME + void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } + void set_time_device_address(uint16_t address) { this->time_device_address_ = address; } + void send_time() { this->send_time_requested_ = true; } +#endif + + protected: + bool parse_byte_(uint8_t byte); + + uint16_t address_; + std::vector devices_; + std::set unknown_devices_; + + std::vector rx_buffer_; + std::queue> tx_queue_; + uint32_t last_rx_; + uint32_t last_tx_; + +#ifdef USE_TIME + time::RealTimeClock *time_id_{nullptr}; + uint16_t time_device_address_; + bool send_time_requested_; + bool do_send_time_(); +#endif +}; + +class UponorSmatrixDevice : public Parented { + public: + void set_device_address(uint16_t address) { this->address_ = address; } + + virtual void on_device_data(const UponorSmatrixData *data, size_t data_len) = 0; + bool send(const UponorSmatrixData *data, size_t data_len) { + return this->parent_->send(this->address_, data, data_len); + } + + protected: + friend UponorSmatrixComponent; + uint16_t address_; +}; + +inline float raw_to_celsius(uint16_t raw) { + return (raw == UPONOR_INVALID_VALUE) ? NAN : fahrenheit_to_celsius(raw / 10.0f); +} + +inline uint16_t celsius_to_raw(float celsius) { + return std::isnan(celsius) ? UPONOR_INVALID_VALUE + : static_cast(lroundf(celsius_to_fahrenheit(celsius) * 10.0f)); +} + +} // namespace uponor_smatrix +} // namespace esphome diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index 07d7d8f2cf..30220751b6 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -1,7 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor +from esphome.components import sensor, time from esphome.const import ( + CONF_TIME_ID, + DEVICE_CLASS_TIMESTAMP, ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, @@ -10,19 +12,50 @@ from esphome.const import ( ) uptime_ns = cg.esphome_ns.namespace("uptime") -UptimeSensor = uptime_ns.class_("UptimeSensor", sensor.Sensor, cg.PollingComponent) +UptimeSecondsSensor = uptime_ns.class_( + "UptimeSecondsSensor", sensor.Sensor, cg.PollingComponent +) +UptimeTimestampSensor = uptime_ns.class_( + "UptimeTimestampSensor", sensor.Sensor, cg.Component +) -CONFIG_SCHEMA = sensor.sensor_schema( - UptimeSensor, - unit_of_measurement=UNIT_SECOND, - icon=ICON_TIMER, - accuracy_decimals=0, - state_class=STATE_CLASS_TOTAL_INCREASING, - device_class=DEVICE_CLASS_DURATION, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, -).extend(cv.polling_component_schema("60s")) + +CONFIG_SCHEMA = cv.typed_schema( + { + "seconds": sensor.sensor_schema( + UptimeSecondsSensor, + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=DEVICE_CLASS_DURATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("60s")), + "timestamp": sensor.sensor_schema( + UptimeTimestampSensor, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + .extend( + cv.Schema( + { + cv.GenerateID(CONF_TIME_ID): cv.All( + cv.requires_component("time"), cv.use_id(time.RealTimeClock) + ), + } + ) + ) + .extend(cv.COMPONENT_SCHEMA), + }, + default_type="seconds", +) async def to_code(config): var = await sensor.new_sensor(config) await cg.register_component(var, config) + if time_id_config := config.get(CONF_TIME_ID): + time_id = await cg.get_variable(time_id_config) + cg.add(var.set_time(time_id)) diff --git a/esphome/components/uptime/uptime_seconds_sensor.cpp b/esphome/components/uptime/uptime_seconds_sensor.cpp new file mode 100644 index 0000000000..fa6b9d621d --- /dev/null +++ b/esphome/components/uptime/uptime_seconds_sensor.cpp @@ -0,0 +1,38 @@ +#include "uptime_seconds_sensor.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uptime { + +static const char *const TAG = "uptime.sensor"; + +void UptimeSecondsSensor::update() { + const uint32_t ms = millis(); + const uint64_t ms_mask = (1ULL << 32) - 1ULL; + const uint32_t last_ms = this->uptime_ & ms_mask; + if (ms < last_ms) { + this->uptime_ += ms_mask + 1ULL; + ESP_LOGD(TAG, "Detected roll-over \xf0\x9f\xa6\x84"); + } + this->uptime_ &= ~ms_mask; + this->uptime_ |= ms; + + // Do separate second and milliseconds conversion to avoid floating point division errors + // Probably some IEEE standard already guarantees this division can be done without loss + // of precision in a single division, but let's do it like this to be sure. + const uint64_t seconds_int = this->uptime_ / 1000ULL; + const float seconds = float(seconds_int) + (this->uptime_ % 1000ULL) / 1000.0f; + this->publish_state(seconds); +} +std::string UptimeSecondsSensor::unique_id() { return get_mac_address() + "-uptime"; } +float UptimeSecondsSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void UptimeSecondsSensor::dump_config() { + LOG_SENSOR("", "Uptime Sensor", this); + ESP_LOGCONFIG(TAG, " Type: Seconds"); +} + +} // namespace uptime +} // namespace esphome diff --git a/esphome/components/uptime/uptime_seconds_sensor.h b/esphome/components/uptime/uptime_seconds_sensor.h new file mode 100644 index 0000000000..41b3647822 --- /dev/null +++ b/esphome/components/uptime/uptime_seconds_sensor.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uptime { + +class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { + public: + void update() override; + void dump_config() override; + + float get_setup_priority() const override; + + std::string unique_id() override; + + protected: + uint64_t uptime_{0}; +}; + +} // namespace uptime +} // namespace esphome diff --git a/esphome/components/uptime/uptime_sensor.cpp b/esphome/components/uptime/uptime_sensor.cpp deleted file mode 100644 index 40325d2a36..0000000000 --- a/esphome/components/uptime/uptime_sensor.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "uptime_sensor.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace uptime { - -static const char *const TAG = "uptime.sensor"; - -void UptimeSensor::update() { - const uint32_t ms = millis(); - const uint64_t ms_mask = (1ULL << 32) - 1ULL; - const uint32_t last_ms = this->uptime_ & ms_mask; - if (ms < last_ms) { - this->uptime_ += ms_mask + 1ULL; - ESP_LOGD(TAG, "Detected roll-over \xf0\x9f\xa6\x84"); - } - this->uptime_ &= ~ms_mask; - this->uptime_ |= ms; - - // Do separate second and milliseconds conversion to avoid floating point division errors - // Probably some IEEE standard already guarantees this division can be done without loss - // of precision in a single division, but let's do it like this to be sure. - const uint64_t seconds_int = this->uptime_ / 1000ULL; - const float seconds = float(seconds_int) + (this->uptime_ % 1000ULL) / 1000.0f; - this->publish_state(seconds); -} -std::string UptimeSensor::unique_id() { return get_mac_address() + "-uptime"; } -float UptimeSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void UptimeSensor::dump_config() { LOG_SENSOR("", "Uptime Sensor", this); } - -} // namespace uptime -} // namespace esphome diff --git a/esphome/components/uptime/uptime_sensor.h b/esphome/components/uptime/uptime_sensor.h deleted file mode 100644 index dab380d2d9..0000000000 --- a/esphome/components/uptime/uptime_sensor.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" - -namespace esphome { -namespace uptime { - -class UptimeSensor : public sensor::Sensor, public PollingComponent { - public: - void update() override; - void dump_config() override; - - float get_setup_priority() const override; - - std::string unique_id() override; - - protected: - uint64_t uptime_{0}; -}; - -} // namespace uptime -} // namespace esphome diff --git a/esphome/components/uptime/uptime_timestamp_sensor.cpp b/esphome/components/uptime/uptime_timestamp_sensor.cpp new file mode 100644 index 0000000000..fa8cb2bb61 --- /dev/null +++ b/esphome/components/uptime/uptime_timestamp_sensor.cpp @@ -0,0 +1,39 @@ +#include "uptime_timestamp_sensor.h" + +#ifdef USE_TIME + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uptime { + +static const char *const TAG = "uptime.sensor"; + +void UptimeTimestampSensor::setup() { + this->time_->add_on_time_sync_callback([this]() { + if (this->has_state_) + return; // No need to update the timestamp if it's already set + + auto now = this->time_->now(); + const uint32_t ms = millis(); + if (!now.is_valid()) + return; // No need to update the timestamp if the time is not valid + + time_t timestamp = now.timestamp; + uint32_t seconds = ms / 1000; + timestamp -= seconds; + this->publish_state(timestamp); + }); +} +float UptimeTimestampSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void UptimeTimestampSensor::dump_config() { + LOG_SENSOR("", "Uptime Sensor", this); + ESP_LOGCONFIG(TAG, " Type: Timestamp"); +} + +} // namespace uptime +} // namespace esphome + +#endif // USE_TIME diff --git a/esphome/components/uptime/uptime_timestamp_sensor.h b/esphome/components/uptime/uptime_timestamp_sensor.h new file mode 100644 index 0000000000..f38b5d53b4 --- /dev/null +++ b/esphome/components/uptime/uptime_timestamp_sensor.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_TIME + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/time/real_time_clock.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uptime { + +class UptimeTimestampSensor : public sensor::Sensor, public Component { + public: + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + void set_time(time::RealTimeClock *time) { this->time_ = time; } + + protected: + time::RealTimeClock *time_; +}; + +} // namespace uptime +} // namespace esphome + +#endif // USE_TIME diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py new file mode 100644 index 0000000000..e55bb522de --- /dev/null +++ b/esphome/components/valve/__init__.py @@ -0,0 +1,204 @@ +from esphome import automation +from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ID, + CONF_MQTT_ID, + CONF_ON_OPEN, + CONF_POSITION, + CONF_POSITION_COMMAND_TOPIC, + CONF_POSITION_STATE_TOPIC, + CONF_STATE, + CONF_STOP, + CONF_TRIGGER_ID, + CONF_WEB_SERVER, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_GAS, + DEVICE_CLASS_WATER, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +IS_PLATFORM_COMPONENT = True + +CODEOWNERS = ["@esphome/core"] + +DEVICE_CLASSES = [ + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_GAS, + DEVICE_CLASS_WATER, +] + +valve_ns = cg.esphome_ns.namespace("valve") + +Valve = valve_ns.class_("Valve", cg.EntityBase) + +VALVE_OPEN = valve_ns.VALVE_OPEN +VALVE_CLOSED = valve_ns.VALVE_CLOSED + +VALVE_STATES = { + "OPEN": VALVE_OPEN, + "CLOSED": VALVE_CLOSED, +} +validate_valve_state = cv.enum(VALVE_STATES, upper=True) + +ValveOperation = valve_ns.enum("ValveOperation") +VALVE_OPERATIONS = { + "IDLE": ValveOperation.VALVE_OPERATION_IDLE, + "OPENING": ValveOperation.VALVE_OPERATION_OPENING, + "CLOSING": ValveOperation.VALVE_OPERATION_CLOSING, +} +validate_valve_operation = cv.enum(VALVE_OPERATIONS, upper=True) + +# Actions +OpenAction = valve_ns.class_("OpenAction", automation.Action) +CloseAction = valve_ns.class_("CloseAction", automation.Action) +StopAction = valve_ns.class_("StopAction", automation.Action) +ToggleAction = valve_ns.class_("ToggleAction", automation.Action) +ControlAction = valve_ns.class_("ControlAction", automation.Action) +ValvePublishAction = valve_ns.class_("ValvePublishAction", automation.Action) +ValveIsOpenCondition = valve_ns.class_("ValveIsOpenCondition", Condition) +ValveIsClosedCondition = valve_ns.class_("ValveIsClosedCondition", Condition) + +# Triggers +ValveOpenTrigger = valve_ns.class_("ValveOpenTrigger", automation.Trigger.template()) +ValveClosedTrigger = valve_ns.class_( + "ValveClosedTrigger", automation.Trigger.template() +) + +CONF_ON_CLOSED = "on_closed" + +VALVE_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(Valve), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTValveComponent), + cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), + cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_POSITION_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), + cv.Optional(CONF_ON_OPEN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValveOpenTrigger), + } + ), + cv.Optional(CONF_ON_CLOSED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValveClosedTrigger), + } + ), + } + ) +) + + +async def setup_valve_core_(var, config): + await setup_entity(var, config) + + if device_class_config := config.get(CONF_DEVICE_CLASS): + cg.add(var.set_device_class(device_class_config)) + + for conf in config.get(CONF_ON_OPEN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CLOSED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + if mqtt_id_config := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id_config, var) + await mqtt.register_mqtt_component(mqtt_, config) + + if position_state_topic_config := config.get(CONF_POSITION_STATE_TOPIC): + cg.add(mqtt_.set_custom_position_state_topic(position_state_topic_config)) + if position_command_topic_config := config.get(CONF_POSITION_COMMAND_TOPIC): + cg.add( + mqtt_.set_custom_position_command_topic(position_command_topic_config) + ) + + if web_server_config := config.get(CONF_WEB_SERVER): + await web_server.add_entity_config(var, web_server_config) + + +async def register_valve(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_valve(var)) + await setup_valve_core_(var, config) + + +async def new_valve(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_valve(var, config) + return var + + +VALVE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Valve), + } +) + + +@automation.register_action("valve.open", OpenAction, VALVE_ACTION_SCHEMA) +async def valve_open_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("valve.close", CloseAction, VALVE_ACTION_SCHEMA) +async def valve_close_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("valve.stop", StopAction, VALVE_ACTION_SCHEMA) +async def valve_stop_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("valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA) +def valve_toggle_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) + + +VALVE_CONTROL_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Valve), + cv.Optional(CONF_STOP): cv.templatable(cv.boolean), + cv.Exclusive(CONF_STATE, "pos"): cv.templatable(validate_valve_state), + cv.Exclusive(CONF_POSITION, "pos"): cv.templatable(cv.percentage), + } +) + + +@automation.register_action("valve.control", ControlAction, VALVE_CONTROL_ACTION_SCHEMA) +async def valve_control_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if stop_config := config.get(CONF_STOP): + template_ = await cg.templatable(stop_config, args, bool) + cg.add(var.set_stop(template_)) + if state_config := config.get(CONF_STATE): + template_ = await cg.templatable(state_config, args, float) + cg.add(var.set_position(template_)) + if (position_config := config.get(CONF_POSITION)) is not None: + template_ = await cg.templatable(position_config, args, float) + cg.add(var.set_position(template_)) + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_VALVE") + cg.add_global(valve_ns.using) diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h new file mode 100644 index 0000000000..24c94a5570 --- /dev/null +++ b/esphome/components/valve/automation.h @@ -0,0 +1,129 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "valve.h" + +namespace esphome { +namespace valve { + +template class OpenAction : public Action { + public: + explicit OpenAction(Valve *valve) : valve_(valve) {} + + void play(Ts... x) override { this->valve_->make_call().set_command_open().perform(); } + + protected: + Valve *valve_; +}; + +template class CloseAction : public Action { + public: + explicit CloseAction(Valve *valve) : valve_(valve) {} + + void play(Ts... x) override { this->valve_->make_call().set_command_close().perform(); } + + protected: + Valve *valve_; +}; + +template class StopAction : public Action { + public: + explicit StopAction(Valve *valve) : valve_(valve) {} + + void play(Ts... x) override { this->valve_->make_call().set_command_stop().perform(); } + + protected: + Valve *valve_; +}; + +template class ToggleAction : public Action { + public: + explicit ToggleAction(Valve *valve) : valve_(valve) {} + + void play(Ts... x) override { this->valve_->make_call().set_command_toggle().perform(); } + + protected: + Valve *valve_; +}; + +template class ControlAction : public Action { + public: + explicit ControlAction(Valve *valve) : valve_(valve) {} + + TEMPLATABLE_VALUE(bool, stop) + TEMPLATABLE_VALUE(float, position) + + void play(Ts... x) override { + auto call = this->valve_->make_call(); + if (this->stop_.has_value()) + call.set_stop(this->stop_.value(x...)); + if (this->position_.has_value()) + call.set_position(this->position_.value(x...)); + call.perform(); + } + + protected: + Valve *valve_; +}; + +template class ValvePublishAction : public Action { + public: + ValvePublishAction(Valve *valve) : valve_(valve) {} + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(ValveOperation, current_operation) + + void play(Ts... x) override { + if (this->position_.has_value()) + this->valve_->position = this->position_.value(x...); + if (this->current_operation_.has_value()) + this->valve_->current_operation = this->current_operation_.value(x...); + this->valve_->publish_state(); + } + + protected: + Valve *valve_; +}; + +template class ValveIsOpenCondition : public Condition { + public: + ValveIsOpenCondition(Valve *valve) : valve_(valve) {} + bool check(Ts... x) override { return this->valve_->is_fully_open(); } + + protected: + Valve *valve_; +}; + +template class ValveIsClosedCondition : public Condition { + public: + ValveIsClosedCondition(Valve *valve) : valve_(valve) {} + bool check(Ts... x) override { return this->valve_->is_fully_closed(); } + + protected: + Valve *valve_; +}; + +class ValveOpenTrigger : public Trigger<> { + public: + ValveOpenTrigger(Valve *a_valve) { + a_valve->add_on_state_callback([this, a_valve]() { + if (a_valve->is_fully_open()) { + this->trigger(); + } + }); + } +}; + +class ValveClosedTrigger : public Trigger<> { + public: + ValveClosedTrigger(Valve *a_valve) { + a_valve->add_on_state_callback([this, a_valve]() { + if (a_valve->is_fully_closed()) { + this->trigger(); + } + }); + } +}; + +} // namespace valve +} // namespace esphome diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp new file mode 100644 index 0000000000..d1ec17945a --- /dev/null +++ b/esphome/components/valve/valve.cpp @@ -0,0 +1,179 @@ +#include "valve.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace valve { + +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) { + if (pos == VALVE_OPEN) { + return "OPEN"; + } else if (pos == VALVE_CLOSED) { + return "CLOSE"; + } else { + return "UNKNOWN"; + } +} +const char *valve_operation_to_str(ValveOperation op) { + switch (op) { + case VALVE_OPERATION_IDLE: + return "IDLE"; + case VALVE_OPERATION_OPENING: + return "OPENING"; + case VALVE_OPERATION_CLOSING: + return "CLOSING"; + default: + return "UNKNOWN"; + } +} + +Valve::Valve() : position{VALVE_OPEN} {} + +ValveCall::ValveCall(Valve *parent) : parent_(parent) {} +ValveCall &ValveCall::set_command(const char *command) { + if (strcasecmp(command, "OPEN") == 0) { + this->set_command_open(); + } else if (strcasecmp(command, "CLOSE") == 0) { + this->set_command_close(); + } else if (strcasecmp(command, "STOP") == 0) { + this->set_command_stop(); + } else if (strcasecmp(command, "TOGGLE") == 0) { + this->set_command_toggle(); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); + } + return *this; +} +ValveCall &ValveCall::set_command_open() { + this->position_ = VALVE_OPEN; + return *this; +} +ValveCall &ValveCall::set_command_close() { + this->position_ = VALVE_CLOSED; + return *this; +} +ValveCall &ValveCall::set_command_stop() { + this->stop_ = true; + return *this; +} +ValveCall &ValveCall::set_command_toggle() { + this->toggle_ = true; + return *this; +} +ValveCall &ValveCall::set_position(float position) { + this->position_ = position; + return *this; +} +void ValveCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + auto traits = this->parent_->get_traits(); + this->validate_(); + if (this->stop_) { + ESP_LOGD(TAG, " Command: STOP"); + } + if (this->position_.has_value()) { + 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_)); + } + } + if (this->toggle_.has_value()) { + ESP_LOGD(TAG, " Command: TOGGLE"); + } + this->parent_->control(*this); +} +const optional &ValveCall::get_position() const { return this->position_; } +const optional &ValveCall::get_toggle() const { return this->toggle_; } +void ValveCall::validate_() { + auto traits = this->parent_->get_traits(); + if (this->position_.has_value()) { + auto pos = *this->position_; + if (!traits.get_supports_position() && pos != VALVE_OPEN && pos != VALVE_CLOSED) { + ESP_LOGW(TAG, "'%s' - This valve device does not support setting position!", this->parent_->get_name().c_str()); + this->position_.reset(); + } else if (pos < 0.0f || pos > 1.0f) { + ESP_LOGW(TAG, "'%s' - Position %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), pos); + this->position_ = clamp(pos, 0.0f, 1.0f); + } + } + if (this->toggle_.has_value()) { + if (!traits.get_supports_toggle()) { + ESP_LOGW(TAG, "'%s' - This valve device does not support toggle!", this->parent_->get_name().c_str()); + this->toggle_.reset(); + } + } + if (this->stop_) { + if (this->position_.has_value()) { + ESP_LOGW(TAG, "Cannot set position when stopping a valve!"); + this->position_.reset(); + } + if (this->toggle_.has_value()) { + ESP_LOGW(TAG, "Cannot set toggle when stopping a valve!"); + this->toggle_.reset(); + } + } +} +ValveCall &ValveCall::set_stop(bool stop) { + this->stop_ = stop; + return *this; +} +bool ValveCall::get_stop() const { return this->stop_; } + +ValveCall Valve::make_call() { return {this}; } + +void Valve::add_on_state_callback(std::function &&f) { this->state_callback_.add(std::move(f)); } +void Valve::publish_state(bool save) { + this->position = clamp(this->position, 0.0f, 1.0f); + + ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str()); + auto traits = this->get_traits(); + if (traits.get_supports_position()) { + ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); + } else { + if (this->position == VALVE_OPEN) { + ESP_LOGD(TAG, " State: OPEN"); + } else if (this->position == VALVE_CLOSED) { + ESP_LOGD(TAG, " State: CLOSED"); + } else { + ESP_LOGD(TAG, " State: UNKNOWN"); + } + } + ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation)); + + this->state_callback_.call(); + + if (save) { + ValveRestoreState restore{}; + memset(&restore, 0, sizeof(restore)); + restore.position = this->position; + this->rtc_.save(&restore); + } +} +optional Valve::restore_state_() { + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + ValveRestoreState recovered{}; + if (!this->rtc_.load(&recovered)) + return {}; + return recovered; +} + +bool Valve::is_fully_open() const { return this->position == VALVE_OPEN; } +bool Valve::is_fully_closed() const { return this->position == VALVE_CLOSED; } + +ValveCall ValveRestoreState::to_call(Valve *valve) { + auto call = valve->make_call(); + call.set_position(this->position); + return call; +} +void ValveRestoreState::apply(Valve *valve) { + valve->position = this->position; + valve->publish_state(); +} + +} // namespace valve +} // namespace esphome diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h new file mode 100644 index 0000000000..0e14a8d8f0 --- /dev/null +++ b/esphome/components/valve/valve.h @@ -0,0 +1,152 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" +#include "valve_traits.h" + +namespace esphome { +namespace valve { + +const extern float VALVE_OPEN; +const extern float VALVE_CLOSED; + +#define LOG_VALVE(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + auto traits_ = (obj)->get_traits(); \ + if (traits_.get_is_assumed_state()) { \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ + } \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + } \ + } + +class Valve; + +class ValveCall { + public: + ValveCall(Valve *parent); + + /// Set the command as a string, "STOP", "OPEN", "CLOSE", "TOGGLE". + ValveCall &set_command(const char *command); + /// Set the command to open the valve. + ValveCall &set_command_open(); + /// Set the command to close the valve. + ValveCall &set_command_close(); + /// Set the command to stop the valve. + ValveCall &set_command_stop(); + /// Set the command to toggle the valve. + ValveCall &set_command_toggle(); + /// Set the call to a certain target position. + ValveCall &set_position(float position); + /// Set whether this valve call should stop the valve. + ValveCall &set_stop(bool stop); + + /// Perform the valve call. + void perform(); + + const optional &get_position() const; + bool get_stop() const; + const optional &get_toggle() const; + + protected: + void validate_(); + + Valve *parent_; + bool stop_{false}; + optional position_{}; + optional toggle_{}; +}; + +/// Struct used to store the restored state of a valve +struct ValveRestoreState { + float position; + + /// Convert this struct to a valve call that can be performed. + ValveCall to_call(Valve *valve); + /// Apply these settings to the valve + void apply(Valve *valve); +} __attribute__((packed)); + +/// Enum encoding the current operation of a valve. +enum ValveOperation : uint8_t { + /// The valve is currently idle (not moving) + VALVE_OPERATION_IDLE = 0, + /// The valve is currently opening. + VALVE_OPERATION_OPENING, + /// The valve is currently closing. + VALVE_OPERATION_CLOSING, +}; + +const char *valve_operation_to_str(ValveOperation op); + +/** Base class for all valve devices. + * + * Valves currently have three properties: + * - position - The current position of the valve from 0.0 (fully closed) to 1.0 (fully open). + * For valves with only binary OPEN/CLOSED position this will always be either 0.0 or 1.0 + * - current_operation - The operation the valve is currently performing, this can + * be one of IDLE, OPENING and CLOSING. + * + * For users: All valve operations must be performed over the .make_call() interface. + * To command a valve, use .make_call() to create a call object, set all properties + * you wish to set, and activate the command with .perform(). + * For reading out the current values of the valve, use the public .position, etc. + * properties (these are read-only for users) + * + * For integrations: Integrations must implement two methods: control() and get_traits(). + * Control will be called with the arguments supplied by the user and should be used + * to control all values of the valve. Also implement get_traits() to return what operations + * the valve supports. + */ +class Valve : public EntityBase, public EntityBase_DeviceClass { + public: + explicit Valve(); + + /// The current operation of the valve (idle, opening, closing). + ValveOperation current_operation{VALVE_OPERATION_IDLE}; + /** The position of the valve from 0.0 (fully closed) to 1.0 (fully open). + * + * For binary valves this is always equals to 0.0 or 1.0 (see also VALVE_OPEN and + * VALVE_CLOSED constants). + */ + float position; + + /// Construct a new valve call used to control the valve. + ValveCall make_call(); + + void add_on_state_callback(std::function &&f); + + /** Publish the current state of the valve. + * + * First set the .position, etc. values and then call this method + * to publish the state of the valve. + * + * @param save Whether to save the updated values in RTC area. + */ + void publish_state(bool save = true); + + virtual ValveTraits get_traits() = 0; + + /// Helper method to check if the valve is fully open. Equivalent to comparing .position against 1.0 + bool is_fully_open() const; + /// Helper method to check if the valve is fully closed. Equivalent to comparing .position against 0.0 + bool is_fully_closed() const; + + protected: + friend ValveCall; + + virtual void control(const ValveCall &call) = 0; + + optional restore_state_(); + + CallbackManager state_callback_{}; + + ESPPreferenceObject rtc_; +}; + +} // namespace valve +} // namespace esphome diff --git a/esphome/components/valve/valve_traits.h b/esphome/components/valve/valve_traits.h new file mode 100644 index 0000000000..7e9aab2f26 --- /dev/null +++ b/esphome/components/valve/valve_traits.h @@ -0,0 +1,27 @@ +#pragma once + +namespace esphome { +namespace valve { + +class ValveTraits { + public: + ValveTraits() = default; + + bool get_is_assumed_state() const { return this->is_assumed_state_; } + void set_is_assumed_state(bool is_assumed_state) { this->is_assumed_state_ = is_assumed_state; } + bool get_supports_position() const { return this->supports_position_; } + void set_supports_position(bool supports_position) { this->supports_position_ = supports_position; } + bool get_supports_toggle() const { return this->supports_toggle_; } + void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; } + bool get_supports_stop() const { return this->supports_stop_; } + void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; } + + protected: + bool is_assumed_state_{false}; + bool supports_position_{false}; + bool supports_toggle_{false}; + bool supports_stop_{false}; +}; + +} // namespace valve +} // namespace esphome diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index 70f130e23b..99a473a3dc 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -15,6 +15,7 @@ VBus = vbus_ns.class_("VBus", uart.UARTDevice, cg.Component) CONF_VBUS_ID = "vbus_id" CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus" +CONF_DELTASOL_BS_2009 = "deltasol_bs_2009" CONF_DELTASOL_C = "deltasol_c" CONF_DELTASOL_CS2 = "deltasol_cs2" CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py index 9901fb2724..70fbda2d1f 100644 --- a/esphome/components/vbus/binary_sensor/__init__.py +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -18,12 +18,14 @@ from .. import ( VBus, CONF_VBUS_ID, CONF_DELTASOL_BS_PLUS, + CONF_DELTASOL_BS_2009, CONF_DELTASOL_C, CONF_DELTASOL_CS2, CONF_DELTASOL_CS_PLUS, ) DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component) +DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) @@ -42,6 +44,7 @@ CONF_COLLECTOR_FROST = "collector_frost" CONF_TUBE_COLLECTOR = "tube_collector" CONF_RECOOLING = "recooling" CONF_HQM = "hqm" +CONF_FROST_PROTECTION_ACTIVE = "frost_protection_active" CONFIG_SCHEMA = cv.typed_schema( { @@ -87,6 +90,33 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS_2009: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS_2009), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional( + CONF_FROST_PROTECTION_ACTIVE + ): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -222,6 +252,28 @@ async def to_code(config): sens = await binary_sensor.new_binary_sensor(config[CONF_HQM]) cg.add(var.set_hqm_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS_2009: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x427B)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + if CONF_FROST_PROTECTION_ACTIVE in config: + sens = await binary_sensor.new_binary_sensor( + config[CONF_FROST_PROTECTION_ACTIVE] + ) + cg.add(var.set_frost_protection_active_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp index 6edbae22ba..4ccd149935 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -50,6 +50,28 @@ void DeltaSolBSPlusBSensor::handle_message(std::vector &message) { this->hqm_bsensor_->publish_state(message[15] & 0x20); } +void DeltaSolBS2009BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol BS 2009:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Frost Protection Active", this->frost_protection_active_bsensor_); +} + +void DeltaSolBS2009BSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[20] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[20] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[20] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[20] & 8); + if (this->frost_protection_active_bsensor_ != nullptr) + this->frost_protection_active_bsensor_->publish_state(message[25] & 1); +} + void DeltaSolCBSensor::dump_config() { ESP_LOGCONFIG(TAG, "Deltasol C:"); LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); @@ -125,8 +147,9 @@ void VBusCustomBSensor::dump_config() { ESP_LOGCONFIG(TAG, " Command: 0x%04x", this->command_); } ESP_LOGCONFIG(TAG, " Binary Sensors:"); - for (VBusCustomSubBSensor *bsensor : this->bsensors_) + for (VBusCustomSubBSensor *bsensor : this->bsensors_) { LOG_BINARY_SENSOR(" ", "-", bsensor); + } } void VBusCustomBSensor::handle_message(std::vector &message) { diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index c0a823a0ab..146aa1b673 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -39,6 +39,27 @@ class DeltaSolBSPlusBSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; +class DeltaSolBS2009BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + void set_frost_protection_active_bsensor(binary_sensor::BinarySensor *bsensor) { + this->frost_protection_active_bsensor_ = bsensor; + } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *frost_protection_active_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + class DeltaSolCBSensor : public VBusListener, public Component { public: void dump_config() override; diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index bce28758ce..2b89da6d32 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -22,6 +22,7 @@ from esphome.const import ( ICON_THERMOMETER, ICON_TIMER, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, UNIT_HOUR, UNIT_MINUTE, @@ -33,12 +34,14 @@ from .. import ( VBus, CONF_VBUS_ID, CONF_DELTASOL_BS_PLUS, + CONF_DELTASOL_BS_2009, CONF_DELTASOL_C, CONF_DELTASOL_CS2, CONF_DELTASOL_CS_PLUS, ) DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component) +DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) @@ -126,7 +129,88 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_BS_2009: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS_2009), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, @@ -207,7 +291,7 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, @@ -270,7 +354,7 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_VERSION): sensor.sensor_schema( accuracy_decimals=2, @@ -350,7 +434,7 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, @@ -437,6 +521,44 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_VERSION]) cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS_2009: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x427B)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index 8261773431..e81c0486d4 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -57,6 +57,47 @@ void DeltaSolBSPlusSensor::handle_message(std::vector &message) { this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f); } +void DeltaSolBS2009Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol BS 2009:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolBS2009Sensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[12]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 10)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 18)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state(get_u16(message, 28) + get_u16(message, 30) * 1000); + } + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 22)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 32) * 0.01f); +} + void DeltaSolCSensor::dump_config() { ESP_LOGCONFIG(TAG, "Deltasol C:"); LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); @@ -166,9 +207,9 @@ void DeltaSolCSPlusSensor::handle_message(std::vector &message) { if (this->heat_quantity_sensor_ != nullptr) this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28)); if (this->time_sensor_ != nullptr) - this->time_sensor_->publish_state(get_u16(message, 12)); + this->time_sensor_->publish_state(get_u16(message, 22)); if (this->version_sensor_ != nullptr) - this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f); + this->version_sensor_->publish_state(get_u16(message, 32) * 0.01f); if (this->flow_rate_sensor_ != nullptr) this->flow_rate_sensor_->publish_state(get_u16(message, 38)); } @@ -191,8 +232,9 @@ void VBusCustomSensor::dump_config() { ESP_LOGCONFIG(TAG, " Command: 0x%04x", this->command_); } ESP_LOGCONFIG(TAG, " Sensors:"); - for (VBusCustomSubSensor *sensor : this->sensors_) + for (VBusCustomSubSensor *sensor : this->sensors_) { LOG_SENSOR(" ", "-", sensor); + } } void VBusCustomSensor::handle_message(std::vector &message) { diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h index 6ba752b68c..d5535b2019 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.h +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -37,6 +37,37 @@ class DeltaSolBSPlusSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; +class DeltaSolBS2009Sensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + class DeltaSolCSensor : public VBusListener, public Component { public: void dump_config() override; diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index c9758891cc..e474dcfe17 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -1,6 +1,7 @@ #include "vbus.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include namespace esphome { namespace vbus { @@ -64,8 +65,8 @@ void VBus::loop() { uint16_t id = (this->buffer_[8] << 8) + this->buffer_[7]; uint32_t value = (this->buffer_[12] << 24) + (this->buffer_[11] << 16) + (this->buffer_[10] << 8) + this->buffer_[9]; - ESP_LOGV(TAG, "P1 C%04x %04x->%04x: %04x %04x (%d)", this->command_, this->source_, this->dest_, id, value, - value); + ESP_LOGV(TAG, "P1 C%04x %04x->%04x: %04x %04" PRIx32 " (%" PRIu32 ")", this->command_, this->source_, + this->dest_, id, value, value); } else if ((this->protocol_ == 0x10) && (this->buffer_.size() == 9)) { if (!checksum(this->buffer_.data(), 0, 9)) { ESP_LOGE(TAG, "P1 checksum failed"); diff --git a/esphome/components/veml3235/__init__.py b/esphome/components/veml3235/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/veml3235/sensor.py b/esphome/components/veml3235/sensor.py new file mode 100644 index 0000000000..79ba510e41 --- /dev/null +++ b/esphome/components/veml3235/sensor.py @@ -0,0 +1,84 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_GAIN, + CONF_INTEGRATION_TIME, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["i2c"] + +CONF_AUTO_GAIN = "auto_gain" +CONF_AUTO_GAIN_THRESHOLD_HIGH = "auto_gain_threshold_high" +CONF_AUTO_GAIN_THRESHOLD_LOW = "auto_gain_threshold_low" +CONF_DIGITAL_GAIN = "digital_gain" + +veml3235_ns = cg.esphome_ns.namespace("veml3235") + +VEML3235Sensor = veml3235_ns.class_( + "VEML3235Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) +VEML3235IntegrationTime = veml3235_ns.enum("VEML3235IntegrationTime") +VEML3235_INTEGRATION_TIMES = { + "50ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_50MS, + "100ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_100MS, + "200ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_200MS, + "400ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_400MS, + "800ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_800MS, +} +VEML3235ComponentDigitalGain = veml3235_ns.enum("VEML3235ComponentDigitalGain") +DIGITAL_GAINS = { + "1X": VEML3235ComponentDigitalGain.VEML3235_DIGITAL_GAIN_1X, + "2X": VEML3235ComponentDigitalGain.VEML3235_DIGITAL_GAIN_2X, +} +VEML3235ComponentGain = veml3235_ns.enum("VEML3235ComponentGain") +GAINS = { + "1X": VEML3235ComponentGain.VEML3235_GAIN_1X, + "2X": VEML3235ComponentGain.VEML3235_GAIN_2X, + "4X": VEML3235ComponentGain.VEML3235_GAIN_4X, + "AUTO": VEML3235ComponentGain.VEML3235_GAIN_AUTO, +} + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + VEML3235Sensor, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Optional(CONF_DIGITAL_GAIN, default="1X"): cv.enum( + DIGITAL_GAINS, upper=True + ), + cv.Optional(CONF_AUTO_GAIN, default=True): cv.boolean, + cv.Optional(CONF_AUTO_GAIN_THRESHOLD_HIGH, default="90%"): cv.percentage, + cv.Optional(CONF_AUTO_GAIN_THRESHOLD_LOW, default="20%"): cv.percentage, + cv.Optional(CONF_GAIN, default="1X"): cv.enum(GAINS, upper=True), + cv.Optional(CONF_INTEGRATION_TIME, default="50ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.enum(VEML3235_INTEGRATION_TIMES, lower=True), + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x10)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_auto_gain(config[CONF_AUTO_GAIN])) + cg.add(var.set_auto_gain_threshold_high(config[CONF_AUTO_GAIN_THRESHOLD_HIGH])) + cg.add(var.set_auto_gain_threshold_low(config[CONF_AUTO_GAIN_THRESHOLD_LOW])) + cg.add(var.set_digital_gain(DIGITAL_GAINS[config[CONF_DIGITAL_GAIN]])) + cg.add(var.set_gain(GAINS[config[CONF_GAIN]])) + cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp new file mode 100644 index 0000000000..2410bfdda2 --- /dev/null +++ b/esphome/components/veml3235/veml3235.cpp @@ -0,0 +1,230 @@ +#include "veml3235.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace veml3235 { + +static const char *const TAG = "veml3235.sensor"; + +void VEML3235Sensor::setup() { + uint8_t device_id[] = {0, 0}; + + ESP_LOGCONFIG(TAG, "Setting up VEML3235 '%s'...", this->name_.c_str()); + + if (!this->refresh_config_reg()) { + ESP_LOGE(TAG, "Unable to write configuration"); + this->mark_failed(); + return; + } + if ((this->write(&ID_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(device_id, 2)) { + ESP_LOGE(TAG, "Unable to read ID"); + this->mark_failed(); + return; + } else if (device_id[0] != DEVICE_ID) { + ESP_LOGE(TAG, "Incorrect device ID - expected 0x%.2x, read 0x%.2x", DEVICE_ID, device_id[0]); + this->mark_failed(); + return; + } +} + +bool VEML3235Sensor::refresh_config_reg(bool force_on) { + uint16_t data = this->power_on_ || force_on ? 0 : SHUTDOWN_BITS; + + data |= (uint16_t(this->integration_time_ << CONFIG_REG_IT_BIT)); + data |= (uint16_t(this->digital_gain_ << CONFIG_REG_DG_BIT)); + data |= (uint16_t(this->gain_ << CONFIG_REG_G_BIT)); + data |= 0x1; // mandatory 1 here per RM + + ESP_LOGVV(TAG, "Writing 0x%.4x to register 0x%.2x", data, CONFIG_REG); + return this->write_byte_16(CONFIG_REG, data); +} + +float VEML3235Sensor::read_lx_() { + if (!this->power_on_) { // if off, turn on + if (!this->refresh_config_reg(true)) { + ESP_LOGW(TAG, "Turning on failed"); + this->status_set_warning(); + return NAN; + } + delay(4); // from RM: a wait time of 4 ms should be observed before the first measurement is picked up, to allow + // for a correct start of the signal processor and oscillator + } + + uint8_t als_regs[] = {0, 0}; + if ((this->write(&ALS_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(als_regs, 2)) { + this->status_set_warning(); + return NAN; + } + + this->status_clear_warning(); + + float als_raw_value_multiplier = LUX_MULTIPLIER_BASE; + uint16_t als_raw_value = encode_uint16(als_regs[1], als_regs[0]); + // determine multiplier value based on gains and integration time + if (this->digital_gain_ == VEML3235_DIGITAL_GAIN_1X) { + als_raw_value_multiplier *= 2; + } + switch (this->gain_) { + case VEML3235_GAIN_1X: + als_raw_value_multiplier *= 4; + break; + case VEML3235_GAIN_2X: + als_raw_value_multiplier *= 2; + break; + default: + break; + } + switch (this->integration_time_) { + case VEML3235_INTEGRATION_TIME_50MS: + als_raw_value_multiplier *= 16; + break; + case VEML3235_INTEGRATION_TIME_100MS: + als_raw_value_multiplier *= 8; + break; + case VEML3235_INTEGRATION_TIME_200MS: + als_raw_value_multiplier *= 4; + break; + case VEML3235_INTEGRATION_TIME_400MS: + als_raw_value_multiplier *= 2; + break; + default: + break; + } + // finally, determine and return the actual lux value + float lx = float(als_raw_value) * als_raw_value_multiplier; + ESP_LOGVV(TAG, "'%s': ALS raw = %u, multiplier = %.5f", this->get_name().c_str(), als_raw_value, + als_raw_value_multiplier); + ESP_LOGD(TAG, "'%s': Illuminance = %.4flx", this->get_name().c_str(), lx); + + if (!this->power_on_) { // turn off if required + if (!this->refresh_config_reg()) { + ESP_LOGW(TAG, "Turning off failed"); + this->status_set_warning(); + } + } + + if (this->auto_gain_) { + this->adjust_gain_(als_raw_value); + } + + return lx; +} + +void VEML3235Sensor::adjust_gain_(const uint16_t als_raw_value) { + if ((als_raw_value > UINT16_MAX * this->auto_gain_threshold_low_) && + (als_raw_value < UINT16_MAX * this->auto_gain_threshold_high_)) { + return; + } + + if (als_raw_value >= UINT16_MAX * 0.9) { // over-saturated, reset all gains and start over + this->digital_gain_ = VEML3235_DIGITAL_GAIN_1X; + this->gain_ = VEML3235_GAIN_1X; + this->integration_time_ = VEML3235_INTEGRATION_TIME_50MS; + this->refresh_config_reg(); + return; + } + + if (this->gain_ != VEML3235_GAIN_4X) { // increase gain if possible + switch (this->gain_) { + case VEML3235_GAIN_1X: + this->gain_ = VEML3235_GAIN_2X; + break; + case VEML3235_GAIN_2X: + this->gain_ = VEML3235_GAIN_4X; + break; + default: + break; + } + this->refresh_config_reg(); + return; + } + // gain is maxed out; reset it and try to increase digital gain + if (this->digital_gain_ != VEML3235_DIGITAL_GAIN_2X) { // increase digital gain if possible + this->digital_gain_ = VEML3235_DIGITAL_GAIN_2X; + this->gain_ = VEML3235_GAIN_1X; + this->refresh_config_reg(); + return; + } + // digital gain is maxed out; reset it and try to increase integration time + if (this->integration_time_ != VEML3235_INTEGRATION_TIME_800MS) { // increase integration time if possible + switch (this->integration_time_) { + case VEML3235_INTEGRATION_TIME_50MS: + this->integration_time_ = VEML3235_INTEGRATION_TIME_100MS; + break; + case VEML3235_INTEGRATION_TIME_100MS: + this->integration_time_ = VEML3235_INTEGRATION_TIME_200MS; + break; + case VEML3235_INTEGRATION_TIME_200MS: + this->integration_time_ = VEML3235_INTEGRATION_TIME_400MS; + break; + case VEML3235_INTEGRATION_TIME_400MS: + this->integration_time_ = VEML3235_INTEGRATION_TIME_800MS; + break; + default: + break; + } + this->digital_gain_ = VEML3235_DIGITAL_GAIN_1X; + this->gain_ = VEML3235_GAIN_1X; + this->refresh_config_reg(); + return; + } +} + +void VEML3235Sensor::dump_config() { + uint8_t digital_gain = 1; + uint8_t gain = 1; + uint16_t integration_time = 0; + + if (this->digital_gain_ == VEML3235_DIGITAL_GAIN_2X) { + digital_gain = 2; + } + switch (this->gain_) { + case VEML3235_GAIN_2X: + gain = 2; + break; + case VEML3235_GAIN_4X: + gain = 4; + break; + default: + break; + } + switch (this->integration_time_) { + case VEML3235_INTEGRATION_TIME_50MS: + integration_time = 50; + break; + case VEML3235_INTEGRATION_TIME_100MS: + integration_time = 100; + break; + case VEML3235_INTEGRATION_TIME_200MS: + integration_time = 200; + break; + case VEML3235_INTEGRATION_TIME_400MS: + integration_time = 400; + break; + case VEML3235_INTEGRATION_TIME_800MS: + integration_time = 800; + break; + default: + break; + } + + LOG_SENSOR("", "VEML3235", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed"); + } + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Auto-gain enabled: %s", YESNO(this->auto_gain_)); + if (this->auto_gain_) { + ESP_LOGCONFIG(TAG, " Auto-gain upper threshold: %f%%", this->auto_gain_threshold_high_ * 100.0); + ESP_LOGCONFIG(TAG, " Auto-gain lower threshold: %f%%", this->auto_gain_threshold_low_ * 100.0); + ESP_LOGCONFIG(TAG, " Values below will be used as initial values only"); + } + ESP_LOGCONFIG(TAG, " Digital gain: %uX", digital_gain); + ESP_LOGCONFIG(TAG, " Gain: %uX", gain); + ESP_LOGCONFIG(TAG, " Integration time: %ums", integration_time); +} + +} // namespace veml3235 +} // namespace esphome diff --git a/esphome/components/veml3235/veml3235.h b/esphome/components/veml3235/veml3235.h new file mode 100644 index 0000000000..2b0d6b23ea --- /dev/null +++ b/esphome/components/veml3235/veml3235.h @@ -0,0 +1,109 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace veml3235 { + +// Register IDs/locations +// +static const uint8_t CONFIG_REG = 0x00; +static const uint8_t W_REG = 0x04; +static const uint8_t ALS_REG = 0x05; +static const uint8_t ID_REG = 0x09; + +// Bit offsets within CONFIG_REG +// +static const uint8_t CONFIG_REG_IT_BIT = 12; +static const uint8_t CONFIG_REG_DG_BIT = 5; +static const uint8_t CONFIG_REG_G_BIT = 3; + +// Other important constants +// +static const uint8_t DEVICE_ID = 0x35; +static const uint16_t SHUTDOWN_BITS = 0x0018; + +// Base multiplier value for lux computation +// +static const float LUX_MULTIPLIER_BASE = 0.00213; + +// Enum for conversion/integration time settings for the VEML3235. +// +// Specific values of the enum constants are register values taken from the VEML3235 datasheet. +// Longer times mean more accurate results, but will take more energy/more time. +// +enum VEML3235ComponentIntegrationTime { + VEML3235_INTEGRATION_TIME_50MS = 0b000, + VEML3235_INTEGRATION_TIME_100MS = 0b001, + VEML3235_INTEGRATION_TIME_200MS = 0b010, + VEML3235_INTEGRATION_TIME_400MS = 0b011, + VEML3235_INTEGRATION_TIME_800MS = 0b100, +}; + +// Enum for digital gain settings for the VEML3235. +// Higher values are better for low light situations, but can increase noise. +// +enum VEML3235ComponentDigitalGain { + VEML3235_DIGITAL_GAIN_1X = 0b0, + VEML3235_DIGITAL_GAIN_2X = 0b1, +}; + +// Enum for gain settings for the VEML3235. +// Higher values are better for low light situations, but can increase noise. +// +enum VEML3235ComponentGain { + VEML3235_GAIN_1X = 0b00, + VEML3235_GAIN_2X = 0b01, + VEML3235_GAIN_4X = 0b11, +}; + +class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override { this->publish_state(this->read_lx_()); } + float get_setup_priority() const override { return setup_priority::DATA; } + + // Used by ESPHome framework. Does NOT actually set the value on the device. + void set_auto_gain(bool auto_gain) { this->auto_gain_ = auto_gain; } + void set_auto_gain_threshold_high(float auto_gain_threshold_high) { + this->auto_gain_threshold_high_ = auto_gain_threshold_high; + } + void set_auto_gain_threshold_low(float auto_gain_threshold_low) { + this->auto_gain_threshold_low_ = auto_gain_threshold_low; + } + void set_power_on(bool power_on) { this->power_on_ = power_on; } + void set_digital_gain(VEML3235ComponentDigitalGain digital_gain) { this->digital_gain_ = digital_gain; } + void set_gain(VEML3235ComponentGain gain) { this->gain_ = gain; } + void set_integration_time(VEML3235ComponentIntegrationTime integration_time) { + this->integration_time_ = integration_time; + } + + bool auto_gain() { return this->auto_gain_; } + float auto_gain_threshold_high() { return this->auto_gain_threshold_high_; } + float auto_gain_threshold_low() { return this->auto_gain_threshold_low_; } + VEML3235ComponentDigitalGain digital_gain() { return this->digital_gain_; } + VEML3235ComponentGain gain() { return this->gain_; } + VEML3235ComponentIntegrationTime integration_time() { return this->integration_time_; } + + // Updates the configuration register on the device + bool refresh_config_reg(bool force_on = false); + + protected: + float read_lx_(); + void adjust_gain_(uint16_t als_raw_value); + + bool auto_gain_{true}; + bool power_on_{true}; + float auto_gain_threshold_high_{0.9}; + float auto_gain_threshold_low_{0.2}; + VEML3235ComponentDigitalGain digital_gain_{VEML3235_DIGITAL_GAIN_1X}; + VEML3235ComponentGain gain_{VEML3235_GAIN_1X}; + VEML3235ComponentIntegrationTime integration_time_{VEML3235_INTEGRATION_TIME_50MS}; +}; + +} // namespace veml3235 +} // namespace esphome diff --git a/esphome/components/veml7700/__init__.py b/esphome/components/veml7700/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/veml7700/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/veml7700/sensor.py b/esphome/components/veml7700/sensor.py new file mode 100644 index 0000000000..308f1c1c00 --- /dev/null +++ b/esphome/components/veml7700/sensor.py @@ -0,0 +1,190 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ACTUAL_GAIN, + CONF_ACTUAL_INTEGRATION_TIME, + CONF_AMBIENT_LIGHT, + CONF_AUTO_MODE, + CONF_FULL_SPECTRUM, + CONF_FULL_SPECTRUM_COUNTS, + CONF_GAIN, + CONF_GLASS_ATTENUATION_FACTOR, + CONF_ID, + CONF_INFRARED, + CONF_INTEGRATION_TIME, + CONF_NAME, + DEVICE_CLASS_ILLUMINANCE, + ICON_BRIGHTNESS_5, + ICON_BRIGHTNESS_6, + ICON_TIMER, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, + UNIT_MILLISECOND, +) + +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +UNIT_COUNTS = "#" +ICON_MULTIPLICATION = "mdi:multiplication" +ICON_BRIGHTNESS_7 = "mdi:brightness-7" + +CONF_AMBIENT_LIGHT_COUNTS = "ambient_light_counts" +CONF_LUX_COMPENSATION = "lux_compensation" + +veml7700_ns = cg.esphome_ns.namespace("veml7700") + +VEML7700Component = veml7700_ns.class_( + "VEML7700Component", cg.PollingComponent, i2c.I2CDevice +) + +Gain = veml7700_ns.enum("Gain") +GAINS = { + "1/8X": Gain.X_1_8, + "1/4X": Gain.X_1_4, + "1X": Gain.X_1, + "2X": Gain.X_2, +} + +IntegrationTime = veml7700_ns.enum("IntegrationTime") +INTEGRATION_TIMES = { + 25: IntegrationTime.INTEGRATION_TIME_25MS, + 50: IntegrationTime.INTEGRATION_TIME_50MS, + 100: IntegrationTime.INTEGRATION_TIME_100MS, + 200: IntegrationTime.INTEGRATION_TIME_200MS, + 400: IntegrationTime.INTEGRATION_TIME_400MS, + 800: IntegrationTime.INTEGRATION_TIME_800MS, +} + + +def validate_integration_time(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(INTEGRATION_TIMES, int=True)(value) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(VEML7700Component), + cv.Optional(CONF_AUTO_MODE, default=True): cv.boolean, + cv.Optional(CONF_GAIN, default="1/8X"): cv.enum(GAINS, upper=True), + cv.Optional( + CONF_INTEGRATION_TIME, default="100ms" + ): validate_integration_time, + cv.Optional(CONF_LUX_COMPENSATION, default=True): cv.boolean, + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range( + min=1.0 + ), + cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_AMBIENT_LIGHT_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_FULL_SPECTRUM): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + icon=ICON_BRIGHTNESS_7, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_FULL_SPECTRUM_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_7, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_INFRARED): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTUAL_GAIN): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_MULTIPLICATION, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTUAL_INTEGRATION_TIME): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x10)), +) + + +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) + + if als_config := config.get(CONF_AMBIENT_LIGHT): + sens = await sensor.new_sensor(als_config) + cg.add(var.set_ambient_light_sensor(sens)) + + if als_cnt_config := config.get(CONF_AMBIENT_LIGHT_COUNTS): + sens = await sensor.new_sensor(als_cnt_config) + cg.add(var.set_ambient_light_counts_sensor(sens)) + + if full_spect_config := config.get(CONF_FULL_SPECTRUM): + sens = await sensor.new_sensor(full_spect_config) + cg.add(var.set_white_sensor(sens)) + + if full_spect_cnt_config := config.get(CONF_FULL_SPECTRUM_COUNTS): + sens = await sensor.new_sensor(full_spect_cnt_config) + cg.add(var.set_white_counts_sensor(sens)) + + if infrared_config := config.get(CONF_INFRARED): + sens = await sensor.new_sensor(infrared_config) + cg.add(var.set_infrared_sensor(sens)) + + if act_gain_config := config.get(CONF_ACTUAL_GAIN): + sens = await sensor.new_sensor(act_gain_config) + cg.add(var.set_actual_gain_sensor(sens)) + + if act_itime_config := config.get(CONF_ACTUAL_INTEGRATION_TIME): + sens = await sensor.new_sensor(act_itime_config) + cg.add(var.set_actual_integration_time_sensor(sens)) + + cg.add(var.set_enable_automatic_mode(config[CONF_AUTO_MODE])) + cg.add(var.set_enable_lux_compensation(config[CONF_LUX_COMPENSATION])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) + cg.add(var.set_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR])) diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp new file mode 100644 index 0000000000..a8c1411c68 --- /dev/null +++ b/esphome/components/veml7700/veml7700.cpp @@ -0,0 +1,437 @@ +#include "veml7700.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace veml7700 { + +static const char *const TAG = "veml7700"; +static const size_t VEML_REG_SIZE = 2; + +static float reduce_to_zero(float a, float b) { return (a > b) ? (a - b) : 0; } + +template T get_next(const T (&array)[size], const T val) { + size_t i = 0; + size_t idx = -1; + while (idx == -1 && i < size) { + if (array[i] == val) { + idx = i; + break; + } + i++; + } + if (idx == -1 || i + 1 >= size) + return val; + return array[i + 1]; +} + +template T get_prev(const T (&array)[size], const T val) { + size_t i = size - 1; + size_t idx = -1; + while (idx == -1 && i > 0) { + if (array[i] == val) { + idx = i; + break; + } + i--; + } + if (idx == -1 || i == 0) + return val; + return array[i - 1]; +} + +static uint16_t get_itime_ms(IntegrationTime time) { + uint16_t ms = 0; + switch (time) { + case INTEGRATION_TIME_100MS: + ms = 100; + break; + case INTEGRATION_TIME_200MS: + ms = 200; + break; + case INTEGRATION_TIME_400MS: + ms = 400; + break; + case INTEGRATION_TIME_800MS: + ms = 800; + break; + case INTEGRATION_TIME_50MS: + ms = 50; + break; + case INTEGRATION_TIME_25MS: + ms = 25; + break; + default: + ms = 100; + } + return ms; +} + +static float get_gain_coeff(Gain gain) { + static const float GAIN_FLOAT[GAINS_COUNT] = {1.0f, 2.0f, 0.125f, 0.25f}; + return GAIN_FLOAT[gain & 0b11]; +} + +static const char *get_gain_str(Gain gain) { + static const char *gain_str[GAINS_COUNT] = {"1x", "2x", "1/8x", "1/4x"}; + return gain_str[gain & 0b11]; +} + +void VEML7700Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up VEML7700/6030..."); + + auto err = this->configure_(); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Sensor configuration failed"); + this->mark_failed(); + } else { + this->state_ = State::INITIAL_SETUP_COMPLETED; + } +} + +void VEML7700Component::dump_config() { + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Automatic gain/time: %s", YESNO(this->automatic_mode_enabled_)); + if (!this->automatic_mode_enabled_) { + ESP_LOGCONFIG(TAG, " Gain: %s", get_gain_str(this->gain_)); + ESP_LOGCONFIG(TAG, " Integration time: %d ms", get_itime_ms(this->integration_time_)); + } + ESP_LOGCONFIG(TAG, " Lux compensation: %s", YESNO(this->lux_compensation_enabled_)); + ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_); + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "ALS channel lux", this->ambient_light_sensor_); + LOG_SENSOR(" ", "ALS channel counts", this->ambient_light_counts_sensor_); + LOG_SENSOR(" ", "WHITE channel lux", this->white_sensor_); + LOG_SENSOR(" ", "WHITE channel counts", this->white_counts_sensor_); + LOG_SENSOR(" ", "FAKE_IR channel lux", this->fake_infrared_sensor_); + LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_); + LOG_SENSOR(" ", "Actual integration time", this->actual_integration_time_sensor_); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with I2C VEML7700/6030 failed!"); + } +} + +void VEML7700Component::update() { + if (this->is_ready() && this->state_ == State::IDLE) { + ESP_LOGV(TAG, "Update: Initiating new data collection"); + + this->state_ = this->automatic_mode_enabled_ ? State::COLLECTING_DATA_AUTO : State::COLLECTING_DATA; + + this->readings_.als_counts = 0; + this->readings_.white_counts = 0; + this->readings_.actual_time = this->integration_time_; + this->readings_.actual_gain = this->gain_; + this->readings_.als_lux = 0; + this->readings_.white_lux = 0; + this->readings_.fake_infrared_lux = 0; + } else { + ESP_LOGV(TAG, "Update: Component not ready yet"); + } +} + +void VEML7700Component::loop() { + ErrorCode err = i2c::ERROR_OK; + + if (this->state_ == State::INITIAL_SETUP_COMPLETED) { + // Datasheet: 2.5 ms before the first measurement is needed, allowing for the correct start of the signal processor + // and oscillator. + // Reality: wait for couple integration times to have first samples captured + this->set_timeout(2 * this->integration_time_, [this]() { this->state_ = State::IDLE; }); + } + + if (this->is_ready()) { + switch (this->state_) { + case State::IDLE: + // doing nothing, having best time + break; + + case State::COLLECTING_DATA: + err = this->read_sensor_output_(this->readings_); + this->state_ = (err == i2c::ERROR_OK) ? State::DATA_COLLECTED : State::IDLE; + break; + + case State::COLLECTING_DATA_AUTO: // Automatic mode - we start here to reconfigure device first + case State::DATA_COLLECTED: + if (!this->are_adjustments_required_(this->readings_)) { + this->state_ = State::READY_TO_PUBLISH_PART_1; + } else { + // if sensitivity adjustment needed - + // shutdown device to change config and wait one integration time period + this->state_ = State::ADJUSTMENT_IN_PROGRESS; + err = this->reconfigure_time_and_gain_(this->readings_.actual_time, this->readings_.actual_gain, true); + if (err == i2c::ERROR_OK) { + this->set_timeout(1 * get_itime_ms(this->readings_.actual_time), + [this]() { this->state_ = State::READY_TO_APPLY_ADJUSTMENTS; }); + } else { + this->state_ = State::IDLE; + } + } + break; + + case State::ADJUSTMENT_IN_PROGRESS: + // nothing to be done, just waiting for the timeout + break; + + case State::READY_TO_APPLY_ADJUSTMENTS: + // second stage of sensitivity adjustment - turn device back on + // and wait 2-3 integration time periods to get good data samples + this->state_ = State::ADJUSTMENT_IN_PROGRESS; + err = this->reconfigure_time_and_gain_(this->readings_.actual_time, this->readings_.actual_gain, false); + if (err == i2c::ERROR_OK) { + this->set_timeout(3 * get_itime_ms(this->readings_.actual_time), + [this]() { this->state_ = State::COLLECTING_DATA; }); + } else { + this->state_ = State::IDLE; + } + break; + + case State::READY_TO_PUBLISH_PART_1: + this->status_clear_warning(); + + this->apply_lux_calculation_(this->readings_); + this->apply_lux_compensation_(this->readings_); + this->apply_glass_attenuation_(this->readings_); + + this->publish_data_part_1_(this->readings_); + this->state_ = State::READY_TO_PUBLISH_PART_2; + break; + + case State::READY_TO_PUBLISH_PART_2: + this->publish_data_part_2_(this->readings_); + this->state_ = State::READY_TO_PUBLISH_PART_3; + break; + + case State::READY_TO_PUBLISH_PART_3: + this->publish_data_part_3_(this->readings_); + this->state_ = State::IDLE; + break; + + default: + break; + } + if (err != i2c::ERROR_OK) + this->status_set_warning(); + } +} + +ErrorCode VEML7700Component::configure_() { + ESP_LOGV(TAG, "Configure"); + + ConfigurationRegister als_conf{0}; + als_conf.ALS_INT_EN = false; + als_conf.ALS_PERS = Persistence::PERSISTENCE_1; + als_conf.ALS_IT = this->integration_time_; + als_conf.ALS_GAIN = this->gain_; + + als_conf.ALS_SD = true; + ESP_LOGV(TAG, "Shutdown before config. ALS_CONF_0 to 0x%04X", als_conf.raw); + auto err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Failed to shutdown, I2C error %d", err); + return err; + } + delay(3); + + als_conf.ALS_SD = false; + ESP_LOGV(TAG, "Turning on. Setting ALS_CONF_0 to 0x%04X", als_conf.raw); + err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Failed to turn on, I2C error %d", err); + return err; + } + + PSMRegister psm{0}; + psm.PSM = PSMMode::PSM_MODE_1; + psm.PSM_EN = false; + ESP_LOGV(TAG, "Setting PSM to 0x%04X", psm.raw); + err = this->write_register((uint8_t) CommandRegisters::PWR_SAVING, psm.raw_bytes, VEML_REG_SIZE); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Failed to set PSM, I2C error %d", err); + return err; + } + + return err; +} + +ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Gain gain, bool shutdown) { + ESP_LOGV(TAG, "Reconfigure time and gain (%d ms, %s) %s", get_itime_ms(time), get_gain_str(gain), + shutdown ? "Shutting down" : "Turning back on"); + + ConfigurationRegister als_conf{0}; + als_conf.raw = 0; + + // We have to before changing parameters + als_conf.ALS_SD = shutdown; + als_conf.ALS_INT_EN = false; + als_conf.ALS_PERS = Persistence::PERSISTENCE_1; + als_conf.ALS_IT = time; + als_conf.ALS_GAIN = gain; + auto err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "%s failed", shutdown ? "Shutdown" : "Turn on"); + } + + return err; +} + +ErrorCode VEML7700Component::read_sensor_output_(Readings &data) { + auto als_err = + this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE, false); + if (als_err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Error reading ALS register, err = %d", als_err); + } + auto white_err = + this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE, false); + if (white_err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Error reading WHITE register, err = %d", white_err); + } + + ConfigurationRegister conf{0}; + auto err = + this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE, false); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Error reading ALS_CONF_0 register, err = %d", white_err); + } + data.actual_time = conf.ALS_IT; + data.actual_gain = conf.ALS_GAIN; + + ESP_LOGV(TAG, "Data from sensors: ALS = %d, WHITE = %d, Gain = %s, Time = %d ms", data.als_counts, data.white_counts, + get_gain_str(data.actual_gain), get_itime_ms(data.actual_time)); + return std::max(als_err, white_err); +} + +bool VEML7700Component::are_adjustments_required_(Readings &data) { + // skip first sample in auto mode - + // we need to reconfigure device after last measurement + if (this->state_ == State::COLLECTING_DATA_AUTO) + return true; + + if (!this->automatic_mode_enabled_) + return false; + + // Recommended thresholds as per datasheet + static constexpr uint16_t LOW_INTENSITY_THRESHOLD = 100; + static constexpr uint16_t HIGH_INTENSITY_THRESHOLD = 10000; + + static const IntegrationTime TIMES[INTEGRATION_TIMES_COUNT] = {INTEGRATION_TIME_25MS, INTEGRATION_TIME_50MS, + INTEGRATION_TIME_100MS, INTEGRATION_TIME_200MS, + INTEGRATION_TIME_400MS, INTEGRATION_TIME_800MS}; + static const Gain GAINS[GAINS_COUNT] = {X_1_8, X_1_4, X_1, X_2}; + + if (data.als_counts <= LOW_INTENSITY_THRESHOLD) { + Gain next_gain = get_next(GAINS, data.actual_gain); + if (next_gain != data.actual_gain) { + data.actual_gain = next_gain; + return true; + } + IntegrationTime next_time = get_next(TIMES, data.actual_time); + if (next_time != data.actual_time) { + data.actual_time = next_time; + return true; + } + } else if (data.als_counts >= HIGH_INTENSITY_THRESHOLD) { + Gain prev_gain = get_prev(GAINS, data.actual_gain); + if (prev_gain != data.actual_gain) { + data.actual_gain = prev_gain; + return true; + } + IntegrationTime prev_time = get_prev(TIMES, data.actual_time); + if (prev_time != data.actual_time) { + data.actual_time = prev_time; + return true; + } + } + + // Counts are either good (between thresholds) + // or there is no room to change sensitivity anymore + return false; +} + +void VEML7700Component::apply_lux_calculation_(Readings &data) { + static const float MAX_GAIN = 2.0f; + static const float MAX_ITIME_MS = 800.0f; + static const float MAX_LX_RESOLUTION = 0.0036f; + float lux_resolution = (MAX_ITIME_MS / (float) get_itime_ms(data.actual_time)) * + (MAX_GAIN / get_gain_coeff(data.actual_gain)) * MAX_LX_RESOLUTION; + ESP_LOGV(TAG, "Lux resolution for (%d, %s) = %.4f ", get_itime_ms(data.actual_time), get_gain_str(data.actual_gain), + lux_resolution); + + data.als_lux = lux_resolution * (float) data.als_counts; + data.white_lux = lux_resolution * (float) data.white_counts; + data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux); + + ESP_LOGV(TAG, "%s mode - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx", + this->automatic_mode_enabled_ ? "Automatic" : "Manual", data.als_lux, data.white_lux, + data.fake_infrared_lux); +} + +void VEML7700Component::apply_lux_compensation_(Readings &data) { + if (!this->lux_compensation_enabled_) + return; + auto &local_data = data; + // Always apply correction for G1/4 and G1/8 + // Other Gains G1 and G2 are not supposed to be used for lux > 1000, + // corrections may help, but not a lot. + // + // "Illumination values higher than 1000 lx show non-linearity. + // This non-linearity is the same for all sensors, so a compensation formula can be applied + // if this light level is exceeded" + auto compensate = [&local_data](float &lux) { + auto calculate_high_lux_compensation = [](float lux_veml) -> float { + return (((6.0135e-13 * lux_veml - 9.3924e-9) * lux_veml + 8.1488e-5) * lux_veml + 1.0023) * lux_veml; + }; + + if (lux > 1000.0f || local_data.actual_gain == Gain::X_1_8 || local_data.actual_gain == Gain::X_1_4) { + lux = calculate_high_lux_compensation(lux); + } + }; + + compensate(data.als_lux); + compensate(data.white_lux); + data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux); + + ESP_LOGV(TAG, "Lux compensation - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx", data.als_lux, data.white_lux, + data.fake_infrared_lux); +} + +void VEML7700Component::apply_glass_attenuation_(Readings &data) { + data.als_lux *= this->glass_attenuation_factor_; + data.white_lux *= this->glass_attenuation_factor_; + data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux); + ESP_LOGV(TAG, "Glass attenuation - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx", data.als_lux, data.white_lux, + data.fake_infrared_lux); +} + +void VEML7700Component::publish_data_part_1_(Readings &data) { + if (this->ambient_light_sensor_ != nullptr) { + this->ambient_light_sensor_->publish_state(data.als_lux); + } + if (this->white_sensor_ != nullptr) { + this->white_sensor_->publish_state(data.white_lux); + } +} + +void VEML7700Component::publish_data_part_2_(Readings &data) { + if (this->fake_infrared_sensor_ != nullptr) { + this->fake_infrared_sensor_->publish_state(data.fake_infrared_lux); + } + if (this->ambient_light_counts_sensor_ != nullptr) { + this->ambient_light_counts_sensor_->publish_state(data.als_counts); + } + if (this->white_counts_sensor_ != nullptr) { + this->white_counts_sensor_->publish_state(data.white_counts); + } +} + +void VEML7700Component::publish_data_part_3_(Readings &data) { + if (this->actual_gain_sensor_ != nullptr) { + this->actual_gain_sensor_->publish_state(get_gain_coeff(data.actual_gain)); + } + if (this->actual_integration_time_sensor_ != nullptr) { + this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.actual_time)); + } +} +} // namespace veml7700 +} // namespace esphome diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h new file mode 100644 index 0000000000..17fee6b851 --- /dev/null +++ b/esphome/components/veml7700/veml7700.h @@ -0,0 +1,202 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" + +namespace esphome { +namespace veml7700 { + +using esphome::i2c::ErrorCode; + +// +// Datasheet: https://www.vishay.com/docs/84286/veml7700.pdf +// + +enum class CommandRegisters : uint8_t { + ALS_CONF_0 = 0x00, // W: ALS gain, integration time, interrupt, and shutdown + ALS_WH = 0x01, // W: ALS high threshold window setting + ALS_WL = 0x02, // W: ALS low threshold window setting + PWR_SAVING = 0x03, // W: Set (15 : 3) 0000 0000 0000 0b + ALS = 0x04, // R: MSB, LSB data of whole ALS 16 bits + WHITE = 0x05, // R: MSB, LSB data of whole WHITE 16 bits + ALS_INT = 0x06 // R: ALS INT trigger event +}; + +enum Gain : uint16_t { + X_1 = 0, + X_2 = 1, + X_1_8 = 2, + X_1_4 = 3, +}; +const uint8_t GAINS_COUNT = 4; + +enum IntegrationTime : uint16_t { + INTEGRATION_TIME_25MS = 0b1100, + INTEGRATION_TIME_50MS = 0b1000, + INTEGRATION_TIME_100MS = 0b0000, + INTEGRATION_TIME_200MS = 0b0001, + INTEGRATION_TIME_400MS = 0b0010, + INTEGRATION_TIME_800MS = 0b0011, +}; +const uint8_t INTEGRATION_TIMES_COUNT = 6; + +enum Persistence : uint16_t { + PERSISTENCE_1 = 0, + PERSISTENCE_2 = 1, + PERSISTENCE_4 = 2, + PERSISTENCE_8 = 3, +}; + +enum PSMMode : uint16_t { + PSM_MODE_1 = 0, + PSM_MODE_2 = 1, + PSM_MODE_3 = 2, + PSM_MODE_4 = 3, +}; + +// The following section with bit-fields brings GCC compilation 'notes' about padding bytes due to bug in older GCC back +// in 2009 "Packed bit-fields of type char were not properly bit-packed on many targets prior to GCC 4.4" Even more to +// this - this message can't be disabled with "#pragma GCC diagnostic ignored" due to another bug which was only fixed +// in GCC 13 in 2022 :) No actions required, it is just a note. The code is correct. + +// +// VEML7700_CR_ALS_CONF_0 Register (0x00) +// +union ConfigurationRegister { + uint16_t raw; + uint8_t raw_bytes[2]; + struct { + bool ALS_SD : 1; // ALS shut down setting: 0 = ALS power on, 1 = ALS shut + // down + bool ALS_INT_EN : 1; // ALS interrupt enable setting: 0 = ALS INT disable, 1 + // = ALS INT enable + bool reserved_2 : 1; // 0 + bool reserved_3 : 1; // 0 + Persistence ALS_PERS : 2; // 00 - 1, 01- 2, 10 - 4, 11 - 8 + IntegrationTime ALS_IT : 4; // ALS integration time setting + bool reserved_10 : 1; // 0 + Gain ALS_GAIN : 2; // Gain selection + bool reserved_13 : 1; // 0 + bool reserved_14 : 1; // 0 + bool reserved_15 : 1; // 0 + } __attribute__((packed)); +}; + +// +// Power Saving Mode: PSM Register (0x03) +// +union PSMRegister { + uint16_t raw; + uint8_t raw_bytes[2]; + struct { + bool PSM_EN : 1; + PSMMode PSM : 2; + uint16_t reserved : 13; + } __attribute__((packed)); +}; + +class VEML7700Component : public PollingComponent, public i2c::I2CDevice { + public: + // + // EspHome framework functions + // + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + void loop() override; + + // + // Configuration setters + // + void set_gain(Gain gain) { this->gain_ = gain; } + void set_integration_time(IntegrationTime time) { this->integration_time_ = time; } + void set_enable_automatic_mode(bool enable) { this->automatic_mode_enabled_ = enable; } + void set_enable_lux_compensation(bool enable) { this->lux_compensation_enabled_ = enable; } + void set_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; } + + void set_ambient_light_sensor(sensor::Sensor *sensor) { this->ambient_light_sensor_ = sensor; } + void set_ambient_light_counts_sensor(sensor::Sensor *sensor) { this->ambient_light_counts_sensor_ = sensor; } + void set_white_sensor(sensor::Sensor *sensor) { this->white_sensor_ = sensor; } + void set_white_counts_sensor(sensor::Sensor *sensor) { this->white_counts_sensor_ = sensor; } + void set_infrared_sensor(sensor::Sensor *sensor) { this->fake_infrared_sensor_ = sensor; } + void set_actual_gain_sensor(sensor::Sensor *sensor) { this->actual_gain_sensor_ = sensor; } + void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } + + protected: + // + // Internal state machine, used to split all the actions into + // small steps in loop() to make sure we are not blocking execution + // + enum class State : uint8_t { + NOT_INITIALIZED, + INITIAL_SETUP_COMPLETED, + IDLE, + COLLECTING_DATA, + COLLECTING_DATA_AUTO, + DATA_COLLECTED, + ADJUSTMENT_NEEDED, + ADJUSTMENT_IN_PROGRESS, + READY_TO_APPLY_ADJUSTMENTS, + READY_TO_PUBLISH_PART_1, + READY_TO_PUBLISH_PART_2, + READY_TO_PUBLISH_PART_3 + } state_{State::NOT_INITIALIZED}; + + // + // Current measurements data + // + struct Readings { + uint16_t als_counts{0}; + uint16_t white_counts{0}; + IntegrationTime actual_time{INTEGRATION_TIME_100MS}; + Gain actual_gain{X_1_8}; + float als_lux{0}; + float white_lux{0}; + float fake_infrared_lux{0}; + ErrorCode err{i2c::ERROR_OK}; + } readings_; + + // + // Device interaction + // + ErrorCode configure_(); + ErrorCode reconfigure_time_and_gain_(IntegrationTime time, Gain gain, bool shutdown); + ErrorCode read_sensor_output_(Readings &data); + + // + // Working with the data + // + bool are_adjustments_required_(Readings &data); + void apply_lux_calculation_(Readings &data); + void apply_lux_compensation_(Readings &data); + void apply_glass_attenuation_(Readings &data); + void publish_data_part_1_(Readings &data); + void publish_data_part_2_(Readings &data); + void publish_data_part_3_(Readings &data); + + // + // Component configuration + // + bool automatic_mode_enabled_{true}; + bool lux_compensation_enabled_{true}; + float glass_attenuation_factor_{1.0}; + IntegrationTime integration_time_{INTEGRATION_TIME_100MS}; + Gain gain_{X_1}; + + // + // Sensors for publishing data + // + sensor::Sensor *ambient_light_sensor_{nullptr}; // Human eye range 500-600 nm, lx + sensor::Sensor *ambient_light_counts_sensor_{nullptr}; // Raw counts + sensor::Sensor *white_sensor_{nullptr}; // Wide range 450-950 nm, lx + sensor::Sensor *white_counts_sensor_{nullptr}; // Raw counts + sensor::Sensor *fake_infrared_sensor_{nullptr}; // Artificial. = WHITE lx - ALS lx. + sensor::Sensor *actual_gain_sensor_{nullptr}; // Actual gain multiplier for the measurement + sensor::Sensor *actual_integration_time_sensor_{nullptr}; // Actual integration time for the measurement +}; + +} // namespace veml7700 +} // namespace esphome diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 624fcdf52c..a4fb572208 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -1,22 +1,55 @@ -import esphome.config_validation as cv -import esphome.codegen as cg - -from esphome.const import CONF_ID, CONF_MICROPHONE, CONF_SPEAKER from esphome import automation -from esphome.automation import register_action -from esphome.components import microphone, speaker +from esphome.automation import register_action, register_condition +import esphome.codegen as cg +from esphome.components import media_player, microphone, speaker +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MEDIA_PLAYER, + CONF_MICROPHONE, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, + CONF_ON_ERROR, + CONF_ON_IDLE, + CONF_SPEAKER, +) AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz"] +CONF_ON_END = "on_end" +CONF_ON_INTENT_END = "on_intent_end" +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_TTS_START = "on_tts_start" +CONF_ON_STT_VAD_END = "on_stt_vad_end" +CONF_ON_STT_VAD_START = "on_stt_vad_start" CONF_ON_TTS_END = "on_tts_end" -CONF_ON_END = "on_end" -CONF_ON_ERROR = "on_error" +CONF_ON_TTS_START = "on_tts_start" +CONF_ON_TTS_STREAM_START = "on_tts_stream_start" +CONF_ON_TTS_STREAM_END = "on_tts_stream_end" +CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" + +CONF_SILENCE_DETECTION = "silence_detection" +CONF_USE_WAKE_WORD = "use_wake_word" +CONF_VAD_THRESHOLD = "vad_threshold" + +CONF_AUTO_GAIN = "auto_gain" +CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" +CONF_VOLUME_MULTIPLIER = "volume_multiplier" + +CONF_WAKE_WORD = "wake_word" + +CONF_CONVERSATION_TIMEOUT = "conversation_timeout" + +CONF_ON_TIMER_STARTED = "on_timer_started" +CONF_ON_TIMER_UPDATED = "on_timer_updated" +CONF_ON_TIMER_CANCELLED = "on_timer_cancelled" +CONF_ON_TIMER_FINISHED = "on_timer_finished" +CONF_ON_TIMER_TICK = "on_timer_tick" voice_assistant_ns = cg.esphome_ns.namespace("voice_assistant") @@ -25,24 +58,110 @@ VoiceAssistant = voice_assistant_ns.class_("VoiceAssistant", cg.Component) StartAction = voice_assistant_ns.class_( "StartAction", automation.Action, cg.Parented.template(VoiceAssistant) ) +StartContinuousAction = voice_assistant_ns.class_( + "StartContinuousAction", automation.Action, cg.Parented.template(VoiceAssistant) +) StopAction = voice_assistant_ns.class_( "StopAction", automation.Action, cg.Parented.template(VoiceAssistant) ) +IsRunningCondition = voice_assistant_ns.class_( + "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) +) +ConnectedCondition = voice_assistant_ns.class_( + "ConnectedCondition", automation.Condition, cg.Parented.template(VoiceAssistant) +) + +Timer = voice_assistant_ns.struct("Timer") -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(VoiceAssistant), - cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), - cv.Optional(CONF_SPEAKER): cv.use_id(speaker.Speaker), - cv.Optional(CONF_ON_START): automation.validate_automation(single=True), - cv.Optional(CONF_ON_STT_END): automation.validate_automation(single=True), - cv.Optional(CONF_ON_TTS_START): automation.validate_automation(single=True), - cv.Optional(CONF_ON_TTS_END): automation.validate_automation(single=True), - cv.Optional(CONF_ON_END): automation.validate_automation(single=True), - cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), - } -).extend(cv.COMPONENT_SCHEMA) +def tts_stream_validate(config): + if CONF_SPEAKER not in config and ( + CONF_ON_TTS_STREAM_START in config or CONF_ON_TTS_STREAM_END in config + ): + raise cv.Invalid( + f"{CONF_SPEAKER} is required when using {CONF_ON_TTS_STREAM_START} and/or {CONF_ON_TTS_STREAM_END}" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(VoiceAssistant), + cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), + cv.Exclusive(CONF_SPEAKER, "output"): cv.use_id(speaker.Speaker), + cv.Exclusive(CONF_MEDIA_PLAYER, "output"): cv.use_id( + media_player.MediaPlayer + ), + cv.Optional(CONF_USE_WAKE_WORD, default=False): cv.boolean, + cv.Optional(CONF_VAD_THRESHOLD): cv.All( + cv.requires_component("esp_adf"), cv.only_with_esp_idf, cv.uint8_t + ), + cv.Optional(CONF_NOISE_SUPPRESSION_LEVEL, default=0): cv.int_range(0, 4), + cv.Optional(CONF_AUTO_GAIN, default="0dBFS"): cv.All( + cv.float_with_unit("decibel full scale", "(dBFS|dbfs|DBFS)"), + cv.int_range(0, 31), + ), + cv.Optional( + CONF_CONVERSATION_TIMEOUT, default="300s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_VOLUME_MULTIPLIER, default=1.0): cv.float_range( + min=0.0, min_included=False + ), + cv.Optional(CONF_ON_LISTENING): automation.validate_automation(single=True), + cv.Optional(CONF_ON_START): automation.validate_automation(single=True), + cv.Optional(CONF_ON_WAKE_WORD_DETECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_END): automation.validate_automation(single=True), + cv.Optional(CONF_ON_TTS_START): automation.validate_automation(single=True), + cv.Optional(CONF_ON_TTS_END): automation.validate_automation(single=True), + cv.Optional(CONF_ON_END): automation.validate_automation(single=True), + cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), + cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TTS_STREAM_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TTS_STREAM_END): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_IDLE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_TIMER_STARTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TIMER_UPDATED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TIMER_CANCELLED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TIMER_FINISHED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TIMER_TICK): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + tts_stream_validate, +) async def to_code(config): @@ -56,11 +175,37 @@ async def to_code(config): spkr = await cg.get_variable(config[CONF_SPEAKER]) cg.add(var.set_speaker(spkr)) + if CONF_MEDIA_PLAYER in config: + mp = await cg.get_variable(config[CONF_MEDIA_PLAYER]) + cg.add(var.set_media_player(mp)) + + cg.add(var.set_use_wake_word(config[CONF_USE_WAKE_WORD])) + + if (vad_threshold := config.get(CONF_VAD_THRESHOLD)) is not None: + cg.add(var.set_vad_threshold(vad_threshold)) + + cg.add(var.set_noise_suppression_level(config[CONF_NOISE_SUPPRESSION_LEVEL])) + cg.add(var.set_auto_gain(config[CONF_AUTO_GAIN])) + cg.add(var.set_volume_multiplier(config[CONF_VOLUME_MULTIPLIER])) + cg.add(var.set_conversation_timeout(config[CONF_CONVERSATION_TIMEOUT])) + + if CONF_ON_LISTENING in config: + await automation.build_automation( + var.get_listening_trigger(), [], config[CONF_ON_LISTENING] + ) + if CONF_ON_START in config: await automation.build_automation( var.get_start_trigger(), [], config[CONF_ON_START] ) + if CONF_ON_WAKE_WORD_DETECTED in config: + await automation.build_automation( + var.get_wake_word_detected_trigger(), + [], + config[CONF_ON_WAKE_WORD_DETECTED], + ) + if CONF_ON_STT_END in config: await automation.build_automation( var.get_stt_end_trigger(), [(cg.std_string, "x")], config[CONF_ON_STT_END] @@ -90,16 +235,141 @@ async def to_code(config): config[CONF_ON_ERROR], ) + if CONF_ON_CLIENT_CONNECTED in config: + await automation.build_automation( + var.get_client_connected_trigger(), + [], + config[CONF_ON_CLIENT_CONNECTED], + ) + + if CONF_ON_CLIENT_DISCONNECTED in config: + await automation.build_automation( + var.get_client_disconnected_trigger(), + [], + config[CONF_ON_CLIENT_DISCONNECTED], + ) + + if CONF_ON_INTENT_START in config: + await automation.build_automation( + var.get_intent_start_trigger(), + [], + config[CONF_ON_INTENT_START], + ) + + if CONF_ON_INTENT_END in config: + await automation.build_automation( + var.get_intent_end_trigger(), + [], + config[CONF_ON_INTENT_END], + ) + + if CONF_ON_STT_VAD_START in config: + await automation.build_automation( + var.get_stt_vad_start_trigger(), + [], + config[CONF_ON_STT_VAD_START], + ) + + if CONF_ON_STT_VAD_END in config: + await automation.build_automation( + var.get_stt_vad_end_trigger(), + [], + config[CONF_ON_STT_VAD_END], + ) + + if CONF_ON_TTS_STREAM_START in config: + await automation.build_automation( + var.get_tts_stream_start_trigger(), + [], + config[CONF_ON_TTS_STREAM_START], + ) + + if CONF_ON_TTS_STREAM_END in config: + await automation.build_automation( + var.get_tts_stream_end_trigger(), + [], + config[CONF_ON_TTS_STREAM_END], + ) + + if CONF_ON_IDLE in config: + await automation.build_automation( + var.get_idle_trigger(), + [], + config[CONF_ON_IDLE], + ) + + has_timers = False + if on_timer_started := config.get(CONF_ON_TIMER_STARTED): + await automation.build_automation( + var.get_timer_started_trigger(), + [(Timer, "timer")], + on_timer_started, + ) + has_timers = True + + if on_timer_updated := config.get(CONF_ON_TIMER_UPDATED): + await automation.build_automation( + var.get_timer_updated_trigger(), + [(Timer, "timer")], + on_timer_updated, + ) + has_timers = True + + if on_timer_cancelled := config.get(CONF_ON_TIMER_CANCELLED): + await automation.build_automation( + var.get_timer_cancelled_trigger(), + [(Timer, "timer")], + on_timer_cancelled, + ) + has_timers = True + + if on_timer_finished := config.get(CONF_ON_TIMER_FINISHED): + await automation.build_automation( + var.get_timer_finished_trigger(), + [(Timer, "timer")], + on_timer_finished, + ) + has_timers = True + + if on_timer_tick := config.get(CONF_ON_TIMER_TICK): + await automation.build_automation( + var.get_timer_tick_trigger(), + [(cg.std_vector.template(Timer), "timers")], + on_timer_tick, + ) + has_timers = True + + cg.add(var.set_has_timers(has_timers)) + cg.add_define("USE_VOICE_ASSISTANT") VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssistant)}) -@register_action("voice_assistant.start", StartAction, VOICE_ASSISTANT_ACTION_SCHEMA) +@register_action( + "voice_assistant.start_continuous", + StartContinuousAction, + VOICE_ASSISTANT_ACTION_SCHEMA, +) +@register_action( + "voice_assistant.start", + StartAction, + VOICE_ASSISTANT_ACTION_SCHEMA.extend( + { + cv.Optional(CONF_SILENCE_DETECTION, default=True): cv.boolean, + cv.Optional(CONF_WAKE_WORD): cv.templatable(cv.string), + } + ), +) async def voice_assistant_listen_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) + if CONF_SILENCE_DETECTION in config: + cg.add(var.set_silence_detection(config[CONF_SILENCE_DETECTION])) + if wake_word := config.get(CONF_WAKE_WORD): + templ = await cg.templatable(wake_word, args, cg.std_string) + cg.add(var.set_wake_word(templ)) return var @@ -108,3 +378,21 @@ async def voice_assistant_stop_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 + + +@register_condition( + "voice_assistant.is_running", IsRunningCondition, VOICE_ASSISTANT_ACTION_SCHEMA +) +async def voice_assistant_is_running_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@register_condition( + "voice_assistant.connected", ConnectedCondition, VOICE_ASSISTANT_ACTION_SCHEMA +) +async def voice_assistant_connected_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 4245578711..6f164f69d3 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -4,6 +4,7 @@ #include "esphome/core/log.h" +#include #include namespace esphome { @@ -11,30 +12,39 @@ namespace voice_assistant { static const char *const TAG = "voice_assistant"; +#ifdef SAMPLE_RATE_HZ +#undef SAMPLE_RATE_HZ +#endif + +static const size_t SAMPLE_RATE_HZ = 16000; +static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms +static const size_t BUFFER_SIZE = 512 * SAMPLE_RATE_HZ / 1000; +static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t); +static const size_t RECEIVE_SIZE = 1024; +static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE; + +VoiceAssistant::VoiceAssistant() { global_voice_assistant = this; } + float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } -void VoiceAssistant::setup() { - ESP_LOGCONFIG(TAG, "Setting up Voice Assistant..."); - - global_voice_assistant = this; - +bool VoiceAssistant::start_udp_socket_() { this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); - if (socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket."); + if (this->socket_ == nullptr) { + ESP_LOGE(TAG, "Could not create socket"); this->mark_failed(); - return; + return false; } int enable = 1; - int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); // we can still continue } - err = socket_->setblocking(false); + err = this->socket_->setblocking(false); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + ESP_LOGE(TAG, "Socket unable to set nonblocking mode: errno %d", err); this->mark_failed(); - return; + return false; } #ifdef USE_SPEAKER @@ -43,47 +53,501 @@ void VoiceAssistant::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), 6055); if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); + ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno); this->mark_failed(); - return; + return false; } - server.ss_family = AF_INET; - err = socket_->bind((struct sockaddr *) &server, sizeof(server)); + err = this->socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); this->mark_failed(); - return; + return false; + } + } +#endif + this->udp_socket_running_ = true; + return true; +} + +bool VoiceAssistant::allocate_buffers_() { + if (this->send_buffer_ != nullptr) { + return true; // Already allocated + } + +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + ExternalRAMAllocator speaker_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); + if (this->speaker_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate speaker buffer"); + return false; } } #endif - this->mic_->add_data_callback([this](const std::vector &data) { - if (!this->running_) { - return; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); + if (this->input_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate input buffer"); + return false; + } + +#ifdef USE_ESP_ADF + this->vad_instance_ = vad_create(VAD_MODE_4); +#endif + + this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t)); + if (this->ring_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate ring buffer"); + return false; + } + + ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); + if (send_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate send buffer"); + return false; + } + + return true; +} + +void VoiceAssistant::clear_buffers_() { + if (this->send_buffer_ != nullptr) { + memset(this->send_buffer_, 0, SEND_BUFFER_SIZE); + } + + if (this->input_buffer_ != nullptr) { + memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t)); + } + + if (this->ring_buffer_ != nullptr) { + this->ring_buffer_->reset(); + } + +#ifdef USE_SPEAKER + if (this->speaker_buffer_ != nullptr) { + memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); + + this->speaker_buffer_size_ = 0; + this->speaker_buffer_index_ = 0; + this->speaker_bytes_received_ = 0; + } +#endif +} + +void VoiceAssistant::deallocate_buffers_() { + ExternalRAMAllocator send_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + send_deallocator.deallocate(this->send_buffer_, SEND_BUFFER_SIZE); + this->send_buffer_ = nullptr; + + if (this->ring_buffer_ != nullptr) { + this->ring_buffer_.reset(); + this->ring_buffer_ = nullptr; + } + +#ifdef USE_ESP_ADF + if (this->vad_instance_ != nullptr) { + vad_destroy(this->vad_instance_); + this->vad_instance_ = nullptr; + } +#endif + + ExternalRAMAllocator input_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + input_deallocator.deallocate(this->input_buffer_, INPUT_BUFFER_SIZE); + this->input_buffer_ = nullptr; + +#ifdef USE_SPEAKER + if (this->speaker_buffer_ != nullptr) { + ExternalRAMAllocator speaker_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE); + this->speaker_buffer_ = nullptr; + } +#endif +} + +void VoiceAssistant::reset_conversation_id() { + this->conversation_id_ = ""; + ESP_LOGD(TAG, "reset conversation ID"); +} + +int VoiceAssistant::read_microphone_() { + size_t bytes_read = 0; + if (this->mic_->is_running()) { // Read audio into input buffer + bytes_read = this->mic_->read(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t)); + if (bytes_read == 0) { + memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t)); + return 0; } - this->socket_->sendto(data.data(), data.size() * sizeof(int16_t), 0, (struct sockaddr *) &this->dest_addr_, - sizeof(this->dest_addr_)); - }); + // Write audio into ring buffer + this->ring_buffer_->write((void *) this->input_buffer_, bytes_read); + } else { + ESP_LOGD(TAG, "microphone not running"); + } + return bytes_read; } void VoiceAssistant::loop() { -#ifdef USE_SPEAKER - if (this->speaker_ == nullptr) { + if (this->api_client_ == nullptr && this->state_ != State::IDLE && this->state_ != State::STOP_MICROPHONE && + this->state_ != State::STOPPING_MICROPHONE) { + if (this->mic_->is_running() || this->state_ == State::STARTING_MICROPHONE) { + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + } else { + this->set_state_(State::IDLE, State::IDLE); + } + this->continuous_ = false; + this->signal_stop_(); + this->clear_buffers_(); return; } - - uint8_t buf[1024]; - auto len = this->socket_->read(buf, sizeof(buf)); - if (len == -1) { - return; - } - this->speaker_->play(buf, len); + switch (this->state_) { + case State::IDLE: { + if (this->continuous_ && this->desired_state_ == State::IDLE) { + this->idle_trigger_->trigger(); +#ifdef USE_ESP_ADF + if (this->use_wake_word_) { + this->set_state_(State::START_MICROPHONE, State::WAIT_FOR_VAD); + } else #endif + { + this->set_state_(State::START_MICROPHONE, State::START_PIPELINE); + } + } else { + this->high_freq_.stop(); + } + break; + } + case State::START_MICROPHONE: { + ESP_LOGD(TAG, "Starting Microphone"); + if (!this->allocate_buffers_()) { + this->status_set_error("Failed to allocate buffers"); + return; + } + if (this->status_has_error()) { + this->status_clear_error(); + } + this->clear_buffers_(); + + this->mic_->start(); + this->high_freq_.start(); + this->set_state_(State::STARTING_MICROPHONE); + break; + } + case State::STARTING_MICROPHONE: { + if (this->mic_->is_running()) { + this->set_state_(this->desired_state_); + } + break; + } +#ifdef USE_ESP_ADF + case State::WAIT_FOR_VAD: { + this->read_microphone_(); + ESP_LOGD(TAG, "Waiting for speech..."); + this->set_state_(State::WAITING_FOR_VAD); + break; + } + case State::WAITING_FOR_VAD: { + size_t bytes_read = this->read_microphone_(); + if (bytes_read > 0) { + vad_state_t vad_state = + vad_process(this->vad_instance_, this->input_buffer_, SAMPLE_RATE_HZ, VAD_FRAME_LENGTH_MS); + if (vad_state == VAD_SPEECH) { + if (this->vad_counter_ < this->vad_threshold_) { + this->vad_counter_++; + } else { + ESP_LOGD(TAG, "VAD detected speech"); + this->set_state_(State::START_PIPELINE, State::STREAMING_MICROPHONE); + + // Reset for next time + this->vad_counter_ = 0; + } + } else { + if (this->vad_counter_ > 0) { + this->vad_counter_--; + } + } + } + break; + } +#endif + case State::START_PIPELINE: { + this->read_microphone_(); + ESP_LOGD(TAG, "Requesting start..."); + uint32_t flags = 0; + if (this->use_wake_word_) + flags |= api::enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD; + if (this->silence_detection_) + flags |= api::enums::VOICE_ASSISTANT_REQUEST_USE_VAD; + api::VoiceAssistantAudioSettings audio_settings; + audio_settings.noise_suppression_level = this->noise_suppression_level_; + audio_settings.auto_gain = this->auto_gain_; + audio_settings.volume_multiplier = this->volume_multiplier_; + + api::VoiceAssistantRequest msg; + msg.start = true; + msg.conversation_id = this->conversation_id_; + msg.flags = flags; + msg.audio_settings = audio_settings; + msg.wake_word_phrase = this->wake_word_; + this->wake_word_ = ""; + + if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { + ESP_LOGW(TAG, "Could not request start"); + this->error_trigger_->trigger("not-connected", "Could not request start"); + this->continuous_ = false; + this->set_state_(State::IDLE, State::IDLE); + break; + } + this->set_state_(State::STARTING_PIPELINE); + this->set_timeout("reset-conversation_id", this->conversation_timeout_, + [this]() { this->reset_conversation_id(); }); + break; + } + case State::STARTING_PIPELINE: { + this->read_microphone_(); + break; // State changed when udp server port received + } + case State::STREAMING_MICROPHONE: { + this->read_microphone_(); + size_t available = this->ring_buffer_->available(); + while (available >= SEND_BUFFER_SIZE) { + 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.data.assign((char *) this->send_buffer_, read_bytes); + this->api_client_->send_voice_assistant_audio(msg); + } else { + if (!this->udp_socket_running_) { + if (!this->start_udp_socket_()) { + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + break; + } + } + this->socket_->sendto(this->send_buffer_, read_bytes, 0, (struct sockaddr *) &this->dest_addr_, + sizeof(this->dest_addr_)); + } + available = this->ring_buffer_->available(); + } + + break; + } + case State::STOP_MICROPHONE: { + if (this->mic_->is_running()) { + this->mic_->stop(); + this->set_state_(State::STOPPING_MICROPHONE); + } else { + this->set_state_(this->desired_state_); + } + break; + } + case State::STOPPING_MICROPHONE: { + if (this->mic_->is_stopped()) { + this->set_state_(this->desired_state_); + } + break; + } + case State::AWAITING_RESPONSE: { + break; // State changed by events + } + case State::STREAMING_RESPONSE: { + bool playing = false; +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + ssize_t received_len = 0; + if (this->audio_mode_ == AUDIO_MODE_UDP) { + if (this->speaker_buffer_index_ + RECEIVE_SIZE < SPEAKER_BUFFER_SIZE) { + received_len = this->socket_->read(this->speaker_buffer_ + this->speaker_buffer_index_, RECEIVE_SIZE); + if (received_len > 0) { + this->speaker_buffer_index_ += received_len; + this->speaker_buffer_size_ += received_len; + this->speaker_bytes_received_ += received_len; + } + } else { + ESP_LOGD(TAG, "Receive buffer full"); + } + } + // Build a small buffer of audio before sending to the speaker + bool end_of_stream = this->stream_ended_ && (this->audio_mode_ == AUDIO_MODE_API || received_len < 0); + if (this->speaker_bytes_received_ > RECEIVE_SIZE * 4 || end_of_stream) + this->write_speaker_(); + if (this->wait_for_stream_end_) { + this->cancel_timeout("playing"); + if (end_of_stream) { + ESP_LOGD(TAG, "End of audio stream received"); + this->cancel_timeout("speaker-timeout"); + this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); + } + break; // We dont want to timeout here as the STREAM_END event will take care of that. + } + playing = this->speaker_->is_running(); + } +#endif +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + playing = (this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING); + } +#endif + if (playing) { + this->set_timeout("playing", 2000, [this]() { + this->cancel_timeout("speaker-timeout"); + this->set_state_(State::IDLE, State::IDLE); + + api::VoiceAssistantAnnounceFinished msg; + msg.success = true; + this->api_client_->send_voice_assistant_announce_finished(msg); + }); + } + break; + } + case State::RESPONSE_FINISHED: { +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + if (this->speaker_buffer_size_ > 0) { + this->write_speaker_(); + break; + } + if (this->speaker_->has_buffered_data() || this->speaker_->is_running()) { + break; + } + ESP_LOGD(TAG, "Speaker has finished outputting all audio"); + this->speaker_->stop(); + this->cancel_timeout("speaker-timeout"); + this->cancel_timeout("playing"); + + this->clear_buffers_(); + + this->wait_for_stream_end_ = false; + this->stream_ended_ = false; + + this->tts_stream_end_trigger_->trigger(); + } +#endif + this->set_state_(State::IDLE, State::IDLE); + break; + } + default: + break; + } } -void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { - ESP_LOGD(TAG, "Starting..."); +#ifdef USE_SPEAKER +void VoiceAssistant::write_speaker_() { + if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { + if (this->speaker_buffer_size_ > 0) { + size_t write_chunk = std::min(this->speaker_buffer_size_, 4 * 1024); + size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk); + if (written > 0) { + memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); + this->speaker_buffer_size_ -= written; + this->speaker_buffer_index_ -= written; + this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); + } else { + ESP_LOGV(TAG, "Speaker buffer full, trying again next loop"); + } + } + } +} +#endif + +void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { + if (!subscribe) { + if (this->api_client_ == nullptr || client != this->api_client_) { + ESP_LOGE(TAG, "Client attempting to unsubscribe that is not the current API Client"); + return; + } + this->api_client_ = nullptr; + this->client_disconnected_trigger_->trigger(); + return; + } + + if (this->api_client_ != nullptr) { + ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant"); + ESP_LOGE(TAG, "Current client: %s", this->api_client_->get_client_combined_info().c_str()); + ESP_LOGE(TAG, "New client: %s", client->get_client_combined_info().c_str()); + return; + } + + this->api_client_ = client; + this->client_connected_trigger_->trigger(); +} + +static const LogString *voice_assistant_state_to_string(State state) { + switch (state) { + case State::IDLE: + return LOG_STR("IDLE"); + case State::START_MICROPHONE: + return LOG_STR("START_MICROPHONE"); + case State::STARTING_MICROPHONE: + return LOG_STR("STARTING_MICROPHONE"); + case State::WAIT_FOR_VAD: + return LOG_STR("WAIT_FOR_VAD"); + case State::WAITING_FOR_VAD: + return LOG_STR("WAITING_FOR_VAD"); + case State::START_PIPELINE: + return LOG_STR("START_PIPELINE"); + case State::STARTING_PIPELINE: + return LOG_STR("STARTING_PIPELINE"); + case State::STREAMING_MICROPHONE: + return LOG_STR("STREAMING_MICROPHONE"); + case State::STOP_MICROPHONE: + return LOG_STR("STOP_MICROPHONE"); + case State::STOPPING_MICROPHONE: + return LOG_STR("STOPPING_MICROPHONE"); + case State::AWAITING_RESPONSE: + return LOG_STR("AWAITING_RESPONSE"); + case State::STREAMING_RESPONSE: + return LOG_STR("STREAMING_RESPONSE"); + case State::RESPONSE_FINISHED: + return LOG_STR("RESPONSE_FINISHED"); + default: + return LOG_STR("UNKNOWN"); + } +}; + +void VoiceAssistant::set_state_(State state) { + State old_state = this->state_; + this->state_ = state; + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(voice_assistant_state_to_string(old_state)), + LOG_STR_ARG(voice_assistant_state_to_string(state))); +} + +void VoiceAssistant::set_state_(State state, State desired_state) { + this->set_state_(state); + this->desired_state_ = desired_state; + ESP_LOGD(TAG, "Desired state set to %s", LOG_STR_ARG(voice_assistant_state_to_string(desired_state))); +} + +void VoiceAssistant::failed_to_start() { + ESP_LOGE(TAG, "Failed to start server. See Home Assistant logs for more details."); + this->error_trigger_->trigger("failed-to-start", "Failed to start server. See Home Assistant logs for more details."); + this->set_state_(State::STOP_MICROPHONE, State::IDLE); +} + +void VoiceAssistant::start_streaming() { + if (this->state_ != State::STARTING_PIPELINE) { + this->signal_stop_(); + return; + } + + ESP_LOGD(TAG, "Client started, streaming microphone"); + this->audio_mode_ = AUDIO_MODE_API; + + if (this->mic_->is_running()) { + this->set_state_(State::STREAMING_MICROPHONE, State::STREAMING_MICROPHONE); + } else { + this->set_state_(State::START_MICROPHONE, State::STREAMING_MICROPHONE); + } +} + +void VoiceAssistant::start_streaming(struct sockaddr_storage *addr, uint16_t port) { + if (this->state_ != State::STARTING_PIPELINE) { + this->signal_stop_(); + return; + } + + ESP_LOGD(TAG, "Client started, streaming microphone"); + this->audio_mode_ = AUDIO_MODE_UDP; memcpy(&this->dest_addr_, addr, sizeof(this->dest_addr_)); if (this->dest_addr_.ss_family == AF_INET) { @@ -98,31 +562,92 @@ void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { ESP_LOGW(TAG, "Unknown address family: %d", this->dest_addr_.ss_family); return; } - this->running_ = true; - this->mic_->start(); -} -void VoiceAssistant::request_start() { - ESP_LOGD(TAG, "Requesting start..."); - if (!api::global_api_server->start_voice_assistant()) { - ESP_LOGW(TAG, "Could not request start."); - this->error_trigger_->trigger("not-connected", "Could not request start."); + if (this->mic_->is_running()) { + this->set_state_(State::STREAMING_MICROPHONE, State::STREAMING_MICROPHONE); + } else { + this->set_state_(State::START_MICROPHONE, State::STREAMING_MICROPHONE); } } -void VoiceAssistant::signal_stop() { - ESP_LOGD(TAG, "Signaling stop..."); - this->mic_->stop(); - this->running_ = false; - api::global_api_server->stop_voice_assistant(); +void VoiceAssistant::request_start(bool continuous, bool silence_detection) { + if (this->api_client_ == nullptr) { + ESP_LOGE(TAG, "No API client connected"); + this->set_state_(State::IDLE, State::IDLE); + this->continuous_ = false; + return; + } + if (this->state_ == State::IDLE) { + this->continuous_ = continuous; + this->silence_detection_ = silence_detection; +#ifdef USE_ESP_ADF + if (this->use_wake_word_) { + this->set_state_(State::START_MICROPHONE, State::WAIT_FOR_VAD); + } else +#endif + { + this->set_state_(State::START_MICROPHONE, State::START_PIPELINE); + } + } +} + +void VoiceAssistant::request_stop() { + this->continuous_ = false; + + switch (this->state_) { + case State::IDLE: + break; + case State::START_MICROPHONE: + case State::STARTING_MICROPHONE: + case State::WAIT_FOR_VAD: + case State::WAITING_FOR_VAD: + case State::START_PIPELINE: + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + break; + case State::STARTING_PIPELINE: + case State::STREAMING_MICROPHONE: + this->signal_stop_(); + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + break; + case State::STOP_MICROPHONE: + case State::STOPPING_MICROPHONE: + this->desired_state_ = State::IDLE; + break; + case State::AWAITING_RESPONSE: + case State::STREAMING_RESPONSE: + case State::RESPONSE_FINISHED: + break; // Let the incoming audio stream finish then it will go to idle. + } +} + +void VoiceAssistant::signal_stop_() { memset(&this->dest_addr_, 0, sizeof(this->dest_addr_)); + if (this->api_client_ == nullptr) { + return; + } + ESP_LOGD(TAG, "Signaling stop..."); + api::VoiceAssistantRequest msg; + msg.start = false; + this->api_client_->send_voice_assistant_request(msg); } void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { + ESP_LOGD(TAG, "Event Type: %" PRId32, msg.event_type); switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); - this->start_trigger_->trigger(); + this->defer([this]() { this->start_trigger_->trigger(); }); + break; + case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: + break; + case api::enums::VOICE_ASSISTANT_WAKE_WORD_END: { + ESP_LOGD(TAG, "Wake word detected"); + this->defer([this]() { this->wake_word_detected_trigger_->trigger(); }); + break; + } + case api::enums::VOICE_ASSISTANT_STT_START: + ESP_LOGD(TAG, "STT started"); + this->defer([this]() { this->listening_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_END: { std::string text; @@ -132,11 +657,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in STT_END event."); + ESP_LOGW(TAG, "No text in STT_END event"); return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); - this->stt_end_trigger_->trigger(text); + this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); + break; + } + case api::enums::VOICE_ASSISTANT_INTENT_START: + ESP_LOGD(TAG, "Intent started"); + this->defer([this]() { this->intent_start_trigger_->trigger(); }); + break; + case api::enums::VOICE_ASSISTANT_INTENT_END: { + for (auto arg : msg.data) { + if (arg.name == "conversation_id") { + this->conversation_id_ = std::move(arg.value); + } + } + this->defer([this]() { this->intent_end_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -147,11 +685,18 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in TTS_START event."); + ESP_LOGW(TAG, "No text in TTS_START event"); return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); - this->tts_start_trigger_->trigger(text); + this->defer([this, text]() { + this->tts_start_trigger_->trigger(text); +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + this->speaker_->start(); + } +#endif + }); break; } case api::enums::VOICE_ASSISTANT_TTS_END: { @@ -162,17 +707,42 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (url.empty()) { - ESP_LOGW(TAG, "No url in TTS_END event."); + ESP_LOGW(TAG, "No url in TTS_END event"); return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); - this->tts_end_trigger_->trigger(url); + this->defer([this, url]() { +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); + } +#endif + this->tts_end_trigger_->trigger(url); + }); + State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; + this->set_state_(new_state, new_state); break; } - case api::enums::VOICE_ASSISTANT_RUN_END: + case api::enums::VOICE_ASSISTANT_RUN_END: { ESP_LOGD(TAG, "Assist Pipeline ended"); - this->end_trigger_->trigger(); + if (this->state_ == State::STREAMING_MICROPHONE) { + this->ring_buffer_->reset(); +#ifdef USE_ESP_ADF + if (this->use_wake_word_) { + // No need to stop the microphone since we didn't use the speaker + this->set_state_(State::WAIT_FOR_VAD, State::WAITING_FOR_VAD); + } else +#endif + { + this->set_state_(State::IDLE, State::IDLE); + } + } else if (this->state_ == State::AWAITING_RESPONSE) { + // No TTS start event ("nevermind") + this->set_state_(State::IDLE, State::IDLE); + } + this->defer([this]() { this->end_trigger_->trigger(); }); break; + } case api::enums::VOICE_ASSISTANT_ERROR: { std::string code = ""; std::string message = ""; @@ -183,14 +753,139 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { message = std::move(arg.value); } } + if (code == "wake-word-timeout" || code == "wake_word_detection_aborted" || code == "no_wake_word") { + // Don't change state here since either the "tts-end" or "run-end" events will do it. + return; + } else if (code == "wake-provider-missing" || code == "wake-engine-missing") { + // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again. + this->defer([this, code, message]() { + this->request_stop(); + this->error_trigger_->trigger(code, message); + }); + return; + } ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); - this->error_trigger_->trigger(code, message); + if (this->state_ != State::IDLE) { + this->signal_stop_(); + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + } + this->defer([this, code, message]() { this->error_trigger_->trigger(code, message); }); + break; } + case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + this->wait_for_stream_end_ = true; + ESP_LOGD(TAG, "TTS stream start"); + this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); + } +#endif + break; + } + case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + this->stream_ended_ = true; + ESP_LOGD(TAG, "TTS stream end"); + } +#endif + break; + } + case api::enums::VOICE_ASSISTANT_STT_VAD_START: + ESP_LOGD(TAG, "Starting STT by VAD"); + this->defer([this]() { this->stt_vad_start_trigger_->trigger(); }); + break; + case api::enums::VOICE_ASSISTANT_STT_VAD_END: + ESP_LOGD(TAG, "STT by VAD end"); + this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); + this->defer([this]() { this->stt_vad_end_trigger_->trigger(); }); + break; default: + ESP_LOGD(TAG, "Unhandled event type: %" PRId32, msg.event_type); break; } } +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()); + } else { + ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); + } + } +#endif +} + +void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) { + Timer timer = { + .id = msg.timer_id, + .name = msg.name, + .total_seconds = msg.total_seconds, + .seconds_left = msg.seconds_left, + .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()); + + switch (msg.event_type) { + case api::enums::VOICE_ASSISTANT_TIMER_STARTED: + this->timer_started_trigger_->trigger(timer); + break; + case api::enums::VOICE_ASSISTANT_TIMER_UPDATED: + this->timer_updated_trigger_->trigger(timer); + break; + case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED: + this->timer_cancelled_trigger_->trigger(timer); + this->timers_.erase(timer.id); + break; + case api::enums::VOICE_ASSISTANT_TIMER_FINISHED: + this->timer_finished_trigger_->trigger(timer); + this->timers_.erase(timer.id); + break; + } + + if (this->timers_.empty()) { + this->cancel_interval("timer-event"); + this->timer_tick_running_ = false; + } else if (!this->timer_tick_running_) { + this->set_interval("timer-event", 1000, [this]() { this->timer_tick_(); }); + this->timer_tick_running_ = true; + } +} + +void VoiceAssistant::timer_tick_() { + std::vector res; + res.reserve(this->timers_.size()); + for (auto &pair : this->timers_) { + auto &timer = pair.second; + if (timer.is_active && timer.seconds_left > 0) { + timer.seconds_left--; + } + res.push_back(timer); + } + this->timer_tick_trigger_->trigger(res); +} + +void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) { +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->tts_start_trigger_->trigger(msg.text); + this->media_player_->make_call().set_media_url(msg.media_id).set_announcement(true).perform(); + this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE); + this->tts_end_trigger_->trigger(msg.media_id); + this->end_trigger_->trigger(); + } +#endif +} + VoiceAssistant *global_voice_assistant = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace voice_assistant diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index c1a6e8883b..0016d3157c 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -6,84 +6,331 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include "esphome/core/ring_buffer.h" +#include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" -#include "esphome/components/api/api_server.h" #include "esphome/components/microphone/microphone.h" #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" #endif +#ifdef USE_MEDIA_PLAYER +#include "esphome/components/media_player/media_player.h" +#endif #include "esphome/components/socket/socket.h" +#ifdef USE_ESP_ADF +#include +#endif + +#include +#include + namespace esphome { namespace voice_assistant { // Version 1: Initial version // Version 2: Adds raw speaker support -static const uint32_t INITIAL_VERSION = 1; -static const uint32_t SPEAKER_SUPPORT = 2; +static const uint32_t LEGACY_INITIAL_VERSION = 1; +static const uint32_t LEGACY_SPEAKER_SUPPORT = 2; + +enum VoiceAssistantFeature : uint32_t { + FEATURE_VOICE_ASSISTANT = 1 << 0, + FEATURE_SPEAKER = 1 << 1, + FEATURE_API_AUDIO = 1 << 2, + FEATURE_TIMERS = 1 << 3, +}; + +enum class State { + IDLE, + START_MICROPHONE, + STARTING_MICROPHONE, + WAIT_FOR_VAD, + WAITING_FOR_VAD, + START_PIPELINE, + STARTING_PIPELINE, + STREAMING_MICROPHONE, + STOP_MICROPHONE, + STOPPING_MICROPHONE, + AWAITING_RESPONSE, + STREAMING_RESPONSE, + RESPONSE_FINISHED, +}; + +enum AudioMode : uint8_t { + AUDIO_MODE_UDP, + AUDIO_MODE_API, +}; + +struct Timer { + std::string id; + std::string name; + uint32_t total_seconds; + uint32_t seconds_left; + bool is_active; + + 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)); + } +}; + +struct WakeWord { + std::string id; + std::string wake_word; + std::vector trained_languages; +}; + +struct Configuration { + std::vector available_wake_words; + std::vector active_wake_words; + uint32_t max_active_wake_words; +}; class VoiceAssistant : public Component { public: - void setup() override; + VoiceAssistant(); + void loop() override; float get_setup_priority() const override; - void start(struct sockaddr_storage *addr, uint16_t port); + void start_streaming(); + void start_streaming(struct sockaddr_storage *addr, uint16_t port); + void failed_to_start(); void set_microphone(microphone::Microphone *mic) { this->mic_ = mic; } #ifdef USE_SPEAKER - void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } + void set_speaker(speaker::Speaker *speaker) { + this->speaker_ = speaker; + this->local_output_ = true; + } +#endif +#ifdef USE_MEDIA_PLAYER + void set_media_player(media_player::MediaPlayer *media_player) { + this->media_player_ = media_player; + this->local_output_ = true; + } #endif - uint32_t get_version() const { + uint32_t get_legacy_version() const { #ifdef USE_SPEAKER - if (this->speaker_ != nullptr) - return SPEAKER_SUPPORT; + if (this->speaker_ != nullptr) { + return LEGACY_SPEAKER_SUPPORT; + } #endif - return INITIAL_VERSION; + return LEGACY_INITIAL_VERSION; } - void request_start(); - void signal_stop(); + uint32_t get_feature_flags() const { + uint32_t flags = 0; + flags |= VoiceAssistantFeature::FEATURE_VOICE_ASSISTANT; + flags |= VoiceAssistantFeature::FEATURE_API_AUDIO; +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + flags |= VoiceAssistantFeature::FEATURE_SPEAKER; + } +#endif + + if (this->has_timers_) { + flags |= VoiceAssistantFeature::FEATURE_TIMERS; + } + + return flags; + } + + void request_start(bool continuous, bool silence_detection); + void request_stop(); void on_event(const api::VoiceAssistantEventResponse &msg); + void on_audio(const api::VoiceAssistantAudio &msg); + void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg); + void on_announce(const api::VoiceAssistantAnnounceRequest &msg); + void on_set_configuration(const std::vector &active_wake_words){}; + const Configuration &get_configuration() { return this->config_; }; - Trigger<> *get_start_trigger() const { return this->start_trigger_; } - Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } - Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } - Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } + bool is_running() const { return this->state_ != State::IDLE; } + void set_continuous(bool continuous) { this->continuous_ = continuous; } + bool is_continuous() const { return this->continuous_; } + + void set_use_wake_word(bool use_wake_word) { this->use_wake_word_ = use_wake_word; } +#ifdef USE_ESP_ADF + void set_vad_threshold(uint8_t vad_threshold) { this->vad_threshold_ = vad_threshold; } +#endif + + void set_noise_suppression_level(uint8_t noise_suppression_level) { + this->noise_suppression_level_ = noise_suppression_level; + } + void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; } + void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; } + void set_conversation_timeout(uint32_t conversation_timeout) { this->conversation_timeout_ = conversation_timeout; } + void reset_conversation_id(); + + Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } + Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } + Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_end_trigger() const { return this->end_trigger_; } + Trigger<> *get_start_trigger() const { return this->start_trigger_; } + Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } + Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } +#ifdef USE_SPEAKER + Trigger<> *get_tts_stream_start_trigger() const { return this->tts_stream_start_trigger_; } + Trigger<> *get_tts_stream_end_trigger() const { return this->tts_stream_end_trigger_; } +#endif + Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } + Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } + Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } + Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger<> *get_idle_trigger() const { return this->idle_trigger_; } + + Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger<> *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } + + void client_subscription(api::APIConnection *client, bool subscribe); + api::APIConnection *get_api_connection() const { return this->api_client_; } + + void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; } + + Trigger *get_timer_started_trigger() const { return this->timer_started_trigger_; } + Trigger *get_timer_updated_trigger() const { return this->timer_updated_trigger_; } + Trigger *get_timer_cancelled_trigger() const { return this->timer_cancelled_trigger_; } + Trigger *get_timer_finished_trigger() const { return this->timer_finished_trigger_; } + Trigger> *get_timer_tick_trigger() const { return this->timer_tick_trigger_; } + void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; } + const std::unordered_map &get_timers() const { return this->timers_; } protected: + bool allocate_buffers_(); + void clear_buffers_(); + void deallocate_buffers_(); + + int read_microphone_(); + void set_state_(State state); + void set_state_(State state, State desired_state); + void signal_stop_(); + std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; - Trigger<> *start_trigger_ = new Trigger<>(); - Trigger *stt_end_trigger_ = new Trigger(); - Trigger *tts_start_trigger_ = new Trigger(); - Trigger *tts_end_trigger_ = new Trigger(); + Trigger<> *intent_end_trigger_ = new Trigger<>(); + Trigger<> *intent_start_trigger_ = new Trigger<>(); + Trigger<> *listening_trigger_ = new Trigger<>(); Trigger<> *end_trigger_ = new Trigger<>(); + Trigger<> *start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); +#ifdef USE_SPEAKER + Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); + Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); +#endif + Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); + Trigger *stt_end_trigger_ = new Trigger(); + Trigger *tts_end_trigger_ = new Trigger(); + Trigger *tts_start_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); + Trigger<> *idle_trigger_ = new Trigger<>(); + + Trigger<> *client_connected_trigger_ = new Trigger<>(); + Trigger<> *client_disconnected_trigger_ = new Trigger<>(); + + api::APIConnection *api_client_{nullptr}; + + std::unordered_map timers_; + void timer_tick_(); + Trigger *timer_started_trigger_ = new Trigger(); + Trigger *timer_finished_trigger_ = new Trigger(); + Trigger *timer_updated_trigger_ = new Trigger(); + Trigger *timer_cancelled_trigger_ = new Trigger(); + Trigger> *timer_tick_trigger_ = new Trigger>(); + bool has_timers_{false}; + bool timer_tick_running_{false}; microphone::Microphone *mic_{nullptr}; #ifdef USE_SPEAKER + void write_speaker_(); speaker::Speaker *speaker_{nullptr}; + uint8_t *speaker_buffer_{nullptr}; + size_t speaker_buffer_index_{0}; + size_t speaker_buffer_size_{0}; + size_t speaker_bytes_received_{0}; + bool wait_for_stream_end_{false}; + bool stream_ended_{false}; +#endif +#ifdef USE_MEDIA_PLAYER + media_player::MediaPlayer *media_player_{nullptr}; #endif - bool running_{false}; + bool local_output_{false}; + + std::string conversation_id_{""}; + + std::string wake_word_{""}; + + HighFrequencyLoopRequester high_freq_; + +#ifdef USE_ESP_ADF + vad_handle_t vad_instance_; + uint8_t vad_threshold_{5}; + uint8_t vad_counter_{0}; +#endif + std::unique_ptr ring_buffer_; + + bool use_wake_word_; + uint8_t noise_suppression_level_; + uint8_t auto_gain_; + float volume_multiplier_; + uint32_t conversation_timeout_; + + uint8_t *send_buffer_{nullptr}; + int16_t *input_buffer_{nullptr}; + + bool continuous_{false}; + bool silence_detection_; + + State state_{State::IDLE}; + State desired_state_{State::IDLE}; + + AudioMode audio_mode_{AUDIO_MODE_UDP}; + bool udp_socket_running_{false}; + bool start_udp_socket_(); + + Configuration config_{}; }; template class StartAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, wake_word); + public: - void play(Ts... x) override { this->parent_->request_start(); } + void play(Ts... x) override { + this->parent_->set_wake_word(this->wake_word_.value(x...)); + this->parent_->request_start(false, this->silence_detection_); + } + + void set_silence_detection(bool silence_detection) { this->silence_detection_ = silence_detection; } + + protected: + bool silence_detection_; +}; + +template class StartContinuousAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->request_start(true, true); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->signal_stop(); } + void play(Ts... x) override { this->parent_->request_stop(); } +}; + +template class IsRunningCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } +}; + +template class ConnectedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->get_api_connection() != nullptr; } }; extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/wake_on_lan/__init__.py b/esphome/components/wake_on_lan/__init__.py index 3548fb02f4..90539e5d3c 100644 --- a/esphome/components/wake_on_lan/__init__.py +++ b/esphome/components/wake_on_lan/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@willwill2will54"] +CODEOWNERS = ["@willwill2will54", "@clydebarrow"] diff --git a/esphome/components/wake_on_lan/button.py b/esphome/components/wake_on_lan/button.py index 778ea60cfa..b09e87e811 100644 --- a/esphome/components/wake_on_lan/button.py +++ b/esphome/components/wake_on_lan/button.py @@ -2,6 +2,16 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.core import CORE + +DEPENDENCIES = ["network"] + + +def AUTO_LOAD(): + if CORE.is_esp8266 or CORE.is_rp2040: + return [] + return ["socket"] + CONF_TARGET_MAC_ADDRESS = "target_mac_address" @@ -9,25 +19,19 @@ wake_on_lan_ns = cg.esphome_ns.namespace("wake_on_lan") WakeOnLanButton = wake_on_lan_ns.class_("WakeOnLanButton", button.Button, cg.Component) -DEPENDENCIES = ["network"] - -CONFIG_SCHEMA = cv.All( +CONFIG_SCHEMA = ( button.button_schema(WakeOnLanButton) .extend(cv.COMPONENT_SCHEMA) .extend( - cv.Schema( - { - cv.Required(CONF_TARGET_MAC_ADDRESS): cv.mac_address, - } - ), - ), - cv.only_with_arduino, + { + cv.Required(CONF_TARGET_MAC_ADDRESS): cv.mac_address, + } + ) ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - - yield cg.add(var.set_macaddr(*config[CONF_TARGET_MAC_ADDRESS].parts)) - yield cg.register_component(var, config) - yield button.register_button(var, config) + cg.add(var.set_macaddr(*config[CONF_TARGET_MAC_ADDRESS].parts)) + await cg.register_component(var, config) + await button.register_button(var, config) diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index 893aa75895..d18cdf89c8 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -1,6 +1,5 @@ -#ifdef USE_ARDUINO - #include "wake_on_lan.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" @@ -22,37 +21,69 @@ void WakeOnLanButton::set_macaddr(uint8_t a, uint8_t b, uint8_t c, uint8_t d, ui void WakeOnLanButton::dump_config() { LOG_BUTTON("", "Wake-on-LAN Button", this); - ESP_LOGCONFIG(TAG, " Target MAC address: %02X:%02X:%02X:%02X:%02X:%02X", macaddr_[0], macaddr_[1], macaddr_[2], - macaddr_[3], macaddr_[4], macaddr_[5]); + ESP_LOGCONFIG(TAG, " Target MAC address: %02X:%02X:%02X:%02X:%02X:%02X", this->macaddr_[0], this->macaddr_[1], + this->macaddr_[2], this->macaddr_[3], this->macaddr_[4], this->macaddr_[5]); } void WakeOnLanButton::press_action() { + if (!network::is_connected()) { + ESP_LOGW(TAG, "Network not connected"); + return; + } ESP_LOGI(TAG, "Sending Wake-on-LAN Packet..."); - bool begin_status = false; - bool end_status = false; - uint32_t interface = esphome::network::get_ip_address(); - IPAddress interface_ip = IPAddress(interface); +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + struct sockaddr_storage saddr {}; + auto addr_len = + socket::set_sockaddr(reinterpret_cast(&saddr), sizeof(saddr), "255.255.255.255", this->port_); + uint8_t buffer[6 + sizeof this->macaddr_ * 16]; + memcpy(buffer, PREFIX, sizeof(PREFIX)); + for (size_t i = 0; i != 16; i++) { + memcpy(buffer + i * sizeof(this->macaddr_) + sizeof(PREFIX), this->macaddr_, sizeof(this->macaddr_)); + } + if (this->broadcast_socket_->sendto(buffer, sizeof(buffer), 0, reinterpret_cast(&saddr), + addr_len) <= 0) + ESP_LOGW(TAG, "sendto() error %d", errno); +#else IPAddress broadcast = IPAddress(255, 255, 255, 255); -#ifdef USE_ESP8266 - begin_status = this->udp_client_.beginPacketMulticast(broadcast, 9, interface_ip, 128); -#endif -#ifdef USE_ESP32 - begin_status = this->udp_client_.beginPacket(broadcast, 9); -#endif - - if (begin_status) { - this->udp_client_.write(PREFIX, 6); - for (size_t i = 0; i < 16; i++) { - this->udp_client_.write(macaddr_, 6); + for (auto ip : esphome::network::get_ip_addresses()) { + if (ip.is_ip4()) { + if (this->udp_client_.beginPacketMulticast(broadcast, 9, ip, 128) != 0) { + this->udp_client_.write(PREFIX, 6); + for (size_t i = 0; i < 16; i++) { + this->udp_client_.write(macaddr_, 6); + } + if (this->udp_client_.endPacket() != 0) + return; + ESP_LOGW(TAG, "WOL broadcast failed"); + return; + } } - end_status = this->udp_client_.endPacket(); } - if (!begin_status || end_status) { - ESP_LOGE(TAG, "Sending Wake-on-LAN Packet Failed!"); + ESP_LOGW(TAG, "No ip4 addresses to broadcast to"); +#endif +} + +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->mark_failed(); + this->status_set_error("Could not create socket"); + return; } + int enable = 1; + auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + this->status_set_warning("Socket unable to set reuseaddr"); + // we can still continue + } + err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); + if (err != 0) { + this->status_set_warning("Socket unable to set broadcast"); + } +#endif } } // namespace wake_on_lan } // namespace esphome - #endif diff --git a/esphome/components/wake_on_lan/wake_on_lan.h b/esphome/components/wake_on_lan/wake_on_lan.h index 72f900e3fa..f516c4d669 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.h +++ b/esphome/components/wake_on_lan/wake_on_lan.h @@ -1,10 +1,13 @@ #pragma once - -#ifdef USE_ARDUINO - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include "esphome/components/button/button.h" #include "esphome/core/component.h" +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +#include "esphome/components/socket/socket.h" +#else #include "WiFiUdp.h" +#endif namespace esphome { namespace wake_on_lan { @@ -14,14 +17,20 @@ class WakeOnLanButton : public button::Button, public Component { void set_macaddr(uint8_t a, uint8_t b, uint8_t c, uint8_t d, uint8_t e, uint8_t f); void dump_config() override; + void setup() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } protected: +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + std::unique_ptr broadcast_socket_{}; +#else WiFiUDP udp_client_{}; +#endif void press_action() override; + uint16_t port_{9}; uint8_t macaddr_[6]; }; } // namespace wake_on_lan } // namespace esphome - #endif diff --git a/esphome/components/watchdog/__init__.py b/esphome/components/watchdog/__init__.py new file mode 100644 index 0000000000..6fb87e45a4 --- /dev/null +++ b/esphome/components/watchdog/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@oarcher"] diff --git a/esphome/components/watchdog/watchdog.cpp b/esphome/components/watchdog/watchdog.cpp new file mode 100644 index 0000000000..3a94a658e8 --- /dev/null +++ b/esphome/components/watchdog/watchdog.cpp @@ -0,0 +1,74 @@ +#include "watchdog.h" + +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#include +#include +#ifdef USE_ESP32 +#include "esp_idf_version.h" +#include "esp_task_wdt.h" +#endif +#ifdef USE_RP2040 +#include "hardware/watchdog.h" +#include "pico/stdlib.h" +#endif + +namespace esphome { +namespace watchdog { + +static const char *const TAG = "http_request.watchdog"; + +WatchdogManager::WatchdogManager(uint32_t timeout_ms) : timeout_ms_(timeout_ms) { + if (timeout_ms == 0) { + return; + } + this->saved_timeout_ms_ = this->get_timeout_(); + this->set_timeout_(timeout_ms); +} + +WatchdogManager::~WatchdogManager() { + if (this->timeout_ms_ == 0) { + return; + } + this->set_timeout_(this->saved_timeout_ms_); +} + +void WatchdogManager::set_timeout_(uint32_t timeout_ms) { + ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms); +#ifdef USE_ESP32 +#if ESP_IDF_VERSION_MAJOR >= 5 + esp_task_wdt_config_t wdt_config = { + .timeout_ms = timeout_ms, + .idle_core_mask = 0x03, + .trigger_panic = true, + }; + esp_task_wdt_reconfigure(&wdt_config); +#else + esp_task_wdt_init(timeout_ms / 1000, true); +#endif // ESP_IDF_VERSION_MAJOR +#endif // USE_ESP32 + +#ifdef USE_RP2040 + watchdog_enable(timeout_ms, true); +#endif +} + +uint32_t WatchdogManager::get_timeout_() { + uint32_t timeout_ms = 0; + +#ifdef USE_ESP32 + timeout_ms = (uint32_t) CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; +#endif // USE_ESP32 + +#ifdef USE_RP2040 + timeout_ms = watchdog_get_count() / 1000; +#endif + + ESP_LOGVV(TAG, "get_timeout: %" PRIu32 "ms", timeout_ms); + + return timeout_ms; +} + +} // namespace watchdog +} // namespace esphome diff --git a/esphome/components/watchdog/watchdog.h b/esphome/components/watchdog/watchdog.h new file mode 100644 index 0000000000..899ec3fde0 --- /dev/null +++ b/esphome/components/watchdog/watchdog.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/defines.h" + +#include + +namespace esphome { +namespace watchdog { + +class WatchdogManager { + public: + WatchdogManager(uint32_t timeout_ms); + ~WatchdogManager(); + + private: + uint32_t get_timeout_(); + void set_timeout_(uint32_t timeout_ms); + + uint32_t saved_timeout_ms_{0}; + uint32_t timeout_ms_{0}; +}; + +} // namespace watchdog +} // namespace esphome diff --git a/esphome/components/waveshare_epaper/__init__.py b/esphome/components/waveshare_epaper/__init__.py index e69de29bb2..c58ce8a01e 100644 --- a/esphome/components/waveshare_epaper/__init__.py +++ b/esphome/components/waveshare_epaper/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index d0276f119a..8287788de5 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import core, pins +import esphome.codegen as cg from esphome.components import display, spi +import esphome.config_validation as cv from esphome.const import ( CONF_BUSY_PIN, CONF_DC_PIN, @@ -17,8 +17,12 @@ from esphome.const import ( DEPENDENCIES = ["spi"] waveshare_epaper_ns = cg.esphome_ns.namespace("waveshare_epaper") -WaveshareEPaper = waveshare_epaper_ns.class_( - "WaveshareEPaper", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +WaveshareEPaperBase = waveshare_epaper_ns.class_( + "WaveshareEPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +) +WaveshareEPaper = waveshare_epaper_ns.class_("WaveshareEPaper", WaveshareEPaperBase) +WaveshareEPaperBWR = waveshare_epaper_ns.class_( + "WaveshareEPaperBWR", WaveshareEPaperBase ) WaveshareEPaperTypeA = waveshare_epaper_ns.class_( "WaveshareEPaperTypeA", WaveshareEPaper @@ -26,10 +30,28 @@ WaveshareEPaperTypeA = waveshare_epaper_ns.class_( WaveshareEPaper2P7In = waveshare_epaper_ns.class_( "WaveshareEPaper2P7In", WaveshareEPaper ) +WaveshareEPaper2P7InB = waveshare_epaper_ns.class_( + "WaveshareEPaper2P7InB", WaveshareEPaperBWR +) +WaveshareEPaper2P7InBV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P7InBV2", WaveshareEPaperBWR +) +WaveshareEPaper2P7InV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P7InV2", WaveshareEPaper +) WaveshareEPaper2P9InB = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InB", WaveshareEPaper ) -GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper) +WaveshareEPaper2P9InBV3 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P9InBV3", WaveshareEPaper +) +WaveshareEPaper2P9InV2R2 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P9InV2R2", WaveshareEPaper +) +GDEW029T5 = waveshare_epaper_ns.class_("GDEW029T5", WaveshareEPaper) +WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_( + "WaveshareEPaper2P9InDKE", WaveshareEPaper +) WaveshareEPaper4P2In = waveshare_epaper_ns.class_( "WaveshareEPaper4P2In", WaveshareEPaper ) @@ -51,6 +73,9 @@ WaveshareEPaper7P5InBC = waveshare_epaper_ns.class_( WaveshareEPaper7P5InBV2 = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InBV2", WaveshareEPaper ) +WaveshareEPaper7P5InBV3 = waveshare_epaper_ns.class_( + "WaveshareEPaper7P5InBV3", WaveshareEPaper +) WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InV2", WaveshareEPaper ) @@ -63,6 +88,16 @@ WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_( "WaveshareEPaper2P13InDKE", WaveshareEPaper ) +WaveshareEPaper2P13InV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P13InV2", WaveshareEPaper +) +WaveshareEPaper2P13InV3 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P13InV3", WaveshareEPaper +) +WaveshareEPaper13P3InK = waveshare_epaper_ns.class_( + "WaveshareEPaper13P3InK", WaveshareEPaper +) +GDEW0154M09 = waveshare_epaper_ns.class_("GDEW0154M09", WaveshareEPaper) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel") WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeBModel") @@ -71,28 +106,41 @@ MODELS = { "1.54in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN), "1.54inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN_V2), "2.13in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN), + "2.13inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN_V2), "2.13in-ttgo": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN), "2.13in-ttgo-b1": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B1), "2.13in-ttgo-b73": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B73), "2.13in-ttgo-b74": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B74), "2.90in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), "2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2), - "gdey029t94": ("c", GDEY029T94), + "gdew029t5": ("c", GDEW029T5), "2.70in": ("b", WaveshareEPaper2P7In), + "2.70in-b": ("b", WaveshareEPaper2P7InB), + "2.70in-bv2": ("b", WaveshareEPaper2P7InBV2), + "2.70inv2": ("b", WaveshareEPaper2P7InV2), "2.90in-b": ("b", WaveshareEPaper2P9InB), + "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), + "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2), + "2.90in-dke": ("c", WaveshareEPaper2P9InDKE), "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "5.83in": ("b", WaveshareEPaper5P8In), "5.83inv2": ("b", WaveshareEPaper5P8InV2), "7.50in": ("b", WaveshareEPaper7P5In), "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), + "7.50in-bv3": ("b", WaveshareEPaper7P5InBV3), "7.50in-bc": ("b", WaveshareEPaper7P5InBC), "7.50inv2": ("b", WaveshareEPaper7P5InV2), "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), + "2.13inv3": ("c", WaveshareEPaper2P13InV3), + "1.54in-m5coreink-m09": ("c", GDEW0154M09), + "13.3in-k": ("b", WaveshareEPaper13P3InK), } +RESET_PIN_REQUIRED_MODELS = ("2.13inv2", "2.13in-ttgo-b74") + def validate_full_update_every_only_types_ac(value): if CONF_FULL_UPDATE_EVERY not in value: @@ -109,15 +157,23 @@ def validate_full_update_every_only_types_ac(value): return value +def validate_reset_pin_required(config): + if config[CONF_MODEL] in RESET_PIN_REQUIRED_MODELS and CONF_RESET_PIN not in config: + raise cv.Invalid( + f"'{CONF_RESET_PIN}' is required for model {config[CONF_MODEL]}" + ) + return config + + CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(WaveshareEPaper), + cv.GenerateID(): cv.declare_id(WaveshareEPaperBase), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True), cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, + cv.Optional(CONF_FULL_UPDATE_EVERY): cv.int_range(min=1, max=4294967295), cv.Optional(CONF_RESET_DURATION): cv.All( cv.positive_time_period_milliseconds, cv.Range(max=core.TimePeriod(milliseconds=500)), @@ -127,9 +183,14 @@ CONFIG_SCHEMA = cv.All( .extend(cv.polling_component_schema("1s")) .extend(spi.spi_device_schema()), validate_full_update_every_only_types_ac, + validate_reset_pin_required, cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "waveshare_epaper", require_miso=False, require_mosi=True +) + async def to_code(config): model_type, model = MODELS[config[CONF_MODEL]] @@ -142,7 +203,6 @@ async def to_code(config): else: raise NotImplementedError() - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) @@ -151,7 +211,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) if CONF_RESET_PIN in config: diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp new file mode 100644 index 0000000000..196aeed3f7 --- /dev/null +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -0,0 +1,186 @@ +#include "waveshare_epaper.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace waveshare_epaper { + +static const char *const TAG = "waveshare_2.13v3"; + +static const uint8_t PARTIAL_LUT[] = { + 0x32, // cmd + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, +}; + +static const uint8_t FULL_LUT[] = { + 0x32, // CMD + 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0xF, 0x0, 0x0, 0x2, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, +}; + +static const uint8_t SW_RESET = 0x12; +static const uint8_t ACTIVATE = 0x20; +static const uint8_t WRITE_BUFFER = 0x24; +static const uint8_t WRITE_BASE = 0x26; + +static const uint8_t DRV_OUT_CTL[] = {0x01, 0x27, 0x01, 0x00}; // driver output control +static const uint8_t GATEV[] = {0x03, 0x17}; +static const uint8_t SRCV[] = {0x04, 0x41, 0x0C, 0x32}; +static const uint8_t SLEEP[] = {0x10, 0x01}; +static const uint8_t DATA_ENTRY[] = {0x11, 0x03}; // data entry mode +static const uint8_t TEMP_SENS[] = {0x18, 0x80}; // Temp sensor +static const uint8_t DISPLAY_UPDATE[] = {0x21, 0x00, 0x80}; // Display update control +static const uint8_t UPSEQ[] = {0x22, 0xC0}; +static const uint8_t ON_FULL[] = {0x22, 0xC7}; +static const uint8_t ON_PARTIAL[] = {0x22, 0x0F}; +static const uint8_t VCOM[] = {0x2C, 0x36}; +static const uint8_t CMD5[] = {0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t BORDER_PART[] = {0x3C, 0x80}; // border waveform +static const uint8_t BORDER_FULL[] = {0x3C, 0x05}; // border waveform +static const uint8_t CMD1[] = {0x3F, 0x22}; +static const uint8_t RAM_X_START[] = {0x44, 0x00, 121 / 8}; // set ram_x_address_start_end +static const uint8_t RAM_Y_START[] = {0x45, 0x00, 0x00, 250 - 1, 0}; // set ram_y_address_start_end +static const uint8_t RAM_X_POS[] = {0x4E, 0x00}; // set ram_x_address_counter +// static const uint8_t RAM_Y_POS[] = {0x4F, 0x00, 0x00}; // set ram_y_address_counter +#define SEND(x) this->cmd_data(x, sizeof(x)) + +void WaveshareEPaper2P13InV3::write_lut_(const uint8_t *lut) { + this->wait_until_idle_(); + this->cmd_data(lut, sizeof(PARTIAL_LUT)); + SEND(CMD1); + SEND(GATEV); + SEND(SRCV); + SEND(VCOM); +} + +// write the buffer starting on line top, up to line bottom. +void WaveshareEPaper2P13InV3::write_buffer_(uint8_t cmd, int top, int bottom) { + this->wait_until_idle_(); + this->set_window_(top, bottom); + this->command(cmd); + this->start_data_(); + auto width_bytes = this->get_width_internal() / 8; + this->write_array(this->buffer_ + top * width_bytes, (bottom - top) * width_bytes); + this->end_data_(); +} + +void WaveshareEPaper2P13InV3::send_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + } +} + +void WaveshareEPaper2P13InV3::setup() { + setup_pins_(); + delay(20); + this->send_reset_(); + // as a one-off delay this is not worth working around. + delay(100); // NOLINT + this->wait_until_idle_(); + this->command(SW_RESET); + this->wait_until_idle_(); + + SEND(DRV_OUT_CTL); + SEND(DATA_ENTRY); + SEND(CMD5); + this->set_window_(0, this->get_height_internal()); + SEND(BORDER_FULL); + SEND(DISPLAY_UPDATE); + SEND(TEMP_SENS); + this->wait_until_idle_(); + this->write_lut_(FULL_LUT); +} + +// t and b are y positions, i.e. line numbers. +void WaveshareEPaper2P13InV3::set_window_(int t, int b) { + uint8_t buffer[3]; + + SEND(RAM_X_START); + SEND(RAM_Y_START); + SEND(RAM_X_POS); + buffer[0] = 0x4F; + buffer[1] = (uint8_t) t; + buffer[2] = (uint8_t) (t >> 8); + SEND(buffer); +} + +// must implement, but we override setup to have more control +void WaveshareEPaper2P13InV3::initialize() {} + +void WaveshareEPaper2P13InV3::partial_update_() { + this->send_reset_(); + this->set_timeout(100, [this] { + this->write_lut_(PARTIAL_LUT); + SEND(BORDER_PART); + SEND(UPSEQ); + this->command(ACTIVATE); + this->set_timeout(100, [this] { + this->wait_until_idle_(); + this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal()); + SEND(ON_PARTIAL); + this->command(ACTIVATE); // Activate Display Update Sequence + this->is_busy_ = false; + }); + }); +} + +void WaveshareEPaper2P13InV3::full_update_() { + ESP_LOGI(TAG, "Performing full e-paper update."); + this->write_lut_(FULL_LUT); + this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal()); + this->write_buffer_(WRITE_BASE, 0, this->get_height_internal()); + SEND(ON_FULL); + this->command(ACTIVATE); // don't wait here + this->is_busy_ = false; +} + +void WaveshareEPaper2P13InV3::display() { + if (this->is_busy_ || (this->busy_pin_ != nullptr && this->busy_pin_->digital_read())) + return; + this->is_busy_ = true; + const bool partial = this->at_update_ != 0; + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; + if (partial) { + this->partial_update_(); + } else { + this->full_update_(); + } +} + +int WaveshareEPaper2P13InV3::get_width_internal() { return 128; } + +int WaveshareEPaper2P13InV3::get_height_internal() { return 250; } + +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_UPDATE_INTERVAL(this) +} + +void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + +} // namespace waveshare_epaper +} // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 42f5bc54e3..7c1d436673 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include namespace esphome { namespace waveshare_epaper { @@ -82,7 +83,34 @@ static const uint8_t PARTIAL_UPDATE_LUT_TTGO_B1[LUT_SIZE_TTGO_B1] = { 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -void WaveshareEPaper::setup_pins_() { +// clang-format off +// Disable formatting to preserve the same look as in Waveshare examples +static const uint8_t PARTIAL_UPD_2IN9_LUT_SIZE = 159; +static const uint8_t PARTIAL_UPD_2IN9_LUT[PARTIAL_UPD_2IN9_LUT_SIZE] = +{ + 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, + 0x22, 0x17, 0x41, 0xB0, 0x32, 0x36, +}; +// clang-format on + +void WaveshareEPaperBase::setup_pins_() { this->init_internal_(this->get_buffer_length_()); this->dc_pin_->setup(); // OUTPUT this->dc_pin_->digital_write(false); @@ -97,19 +125,31 @@ void WaveshareEPaper::setup_pins_() { this->reset_(); } -float WaveshareEPaper::get_setup_priority() const { return setup_priority::PROCESSOR; } -void WaveshareEPaper::command(uint8_t value) { +float WaveshareEPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; } +void WaveshareEPaperBase::command(uint8_t value) { this->start_command_(); this->write_byte(value); this->end_command_(); } -void WaveshareEPaper::data(uint8_t value) { +void WaveshareEPaperBase::data(uint8_t value) { this->start_data_(); this->write_byte(value); this->end_data_(); } -bool WaveshareEPaper::wait_until_idle_() { - if (this->busy_pin_ == nullptr) { + +// write a command followed by one or more bytes of data. +// The command is the first byte, length is the total including cmd. +void WaveshareEPaperBase::cmd_data(const uint8_t *c_data, size_t length) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(c_data[0]); + this->dc_pin_->digital_write(true); + this->write_array(c_data + 1, length - 1); + this->disable(); +} + +bool WaveshareEPaperBase::wait_until_idle_() { + if (this->busy_pin_ == nullptr || !this->busy_pin_->digital_read()) { return true; } @@ -119,11 +159,11 @@ bool WaveshareEPaper::wait_until_idle_() { ESP_LOGE(TAG, "Timeout while displaying image!"); return false; } - delay(10); + delay(1); } return true; } -void WaveshareEPaper::update() { +void WaveshareEPaperBase::update() { this->do_update_(); this->display(); } @@ -146,32 +186,84 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color this->buffer_[pos] &= ~(0x80 >> subpos); } } + uint32_t WaveshareEPaper::get_buffer_length_() { return this->get_width_controller() * this->get_height_internal() / 8u; +} // just a black buffer +uint32_t WaveshareEPaperBWR::get_buffer_length_() { + return this->get_width_controller() * this->get_height_internal() / 4u; +} // black and red buffer + +void WaveshareEPaperBWR::fill(Color color) { + this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } -void WaveshareEPaper::start_command_() { +void HOT WaveshareEPaperBWR::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) + return; + + const uint32_t buf_half_len = this->get_buffer_length_() / 2u; + + const uint32_t pos = (x + y * this->get_width_internal()) / 8u; + const uint8_t subpos = x & 0x07; + // flip logic + if (color.is_on()) { + this->buffer_[pos] |= 0x80 >> subpos; + } else { + this->buffer_[pos] &= ~(0x80 >> subpos); + } + + // draw red pixels only, if the color contains red only + if (((color.red > 0) && (color.green == 0) && (color.blue == 0))) { + this->buffer_[pos + buf_half_len] |= 0x80 >> subpos; + } else { + this->buffer_[pos + buf_half_len] &= ~(0x80 >> subpos); + } +} + +void WaveshareEPaperBase::start_command_() { this->dc_pin_->digital_write(false); this->enable(); } -void WaveshareEPaper::end_command_() { this->disable(); } -void WaveshareEPaper::start_data_() { +void WaveshareEPaperBase::end_command_() { this->disable(); } +void WaveshareEPaperBase::start_data_() { this->dc_pin_->digital_write(true); this->enable(); } -void WaveshareEPaper::end_data_() { this->disable(); } -void WaveshareEPaper::on_safe_shutdown() { this->deep_sleep(); } +void WaveshareEPaperBase::end_data_() { this->disable(); } +void WaveshareEPaperBase::on_safe_shutdown() { this->deep_sleep(); } // ======================================================== // Type A // ======================================================== void WaveshareEPaperTypeA::initialize() { - if (this->model_ == TTGO_EPAPER_2_13_IN_B74) { - this->reset_pin_->digital_write(false); - delay(10); - this->reset_pin_->digital_write(true); - delay(10); - this->wait_until_idle_(); + // Achieve display intialization + this->init_display_(); + // If a reset pin is configured, eligible displays can be set to deep sleep + // between updates, as recommended by the hardware provider + if (this->reset_pin_ != nullptr) { + switch (this->model_) { + // More models can be added here to enable deep sleep if eligible + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: + this->deep_sleep_between_updates_ = true; + ESP_LOGI(TAG, "Set the display to deep sleep"); + this->deep_sleep(); + break; + default: + break; + } + } +} +void WaveshareEPaperTypeA::init_display_() { + if (this->model_ == TTGO_EPAPER_2_13_IN_B74 || this->model_ == WAVESHARE_EPAPER_2_13_IN_V2) { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + this->wait_until_idle_(); + } this->command(0x12); // SWRESET this->wait_until_idle_(); @@ -231,6 +323,9 @@ void WaveshareEPaperTypeA::dump_config() { case WAVESHARE_EPAPER_2_13_IN: ESP_LOGCONFIG(TAG, " Model: 2.13in"); break; + case WAVESHARE_EPAPER_2_13_IN_V2: + ESP_LOGCONFIG(TAG, " Model: 2.13inV2"); + break; case TTGO_EPAPER_2_13_IN: ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO)"); break; @@ -250,7 +345,7 @@ void WaveshareEPaperTypeA::dump_config() { ESP_LOGCONFIG(TAG, " Model: 2.9inV2"); break; } - ESP_LOGCONFIG(TAG, " Full Update Every: %u", this->full_update_every_); + ESP_LOGCONFIG(TAG, " 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_); @@ -260,6 +355,13 @@ void HOT WaveshareEPaperTypeA::display() { bool full_update = this->at_update_ == 0; bool prev_full_update = this->at_update_ == 1; + if (this->deep_sleep_between_updates_) { + ESP_LOGI(TAG, "Wake up the display"); + this->reset_(); + this->wait_until_idle_(); + this->init_display_(); + } + if (!this->wait_until_idle_()) { this->status_set_warning(); return; @@ -269,6 +371,8 @@ void HOT WaveshareEPaperTypeA::display() { if (full_update != prev_full_update) { switch (this->model_) { case TTGO_EPAPER_2_13_IN: + case WAVESHARE_EPAPER_2_13_IN_V2: + // Waveshare 2.13" V2 uses the same LUTs as TTGO this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO : PARTIAL_UPDATE_LUT_TTGO, LUT_SIZE_TTGO); break; case TTGO_EPAPER_2_13_IN_B73: @@ -287,6 +391,41 @@ void HOT WaveshareEPaperTypeA::display() { this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; } + if (this->model_ == WAVESHARE_EPAPER_2_13_IN_V2) { + // Set VCOM for full or partial update + this->command(0x2C); + this->data(full_update ? 0x55 : 0x26); + + if (!full_update) { + // Enable "ping-pong" + this->command(0x37); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x40); + this->data(0x00); + this->data(0x00); + this->command(0x22); + this->data(0xc0); + this->command(0x20); + } + } + + // Border waveform + switch (this->model_) { + case TTGO_EPAPER_2_13_IN_B74: + this->command(0x3C); + this->data(full_update ? 0x05 : 0x80); + break; + case WAVESHARE_EPAPER_2_13_IN_V2: + this->command(0x3C); + this->data(full_update ? 0x03 : 0x01); + break; + default: + break; + } + // Set x & y regions we want to write to (full) switch (this->model_) { case TTGO_EPAPER_2_13_IN_B1: @@ -310,12 +449,6 @@ void HOT WaveshareEPaperTypeA::display() { this->data((this->get_height_internal() - 1) >> 8); break; - case TTGO_EPAPER_2_13_IN_B74: - // BorderWaveform - this->command(0x3C); - this->data(full_update ? 0x05 : 0x80); - - // fall through default: // COMMAND SET RAM X ADDRESS START END POSITION this->command(0x44); @@ -347,7 +480,7 @@ void HOT WaveshareEPaperTypeA::display() { this->start_data_(); switch (this->model_) { case TTGO_EPAPER_2_13_IN_B1: { // block needed because of variable initializations - int16_t wb = ((this->get_width_internal()) >> 3); + int16_t wb = ((this->get_width_controller()) >> 3); for (int i = 0; i < this->get_height_internal(); i++) { for (int j = 0; j < wb; j++) { int idx = j + (this->get_height_internal() - 1 - i) * wb; @@ -361,6 +494,14 @@ void HOT WaveshareEPaperTypeA::display() { } this->end_data_(); + if (this->model_ == WAVESHARE_EPAPER_2_13_IN_V2 && full_update) { + // Write base image again on full refresh + this->command(0x26); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + } + // COMMAND DISPLAY UPDATE CONTROL 2 this->command(0x22); switch (this->model_) { @@ -372,6 +513,9 @@ void HOT WaveshareEPaperTypeA::display() { case TTGO_EPAPER_2_13_IN_B73: this->data(0xC7); break; + case WAVESHARE_EPAPER_2_13_IN_V2: + this->data(full_update ? 0xC7 : 0x0C); + break; default: this->data(0xC4); break; @@ -383,6 +527,11 @@ void HOT WaveshareEPaperTypeA::display() { this->command(0xFF); this->status_clear_warning(); + + if (this->deep_sleep_between_updates_) { + ESP_LOGI(TAG, "Set the display back to deep sleep"); + this->deep_sleep(); + } } int WaveshareEPaperTypeA::get_width_internal() { switch (this->model_) { @@ -390,6 +539,7 @@ int WaveshareEPaperTypeA::get_width_internal() { case WAVESHARE_EPAPER_1_54_IN_V2: return 200; case WAVESHARE_EPAPER_2_13_IN: + case WAVESHARE_EPAPER_2_13_IN_V2: case TTGO_EPAPER_2_13_IN: case TTGO_EPAPER_2_13_IN_B73: case TTGO_EPAPER_2_13_IN_B74: @@ -405,6 +555,7 @@ int WaveshareEPaperTypeA::get_width_internal() { int WaveshareEPaperTypeA::get_width_controller() { switch (this->model_) { case WAVESHARE_EPAPER_2_13_IN: + case WAVESHARE_EPAPER_2_13_IN_V2: case TTGO_EPAPER_2_13_IN: case TTGO_EPAPER_2_13_IN_B73: case TTGO_EPAPER_2_13_IN_B74: @@ -420,6 +571,7 @@ int WaveshareEPaperTypeA::get_height_internal() { case WAVESHARE_EPAPER_1_54_IN_V2: return 200; case WAVESHARE_EPAPER_2_13_IN: + case WAVESHARE_EPAPER_2_13_IN_V2: case TTGO_EPAPER_2_13_IN: case TTGO_EPAPER_2_13_IN_B73: case TTGO_EPAPER_2_13_IN_B74: @@ -444,10 +596,13 @@ void WaveshareEPaperTypeA::set_full_update_every(uint32_t full_update_every) { uint32_t WaveshareEPaperTypeA::idle_timeout_() { switch (this->model_) { + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: + case WAVESHARE_EPAPER_2_13_IN_V2: case TTGO_EPAPER_2_13_IN_B1: return 2500; default: - return WaveshareEPaper::idle_timeout_(); + return WaveshareEPaperBase::idle_timeout_(); } } @@ -600,6 +755,299 @@ void WaveshareEPaper2P7In::dump_config() { LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper2P7InV2::initialize() { + this->reset_(); + this->wait_until_idle_(); + + this->command(0x12); // SWRESET + this->wait_until_idle_(); + + // SET WINDOWS + // XRAM_START_AND_END_POSITION + this->command(0x44); + this->data(0x00); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); + // YRAM_START_AND_END_POSITION + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data((get_height_internal() - 1) & 0xFF); + this->data(((get_height_internal() - 1) >> 8) & 0xFF); + + // SET CURSOR + // XRAM_ADDRESS + this->command(0x4E); + this->data(0x00); + // YRAM_ADDRESS + this->command(0x4F); + this->data(0x00); + this->data(0x00); + + this->command(0x11); // data entry mode + this->data(0x03); +} +void HOT WaveshareEPaper2P7InV2::display() { + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // COMMAND DISPLAY REFRESH + this->command(0x22); + this->data(0xF7); + this->command(0x20); +} +int WaveshareEPaper2P7InV2::get_width_internal() { return 176; } +int WaveshareEPaper2P7InV2::get_height_internal() { return 264; } +void WaveshareEPaper2P7InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.7in V2"); + 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); +} + +// ======================================================== +// 2.7inch_e-paper_b +// ======================================================== +// Datasheet: +// - https://www.waveshare.com/w/upload/d/d8/2.7inch-e-paper-b-specification.pdf +// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in7b.c + +static const uint8_t LUT_VCOM_DC_2_7B[44] = {0x00, 0x00, 0x00, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x0A, + 0x00, 0x00, 0x08, 0x00, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x00, 0x0A, + 0x0A, 0x00, 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, + 0x03, 0x0E, 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01}; + +static const uint8_t LUT_WHITE_TO_WHITE_2_7B[42] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x40, 0x0A, 0x0A, 0x00, 0x00, + 0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x80, 0x0A, 0x0A, 0x00, + 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E, + 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01}; + +static const uint8_t LUT_BLACK_TO_WHITE_2_7B[42] = {0xA0, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x0A, 0x00, 0x00, + 0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x90, 0x0A, 0x0A, 0x00, + 0x00, 0x08, 0xB0, 0x04, 0x10, 0x00, 0x00, 0x05, 0xB0, 0x03, 0x0E, + 0x00, 0x00, 0x0A, 0xC0, 0x23, 0x00, 0x00, 0x00, 0x01}; + +static const uint8_t LUT_WHITE_TO_BLACK_2_7B[] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x20, 0x0A, 0x0A, 0x00, 0x00, + 0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x10, 0x0A, 0x0A, 0x00, + 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E, + 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01}; + +static const uint8_t LUT_BLACK_TO_BLACK_2_7B[42] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x40, 0x0A, 0x0A, 0x00, 0x00, + 0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x80, 0x0A, 0x0A, 0x00, + 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E, + 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01}; + +void WaveshareEPaper2P7InB::initialize() { + this->reset_(); + + // command power on + this->command(0x04); + this->wait_until_idle_(); + delay(10); + + // Command panel setting + this->command(0x00); + this->data(0xAF); // KW-BF KWR-AF BWROTP 0f + // command pll control + this->command(0x30); + this->data(0x3A); // 3A 100HZ 29 150Hz 39 200HZ 31 171HZ + + // command power setting + this->command(0x01); + this->data(0x03); // VDS_EN, VDG_EN + this->data(0x00); // VCOM_HV, VGHL_LV[1], VGHL_LV[0] + this->data(0x2B); // VDH + this->data(0x2B); // VDL + this->data(0x09); // VDHR + + // command booster soft start + this->command(0x06); + this->data(0x07); + this->data(0x07); + this->data(0x17); + + // Power optimization - ??? + this->command(0xF8); + this->data(0x60); + this->data(0xA5); + this->command(0xF8); + this->data(0x89); + this->data(0xA5); + this->command(0xF8); + this->data(0x90); + this->data(0x00); + this->command(0xF8); + this->data(0x93); + this->data(0x2A); + this->command(0xF8); + this->data(0x73); + this->data(0x41); + + // COMMAND VCM DC SETTING + this->command(0x82); + this->data(0x12); + + // VCOM_AND_DATA_INTERVAL_SETTING + this->command(0x50); + this->data(0x87); // define by OTP + + delay(2); + // COMMAND LUT FOR VCOM + this->command(0x20); + for (uint8_t i : LUT_VCOM_DC_2_7B) + this->data(i); + // COMMAND LUT WHITE TO WHITE + this->command(0x21); + for (uint8_t i : LUT_WHITE_TO_WHITE_2_7B) + this->data(i); + // COMMAND LUT BLACK TO WHITE + this->command(0x22); + for (uint8_t i : LUT_BLACK_TO_WHITE_2_7B) + this->data(i); + // COMMAND LUT WHITE TO BLACK + this->command(0x23); + for (uint8_t i : LUT_WHITE_TO_BLACK_2_7B) { + this->data(i); + } + // COMMAND LUT BLACK TO BLACK + this->command(0x24); + + for (uint8_t i : LUT_BLACK_TO_BLACK_2_7B) { + this->data(i); + } + + delay(2); +} + +void HOT WaveshareEPaper2P7InB::display() { + uint32_t buf_len_half = this->get_buffer_length_() >> 1; + this->initialize(); + + // TCON_RESOLUTION + this->command(0x61); + this->data(this->get_width_controller() >> 8); + this->data(this->get_width_controller() & 0xff); // 176 + this->data(this->get_height_internal() >> 8); + this->data(this->get_height_internal() & 0xff); // 264 + + // COMMAND DATA START TRANSMISSION 1 (BLACK) + this->command(0x10); + delay(2); + for (uint32_t i = 0; i < buf_len_half; i++) { + this->data(this->buffer_[i]); + } + this->command(0x11); + delay(2); + + // COMMAND DATA START TRANSMISSION 2 (RED) + this->command(0x13); + delay(2); + for (uint32_t i = buf_len_half; i < buf_len_half * 2u; i++) { + this->data(this->buffer_[i]); + } + this->command(0x11); + + delay(2); + + // COMMAND DISPLAY REFRESH + this->command(0x12); + this->wait_until_idle_(); + + this->deep_sleep(); +} +int WaveshareEPaper2P7InB::get_width_internal() { return 176; } +int WaveshareEPaper2P7InB::get_height_internal() { return 264; } +void WaveshareEPaper2P7InB::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.7in B"); + 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); +} + +// ======================================================== +// 2.7inch_e-paper_b_v2 +// ======================================================== +// Datasheet: +// - https://www.waveshare.com/w/upload/7/7b/2.7inch-e-paper-b-v2-specification.pdf +// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in7b_V2.c + +void WaveshareEPaper2P7InBV2::initialize() { + this->reset_(); + + this->wait_until_idle_(); + this->command(0x12); + this->wait_until_idle_(); + + this->command(0x00); + this->data(0x27); + this->data(0x01); + this->data(0x00); + + this->command(0x11); + this->data(0x03); + + // self.SetWindows(0, 0, self.width-1, self.height-1) + // SetWindows(self, Xstart, Ystart, Xend, Yend): + + uint32_t xend = this->get_width_controller() - 1; + uint32_t yend = this->get_height_internal() - 1; + this->command(0x44); + this->data(0x00); + this->data((xend >> 3) & 0xff); + + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data(yend & 0xff); + this->data((yend >> 8) & 0xff); + + // SetCursor(self, Xstart, Ystart): + this->command(0x4E); + this->data(0x00); + this->command(0x4F); + this->data(0x00); + this->data(0x00); +} + +void HOT WaveshareEPaper2P7InBV2::display() { + uint32_t buf_len = this->get_buffer_length_(); + // COMMAND DATA START TRANSMISSION 1 (BLACK) + this->command(0x24); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } + delay(2); + + // COMMAND DATA START TRANSMISSION 2 (RED) + this->command(0x26); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } + + delay(2); + + this->command(0x20); + + this->wait_until_idle_(); +} +int WaveshareEPaper2P7InBV2::get_width_internal() { return 176; } +int WaveshareEPaper2P7InBV2::get_height_internal() { return 264; } +void WaveshareEPaper2P7InBV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.7in B V2"); + 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); +} + // ======================================================== // 2.90in Type B (LUT from OTP) // Datasheet: @@ -679,6 +1127,386 @@ void WaveshareEPaper2P9InB::dump_config() { LOG_UPDATE_INTERVAL(this); } +// DKE 2.9 +// https://www.badge.team/docs/badges/sha2017/hardware/#e-ink-display-the-dke-group-depg0290b1 +// https://www.badge.team/docs/badges/sha2017/hardware/DEPG0290B01V3.0.pdf +static const uint8_t LUT_SIZE_DKE = 70; +static const uint8_t UPDATE_LUT_DKE[LUT_SIZE_DKE] = { + 0xA0, 0x90, 0x50, 0x0, 0x0, 0x0, 0x0, 0x50, 0x90, 0xA0, 0x0, 0x0, 0x0, 0x0, 0xA0, 0x90, 0x50, 0x0, + 0x0, 0x0, 0x0, 0x50, 0x90, 0xA0, 0x0, 0x0, 0x0, 0x0, 0x00, 0x00, 0x00, 0x0, 0x0, 0x0, 0x0, 0xF, + 0xF, 0x0, 0x0, 0x0, 0xF, 0xF, 0x0, 0x0, 0x02, 0xF, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, +}; +static const uint8_t PART_UPDATE_LUT_DKE[LUT_SIZE_DKE] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x50, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0x05, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t FULL_UPDATE_LUT_DKE[LUT_SIZE_DKE] = { + 0x90, 0x50, 0xa0, 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x10, 0xa0, 0xa0, 0x80, 0x00, 0x90, 0x50, 0xa0, 0x50, + 0x50, 0x00, 0x00, 0x00, 0x00, 0x10, 0xa0, 0xa0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, + 0x04, 0x00, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, 0x00, 0x06, 0x05, 0x00, 0x00, 0x00, 0x04, 0x05, 0x00, 0x00, + 0x00, 0x01, 0x0e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +void WaveshareEPaper2P9InDKE::initialize() { + // Hardware reset + delay(10); + this->reset_pin_->digital_write(false); + delayMicroseconds(200); + this->reset_pin_->digital_write(true); + delayMicroseconds(200); + // Wait for busy low + this->wait_until_idle_(); + // Software reset + this->command(0x12); + // Wait for busy low + this->wait_until_idle_(); + // Set Analog Block Control + this->command(0x74); + this->data(0x54); + // Set Digital Block Control + this->command(0x7E); + this->data(0x3B); + // Set display size and driver output control + this->command(0x01); + // this->data(0x27); + // this->data(0x01); + // this->data(0x00); + this->data(this->get_height_internal() - 1); + this->data((this->get_height_internal() - 1) >> 8); + this->data(0x00); // ? GD = 0, SM = 0, TB = 0 + // Ram data entry mode + this->command(0x11); + this->data(0x03); + // Set Ram X address + this->command(0x44); + this->data(0x00); + this->data(0x0F); + // Set Ram Y address + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data(0x27); + this->data(0x01); + // Set border + this->command(0x3C); + // this->data(0x80); + this->data(0x01); + // Set VCOM value + this->command(0x2C); + this->data(0x26); + // Gate voltage setting + this->command(0x03); + this->data(0x17); + // Source voltage setting + this->command(0x04); + this->data(0x41); + this->data(0x00); + this->data(0x32); + // Frame setting 50hz + this->command(0x3A); + this->data(0x30); + this->command(0x3B); + this->data(0x0A); + // Load LUT + this->command(0x32); + for (uint8_t v : FULL_UPDATE_LUT_DKE) + this->data(v); +} + +void HOT WaveshareEPaper2P9InDKE::display() { + ESP_LOGI(TAG, "Performing e-paper update."); + // Set Ram X address counter + this->command(0x4e); + this->data(0); + // Set Ram Y address counter + this->command(0x4f); + this->data(0); + this->data(0); + // Load image (128/8*296) + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + // Image update + this->command(0x22); + this->data(0xC7); + this->command(0x20); + // Wait for busy low + this->wait_until_idle_(); + // Enter deep sleep mode + this->command(0x10); + this->data(0x01); +} +int WaveshareEPaper2P9InDKE::get_width_internal() { return 128; } +int WaveshareEPaper2P9InDKE::get_height_internal() { return 296; } +void WaveshareEPaper2P9InDKE::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in DKE"); + 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); +} +void WaveshareEPaper2P9InDKE::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + +// ======================================================== +// 2.90in Type B (LUT from OTP) +// Datasheet: +// - https://files.waveshare.com/upload/a/af/2.9inch-e-paper-b-v3-specification.pdf +// ======================================================== + +void WaveshareEPaper2P9InBV3::initialize() { + // from https://github.com/waveshareteam/e-Paper/blob/master/Arduino/epd2in9b_V3/epd2in9b_V3.cpp + this->reset_(); + + // COMMAND POWER ON + this->command(0x04); + this->wait_until_idle_(); + + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x0F); + this->data(0x89); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x80); + this->data(0x01); + this->data(0x28); + + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x77); +} +void HOT WaveshareEPaper2P9InBV3::display() { + // COMMAND DATA START TRANSMISSION 1 (B/W data) + this->command(0x10); + delay(2); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + this->command(0x92); + delay(2); + + // COMMAND DATA START TRANSMISSION 2 (RED data) + this->command(0x13); + delay(2); + this->start_data_(); + for (size_t i = 0; i < this->get_buffer_length_(); i++) + this->write_byte(0xFF); + this->end_data_(); + this->command(0x92); + delay(2); + + // COMMAND DISPLAY REFRESH + this->command(0x12); + delay(2); + this->wait_until_idle_(); + + // COMMAND POWER OFF + // NOTE: power off < deep sleep + this->command(0x02); +} +int WaveshareEPaper2P9InBV3::get_width_internal() { return 128; } +int WaveshareEPaper2P9InBV3::get_height_internal() { return 296; } +void WaveshareEPaper2P9InBV3::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in (B) V3"); + 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); +} + +// ======================================================== +// 2.90in v2 rev2 +// based on SDK and examples in ZIP file from: +// https://www.waveshare.com/pico-epaper-2.9.htm +// ======================================================== + +void WaveshareEPaper2P9InV2R2::initialize() { + this->reset_(); + this->wait_until_idle_(); + + this->command(0x12); // SWRESET + this->wait_until_idle_(); + + this->command(0x01); + this->data(0x27); + this->data(0x01); + this->data(0x00); + + this->command(0x11); + this->data(0x03); + + // SetWindows(0, 0, w, h) + this->command(0x44); + this->data(0x00); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); + + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data((this->get_height_internal() - 1) & 0xFF); + this->data(((this->get_height_internal() - 1) >> 8) & 0xFF); + + this->command(0x21); + this->data(0x00); + this->data(0x80); + + // SetCursor(0, 0) + this->command(0x4E); + this->data(0x00); + this->command(0x4f); + this->data(0x00); + this->data(0x00); + + this->wait_until_idle_(); +} + +WaveshareEPaper2P9InV2R2::WaveshareEPaper2P9InV2R2() { this->reset_duration_ = 10; } + +void WaveshareEPaper2P9InV2R2::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(reset_duration_); // NOLINT + this->reset_pin_->digital_write(true); + delay(reset_duration_); // NOLINT + } +} + +void WaveshareEPaper2P9InV2R2::display() { + if (!this->wait_until_idle_()) { + this->status_set_warning(); + ESP_LOGE(TAG, "fail idle 1"); + return; + } + + if (this->full_update_every_ == 1) { + // do single full update + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplay + this->command(0x22); + this->data(0xF7); + this->command(0x20); + return; + } + + // if (this->full_update_every_ == 1 || + if (this->at_update_ == 0) { + // do base update + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + this->command(0x26); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplay + this->command(0x22); + this->data(0xF7); + this->command(0x20); + } else { + // do partial update + this->reset_(); + + this->write_lut_(PARTIAL_UPD_2IN9_LUT, PARTIAL_UPD_2IN9_LUT_SIZE); + + this->command(0x37); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x40); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + + this->command(0x3C); + this->data(0x80); + + this->command(0x22); + this->data(0xC0); + this->command(0x20); + + if (!this->wait_until_idle_()) { + ESP_LOGE(TAG, "fail idle 2"); + } + + // SetWindows(0, 0, w, h) + this->command(0x44); + this->data(0x00); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); + + this->command(0x45); + this->data(0x00); + this->data(0x00); + this->data((this->get_height_internal() - 1) & 0xFF); + this->data(((this->get_height_internal() - 1) >> 8) & 0xFF); + + // SetCursor(0, 0) + this->command(0x4E); + this->data(0x00); + this->command(0x4f); + this->data(0x00); + this->data(0x00); + + // write b/w + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplayPartial + this->command(0x22); + this->data(0x0F); + this->command(0x20); + } + + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; +} + +void WaveshareEPaper2P9InV2R2::write_lut_(const uint8_t *lut, const uint8_t size) { + // COMMAND WRITE LUT REGISTER + this->command(0x32); + for (uint8_t i = 0; i < size; i++) + this->data(lut[i]); +} + +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_); + 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); +} + +void WaveshareEPaper2P9InV2R2::deep_sleep() { + this->command(0x10); + this->data(0x01); +} + +int WaveshareEPaper2P9InV2R2::get_width_internal() { return 128; } +int WaveshareEPaper2P9InV2R2::get_height_internal() { return 296; } +int WaveshareEPaper2P9InV2R2::get_width_controller() { return this->get_width_internal(); } +void WaveshareEPaper2P9InV2R2::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + // ======================================================== // Good Display 2.9in black/white/grey // Datasheet: @@ -686,7 +1514,7 @@ void WaveshareEPaper2P9InB::dump_config() { // - https://github.com/adafruit/Adafruit_EPD/blob/master/src/panels/ThinkInk_290_Grayscale4_T5.h // ======================================================== -void GDEY029T94::initialize() { +void GDEW029T5::initialize() { // from https://www.waveshare.com/w/upload/b/bb/2.9inch-e-paper-b-specification.pdf, page 37 // EPD hardware init start this->reset_(); @@ -732,7 +1560,7 @@ void GDEY029T94::initialize() { // EPD hardware init end } -void HOT GDEY029T94::display() { +void HOT GDEW029T5::display() { // COMMAND DATA START TRANSMISSION 2 (B/W only) this->command(0x13); delay(2); @@ -752,11 +1580,151 @@ void HOT GDEY029T94::display() { // NOTE: power off < deep sleep this->command(0x02); } -int GDEY029T94::get_width_internal() { return 128; } -int GDEY029T94::get_height_internal() { return 296; } -void GDEY029T94::dump_config() { +int GDEW029T5::get_width_internal() { return 128; } +int GDEW029T5::get_height_internal() { return 296; } +void GDEW029T5::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper (Good Display)", this); - ESP_LOGCONFIG(TAG, " Model: 2.9in Greyscale GDEY029T94"); + ESP_LOGCONFIG(TAG, " Model: 2.9in Greyscale GDEW029T5"); + 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); +} + +// ======================================================== +// Good Display 1.54in black/white/grey GDEW0154M09 +// As used in M5Stack Core Ink +// Datasheet: +// - https://v4.cecdn.yun300.cn/100001_1909185148/GDEW0154M09-200709.pdf +// - https://github.com/m5stack/M5Core-Ink +// Reference code from GoodDisplay: +// - https://github.com/GoodDisplay/E-paper-Display-Library-of-GoodDisplay/ +// -> /Monochrome_E-paper-Display/1.54inch_JD79653_GDEW0154M09_200x200/ESP32-Arduino%20IDE/GDEW0154M09_Arduino.ino +// M5Stack Core Ink spec: +// - https://docs.m5stack.com/en/core/coreink +// ======================================================== + +void GDEW0154M09::initialize() { + this->init_internal_(); + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->lastbuff_ = allocator.allocate(this->get_buffer_length_()); + if (this->lastbuff_ != nullptr) { + memset(this->lastbuff_, 0xff, sizeof(uint8_t) * this->get_buffer_length_()); + } + this->clear_(); +} + +void GDEW0154M09::reset_() { + // RST is inverse from other einks in this project + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void GDEW0154M09::init_internal_() { + this->reset_(); + + // clang-format off + // 200x200 resolution: 11 + // LUT from OTP: 0 + // B/W mode (doesn't work): 1 + // scan-up: 1 + // shift-right: 1 + // booster ON: 1 + // no soft reset: 1 + const uint8_t panel_setting_1 = 0b11011111; + + // VCOM status off 0 + // Temp sensing default 1 + // VGL Power Off Floating 1 + // NORG expect refresh 1 + // VCOM Off on displ off 0 + const uint8_t panel_setting_2 = 0b01110; + + const uint8_t wf_t0154_cz_b3_list[] = { + 11, // 11 commands in list + CMD_PSR_PANEL_SETTING, 2, panel_setting_1, panel_setting_2, + CMD_UNDOCUMENTED_0x4D, 1, 0x55, + CMD_UNDOCUMENTED_0xAA, 1, 0x0f, + CMD_UNDOCUMENTED_0xE9, 1, 0x02, + CMD_UNDOCUMENTED_0xB6, 1, 0x11, + CMD_UNDOCUMENTED_0xF3, 1, 0x0a, + CMD_TRES_RESOLUTION_SETTING, 3, 0xc8, 0x00, 0xc8, + CMD_TCON_TCONSETTING, 1, 0x00, + CMD_CDI_VCOM_DATA_INTERVAL, 1, 0xd7, + CMD_PWS_POWER_SAVING, 1, 0x00, + CMD_PON_POWER_ON, 0 + }; + // clang-format on + + this->write_init_list_(wf_t0154_cz_b3_list); + delay(100); // NOLINT + this->wait_until_idle_(); +} + +void GDEW0154M09::write_init_list_(const uint8_t *list) { + uint8_t list_limit = list[0]; + uint8_t *start_ptr = ((uint8_t *) list + 1); + for (uint8_t i = 0; i < list_limit; i++) { + this->command(*(start_ptr + 0)); + for (uint8_t dnum = 0; dnum < *(start_ptr + 1); dnum++) { + this->data(*(start_ptr + 2 + dnum)); + } + start_ptr += (*(start_ptr + 1) + 2); + } +} + +void GDEW0154M09::clear_() { + uint32_t pixsize = this->get_buffer_length_(); + for (uint8_t j = 0; j < 2; j++) { + this->command(CMD_DTM1_DATA_START_TRANS); + for (int count = 0; count < pixsize; count++) { + this->data(0x00); + } + this->command(CMD_DTM2_DATA_START_TRANS2); + for (int count = 0; count < pixsize; count++) { + this->data(0xff); + } + this->command(CMD_DISPLAY_REFRESH); + delay(10); + this->wait_until_idle_(); + } +} + +void HOT GDEW0154M09::display() { + this->init_internal_(); + // "Mode 0 display" for now + this->command(CMD_DTM1_DATA_START_TRANS); + for (int i = 0; i < this->get_buffer_length_(); i++) { + this->data(0xff); + } + this->command(CMD_DTM2_DATA_START_TRANS2); // write 'new' data to SRAM + for (int i = 0; i < this->get_buffer_length_(); i++) { + this->data(this->buffer_[i]); + } + this->command(CMD_DISPLAY_REFRESH); + delay(10); + this->wait_until_idle_(); + this->deep_sleep(); +} + +void GDEW0154M09::deep_sleep() { + // COMMAND DEEP SLEEP + this->command(CMD_POF_POWER_OFF); + this->wait_until_idle_(); + delay(1000); // NOLINT + this->command(CMD_DSLP_DEEP_SLEEP); + this->data(DATA_DSLP_DEEP_SLEEP); +} + +int GDEW0154M09::get_width_internal() { return 200; } +int GDEW0154M09::get_height_internal() { return 200; } +void GDEW0154M09::dump_config() { + LOG_DISPLAY("", "M5Stack CoreInk E-Paper (Good Display)", this); + ESP_LOGCONFIG(TAG, " Model: 1.54in Greyscale GDEW0154M09"); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); @@ -1147,6 +2115,12 @@ void WaveshareEPaper7P5InBV2::initialize() { // COMMAND TCON SETTING this->command(0x60); this->data(0x22); + + this->command(0x82); + this->data(0x08); + this->command(0x30); + this->data(0x06); + // COMMAND RESOLUTION SETTING this->command(0x65); this->data(0x00); @@ -1176,6 +2150,7 @@ void HOT WaveshareEPaper7P5InBV2::display() { this->command(0x12); delay(100); // NOLINT this->wait_until_idle_(); + this->deep_sleep(); } int WaveshareEPaper7P5InBV2::get_width_internal() { return 800; } int WaveshareEPaper7P5InBV2::get_height_internal() { return 480; } @@ -1187,6 +2162,159 @@ void WaveshareEPaper7P5InBV2::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } + +void WaveshareEPaper7P5InBV3::initialize() { this->init_display_(); } +bool WaveshareEPaper7P5InBV3::wait_until_idle_() { + if (this->busy_pin_ == nullptr) { + return true; + } + + const uint32_t start = millis(); + while (this->busy_pin_->digital_read()) { + this->command(0x71); + if (millis() - start > this->idle_timeout_()) { + ESP_LOGI(TAG, "Timeout while displaying image!"); + return false; + } + App.feed_wdt(); + delay(10); + } + delay(200); // NOLINT + return true; +}; +void WaveshareEPaper7P5InBV3::init_display_() { + this->reset_(); + + // COMMAND POWER SETTING + this->command(0x01); + + // 1-0=11: internal power + this->data(0x07); + this->data(0x17); // VGH&VGL + this->data(0x3F); // VSH + this->data(0x26); // VSL + this->data(0x11); // VSHR + + // VCOM DC Setting + this->command(0x82); + this->data(0x24); // VCOM + + // Booster Setting + this->command(0x06); + this->data(0x27); + this->data(0x27); + this->data(0x2F); + this->data(0x17); + + // POWER ON + this->command(0x04); + + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x3F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x03); // source 800 + this->data(0x20); + this->data(0x01); // gate 480 + this->data(0xE0); + // COMMAND ...? + this->command(0x15); + this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x00); + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + // Resolution setting + this->command(0x65); + this->data(0x00); + this->data(0x00); // 800*480 + this->data(0x00); + this->data(0x00); + + uint8_t lut_vcom_7_i_n5_v2[] = { + 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0xF, 0x1, 0xF, 0x1, 0x2, 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_ww_7_i_n5_v2[] = { + 0x10, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x20, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_bw_7_i_n5_v2[] = { + 0x10, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x20, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_wb_7_i_n5_v2[] = { + 0x80, 0xF, 0xF, 0x0, 0x0, 0x3, 0x84, 0xF, 0x1, 0xF, 0x1, 0x4, 0x40, 0xF, 0xF, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_bb_7_i_n5_v2[] = { + 0x80, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x40, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t count; + this->command(0x20); // VCOM + for (count = 0; count < 42; count++) + this->data(lut_vcom_7_i_n5_v2[count]); + + this->command(0x21); // LUTBW + for (count = 0; count < 42; count++) + this->data(lut_ww_7_i_n5_v2[count]); + + this->command(0x22); // LUTBW + for (count = 0; count < 42; count++) + this->data(lut_bw_7_i_n5_v2[count]); + + this->command(0x23); // LUTWB + for (count = 0; count < 42; count++) + this->data(lut_wb_7_i_n5_v2[count]); + + this->command(0x24); // LUTBB + for (count = 0; count < 42; count++) + this->data(lut_bb_7_i_n5_v2[count]); +}; +void HOT WaveshareEPaper7P5InBV3::display() { + this->init_display_(); + uint32_t buf_len = this->get_buffer_length_(); + + this->command(0x10); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(0xFF); + } + + this->command(0x13); // Start Transmission + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(~this->buffer_[i]); + } + + this->command(0x12); // Display Refresh + delay(100); // NOLINT + this->wait_until_idle_(); + this->deep_sleep(); +} +int WaveshareEPaper7P5InBV3::get_width_internal() { return 800; } +int WaveshareEPaper7P5InBV3::get_height_internal() { return 480; } +void WaveshareEPaper7P5InBV3::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5in-bv3"); + 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); +} + void WaveshareEPaper7P5In::initialize() { // COMMAND POWER SETTING this->command(0x01); @@ -1269,6 +2397,23 @@ void WaveshareEPaper7P5In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } +bool WaveshareEPaper7P5InV2::wait_until_idle_() { + if (this->busy_pin_ == nullptr) { + return true; + } + + const uint32_t start = millis(); + while (this->busy_pin_->digital_read()) { + this->command(0x71); + if (millis() - start > this->idle_timeout_()) { + ESP_LOGE(TAG, "Timeout while displaying image!"); + return false; + } + App.feed_wdt(); + delay(10); + } + return true; +} void WaveshareEPaper7P5InV2::initialize() { // COMMAND POWER SETTING this->command(0x01); @@ -1276,10 +2421,21 @@ void WaveshareEPaper7P5InV2::initialize() { this->data(0x07); this->data(0x3f); this->data(0x3f); - this->command(0x04); + + // We don't want the display to be powered at this point delay(100); // NOLINT this->wait_until_idle_(); + + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x07); + + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + // COMMAND PANEL SETTING this->command(0x00); this->data(0x1F); @@ -1290,19 +2446,30 @@ void WaveshareEPaper7P5InV2::initialize() { this->data(0x20); this->data(0x01); this->data(0xE0); - // COMMAND ...? + + // COMMAND DUAL SPI MM_EN, DUSPI_EN this->command(0x15); this->data(0x00); - // COMMAND VCOM AND DATA INTERVAL SETTING - this->command(0x50); - this->data(0x10); - this->data(0x07); - // COMMAND TCON SETTING - this->command(0x60); - this->data(0x22); + + // COMMAND POWER DRIVER HAT DOWN + // This command will turn off booster, controller, source driver, gate driver, VCOM, and + // temperature sensor, but register data will be kept until VDD turned OFF or Deep Sleep Mode. + // Source/Gate/Border/VCOM will be released to floating. + this->command(0x02); } void HOT WaveshareEPaper7P5InV2::display() { uint32_t buf_len = this->get_buffer_length_(); + + // COMMAND POWER ON + ESP_LOGI(TAG, "Power on the display and hat"); + + // This command will turn on booster, controller, regulators, and temperature sensor will be + // activated for one-time sensing before enabling booster. When all voltages are ready, the + // BUSY_N signal will return to high. + this->command(0x04); + delay(200); // NOLINT + this->wait_until_idle_(); + // COMMAND DATA START TRANSMISSION NEW DATA this->command(0x13); delay(2); @@ -1310,14 +2477,23 @@ void HOT WaveshareEPaper7P5InV2::display() { this->data(~(this->buffer_[i])); } + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND DISPLAY REFRESH this->command(0x12); delay(100); // NOLINT this->wait_until_idle_(); + + ESP_LOGV(TAG, "Before command(0x02) (>> power off)"); + this->command(0x02); + this->wait_until_idle_(); + ESP_LOGV(TAG, "After command(0x02) (>> power off)"); } int WaveshareEPaper7P5InV2::get_width_internal() { return 800; } int WaveshareEPaper7P5InV2::get_height_internal() { return 480; } +uint32_t WaveshareEPaper7P5InV2::idle_timeout_() { return 10000; } void WaveshareEPaper7P5InV2::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 7.5inV2rev2"); @@ -1352,11 +2528,10 @@ void WaveshareEPaper7P5InV2alt::initialize() { this->command(0x01); // 1-0=11: internal power - this->data(0x17); - + this->data(0x07); this->data(0x17); // VGH&VGL this->data(0x3F); // VSH - this->data(0x3F); // VSL + this->data(0x26); // VSL this->data(0x11); // VSHR // VCOM DC Setting @@ -1370,10 +2545,6 @@ void WaveshareEPaper7P5InV2alt::initialize() { this->data(0x2F); this->data(0x17); - // OSC Setting - this->command(0x30); - this->data(0x06); // 2-0=100: N=4 ; 5-3=111: M=7 ; 3C=50Hz 3A=100HZ - // POWER ON this->command(0x04); @@ -1395,7 +2566,7 @@ void WaveshareEPaper7P5InV2alt::initialize() { // COMMAND VCOM AND DATA INTERVAL SETTING this->command(0x50); this->data(0x10); - this->data(0x07); + this->data(0x00); // COMMAND TCON SETTING this->command(0x60); this->data(0x22); @@ -1719,8 +2890,9 @@ void HOT WaveshareEPaper2P13InDKE::display() { } else { // set up partial update this->command(0x32); - for (uint8_t v : PART_UPDATE_LUT_TTGO_DKE) - this->data(v); + this->start_data_(); + this->write_array(PART_UPDATE_LUT_TTGO_DKE, sizeof(PART_UPDATE_LUT_TTGO_DKE)); + this->end_data_(); this->command(0x3F); this->data(0x22); @@ -1765,12 +2937,10 @@ void HOT WaveshareEPaper2P13InDKE::display() { this->wait_until_idle_(); // data must be sent again on partial update - delay(300); // NOLINT this->command(0x24); this->start_data_(); this->write_array(this->buffer_, this->get_buffer_length_()); this->end_data_(); - delay(300); // NOLINT } ESP_LOGI(TAG, "Completed e-paper update."); @@ -1782,6 +2952,7 @@ uint32_t WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; } void WaveshareEPaper2P13InDKE::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 2.13inDKE"); + 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_); @@ -1792,5 +2963,88 @@ void WaveshareEPaper2P13InDKE::set_full_update_every(uint32_t full_update_every) this->full_update_every_ = full_update_every; } +// ======================================================== +// 13.3in (K version) +// Datasheet/Specification/Reference: +// - https://files.waveshare.com/wiki/13.3inch-e-Paper-HAT-(K)/13.3-inch-e-Paper-(K)-user-manual.pdf +// - https://github.com/waveshareteam/e-Paper/tree/master/Arduino/epd13in3k +// ======================================================== + +// using default wait_until_idle_() function +void WaveshareEPaper13P3InK::initialize() { + this->wait_until_idle_(); + this->command(0x12); // SWRESET + this->wait_until_idle_(); + + this->command(0x0c); // set soft start + this->data(0xae); + this->data(0xc7); + this->data(0xc3); + this->data(0xc0); + this->data(0x80); + + this->command(0x01); // driver output control + this->data((get_height_internal() - 1) % 256); // Y + this->data((get_height_internal() - 1) / 256); // Y + this->data(0x00); + + this->command(0x11); // data entry mode + this->data(0x03); + + // SET WINDOWS + // XRAM_START_AND_END_POSITION + this->command(0x44); + this->data(0 & 0xFF); + this->data((0 >> 8) & 0x03); + this->data((get_width_internal() - 1) & 0xFF); + this->data(((get_width_internal() - 1) >> 8) & 0x03); + // YRAM_START_AND_END_POSITION + this->command(0x45); + this->data(0 & 0xFF); + this->data((0 >> 8) & 0x03); + this->data((get_height_internal() - 1) & 0xFF); + this->data(((get_height_internal() - 1) >> 8) & 0x03); + + this->command(0x3C); // Border setting + this->data(0x01); + + this->command(0x18); // use the internal temperature sensor + this->data(0x80); + + // SET CURSOR + // XRAM_ADDRESS + this->command(0x4E); + this->data(0 & 0xFF); + this->data((0 >> 8) & 0x03); + // YRAM_ADDRESS + this->command(0x4F); + this->data(0 & 0xFF); + this->data((0 >> 8) & 0x03); +} +void HOT WaveshareEPaper13P3InK::display() { + // do single full update + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // COMMAND DISPLAY REFRESH + this->command(0x22); + this->data(0xF7); + this->command(0x20); +} + +int WaveshareEPaper13P3InK::get_width_internal() { return 960; } +int WaveshareEPaper13P3InK::get_height_internal() { return 680; } +uint32_t WaveshareEPaper13P3InK::idle_timeout_() { return 10000; } +void WaveshareEPaper13P3InK::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 13.3inK"); + 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); +} + } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 1cb46bdb9d..7572982a20 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -7,10 +7,9 @@ namespace esphome { namespace waveshare_epaper { -class WaveshareEPaper : public PollingComponent, - public display::DisplayBuffer, - public spi::SPIDevice { +class WaveshareEPaperBase : public display::DisplayBuffer, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; @@ -20,6 +19,7 @@ class WaveshareEPaper : public PollingComponent, void command(uint8_t value); void data(uint8_t value); + void cmd_data(const uint8_t *data, size_t length); virtual void display() = 0; virtual void initialize() = 0; @@ -27,8 +27,6 @@ class WaveshareEPaper : public PollingComponent, void update() override; - void fill(Color color) override; - void setup() override { this->setup_pins_(); this->initialize(); @@ -36,11 +34,7 @@ class WaveshareEPaper : public PollingComponent, void on_safe_shutdown() override; - display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } - protected: - void draw_absolute_pixel_internal(int x, int y, Color color) override; - bool wait_until_idle_(); void setup_pins_(); @@ -50,13 +44,13 @@ class WaveshareEPaper : public PollingComponent, this->reset_pin_->digital_write(false); delay(reset_duration_); // NOLINT this->reset_pin_->digital_write(true); - delay(200); // NOLINT + delay(20); } } virtual int get_width_controller() { return this->get_width_internal(); }; - uint32_t get_buffer_length_(); + virtual uint32_t get_buffer_length_() = 0; // NOLINT(readability-identifier-naming) uint32_t reset_duration_{200}; void start_command_(); @@ -70,10 +64,33 @@ class WaveshareEPaper : public PollingComponent, virtual uint32_t idle_timeout_() { return 1000u; } // NOLINT(readability-identifier-naming) }; +class WaveshareEPaper : public WaveshareEPaperBase { + public: + void fill(Color color) override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + uint32_t get_buffer_length_() override; +}; + +class WaveshareEPaperBWR : public WaveshareEPaperBase { + public: + void fill(Color color) override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + uint32_t get_buffer_length_() override; +}; + enum WaveshareEPaperTypeAModel { WAVESHARE_EPAPER_1_54_IN = 0, WAVESHARE_EPAPER_1_54_IN_V2, WAVESHARE_EPAPER_2_13_IN, + WAVESHARE_EPAPER_2_13_IN_V2, WAVESHARE_EPAPER_2_9_IN, WAVESHARE_EPAPER_2_9_IN_V2, TTGO_EPAPER_2_13_IN, @@ -93,15 +110,27 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { void display() override; void deep_sleep() override { - if (this->model_ == WAVESHARE_EPAPER_2_9_IN_V2 || this->model_ == WAVESHARE_EPAPER_1_54_IN_V2) { - // COMMAND DEEP SLEEP MODE - this->command(0x10); - this->data(0x01); - } else { - // COMMAND DEEP SLEEP MODE - this->command(0x10); + switch (this->model_) { + // Models with specific deep sleep command and data + case WAVESHARE_EPAPER_1_54_IN: + case WAVESHARE_EPAPER_1_54_IN_V2: + case WAVESHARE_EPAPER_2_9_IN_V2: + case WAVESHARE_EPAPER_2_13_IN_V2: + // COMMAND DEEP SLEEP MODE + this->command(0x10); + this->data(0x01); + break; + // Other models default to simple deep sleep command + default: + // COMMAND DEEP SLEEP + this->command(0x10); + break; + } + if (this->model_ != WAVESHARE_EPAPER_2_13_IN_V2) { + // From panel specification: + // "After this command initiated, the chip will enter Deep Sleep Mode, BUSY pad will keep output high." + this->wait_until_idle_(); } - this->wait_until_idle_(); } void set_full_update_every(uint32_t full_update_every); @@ -109,6 +138,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { protected: void write_lut_(const uint8_t *lut, uint8_t size); + void init_display_(); + int get_width_internal() override; int get_height_internal() override; @@ -119,15 +150,20 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { uint32_t at_update_{0}; WaveshareEPaperTypeAModel model_; uint32_t idle_timeout_() override; + + bool deep_sleep_between_updates_{false}; }; enum WaveshareEPaperTypeBModel { WAVESHARE_EPAPER_2_7_IN = 0, + WAVESHARE_EPAPER_2_7_IN_B, + WAVESHARE_EPAPER_2_7_IN_B_V2, WAVESHARE_EPAPER_4_2_IN, WAVESHARE_EPAPER_4_2_IN_B_V2, WAVESHARE_EPAPER_7_5_IN, WAVESHARE_EPAPER_7_5_INV2, WAVESHARE_EPAPER_7_5_IN_B_V2, + WAVESHARE_EPAPER_13_3_IN_K, }; class WaveshareEPaper2P7In : public WaveshareEPaper { @@ -146,11 +182,53 @@ class WaveshareEPaper2P7In : public WaveshareEPaper { protected: int get_width_internal() override; - int get_height_internal() override; }; -class GDEY029T94 : public WaveshareEPaper { +class WaveshareEPaper2P7InB : public WaveshareEPaperBWR { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND VCOM_AND_DATA_INTERVAL_SETTING + this->command(0x50); + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); // deep sleep + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + int get_height_internal() override; +}; + +class WaveshareEPaper2P7InBV2 : public WaveshareEPaperBWR { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x10); + this->data(0x01); + } + + protected: + int get_width_internal() override; + int get_height_internal() override; +}; + +class GDEW029T5 : public WaveshareEPaper { public: void initialize() override; @@ -170,6 +248,62 @@ class GDEY029T94 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P7InV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { ; } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + +class GDEW0154M09 : public WaveshareEPaper { + public: + void initialize() override; + void display() override; + void dump_config() override; + void deep_sleep() override; + + protected: + int get_width_internal() override; + int get_height_internal() override; + + private: + static const uint8_t CMD_DTM1_DATA_START_TRANS = 0x10; + static const uint8_t CMD_DTM2_DATA_START_TRANS2 = 0x13; + static const uint8_t CMD_DISPLAY_REFRESH = 0x12; + static const uint8_t CMD_AUTO_SEQ = 0x17; + static const uint8_t DATA_AUTO_PON_DSR_POF_DSLP = 0xA7; + static const uint8_t CMD_PSR_PANEL_SETTING = 0x00; + static const uint8_t CMD_UNDOCUMENTED_0x4D = 0x4D; // NOLINT + static const uint8_t CMD_UNDOCUMENTED_0xAA = 0xaa; // NOLINT + static const uint8_t CMD_UNDOCUMENTED_0xE9 = 0xe9; // NOLINT + static const uint8_t CMD_UNDOCUMENTED_0xB6 = 0xb6; // NOLINT + static const uint8_t CMD_UNDOCUMENTED_0xF3 = 0xf3; // NOLINT + static const uint8_t CMD_TRES_RESOLUTION_SETTING = 0x61; + static const uint8_t CMD_TCON_TCONSETTING = 0x60; + static const uint8_t CMD_CDI_VCOM_DATA_INTERVAL = 0x50; + static const uint8_t CMD_POF_POWER_OFF = 0x02; + static const uint8_t CMD_DSLP_DEEP_SLEEP = 0x07; + static const uint8_t DATA_DSLP_DEEP_SLEEP = 0xA5; + static const uint8_t CMD_PWS_POWER_SAVING = 0xe3; + static const uint8_t CMD_PON_POWER_ON = 0x04; + static const uint8_t CMD_PTL_PARTIAL_WINDOW = 0x90; + + uint8_t *lastbuff_ = nullptr; + void reset_(); + void clear_(); + void write_init_list_(const uint8_t *list); + void init_internal_(); +}; + class WaveshareEPaper2P9InB : public WaveshareEPaper { public: void initialize() override; @@ -190,6 +324,80 @@ class WaveshareEPaper2P9InB : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P9InBV3 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + +class WaveshareEPaper2P9InV2R2 : public WaveshareEPaper { + public: + WaveshareEPaper2P9InV2R2(); + + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override; + + void set_full_update_every(uint32_t full_update_every); + + protected: + void write_lut_(const uint8_t *lut, uint8_t size); + + int get_width_internal() override; + + int get_height_internal() override; + + int get_width_controller() override; + + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + + private: + void reset_(); +}; + +class WaveshareEPaper2P9InDKE : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x10); + this->data(0x01); + } + + void set_full_update_every(uint32_t full_update_every); + + protected: + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper4P2In : public WaveshareEPaper { public: void initialize() override; @@ -373,6 +581,44 @@ class WaveshareEPaper7P5InBV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P5InBV3 : public WaveshareEPaper { + public: + bool wait_until_idle_(); + + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + this->command(0x02); // Power off + this->wait_until_idle_(); + this->command(0x07); // Deep sleep + this->data(0xA5); + } + + void clear_screen(); + + protected: + int get_width_internal() override; + + int get_height_internal() override; + + void reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(true); + delay(200); // NOLINT + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + delay(200); // NOLINT + } + }; + + void init_display_(); +}; + class WaveshareEPaper7P5InBC : public WaveshareEPaper { public: void initialize() override; @@ -398,6 +644,8 @@ class WaveshareEPaper7P5InBC : public WaveshareEPaper { class WaveshareEPaper7P5InV2 : public WaveshareEPaper { public: + bool wait_until_idle_(); + void initialize() override; void display() override; @@ -417,6 +665,8 @@ class WaveshareEPaper7P5InV2 : public WaveshareEPaper { int get_width_internal() override; int get_height_internal() override; + + uint32_t idle_timeout_() override; }; class WaveshareEPaper7P5InV2alt : public WaveshareEPaper7P5InV2 { @@ -486,5 +736,62 @@ class WaveshareEPaper2P13InDKE : public WaveshareEPaper { uint32_t at_update_{0}; }; +class WaveshareEPaper2P13InV3 : public WaveshareEPaper { + public: + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER DOWN + this->command(0x10); + this->data(0x01); + // cannot wait until idle here, the device no longer responds + } + + void set_full_update_every(uint32_t full_update_every); + + void setup() override; + void initialize() override; + + protected: + int get_width_internal() override; + int get_height_internal() override; + uint32_t idle_timeout_() override; + + void write_buffer_(uint8_t cmd, int top, int bottom); + void set_window_(int t, int b); + void send_reset_(); + void partial_update_(); + void full_update_(); + + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + bool is_busy_{false}; + void write_lut_(const uint8_t *lut); +}; + +class WaveshareEPaper13P3InK : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x10); + this->data(0x01); + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; + + uint32_t idle_timeout_() override; +}; + } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index d8343c6c39..d846a3418b 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -1,42 +1,67 @@ +from __future__ import annotations + +import gzip + import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +import esphome.config_validation as cv from esphome.const import ( + CONF_AUTH, CONF_CSS_INCLUDE, CONF_CSS_URL, + CONF_ENABLE_PRIVATE_NETWORK_ACCESS, CONF_ID, + CONF_INCLUDE_INTERNAL, CONF_JS_INCLUDE, CONF_JS_URL, - CONF_PORT, - CONF_AUTH, - CONF_USERNAME, - CONF_PASSWORD, - CONF_INCLUDE_INTERNAL, - CONF_OTA, - CONF_VERSION, CONF_LOCAL, + CONF_LOG, + CONF_NAME, + CONF_OTA, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERSION, + CONF_WEB_SERVER, + CONF_WEB_SERVER_ID, + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority +import esphome.final_validate as fv AUTO_LOAD = ["json", "web_server_base"] +CONF_SORTING_GROUP_ID = "sorting_group_id" +CONF_SORTING_GROUPS = "sorting_groups" +CONF_SORTING_WEIGHT = "sorting_weight" + web_server_ns = cg.esphome_ns.namespace("web_server") WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) +sorting_groups = {} + def default_url(config): config = config.copy() if config[CONF_VERSION] == 1: - if not (CONF_CSS_URL in config): + if CONF_CSS_URL not in config: config[CONF_CSS_URL] = "https://esphome.io/_static/webserver-v1.min.css" - if not (CONF_JS_URL in config): + if CONF_JS_URL not in config: config[CONF_JS_URL] = "https://esphome.io/_static/webserver-v1.min.js" if config[CONF_VERSION] == 2: - if not (CONF_CSS_URL in config): + if CONF_CSS_URL not in config: config[CONF_CSS_URL] = "" - if not (CONF_JS_URL in config): + if CONF_JS_URL not in config: config[CONF_JS_URL] = "https://oi.esphome.io/v2/www.js" + if config[CONF_VERSION] == 3: + if CONF_CSS_URL not in config: + config[CONF_CSS_URL] = "" + if CONF_JS_URL not in config: + config[CONF_JS_URL] = "https://oi.esphome.io/v3/www.js" return config @@ -46,16 +71,95 @@ def validate_local(config): return config +def validate_ota(config): + if CORE.using_esp_idf and config[CONF_OTA]: + raise cv.Invalid("Enabling 'ota' is not supported for IDF framework yet") + return config + + +def validate_sorting_groups(config): + if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3: + raise cv.Invalid( + f"'{CONF_SORTING_GROUPS}' is only supported in 'web_server' version 3" + ) + return config + + +def _validate_no_sorting_component( + sorting_component: str, + webserver_version: int, + config: dict, + path: list[str] | None = None, +) -> None: + if path is None: + path = [] + if CONF_WEB_SERVER in config and sorting_component in config[CONF_WEB_SERVER]: + raise cv.FinalExternalInvalid( + f"{sorting_component} on entities is not supported in web_server version {webserver_version}", + path=path + [sorting_component], + ) + for p, value in config.items(): + if isinstance(value, dict): + _validate_no_sorting_component( + sorting_component, webserver_version, value, path + [p] + ) + elif isinstance(value, list): + for i, item in enumerate(value): + if isinstance(item, dict): + _validate_no_sorting_component( + sorting_component, webserver_version, item, path + [p, i] + ) + + +def _final_validate_sorting(config): + if (webserver_version := config.get(CONF_VERSION)) != 3: + _validate_no_sorting_component( + CONF_SORTING_WEIGHT, webserver_version, fv.full_config.get() + ) + _validate_no_sorting_component( + CONF_SORTING_GROUP_ID, webserver_version, fv.full_config.get() + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate_sorting + +sorting_group = { + cv.Required(CONF_ID): cv.declare_id(cg.int_), + cv.Required(CONF_NAME): cv.string, + cv.Optional(CONF_SORTING_WEIGHT): cv.float_, +} + +WEBSERVER_SORTING_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WEB_SERVER): cv.Schema( + { + cv.OnlyWith(CONF_WEB_SERVER_ID, "web_server"): cv.use_id(WebServer), + cv.Optional(CONF_SORTING_WEIGHT): cv.All( + cv.requires_component("web_server"), + cv.float_, + ), + cv.Optional(CONF_SORTING_GROUP_ID): cv.All( + cv.requires_component("web_server"), + cv.use_id(cg.int_), + ), + } + ) + } +) + + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WebServer), cv.Optional(CONF_PORT, default=80): cv.port, - cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2), + cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, 3, int=True), cv.Optional(CONF_CSS_URL): cv.string, cv.Optional(CONF_CSS_INCLUDE): cv.file_, cv.Optional(CONF_JS_URL): cv.string, cv.Optional(CONF_JS_INCLUDE): cv.file_, + cv.Optional(CONF_ENABLE_PRIVATE_NETWORK_ACCESS, default=True): cv.boolean, cv.Optional(CONF_AUTH): cv.Schema( { cv.Required(CONF_USERNAME): cv.All( @@ -70,17 +174,87 @@ CONFIG_SCHEMA = cv.All( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.Optional(CONF_OTA, default=True): cv.boolean, + cv.SplitDefault( + CONF_OTA, + esp8266=True, + esp32_arduino=True, + esp32_idf=False, + bk72xx=True, + rtl87xx=True, + ): cv.boolean, + cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, + cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_arduino, - cv.only_on(["esp32", "esp8266"]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), default_url, validate_local, + validate_ota, + validate_sorting_groups, ) +def add_sorting_groups(web_server_var, config): + for group in config: + sorting_groups[group[CONF_ID]] = group[CONF_NAME] + group_sorting_weight = group.get(CONF_SORTING_WEIGHT, 50) + cg.add( + web_server_var.add_sorting_group( + hash(group[CONF_ID]), group[CONF_NAME], group_sorting_weight + ) + ) + + +async def add_entity_config(entity, config): + web_server = await cg.get_variable(config[CONF_WEB_SERVER_ID]) + sorting_weight = config.get(CONF_SORTING_WEIGHT, 50) + sorting_group_hash = hash(config.get(CONF_SORTING_GROUP_ID)) + + cg.add( + web_server.add_entity_config( + entity, + sorting_weight, + sorting_group_hash, + ) + ) + + +def build_index_html(config) -> str: + html = "" + css_include = config.get(CONF_CSS_INCLUDE) + js_include = config.get(CONF_JS_INCLUDE) + if css_include: + html += "" + if config[CONF_CSS_URL]: + html += f'' + html += "" + if js_include: + html += "" + html += "" + if config[CONF_JS_URL]: + html += f'' + html += "" + return html + + +def add_resource_as_progmem( + resource_name: str, content: str, compress: bool = True +) -> None: + """Add a resource to progmem.""" + content_encoded = content.encode("utf-8") + if compress: + content_encoded = gzip.compress(content_encoded) + content_encoded_size = len(content_encoded) + bytes_as_int = ", ".join(str(x) for x in content_encoded) + uint8_t = f"const uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" + size_t = ( + f"const size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" + ) + cg.add_global(cg.RawExpression(uint8_t)) + cg.add_global(cg.RawExpression(size_t)) + + @coroutine_with_priority(40.0) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) @@ -88,28 +262,38 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) - cg.add_define("USE_WEBSERVER") + version = config[CONF_VERSION] cg.add(paren.set_port(config[CONF_PORT])) cg.add_define("USE_WEBSERVER") cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT]) - cg.add_define("USE_WEBSERVER_VERSION", config[CONF_VERSION]) - cg.add(var.set_css_url(config[CONF_CSS_URL])) - cg.add(var.set_js_url(config[CONF_JS_URL])) + cg.add_define("USE_WEBSERVER_VERSION", version) + if version >= 2: + # Don't compress the index HTML as the data sizes are almost the same. + add_resource_as_progmem("INDEX_HTML", build_index_html(config), compress=False) + else: + cg.add(var.set_css_url(config[CONF_CSS_URL])) + cg.add(var.set_js_url(config[CONF_JS_URL])) cg.add(var.set_allow_ota(config[CONF_OTA])) + cg.add(var.set_expose_log(config[CONF_LOG])) + if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: + cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") if CONF_AUTH in config: cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) if CONF_CSS_INCLUDE in config: cg.add_define("USE_WEBSERVER_CSS_INCLUDE") path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) - with open(file=path, encoding="utf-8") as myfile: - cg.add(var.set_css_include(myfile.read())) + with open(file=path, encoding="utf-8") as css_file: + add_resource_as_progmem("CSS_INCLUDE", css_file.read()) if CONF_JS_INCLUDE in config: cg.add_define("USE_WEBSERVER_JS_INCLUDE") path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) - with open(file=path, encoding="utf-8") as myfile: - cg.add(var.set_js_include(myfile.read())) + with open(file=path, encoding="utf-8") as js_file: + add_resource_as_progmem("JS_INCLUDE", js_file.read()) cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) if CONF_LOCAL in config and config[CONF_LOCAL]: cg.add_define("USE_WEBSERVER_LOCAL") + + if (sorting_group_config := config.get(CONF_SORTING_GROUPS)) is not None: + add_sorting_groups(var, sorting_group_config) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 6f833a5c83..a02f84c34b 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -1,6 +1,5 @@ -#ifdef USE_ARDUINO - #include "list_entities.h" +#ifdef USE_WEBSERVER #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -14,6 +13,8 @@ ListEntitiesIterator::ListEntitiesIterator(WebServer *web_server) : web_server_( #ifdef USE_BINARY_SENSOR bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send( this->web_server_->binary_sensor_json(binary_sensor, binary_sensor->state, DETAIL_ALL).c_str(), "state"); return true; @@ -21,30 +22,40 @@ bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_ #endif #ifdef USE_COVER bool ListEntitiesIterator::on_cover(cover::Cover *cover) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->cover_json(cover, DETAIL_ALL).c_str(), "state"); return true; } #endif #ifdef USE_FAN bool ListEntitiesIterator::on_fan(fan::Fan *fan) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->fan_json(fan, DETAIL_ALL).c_str(), "state"); return true; } #endif #ifdef USE_LIGHT bool ListEntitiesIterator::on_light(light::LightState *light) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->light_json(light, DETAIL_ALL).c_str(), "state"); return true; } #endif #ifdef USE_SENSOR bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->sensor_json(sensor, sensor->state, DETAIL_ALL).c_str(), "state"); return true; } #endif #ifdef USE_SWITCH bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->switch_json(a_switch, a_switch->state, DETAIL_ALL).c_str(), "state"); return true; @@ -52,12 +63,16 @@ bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { #endif #ifdef USE_BUTTON bool ListEntitiesIterator::on_button(button::Button *button) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->button_json(button, DETAIL_ALL).c_str(), "state"); return true; } #endif #ifdef USE_TEXT_SENSOR bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send( this->web_server_->text_sensor_json(text_sensor, text_sensor->state, DETAIL_ALL).c_str(), "state"); return true; @@ -65,13 +80,26 @@ bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) #endif #ifdef USE_LOCK bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->lock_json(a_lock, a_lock->state, DETAIL_ALL).c_str(), "state"); return true; } #endif +#ifdef USE_VALVE +bool ListEntitiesIterator::on_valve(valve::Valve *valve) { + if (this->web_server_->events_.count() == 0) + return true; + this->web_server_->events_.send(this->web_server_->valve_json(valve, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + #ifdef USE_CLIMATE bool ListEntitiesIterator::on_climate(climate::Climate *climate) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->climate_json(climate, DETAIL_ALL).c_str(), "state"); return true; } @@ -79,19 +107,86 @@ bool ListEntitiesIterator::on_climate(climate::Climate *climate) { #ifdef USE_NUMBER bool ListEntitiesIterator::on_number(number::Number *number) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->number_json(number, number->state, DETAIL_ALL).c_str(), "state"); return true; } #endif +#ifdef USE_DATETIME_DATE +bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { + if (this->web_server_->events_.count() == 0) + return true; + this->web_server_->events_.send(this->web_server_->date_json(date, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_DATETIME_TIME +bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { + this->web_server_->events_.send(this->web_server_->time_json(time, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_DATETIME_DATETIME +bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { + if (this->web_server_->events_.count() == 0) + return true; + this->web_server_->events_.send(this->web_server_->datetime_json(datetime, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_TEXT +bool ListEntitiesIterator::on_text(text::Text *text) { + if (this->web_server_->events_.count() == 0) + return true; + this->web_server_->events_.send(this->web_server_->text_json(text, text->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + #ifdef USE_SELECT bool ListEntitiesIterator::on_select(select::Select *select) { + if (this->web_server_->events_.count() == 0) + return true; this->web_server_->events_.send(this->web_server_->select_json(select, select->state, DETAIL_ALL).c_str(), "state"); return true; } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + if (this->web_server_->events_.count() == 0) + return true; + this->web_server_->events_.send( + this->web_server_->alarm_control_panel_json(a_alarm_control_panel, a_alarm_control_panel->get_state(), DETAIL_ALL) + .c_str(), + "state"); + return true; +} +#endif + +#ifdef USE_EVENT +bool ListEntitiesIterator::on_event(event::Event *event) { + // Null event type, since we are just iterating over entities + const std::string null_event_type = ""; + this->web_server_->events_.send(this->web_server_->event_json(event, null_event_type, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_UPDATE +bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { + if (this->web_server_->events_.count() == 0) + return true; + this->web_server_->events_.send(this->web_server_->update_json(update, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 85868caff8..53e5bc3355 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -1,10 +1,9 @@ #pragma once -#ifdef USE_ARDUINO - +#include "esphome/core/defines.h" +#ifdef USE_WEBSERVER #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -#include "esphome/core/defines.h" namespace esphome { namespace web_server { @@ -43,12 +42,36 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_NUMBER bool on_number(number::Number *number) override; #endif +#ifdef USE_DATETIME_DATE + bool on_date(datetime::DateEntity *date) override; +#endif +#ifdef USE_DATETIME_TIME + bool on_time(datetime::TimeEntity *time) override; +#endif +#ifdef USE_DATETIME_DATETIME + bool on_datetime(datetime::DateTimeEntity *datetime) override; +#endif +#ifdef USE_TEXT + bool on_text(text::Text *text) override; +#endif #ifdef USE_SELECT bool on_select(select::Select *select) override; #endif #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; #endif +#ifdef USE_VALVE + bool on_valve(valve::Valve *valve) override; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; +#endif +#ifdef USE_EVENT + bool on_event(event::Event *event) override; +#endif +#ifdef USE_UPDATE + bool on_update(update::UpdateEntity *update) override; +#endif protected: WebServer *web_server_; @@ -56,5 +79,4 @@ class ListEntitiesIterator : public ComponentIterator { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/web_server/server_index.h b/esphome/components/web_server/server_index.h deleted file mode 100644 index 8eaaaf4581..0000000000 --- a/esphome/components/web_server/server_index.h +++ /dev/null @@ -1,590 +0,0 @@ -#pragma once -// Generated from https://github.com/esphome/esphome-webserver -#include "esphome/core/hal.h" -namespace esphome { - -namespace web_server { - -const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xbd, 0x7d, 0xd9, 0x76, 0xdb, 0xc8, 0x92, 0xe0, 0xf3, - 0x9c, 0xd3, 0x7f, 0x30, 0x2f, 0x30, 0x4a, 0x6d, 0x03, 0x57, 0x20, 0x44, 0x52, 0x96, 0xed, 0x02, 0x05, 0xf2, 0xca, - 0x4b, 0x5d, 0xbb, 0xca, 0x5b, 0x59, 0xb2, 0x6b, 0x51, 0xb1, 0x2c, 0x88, 0x4c, 0x8a, 0x28, 0x83, 0x00, 0x0b, 0x48, - 0x6a, 0x29, 0x0a, 0x7d, 0xfa, 0xa9, 0x9f, 0xe6, 0x9c, 0x59, 0x1f, 0xfa, 0x65, 0x4e, 0xf7, 0xc3, 0x7c, 0xc4, 0x3c, - 0xf7, 0xa7, 0xdc, 0x1f, 0x98, 0xfe, 0x84, 0x89, 0x88, 0x5c, 0x90, 0x00, 0xa9, 0xc5, 0xd5, 0xd5, 0x7d, 0xbc, 0x08, - 0xc8, 0x35, 0x22, 0x32, 0x32, 0xb6, 0x8c, 0x84, 0x76, 0xef, 0x8c, 0xb3, 0x11, 0xbf, 0x98, 0x33, 0x6b, 0xca, 0x67, - 0x49, 0x7f, 0x57, 0xfe, 0xcf, 0xa2, 0x71, 0x7f, 0x37, 0x89, 0xd3, 0x4f, 0x56, 0xce, 0x92, 0x30, 0x1e, 0x65, 0xa9, - 0x35, 0xcd, 0xd9, 0x24, 0x1c, 0x47, 0x3c, 0x0a, 0xe2, 0x59, 0x74, 0xc2, 0xac, 0xad, 0xfe, 0xee, 0x8c, 0xf1, 0xc8, - 0x1a, 0x4d, 0xa3, 0xbc, 0x60, 0x3c, 0x7c, 0x7f, 0xf0, 0x55, 0xeb, 0x51, 0x7f, 0xb7, 0x18, 0xe5, 0xf1, 0x9c, 0x5b, - 0x38, 0x64, 0x38, 0xcb, 0xc6, 0x8b, 0x84, 0xf5, 0x4f, 0xa3, 0xdc, 0x7a, 0xc6, 0xc2, 0x37, 0xc7, 0xbf, 0xb0, 0x11, - 0xf7, 0xc7, 0x6c, 0x12, 0xa7, 0xec, 0x6d, 0x9e, 0xcd, 0x59, 0xce, 0x2f, 0xbc, 0xfd, 0xf5, 0x15, 0x31, 0x2b, 0xbc, - 0x4f, 0xba, 0xea, 0x84, 0xf1, 0x37, 0x67, 0xa9, 0xea, 0xf3, 0x94, 0x89, 0x49, 0xb2, 0xbc, 0xf0, 0x8a, 0x2b, 0xda, - 0xec, 0x5f, 0xcc, 0x8e, 0xb3, 0xa4, 0xf0, 0x9e, 0xe8, 0xfa, 0x79, 0x9e, 0xf1, 0x0c, 0xc1, 0xf2, 0xa7, 0x51, 0x61, - 0xb4, 0xf4, 0xde, 0xac, 0x69, 0x32, 0x97, 0x95, 0x2f, 0x8a, 0x67, 0xe9, 0x62, 0xc6, 0xf2, 0xe8, 0x38, 0x61, 0x5e, - 0xcc, 0x42, 0x87, 0x79, 0xdc, 0x8b, 0xdd, 0xb0, 0xcf, 0xad, 0x38, 0xb5, 0xd8, 0xe0, 0x19, 0xa3, 0x92, 0x25, 0xd3, - 0xad, 0x82, 0x3b, 0x6d, 0x0f, 0xc8, 0x35, 0x89, 0x4f, 0x16, 0xfa, 0xfd, 0x2c, 0x8f, 0xb9, 0x7a, 0x3e, 0x8d, 0x92, - 0x05, 0x0b, 0xe2, 0xd2, 0x0d, 0xd8, 0x21, 0x1f, 0x86, 0xb1, 0xf7, 0x84, 0x06, 0x85, 0x21, 0x97, 0x93, 0x2c, 0x77, - 0x90, 0x56, 0x31, 0x8e, 0xcd, 0x2f, 0x2f, 0x1d, 0x1e, 0x2e, 0x4b, 0xd7, 0x7d, 0xc2, 0xfc, 0x51, 0x94, 0x24, 0x0e, - 0x4e, 0x7c, 0xf7, 0x6e, 0x8c, 0x33, 0xc6, 0x1e, 0x3f, 0x8c, 0x87, 0x6e, 0x2f, 0x9e, 0x38, 0x05, 0x73, 0xab, 0x7e, - 0xd9, 0xc4, 0x2a, 0x98, 0xc3, 0x5d, 0xf7, 0xcd, 0xd5, 0x7d, 0x72, 0xc6, 0x17, 0x39, 0xc0, 0x5e, 0x7a, 0x6f, 0xd4, - 0xcc, 0xfb, 0x58, 0xff, 0x89, 0x3a, 0xf6, 0x00, 0xf6, 0x82, 0x5b, 0x1f, 0xc2, 0xb3, 0x38, 0x1d, 0x67, 0x67, 0xfe, - 0xfe, 0x34, 0x82, 0x1f, 0xef, 0xb2, 0x8c, 0xdf, 0xbd, 0xeb, 0x9c, 0x66, 0xf1, 0xd8, 0x6a, 0x87, 0xa1, 0x59, 0x79, - 0xf1, 0x64, 0x7f, 0xff, 0xf2, 0xb2, 0x51, 0xe0, 0xa7, 0x11, 0x8f, 0x4f, 0x99, 0xe8, 0x0c, 0x00, 0xd8, 0xf0, 0x73, - 0xce, 0xd9, 0x78, 0x9f, 0x5f, 0x24, 0x50, 0xca, 0x18, 0x2f, 0x6c, 0xc0, 0xf1, 0x69, 0x36, 0x02, 0xb2, 0xa5, 0x06, - 0xe1, 0xa1, 0x69, 0xce, 0xe6, 0x49, 0x34, 0x62, 0x58, 0x0f, 0x23, 0x55, 0x3d, 0xaa, 0x46, 0xde, 0x57, 0xa1, 0x58, - 0x5e, 0xc7, 0xf5, 0x72, 0x16, 0xa6, 0xec, 0xcc, 0x7a, 0x15, 0xcd, 0x7b, 0xa3, 0x24, 0x2a, 0x0a, 0x2b, 0x63, 0x4b, - 0x42, 0x21, 0x5f, 0x8c, 0x80, 0x41, 0x08, 0xc1, 0x25, 0x90, 0x89, 0x4f, 0xe3, 0xc2, 0xff, 0xb8, 0x31, 0x2a, 0x8a, - 0x77, 0xac, 0x58, 0x24, 0x7c, 0x23, 0x84, 0xb5, 0xe0, 0x77, 0xc2, 0xf0, 0x2b, 0x97, 0x4f, 0xf3, 0xec, 0xcc, 0x7a, - 0x96, 0xe7, 0xd0, 0xdc, 0x86, 0x29, 0x45, 0x03, 0x2b, 0x2e, 0xac, 0x34, 0xe3, 0x96, 0x1e, 0x0c, 0x17, 0xd0, 0xb7, - 0xde, 0x17, 0xcc, 0x3a, 0x5a, 0xa4, 0x45, 0x34, 0x61, 0xd0, 0xf4, 0xc8, 0xca, 0x72, 0xeb, 0x08, 0x06, 0x3d, 0x82, - 0x25, 0x2b, 0x38, 0xec, 0x1a, 0xdf, 0x76, 0x7b, 0x34, 0x17, 0x14, 0x1e, 0xb0, 0x73, 0x1e, 0xb2, 0x12, 0x18, 0xd3, - 0x2a, 0x34, 0x1a, 0x8e, 0xbb, 0x4c, 0xa0, 0x80, 0x85, 0x39, 0x43, 0x96, 0x75, 0xcc, 0xc6, 0x7a, 0x71, 0x3e, 0xdc, - 0xbd, 0xab, 0x69, 0x0d, 0x34, 0x71, 0xa0, 0x6d, 0xd1, 0x68, 0xeb, 0x09, 0xc4, 0x6b, 0x24, 0x72, 0x3d, 0xe6, 0x4b, - 0xf2, 0xed, 0x5f, 0xa4, 0xa3, 0xfa, 0xd8, 0x50, 0x59, 0xf2, 0x6c, 0x9f, 0xe7, 0x71, 0x7a, 0x02, 0x40, 0xc8, 0x99, - 0xcc, 0x26, 0x65, 0x29, 0x16, 0xff, 0x2d, 0x0b, 0x59, 0xd8, 0xc7, 0xd1, 0x33, 0xe6, 0xd8, 0x05, 0xf5, 0xb0, 0xc3, - 0x10, 0x49, 0x0f, 0x0c, 0xc6, 0x06, 0x2c, 0x60, 0x9b, 0xb6, 0xed, 0x7d, 0xe5, 0x7a, 0x17, 0xc8, 0x41, 0xbe, 0xef, - 0x13, 0xfb, 0x8a, 0xce, 0x71, 0xd8, 0x41, 0xa0, 0xfd, 0x84, 0xa5, 0x27, 0x7c, 0x3a, 0x60, 0x87, 0xed, 0x61, 0xc0, - 0x01, 0xaa, 0xf1, 0x62, 0xc4, 0x1c, 0xe4, 0x47, 0x2f, 0xc7, 0xed, 0xb3, 0xe9, 0xc0, 0x14, 0xb8, 0x30, 0x77, 0x08, - 0xc7, 0xda, 0xd2, 0xb8, 0x8a, 0x45, 0x15, 0x60, 0xc8, 0xe7, 0x36, 0xec, 0xb0, 0x63, 0x96, 0x1b, 0x70, 0xe8, 0x66, - 0xbd, 0xda, 0x0a, 0x2e, 0x60, 0x85, 0xa0, 0x9f, 0x35, 0x59, 0xa4, 0x23, 0x1e, 0x83, 0xe0, 0xb2, 0x37, 0x01, 0x5c, - 0xb1, 0x72, 0x7a, 0xe1, 0x6c, 0xb7, 0x74, 0x9d, 0xd8, 0xdd, 0x64, 0x87, 0xf9, 0x66, 0x67, 0xe8, 0x21, 0x94, 0x9a, - 0xf8, 0x12, 0xf1, 0x18, 0x10, 0x2c, 0xbd, 0xf7, 0x4c, 0x6f, 0xcf, 0x0f, 0x03, 0xe6, 0xaf, 0xf2, 0x71, 0xc8, 0xfd, - 0x59, 0x34, 0x47, 0x6c, 0x18, 0xf1, 0x40, 0x94, 0x8e, 0x10, 0xba, 0xda, 0xba, 0x20, 0xc5, 0xfc, 0x8a, 0x05, 0x5c, - 0x20, 0x08, 0xec, 0xd9, 0x67, 0xd1, 0x68, 0x0a, 0x5b, 0xbc, 0x22, 0xdc, 0x58, 0x6d, 0x87, 0x51, 0xce, 0x22, 0xce, - 0x9e, 0x25, 0x0c, 0xdf, 0x70, 0x05, 0xa0, 0xa7, 0x0d, 0xbc, 0xae, 0xf6, 0x5d, 0x12, 0xf3, 0xd7, 0x19, 0xcc, 0xd3, - 0x13, 0x4c, 0x02, 0x5c, 0x9c, 0xc3, 0x26, 0x47, 0x16, 0xd9, 0xe3, 0xb0, 0x5a, 0xc7, 0x0b, 0x0e, 0xeb, 0x96, 0x62, - 0x0b, 0x1b, 0xa8, 0xed, 0xc5, 0x3e, 0x07, 0x22, 0x3e, 0xc9, 0x52, 0x0e, 0xc3, 0x01, 0xbc, 0x9a, 0x83, 0xfc, 0x68, - 0x3e, 0x67, 0xe9, 0xf8, 0xc9, 0x34, 0x4e, 0xc6, 0x40, 0x8d, 0x12, 0xf0, 0x4d, 0x59, 0x08, 0x78, 0x02, 0x32, 0xc1, - 0xf5, 0x18, 0xd1, 0xf2, 0x21, 0x23, 0xf3, 0xd0, 0xb6, 0x7b, 0x28, 0x81, 0x24, 0x16, 0x28, 0x83, 0x68, 0xe1, 0xde, - 0x81, 0xe8, 0x2f, 0x5c, 0xbe, 0x19, 0xc6, 0x7a, 0x19, 0x25, 0x81, 0xdf, 0xa2, 0xa4, 0x01, 0xfa, 0x33, 0x90, 0x81, - 0x3d, 0x14, 0x5c, 0xdf, 0x49, 0xa9, 0x93, 0x30, 0x85, 0x21, 0x10, 0x60, 0x84, 0x12, 0x44, 0xd2, 0xe0, 0x6d, 0x96, - 0x5c, 0x4c, 0xe2, 0x24, 0xd9, 0x5f, 0xcc, 0xe7, 0x59, 0xce, 0xbd, 0xaf, 0xc3, 0x25, 0xcf, 0x2a, 0x5c, 0x69, 0x93, - 0x17, 0x67, 0x31, 0x47, 0x82, 0xba, 0xcb, 0x51, 0x04, 0x4b, 0xfd, 0x38, 0xcb, 0x12, 0x16, 0xa5, 0x80, 0x06, 0x1b, - 0xd8, 0x76, 0x90, 0x2e, 0x92, 0xa4, 0x77, 0x0c, 0xc3, 0x7e, 0xea, 0x51, 0xb5, 0x90, 0xf8, 0x01, 0x3d, 0xef, 0xe5, - 0x79, 0x74, 0x01, 0x0d, 0xb1, 0x0d, 0xf0, 0x22, 0xac, 0xd6, 0xd7, 0xfb, 0x6f, 0x5e, 0xfb, 0x82, 0xf1, 0xe3, 0xc9, - 0x05, 0x00, 0x5a, 0x56, 0x52, 0x73, 0x92, 0x67, 0xb3, 0xc6, 0xd4, 0x48, 0x87, 0x38, 0x64, 0xbd, 0x2b, 0x40, 0x88, - 0x69, 0x64, 0x58, 0x25, 0x66, 0x42, 0xf0, 0x9a, 0xf8, 0x59, 0x56, 0xe2, 0x1e, 0x18, 0xe0, 0x43, 0x20, 0x8a, 0x61, - 0xca, 0xeb, 0xa1, 0xe5, 0xf9, 0xc5, 0x32, 0x0e, 0x09, 0xce, 0x39, 0xea, 0x5f, 0x84, 0x71, 0x14, 0xc1, 0xec, 0x4b, - 0x31, 0x60, 0xa9, 0x20, 0x8e, 0xcb, 0xd2, 0x8b, 0x34, 0x13, 0xa3, 0xc4, 0x43, 0x81, 0xc2, 0x61, 0x1b, 0x5d, 0x5e, - 0x32, 0x78, 0x71, 0xbd, 0x6f, 0xc2, 0x65, 0xa4, 0xf0, 0x41, 0x0d, 0x85, 0xfb, 0x2b, 0x10, 0x72, 0x02, 0x35, 0xd9, - 0x29, 0xe8, 0x41, 0x80, 0xf3, 0x6b, 0x10, 0xb5, 0x93, 0x04, 0xa1, 0xb8, 0xd3, 0xf1, 0x40, 0x83, 0x3e, 0x99, 0x46, - 0xe9, 0x09, 0x1b, 0x07, 0x11, 0x2b, 0xa5, 0xe4, 0xdd, 0xb3, 0x60, 0x8d, 0x81, 0x9d, 0x0a, 0xeb, 0xf9, 0xc1, 0xab, - 0x97, 0x72, 0xe5, 0x6a, 0xc2, 0x18, 0x16, 0x69, 0x01, 0x6a, 0x15, 0xc4, 0xb6, 0x14, 0xc7, 0xcf, 0xb8, 0x92, 0xde, - 0xa2, 0x24, 0x2e, 0xde, 0xcf, 0xc1, 0xc4, 0x60, 0x6f, 0x61, 0x18, 0x98, 0x3e, 0x84, 0xa9, 0xa8, 0x1c, 0xe6, 0x13, - 0x15, 0x63, 0x5d, 0x04, 0x9d, 0x05, 0xa6, 0xe2, 0x35, 0x73, 0xdc, 0x12, 0x58, 0x95, 0xc7, 0x23, 0x2b, 0x1a, 0x8f, - 0x5f, 0xa4, 0x31, 0x8f, 0xa3, 0x24, 0xfe, 0x8d, 0x28, 0xb9, 0x44, 0x1e, 0xe3, 0x3d, 0xb9, 0x08, 0x80, 0x3b, 0xf5, - 0x48, 0x5c, 0x25, 0x64, 0xef, 0x10, 0x31, 0x84, 0xb4, 0x4c, 0xc2, 0xc3, 0xa1, 0x04, 0x2f, 0xf1, 0xe7, 0x8b, 0x62, - 0x8a, 0x84, 0x95, 0x03, 0xa3, 0x20, 0xcf, 0x8e, 0x0b, 0x96, 0x9f, 0xb2, 0xb1, 0xe6, 0x80, 0x02, 0xb0, 0xa2, 0xe6, - 0x60, 0xbc, 0xd0, 0x8c, 0x8e, 0xd2, 0xa1, 0x0c, 0x86, 0xea, 0x99, 0x62, 0x96, 0x49, 0x66, 0xd6, 0x16, 0x8e, 0x96, - 0x02, 0x8e, 0x30, 0x2a, 0xa4, 0x24, 0xc8, 0x43, 0x85, 0xe1, 0x14, 0xa4, 0x10, 0x68, 0x05, 0x73, 0x9b, 0x2b, 0x4d, - 0xf6, 0x6c, 0x41, 0x2a, 0x21, 0x87, 0x8e, 0xb0, 0x91, 0x09, 0xd2, 0xdc, 0x85, 0x5d, 0x05, 0x52, 0x5e, 0x82, 0x2b, - 0xa4, 0x88, 0x32, 0x73, 0x90, 0x01, 0xc2, 0x6f, 0x84, 0x2e, 0xf4, 0xb1, 0x05, 0xb1, 0x81, 0xaf, 0x57, 0x1e, 0x08, - 0x2b, 0xf1, 0xae, 0x10, 0xf1, 0xae, 0x00, 0x1b, 0x27, 0x46, 0x7e, 0xf2, 0xee, 0x70, 0x3f, 0xcd, 0xf6, 0x46, 0x23, - 0x56, 0x14, 0x19, 0xc0, 0x76, 0x87, 0xda, 0x5f, 0x65, 0x68, 0x01, 0x25, 0x5d, 0x2d, 0xeb, 0xec, 0x82, 0x34, 0xb8, - 0xa9, 0x56, 0x94, 0x4e, 0x0f, 0xec, 0x8f, 0x1f, 0x41, 0x66, 0x7b, 0x92, 0x0c, 0x40, 0xf5, 0x55, 0xc3, 0x4f, 0xd8, - 0x33, 0x75, 0xca, 0xac, 0xb5, 0x2f, 0x9d, 0x3a, 0x48, 0x1e, 0x0c, 0xeb, 0x96, 0xc6, 0x82, 0xae, 0x1d, 0x1a, 0x57, - 0x43, 0x2a, 0xc8, 0xe5, 0x09, 0xa9, 0x6c, 0x63, 0x19, 0xc1, 0x6a, 0x2b, 0x3d, 0x22, 0xbd, 0xc2, 0xa6, 0x20, 0x40, - 0x0f, 0xd9, 0xb0, 0x27, 0xeb, 0xc3, 0x5c, 0x50, 0x2e, 0x67, 0xbf, 0x2e, 0x58, 0xc1, 0x05, 0xeb, 0xc2, 0xb8, 0x05, - 0x8c, 0x5b, 0xae, 0x58, 0x87, 0x35, 0xdb, 0x71, 0x1d, 0x6c, 0x6f, 0xe6, 0xa8, 0xc7, 0x0a, 0xe4, 0xe4, 0xeb, 0xd9, - 0x09, 0x61, 0x65, 0xee, 0xe5, 0xe5, 0x37, 0x6a, 0x90, 0x6a, 0x29, 0xb5, 0x0d, 0xd4, 0x58, 0x13, 0x5b, 0x35, 0x19, - 0xdb, 0xae, 0x54, 0xa8, 0x77, 0x3a, 0xbd, 0x1a, 0x1f, 0xc0, 0x9e, 0x6b, 0x6b, 0x96, 0xae, 0x8c, 0xed, 0xb7, 0x8a, - 0xa6, 0x6f, 0xc4, 0xc8, 0x64, 0x8d, 0xb2, 0x9b, 0xb9, 0x47, 0xed, 0x78, 0x68, 0xbb, 0x52, 0x57, 0x09, 0x86, 0x45, - 0x5d, 0x30, 0x34, 0xa1, 0x9e, 0xeb, 0x2e, 0xb6, 0x66, 0x2a, 0x16, 0xaa, 0xb5, 0x56, 0x0e, 0x04, 0x0f, 0x0f, 0xc1, - 0x38, 0x59, 0xeb, 0x1f, 0xbc, 0x8e, 0x66, 0x0c, 0x29, 0xea, 0x5d, 0xd5, 0x40, 0x3a, 0x10, 0xd0, 0x64, 0xd8, 0x54, - 0x6f, 0xdc, 0x15, 0x56, 0x53, 0x7d, 0x7f, 0xc5, 0x60, 0x45, 0x80, 0x7d, 0x5d, 0xae, 0x59, 0x22, 0xd2, 0x9b, 0x82, - 0x4b, 0x34, 0x7d, 0x44, 0x99, 0x58, 0x13, 0x52, 0xf0, 0x80, 0x3c, 0x2c, 0x7f, 0x63, 0xe1, 0x64, 0x2b, 0xa6, 0x70, - 0xe4, 0x28, 0x53, 0x80, 0xce, 0xa4, 0x04, 0x40, 0x5c, 0xd2, 0xcf, 0xda, 0xc6, 0x42, 0xb2, 0xed, 0x23, 0x1f, 0xf8, - 0x93, 0x24, 0xe2, 0x4e, 0x67, 0xab, 0xed, 0x02, 0x1f, 0x82, 0x10, 0x07, 0x1d, 0x01, 0xe6, 0x7d, 0x85, 0x0a, 0x43, - 0x54, 0x62, 0x97, 0xfb, 0x60, 0x14, 0x4d, 0xe3, 0x09, 0x77, 0x52, 0x54, 0x22, 0x6e, 0xc9, 0x12, 0x50, 0x32, 0x7a, - 0x5f, 0x81, 0x94, 0xe0, 0x42, 0xba, 0x88, 0x6a, 0x2d, 0xd0, 0x14, 0xa4, 0x24, 0xa5, 0x48, 0x0b, 0x2a, 0x08, 0x0c, - 0xa1, 0xd2, 0x53, 0x1c, 0x05, 0xfa, 0x2d, 0x1e, 0x88, 0x41, 0x83, 0x15, 0x8b, 0x32, 0x1e, 0xc4, 0xab, 0x85, 0xa0, - 0x86, 0x7d, 0x9e, 0xbd, 0xcc, 0xce, 0x58, 0xfe, 0x24, 0x42, 0xd8, 0x03, 0xd1, 0xbd, 0x04, 0x49, 0x4f, 0x02, 0x9d, - 0xf5, 0x14, 0xaf, 0x9c, 0x12, 0xd2, 0xb0, 0x10, 0xb3, 0x18, 0x15, 0x21, 0x68, 0x39, 0xa2, 0x7d, 0x8a, 0x5b, 0x8a, - 0xf6, 0x1e, 0xaa, 0x12, 0xa6, 0x79, 0x6b, 0xef, 0x65, 0x9d, 0xb7, 0x60, 0x84, 0xb9, 0xe2, 0xd6, 0xfa, 0x8e, 0x75, - 0x3d, 0xa9, 0x9b, 0x1d, 0xc9, 0x5b, 0x86, 0x32, 0x03, 0xfd, 0x71, 0x79, 0x59, 0x19, 0xe9, 0xa0, 0x4c, 0xb5, 0x34, - 0x47, 0xcb, 0x49, 0x6c, 0x09, 0xb7, 0x04, 0x65, 0x84, 0x86, 0x57, 0x9e, 0x25, 0x89, 0xa1, 0x8b, 0xbc, 0xb8, 0xe7, - 0x34, 0xd4, 0x11, 0x40, 0x31, 0xab, 0x69, 0xa4, 0x01, 0x0f, 0x74, 0x05, 0x2a, 0x25, 0xa5, 0x8d, 0xbc, 0xaa, 0x89, - 0x80, 0x38, 0x1d, 0xb3, 0x5c, 0x38, 0x68, 0x52, 0x87, 0xc2, 0x84, 0x29, 0x30, 0x34, 0x1b, 0x83, 0x84, 0x57, 0x08, - 0x80, 0x79, 0xe2, 0x4f, 0xb3, 0x82, 0xeb, 0x3a, 0x13, 0xfa, 0xf8, 0xf2, 0x32, 0x16, 0xfe, 0x22, 0x32, 0x40, 0xce, - 0x66, 0xd9, 0x29, 0x5b, 0x03, 0x75, 0x4f, 0x0d, 0x66, 0x82, 0x6c, 0x0c, 0x03, 0x4a, 0x14, 0x54, 0xcb, 0x3c, 0x89, - 0x47, 0x4c, 0x6b, 0xa9, 0x99, 0x0f, 0x06, 0x1d, 0x3b, 0x07, 0x19, 0xc1, 0xdc, 0x7e, 0xbf, 0xdf, 0xf6, 0x3a, 0x6e, - 0x29, 0x08, 0xbe, 0x5c, 0xa1, 0xe8, 0x35, 0xfa, 0x51, 0x9a, 0xe0, 0xeb, 0x64, 0x01, 0x77, 0x0d, 0xa5, 0xc8, 0x85, - 0x9f, 0xe4, 0x49, 0x41, 0xec, 0x7a, 0x63, 0x18, 0x94, 0x33, 0x25, 0xb8, 0xd1, 0xc4, 0x15, 0xdb, 0xf6, 0x9d, 0x26, - 0x9b, 0x66, 0x27, 0xb5, 0xc3, 0xd4, 0xc2, 0xc8, 0x35, 0x2f, 0xb4, 0x07, 0x6c, 0x2e, 0x0f, 0x5a, 0x89, 0x54, 0x0d, - 0xbc, 0x0e, 0x10, 0x0a, 0x4f, 0xd7, 0x59, 0x41, 0xa9, 0xea, 0x2c, 0x85, 0xb8, 0xde, 0x40, 0xef, 0x99, 0x04, 0x73, - 0x1d, 0x09, 0xf6, 0xa5, 0x40, 0xe0, 0xe8, 0x91, 0x89, 0xf5, 0x7a, 0x02, 0xcb, 0x73, 0x1c, 0x8d, 0x3e, 0x69, 0x70, - 0x2b, 0xb2, 0x37, 0xd9, 0xc0, 0x69, 0x94, 0x84, 0x86, 0xb8, 0x32, 0xf1, 0x56, 0x12, 0xba, 0xb6, 0x51, 0xc0, 0x21, - 0x5b, 0x61, 0xfb, 0xe6, 0x42, 0x37, 0xb9, 0x5d, 0xb2, 0x87, 0xf2, 0x9f, 0x34, 0x97, 0x5c, 0xc3, 0x72, 0x5c, 0x49, - 0x03, 0xae, 0x18, 0x0f, 0x96, 0xa6, 0x01, 0x09, 0xf0, 0x5d, 0x39, 0x8e, 0x8b, 0xab, 0x49, 0xf0, 0x87, 0x82, 0xf9, - 0xd4, 0x98, 0xe9, 0x46, 0x48, 0xb5, 0x84, 0x93, 0x66, 0xb0, 0x06, 0x4d, 0x1a, 0x0f, 0x4a, 0xd4, 0x7c, 0x83, 0x86, - 0x0a, 0x71, 0xfc, 0x89, 0xa8, 0x42, 0x13, 0x0c, 0xc1, 0xc8, 0xbd, 0x42, 0x32, 0x5c, 0xb6, 0x2a, 0x5a, 0xa4, 0x4c, - 0x8d, 0x49, 0xa5, 0x6a, 0x96, 0xcb, 0xc0, 0xc0, 0xa2, 0xdd, 0xea, 0x4b, 0x4b, 0x5c, 0x89, 0xdc, 0x34, 0xd4, 0xc2, - 0xa4, 0x50, 0xde, 0x84, 0x93, 0xa3, 0xdf, 0xa5, 0xac, 0x77, 0x13, 0x9f, 0x5c, 0xe1, 0x93, 0xfb, 0x86, 0x0f, 0x65, - 0xf2, 0x76, 0x31, 0x28, 0x82, 0xaf, 0x6b, 0x95, 0x68, 0x9f, 0xfa, 0x28, 0x98, 0x5d, 0x2d, 0x74, 0x41, 0xa0, 0x48, - 0x36, 0x49, 0x07, 0x92, 0xdf, 0x50, 0x6c, 0x54, 0x9e, 0x51, 0xe6, 0x8a, 0x0d, 0x52, 0xf3, 0x4a, 0x33, 0x2f, 0x75, - 0x1b, 0xf6, 0x7b, 0x59, 0x4a, 0x3a, 0x71, 0x41, 0x99, 0xd8, 0xbb, 0x8e, 0x36, 0x5e, 0x1a, 0x66, 0xc2, 0xfa, 0x15, - 0xc6, 0x4e, 0x8d, 0x42, 0xa9, 0x14, 0x81, 0x38, 0x36, 0xbe, 0x56, 0x96, 0x41, 0xe6, 0xaf, 0xb1, 0xa7, 0x00, 0x94, - 0x04, 0x16, 0x5f, 0x53, 0xc9, 0x8b, 0xc2, 0x3a, 0x1d, 0xef, 0x10, 0x1d, 0x2b, 0x11, 0x5a, 0x13, 0xf9, 0x5a, 0x9f, - 0xc5, 0x7e, 0xcd, 0x25, 0x34, 0x29, 0x99, 0x0f, 0xf2, 0xc0, 0x56, 0x81, 0x88, 0x4a, 0xb7, 0x25, 0x83, 0x84, 0x1c, - 0xd2, 0x55, 0xa2, 0xd7, 0x46, 0x32, 0x68, 0x9d, 0x0a, 0x89, 0x96, 0x0e, 0xc3, 0xc8, 0x41, 0xc7, 0x9d, 0xd6, 0x62, - 0x85, 0x90, 0x4d, 0x7b, 0x93, 0x58, 0x11, 0x9d, 0xd3, 0x1c, 0x4d, 0x38, 0x53, 0xa7, 0x3b, 0x0e, 0xa0, 0x03, 0x62, - 0x7f, 0x85, 0xf5, 0xd6, 0x9a, 0x9d, 0xae, 0x5f, 0x39, 0x7c, 0x97, 0x97, 0x11, 0xf2, 0x83, 0x30, 0x78, 0x61, 0xcd, - 0x06, 0x4a, 0xf6, 0xee, 0xbd, 0xc4, 0x56, 0x64, 0x7f, 0x56, 0x25, 0x95, 0xa7, 0x50, 0xe3, 0xdc, 0xfa, 0x3a, 0x31, - 0x33, 0xb4, 0xa8, 0x2a, 0xf6, 0x0d, 0xa9, 0xbe, 0xaf, 0x14, 0x76, 0x85, 0xf2, 0xbe, 0x1c, 0x3a, 0x76, 0x5d, 0x37, - 0xc8, 0xc9, 0x79, 0xb9, 0xb3, 0xce, 0x85, 0xbc, 0x7b, 0xd7, 0xf4, 0x99, 0x4e, 0xf5, 0xf0, 0x4f, 0x1c, 0x54, 0xce, - 0xc5, 0x45, 0x4a, 0x16, 0xcc, 0x13, 0xa5, 0x8e, 0x56, 0x1c, 0xd0, 0x76, 0x0f, 0x3d, 0xed, 0xe8, 0x2c, 0x8a, 0xb9, - 0xa5, 0x47, 0x11, 0x9e, 0x36, 0xca, 0x27, 0x69, 0x74, 0x00, 0x5e, 0x68, 0x42, 0x92, 0x13, 0x6e, 0xda, 0xa2, 0xc5, - 0x68, 0xca, 0x30, 0x04, 0xae, 0xec, 0x09, 0x53, 0xf6, 0xdc, 0x41, 0xbc, 0xc5, 0xc0, 0x6c, 0x3d, 0xec, 0x65, 0xb3, - 0x7b, 0xcd, 0xfc, 0x87, 0x35, 0x02, 0xd9, 0x36, 0x53, 0x75, 0x65, 0xe3, 0x5d, 0x8a, 0x48, 0x8c, 0xb0, 0xad, 0x1b, - 0x5b, 0xda, 0xfa, 0xbd, 0x86, 0x7b, 0x5d, 0x39, 0xe6, 0x35, 0xa5, 0xda, 0xd0, 0xc3, 0xca, 0xcd, 0x61, 0xa6, 0x23, - 0x2f, 0x56, 0xd0, 0xed, 0x89, 0xa0, 0x10, 0x38, 0x11, 0xda, 0x1e, 0x54, 0xdc, 0x40, 0xa4, 0xe4, 0x4a, 0xab, 0x66, - 0x8b, 0x64, 0x2c, 0x81, 0x05, 0x17, 0x96, 0x4b, 0x3e, 0x3a, 0x8b, 0x93, 0xa4, 0x2a, 0xfd, 0x43, 0x05, 0xbc, 0x18, - 0xf6, 0x26, 0xd1, 0x2e, 0x30, 0x5a, 0x28, 0x10, 0x5c, 0x6d, 0x84, 0xbd, 0x77, 0xdc, 0x6a, 0xdd, 0x45, 0xc4, 0x91, - 0x9b, 0xd1, 0x08, 0xa8, 0xc7, 0x08, 0xab, 0x66, 0xed, 0xbd, 0x67, 0x18, 0x52, 0x33, 0xf0, 0x41, 0x75, 0x46, 0xc5, - 0x9f, 0x65, 0x4f, 0x7d, 0x26, 0x7a, 0x37, 0xaa, 0xae, 0x66, 0x40, 0x45, 0x05, 0x3e, 0xcc, 0x10, 0x4b, 0x5b, 0x05, - 0x02, 0x72, 0x3d, 0x2c, 0x4a, 0x01, 0x93, 0x34, 0x58, 0x50, 0x0a, 0xac, 0xb5, 0xb2, 0x7b, 0x79, 0x53, 0x30, 0x87, - 0x42, 0xe1, 0xa2, 0xff, 0x93, 0x6c, 0x36, 0x47, 0xcb, 0xac, 0xc1, 0xd4, 0xd0, 0xe0, 0x7d, 0xa3, 0xbe, 0x5c, 0x53, - 0x56, 0xeb, 0x43, 0x3b, 0xb2, 0xc6, 0x4f, 0xda, 0x51, 0x06, 0x87, 0x6a, 0xa1, 0x8b, 0xea, 0x76, 0x73, 0x53, 0xc4, - 0xac, 0xe3, 0x71, 0x9f, 0xf4, 0xb6, 0xb6, 0x26, 0x3d, 0x4d, 0x03, 0x92, 0x49, 0x92, 0xe1, 0x4d, 0x06, 0x28, 0x2b, - 0xe2, 0x2c, 0xcb, 0x06, 0xf9, 0x96, 0x65, 0x89, 0xeb, 0xf7, 0x6d, 0x6f, 0xaf, 0xe6, 0x59, 0x7b, 0x7b, 0x57, 0xbb, - 0xc8, 0x55, 0x9d, 0xf4, 0x20, 0x0f, 0x87, 0x50, 0xb4, 0x62, 0x53, 0x86, 0xcb, 0x59, 0x36, 0x66, 0x81, 0x0d, 0xdd, - 0x53, 0xbb, 0x94, 0x9b, 0x26, 0x81, 0xcd, 0x91, 0x30, 0x67, 0xf9, 0xae, 0x1e, 0x49, 0x0d, 0xf6, 0x80, 0x05, 0xb4, - 0xb9, 0xf0, 0x5d, 0x78, 0x92, 0x64, 0xc7, 0x51, 0x72, 0x20, 0x14, 0x78, 0xad, 0xe5, 0x07, 0x70, 0x19, 0xc9, 0x62, - 0x35, 0x94, 0xd4, 0x77, 0x83, 0xef, 0x82, 0x9b, 0x7b, 0x54, 0xde, 0x8a, 0xdd, 0xf1, 0xdb, 0x7e, 0xc7, 0x56, 0x11, - 0xb1, 0x97, 0xe6, 0x74, 0xa0, 0x71, 0x0a, 0xa0, 0xcc, 0x01, 0x68, 0xb2, 0xc2, 0x9b, 0xb2, 0xf0, 0xe5, 0xe0, 0xa5, - 0x72, 0xa9, 0x33, 0x70, 0x21, 0xc0, 0xc9, 0x4f, 0x62, 0xde, 0xc2, 0xf3, 0x48, 0xdb, 0x5b, 0x8a, 0x0a, 0x8c, 0x2b, - 0x52, 0x5c, 0xba, 0x54, 0xde, 0xa0, 0xf7, 0x31, 0x3c, 0x82, 0x66, 0x1b, 0x1b, 0x4b, 0xe7, 0x55, 0xc4, 0xa7, 0x7e, - 0x1e, 0xa5, 0xe3, 0x6c, 0xe6, 0xb8, 0x9b, 0xb6, 0xed, 0xfa, 0x05, 0x79, 0x22, 0x5f, 0xba, 0xe5, 0xc6, 0x91, 0x37, - 0x62, 0xa1, 0x3d, 0xb0, 0x37, 0x3f, 0x7a, 0x07, 0x2c, 0x3c, 0xda, 0xdd, 0x58, 0x8e, 0x58, 0xd9, 0x3f, 0xf2, 0xce, - 0x75, 0xcc, 0xdd, 0x7b, 0x8b, 0x52, 0x06, 0x7a, 0x85, 0xfd, 0x73, 0x09, 0x06, 0xb0, 0x1b, 0xc5, 0xdf, 0x41, 0xca, - 0xbd, 0xa7, 0x03, 0x11, 0x19, 0xa7, 0xbd, 0xbc, 0xb4, 0x33, 0x8a, 0x18, 0xd8, 0x77, 0xb4, 0xb3, 0x7a, 0xf7, 0x6e, - 0xa5, 0xe6, 0xab, 0x52, 0xf0, 0x3e, 0xc2, 0x9a, 0xa7, 0xee, 0x3d, 0xa7, 0xa3, 0x95, 0xfa, 0x46, 0x1e, 0x33, 0x52, - 0x9a, 0xab, 0x76, 0x82, 0x63, 0x6c, 0xf1, 0xf5, 0xdb, 0xfa, 0x50, 0x44, 0x29, 0xfc, 0x18, 0xac, 0x97, 0x08, 0xd4, - 0x37, 0x38, 0x38, 0xde, 0x41, 0xb8, 0xb5, 0xeb, 0x0c, 0x02, 0xe7, 0x4e, 0xab, 0x75, 0xf9, 0xd3, 0xd6, 0xe1, 0xcf, - 0x51, 0xeb, 0xb7, 0xbd, 0xd6, 0x8f, 0x43, 0xf7, 0xd2, 0xf9, 0x69, 0x6b, 0x70, 0x28, 0xdf, 0x0e, 0x7f, 0xee, 0xff, - 0x54, 0x0c, 0xff, 0x24, 0x0a, 0x37, 0x5c, 0x77, 0xeb, 0xc4, 0x5b, 0xb0, 0x70, 0xab, 0xd5, 0xea, 0xc3, 0xd3, 0x1c, - 0x9e, 0xf0, 0xe7, 0x19, 0xfc, 0xb8, 0x3c, 0xb4, 0xfe, 0xd3, 0x4f, 0xe9, 0xdf, 0xfc, 0x94, 0x0f, 0x71, 0xcc, 0xc3, - 0x9f, 0x7f, 0x2a, 0xec, 0x7b, 0xfd, 0x70, 0x6b, 0xb8, 0xe9, 0x3a, 0xba, 0xe6, 0x4f, 0x61, 0xf5, 0x08, 0xad, 0x0e, - 0x7f, 0x96, 0x6f, 0xf6, 0xbd, 0xa3, 0xdd, 0x7e, 0x38, 0xbc, 0x74, 0xec, 0xcb, 0x7b, 0xee, 0xa5, 0xeb, 0x5e, 0x6e, - 0xe0, 0x3c, 0x27, 0x30, 0xfa, 0x3d, 0xf8, 0x79, 0x0a, 0x3f, 0x6d, 0xf8, 0x39, 0x81, 0x9f, 0x3f, 0x43, 0x37, 0x11, - 0x7f, 0xbb, 0xa4, 0x58, 0xc8, 0x25, 0x1e, 0x58, 0x44, 0xb0, 0x0a, 0xee, 0xc6, 0x56, 0xec, 0x6d, 0x10, 0xd1, 0x60, - 0x1f, 0xfa, 0xbe, 0x8f, 0x61, 0x52, 0x67, 0xf9, 0x71, 0x03, 0x16, 0x1d, 0x39, 0x67, 0x23, 0x60, 0x9e, 0x88, 0x1c, - 0x14, 0x01, 0x17, 0x67, 0xab, 0x05, 0x1e, 0xae, 0x7a, 0xe3, 0x70, 0x83, 0x39, 0x60, 0x14, 0xbc, 0x66, 0xf8, 0xd0, - 0x75, 0xbd, 0x67, 0xf2, 0xcc, 0x10, 0xf7, 0xb9, 0x60, 0xad, 0x34, 0x13, 0x26, 0x8d, 0xed, 0x7a, 0xf3, 0x35, 0x95, - 0xb0, 0xad, 0xd3, 0x13, 0xa8, 0x9b, 0x89, 0x83, 0xb6, 0xef, 0x58, 0xf4, 0x09, 0xb7, 0xe4, 0x2b, 0xe3, 0x10, 0x78, - 0xc5, 0x92, 0x6f, 0x1a, 0x8d, 0x86, 0x8d, 0x28, 0xdc, 0xb1, 0xc7, 0x0c, 0x66, 0x58, 0x31, 0x11, 0x39, 0x29, 0x4d, - 0x61, 0xd9, 0xc2, 0xe4, 0x6f, 0xa3, 0x9c, 0x6f, 0x54, 0x86, 0x6d, 0x58, 0xb3, 0x64, 0x9b, 0x96, 0xfe, 0x2d, 0xa6, - 0x40, 0xd3, 0x92, 0xce, 0x3f, 0xcc, 0xf1, 0xc3, 0x94, 0xd0, 0x7a, 0xed, 0x70, 0xf0, 0xd0, 0x0b, 0x90, 0x3b, 0xa2, - 0x9f, 0xf3, 0x16, 0xd5, 0x18, 0xfc, 0x95, 0x61, 0x06, 0x4f, 0xcc, 0x87, 0x21, 0x9a, 0x65, 0xa9, 0x83, 0x5b, 0x29, - 0x8a, 0xfb, 0x17, 0xb8, 0x33, 0xd2, 0xd2, 0xdb, 0x0f, 0xd5, 0x8e, 0x39, 0xc8, 0x19, 0xfb, 0x2e, 0x4a, 0x3e, 0xb1, - 0xdc, 0x39, 0xf7, 0x3a, 0xdd, 0x2f, 0xa9, 0xb3, 0x87, 0xb6, 0xd9, 0xbb, 0xea, 0x18, 0x4d, 0x99, 0x05, 0xea, 0x88, - 0xb0, 0xd5, 0xf1, 0x72, 0x8c, 0x6a, 0x21, 0x09, 0x0a, 0x2f, 0x0b, 0xbb, 0xc4, 0xe1, 0xf6, 0x6e, 0x71, 0x7a, 0xd2, - 0xb7, 0x03, 0xdb, 0x06, 0x8b, 0xff, 0x80, 0xc2, 0x56, 0xc2, 0xb0, 0x00, 0x83, 0x6c, 0x37, 0xee, 0xf1, 0xcd, 0xcd, - 0x2a, 0xe0, 0x84, 0x07, 0xe9, 0xd4, 0x3d, 0xf1, 0x22, 0x6f, 0x1a, 0xc2, 0x80, 0x23, 0x68, 0x86, 0x5d, 0x7a, 0xa3, - 0xdd, 0x58, 0x4e, 0x83, 0xb1, 0x10, 0x3f, 0x89, 0x0a, 0xfe, 0x02, 0xe3, 0x11, 0xe1, 0x08, 0x8d, 0x7d, 0x9f, 0x9d, - 0xb3, 0x91, 0xb2, 0x33, 0x80, 0x50, 0x91, 0xdb, 0x73, 0x47, 0xa1, 0xd1, 0x0c, 0xe6, 0x0e, 0xc3, 0x83, 0x81, 0x0d, - 0x7b, 0x09, 0x76, 0x65, 0x18, 0x1d, 0x76, 0x86, 0x83, 0x34, 0x5c, 0xb0, 0x40, 0xd3, 0x56, 0x16, 0xcd, 0x6b, 0x45, - 0xdd, 0xe1, 0xc0, 0x99, 0x80, 0x91, 0x0e, 0xb6, 0xb8, 0x83, 0x6f, 0x18, 0xa1, 0x28, 0xc2, 0x77, 0xec, 0xe4, 0xd9, - 0xf9, 0xdc, 0xb1, 0x77, 0xb7, 0xec, 0x4d, 0x2c, 0xf5, 0x6c, 0x60, 0x2f, 0x98, 0x3b, 0x3c, 0x73, 0xcd, 0xce, 0xdb, - 0x43, 0x04, 0x15, 0x0b, 0x71, 0xf2, 0xb3, 0x81, 0xdd, 0x17, 0x53, 0xb7, 0x61, 0xd0, 0x54, 0x2e, 0x3f, 0xae, 0xe8, - 0x01, 0xa1, 0xaa, 0xba, 0x2a, 0xe8, 0xa0, 0xac, 0x1b, 0x38, 0x53, 0x13, 0x89, 0x16, 0x4e, 0x26, 0xa9, 0x00, 0x0e, - 0x0f, 0x36, 0x83, 0x49, 0x8d, 0x6e, 0xdb, 0xc3, 0xc1, 0x59, 0x70, 0xcf, 0xbe, 0xa7, 0x5e, 0x4e, 0x59, 0x70, 0xc2, - 0xc4, 0xf4, 0xa7, 0x20, 0xed, 0xf0, 0xe7, 0x09, 0x03, 0x24, 0xcf, 0xa8, 0x68, 0x21, 0x8b, 0xe6, 0x58, 0x74, 0x10, - 0x20, 0xa8, 0x5e, 0xa1, 0xad, 0x3f, 0xb1, 0x26, 0xe3, 0x90, 0x60, 0xbf, 0x7b, 0x17, 0x96, 0x66, 0xb3, 0x33, 0xc4, - 0xf3, 0x86, 0x9c, 0x17, 0xdf, 0xc5, 0x1c, 0x54, 0xc2, 0x56, 0xdf, 0x76, 0x07, 0xb6, 0x85, 0x4b, 0xdb, 0xcb, 0x36, - 0x43, 0x41, 0xe1, 0x78, 0xf3, 0x80, 0x05, 0xd3, 0x7e, 0xd8, 0x1e, 0x38, 0xb9, 0x50, 0x1d, 0x09, 0x9e, 0x5b, 0x0a, - 0x09, 0xde, 0xf6, 0xa6, 0x20, 0xd0, 0x91, 0x73, 0x37, 0xec, 0x4d, 0x55, 0x08, 0x45, 0x1f, 0x37, 0xc7, 0x6e, 0x10, - 0xc3, 0x0f, 0xa7, 0x85, 0x4c, 0x33, 0xd5, 0x7d, 0xb5, 0x66, 0x76, 0x83, 0xb1, 0xb2, 0xc8, 0x93, 0x30, 0xdb, 0x74, - 0x30, 0x42, 0x0b, 0x92, 0x76, 0x77, 0x00, 0x30, 0x6c, 0x3a, 0x8a, 0xd3, 0xb6, 0x14, 0xab, 0x29, 0xfb, 0xfc, 0x50, - 0x2f, 0xc7, 0x94, 0x0d, 0xa6, 0xcc, 0xaf, 0xb4, 0x0f, 0x80, 0x15, 0x24, 0x5e, 0x3e, 0x54, 0x67, 0x5e, 0xcf, 0x6b, - 0xe7, 0x5b, 0x4b, 0x25, 0x8a, 0x98, 0x67, 0x48, 0x28, 0x5e, 0x6a, 0x37, 0x4c, 0x98, 0xdb, 0x73, 0x24, 0x86, 0x66, - 0xf9, 0xb0, 0x0d, 0x4c, 0xaf, 0x02, 0xec, 0xa9, 0xb9, 0x2d, 0x92, 0xb0, 0x6a, 0xee, 0x1d, 0x02, 0x6b, 0x0f, 0x81, - 0x87, 0x68, 0x1b, 0xf5, 0x54, 0x34, 0x9f, 0x25, 0xe1, 0xf3, 0xc6, 0x71, 0x71, 0x84, 0x27, 0x42, 0xfb, 0xfe, 0x68, - 0x91, 0x83, 0x3c, 0xe0, 0xaf, 0xc1, 0x32, 0x08, 0x65, 0x53, 0x74, 0xf4, 0xf0, 0x08, 0xd8, 0x23, 0xc4, 0x1b, 0x61, - 0x73, 0xa3, 0x1a, 0x2d, 0x4a, 0x32, 0x5e, 0xe8, 0x60, 0xb8, 0xc7, 0xa5, 0x6b, 0x8f, 0x82, 0x41, 0x9e, 0x18, 0x3b, - 0x78, 0xe6, 0xef, 0x8f, 0xb0, 0x1a, 0x27, 0x28, 0xdc, 0x92, 0x76, 0x5b, 0x25, 0xfe, 0xf6, 0xfd, 0x14, 0x24, 0x38, - 0xd6, 0x81, 0x9f, 0x75, 0xf7, 0x6e, 0x22, 0x91, 0xda, 0x4d, 0x7b, 0x74, 0x12, 0x81, 0xf1, 0xe0, 0xdc, 0x4f, 0xa1, - 0x1a, 0x49, 0x44, 0x45, 0x39, 0x5a, 0xa0, 0xe6, 0xa9, 0x5a, 0x05, 0xdf, 0xa1, 0x19, 0x81, 0xe7, 0x18, 0xb6, 0x26, - 0x3f, 0x55, 0x37, 0x16, 0xb1, 0x7c, 0xd7, 0xa5, 0xa3, 0x2d, 0x3c, 0x80, 0x14, 0x8c, 0x26, 0x18, 0xc6, 0xa5, 0xa0, - 0x64, 0xc5, 0x7f, 0x1f, 0x8d, 0x58, 0xf9, 0xf4, 0x30, 0xdb, 0xdc, 0x1c, 0x8a, 0x73, 0x0b, 0x62, 0x1c, 0x6e, 0x44, - 0x57, 0xe3, 0x0a, 0x80, 0xfa, 0x74, 0x4e, 0x5c, 0x0f, 0x4c, 0x2b, 0xd6, 0x74, 0x29, 0xf6, 0xc9, 0x61, 0x06, 0xa0, - 0xe0, 0x96, 0x73, 0xe8, 0x0f, 0xfe, 0x3c, 0x04, 0xf7, 0xd8, 0xff, 0x93, 0xbb, 0xa5, 0x04, 0x4d, 0x4f, 0x9e, 0x29, - 0x2e, 0xe9, 0x8c, 0xb5, 0xe3, 0x51, 0x6c, 0x34, 0x28, 0xbc, 0x14, 0x30, 0x00, 0x6d, 0x0e, 0x32, 0xa1, 0xe2, 0x20, - 0xe4, 0xa8, 0xc0, 0xf6, 0x71, 0xf3, 0x73, 0xdc, 0xd9, 0x4f, 0xc1, 0xc2, 0x1b, 0xe8, 0xb7, 0x97, 0xf0, 0xf6, 0x67, - 0xfd, 0xf6, 0x0b, 0x0b, 0x7e, 0x29, 0x65, 0xe8, 0xbe, 0x36, 0xc5, 0x03, 0x35, 0x45, 0x29, 0x96, 0xc8, 0xa0, 0x21, - 0x73, 0xf3, 0x95, 0x98, 0x0d, 0x77, 0x4b, 0x20, 0x86, 0x12, 0x5d, 0xb9, 0xcf, 0xa3, 0x13, 0x24, 0xae, 0x6b, 0x92, - 0xc2, 0xc8, 0x25, 0x30, 0x11, 0xae, 0xf8, 0x96, 0x98, 0xb3, 0xdf, 0x06, 0x1b, 0xbc, 0x96, 0x77, 0x80, 0xf6, 0x1d, - 0x9b, 0xcd, 0xf9, 0xc5, 0x3e, 0x29, 0xfa, 0x40, 0xa6, 0x0d, 0x88, 0xb3, 0xf3, 0x76, 0x2f, 0xde, 0xe5, 0xbd, 0x18, - 0xa4, 0x7a, 0xae, 0x58, 0x0c, 0xf7, 0xaa, 0xf7, 0x16, 0xa3, 0x94, 0x26, 0x33, 0x79, 0x35, 0xf4, 0xba, 0x12, 0xbd, - 0xcd, 0x4d, 0x40, 0xb0, 0x67, 0x74, 0xe5, 0xa2, 0x6b, 0x59, 0x0a, 0x9a, 0x00, 0x44, 0x8f, 0xea, 0x2c, 0x47, 0x1c, - 0x87, 0xd9, 0x6c, 0x50, 0x3c, 0x62, 0xee, 0xda, 0x51, 0x71, 0x4c, 0xec, 0x2e, 0x13, 0x76, 0x00, 0x33, 0xe2, 0xf2, - 0x56, 0x47, 0x44, 0x87, 0x45, 0x7f, 0x1d, 0xdf, 0xfe, 0xe8, 0xb1, 0xcd, 0x8e, 0x0b, 0x1a, 0xa4, 0x36, 0xd6, 0xc3, - 0x6a, 0x2c, 0xa8, 0x0f, 0x3f, 0x6a, 0x2a, 0x95, 0xc5, 0xe6, 0x66, 0x59, 0x3f, 0xaa, 0x55, 0x3b, 0xb8, 0x76, 0x9a, - 0x72, 0xde, 0xcc, 0x06, 0xe1, 0x40, 0xc4, 0x04, 0x0a, 0xb4, 0xb4, 0xb2, 0x62, 0x80, 0x21, 0x65, 0x39, 0xca, 0xa7, - 0x90, 0x79, 0x71, 0x59, 0xea, 0xd4, 0x17, 0x19, 0x8f, 0x0c, 0xf1, 0xd4, 0x93, 0x8c, 0x15, 0x50, 0xb0, 0x5e, 0xea, - 0x25, 0xb4, 0x44, 0x80, 0xf9, 0x33, 0x95, 0x43, 0x23, 0x2c, 0x90, 0x28, 0x34, 0xcc, 0x12, 0x65, 0x7c, 0x16, 0x61, - 0x0c, 0xda, 0xfe, 0x49, 0x2d, 0xf6, 0x55, 0x28, 0xa3, 0xa3, 0x38, 0xcc, 0x87, 0x01, 0xd5, 0x2f, 0xa4, 0x04, 0x9b, - 0x86, 0xef, 0x81, 0x8d, 0x2a, 0xc7, 0x93, 0x04, 0xe1, 0xd3, 0x38, 0x67, 0xe4, 0x29, 0x6c, 0x48, 0x98, 0xa5, 0x69, - 0x1b, 0xa9, 0x76, 0x91, 0x19, 0x84, 0x72, 0x51, 0xf0, 0x1a, 0x67, 0x17, 0x59, 0xb8, 0xd2, 0x1a, 0xcc, 0x8f, 0x37, - 0x26, 0x40, 0xd9, 0xe5, 0x65, 0x26, 0x7c, 0xdc, 0x88, 0xec, 0x0d, 0x5d, 0x31, 0x1d, 0x28, 0xa4, 0x02, 0x27, 0x22, - 0x8b, 0x87, 0xce, 0x50, 0x68, 0x84, 0x03, 0x3a, 0x45, 0xce, 0x5d, 0x63, 0xd3, 0xe7, 0x03, 0xed, 0x1b, 0xa5, 0xa1, - 0x93, 0x80, 0x10, 0x10, 0xb8, 0x1b, 0xd6, 0x54, 0x3a, 0x48, 0x83, 0x84, 0x4a, 0xd1, 0xcf, 0x01, 0xfc, 0xc3, 0x48, - 0x52, 0x00, 0xec, 0x87, 0x6a, 0xa4, 0x88, 0xb2, 0x2c, 0x70, 0x01, 0x68, 0xae, 0x7d, 0x5c, 0x09, 0x5f, 0x18, 0xa8, - 0x30, 0x3d, 0xcd, 0xca, 0x4b, 0xa1, 0x44, 0x1e, 0xaf, 0x49, 0x59, 0x23, 0x99, 0x7c, 0x8a, 0x0e, 0x9f, 0xf2, 0xae, - 0x5f, 0x4b, 0x3c, 0x74, 0xc1, 0x53, 0x58, 0x56, 0xf5, 0xfc, 0x2a, 0xe4, 0xe4, 0x5c, 0x83, 0xae, 0x90, 0x42, 0x7f, - 0xc5, 0x49, 0xde, 0x7b, 0xe5, 0x57, 0xb5, 0xd4, 0x18, 0xca, 0xde, 0xaf, 0x6b, 0x86, 0xe5, 0xe5, 0xbc, 0x0a, 0x53, - 0x10, 0x70, 0x4b, 0x96, 0x04, 0x4b, 0xa9, 0x21, 0xc0, 0xc2, 0xf6, 0x48, 0x2b, 0x05, 0x79, 0xa9, 0xc3, 0x3b, 0x4f, - 0xc1, 0x0a, 0x30, 0x0e, 0xb5, 0x54, 0x32, 0x8d, 0x24, 0xbe, 0x54, 0xa2, 0xc0, 0x94, 0xfb, 0x23, 0xf0, 0x53, 0x9b, - 0x27, 0x5d, 0xe7, 0xae, 0x1f, 0xcf, 0x30, 0xb5, 0x87, 0x40, 0x8f, 0xbd, 0x3b, 0x60, 0x4a, 0xd4, 0x75, 0x58, 0x41, - 0x1c, 0x9a, 0xd5, 0x34, 0x0b, 0x98, 0x31, 0x6d, 0xd0, 0x92, 0x6d, 0xb0, 0xe5, 0x72, 0xb0, 0x8f, 0xc4, 0xf6, 0xac, - 0x56, 0x40, 0xe8, 0x1a, 0x34, 0x30, 0xe4, 0x2e, 0x15, 0x5a, 0x98, 0xf7, 0xba, 0x54, 0x84, 0xfb, 0x73, 0xc0, 0xa5, - 0x15, 0x9c, 0x79, 0x19, 0x0d, 0xbc, 0x1f, 0x1f, 0x27, 0x98, 0xf8, 0x82, 0x58, 0x81, 0x1d, 0x1c, 0x74, 0x9a, 0x4d, - 0x81, 0x53, 0x71, 0x91, 0x32, 0x58, 0x56, 0x94, 0xda, 0xf0, 0x43, 0x8a, 0x6c, 0xdd, 0xe5, 0x81, 0xee, 0x42, 0x2c, - 0x80, 0x9d, 0x7e, 0xc3, 0xc8, 0xb7, 0xac, 0x97, 0x01, 0x83, 0x53, 0xad, 0x71, 0x10, 0xf8, 0xcd, 0xcd, 0x64, 0x58, - 0xa6, 0xc4, 0x76, 0x4d, 0x56, 0x17, 0x90, 0xc3, 0x50, 0x4d, 0xdc, 0x41, 0x58, 0x2a, 0x7b, 0xbc, 0x28, 0x67, 0xb8, - 0x5c, 0xca, 0x42, 0x6e, 0x9e, 0x57, 0xd3, 0x7c, 0x6e, 0xa5, 0xd9, 0x74, 0xbc, 0x15, 0x5f, 0x14, 0xfc, 0x03, 0x27, - 0x96, 0x56, 0x3d, 0xa5, 0x56, 0x78, 0x94, 0xb9, 0x25, 0xeb, 0x94, 0xd4, 0xea, 0xba, 0x81, 0x6a, 0x84, 0xa7, 0x69, - 0xd8, 0x08, 0x84, 0x98, 0xe0, 0xe2, 0xd7, 0x4d, 0x26, 0xa6, 0xbd, 0x25, 0xa4, 0x8e, 0xb0, 0x7b, 0x28, 0x27, 0xb8, - 0xab, 0x79, 0xf6, 0x79, 0x38, 0xbf, 0x9a, 0xb9, 0xf7, 0x0c, 0xe6, 0x7e, 0x1c, 0x72, 0x83, 0xd1, 0x63, 0x99, 0xf0, - 0x23, 0x63, 0x1f, 0xb9, 0xaa, 0x7a, 0x72, 0x12, 0x56, 0x22, 0x4b, 0x3c, 0x19, 0x47, 0x1d, 0xc6, 0xa9, 0x68, 0x4d, - 0x90, 0x5d, 0x5e, 0x16, 0xe6, 0x5e, 0xa0, 0xa0, 0xa9, 0xc7, 0xeb, 0x71, 0xda, 0x8a, 0x9d, 0x8d, 0x48, 0xe4, 0xde, - 0xab, 0x5a, 0x24, 0xb2, 0xe2, 0x73, 0x1c, 0xe9, 0x8a, 0x83, 0xdc, 0x27, 0x27, 0xab, 0x9b, 0x54, 0xe8, 0x16, 0x8d, - 0xb6, 0xb1, 0x47, 0xf5, 0x81, 0xa4, 0x9e, 0x51, 0x81, 0x55, 0x8d, 0x7d, 0xf7, 0x6e, 0x47, 0xa4, 0x5b, 0x2a, 0xc5, - 0x06, 0x4b, 0x0b, 0xa3, 0x19, 0xa3, 0x60, 0x50, 0x52, 0x64, 0xa0, 0x46, 0xf9, 0x15, 0x82, 0x61, 0x8f, 0x1a, 0x80, - 0xe2, 0x5c, 0x5f, 0xfd, 0xb8, 0x94, 0x6c, 0x21, 0x20, 0x71, 0x97, 0x0c, 0xc4, 0x9a, 0x60, 0x66, 0xe4, 0x93, 0xf7, - 0xc0, 0x79, 0x03, 0x86, 0x0e, 0x01, 0xf8, 0x05, 0x62, 0xd3, 0x83, 0x89, 0x6d, 0x13, 0x51, 0xf4, 0xd9, 0xc0, 0x73, - 0x00, 0x76, 0x5e, 0x85, 0x46, 0xdf, 0x55, 0x29, 0x60, 0xc8, 0x06, 0x6e, 0xc0, 0xaa, 0xb0, 0xdc, 0xde, 0x73, 0x70, - 0x1b, 0xe0, 0xf5, 0x99, 0x6c, 0xbe, 0x81, 0x79, 0x82, 0xd5, 0xd9, 0x85, 0x5f, 0x59, 0xd6, 0xe2, 0xdc, 0xe9, 0xa0, - 0x51, 0xaf, 0x28, 0x21, 0x6a, 0xf7, 0xb1, 0xf6, 0x39, 0x46, 0x58, 0xc4, 0xfb, 0x2b, 0x7c, 0xd7, 0xe3, 0x96, 0x7b, - 0x1a, 0x2d, 0xc2, 0x74, 0x95, 0x34, 0x06, 0x25, 0xeb, 0x7e, 0x32, 0xe2, 0x5e, 0xee, 0x8b, 0x58, 0x70, 0x85, 0x23, - 0xab, 0x42, 0x8a, 0x0d, 0x24, 0xe9, 0x69, 0x8f, 0x0e, 0xd8, 0x37, 0x9a, 0xbd, 0x80, 0x32, 0xef, 0x2b, 0x52, 0x49, - 0x48, 0x69, 0x76, 0x43, 0x24, 0x09, 0x6b, 0x45, 0x9e, 0x3a, 0xef, 0x3b, 0xda, 0xe7, 0x56, 0x12, 0xc1, 0x08, 0x4e, - 0xc2, 0x74, 0xac, 0x3c, 0x68, 0x0a, 0x70, 0x15, 0x1d, 0x31, 0x7d, 0x13, 0x90, 0xdf, 0x0c, 0xe4, 0xf6, 0x4a, 0x72, - 0x6d, 0xae, 0x61, 0x78, 0x82, 0x04, 0xab, 0x22, 0x11, 0x78, 0x44, 0x8d, 0x09, 0xc9, 0xeb, 0x3c, 0x0f, 0x30, 0xe1, - 0x6b, 0x7b, 0x13, 0x00, 0xca, 0xc9, 0x55, 0x71, 0x56, 0x02, 0xdd, 0x80, 0xe5, 0xfa, 0x38, 0x35, 0x2a, 0x12, 0x17, - 0x37, 0xa6, 0xab, 0x5b, 0xfa, 0x33, 0xb4, 0x9c, 0xc9, 0x10, 0xd3, 0x41, 0x10, 0x90, 0xa9, 0x8f, 0x99, 0x23, 0x64, - 0xae, 0xb0, 0x3e, 0xe7, 0x4e, 0x6d, 0xea, 0x1e, 0xa3, 0x6e, 0x9e, 0xa4, 0x16, 0xaf, 0xd3, 0xa6, 0x94, 0x88, 0x49, - 0x89, 0x39, 0x13, 0xa9, 0xd8, 0x4c, 0x89, 0x3b, 0xb7, 0xbe, 0xd1, 0x42, 0xda, 0x68, 0x33, 0x91, 0x83, 0xcd, 0x2a, - 0x79, 0x4f, 0x60, 0x3c, 0x17, 0x84, 0x2f, 0x91, 0xb1, 0x96, 0x63, 0xe6, 0x98, 0x08, 0x56, 0x2f, 0xa6, 0x22, 0x7f, - 0xe7, 0xe8, 0x34, 0x7b, 0x83, 0x1e, 0xa4, 0xde, 0x40, 0x62, 0xd6, 0xc4, 0x77, 0x21, 0x0d, 0x75, 0x84, 0x40, 0x65, - 0x54, 0xcb, 0x74, 0x9c, 0x58, 0x85, 0x6f, 0x04, 0x5f, 0xbd, 0xd5, 0xc7, 0xf9, 0xc6, 0x73, 0x63, 0x35, 0x82, 0x18, - 0xbc, 0x85, 0x7c, 0xe8, 0x49, 0x11, 0x0e, 0x84, 0xcb, 0x37, 0x37, 0x7b, 0xf9, 0x2e, 0xaf, 0x42, 0x24, 0x15, 0x8c, - 0x31, 0x66, 0x14, 0xe3, 0x9e, 0xa8, 0xa9, 0xc5, 0x1c, 0x06, 0x96, 0xad, 0xc3, 0x1c, 0x0f, 0x00, 0xa0, 0xa5, 0x29, - 0xbd, 0x6a, 0x2a, 0x54, 0x9e, 0xe7, 0x12, 0x3e, 0xd5, 0x21, 0xaa, 0x6a, 0xfc, 0x76, 0x7d, 0x06, 0x0a, 0xc1, 0x7d, - 0xa7, 0xe3, 0xe1, 0x21, 0x04, 0xac, 0xa2, 0x90, 0x05, 0x7a, 0x83, 0xf6, 0xaa, 0x44, 0x28, 0x66, 0x4e, 0xd6, 0x63, - 0x86, 0x93, 0x0a, 0xb6, 0x50, 0x09, 0x4b, 0xa5, 0x05, 0x7e, 0xb5, 0x11, 0x9a, 0xa7, 0x8c, 0x7b, 0xaf, 0x2a, 0x9c, - 0x41, 0x7f, 0x30, 0x6f, 0x95, 0x51, 0xdf, 0xae, 0x9c, 0xc8, 0x54, 0x60, 0xe2, 0x66, 0x96, 0xda, 0xef, 0x97, 0x75, - 0xda, 0xcf, 0x2b, 0xe4, 0x3e, 0x27, 0xcd, 0xd7, 0xb9, 0x85, 0xe6, 0x93, 0xe1, 0x7e, 0xa5, 0xfc, 0xd0, 0xc2, 0xa8, - 0x29, 0xbf, 0xbc, 0xae, 0xfc, 0x0a, 0x4f, 0x85, 0xb7, 0xfa, 0x5d, 0x14, 0xba, 0xa8, 0xcf, 0xc1, 0x10, 0xd2, 0x8f, - 0xe0, 0x1a, 0x1a, 0x3c, 0x28, 0x92, 0xc5, 0x62, 0xed, 0x82, 0xb8, 0x3e, 0xe6, 0x54, 0x3b, 0x94, 0x31, 0x46, 0x3c, - 0x2d, 0x39, 0x48, 0x32, 0x38, 0x18, 0xbf, 0x81, 0x01, 0x31, 0x29, 0x09, 0xe9, 0x10, 0x3a, 0x6b, 0x33, 0x11, 0x95, - 0xbb, 0x78, 0xb3, 0x71, 0x59, 0x53, 0x28, 0xc2, 0x4e, 0x30, 0x53, 0x29, 0x15, 0x04, 0xd2, 0xe4, 0xbb, 0xd3, 0xa9, - 0x05, 0x43, 0x0b, 0xd7, 0x54, 0x40, 0x5e, 0xdb, 0xf5, 0xa0, 0xc9, 0x7b, 0x8a, 0xa1, 0xaf, 0x53, 0x23, 0x5e, 0x66, - 0xf0, 0x35, 0x6c, 0xfe, 0x9a, 0x28, 0xc9, 0x43, 0x26, 0x62, 0xaf, 0xe0, 0x13, 0x21, 0x9b, 0x82, 0x9d, 0x09, 0xf4, - 0x43, 0xbb, 0xb2, 0x97, 0xee, 0x16, 0x95, 0x4b, 0x8b, 0xc6, 0x56, 0xa2, 0x66, 0xcd, 0x0f, 0xe3, 0xcd, 0x14, 0xf6, - 0xb3, 0x47, 0x09, 0x04, 0xa4, 0xa9, 0x9c, 0xa4, 0x9a, 0xf7, 0x30, 0x1d, 0x02, 0x48, 0xb0, 0xfb, 0x09, 0x2c, 0xf4, - 0x9b, 0x12, 0x13, 0x2c, 0xaa, 0xc6, 0x6e, 0x73, 0xd0, 0x9a, 0x73, 0xd2, 0x7c, 0x73, 0xd4, 0xda, 0x9b, 0xca, 0x7a, - 0xc6, 0xec, 0x00, 0xdb, 0x76, 0x37, 0x8b, 0xc3, 0x74, 0xb3, 0x33, 0x34, 0x04, 0x17, 0x1e, 0xff, 0x27, 0x25, 0xa6, - 0x81, 0xe4, 0x52, 0x37, 0x7e, 0x42, 0x1d, 0x86, 0xff, 0x2d, 0x49, 0x01, 0x0f, 0x6a, 0xab, 0xb1, 0xe2, 0xdc, 0x2b, - 0x8e, 0x92, 0xcb, 0xaa, 0xda, 0xd5, 0x12, 0x34, 0x74, 0x23, 0x19, 0x13, 0xc5, 0x3c, 0x27, 0x00, 0x46, 0xb1, 0xf9, - 0x53, 0xa6, 0x93, 0xbc, 0x7f, 0x59, 0x9b, 0xda, 0xed, 0xfb, 0x7e, 0x94, 0x9f, 0xd0, 0x91, 0x8a, 0xca, 0xe6, 0x24, - 0xe6, 0xdf, 0x16, 0x60, 0x9a, 0x13, 0x1f, 0xea, 0xb9, 0x86, 0xa1, 0x00, 0x5f, 0xd9, 0x50, 0x6a, 0xb6, 0x97, 0xbf, - 0x77, 0xb6, 0xfb, 0x92, 0x28, 0x82, 0x05, 0x1a, 0x74, 0xb9, 0x02, 0x5f, 0xc0, 0x32, 0xb8, 0x25, 0xfd, 0xf4, 0xa6, - 0xbf, 0x0a, 0x3e, 0x63, 0xff, 0x0b, 0x40, 0xab, 0x02, 0x03, 0xca, 0x9d, 0xa6, 0x61, 0x25, 0xc4, 0x25, 0x2a, 0xcc, - 0x2a, 0xce, 0x1f, 0xd7, 0x79, 0xdd, 0xb4, 0x2c, 0x31, 0x28, 0x3f, 0x77, 0x0d, 0x37, 0xbe, 0xd7, 0xc8, 0x1f, 0xdf, - 0x7b, 0x0e, 0xba, 0x9d, 0x48, 0x7b, 0xf7, 0x6e, 0x7e, 0x87, 0x2c, 0x34, 0xbc, 0x17, 0x36, 0x87, 0xb6, 0x48, 0x97, - 0x5c, 0x3d, 0x63, 0x31, 0xde, 0x16, 0xa1, 0x32, 0x7c, 0xc0, 0x82, 0x39, 0x60, 0x08, 0x1e, 0x3b, 0x95, 0xc9, 0x67, - 0xd8, 0x68, 0x8a, 0x5d, 0x73, 0x61, 0xf0, 0x81, 0xaa, 0x2c, 0x24, 0x2f, 0xd6, 0xc9, 0xf6, 0xec, 0x14, 0x9e, 0x5f, - 0xc6, 0x05, 0x50, 0x07, 0xd0, 0xaf, 0xa8, 0x2c, 0x36, 0x90, 0x8b, 0x9b, 0xb2, 0xd6, 0x2b, 0x1a, 0x8f, 0xaf, 0xed, - 0xc2, 0xea, 0x0a, 0x7c, 0x1a, 0xa5, 0xe3, 0x44, 0x4c, 0x62, 0x26, 0x55, 0xae, 0xc9, 0xb5, 0xd1, 0xbd, 0xb4, 0x45, - 0xf3, 0x5c, 0x48, 0xf0, 0x8a, 0xc0, 0x0d, 0xa1, 0xaf, 0xf4, 0xe5, 0x7a, 0x03, 0x05, 0x8f, 0xda, 0x9b, 0x8b, 0x60, - 0x62, 0xe2, 0x31, 0x43, 0x6a, 0xfa, 0x75, 0x38, 0x15, 0xdf, 0xfc, 0xb6, 0xe2, 0xf0, 0xeb, 0x9c, 0xb1, 0x86, 0x02, - 0x20, 0x3e, 0x79, 0x70, 0xb5, 0x9b, 0xf4, 0x4a, 0x69, 0x07, 0xa5, 0x11, 0xe2, 0xdb, 0x0a, 0x5f, 0x77, 0xa9, 0xf8, - 0xca, 0x55, 0xf7, 0xbe, 0x8e, 0x99, 0x71, 0xc1, 0xe8, 0x39, 0x9f, 0x25, 0x8d, 0x6b, 0x37, 0x74, 0x57, 0xe7, 0x47, - 0xef, 0x07, 0x99, 0xb7, 0x70, 0x0c, 0x6c, 0x72, 0xcc, 0x9c, 0xe7, 0xde, 0x6b, 0xe3, 0x44, 0xf9, 0x5b, 0xf3, 0x88, - 0x57, 0x0e, 0xb3, 0xee, 0x24, 0xf9, 0xdb, 0xc1, 0xb7, 0xc1, 0xd5, 0x2d, 0x8d, 0x13, 0xe4, 0xae, 0x3a, 0x41, 0x26, - 0xca, 0xcd, 0xf4, 0x86, 0xdb, 0xbb, 0xad, 0x40, 0x10, 0xa7, 0x62, 0xfa, 0xa8, 0x1c, 0xd7, 0x8f, 0x16, 0xa8, 0x54, - 0x44, 0x7c, 0xaa, 0x72, 0x57, 0xae, 0x4c, 0x0d, 0xf5, 0xb8, 0x4e, 0x66, 0xa1, 0x69, 0xd6, 0xe4, 0x52, 0x36, 0x3d, - 0x46, 0xa6, 0xd9, 0xa9, 0x36, 0xbf, 0x7b, 0xe5, 0x21, 0x1d, 0x43, 0x73, 0xb1, 0x56, 0x0b, 0xee, 0x77, 0x15, 0x85, - 0x77, 0xbd, 0xd8, 0x48, 0x65, 0xa8, 0x59, 0x8f, 0xa2, 0x8f, 0xe3, 0x36, 0x73, 0x79, 0x94, 0xfd, 0x59, 0x03, 0xc0, - 0x74, 0x84, 0x45, 0x77, 0xd3, 0x33, 0xf6, 0x04, 0x7a, 0x7a, 0x22, 0x83, 0x44, 0xaf, 0x74, 0xbe, 0x6a, 0x95, 0x58, - 0xba, 0x86, 0xc0, 0xee, 0x35, 0x19, 0xab, 0x92, 0x76, 0xab, 0xf5, 0xab, 0x79, 0x3e, 0x4f, 0xf9, 0x4a, 0x9e, 0x4f, - 0xcd, 0xa2, 0xbb, 0xd3, 0x76, 0xaf, 0x4f, 0x0d, 0x15, 0x73, 0xad, 0x6f, 0xf2, 0x3b, 0xa6, 0xeb, 0x60, 0xa8, 0x45, - 0x90, 0x59, 0xed, 0xaa, 0x67, 0x65, 0x39, 0xab, 0x67, 0x72, 0xcc, 0x84, 0x6f, 0x2a, 0xdd, 0x21, 0xba, 0x61, 0xaa, - 0x66, 0xfa, 0xb1, 0xb1, 0x2d, 0x64, 0x9b, 0xe7, 0x17, 0xe3, 0x1c, 0x28, 0x2d, 0xf7, 0x97, 0x09, 0xc3, 0x8f, 0x97, - 0x97, 0x3f, 0x0a, 0x39, 0x55, 0x75, 0xf4, 0x96, 0x2f, 0x75, 0xcf, 0x60, 0x56, 0x2a, 0x27, 0xe2, 0x23, 0x5b, 0x3f, - 0x78, 0x73, 0xf7, 0x0a, 0x58, 0x3e, 0x02, 0x76, 0x1f, 0x99, 0xd3, 0x18, 0xaa, 0xda, 0xc0, 0x3f, 0xac, 0x1f, 0x6c, - 0xdd, 0x1e, 0xfe, 0x61, 0xf0, 0x43, 0x70, 0x6d, 0x63, 0x63, 0x1b, 0x6f, 0xd7, 0x12, 0x41, 0x5e, 0xe1, 0x81, 0x3e, - 0x5e, 0x7d, 0x14, 0xb4, 0x5c, 0x27, 0xb6, 0x07, 0x0e, 0x85, 0xad, 0x41, 0xbe, 0x49, 0x99, 0x34, 0x5a, 0x14, 0x3c, - 0x9b, 0xc9, 0x19, 0x0a, 0x79, 0xcd, 0xc7, 0x41, 0xdb, 0x11, 0xfe, 0x06, 0x4e, 0xed, 0x78, 0x79, 0xf9, 0x09, 0xfa, - 0x80, 0xa7, 0x2b, 0xa5, 0xa9, 0x88, 0x53, 0xca, 0x2d, 0xba, 0x5a, 0xe7, 0xc1, 0x48, 0x71, 0x31, 0x45, 0xa5, 0xe3, - 0x2e, 0xaf, 0x9d, 0x8d, 0x9c, 0xfe, 0x12, 0xaf, 0x2e, 0xd2, 0xe5, 0x23, 0x91, 0xad, 0x5a, 0x7a, 0x2f, 0xf4, 0xe9, - 0xb6, 0x3d, 0x63, 0x7c, 0x9a, 0x8d, 0xe9, 0x60, 0xc6, 0xc7, 0x89, 0xf0, 0xfa, 0xc4, 0x58, 0xdf, 0x2d, 0x02, 0xd3, - 0xcd, 0xb1, 0xc9, 0x0f, 0xc7, 0xeb, 0xcd, 0x66, 0x8d, 0x3b, 0x78, 0xe3, 0x3c, 0x71, 0x96, 0x25, 0x46, 0x54, 0x96, - 0x1a, 0x1e, 0xd0, 0x0a, 0x71, 0xf3, 0x9e, 0x09, 0x8c, 0xcb, 0x2e, 0x48, 0x6a, 0xbb, 0x81, 0xc0, 0xc5, 0x9e, 0xc4, - 0x2c, 0x19, 0xdb, 0x1e, 0x94, 0x07, 0xfa, 0x62, 0x34, 0xdd, 0x02, 0xa6, 0xe5, 0xb5, 0xb3, 0xb3, 0xd4, 0xf6, 0xaa, - 0xa9, 0x02, 0x98, 0x25, 0xcb, 0xe3, 0x13, 0x64, 0xdd, 0x6f, 0xa0, 0x8b, 0x18, 0x30, 0x36, 0xae, 0xcc, 0xb9, 0xcb, - 0x75, 0x2b, 0xe2, 0x1b, 0x4d, 0xa4, 0x49, 0x7d, 0x48, 0x7d, 0x87, 0x61, 0xad, 0xae, 0x72, 0x90, 0xc0, 0x3d, 0xf2, - 0x6e, 0x89, 0x4b, 0x4f, 0x9f, 0x59, 0x4c, 0xaa, 0xf4, 0x2d, 0x75, 0x2d, 0xae, 0x19, 0xf6, 0x8a, 0x07, 0x60, 0x7f, - 0x60, 0xdc, 0x22, 0x16, 0xf1, 0x76, 0x5e, 0x4b, 0x61, 0x6d, 0xcc, 0x81, 0xe6, 0x86, 0x1b, 0xbc, 0x60, 0xd5, 0x9a, - 0x81, 0x19, 0x66, 0x9c, 0x91, 0xfc, 0x66, 0xdc, 0xab, 0x9a, 0x38, 0x72, 0x15, 0x40, 0xf4, 0x2d, 0xe9, 0x92, 0x1c, - 0x5e, 0xc9, 0x72, 0xd5, 0x19, 0xf2, 0xaf, 0xb0, 0xce, 0x7a, 0x71, 0x02, 0x66, 0xd2, 0x94, 0x97, 0x98, 0x98, 0x22, - 0x2e, 0x37, 0xcb, 0x98, 0xa7, 0xe9, 0xb3, 0x68, 0x07, 0x27, 0x37, 0x12, 0x38, 0x62, 0xdf, 0x58, 0x86, 0x66, 0xc2, - 0x46, 0x4c, 0xa4, 0x51, 0x29, 0x25, 0x7c, 0x20, 0x97, 0x5a, 0xf2, 0x97, 0xb9, 0xbc, 0xfa, 0x72, 0x9b, 0xe0, 0x80, - 0xbc, 0x06, 0x96, 0x43, 0xe3, 0xb8, 0x65, 0x20, 0x11, 0x8b, 0x01, 0x31, 0x6a, 0x55, 0xae, 0x26, 0xa3, 0x3a, 0x99, - 0xaf, 0x90, 0x0b, 0x15, 0x79, 0x70, 0x4b, 0xa0, 0xe4, 0xcf, 0x31, 0x75, 0x30, 0x2b, 0xb5, 0x9b, 0x16, 0x9b, 0x24, - 0xef, 0x99, 0x01, 0xc9, 0xf5, 0xd7, 0xf0, 0xd0, 0xf8, 0xc5, 0x2b, 0x73, 0x4a, 0xf8, 0xa2, 0x8c, 0xa5, 0xa5, 0x31, - 0x97, 0xfe, 0x83, 0xbc, 0x4f, 0x2b, 0x01, 0xfb, 0x15, 0xc4, 0x94, 0x81, 0x4b, 0x6c, 0x5c, 0x90, 0x94, 0xd7, 0xf2, - 0x94, 0xdd, 0xd7, 0x50, 0xbe, 0x2b, 0x26, 0x5d, 0xa5, 0xb2, 0xae, 0xb0, 0xea, 0x7e, 0x5d, 0xb0, 0xfc, 0x62, 0x9f, - 0x61, 0x6e, 0x32, 0x1a, 0x64, 0x2b, 0x66, 0x36, 0xe5, 0x57, 0x7b, 0xd7, 0x7e, 0xe5, 0xa1, 0xa4, 0x43, 0xb5, 0x4a, - 0x37, 0xaf, 0xdc, 0x70, 0x8c, 0x1b, 0x37, 0x1c, 0x01, 0x6c, 0x0c, 0x3b, 0x55, 0xa4, 0xd6, 0xf9, 0xef, 0xab, 0xe1, - 0x27, 0xda, 0x6b, 0x43, 0xbd, 0xeb, 0x86, 0x6b, 0xd3, 0xd3, 0xaf, 0x41, 0xd5, 0xc8, 0x12, 0xba, 0x0e, 0x55, 0x4c, - 0x46, 0xa2, 0xc4, 0x74, 0x95, 0xf2, 0xa8, 0xaf, 0x11, 0xe7, 0x20, 0x6e, 0x28, 0x7f, 0xf1, 0x2f, 0xe1, 0xc5, 0x51, - 0x80, 0x46, 0xd4, 0x72, 0x92, 0xa5, 0xbc, 0x35, 0x89, 0x66, 0x71, 0x72, 0x11, 0x2c, 0xe2, 0xd6, 0x2c, 0x4b, 0xb3, - 0x62, 0x0e, 0x5c, 0xe9, 0x15, 0x17, 0x60, 0xc3, 0xcf, 0x5a, 0x8b, 0xd8, 0x7b, 0xce, 0x92, 0x53, 0xc6, 0xe3, 0x51, - 0xe4, 0xd9, 0x7b, 0x39, 0x88, 0x07, 0xeb, 0x75, 0x94, 0xe7, 0xd9, 0x99, 0xed, 0xbd, 0xcb, 0x8e, 0x81, 0x69, 0xbd, - 0x37, 0xe7, 0x17, 0x27, 0x2c, 0xf5, 0xde, 0x1f, 0x2f, 0x52, 0xbe, 0xf0, 0x8a, 0x28, 0x2d, 0x5a, 0x05, 0xcb, 0xe3, - 0x09, 0xa8, 0x89, 0x24, 0xcb, 0x5b, 0x98, 0xff, 0x3c, 0x63, 0x41, 0x12, 0x9f, 0x4c, 0xb9, 0x35, 0x8e, 0xf2, 0x4f, - 0xbd, 0x56, 0x6b, 0x9e, 0xc7, 0xb3, 0x28, 0xbf, 0x68, 0x51, 0x8b, 0xe0, 0x8b, 0xf6, 0x76, 0xf4, 0xe5, 0xe4, 0x7e, - 0x8f, 0xe7, 0xd0, 0x37, 0x46, 0x2a, 0x06, 0x20, 0x7c, 0xac, 0xed, 0x9d, 0xf6, 0xac, 0xb8, 0x23, 0x4e, 0x94, 0xa2, - 0x94, 0x97, 0x47, 0xde, 0x19, 0x03, 0xb8, 0xfd, 0x63, 0x9e, 0x7a, 0xe0, 0xcb, 0xf1, 0x2c, 0x5d, 0x8e, 0x16, 0x79, - 0x01, 0x03, 0xcc, 0xb3, 0x38, 0xe5, 0x2c, 0xef, 0x1d, 0x67, 0x39, 0x90, 0xad, 0x95, 0x47, 0xe3, 0x78, 0x51, 0x04, - 0xf7, 0xe7, 0xe7, 0x3d, 0xb4, 0x15, 0x4e, 0xf2, 0x6c, 0x91, 0x8e, 0xe5, 0x5c, 0x71, 0x0a, 0x1b, 0x23, 0xe6, 0x66, - 0x05, 0x7d, 0x09, 0x05, 0xe0, 0x4b, 0x59, 0x94, 0xb7, 0x4e, 0xb0, 0x33, 0x1a, 0xfa, 0xed, 0x31, 0x3b, 0xf1, 0xf2, - 0x93, 0xe3, 0xc8, 0xe9, 0x74, 0x1f, 0x7a, 0xea, 0x9f, 0xbf, 0xe3, 0x82, 0xe1, 0xbe, 0xb6, 0xb8, 0xd3, 0x6e, 0xff, - 0xad, 0xdb, 0x6b, 0xcc, 0x42, 0x00, 0x05, 0x9d, 0xf9, 0xb9, 0x55, 0x64, 0x09, 0xac, 0xcf, 0xba, 0x9e, 0xbd, 0x39, - 0xf8, 0x4d, 0x71, 0x7a, 0x12, 0x74, 0xe7, 0xe7, 0x25, 0x62, 0x17, 0x88, 0x84, 0x4c, 0x89, 0xa4, 0x7c, 0x5b, 0xfe, - 0x5e, 0x88, 0x1f, 0xad, 0x87, 0xb8, 0xab, 0x20, 0xae, 0xa8, 0xde, 0x1a, 0xc3, 0x3e, 0x20, 0xf2, 0x77, 0x0a, 0x01, - 0xc8, 0x14, 0x9c, 0xc0, 0x5c, 0xc1, 0x41, 0x2f, 0xbf, 0x1b, 0x8c, 0xee, 0x7a, 0x30, 0x1e, 0xdd, 0x04, 0x46, 0x9e, - 0x8e, 0x97, 0xf5, 0x75, 0xed, 0x80, 0x73, 0xda, 0x9b, 0x32, 0xe4, 0xa7, 0xa0, 0x8b, 0xcf, 0x67, 0xf1, 0x98, 0x4f, - 0xc5, 0x23, 0xb1, 0xf3, 0x99, 0xa8, 0xdb, 0x69, 0xb7, 0xc5, 0x7b, 0x01, 0x0a, 0x2d, 0xe8, 0xf8, 0xd8, 0x00, 0x98, - 0xe8, 0xab, 0xab, 0x3e, 0x62, 0xf3, 0xdd, 0x8d, 0x5f, 0xaa, 0xf1, 0x2e, 0x54, 0xde, 0xa0, 0x50, 0x11, 0xea, 0x9b, - 0x2d, 0x98, 0xf1, 0x96, 0xf7, 0x3b, 0xfa, 0xa0, 0x6a, 0xf0, 0x1d, 0x23, 0xad, 0x17, 0x70, 0xcf, 0xcc, 0x05, 0xea, - 0xa5, 0x7d, 0x0c, 0x49, 0xb5, 0x5a, 0x2e, 0xe8, 0x0d, 0x86, 0x21, 0x24, 0x3a, 0x10, 0x74, 0xf2, 0x41, 0x41, 0xdf, - 0xd4, 0xc8, 0xdc, 0xa0, 0x70, 0x32, 0x17, 0xb6, 0x7c, 0xa6, 0xe5, 0x3a, 0x28, 0x69, 0xf0, 0xb2, 0xbf, 0x62, 0xb2, - 0x01, 0x48, 0xef, 0x4a, 0xd2, 0x7e, 0xaf, 0x4f, 0x9e, 0x94, 0xc7, 0x97, 0x8d, 0x88, 0x70, 0xe0, 0xea, 0xf3, 0x29, - 0xba, 0xdd, 0xfa, 0x3b, 0x31, 0x46, 0x46, 0xcd, 0x96, 0xed, 0x0e, 0x98, 0x4e, 0xca, 0xc2, 0xe4, 0x33, 0x56, 0xe2, - 0x28, 0x5f, 0xb3, 0xf0, 0x7b, 0x0c, 0xbc, 0xb2, 0x50, 0x78, 0x69, 0xca, 0x47, 0x9b, 0x5d, 0x77, 0xfb, 0x1f, 0x16, - 0x3c, 0xa6, 0x64, 0xe7, 0xc3, 0xe1, 0xbf, 0xc1, 0x67, 0x70, 0x34, 0x06, 0x45, 0xb6, 0xc8, 0x47, 0x14, 0x04, 0x5c, - 0x0d, 0x26, 0xd8, 0xa4, 0xcb, 0x6d, 0x8f, 0x69, 0x15, 0x82, 0x1e, 0x76, 0xed, 0xfb, 0x09, 0x9c, 0xce, 0x57, 0xc4, - 0xf5, 0x05, 0x19, 0x40, 0x51, 0x10, 0xa2, 0x56, 0x1c, 0x53, 0x2a, 0x1d, 0x5d, 0xed, 0xf4, 0x17, 0x69, 0x0c, 0x42, - 0xf4, 0x63, 0x3c, 0xa6, 0x2b, 0x2d, 0xf1, 0x98, 0xce, 0x38, 0x5a, 0x94, 0xd3, 0x84, 0x41, 0x73, 0x28, 0x90, 0xb4, - 0xc5, 0x67, 0x99, 0x23, 0x63, 0xb7, 0x6c, 0x3c, 0xa7, 0x30, 0xb4, 0xf0, 0x38, 0x9b, 0x45, 0x71, 0x1a, 0xe0, 0xa7, - 0x47, 0x3c, 0x3d, 0x62, 0x80, 0x5d, 0x3c, 0xf8, 0xa9, 0xc8, 0xdc, 0x71, 0xfd, 0x5f, 0x40, 0x44, 0x51, 0xff, 0x52, - 0xba, 0x78, 0x1a, 0x2e, 0x75, 0x82, 0x5c, 0x2f, 0x05, 0xb1, 0xc6, 0x95, 0x39, 0xcc, 0x28, 0x84, 0xb2, 0xcb, 0xe9, - 0xc7, 0xa0, 0xd5, 0x09, 0x3a, 0xda, 0x2b, 0xae, 0x5d, 0x74, 0x15, 0x69, 0x32, 0xf2, 0xb2, 0x24, 0xc1, 0xa0, 0x9f, - 0x05, 0x9c, 0xd5, 0xbb, 0x86, 0xd5, 0x93, 0x2c, 0x8f, 0xb1, 0xa1, 0x93, 0xd4, 0xa9, 0x01, 0x41, 0xc7, 0x0c, 0x57, - 0x4c, 0xe5, 0x96, 0x11, 0x31, 0xe1, 0x63, 0x92, 0x0d, 0xf5, 0x6b, 0x4a, 0x78, 0x25, 0xa9, 0x9e, 0x5d, 0xa5, 0xb3, - 0xa4, 0x8f, 0x76, 0x85, 0x30, 0xb1, 0x88, 0xc7, 0x42, 0x1b, 0x76, 0xb7, 0x6d, 0xfd, 0x79, 0x04, 0x54, 0xfa, 0x14, - 0xda, 0x1b, 0x4b, 0x47, 0xe5, 0xec, 0xe7, 0x30, 0xd7, 0x9e, 0x50, 0xa8, 0x74, 0xd9, 0xdf, 0xee, 0x6f, 0x2c, 0x79, - 0xb9, 0xbb, 0x25, 0x7a, 0xf7, 0x8f, 0xca, 0x82, 0x74, 0x9f, 0x19, 0xa3, 0xab, 0xa6, 0x10, 0x75, 0x30, 0x2c, 0x65, - 0x06, 0xe3, 0xb8, 0xb9, 0xb6, 0xe7, 0x44, 0x30, 0x5a, 0xb2, 0x5d, 0x81, 0x99, 0x50, 0x51, 0x0e, 0xdb, 0x5d, 0xe7, - 0xba, 0x14, 0x22, 0xbd, 0xa3, 0xb7, 0x0a, 0xc5, 0x11, 0x42, 0x30, 0xd8, 0x58, 0xc6, 0x65, 0xb8, 0xb1, 0x64, 0xe9, - 0x28, 0x1b, 0xb3, 0xf7, 0xef, 0x5e, 0xe0, 0x75, 0x86, 0x2c, 0x45, 0xb9, 0x97, 0xb9, 0xe5, 0x11, 0x18, 0x42, 0x08, - 0x69, 0xae, 0xbe, 0x26, 0x03, 0xc0, 0x88, 0x98, 0x8e, 0x45, 0xa3, 0x22, 0x28, 0xac, 0xb4, 0xad, 0x81, 0x80, 0x10, - 0x1c, 0x4e, 0x2c, 0x00, 0x53, 0x91, 0x7a, 0x31, 0xc0, 0x4f, 0xb4, 0xee, 0xc3, 0x40, 0xbb, 0x5b, 0xa2, 0x11, 0xe0, - 0x9a, 0x23, 0x1a, 0x15, 0xaa, 0x98, 0xfd, 0x63, 0xa2, 0x3b, 0x8e, 0x4f, 0x35, 0x39, 0x29, 0x15, 0xba, 0xbf, 0x9b, - 0x44, 0xc7, 0x2c, 0x81, 0x21, 0x8b, 0xcb, 0xcb, 0x36, 0x8c, 0x24, 0x5e, 0xad, 0xdd, 0x38, 0x9d, 0x2f, 0xe4, 0x57, - 0xbd, 0x60, 0xe2, 0x0e, 0x1e, 0xb4, 0xe2, 0xa5, 0x83, 0x81, 0x3a, 0x39, 0x0c, 0xe4, 0x00, 0x00, 0x22, 0x1d, 0x5a, - 0x20, 0x74, 0x15, 0xab, 0x40, 0x69, 0x3c, 0x5e, 0x2d, 0x83, 0xdd, 0x39, 0xc7, 0xd2, 0x14, 0x9e, 0x67, 0x71, 0x8a, - 0x8f, 0x05, 0x3e, 0x46, 0xe7, 0xf8, 0x98, 0xc1, 0xa3, 0xc6, 0x3d, 0x2f, 0xed, 0x7f, 0xd7, 0x55, 0xc9, 0xe4, 0x0a, - 0x58, 0x9a, 0x00, 0xd9, 0xe5, 0x25, 0xa8, 0x17, 0x4d, 0x82, 0xdd, 0x2d, 0x20, 0x16, 0x72, 0x8f, 0xf8, 0xc6, 0x0b, - 0x33, 0xc9, 0xc8, 0x8a, 0x79, 0x4b, 0x94, 0x5b, 0xa4, 0xc4, 0x43, 0xf0, 0xf1, 0x72, 0xa7, 0x61, 0xab, 0x78, 0x32, - 0x9b, 0xe5, 0x09, 0xbe, 0xb8, 0xb6, 0x25, 0x3e, 0xc2, 0x21, 0x88, 0x42, 0x8f, 0x88, 0xa1, 0x2e, 0xe3, 0xf2, 0xf3, - 0x3a, 0x71, 0x68, 0xe3, 0x2c, 0x60, 0x2e, 0xa2, 0xd2, 0xe1, 0x51, 0x9c, 0x88, 0xc6, 0x6b, 0xf0, 0x69, 0xa4, 0x25, - 0x12, 0x3a, 0xbb, 0x5b, 0x15, 0x6c, 0x00, 0xfc, 0x48, 0x5c, 0xea, 0x76, 0x44, 0xca, 0xa5, 0x2d, 0xca, 0xe9, 0xa8, - 0x5e, 0x6e, 0x73, 0x19, 0x48, 0x16, 0xa1, 0x79, 0x8d, 0x2a, 0xa5, 0x48, 0xda, 0x93, 0x28, 0x5d, 0xd7, 0x14, 0xa0, - 0x9f, 0x33, 0x36, 0xf6, 0x6c, 0x0b, 0xe4, 0xab, 0x78, 0xfe, 0x98, 0xb0, 0x53, 0x26, 0x3f, 0xcc, 0xa2, 0x07, 0xd1, - 0x95, 0x23, 0xb0, 0x00, 0xb8, 0xbc, 0x33, 0x2a, 0xd9, 0x53, 0xe1, 0x28, 0x29, 0x51, 0x47, 0xc4, 0xb3, 0x8d, 0x41, - 0x9b, 0x73, 0xb4, 0xeb, 0xc3, 0x7a, 0xa0, 0x93, 0x6c, 0x5b, 0xc0, 0x4b, 0x66, 0xe3, 0xcd, 0xc8, 0xc1, 0x00, 0xc7, - 0x39, 0x36, 0x4d, 0x59, 0x51, 0xac, 0x03, 0x0b, 0x9c, 0x60, 0xcf, 0xae, 0x9a, 0xd8, 0xb5, 0x0e, 0x00, 0x40, 0x77, - 0x67, 0x47, 0x4c, 0x0b, 0x15, 0x6c, 0x32, 0x81, 0x8d, 0x87, 0x1a, 0x23, 0xe1, 0x18, 0xf6, 0x0f, 0xfb, 0xf6, 0x6b, - 0xd8, 0xe3, 0x36, 0xf8, 0x57, 0xae, 0x3e, 0xc0, 0xa5, 0xe9, 0x95, 0x10, 0x32, 0xe6, 0x10, 0x9d, 0x8d, 0x61, 0xf4, - 0x93, 0x81, 0x54, 0x36, 0xfa, 0xb4, 0x06, 0x27, 0xe0, 0xc2, 0x0d, 0x11, 0x40, 0x6e, 0xc8, 0x56, 0xfb, 0x5f, 0xff, - 0xe9, 0x7f, 0xfd, 0x37, 0x18, 0x9b, 0xfa, 0xb9, 0xa5, 0x75, 0x75, 0xab, 0xff, 0x09, 0xad, 0x16, 0xe9, 0x0d, 0xed, - 0xfe, 0xfa, 0x0f, 0xff, 0x1d, 0x9a, 0xd1, 0x45, 0x23, 0x90, 0x59, 0x04, 0xd1, 0x08, 0x6d, 0xbb, 0xcf, 0x02, 0xa9, - 0x36, 0xc8, 0x95, 0x33, 0xfd, 0x23, 0x82, 0x5d, 0xf0, 0x6c, 0x7e, 0x2d, 0x38, 0x08, 0xf5, 0x28, 0xc9, 0x0a, 0xa6, - 0xe1, 0x11, 0x72, 0xfe, 0xf3, 0x00, 0xa2, 0xb9, 0xe6, 0xb0, 0x9b, 0x0a, 0x4b, 0x8f, 0x23, 0x56, 0x68, 0xdd, 0x38, - 0x8d, 0x05, 0x2c, 0x18, 0x27, 0x74, 0x28, 0x5c, 0x02, 0x4b, 0x26, 0x9e, 0xe0, 0x81, 0x04, 0x8f, 0x5b, 0xff, 0x78, - 0xd9, 0xfa, 0xc1, 0x34, 0xc3, 0x89, 0xb1, 0x44, 0x84, 0x48, 0x8d, 0x00, 0x3f, 0x41, 0x38, 0x7e, 0xd4, 0xcf, 0xd1, - 0xb9, 0x7e, 0x46, 0x01, 0x2a, 0x26, 0x00, 0x3d, 0x38, 0x43, 0x13, 0xc7, 0x9c, 0x41, 0x64, 0x37, 0x54, 0xee, 0xb1, - 0x91, 0x24, 0x23, 0x84, 0xe4, 0x47, 0xcc, 0x25, 0xc5, 0x9b, 0x43, 0x8b, 0x9c, 0x7d, 0x4c, 0xb2, 0x33, 0x8c, 0xb9, - 0x21, 0x91, 0xae, 0xaa, 0x2f, 0xff, 0xe5, 0x9f, 0x7d, 0xff, 0x5f, 0xfe, 0xf9, 0x8a, 0x06, 0x53, 0xd8, 0x13, 0x60, - 0x24, 0xf3, 0x50, 0xd3, 0xb9, 0x81, 0xd6, 0xfa, 0x41, 0x11, 0xcf, 0xf5, 0x35, 0x12, 0x71, 0x2c, 0x95, 0x78, 0xcb, - 0x47, 0x42, 0x5b, 0x33, 0xc5, 0xcd, 0xb3, 0x20, 0x64, 0x57, 0x4c, 0x83, 0x55, 0x37, 0xcc, 0x73, 0xe4, 0x06, 0xd7, - 0xd0, 0xe5, 0x33, 0x31, 0x5e, 0x0f, 0xc6, 0x8d, 0x10, 0x78, 0xa0, 0x65, 0x84, 0x1e, 0x7a, 0x22, 0xb4, 0x48, 0x20, - 0x96, 0x41, 0xea, 0x94, 0x1a, 0x40, 0x9e, 0x75, 0x40, 0x13, 0x50, 0x93, 0xb8, 0xd2, 0xe1, 0x64, 0x06, 0x1d, 0xe7, - 0xfd, 0x57, 0x78, 0x59, 0xd0, 0xc2, 0xde, 0xa8, 0xc1, 0x0b, 0x32, 0x38, 0x38, 0x19, 0x1c, 0x52, 0xd3, 0x99, 0xba, - 0x1e, 0x1d, 0xa7, 0x6c, 0xbd, 0x4e, 0xff, 0x88, 0xdd, 0x6b, 0x5a, 0x99, 0x4b, 0xad, 0x1c, 0x4b, 0x2b, 0x5a, 0x6a, - 0x65, 0xfc, 0x24, 0x4c, 0x43, 0x2b, 0xc7, 0x57, 0x6a, 0x65, 0xa4, 0xdc, 0x00, 0x47, 0x0e, 0xed, 0x4d, 0x8c, 0x0e, - 0x19, 0x36, 0x00, 0x47, 0xfb, 0x67, 0x34, 0x65, 0xa3, 0x4f, 0xd2, 0xfc, 0x21, 0x04, 0x30, 0x3c, 0xa2, 0x8d, 0x3c, - 0x81, 0x01, 0xa8, 0xf2, 0xa3, 0x52, 0x6f, 0x7a, 0x7c, 0x34, 0x26, 0xe0, 0xee, 0x72, 0xc2, 0x50, 0xf4, 0xc3, 0x9a, - 0x7d, 0xcd, 0xca, 0x2d, 0x1c, 0x47, 0x6c, 0x18, 0xf1, 0x0c, 0x98, 0x6d, 0xe1, 0x60, 0x47, 0xde, 0x52, 0x04, 0xdb, - 0x02, 0xfb, 0xed, 0x9b, 0xfd, 0x03, 0xdb, 0x3b, 0xce, 0xc6, 0x17, 0x81, 0x0d, 0xde, 0x0c, 0x58, 0x39, 0xae, 0xcf, - 0xa7, 0x2c, 0x75, 0x94, 0x3f, 0x91, 0x25, 0xe0, 0xaa, 0x65, 0x27, 0xe2, 0xdb, 0x10, 0xcd, 0x83, 0x02, 0x20, 0x2c, - 0x7d, 0x3c, 0xb2, 0xbf, 0xcb, 0xc5, 0x77, 0x55, 0x79, 0x8e, 0x8f, 0x7d, 0x4c, 0x95, 0xd8, 0xdd, 0x82, 0x07, 0x7c, - 0xd9, 0x47, 0xbd, 0xa7, 0xdf, 0x04, 0xb0, 0x85, 0x78, 0xdf, 0xc2, 0xf6, 0x5b, 0xaa, 0x2f, 0x42, 0xd1, 0x97, 0xdc, - 0xa6, 0x4d, 0xfe, 0xca, 0x66, 0xa4, 0xb1, 0xc7, 0x68, 0x12, 0x92, 0xc9, 0x0f, 0x24, 0xe1, 0x63, 0x5d, 0x22, 0xcc, - 0x0c, 0xa3, 0x88, 0x46, 0xa9, 0x8c, 0x02, 0x59, 0x85, 0x13, 0x92, 0x19, 0x29, 0x26, 0x83, 0x9f, 0x04, 0xfe, 0x91, - 0xf9, 0x1d, 0x34, 0xf1, 0xc9, 0x22, 0x8d, 0xe4, 0xe1, 0x5f, 0xbc, 0x33, 0xe6, 0x5d, 0x1c, 0x51, 0x4b, 0xe5, 0x74, - 0x63, 0x34, 0x08, 0x83, 0x13, 0x6d, 0x15, 0x5d, 0x01, 0x3b, 0x28, 0x89, 0xe6, 0x05, 0x0b, 0xd4, 0x83, 0xf4, 0xbf, - 0xd1, 0x8d, 0x5f, 0x0d, 0x78, 0x98, 0xf6, 0x52, 0xc9, 0xa7, 0x4b, 0xd3, 0x41, 0x7f, 0x00, 0x0e, 0x3a, 0x5e, 0x2e, - 0x68, 0x45, 0xa0, 0xe5, 0xd3, 0x20, 0x61, 0x13, 0x5e, 0x72, 0xbc, 0xbd, 0xbe, 0x54, 0x11, 0x11, 0xbf, 0xbb, 0x03, - 0x4e, 0xbb, 0xe5, 0xe3, 0xff, 0x37, 0x8d, 0x3d, 0x0e, 0x52, 0x70, 0xb2, 0xe9, 0x3a, 0x0b, 0x5e, 0x15, 0x04, 0x88, - 0xcc, 0xf7, 0xa5, 0x31, 0xd1, 0x88, 0x61, 0xb4, 0xa8, 0xe4, 0x39, 0xc8, 0x6d, 0x8f, 0xe7, 0x66, 0x3b, 0x90, 0xb7, - 0x2b, 0x21, 0xa3, 0xd5, 0xa0, 0xc5, 0xb6, 0x2b, 0xfd, 0x8f, 0xd5, 0xc6, 0x2a, 0xf2, 0x53, 0x7f, 0x5b, 0xa1, 0x90, - 0x11, 0xa3, 0x2a, 0x85, 0xaa, 0x59, 0x8a, 0x1e, 0x26, 0x4e, 0xab, 0xd1, 0xab, 0x1b, 0x2d, 0xd2, 0x92, 0xb6, 0xfd, - 0x21, 0x6d, 0x7b, 0x12, 0x63, 0xc3, 0xa5, 0x98, 0x7b, 0x14, 0x25, 0x23, 0x07, 0x01, 0xb0, 0x5a, 0xd6, 0x23, 0xa0, - 0xa6, 0xab, 0x22, 0x28, 0xfe, 0x43, 0x24, 0x6e, 0x29, 0x84, 0xde, 0x1a, 0x2a, 0x1d, 0x0d, 0xcb, 0xb2, 0x77, 0xc1, - 0x9c, 0xc3, 0xdf, 0xe4, 0x65, 0x08, 0x71, 0x07, 0x56, 0x7f, 0x47, 0xb0, 0x5d, 0xba, 0x43, 0x8f, 0x31, 0xe3, 0xeb, - 0x6c, 0xb6, 0xe2, 0x68, 0xdb, 0xeb, 0x52, 0x3c, 0x01, 0x7b, 0xbf, 0x72, 0x6c, 0x34, 0x62, 0xa9, 0xea, 0xa2, 0x45, - 0x1c, 0x66, 0x53, 0x47, 0x11, 0xcd, 0xff, 0xe6, 0xaa, 0xa0, 0xcc, 0xb7, 0x37, 0x07, 0x65, 0xf8, 0x2d, 0x83, 0x32, - 0xdf, 0xfe, 0xc1, 0x41, 0x99, 0x6f, 0xcc, 0xa0, 0x0c, 0xca, 0xca, 0x17, 0x9f, 0x13, 0x39, 0xc9, 0xb3, 0xb3, 0x22, - 0xec, 0xc8, 0x24, 0x00, 0x10, 0x3b, 0xff, 0x31, 0x21, 0x14, 0x98, 0xa8, 0x11, 0x40, 0xa1, 0x88, 0x89, 0xc8, 0x5b, - 0x04, 0x09, 0x2f, 0xe3, 0x15, 0x6d, 0x9d, 0x20, 0xd8, 0xba, 0xaf, 0x6e, 0x44, 0x81, 0x77, 0xe8, 0xea, 0xb0, 0x51, - 0x57, 0x45, 0x34, 0x02, 0xfa, 0xa4, 0xa9, 0xee, 0xd8, 0xdd, 0x54, 0x99, 0x69, 0xe6, 0x08, 0x3d, 0x75, 0xe0, 0x20, - 0x38, 0x68, 0x69, 0xff, 0xe7, 0xc3, 0x4e, 0x6f, 0xbb, 0x33, 0x83, 0xde, 0xa0, 0x4b, 0xe1, 0xad, 0xdd, 0xdb, 0xde, - 0xc6, 0xb7, 0x33, 0xf5, 0xd6, 0xc5, 0xb7, 0x58, 0xbd, 0xed, 0xe0, 0xdb, 0x48, 0xbd, 0x3d, 0xc0, 0xb7, 0xb1, 0x7a, - 0x7b, 0x88, 0x6f, 0xa7, 0x76, 0x79, 0xc8, 0x35, 0x70, 0x0f, 0x81, 0xb1, 0xc8, 0xb1, 0x08, 0x54, 0x19, 0xec, 0x5b, - 0xbc, 0x49, 0x18, 0x9d, 0x04, 0xb1, 0x27, 0x1c, 0xb0, 0x20, 0xf7, 0xce, 0x40, 0xf8, 0x07, 0x94, 0x38, 0xf7, 0x14, - 0x3f, 0x29, 0x01, 0xfe, 0xca, 0x41, 0x3c, 0x63, 0xea, 0xdb, 0xba, 0x0a, 0x6b, 0xb0, 0x25, 0x0f, 0xdb, 0xc3, 0xb2, - 0xa7, 0xd7, 0x49, 0x04, 0x6c, 0x54, 0x62, 0x02, 0xad, 0x5c, 0x55, 0x27, 0xa6, 0x6b, 0xe9, 0x15, 0xbe, 0x42, 0x95, - 0x18, 0x2e, 0xfb, 0x04, 0x6c, 0xa4, 0xd6, 0x39, 0x38, 0x79, 0x6b, 0xd5, 0x0b, 0x42, 0xa4, 0x15, 0x0a, 0xe1, 0xa4, - 0xdf, 0x0e, 0xa2, 0x13, 0xfd, 0xfc, 0x0a, 0x8c, 0xde, 0xe8, 0x84, 0xdd, 0xa4, 0x6a, 0x08, 0x44, 0x53, 0xcd, 0x28, - 0x20, 0xc8, 0x2a, 0x82, 0xa5, 0x41, 0x67, 0x53, 0xaa, 0x19, 0xa4, 0x4e, 0x5d, 0xf1, 0xd0, 0xf4, 0xf5, 0x22, 0xa0, - 0x68, 0x55, 0xb0, 0x0b, 0xb6, 0x37, 0x95, 0x0a, 0x0a, 0x43, 0x05, 0x16, 0x5c, 0xab, 0x8d, 0xb4, 0x3f, 0x7e, 0xa5, - 0x4e, 0xb2, 0x94, 0x3a, 0x32, 0x0f, 0x2a, 0xf4, 0x29, 0xc5, 0xaa, 0x84, 0xfc, 0xa2, 0x33, 0xc2, 0x3f, 0x52, 0xfe, - 0x7e, 0x31, 0x99, 0x4c, 0xae, 0x55, 0x4f, 0x5f, 0x8c, 0x27, 0xac, 0xcb, 0x76, 0x7a, 0x18, 0xc4, 0x6e, 0x49, 0x89, - 0xd8, 0x29, 0x89, 0x76, 0xcb, 0xdb, 0x35, 0x46, 0xe1, 0x09, 0x1a, 0xeb, 0xf6, 0x7a, 0xac, 0x04, 0xaa, 0x2c, 0x41, - 0x7e, 0x9f, 0xc4, 0x69, 0xd0, 0x2e, 0xfd, 0x53, 0x29, 0xf8, 0xbf, 0x78, 0xf4, 0xe8, 0x51, 0xe9, 0x8f, 0xd5, 0x5b, - 0x7b, 0x3c, 0x2e, 0xfd, 0xd1, 0x52, 0xa3, 0xd1, 0x6e, 0x4f, 0x26, 0xa5, 0x1f, 0xab, 0x82, 0xed, 0xee, 0x68, 0xbc, - 0xdd, 0x2d, 0xfd, 0x33, 0xa3, 0x45, 0xe9, 0x33, 0xf9, 0x96, 0xb3, 0x71, 0x2d, 0x12, 0xfe, 0xb0, 0x0d, 0x95, 0x82, - 0xd1, 0x96, 0xe8, 0xe8, 0x89, 0xc7, 0x20, 0x5a, 0xf0, 0x0c, 0x6c, 0x2c, 0xe0, 0x6d, 0x10, 0xd0, 0x13, 0x29, 0xde, - 0xc5, 0xa7, 0x6b, 0x51, 0xa8, 0xbf, 0x30, 0x65, 0x3a, 0x32, 0x33, 0xc9, 0x73, 0x4e, 0xaa, 0xa0, 0x59, 0x8d, 0x9c, - 0x45, 0xd5, 0x2f, 0x42, 0x5e, 0x49, 0x7b, 0x94, 0x36, 0xd8, 0x52, 0xc8, 0xf8, 0x1f, 0xaf, 0x92, 0xf1, 0x3f, 0xdc, - 0x2c, 0xe3, 0x8f, 0x6f, 0x27, 0xe2, 0x7f, 0xf8, 0x83, 0x45, 0xfc, 0x8f, 0xa6, 0x88, 0x17, 0x42, 0x6c, 0x0f, 0xac, - 0x58, 0x32, 0x5f, 0x8f, 0xb3, 0xf3, 0x16, 0x6e, 0x89, 0xdc, 0x26, 0xe9, 0xb9, 0x71, 0x2b, 0xe1, 0xbf, 0x26, 0xb5, - 0x49, 0x0d, 0x66, 0x7c, 0x07, 0x97, 0x67, 0x27, 0x27, 0x09, 0x53, 0x32, 0xde, 0xa8, 0x20, 0xcb, 0xf8, 0x4d, 0x1a, - 0xda, 0x6f, 0xc0, 0x49, 0x35, 0x4a, 0x26, 0x13, 0x28, 0x9a, 0x4c, 0x6c, 0x95, 0xfa, 0x0b, 0xf2, 0x8c, 0x5a, 0xbd, - 0xae, 0x95, 0x50, 0xab, 0xaf, 0xbe, 0x32, 0xcb, 0xcc, 0x02, 0x19, 0xf5, 0x32, 0xed, 0x09, 0x59, 0x33, 0x8e, 0x0b, - 0xdc, 0x83, 0xd5, 0x77, 0x7b, 0xd1, 0x64, 0x99, 0x81, 0x52, 0x89, 0x47, 0xf8, 0x41, 0x98, 0xe6, 0x37, 0x52, 0x44, - 0x9a, 0xf6, 0x2a, 0x72, 0xd5, 0x51, 0xae, 0xf1, 0x39, 0xbe, 0xea, 0xf8, 0x16, 0x16, 0x5f, 0xa6, 0x6a, 0x3c, 0xbe, - 0x78, 0x31, 0x76, 0xf6, 0xc0, 0x94, 0x8d, 0x8b, 0x37, 0x69, 0x23, 0x05, 0x4e, 0x80, 0x1d, 0x86, 0x26, 0xa6, 0xa5, - 0x20, 0x58, 0x75, 0x17, 0xa0, 0xaa, 0xec, 0x19, 0x9d, 0x64, 0xa6, 0x14, 0x0e, 0x39, 0xa8, 0x91, 0x25, 0x30, 0x07, - 0x93, 0xba, 0x90, 0xbe, 0xcb, 0x2e, 0xf2, 0x47, 0x4e, 0xe5, 0x07, 0xbc, 0xe9, 0xf4, 0x61, 0x29, 0xf5, 0x87, 0xcc, - 0x2c, 0xa8, 0x7a, 0x62, 0xc0, 0x5f, 0xcc, 0x30, 0x2e, 0x55, 0x90, 0x1f, 0x08, 0x37, 0xc7, 0xaf, 0x0d, 0x89, 0x21, - 0x54, 0x2c, 0xbd, 0xa2, 0xde, 0xe5, 0xa5, 0xf9, 0xd1, 0xcb, 0xda, 0x07, 0x12, 0x1b, 0x3c, 0xc0, 0xf0, 0x6b, 0xb5, - 0xa8, 0x0d, 0xb2, 0x05, 0x77, 0x1c, 0x6a, 0xe5, 0xb8, 0xa5, 0xb7, 0xd3, 0x6e, 0x83, 0x8a, 0xf1, 0xc5, 0x27, 0x8d, - 0x1c, 0xdd, 0x59, 0xe2, 0x7b, 0x55, 0xe8, 0x7e, 0xe5, 0x13, 0x63, 0x9a, 0xc4, 0xf8, 0x0d, 0x14, 0x81, 0xa8, 0x71, - 0x0d, 0x46, 0x2d, 0x62, 0xf3, 0xdd, 0x57, 0x6e, 0x9c, 0x41, 0x58, 0x77, 0x1d, 0x07, 0xcb, 0x34, 0xd1, 0x7a, 0x21, - 0xb6, 0x15, 0x56, 0xcd, 0x2a, 0x38, 0x37, 0xe8, 0xcc, 0xe2, 0xcc, 0x88, 0x71, 0xd7, 0xb6, 0x41, 0xa9, 0x82, 0xdc, - 0x22, 0x52, 0xbd, 0x87, 0xf1, 0x58, 0xe1, 0x03, 0x2b, 0xa0, 0xeb, 0xde, 0xa7, 0x01, 0x39, 0xfa, 0xa5, 0x9a, 0xd1, - 0x55, 0x95, 0x2a, 0x28, 0xcd, 0x53, 0x0a, 0x03, 0x19, 0x0a, 0x36, 0xc3, 0x1a, 0xa7, 0x42, 0x6f, 0xc1, 0x34, 0x24, - 0x80, 0xb5, 0x53, 0x86, 0x9e, 0x89, 0xad, 0xc0, 0x16, 0xd2, 0x02, 0x94, 0x1e, 0x76, 0xe8, 0x5b, 0x35, 0xd0, 0xd3, - 0xd5, 0x18, 0xf5, 0x75, 0x7e, 0xda, 0xc5, 0x91, 0x5f, 0x9c, 0x79, 0xf0, 0xcf, 0xfa, 0xd3, 0x12, 0xa4, 0xfc, 0xf1, - 0xa7, 0x98, 0x83, 0x51, 0x3d, 0x6f, 0x61, 0x24, 0x84, 0x42, 0xa6, 0x52, 0x1d, 0xd2, 0xa9, 0xaa, 0xb8, 0xfd, 0xd4, - 0x5b, 0x14, 0xe8, 0xce, 0x91, 0xdf, 0x12, 0xa4, 0x59, 0xca, 0x7a, 0xf5, 0xd3, 0x73, 0xd3, 0x75, 0x50, 0xc4, 0x1a, - 0x2e, 0x33, 0x74, 0xff, 0xf8, 0x05, 0xb8, 0x7f, 0x42, 0x8d, 0xb6, 0x95, 0xdf, 0xd0, 0x5e, 0xdb, 0x3e, 0x90, 0xb4, - 0xdd, 0x24, 0x6b, 0x21, 0x5f, 0xf5, 0x8f, 0xae, 0xf2, 0x6f, 0x6e, 0x3a, 0x4b, 0xc6, 0xf8, 0xac, 0xfa, 0x67, 0x1c, - 0xc2, 0x37, 0x8b, 0xe9, 0x2c, 0xf9, 0x36, 0x90, 0x05, 0xd1, 0x04, 0x3f, 0x16, 0x78, 0x9b, 0x96, 0xc7, 0x94, 0xc8, - 0xb9, 0x44, 0xb5, 0x1e, 0x74, 0x1e, 0x81, 0xc3, 0x76, 0xeb, 0xe1, 0xaf, 0x47, 0xbf, 0x94, 0x34, 0x52, 0xf7, 0x71, - 0x6d, 0xbb, 0x87, 0xf2, 0x22, 0x89, 0x2e, 0xc0, 0x6f, 0x24, 0x1b, 0xe3, 0x18, 0x03, 0xb9, 0xbd, 0x79, 0x26, 0x93, - 0x22, 0x72, 0x96, 0xd0, 0x6f, 0xe4, 0x90, 0x4b, 0xb1, 0xfd, 0x60, 0x7e, 0xae, 0x56, 0xa3, 0xd3, 0x48, 0x76, 0xf8, - 0x43, 0x73, 0x1a, 0xae, 0x4e, 0xa2, 0xa8, 0x9f, 0xcb, 0xef, 0x00, 0x0c, 0xc2, 0xb0, 0x69, 0xe5, 0x02, 0xaa, 0x36, - 0x94, 0x18, 0x59, 0x1d, 0xd5, 0x40, 0x96, 0xbf, 0x0d, 0xaa, 0x32, 0x2a, 0x58, 0x0f, 0xbf, 0xda, 0x18, 0x83, 0x77, - 0x2a, 0x8d, 0xa7, 0x59, 0x3c, 0x1e, 0x27, 0xac, 0xa7, 0xec, 0x23, 0xab, 0xf3, 0x00, 0x93, 0x22, 0xcc, 0x25, 0xab, - 0xaf, 0x8a, 0x41, 0x3c, 0x4d, 0xa7, 0xe8, 0x18, 0xec, 0x35, 0xfc, 0xf4, 0xe2, 0x5a, 0x72, 0xca, 0x6c, 0x81, 0x76, - 0x45, 0x3c, 0x7a, 0xae, 0xe3, 0xb2, 0x03, 0xc6, 0x22, 0x2d, 0x78, 0xbb, 0xc7, 0xb3, 0x79, 0xd0, 0xda, 0xae, 0x23, - 0x82, 0x55, 0x1a, 0x05, 0x6f, 0x0d, 0x5a, 0x1e, 0x5a, 0x07, 0x42, 0xcb, 0x59, 0x7e, 0x47, 0x96, 0xd1, 0x00, 0xf8, - 0x79, 0x3f, 0x5d, 0x54, 0xd6, 0x91, 0xf9, 0xf7, 0xd9, 0x2d, 0x5f, 0xae, 0xdf, 0x2d, 0x5f, 0xaa, 0xdd, 0x72, 0x3d, - 0xc7, 0x7e, 0x31, 0xe9, 0xe0, 0x9f, 0x5e, 0x85, 0x10, 0xac, 0x0a, 0x90, 0xc3, 0x42, 0xbb, 0xb8, 0xd5, 0x85, 0xff, - 0x68, 0xe8, 0xb6, 0x87, 0x7f, 0x7c, 0xb0, 0x00, 0xdb, 0x16, 0x16, 0xe2, 0xbf, 0x76, 0xad, 0xaa, 0x73, 0x1f, 0xeb, - 0xb0, 0xd7, 0xce, 0x6a, 0x5d, 0xf7, 0xfa, 0x4d, 0x0b, 0xf2, 0x8a, 0x3b, 0x81, 0x12, 0xc6, 0xe0, 0xaa, 0x45, 0xc7, - 0xc7, 0x50, 0x3a, 0xc9, 0x46, 0x8b, 0xe2, 0xef, 0x24, 0xfc, 0x92, 0x88, 0xd7, 0x6e, 0xe9, 0xc6, 0x38, 0xaa, 0xab, - 0xc8, 0xb0, 0x51, 0x23, 0x2c, 0xf5, 0x3a, 0x05, 0x05, 0x30, 0x26, 0x73, 0xba, 0xfe, 0xfd, 0x35, 0x9b, 0xe0, 0x3f, - 0x64, 0x6d, 0xd6, 0x22, 0xf3, 0x6f, 0x25, 0xc6, 0xb5, 0x44, 0xf8, 0x2c, 0x1a, 0x98, 0x6b, 0xd8, 0x7e, 0xb4, 0x1e, - 0xdc, 0x43, 0x35, 0xd3, 0x50, 0x29, 0x05, 0xa9, 0x77, 0xc0, 0x0b, 0x88, 0x16, 0x09, 0xbf, 0x7e, 0xd4, 0xab, 0x38, - 0x63, 0x65, 0xd4, 0x6b, 0x04, 0x7a, 0xd5, 0xf6, 0x96, 0x52, 0xfa, 0x8b, 0x2f, 0xef, 0xe3, 0x1f, 0x11, 0xfb, 0x3a, - 0xae, 0x7c, 0x23, 0x11, 0x1b, 0x40, 0xdf, 0x68, 0xa3, 0xe6, 0xfc, 0x08, 0x0d, 0x4e, 0xfe, 0xcf, 0x6d, 0x5b, 0xa3, - 0xb1, 0x7e, 0xab, 0xe6, 0xd2, 0x2a, 0xfd, 0xac, 0xd6, 0x9f, 0x37, 0xf8, 0x2d, 0xdb, 0x8e, 0x84, 0x43, 0x50, 0x6f, - 0x2b, 0x7f, 0x3b, 0xca, 0x4a, 0x63, 0x45, 0xf1, 0xdb, 0xb6, 0xaf, 0x4c, 0x62, 0xea, 0xb1, 0x11, 0x1e, 0x6b, 0x27, - 0x52, 0x9e, 0x6f, 0x63, 0x0f, 0xe1, 0x47, 0xfe, 0x85, 0x85, 0xf7, 0xf0, 0xc3, 0x62, 0xd6, 0xf9, 0x2c, 0x49, 0xc1, - 0xac, 0x9a, 0x72, 0x3e, 0x0f, 0xb6, 0xb6, 0xce, 0xce, 0xce, 0xfc, 0xb3, 0x6d, 0x3f, 0xcb, 0x4f, 0xb6, 0xba, 0xed, - 0x76, 0x1b, 0xbf, 0x07, 0x65, 0x5b, 0xa7, 0x31, 0x3b, 0x7b, 0x0c, 0xee, 0x87, 0xfd, 0xd0, 0x7a, 0x64, 0x3d, 0xdc, - 0xb6, 0x76, 0x1e, 0xd8, 0x16, 0x29, 0x00, 0x28, 0xd9, 0xb6, 0x2d, 0xa1, 0x00, 0x42, 0x1b, 0x8a, 0xfb, 0xbb, 0x27, - 0xca, 0x86, 0xc3, 0x7c, 0x7b, 0x61, 0x21, 0x81, 0xff, 0x96, 0x7d, 0x62, 0xf5, 0xad, 0x2e, 0xca, 0x5a, 0x52, 0x8d, - 0xa8, 0x57, 0xdc, 0xef, 0xa3, 0x68, 0x1e, 0x10, 0x1b, 0x99, 0x85, 0x18, 0x26, 0x13, 0xa5, 0x34, 0x05, 0xda, 0xa5, - 0xc7, 0xf0, 0x84, 0x19, 0x5a, 0x16, 0x3c, 0xbf, 0xea, 0x3e, 0x04, 0x1d, 0x77, 0xda, 0xba, 0x3f, 0x6a, 0xb7, 0x3a, - 0x56, 0xa7, 0xd5, 0xf5, 0x1f, 0x5a, 0x5d, 0xf1, 0x3f, 0xc8, 0xc8, 0x6d, 0xab, 0x03, 0x4f, 0xdb, 0x16, 0xbc, 0x9f, - 0xde, 0x17, 0xe9, 0x17, 0x91, 0xbd, 0xd5, 0xdf, 0xc5, 0x5f, 0x8f, 0x04, 0x48, 0x7d, 0x69, 0x8b, 0x5f, 0xe8, 0x66, - 0x7f, 0x61, 0x96, 0x76, 0x1e, 0xad, 0x2d, 0xee, 0x3e, 0x5c, 0x5b, 0xbc, 0xfd, 0x60, 0x6d, 0xf1, 0xfd, 0x9d, 0x7a, - 0xf1, 0xd6, 0x89, 0xa8, 0xd2, 0x72, 0x21, 0xb4, 0x67, 0x11, 0x30, 0xca, 0xb9, 0xd3, 0x01, 0x38, 0xdb, 0x56, 0x0b, - 0x7f, 0x3c, 0xec, 0xba, 0xba, 0xd7, 0x31, 0xf6, 0xd2, 0x58, 0x3e, 0x7c, 0x04, 0x58, 0x3e, 0xef, 0x3e, 0x18, 0x61, - 0x3b, 0x42, 0x14, 0xfe, 0x9d, 0x6e, 0x3f, 0x1a, 0x81, 0x46, 0xb0, 0xf0, 0x1f, 0xfc, 0x99, 0xee, 0x74, 0x47, 0xe2, - 0xa5, 0x8d, 0xf5, 0x1f, 0x3a, 0x0f, 0x0b, 0x68, 0x8a, 0x7f, 0x7e, 0xd3, 0x26, 0x34, 0x1a, 0xf0, 0xe6, 0xb8, 0xf7, - 0x81, 0x46, 0x8f, 0xa6, 0x5d, 0xff, 0xcb, 0xd3, 0x87, 0xfe, 0xa3, 0x69, 0xe7, 0xe1, 0x07, 0xf1, 0x96, 0x00, 0x05, - 0xbf, 0xc4, 0x7f, 0x1f, 0xb6, 0xdb, 0xd3, 0x56, 0xc7, 0x7f, 0x74, 0xba, 0xed, 0x6f, 0x27, 0xad, 0x07, 0xfe, 0x23, - 0xfc, 0x57, 0x0d, 0x37, 0xcd, 0x66, 0xcc, 0xb6, 0x70, 0xbd, 0x1b, 0x7e, 0xaf, 0x39, 0x47, 0xf7, 0xbe, 0xb5, 0x73, - 0xff, 0xf9, 0x23, 0x58, 0xa3, 0x69, 0xa7, 0x0b, 0xff, 0x5f, 0xf5, 0xf8, 0x01, 0x09, 0x2f, 0x07, 0x8e, 0x18, 0x66, - 0xca, 0x2a, 0xc2, 0xd1, 0xb7, 0xc9, 0xee, 0x79, 0xdf, 0x5f, 0x15, 0x00, 0x61, 0xfc, 0xe6, 0x20, 0x37, 0xbf, 0x5d, - 0x04, 0x84, 0x3e, 0x9c, 0xff, 0x07, 0x46, 0x40, 0xbe, 0x6f, 0x06, 0xb9, 0xcf, 0x57, 0xf3, 0x03, 0x9b, 0xce, 0xda, - 0x6b, 0xe6, 0x1c, 0xfe, 0x85, 0x0d, 0x31, 0x2b, 0x1c, 0x5a, 0x73, 0x6e, 0xc6, 0x83, 0x32, 0xdc, 0xc8, 0xe7, 0x32, - 0xea, 0x5f, 0xf0, 0x2b, 0x08, 0x12, 0xdf, 0x4c, 0x90, 0x5f, 0x6f, 0x47, 0x8f, 0xf8, 0x0f, 0xa6, 0x47, 0xc1, 0x0d, - 0x7a, 0xd4, 0x22, 0xee, 0x14, 0x31, 0x20, 0x47, 0x7f, 0x9f, 0xde, 0x9d, 0xef, 0xf1, 0xab, 0x62, 0x5b, 0x0c, 0x4b, - 0x0a, 0x5b, 0xe4, 0x24, 0xbe, 0xfb, 0x9c, 0x13, 0x02, 0x91, 0x38, 0x1d, 0xda, 0x32, 0x08, 0x33, 0xc7, 0xcf, 0xef, - 0xaa, 0x97, 0x53, 0x71, 0x39, 0x27, 0xa4, 0x9b, 0x75, 0x3b, 0x3a, 0x80, 0x83, 0xb9, 0xec, 0xe1, 0x32, 0xe3, 0x11, - 0xfe, 0x7e, 0x27, 0x1e, 0xf3, 0x04, 0xef, 0xfd, 0xca, 0x3b, 0x72, 0x98, 0x7a, 0xfd, 0x2d, 0xa6, 0x8d, 0xab, 0x83, - 0x82, 0x19, 0x06, 0x0d, 0x5e, 0xb1, 0x71, 0x1c, 0x39, 0xb6, 0x33, 0x87, 0x5d, 0x0b, 0x63, 0xb6, 0x6a, 0x39, 0xdb, - 0x94, 0xae, 0xed, 0xda, 0xea, 0x57, 0x0a, 0xe5, 0xf8, 0x89, 0xb6, 0xf0, 0x50, 0x06, 0x19, 0x6d, 0xe9, 0x05, 0xc0, - 0xf8, 0xaa, 0x24, 0x47, 0x81, 0x5f, 0x59, 0x0e, 0xb6, 0x30, 0x1d, 0x3a, 0x7e, 0x17, 0x5c, 0x09, 0x2a, 0xc6, 0x4f, - 0x5e, 0xfd, 0xe0, 0xb4, 0xb6, 0xc1, 0xb4, 0x31, 0xba, 0xe9, 0x81, 0x86, 0x2b, 0xa1, 0x24, 0x11, 0x20, 0x68, 0x94, - 0x7a, 0xfa, 0x77, 0xac, 0x55, 0x21, 0xa3, 0xe2, 0xf1, 0xc5, 0x81, 0xbc, 0xd6, 0x6e, 0x63, 0xf4, 0x96, 0xa2, 0xf6, - 0xd5, 0x27, 0xb5, 0x36, 0x41, 0x65, 0xd0, 0x2f, 0xba, 0xa4, 0x33, 0x70, 0xd4, 0x0a, 0x98, 0x55, 0x6e, 0x49, 0xef, - 0x21, 0xb4, 0x85, 0x4e, 0x18, 0xb3, 0xd3, 0x78, 0x24, 0x45, 0xbb, 0x67, 0xc9, 0xdb, 0x30, 0x2d, 0xc2, 0x22, 0xec, - 0x78, 0xc2, 0x7f, 0x86, 0x17, 0xd4, 0x6c, 0x61, 0x9a, 0xd9, 0xfd, 0x7b, 0x3d, 0x0d, 0x49, 0x3d, 0x21, 0xdf, 0xc6, - 0xdf, 0xba, 0x79, 0x08, 0xfe, 0xda, 0xdf, 0x85, 0xf7, 0xf0, 0xf7, 0x6e, 0xde, 0x1b, 0xda, 0xae, 0x4f, 0x82, 0xf1, - 0x5e, 0xf5, 0xcb, 0x37, 0x51, 0x2a, 0x6c, 0x82, 0x0e, 0xf3, 0x6e, 0xab, 0xcc, 0xa4, 0xe2, 0xea, 0xee, 0x54, 0x8a, - 0x0b, 0x9e, 0x0d, 0x49, 0x05, 0x42, 0xb4, 0xeb, 0xef, 0x18, 0xe2, 0xf0, 0xb4, 0x85, 0x3f, 0x6b, 0x02, 0xf1, 0x3e, - 0x34, 0x50, 0x12, 0xf1, 0x25, 0x34, 0xdf, 0x16, 0xc2, 0x17, 0xfa, 0xfd, 0x48, 0xe2, 0x4a, 0x88, 0xaa, 0x3a, 0xc7, - 0xac, 0x39, 0x48, 0x12, 0xf9, 0x02, 0xb6, 0x67, 0xc4, 0x9c, 0x04, 0xbb, 0xca, 0x88, 0xca, 0x53, 0xe8, 0xeb, 0xe8, - 0x2f, 0x55, 0xaf, 0xab, 0xf3, 0x6a, 0xbb, 0x67, 0xcd, 0x14, 0xc8, 0xf0, 0x8d, 0xc3, 0x2a, 0xba, 0x9d, 0x21, 0xbe, - 0xd8, 0x26, 0xb6, 0x72, 0xf5, 0x4d, 0xbb, 0x35, 0x59, 0xc0, 0xe6, 0xa6, 0x60, 0x15, 0xd3, 0xd0, 0xbe, 0xc0, 0xf4, - 0x19, 0xfc, 0x59, 0x15, 0xab, 0x07, 0xc9, 0x50, 0x7e, 0x12, 0xe1, 0x2f, 0x9c, 0xa1, 0x1f, 0x65, 0xb5, 0x01, 0x39, - 0x7d, 0x92, 0x93, 0x20, 0x7d, 0x31, 0x2e, 0x9b, 0x48, 0x80, 0xcd, 0x80, 0xbf, 0xbf, 0xb0, 0xba, 0x0d, 0x22, 0xaf, - 0x79, 0x62, 0x6a, 0xc1, 0x38, 0xce, 0xe9, 0xf6, 0xb0, 0xc2, 0xbf, 0x16, 0xd5, 0xac, 0x48, 0x4d, 0xbb, 0x92, 0x15, - 0x03, 0x1b, 0x8b, 0xec, 0x40, 0x26, 0xc0, 0x99, 0xdf, 0xa4, 0x36, 0xaf, 0x77, 0x8e, 0x45, 0xee, 0x1b, 0x7e, 0xb3, - 0xdf, 0x16, 0x44, 0xb6, 0x41, 0x94, 0x5d, 0x89, 0x13, 0x19, 0x38, 0x78, 0x2b, 0xb2, 0xfa, 0x25, 0x4c, 0xe6, 0x86, - 0xb7, 0xcd, 0xd5, 0xd2, 0xe3, 0xd2, 0x3a, 0xb8, 0x32, 0x86, 0x77, 0xcc, 0x22, 0xee, 0x47, 0x29, 0xe5, 0x29, 0x39, - 0x86, 0x58, 0xf0, 0x3a, 0x6c, 0xdb, 0x2d, 0x41, 0xf2, 0x18, 0xbf, 0xa5, 0x4a, 0x90, 0xde, 0x87, 0x42, 0x95, 0x4b, - 0xfd, 0x7d, 0x2d, 0x15, 0x78, 0xda, 0xed, 0xbf, 0x39, 0xd8, 0xb3, 0xc4, 0xc6, 0xde, 0xdd, 0x82, 0xd7, 0x5d, 0xf2, - 0x8e, 0x45, 0xc6, 0x46, 0x28, 0x32, 0x36, 0x2c, 0x91, 0xe7, 0x25, 0x52, 0x67, 0xb7, 0x04, 0xd6, 0xb6, 0xc5, 0xd2, - 0x91, 0x08, 0xeb, 0xcd, 0xc0, 0x83, 0x88, 0xf1, 0x33, 0x66, 0x5b, 0xd8, 0xb5, 0x85, 0x0b, 0x6f, 0xab, 0xe4, 0x17, - 0x65, 0x33, 0xf0, 0x54, 0x05, 0x01, 0x41, 0xd3, 0x33, 0x95, 0x07, 0x23, 0x87, 0xd2, 0x69, 0xb1, 0xab, 0xad, 0x8b, - 0xc5, 0xf1, 0x0c, 0xc4, 0x92, 0xca, 0x5d, 0x79, 0x2f, 0x3b, 0xec, 0xd2, 0x54, 0xfd, 0xa3, 0x72, 0x5d, 0x94, 0x72, - 0xda, 0xe9, 0xef, 0x46, 0xd2, 0x06, 0xc2, 0xbd, 0x5c, 0xc0, 0x66, 0x06, 0xd5, 0x87, 0x86, 0x86, 0x1f, 0x67, 0x5b, - 0x67, 0xec, 0xb8, 0x15, 0xcd, 0xe3, 0x2a, 0x24, 0x88, 0x1a, 0xb1, 0xbf, 0xab, 0x94, 0xa3, 0x4c, 0xf5, 0x94, 0x8f, - 0x91, 0x91, 0xdc, 0x81, 0x84, 0x24, 0x86, 0x2d, 0x65, 0xbc, 0x91, 0x0c, 0x49, 0x58, 0x0c, 0x00, 0x96, 0xf8, 0x59, - 0xc5, 0x25, 0xa5, 0x66, 0x28, 0xed, 0xfe, 0x5f, 0xff, 0xf7, 0xff, 0x91, 0xa1, 0x46, 0xa0, 0x2d, 0x80, 0x85, 0xd9, - 0x31, 0xd5, 0xa9, 0x23, 0x3b, 0x07, 0xe7, 0x34, 0x1e, 0xb7, 0xa6, 0x51, 0x32, 0x01, 0x08, 0x0a, 0x26, 0xee, 0x14, - 0xc8, 0x7a, 0xe0, 0x0a, 0x09, 0x96, 0x79, 0x62, 0x2f, 0xc1, 0xab, 0x17, 0xe1, 0xb2, 0xfd, 0xae, 0xdc, 0x59, 0x95, - 0xb3, 0x4c, 0x0c, 0x6e, 0x64, 0xd2, 0x1a, 0x3c, 0x58, 0xcb, 0xa6, 0x55, 0xbf, 0x13, 0x4a, 0x0a, 0x13, 0x56, 0x4b, - 0xa5, 0x85, 0x96, 0xfa, 0x70, 0xe4, 0x5f, 0xff, 0xe9, 0xbf, 0xfc, 0x0f, 0xf5, 0x8a, 0x67, 0x1e, 0x7f, 0xfd, 0xc7, - 0xbf, 0xff, 0x7f, 0xff, 0xf7, 0xbf, 0x62, 0xa6, 0xb2, 0x3c, 0x17, 0xa1, 0xad, 0x65, 0x55, 0x87, 0x22, 0x62, 0x8f, - 0x59, 0x95, 0x13, 0x52, 0x4f, 0xb9, 0xdd, 0xa7, 0x09, 0x89, 0x41, 0x25, 0x74, 0xc4, 0xe7, 0x94, 0xa2, 0x4d, 0x54, - 0xbb, 0x86, 0x7c, 0xb0, 0x94, 0x16, 0x1d, 0xf5, 0xdb, 0x3b, 0x6d, 0xbb, 0x5a, 0xde, 0xbe, 0xd1, 0x77, 0x0b, 0x17, - 0xe6, 0x56, 0x89, 0x39, 0xbe, 0x5e, 0xb6, 0xa5, 0x0a, 0x6d, 0x61, 0x49, 0x59, 0x95, 0x5b, 0x18, 0x73, 0x5e, 0xe2, - 0x6b, 0xd0, 0x35, 0x8a, 0x69, 0x95, 0x6b, 0x7d, 0x7a, 0xbf, 0x2c, 0x00, 0xd1, 0x09, 0x2e, 0x8d, 0x08, 0xa0, 0xd1, - 0x79, 0x6a, 0x0b, 0xad, 0x95, 0xe4, 0xa2, 0xa4, 0x51, 0x84, 0x87, 0x73, 0xff, 0xd1, 0xdf, 0x96, 0x7f, 0x9e, 0xa1, - 0x95, 0x60, 0x39, 0xb3, 0xe8, 0x5c, 0xfa, 0x3d, 0x0f, 0xda, 0xed, 0xf9, 0xb9, 0xbb, 0xac, 0x66, 0xf0, 0xae, 0x9a, - 0x8c, 0x82, 0x6e, 0xe6, 0x80, 0x74, 0x10, 0xab, 0xe3, 0x7b, 0x60, 0xea, 0xb7, 0x31, 0x1c, 0x54, 0x96, 0x7f, 0x5a, - 0x52, 0x88, 0x29, 0xfe, 0x0d, 0x0f, 0x4c, 0x65, 0x34, 0x0e, 0x4a, 0x0c, 0x2c, 0x96, 0x46, 0xaf, 0xae, 0xe8, 0x35, - 0xed, 0xac, 0xa6, 0xac, 0x98, 0x07, 0xbe, 0xe6, 0x51, 0xed, 0x7d, 0x3c, 0x7c, 0x9d, 0x76, 0xbc, 0x69, 0x77, 0xa9, - 0x87, 0xe7, 0x3c, 0x9b, 0x99, 0x27, 0xbc, 0x2c, 0x62, 0x23, 0x36, 0x51, 0x51, 0x4c, 0x59, 0x2f, 0x4e, 0x6f, 0xcb, - 0x2f, 0x70, 0xbb, 0x01, 0x6d, 0xb3, 0x88, 0x07, 0xc4, 0xb4, 0x3d, 0xf3, 0x0c, 0x38, 0xc2, 0xd3, 0xf5, 0x6c, 0x69, - 0xcc, 0xd5, 0x13, 0x4d, 0x31, 0x56, 0x58, 0x4f, 0x07, 0x2a, 0x7d, 0xea, 0x6e, 0x0e, 0x25, 0x42, 0x0d, 0xbf, 0xca, - 0xa3, 0xd5, 0x77, 0x35, 0x1f, 0x5d, 0x8a, 0x66, 0x70, 0x8b, 0xd7, 0xd6, 0x0b, 0x35, 0x29, 0x6a, 0x3f, 0x80, 0xf5, - 0x43, 0x60, 0xda, 0xcd, 0x56, 0x54, 0x88, 0xad, 0xde, 0x85, 0xbf, 0x6a, 0x7b, 0x3c, 0x9a, 0xcf, 0xa9, 0xa1, 0x0b, - 0xdc, 0x48, 0x76, 0x35, 0x4a, 0x0a, 0x4a, 0x1b, 0x10, 0xa7, 0xf4, 0xb2, 0x8d, 0x64, 0x5b, 0xf1, 0x24, 0xcf, 0xef, - 0xe9, 0x57, 0x8c, 0xff, 0x7f, 0x2a, 0xa5, 0xd0, 0x17, 0x78, 0x7c, 0x00, 0x00}; - -} // namespace web_server -} // namespace esphome diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h new file mode 100644 index 0000000000..c9932624ba --- /dev/null +++ b/esphome/components/web_server/server_index_v2.h @@ -0,0 +1,646 @@ +#pragma once +// Generated from https://github.com/esphome/esphome-webserver + +#ifdef USE_WEBSERVER_LOCAL +#if USE_WEBSERVER_VERSION == 2 + +#include "esphome/core/hal.h" + +namespace esphome { +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, 0xba, 0x58, 0x06, 0xd5, 0xe4, 0x96, 0x65, 0x67, 0x3b, + 0x3b, 0xbe, 0xc5, 0xb2, 0x93, 0x9d, 0x30, 0xdc, 0x12, 0x44, 0x34, 0xc9, 0xb6, 0x41, 0x34, 0x03, 0x34, 0x29, 0x29, + 0x24, 0x4e, 0xcd, 0x07, 0x4c, 0xd5, 0x54, 0xcd, 0xd3, 0xbc, 0x4c, 0xcd, 0x79, 0x98, 0x8f, 0x98, 0xe7, 0xf3, 0x29, + 0xe7, 0x07, 0x66, 0x3e, 0x61, 0x6a, 0xf5, 0x05, 0x68, 0xf0, 0x22, 0xcb, 0x49, 0xce, 0x39, 0x53, 0xa9, 0x58, 0x44, + 0x5f, 0x57, 0xaf, 0x5e, 0xbd, 0xee, 0x0d, 0x9c, 0xee, 0x44, 0x7c, 0x20, 0xee, 0xa6, 0xd4, 0x19, 0x8b, 0x49, 0xdc, + 0x39, 0xd5, 0xff, 0xd2, 0x30, 0xea, 0x9c, 0xc6, 0x2c, 0xf9, 0xe8, 0xa4, 0x34, 0x26, 0x6c, 0xc0, 0x13, 0x67, 0x9c, + 0xd2, 0x21, 0x89, 0x42, 0x11, 0x06, 0x6c, 0x12, 0x8e, 0xa8, 0xb3, 0xdf, 0x39, 0x9d, 0x50, 0x11, 0x3a, 0x83, 0x71, + 0x98, 0x66, 0x54, 0x90, 0xf7, 0xef, 0xbe, 0xae, 0x9f, 0x74, 0x4e, 0xb3, 0x41, 0xca, 0xa6, 0xc2, 0x81, 0x21, 0xc9, + 0x84, 0x47, 0xb3, 0x98, 0x76, 0xf6, 0xf7, 0x6f, 0x6e, 0x6e, 0xfc, 0x0f, 0xd9, 0x17, 0x03, 0x9e, 0x64, 0xc2, 0x79, + 0x41, 0x6e, 0x58, 0x12, 0xf1, 0x1b, 0x4c, 0x05, 0x79, 0xe1, 0x5f, 0x8c, 0xc3, 0x88, 0xdf, 0xbc, 0xe5, 0x5c, 0xec, + 0xed, 0x79, 0xea, 0xf1, 0xee, 0xfc, 0xe2, 0x82, 0x10, 0x32, 0xe7, 0x2c, 0x72, 0x1a, 0xcb, 0x65, 0x59, 0xe8, 0x27, + 0xa1, 0x60, 0x73, 0xaa, 0xba, 0xa0, 0xbd, 0x3d, 0x37, 0x8c, 0xf8, 0x54, 0xd0, 0xe8, 0x42, 0xdc, 0xc5, 0xf4, 0x62, + 0x4c, 0xa9, 0xc8, 0x5c, 0x96, 0x38, 0x4f, 0xf9, 0x60, 0x36, 0xa1, 0x89, 0xf0, 0xa7, 0x29, 0x17, 0x1c, 0x20, 0xd9, + 0xdb, 0x73, 0x53, 0x3a, 0x8d, 0xc3, 0x01, 0x85, 0xfa, 0xf3, 0x8b, 0x8b, 0xb2, 0x47, 0xd9, 0x08, 0x33, 0x41, 0x2e, + 0xee, 0x26, 0xd7, 0x3c, 0xf6, 0x10, 0x8e, 0x05, 0x49, 0xe8, 0x8d, 0xf3, 0x03, 0x0d, 0x3f, 0xbe, 0x0c, 0xa7, 0xed, + 0x41, 0x1c, 0x66, 0x99, 0x73, 0x2d, 0x16, 0x72, 0x09, 0xe9, 0x6c, 0x20, 0x78, 0xea, 0x09, 0x4c, 0x31, 0x43, 0x0b, + 0x36, 0xf4, 0xc4, 0x98, 0x65, 0xfe, 0xe5, 0xee, 0x20, 0xcb, 0xde, 0xd2, 0x6c, 0x16, 0x8b, 0x5d, 0xb2, 0xd3, 0xc0, + 0x6c, 0x87, 0x10, 0x26, 0x90, 0x18, 0xa7, 0xfc, 0xc6, 0x79, 0x96, 0xa6, 0x3c, 0xf5, 0xdc, 0xf3, 0x8b, 0x0b, 0xd5, + 0xc2, 0x61, 0x99, 0x93, 0x70, 0xe1, 0x14, 0xe3, 0x85, 0xd7, 0x31, 0xf5, 0x9d, 0xf7, 0x19, 0x75, 0xae, 0x66, 0x49, + 0x16, 0x0e, 0xe9, 0xf9, 0xc5, 0xc5, 0x95, 0xc3, 0x53, 0xe7, 0x6a, 0x90, 0x65, 0x57, 0x0e, 0x4b, 0x32, 0x41, 0xc3, + 0xc8, 0x77, 0x51, 0x5b, 0x4e, 0x36, 0xc8, 0xb2, 0x77, 0xf4, 0x56, 0x10, 0x81, 0xe5, 0xa3, 0x20, 0x34, 0x1f, 0x51, + 0xe1, 0x64, 0xc5, 0xba, 0x3c, 0xb4, 0x88, 0xa9, 0x70, 0x04, 0x91, 0xf5, 0xbc, 0xad, 0x70, 0x4f, 0xd5, 0xa3, 0x68, + 0xb3, 0xa1, 0x47, 0xc5, 0xde, 0x9e, 0x28, 0xf0, 0x8c, 0xd4, 0xd2, 0x1c, 0x46, 0xe8, 0x8e, 0x29, 0xdb, 0xdb, 0xa3, + 0x7e, 0x4c, 0x93, 0x91, 0x18, 0x13, 0x42, 0x9a, 0x6d, 0xb6, 0xb7, 0xe7, 0x09, 0x12, 0x0b, 0x7f, 0x44, 0x85, 0x47, + 0x11, 0xc2, 0x65, 0xef, 0xbd, 0x3d, 0x4f, 0x21, 0x81, 0x13, 0x85, 0xb8, 0x0a, 0x8e, 0x91, 0xaf, 0xb1, 0x7f, 0x71, + 0x97, 0x0c, 0x3c, 0x1b, 0x7e, 0x84, 0xd9, 0xde, 0x5e, 0x2c, 0xfc, 0x0c, 0x46, 0xc4, 0x02, 0xa1, 0x3c, 0xa5, 0x62, + 0x96, 0x26, 0x8e, 0xc8, 0x05, 0xbf, 0x10, 0x29, 0x4b, 0x46, 0x1e, 0x5a, 0x98, 0x32, 0xab, 0x63, 0x9e, 0x2b, 0x70, + 0xdf, 0x08, 0x92, 0x92, 0x0e, 0xcc, 0x78, 0x2d, 0x3c, 0xd8, 0x45, 0x3e, 0x74, 0x52, 0x42, 0xdc, 0x4c, 0xf6, 0x75, + 0xbb, 0x69, 0x90, 0xd6, 0x5c, 0x17, 0x2b, 0x28, 0x31, 0x13, 0x08, 0x7f, 0x24, 0x5e, 0x8a, 0x7d, 0xdf, 0x17, 0x88, + 0x74, 0x16, 0x06, 0x2b, 0xa9, 0xb5, 0xce, 0x6e, 0xda, 0x6b, 0xf4, 0x03, 0xe1, 0xa7, 0x34, 0x9a, 0x0d, 0xa8, 0xe7, + 0x31, 0x9c, 0xe1, 0x04, 0x91, 0x0e, 0xab, 0x79, 0x9c, 0x74, 0x60, 0xbb, 0x79, 0x75, 0xaf, 0x09, 0xd9, 0x69, 0x20, + 0x0d, 0x23, 0x37, 0x00, 0x02, 0x86, 0x35, 0x3c, 0x9c, 0x10, 0x37, 0x99, 0x4d, 0xae, 0x69, 0xea, 0x16, 0xcd, 0xda, + 0x15, 0xb2, 0x98, 0x65, 0xd4, 0x19, 0x64, 0x99, 0x33, 0x9c, 0x25, 0x03, 0xc1, 0x78, 0xe2, 0xb8, 0x35, 0x5e, 0x73, + 0x15, 0x39, 0x14, 0xd4, 0xe0, 0xa2, 0x1c, 0x79, 0x19, 0xaa, 0xa5, 0xbd, 0xa4, 0xd6, 0xec, 0x63, 0x80, 0x12, 0xb5, + 0xf5, 0x78, 0x1a, 0x01, 0x14, 0xa7, 0xb0, 0xc6, 0x1c, 0xbf, 0x17, 0xb0, 0x4a, 0xb9, 0x44, 0x2a, 0xba, 0xa9, 0xbf, + 0x7e, 0x50, 0x88, 0xf0, 0x27, 0xe1, 0xd4, 0xa3, 0xa4, 0x43, 0x25, 0x71, 0x85, 0xc9, 0x00, 0x60, 0xad, 0xec, 0x5b, + 0x97, 0x06, 0xd4, 0x2f, 0x49, 0x0a, 0x05, 0xc2, 0x1f, 0xf2, 0xf4, 0x59, 0x38, 0x18, 0x43, 0xbf, 0x82, 0x60, 0x22, + 0x73, 0xde, 0x06, 0x29, 0x0d, 0x05, 0x7d, 0x16, 0x53, 0x78, 0xf2, 0x5c, 0xd9, 0xd3, 0x45, 0x38, 0x23, 0x2f, 0xfc, + 0x98, 0x89, 0x57, 0x3c, 0x19, 0xd0, 0x76, 0x66, 0x51, 0x17, 0x83, 0x7d, 0x3f, 0x13, 0x22, 0x65, 0xd7, 0x33, 0x41, + 0x3d, 0x37, 0x81, 0x16, 0x2e, 0xce, 0x10, 0x66, 0xbe, 0xa0, 0xb7, 0xe2, 0x9c, 0x27, 0x82, 0x26, 0x82, 0x50, 0x83, + 0x54, 0x9c, 0xfa, 0xe1, 0x74, 0x4a, 0x93, 0xe8, 0x7c, 0xcc, 0xe2, 0xc8, 0x63, 0x28, 0x47, 0x39, 0x0e, 0x05, 0x81, + 0x35, 0x92, 0x4e, 0x1a, 0xc0, 0x3f, 0xdb, 0x57, 0xe3, 0x09, 0xd2, 0x91, 0x87, 0x82, 0x12, 0xd7, 0x6d, 0x0f, 0x79, + 0xea, 0xe9, 0x15, 0x38, 0x7c, 0xe8, 0x08, 0x98, 0xe3, 0xed, 0x2c, 0xa6, 0x19, 0xa2, 0x35, 0xc2, 0x8a, 0x6d, 0xd4, + 0x08, 0x7e, 0x03, 0x14, 0x9f, 0x23, 0x2f, 0x45, 0x41, 0xda, 0x9e, 0x87, 0xa9, 0xf3, 0xb5, 0x3e, 0x51, 0x4f, 0x0d, + 0x37, 0x1b, 0x0b, 0xf2, 0xd4, 0x17, 0xe9, 0x2c, 0x13, 0x34, 0x7a, 0x77, 0x37, 0xa5, 0x19, 0x7e, 0x27, 0xc8, 0x58, + 0x74, 0xc7, 0xc2, 0xa7, 0x93, 0xa9, 0xb8, 0xbb, 0x90, 0x8c, 0x31, 0x70, 0x5d, 0x3c, 0x80, 0x96, 0x29, 0x0d, 0x07, + 0xc0, 0xcc, 0x34, 0xb6, 0xde, 0xf0, 0xf8, 0x6e, 0xc8, 0xe2, 0xf8, 0x62, 0x36, 0x9d, 0xf2, 0x54, 0xe0, 0xbf, 0x92, + 0x85, 0xe0, 0x25, 0x6a, 0x60, 0x2f, 0x17, 0xd9, 0x0d, 0x13, 0x83, 0xb1, 0x27, 0xd0, 0x62, 0x10, 0x66, 0xd4, 0x79, + 0xc2, 0x79, 0x4c, 0xc3, 0x24, 0x48, 0x49, 0xda, 0x7d, 0x27, 0x82, 0x64, 0x16, 0xc7, 0xed, 0xeb, 0x94, 0x86, 0x1f, + 0xdb, 0xb2, 0xfa, 0xf5, 0xf5, 0x07, 0x3a, 0x10, 0x81, 0xfc, 0x7d, 0x96, 0xa6, 0xe1, 0x1d, 0x34, 0x24, 0x04, 0x9a, + 0x75, 0xd3, 0xe0, 0x6f, 0x17, 0xaf, 0x5f, 0xf9, 0xea, 0x90, 0xb0, 0xe1, 0x9d, 0x97, 0x16, 0x07, 0x2f, 0xcd, 0xf1, + 0x30, 0xe5, 0x93, 0x95, 0xa9, 0x15, 0xd6, 0xd2, 0xf6, 0x16, 0x10, 0x28, 0x49, 0x77, 0xd4, 0xd0, 0x36, 0x04, 0xaf, + 0x24, 0xcd, 0x43, 0x25, 0xd1, 0xf3, 0xc2, 0x3f, 0x81, 0x2a, 0xf6, 0x52, 0x74, 0x3f, 0xb4, 0x22, 0xbd, 0x5b, 0x50, + 0x22, 0xe1, 0x9c, 0x82, 0x84, 0x01, 0x18, 0x07, 0xa1, 0x18, 0x8c, 0x17, 0x54, 0x0e, 0x96, 0x1b, 0x88, 0x69, 0x9e, + 0xe3, 0x9b, 0x82, 0xde, 0xc5, 0x0e, 0x21, 0xa9, 0x64, 0x54, 0x44, 0x2c, 0x97, 0x29, 0x21, 0x29, 0xc2, 0x3f, 0x90, + 0x45, 0x68, 0xd6, 0x13, 0xec, 0x34, 0x30, 0x9c, 0xcb, 0x40, 0x71, 0x17, 0x3c, 0xe0, 0xc9, 0x9c, 0xa6, 0x82, 0xa6, + 0xc1, 0x5f, 0x71, 0x4a, 0x87, 0x31, 0x40, 0xb1, 0xd3, 0xc4, 0xe3, 0x30, 0x3b, 0x1f, 0x87, 0xc9, 0x88, 0x46, 0xc1, + 0x8d, 0xc8, 0xf1, 0xdf, 0x89, 0x3b, 0x64, 0x49, 0x18, 0xb3, 0x5f, 0x69, 0xe4, 0x6a, 0x69, 0x70, 0xe6, 0xd0, 0x5b, + 0x41, 0x93, 0x28, 0x73, 0x9e, 0xbf, 0x7b, 0xf9, 0x42, 0xef, 0x63, 0x45, 0x40, 0xa0, 0x45, 0x36, 0x9b, 0xd2, 0xd4, + 0x43, 0x58, 0x0b, 0x88, 0x67, 0x4c, 0x32, 0xc7, 0x97, 0xe1, 0x54, 0x95, 0xb0, 0xec, 0xfd, 0x34, 0x0a, 0x05, 0x7d, + 0x43, 0x93, 0x88, 0x25, 0x23, 0xb2, 0xd3, 0x54, 0xe5, 0xe3, 0x50, 0x57, 0x44, 0x45, 0xd1, 0xe5, 0xee, 0xb3, 0x58, + 0xae, 0xbb, 0x78, 0x9c, 0x79, 0x28, 0xcf, 0x44, 0x28, 0xd8, 0xc0, 0x09, 0xa3, 0xe8, 0x9b, 0x84, 0x09, 0x26, 0x01, + 0x4c, 0x61, 0x7b, 0x80, 0x44, 0xa9, 0x12, 0x15, 0x06, 0x70, 0x0f, 0x61, 0xcf, 0xd3, 0x02, 0x60, 0x8c, 0xf4, 0x7e, + 0xed, 0xed, 0x95, 0xec, 0xbe, 0x4b, 0x03, 0x55, 0x49, 0x7a, 0x7d, 0xe4, 0x4f, 0x67, 0x19, 0x6c, 0xb4, 0x99, 0x02, + 0xa4, 0x0b, 0xbf, 0xce, 0x68, 0x3a, 0xa7, 0x51, 0x41, 0x1c, 0x99, 0x87, 0x16, 0x2b, 0x73, 0xe8, 0x63, 0x21, 0x48, + 0xaf, 0xdf, 0xb6, 0xf9, 0x36, 0xd5, 0x74, 0x9e, 0xf2, 0x29, 0x4d, 0x05, 0xa3, 0x59, 0xc1, 0x4a, 0x3c, 0x90, 0xa2, + 0x05, 0x3b, 0xc9, 0x88, 0x59, 0xdf, 0xd4, 0x63, 0x98, 0xa2, 0x0a, 0xc3, 0x30, 0x82, 0xf6, 0xd9, 0x5c, 0x4a, 0x8c, + 0x0c, 0x33, 0x84, 0x85, 0x82, 0x34, 0x43, 0x28, 0x47, 0x58, 0x18, 0x70, 0x15, 0x2b, 0xd2, 0xb3, 0xdd, 0x81, 0xa8, + 0x26, 0x3f, 0x48, 0x51, 0x0d, 0x0c, 0x2d, 0x14, 0x74, 0x6f, 0xcf, 0xa3, 0x7e, 0x41, 0x14, 0x64, 0xa7, 0xa9, 0xf7, + 0xc8, 0x42, 0xd6, 0x16, 0xb0, 0x61, 0x62, 0x81, 0x29, 0xc2, 0x3b, 0xd4, 0x4f, 0xf8, 0xd9, 0x60, 0x40, 0xb3, 0x8c, + 0xa7, 0x7b, 0x7b, 0x3b, 0xb2, 0x7d, 0xa1, 0x4d, 0xc0, 0x1e, 0xbe, 0xbe, 0x49, 0x4a, 0x08, 0x50, 0x29, 0x61, 0xb5, + 0x5c, 0x10, 0x20, 0xa7, 0xa4, 0xc2, 0xe1, 0x76, 0x8d, 0xe2, 0x11, 0xb8, 0x97, 0x97, 0x6e, 0x4d, 0x60, 0x8d, 0x86, + 0x11, 0x35, 0x53, 0xdf, 0x3d, 0xa5, 0x4a, 0xb5, 0x92, 0x8a, 0xc7, 0x1a, 0x66, 0xd4, 0xf9, 0xf1, 0x23, 0x3a, 0x64, + 0x89, 0xb5, 0xec, 0x0a, 0x48, 0x58, 0xe0, 0x0c, 0xe5, 0xd6, 0x86, 0x6e, 0x1c, 0x5a, 0xea, 0x34, 0x6a, 0xe7, 0x16, + 0x23, 0xa9, 0x47, 0x58, 0xdb, 0xd8, 0xa3, 0xfd, 0x1c, 0x4b, 0xd4, 0x9b, 0xd5, 0x24, 0x12, 0xd0, 0x9e, 0xe8, 0xb7, + 0x75, 0x3d, 0xc9, 0x14, 0xe6, 0x52, 0xfa, 0xcb, 0x8c, 0x66, 0x42, 0xd1, 0xb1, 0x27, 0x70, 0x82, 0x19, 0xca, 0xe1, + 0xb8, 0x0d, 0xd9, 0x68, 0x96, 0x82, 0xba, 0x03, 0x47, 0x91, 0x26, 0xb3, 0x09, 0x35, 0x4f, 0x9b, 0x60, 0x7b, 0x3d, + 0x05, 0x81, 0x98, 0x01, 0x4d, 0xdf, 0x4f, 0x4e, 0x00, 0xab, 0x40, 0xcb, 0xe5, 0x0f, 0x66, 0x90, 0x72, 0x2b, 0x0b, + 0x15, 0x6d, 0x65, 0x4f, 0xfe, 0x8e, 0xb4, 0x3c, 0xde, 0x69, 0x2a, 0xe8, 0xff, 0xde, 0x27, 0x3b, 0x8d, 0x82, 0x82, + 0x35, 0x4e, 0x15, 0x30, 0x0a, 0x85, 0xaf, 0xd5, 0x40, 0x48, 0x4a, 0xf7, 0x0a, 0xb1, 0xf8, 0xe3, 0x35, 0x3a, 0x1d, + 0x93, 0x1e, 0xe8, 0x19, 0xfe, 0xb8, 0xbf, 0x8d, 0x98, 0x0c, 0x37, 0xf0, 0xc4, 0x7a, 0x5d, 0xc9, 0x34, 0xe6, 0x55, + 0xa6, 0xb1, 0xb2, 0x08, 0x77, 0x5a, 0x74, 0x71, 0x0b, 0x1a, 0xd3, 0xc7, 0xbc, 0xac, 0xc2, 0x4c, 0x02, 0x53, 0x2e, + 0xc9, 0x1a, 0xe2, 0x55, 0x38, 0xa1, 0x99, 0x47, 0x11, 0xde, 0xd6, 0x40, 0x11, 0x27, 0x34, 0xe9, 0x5b, 0x62, 0x33, + 0x03, 0xb1, 0xc9, 0x90, 0xd2, 0xca, 0xaa, 0xc7, 0x2d, 0xc3, 0xb4, 0x97, 0xf5, 0x4b, 0x65, 0xce, 0x5a, 0xbc, 0x94, + 0xc7, 0x9a, 0xba, 0x0d, 0xfe, 0x54, 0x99, 0x42, 0x9a, 0x54, 0x1a, 0x32, 0x84, 0x77, 0x1a, 0xab, 0xfb, 0x68, 0x5a, + 0x95, 0x6b, 0xec, 0xf5, 0x61, 0x1f, 0xa4, 0xb8, 0xf0, 0x59, 0x26, 0xff, 0x56, 0xce, 0x19, 0xa0, 0xed, 0x02, 0xc8, + 0xc2, 0x1f, 0xc6, 0xa1, 0xf0, 0x9a, 0xfb, 0x0d, 0xd0, 0x44, 0xe7, 0x14, 0xa4, 0x09, 0x42, 0xeb, 0x4b, 0xa1, 0xfe, + 0x2c, 0xc9, 0xc6, 0x6c, 0x28, 0xbc, 0x50, 0x48, 0x86, 0x42, 0xe3, 0x8c, 0x3a, 0xa2, 0xa2, 0x0f, 0x4b, 0x66, 0x13, + 0x02, 0xa9, 0x15, 0xca, 0x17, 0x35, 0x90, 0x4a, 0xa6, 0x05, 0xbc, 0xa1, 0xd4, 0xa5, 0x4b, 0x1e, 0x63, 0x5a, 0x33, + 0xd0, 0x17, 0x9b, 0x5d, 0x35, 0x62, 0xa0, 0x59, 0x01, 0xb3, 0x54, 0x56, 0x16, 0xd8, 0xfc, 0x41, 0x17, 0x0a, 0x5f, + 0xf0, 0x17, 0xfc, 0x86, 0xa6, 0xe7, 0x21, 0x00, 0x1f, 0xa8, 0xee, 0xb9, 0x12, 0x03, 0x92, 0xdb, 0x8b, 0xb6, 0xa1, + 0x97, 0x4b, 0xb9, 0xf0, 0x37, 0x29, 0x9f, 0xb0, 0x8c, 0x82, 0xa6, 0xa6, 0xf0, 0x9f, 0xc0, 0x29, 0x93, 0xc7, 0x11, + 0x44, 0x0d, 0x2d, 0xe8, 0xeb, 0xec, 0x45, 0x95, 0xbe, 0x2e, 0x77, 0x9f, 0x8d, 0x0c, 0xfb, 0xab, 0x1e, 0x62, 0x84, + 0x3d, 0x6d, 0x4f, 0x58, 0x52, 0xce, 0x1f, 0x23, 0x2d, 0xde, 0x97, 0x4b, 0x61, 0x99, 0x6d, 0x15, 0x5d, 0x91, 0xaa, + 0x63, 0x83, 0xf2, 0x30, 0x8a, 0x40, 0xab, 0x4b, 0x79, 0x1c, 0x5b, 0x82, 0x0a, 0xb3, 0x76, 0x21, 0x9a, 0x2e, 0x77, + 0x9f, 0x5d, 0xdc, 0x27, 0x9d, 0xa0, 0xde, 0x16, 0x50, 0x06, 0xd0, 0x24, 0xa2, 0x29, 0x98, 0x91, 0xd6, 0x6e, 0x69, + 0x19, 0x7b, 0xce, 0x93, 0x84, 0x0e, 0x04, 0x8d, 0xc0, 0x4a, 0x61, 0x44, 0xf8, 0x63, 0x9e, 0x89, 0xa2, 0xb0, 0x84, + 0x9e, 0x59, 0xd0, 0x33, 0x7f, 0x10, 0xc6, 0xb1, 0xa7, 0x2c, 0x92, 0x09, 0x9f, 0xd3, 0x0d, 0x50, 0xb7, 0x2b, 0x20, + 0x17, 0xc3, 0x50, 0x6b, 0x18, 0xea, 0x67, 0xd3, 0x98, 0x0d, 0x68, 0x21, 0xb8, 0x2e, 0x7c, 0x96, 0x44, 0xf4, 0x16, + 0xf8, 0x08, 0xea, 0x74, 0x3a, 0x0d, 0xdc, 0x44, 0xb9, 0x42, 0xf8, 0x62, 0x0d, 0xb1, 0xf7, 0x88, 0x4c, 0x20, 0x32, + 0xd2, 0x59, 0x6c, 0xe2, 0x07, 0x14, 0x59, 0x72, 0x92, 0x19, 0xcb, 0x4a, 0xf1, 0x66, 0x84, 0x23, 0x1a, 0x53, 0x41, + 0x0d, 0x2f, 0x07, 0xfd, 0x59, 0x1d, 0xdd, 0xb7, 0x05, 0xfe, 0x0a, 0x72, 0x32, 0xa7, 0xcc, 0xec, 0x79, 0x56, 0x58, + 0xea, 0xe5, 0xf6, 0x94, 0xd8, 0xee, 0x0a, 0xb5, 0x3d, 0xa1, 0x10, 0xe1, 0x60, 0xac, 0x4c, 0x74, 0x6f, 0x6d, 0x49, + 0xe5, 0x18, 0x9a, 0xaf, 0x17, 0x87, 0xe8, 0xbd, 0x01, 0x73, 0x13, 0x0a, 0x2e, 0x34, 0x53, 0xa0, 0x60, 0xf5, 0xa9, + 0x6d, 0x3b, 0x0f, 0xe3, 0xf8, 0x3a, 0x1c, 0x7c, 0xac, 0x52, 0x7f, 0x49, 0x06, 0x64, 0x95, 0x1b, 0x5b, 0x55, 0x16, + 0xcb, 0xb2, 0xd7, 0x6d, 0xb8, 0x74, 0xe5, 0xa0, 0x78, 0x3b, 0x8d, 0x92, 0xec, 0xab, 0x1b, 0xbd, 0x95, 0xda, 0x25, + 0x44, 0x4c, 0xaf, 0xcc, 0x03, 0x2e, 0xf0, 0x49, 0x8a, 0x33, 0xfc, 0x40, 0xd3, 0x1d, 0xd8, 0x1a, 0xf9, 0x0a, 0x20, + 0x02, 0x2d, 0xf2, 0x88, 0x65, 0xdb, 0x31, 0xf0, 0x87, 0x40, 0xf9, 0xd4, 0x9a, 0xe1, 0xa1, 0x80, 0x16, 0x3c, 0x4e, + 0xab, 0xcc, 0x05, 0x64, 0x5a, 0x9b, 0x30, 0x8c, 0xe6, 0x5b, 0xd0, 0x5c, 0x24, 0xbd, 0xbf, 0x56, 0x55, 0xa0, 0x93, + 0x01, 0x14, 0x59, 0xdb, 0x56, 0x26, 0x2a, 0x14, 0xa0, 0x79, 0x2a, 0x93, 0x22, 0x37, 0xa9, 0x18, 0x8f, 0x5a, 0x5d, + 0x57, 0xf6, 0xb7, 0x66, 0xb9, 0x9c, 0x78, 0x9e, 0x97, 0x81, 0xfd, 0x66, 0xf4, 0xfa, 0x72, 0x11, 0xd9, 0xda, 0x22, + 0x32, 0xdf, 0x32, 0xb2, 0x50, 0x49, 0xcb, 0x56, 0xf7, 0xe0, 0xaf, 0xc8, 0x6e, 0x04, 0xca, 0xaa, 0x0f, 0xfc, 0x19, + 0x15, 0xec, 0x36, 0x26, 0x02, 0x73, 0x6d, 0xe0, 0x68, 0x4a, 0x03, 0x86, 0x51, 0x76, 0x49, 0x90, 0x3a, 0x1a, 0x15, + 0x63, 0x37, 0xc1, 0x1c, 0xad, 0x68, 0xf6, 0x79, 0xae, 0x71, 0x44, 0x91, 0xde, 0x9b, 0x8a, 0x4a, 0x6c, 0x61, 0x05, + 0x27, 0x44, 0xab, 0xc1, 0x4a, 0xeb, 0x59, 0xc5, 0x4d, 0x31, 0x2e, 0x1c, 0xd4, 0x12, 0x35, 0x15, 0x7d, 0xd2, 0x28, + 0x56, 0x09, 0xc2, 0x63, 0xa3, 0x91, 0xf2, 0x72, 0xdd, 0x84, 0xb8, 0xc6, 0x1b, 0xe1, 0x76, 0x17, 0x15, 0x93, 0x30, + 0xb0, 0x9a, 0xe5, 0x01, 0xb0, 0x54, 0xbe, 0x09, 0xdd, 0x9b, 0x68, 0xa6, 0x32, 0x8e, 0x85, 0x70, 0x6e, 0x23, 0xdc, + 0xc2, 0x6c, 0xa2, 0x38, 0x57, 0xd2, 0x27, 0xe3, 0x6a, 0x5f, 0x8f, 0x62, 0xae, 0xf6, 0x61, 0x0d, 0x89, 0xab, 0x8a, + 0xa7, 0x24, 0x41, 0x30, 0x60, 0x33, 0x50, 0xee, 0x6c, 0xf9, 0xe0, 0x01, 0xec, 0x6c, 0xb9, 0x5c, 0x23, 0xba, 0x8d, + 0xfa, 0x27, 0xf2, 0x4b, 0xa3, 0x70, 0xb9, 0xbc, 0x11, 0xc8, 0xd3, 0x9a, 0x2f, 0xa6, 0xa8, 0x6b, 0x38, 0xee, 0xd9, + 0x0b, 0x68, 0x25, 0x15, 0xd1, 0xb2, 0xa4, 0x30, 0x19, 0xaa, 0x34, 0x5b, 0xdd, 0x27, 0x61, 0xb1, 0xed, 0xf3, 0x35, + 0xee, 0x25, 0x0b, 0xb5, 0x98, 0x2e, 0x97, 0x7c, 0xae, 0x87, 0x66, 0x08, 0xa1, 0x20, 0x93, 0x56, 0xcc, 0xce, 0x26, + 0xc3, 0x72, 0x6f, 0x2f, 0xb3, 0x06, 0xba, 0x2c, 0xd8, 0xc4, 0x07, 0x0f, 0x44, 0x72, 0x76, 0x97, 0x48, 0xdd, 0xe5, + 0x83, 0x11, 0x42, 0x6b, 0x66, 0x69, 0xa3, 0x0d, 0xd6, 0x78, 0x78, 0x13, 0x32, 0xe1, 0x14, 0xa3, 0x28, 0x6b, 0xdc, + 0xa3, 0x68, 0xa1, 0x55, 0x0d, 0x3f, 0xa5, 0xa0, 0x3c, 0x02, 0x4f, 0x30, 0x2a, 0xb4, 0xa2, 0xfb, 0xc1, 0x98, 0x82, + 0x23, 0xd8, 0x68, 0x11, 0x85, 0x5d, 0xb8, 0xa3, 0xa5, 0x88, 0x1e, 0x78, 0x33, 0xec, 0xf9, 0x6a, 0xf7, 0x8a, 0x1d, + 0x30, 0xa5, 0xe9, 0x90, 0xa7, 0x13, 0x53, 0x97, 0xaf, 0x3c, 0x6b, 0xce, 0xc8, 0x86, 0xde, 0xc6, 0xb1, 0xb5, 0xfa, + 0xdf, 0x5e, 0x31, 0xba, 0x4b, 0x73, 0xbd, 0x22, 0x4a, 0x0b, 0xe9, 0xab, 0xfc, 0x81, 0x86, 0x32, 0x33, 0xdb, 0xbc, + 0xd7, 0xce, 0xd4, 0xb6, 0x72, 0x98, 0xec, 0x34, 0xdb, 0x85, 0xcd, 0x67, 0xa8, 0xa1, 0xad, 0x1c, 0x1b, 0x5a, 0xa4, + 0xf2, 0x59, 0x1c, 0x69, 0x60, 0x19, 0xc2, 0x54, 0xd3, 0xd1, 0x0d, 0x8b, 0xe3, 0xb2, 0xf4, 0x73, 0xf8, 0x7a, 0xa6, + 0xf9, 0x7a, 0x62, 0xf8, 0x3a, 0x70, 0x0a, 0xe0, 0xeb, 0x6a, 0xb8, 0xb2, 0x7b, 0xb2, 0x76, 0x3a, 0x13, 0xc5, 0xd1, + 0x33, 0x69, 0x47, 0xc3, 0x7c, 0x33, 0x03, 0x01, 0x2a, 0x34, 0xaf, 0x8f, 0x9e, 0x76, 0xc2, 0x80, 0x01, 0xa8, 0x5c, + 0x98, 0xd4, 0x76, 0x51, 0x7c, 0xf4, 0x10, 0xce, 0x72, 0x5a, 0x50, 0xf6, 0xd9, 0x33, 0x70, 0xd2, 0x59, 0xcb, 0x01, + 0x21, 0x26, 0x8b, 0x3f, 0x4b, 0x89, 0x32, 0xab, 0x63, 0x7a, 0x75, 0x99, 0x59, 0x1d, 0x70, 0xfa, 0x72, 0x75, 0xd1, + 0xfd, 0xbc, 0x5e, 0x2e, 0x8f, 0x15, 0xcb, 0x2b, 0xf7, 0x7b, 0xb9, 0xf4, 0x56, 0x4a, 0xc0, 0x7f, 0xaf, 0x4d, 0x94, + 0xb4, 0x18, 0x1d, 0x78, 0x80, 0x8d, 0x19, 0x28, 0xc8, 0xd5, 0xa2, 0x0b, 0x11, 0xf7, 0xe2, 0x53, 0x0e, 0x1e, 0xe9, + 0xa6, 0x57, 0xfd, 0xcf, 0xf9, 0x64, 0x0a, 0xda, 0xd8, 0x0a, 0x49, 0x8f, 0xa8, 0x9e, 0xb0, 0xac, 0xcf, 0x37, 0x94, + 0x55, 0xfa, 0xc8, 0xf3, 0x58, 0xa1, 0xa6, 0xc2, 0x5e, 0xde, 0x69, 0xe4, 0xb3, 0xa2, 0xa8, 0x60, 0x1c, 0x9b, 0x9c, + 0x2a, 0xe7, 0xab, 0x2e, 0x19, 0x53, 0xf1, 0xda, 0x63, 0x8a, 0x0f, 0x33, 0xe0, 0x75, 0x16, 0xfb, 0x31, 0xe4, 0x6e, + 0xef, 0x7f, 0x5e, 0x22, 0x67, 0x91, 0xaf, 0xa0, 0x6f, 0x91, 0xe7, 0x67, 0xca, 0xc8, 0xc6, 0x67, 0xdb, 0xad, 0xe1, + 0xb2, 0x4e, 0x1b, 0x8b, 0xbd, 0x3e, 0x3e, 0x5b, 0x57, 0x1d, 0xc9, 0x62, 0xc2, 0x23, 0x1a, 0xb8, 0x7c, 0x4a, 0x13, + 0x37, 0x07, 0xaf, 0xaa, 0xde, 0xfb, 0x81, 0xf0, 0x16, 0x6f, 0xab, 0xee, 0xd5, 0xe0, 0x2c, 0x07, 0xef, 0xd7, 0xd7, + 0xeb, 0x8e, 0xd7, 0xef, 0x69, 0x9a, 0x49, 0x45, 0xb4, 0xd0, 0x69, 0xbf, 0x2e, 0xc5, 0xd2, 0xd7, 0xc1, 0xd6, 0xf6, + 0xa5, 0x09, 0xe2, 0x36, 0xfd, 0x63, 0xff, 0xc0, 0x45, 0xd2, 0x2d, 0xfc, 0x93, 0x3e, 0xf0, 0x1f, 0x8c, 0x5b, 0xf8, + 0x19, 0xf9, 0x50, 0xf5, 0x0a, 0x47, 0x82, 0x3c, 0xeb, 0x3e, 0x33, 0x16, 0x33, 0x8f, 0xd9, 0xe0, 0xce, 0x73, 0x63, + 0x26, 0xea, 0x10, 0x7a, 0x73, 0xf1, 0x42, 0x55, 0x80, 0x4b, 0x51, 0xba, 0xb3, 0x73, 0x63, 0xeb, 0x61, 0x21, 0x88, + 0xbb, 0x1b, 0x33, 0xb1, 0xeb, 0xe2, 0x09, 0xb9, 0x82, 0x1f, 0xbb, 0x0b, 0xef, 0x65, 0x28, 0xc6, 0x7e, 0x1a, 0x26, + 0x11, 0x9f, 0x78, 0xa8, 0xe6, 0xba, 0xc8, 0xcf, 0xa4, 0xbd, 0xf1, 0x18, 0xe5, 0xbb, 0x57, 0xf8, 0x56, 0x10, 0xb7, + 0xeb, 0xd6, 0x26, 0xf8, 0x95, 0x20, 0x57, 0xa7, 0xbb, 0x8b, 0x5b, 0x91, 0x77, 0xae, 0xf0, 0x6d, 0xe1, 0xb1, 0xc7, + 0x6f, 0x88, 0x87, 0x48, 0xe7, 0x56, 0x43, 0x73, 0xce, 0x27, 0xca, 0x73, 0xef, 0x22, 0xfc, 0x1e, 0xe2, 0x2a, 0x69, + 0xc9, 0x6d, 0x74, 0x68, 0x65, 0x87, 0xb8, 0x5c, 0xba, 0x08, 0xdc, 0xbd, 0x3d, 0xab, 0xac, 0x50, 0x15, 0xf0, 0x99, + 0x20, 0x15, 0x83, 0x1c, 0xbf, 0x95, 0x11, 0x9a, 0x33, 0xe1, 0xa5, 0xc8, 0x0c, 0xe3, 0x19, 0x3f, 0xb4, 0x3e, 0x9a, + 0x69, 0x4f, 0x79, 0x18, 0x7c, 0x26, 0x68, 0x1a, 0x0a, 0x9e, 0xf6, 0x91, 0xad, 0x7e, 0xe0, 0xbf, 0x91, 0xab, 0x9e, + 0xf3, 0x9f, 0xbe, 0xf8, 0x79, 0xf8, 0x73, 0xda, 0xbf, 0xc2, 0xaf, 0xc9, 0xfe, 0xa9, 0xd7, 0x0d, 0xbc, 0x9d, 0x7a, + 0x7d, 0xf9, 0xf3, 0x7e, 0xef, 0x1f, 0x61, 0xfd, 0xd7, 0xb3, 0xfa, 0x4f, 0x7d, 0xb4, 0xf4, 0x7e, 0xde, 0xef, 0xf6, + 0xf4, 0x53, 0xef, 0x1f, 0x9d, 0x9f, 0xb3, 0xfe, 0x9f, 0x55, 0xe1, 0x2e, 0x42, 0xfb, 0x23, 0x3c, 0x13, 0x64, 0xbf, + 0x5e, 0xef, 0xec, 0x8f, 0xf0, 0x54, 0x90, 0x7d, 0xf8, 0x7b, 0x4d, 0xde, 0xd2, 0xd1, 0xb3, 0xdb, 0xa9, 0x77, 0xd5, + 0x59, 0xee, 0x2e, 0xfe, 0x96, 0xc3, 0xa8, 0xbd, 0x7f, 0xfc, 0xfc, 0x73, 0xe6, 0x7e, 0xd5, 0x21, 0xfb, 0xfd, 0x1a, + 0xf2, 0xa0, 0xf4, 0xcf, 0x44, 0xfe, 0xeb, 0x75, 0x83, 0xde, 0x3f, 0x34, 0x14, 0xee, 0x57, 0x3f, 0x5f, 0x9d, 0x76, + 0x48, 0x7f, 0xe9, 0xb9, 0xcb, 0xaf, 0xd0, 0x12, 0xa1, 0xe5, 0x2e, 0xba, 0xc2, 0xee, 0xc8, 0x45, 0x78, 0x24, 0xc8, + 0xfe, 0x57, 0xfb, 0x23, 0x3c, 0x17, 0x64, 0xdf, 0xdd, 0x1f, 0xe1, 0x67, 0x82, 0xec, 0xff, 0xc3, 0xeb, 0x06, 0xca, + 0xc3, 0xb6, 0x94, 0xee, 0x8d, 0x25, 0x04, 0x37, 0xc2, 0x94, 0x86, 0x4b, 0xc1, 0x44, 0x4c, 0xd1, 0xee, 0x3e, 0xc3, + 0x17, 0x12, 0x4d, 0x9e, 0x00, 0x27, 0x0c, 0xd8, 0x76, 0xde, 0xe2, 0x12, 0x36, 0x1b, 0x68, 0x66, 0x37, 0x48, 0xb1, + 0xf2, 0x03, 0x64, 0x81, 0xc0, 0xf3, 0x30, 0x9e, 0xd1, 0x2c, 0xa0, 0x39, 0xc2, 0x03, 0x72, 0x21, 0xbc, 0x26, 0xc2, + 0xcf, 0x05, 0xfc, 0x68, 0x21, 0x7c, 0xa1, 0x03, 0x98, 0x70, 0x90, 0x15, 0x51, 0x25, 0x5c, 0x69, 0x2c, 0x2e, 0xc2, + 0xd3, 0x0d, 0x95, 0x62, 0x0c, 0xde, 0x05, 0x84, 0x87, 0x95, 0x70, 0x27, 0xbe, 0x21, 0x86, 0x24, 0xde, 0xa5, 0x94, + 0xfe, 0x10, 0xc6, 0x1f, 0x69, 0xea, 0xdd, 0xe2, 0x66, 0xeb, 0x31, 0x96, 0x2e, 0xe8, 0x9d, 0x26, 0x6a, 0x17, 0xb1, + 0xaa, 0x73, 0xa1, 0x62, 0x04, 0x20, 0x64, 0xab, 0xbe, 0x18, 0xd8, 0xf1, 0x9d, 0x74, 0xcd, 0x61, 0x95, 0x86, 0x37, + 0x2e, 0xaa, 0xc6, 0x45, 0x59, 0x32, 0x0f, 0x63, 0x16, 0x39, 0x82, 0x4e, 0xa6, 0x71, 0x28, 0xa8, 0xa3, 0xd7, 0xeb, + 0x84, 0x30, 0x90, 0x5b, 0xa8, 0x0c, 0x91, 0x65, 0x70, 0x46, 0x26, 0xe0, 0x04, 0x67, 0xc5, 0x83, 0xe8, 0x94, 0x56, + 0x3b, 0x5e, 0x96, 0xc1, 0xaf, 0xd5, 0xf8, 0x5e, 0xbd, 0x09, 0x8e, 0xb0, 0xbe, 0x14, 0xcf, 0x19, 0x4e, 0x08, 0x08, + 0xd1, 0x56, 0xd7, 0x3d, 0xcd, 0xe6, 0xa3, 0x8e, 0x0b, 0xb1, 0x19, 0x4e, 0x5e, 0x4b, 0xbf, 0x10, 0x34, 0x18, 0x93, + 0x46, 0x7b, 0x7c, 0x4a, 0xdb, 0xe3, 0x5a, 0xcd, 0xe8, 0xd0, 0x31, 0x49, 0x7b, 0x63, 0xd5, 0x3d, 0xc4, 0x11, 0x9e, + 0x91, 0x7a, 0x13, 0x8f, 0x48, 0x43, 0x76, 0x69, 0x8f, 0x4e, 0x63, 0x3d, 0xcd, 0xde, 0x9e, 0xc7, 0xfd, 0x38, 0xcc, + 0xc4, 0x37, 0x60, 0xec, 0x93, 0x11, 0x8e, 0x08, 0xf7, 0xe9, 0x2d, 0x1d, 0x78, 0x31, 0xc2, 0x91, 0xe6, 0x34, 0xa8, + 0x8d, 0x46, 0xc4, 0x6a, 0x06, 0x46, 0x04, 0x79, 0xdd, 0x8d, 0x7a, 0xcd, 0x3e, 0x21, 0xc4, 0xdd, 0xa9, 0xd7, 0xdd, + 0x2e, 0x27, 0x33, 0x11, 0x40, 0x89, 0xa5, 0x2a, 0x93, 0x29, 0x14, 0xb5, 0xac, 0x22, 0xef, 0x99, 0xf0, 0x05, 0xcd, + 0x84, 0x07, 0xc5, 0x60, 0xfe, 0x67, 0x86, 0xb0, 0xdd, 0xd3, 0x7d, 0xb7, 0x06, 0xa5, 0x92, 0x38, 0x11, 0xe6, 0xe4, + 0x1a, 0x05, 0x51, 0xef, 0xa0, 0x6f, 0xf3, 0x7f, 0x59, 0x08, 0x93, 0x5f, 0x77, 0xa3, 0x5e, 0x43, 0x4e, 0xde, 0x71, + 0xbb, 0x1e, 0x27, 0x99, 0x52, 0xd0, 0xba, 0x59, 0xf0, 0x5a, 0x2e, 0x15, 0x05, 0x1a, 0x38, 0x3d, 0xef, 0x8c, 0xd4, + 0x5b, 0x81, 0x37, 0xb3, 0x17, 0x51, 0x87, 0xc9, 0x34, 0x16, 0x70, 0x48, 0xa0, 0x3d, 0xe6, 0x04, 0x66, 0x2c, 0xbb, + 0x5d, 0x07, 0xfa, 0xf9, 0x2b, 0xf7, 0xab, 0xee, 0x5c, 0x04, 0x23, 0xa1, 0xa6, 0x9f, 0x8b, 0xe5, 0x12, 0xfe, 0x8e, + 0x44, 0x97, 0x93, 0x6b, 0x59, 0x34, 0xd3, 0x45, 0x53, 0x28, 0x7a, 0x1d, 0x00, 0xa8, 0x38, 0x2b, 0x94, 0x2c, 0xb5, + 0x27, 0x73, 0x22, 0x61, 0xdf, 0xdb, 0x4b, 0x7b, 0xe3, 0x5a, 0xb3, 0x0f, 0xfe, 0xfd, 0x54, 0x64, 0x3f, 0x30, 0x31, + 0xf6, 0xdc, 0xfd, 0x8e, 0x8b, 0xba, 0xae, 0x03, 0x5b, 0xdb, 0x4e, 0x6a, 0x44, 0x61, 0x38, 0xae, 0xbd, 0x12, 0xc1, + 0xac, 0x43, 0x1a, 0x5d, 0x8f, 0x69, 0x7f, 0x1e, 0xc2, 0xb1, 0x66, 0x9c, 0x0d, 0x3c, 0x43, 0x35, 0x21, 0x6a, 0xe6, + 0x79, 0x86, 0x6a, 0x93, 0xda, 0x1c, 0x05, 0x71, 0x6d, 0x52, 0xf3, 0x66, 0x84, 0x90, 0x7a, 0xab, 0xe8, 0x66, 0xa4, + 0xdf, 0x18, 0x05, 0x73, 0xe3, 0xec, 0xec, 0xc9, 0xe3, 0x90, 0xd4, 0xbc, 0xb4, 0x47, 0xfb, 0xcb, 0xa5, 0x7b, 0xda, + 0xed, 0xb8, 0xa8, 0xe6, 0x19, 0x42, 0xdb, 0x37, 0x94, 0x86, 0x10, 0x66, 0xfd, 0x5c, 0x87, 0x92, 0xde, 0x55, 0xc2, + 0x46, 0x8b, 0xf2, 0xb0, 0x5b, 0x3c, 0x80, 0xe6, 0x85, 0x1d, 0xa3, 0xf4, 0xd5, 0x29, 0x2c, 0xd3, 0x10, 0x73, 0x42, + 0x1a, 0x98, 0x13, 0xe3, 0xbb, 0x1e, 0x13, 0x51, 0x12, 0x7c, 0x4c, 0xca, 0xe6, 0xb8, 0x17, 0xe2, 0xa8, 0x4f, 0x5e, + 0x2a, 0x7b, 0xa4, 0x6d, 0xfc, 0xe2, 0x34, 0x26, 0xef, 0x56, 0xa2, 0xb7, 0x21, 0xc4, 0x56, 0x6e, 0xfc, 0xc1, 0x2c, + 0x4d, 0x69, 0x22, 0x5e, 0xf1, 0x48, 0xab, 0x69, 0x34, 0x06, 0x4b, 0x09, 0xc2, 0xb2, 0x18, 0x74, 0xb4, 0x96, 0x39, + 0x19, 0xb3, 0xb5, 0xea, 0x11, 0x99, 0x29, 0xf5, 0x49, 0x06, 0x6b, 0xdb, 0x23, 0x6d, 0x17, 0x7b, 0x08, 0xcf, 0x74, + 0x14, 0xd7, 0xf3, 0x7d, 0x7f, 0xe4, 0x0f, 0xa0, 0x1a, 0x26, 0xc8, 0x50, 0x2e, 0xcf, 0x91, 0x97, 0x91, 0x1b, 0x3f, + 0xa1, 0xb7, 0x72, 0x56, 0x0f, 0x95, 0x92, 0xd9, 0x1c, 0xaf, 0xd3, 0x71, 0x5b, 0xb2, 0x9b, 0xcc, 0x4f, 0x78, 0x44, + 0x01, 0x3d, 0x10, 0xb7, 0xd7, 0x45, 0xe3, 0x30, 0xb3, 0xe3, 0x53, 0x25, 0x7c, 0x3d, 0xdb, 0x79, 0x3d, 0x02, 0x8f, + 0xaf, 0xd4, 0xb5, 0x8a, 0xc6, 0xca, 0x0d, 0x8e, 0x10, 0x1b, 0x7a, 0x23, 0x1f, 0xe2, 0x7a, 0x92, 0x84, 0x04, 0x98, + 0x72, 0x23, 0x9b, 0xa8, 0x26, 0xc5, 0x98, 0x73, 0x12, 0xf5, 0x78, 0xad, 0x26, 0xbd, 0xd0, 0x33, 0x45, 0x12, 0x23, + 0x84, 0xe7, 0xc5, 0xd9, 0x32, 0xed, 0x5e, 0x0b, 0x52, 0x9d, 0xca, 0x9b, 0x57, 0xdd, 0xb9, 0x35, 0x21, 0x90, 0xf4, + 0x14, 0x0a, 0x6f, 0x82, 0xf0, 0x13, 0xb2, 0xef, 0xf5, 0xfc, 0xee, 0x5f, 0xfa, 0xa8, 0xeb, 0xf9, 0x7f, 0x46, 0xfb, + 0x8a, 0x73, 0xcc, 0x51, 0x3b, 0x56, 0x73, 0x2c, 0x64, 0xfc, 0xb2, 0x89, 0xa5, 0x27, 0x31, 0x48, 0x70, 0x12, 0x4e, + 0x68, 0xf0, 0x04, 0x0e, 0xb9, 0x21, 0x9c, 0xd7, 0x02, 0x03, 0x25, 0x05, 0x4f, 0x34, 0x2f, 0xf1, 0xdd, 0xee, 0x0b, + 0x51, 0x3c, 0x75, 0xdd, 0xee, 0x87, 0xf2, 0xe9, 0x2f, 0x6e, 0xf7, 0x1b, 0x11, 0xfc, 0x9a, 0x6b, 0x6f, 0x77, 0x65, + 0x8e, 0x63, 0x33, 0x47, 0xae, 0xb6, 0xc6, 0xc2, 0xdd, 0x0c, 0xad, 0x3b, 0x3a, 0x46, 0x28, 0x67, 0xc3, 0x82, 0x19, + 0x65, 0xbe, 0x08, 0x47, 0x80, 0x54, 0x6b, 0x0f, 0x32, 0x3b, 0xae, 0x5f, 0xae, 0x18, 0x48, 0xc5, 0xd0, 0x2b, 0x20, + 0x73, 0xd4, 0x69, 0xa0, 0x45, 0xa5, 0xad, 0xd4, 0x99, 0xaa, 0x71, 0xf4, 0x82, 0x4f, 0xcf, 0x49, 0xa3, 0x3d, 0x3f, + 0x1d, 0xb5, 0xe7, 0xb5, 0x1a, 0xca, 0x0c, 0x69, 0xcd, 0x7a, 0xf3, 0x3e, 0x7e, 0x03, 0x4e, 0x3d, 0x9b, 0x96, 0x70, + 0x65, 0x79, 0x2d, 0xbd, 0xbc, 0x5a, 0x2d, 0xc9, 0x51, 0xdb, 0xea, 0x3a, 0x52, 0x5d, 0xf3, 0x5c, 0xe1, 0x64, 0x95, + 0xd4, 0x4e, 0x90, 0x2c, 0x81, 0x64, 0x28, 0x42, 0xc8, 0xad, 0x40, 0x1b, 0x47, 0x85, 0x31, 0xa1, 0xbb, 0x3c, 0xb3, + 0xc0, 0x3e, 0x95, 0x94, 0xf0, 0x00, 0x0b, 0xd0, 0xb5, 0xf0, 0x04, 0x4f, 0xf0, 0xac, 0xd6, 0x94, 0x64, 0x5e, 0x6f, + 0xb6, 0xab, 0x63, 0x3d, 0x2a, 0xc7, 0xc2, 0xb3, 0x1a, 0x99, 0x14, 0x58, 0xca, 0x93, 0x5a, 0x2d, 0xaf, 0x06, 0x3b, + 0xcd, 0xc9, 0xad, 0x04, 0x20, 0x6e, 0x57, 0x93, 0x32, 0x8c, 0x84, 0x2d, 0x65, 0x2a, 0xf3, 0x59, 0x92, 0xd0, 0x14, + 0xa4, 0x28, 0x11, 0x98, 0xe5, 0x79, 0x29, 0xd9, 0x41, 0x8c, 0x62, 0x4a, 0x52, 0xe0, 0x3c, 0xd2, 0xee, 0xc2, 0x09, + 0xe6, 0x78, 0x2c, 0xf9, 0x06, 0x21, 0xe4, 0xc2, 0xa4, 0xb3, 0x08, 0xc9, 0x83, 0x62, 0xc2, 0x2c, 0x99, 0x94, 0x11, + 0xea, 0x5f, 0xee, 0x9e, 0xf3, 0x7b, 0x6d, 0xb2, 0x1e, 0xeb, 0x07, 0xb2, 0x59, 0xac, 0x39, 0x57, 0x48, 0xde, 0x7b, + 0x02, 0x15, 0xd1, 0x11, 0x5f, 0x32, 0xc0, 0xa7, 0x2c, 0xa5, 0x52, 0x07, 0xdf, 0x35, 0x76, 0x5f, 0x5c, 0x55, 0x20, + 0x63, 0xdb, 0x7b, 0x03, 0x88, 0x0c, 0xc1, 0xb9, 0x93, 0x90, 0xb5, 0x66, 0x97, 0xbb, 0x67, 0xaf, 0x37, 0xd9, 0xc0, + 0xcb, 0xa5, 0xb6, 0x7e, 0xa5, 0x6e, 0x83, 0xc3, 0x12, 0xd2, 0x58, 0xff, 0x08, 0xbc, 0x58, 0xaa, 0x48, 0xa1, 0x97, + 0x02, 0x15, 0x5d, 0xee, 0x9e, 0xbd, 0xf3, 0x52, 0xe9, 0x5b, 0x42, 0xd8, 0x5e, 0xb6, 0xc7, 0x89, 0x37, 0x26, 0x14, + 0xa9, 0xb5, 0x17, 0xac, 0x8b, 0x5b, 0x02, 0x3c, 0x18, 0xcb, 0x4a, 0xb0, 0x20, 0x7a, 0xac, 0x4f, 0x62, 0x8d, 0x01, + 0x12, 0x23, 0x1c, 0x57, 0xec, 0x32, 0x02, 0x1b, 0x20, 0xe7, 0xba, 0x80, 0x9d, 0xf0, 0x95, 0xea, 0x87, 0x70, 0x2c, + 0x67, 0x15, 0xb9, 0x12, 0x1e, 0x4f, 0xd6, 0xb2, 0xd2, 0x4a, 0x73, 0xf4, 0x7b, 0xb0, 0x9d, 0xcc, 0xc3, 0x2b, 0x62, + 0x2c, 0x09, 0x5d, 0xf0, 0xd4, 0xa4, 0x8f, 0x5d, 0xee, 0x9e, 0xbd, 0xd4, 0x19, 0x64, 0xd3, 0xd0, 0xf0, 0xfb, 0x35, + 0x13, 0xf3, 0xec, 0xa5, 0x5f, 0xd6, 0xca, 0xc6, 0x97, 0xbb, 0x67, 0xef, 0x37, 0x35, 0x83, 0xf2, 0x7c, 0x56, 0xda, + 0xf8, 0x12, 0xbe, 0x05, 0x8d, 0x83, 0x85, 0x16, 0x0e, 0x01, 0xcb, 0xb1, 0x14, 0x48, 0x41, 0x96, 0x17, 0xae, 0x91, + 0xa7, 0x38, 0x21, 0x32, 0x0c, 0x54, 0xdd, 0x35, 0xad, 0xe6, 0x31, 0x9e, 0x5c, 0x0c, 0xf8, 0x94, 0x6e, 0x89, 0x0d, + 0xdd, 0x22, 0x9f, 0x4d, 0x20, 0x75, 0x46, 0x82, 0xce, 0xf0, 0x4e, 0x03, 0xb5, 0xab, 0xe2, 0x2b, 0x91, 0x44, 0xca, + 0x2b, 0xb2, 0x05, 0x8f, 0x49, 0x03, 0xc7, 0xa4, 0x81, 0x43, 0x92, 0xf5, 0x1a, 0x4a, 0x40, 0xb4, 0xc3, 0x62, 0x5c, + 0x25, 0x66, 0x20, 0x2b, 0x4c, 0x9f, 0x56, 0x25, 0x80, 0xa3, 0x76, 0x28, 0x7d, 0x8f, 0x52, 0xa6, 0x47, 0x92, 0x2c, + 0xde, 0x7a, 0x1c, 0x73, 0x39, 0xf0, 0x05, 0xbb, 0x8e, 0x21, 0xb1, 0x04, 0x56, 0x85, 0x05, 0x0a, 0x8a, 0xa6, 0x4d, + 0xdd, 0x34, 0xf4, 0xe5, 0x3e, 0x71, 0x1c, 0xfa, 0xc0, 0xb9, 0x71, 0xa8, 0xf3, 0x70, 0xb2, 0xf5, 0x2e, 0xc7, 0x7b, + 0x7b, 0x9e, 0xea, 0xf4, 0x8b, 0xf0, 0xb8, 0xa9, 0x2f, 0x23, 0x77, 0xdf, 0x2b, 0x5e, 0x11, 0x21, 0x09, 0x7f, 0xad, + 0x16, 0xf7, 0x73, 0x08, 0x43, 0x7b, 0x61, 0x15, 0x83, 0x06, 0x78, 0xa9, 0xeb, 0x55, 0x97, 0x5f, 0xab, 0x15, 0x51, + 0xda, 0x2a, 0xb6, 0x6e, 0x71, 0x92, 0xcf, 0xbd, 0x22, 0xf5, 0xa7, 0xb1, 0x96, 0x2f, 0x65, 0x40, 0x40, 0xcc, 0xa6, + 0x59, 0x66, 0x16, 0x63, 0x1d, 0x09, 0x06, 0xed, 0xbe, 0xd1, 0x59, 0x0b, 0x58, 0x66, 0x57, 0xe9, 0x46, 0x86, 0x9d, + 0xb5, 0x50, 0x60, 0x1a, 0x41, 0x54, 0x0a, 0x1a, 0xd5, 0x72, 0x4d, 0xde, 0x6f, 0xd7, 0x73, 0x2e, 0x71, 0x86, 0xb4, + 0x93, 0x4b, 0x42, 0x21, 0x91, 0xd5, 0x2a, 0x90, 0xf2, 0x9c, 0x4c, 0xb7, 0x93, 0xfc, 0x99, 0x45, 0xf2, 0x4f, 0x08, + 0xb5, 0xc8, 0x5f, 0xb9, 0x38, 0x7c, 0xae, 0x9d, 0x0b, 0x99, 0xa9, 0x3a, 0x9f, 0x12, 0x70, 0xa2, 0x55, 0x31, 0x5a, + 0x09, 0x2b, 0x6e, 0x61, 0x28, 0xf6, 0x09, 0x91, 0x6e, 0x48, 0x6c, 0x62, 0xc0, 0x5e, 0x19, 0x54, 0x83, 0xa9, 0x37, + 0xf9, 0xf4, 0x6c, 0x0e, 0x78, 0xf6, 0xfe, 0xfe, 0x78, 0xe8, 0xf9, 0x74, 0xfd, 0xe4, 0x5a, 0xb9, 0x9f, 0xb0, 0x6a, + 0xeb, 0xe0, 0x56, 0x33, 0x41, 0x61, 0xfe, 0x22, 0x8e, 0x5d, 0x65, 0x3e, 0x2b, 0x87, 0xd0, 0xc8, 0x3f, 0x80, 0xb6, + 0xd9, 0x94, 0x2d, 0xa8, 0x35, 0x2c, 0xf0, 0x23, 0x95, 0x81, 0x1a, 0xa6, 0x5b, 0xd8, 0xc7, 0x99, 0x6c, 0x40, 0x93, + 0x68, 0x73, 0xf5, 0x93, 0x5c, 0x93, 0x89, 0x02, 0x0d, 0x2d, 0x80, 0xff, 0x29, 0x92, 0x07, 0xba, 0x91, 0x72, 0x01, + 0x10, 0x34, 0x95, 0x78, 0x2a, 0x11, 0xe6, 0xba, 0xa5, 0xf7, 0xfd, 0xf9, 0x0e, 0x21, 0xd3, 0xd2, 0xfb, 0xf8, 0xb6, + 0x4c, 0xbd, 0x02, 0xb2, 0x40, 0x01, 0x98, 0x8f, 0x45, 0x81, 0x0a, 0x5f, 0x5e, 0x98, 0xe6, 0xd2, 0x84, 0xf4, 0x4b, + 0x8d, 0xdb, 0x0a, 0x6d, 0x4a, 0xb7, 0x9c, 0xaa, 0x37, 0x68, 0x58, 0xa9, 0xdd, 0x85, 0xda, 0xb7, 0x42, 0xc2, 0x08, + 0xcf, 0xef, 0x64, 0x6b, 0x33, 0x6e, 0xfe, 0x71, 0x35, 0x7f, 0x65, 0x65, 0x53, 0x7c, 0x96, 0x64, 0x34, 0x15, 0x4f, + 0xe8, 0x90, 0xa7, 0x10, 0xb3, 0x28, 0x70, 0x82, 0xf2, 0x5d, 0xcb, 0x6f, 0x27, 0xd7, 0x67, 0x05, 0x0a, 0x56, 0x16, + 0x28, 0x7f, 0x7d, 0x94, 0x41, 0xeb, 0xcb, 0xd5, 0x5e, 0xd3, 0xbd, 0xbd, 0xf7, 0x25, 0x9a, 0x34, 0x94, 0x12, 0x0a, + 0x8b, 0x69, 0x29, 0x95, 0x46, 0x47, 0x72, 0x77, 0xbd, 0xc2, 0x09, 0x60, 0x18, 0x86, 0xcd, 0x7b, 0x9e, 0x13, 0x91, + 0x8f, 0x56, 0x59, 0xbc, 0x76, 0x4e, 0x30, 0xdb, 0x70, 0x01, 0x0e, 0x0f, 0xa6, 0xb6, 0xf2, 0x16, 0x65, 0x65, 0x32, + 0x6c, 0x01, 0xc3, 0x39, 0x20, 0xcb, 0x93, 0x66, 0x88, 0x45, 0x81, 0x1b, 0xcd, 0x92, 0x73, 0xd0, 0x2b, 0xc7, 0x38, + 0xf3, 0xc7, 0x90, 0xfe, 0x5a, 0x39, 0xb2, 0x08, 0x61, 0x95, 0x98, 0x63, 0xa5, 0x12, 0x9c, 0x3d, 0xdf, 0xe4, 0x52, + 0x36, 0x44, 0x4d, 0xa5, 0xd4, 0x91, 0x2d, 0x50, 0xd1, 0xc1, 0x9f, 0x7b, 0x4c, 0x2b, 0x6e, 0x26, 0x6e, 0x06, 0x0c, + 0xf8, 0x89, 0xf0, 0x54, 0x30, 0x0a, 0x64, 0x06, 0xf7, 0x67, 0x5e, 0x65, 0xea, 0x36, 0x97, 0xdd, 0xb0, 0x46, 0xdc, + 0xd8, 0x46, 0x13, 0x97, 0x71, 0xbd, 0xf3, 0x92, 0x97, 0x0e, 0x55, 0x06, 0xb5, 0x30, 0x5c, 0xb0, 0x4c, 0x24, 0xb1, + 0x96, 0x3f, 0x54, 0x49, 0xd1, 0x45, 0x23, 0x4c, 0x25, 0x18, 0xef, 0xe4, 0x1e, 0xd0, 0x1c, 0xfe, 0x2e, 0xce, 0x84, + 0xb5, 0xa3, 0xc6, 0x89, 0x2d, 0xe7, 0xb4, 0xa4, 0xfe, 0x5b, 0x48, 0x75, 0x59, 0x3d, 0xf3, 0xcf, 0xa5, 0x2c, 0x64, + 0x38, 0xab, 0x30, 0xf6, 0x44, 0x32, 0x76, 0x04, 0x7a, 0x9a, 0x49, 0xfc, 0xee, 0xea, 0x8c, 0x17, 0xa6, 0xa5, 0x9c, + 0x26, 0xb1, 0x37, 0x45, 0xb4, 0xdc, 0xfa, 0xbd, 0xb2, 0x1b, 0x01, 0x23, 0x90, 0x05, 0x84, 0x35, 0x67, 0x4f, 0x10, + 0xce, 0x6a, 0xb5, 0x76, 0x76, 0x4a, 0x4b, 0x27, 0x49, 0x09, 0x23, 0x83, 0x80, 0x2e, 0x10, 0x7c, 0x45, 0x86, 0x42, + 0xc8, 0xdf, 0x64, 0x66, 0x67, 0xe0, 0x6b, 0x3f, 0x7b, 0xeb, 0xd9, 0x5c, 0xcd, 0x6e, 0x5b, 0x04, 0x4d, 0x61, 0x3d, + 0x5e, 0x19, 0x70, 0x79, 0x73, 0x7f, 0x82, 0x07, 0xc0, 0xbd, 0xd3, 0xc4, 0x90, 0x8a, 0x86, 0xda, 0x42, 0xb1, 0x84, + 0xe2, 0xf4, 0xb5, 0x51, 0x99, 0x95, 0x68, 0x4f, 0xd6, 0x16, 0xa5, 0x31, 0x2b, 0x48, 0x96, 0xe7, 0x19, 0x2d, 0xc3, + 0xfb, 0x2b, 0xe9, 0x97, 0x52, 0xb8, 0xac, 0x7b, 0xdb, 0xcf, 0xa7, 0x44, 0x60, 0x8b, 0x50, 0xdf, 0x6c, 0x8b, 0x7d, + 0x94, 0x60, 0xc2, 0xb9, 0xd6, 0x42, 0xf1, 0xd7, 0x4d, 0x42, 0x11, 0x27, 0xfa, 0xc8, 0x4b, 0x81, 0xd8, 0x7c, 0x80, + 0x40, 0xd4, 0x6e, 0x76, 0x23, 0x13, 0x41, 0x1d, 0xa9, 0xc8, 0xc4, 0xea, 0x96, 0x92, 0x04, 0x33, 0xbd, 0x1b, 0x9d, + 0xd6, 0x72, 0xc9, 0x7a, 0x0d, 0x70, 0x23, 0xb9, 0x2e, 0xfc, 0x6c, 0xaa, 0x9f, 0x16, 0x27, 0x56, 0x6e, 0x60, 0x8f, + 0x15, 0x26, 0x0b, 0xf2, 0x21, 0xc1, 0xd9, 0x93, 0x49, 0x59, 0x92, 0xa6, 0x35, 0x05, 0x69, 0x02, 0x27, 0xac, 0x08, + 0x33, 0x01, 0xc4, 0x52, 0x56, 0x68, 0x03, 0xd2, 0xdb, 0x98, 0xfb, 0x67, 0xcc, 0xcb, 0x4f, 0x6b, 0xa2, 0x15, 0xb9, + 0xa2, 0xd4, 0x87, 0x4a, 0xbe, 0x81, 0x86, 0x40, 0xeb, 0x87, 0x3b, 0xd2, 0x04, 0x2d, 0x45, 0x39, 0xb2, 0xe5, 0x10, + 0x6e, 0x80, 0x13, 0x6d, 0xe7, 0xbd, 0x8a, 0xf0, 0x6e, 0x90, 0x26, 0x98, 0x5b, 0x74, 0xfd, 0x9c, 0x88, 0x0a, 0x2b, + 0x19, 0x13, 0x6d, 0x29, 0xe1, 0x50, 0x92, 0xa9, 0x20, 0x49, 0xaf, 0xd1, 0x07, 0x05, 0xb4, 0x1d, 0x9f, 0x26, 0xa5, + 0x09, 0x1c, 0xd7, 0x6a, 0x28, 0x34, 0xb3, 0x8e, 0x7b, 0xac, 0x16, 0xf7, 0x31, 0xc5, 0xb1, 0x32, 0x4c, 0x2e, 0xf6, + 0xf6, 0xbc, 0xb0, 0x9c, 0xb7, 0x17, 0xf7, 0x11, 0xe6, 0xcb, 0xa5, 0x27, 0xc1, 0x0a, 0xd1, 0x72, 0x19, 0xda, 0x60, + 0xc9, 0x6a, 0xe8, 0x36, 0xed, 0x0a, 0x32, 0x95, 0x02, 0x70, 0x0a, 0x10, 0xd6, 0x88, 0x17, 0x6a, 0xf7, 0x5e, 0x08, + 0xee, 0xa8, 0x5a, 0xd2, 0x8b, 0x6b, 0xcd, 0xbe, 0xc5, 0xb8, 0x7a, 0x71, 0x9f, 0x84, 0x39, 0xdf, 0xdb, 0xdb, 0xc9, + 0xb4, 0x88, 0xfc, 0x00, 0xa2, 0xec, 0x83, 0x94, 0x2c, 0x6a, 0x40, 0x7b, 0x37, 0x56, 0x9d, 0x01, 0x05, 0x45, 0xe9, + 0x6d, 0x35, 0xed, 0x2a, 0x59, 0x10, 0x45, 0x23, 0xac, 0x83, 0xc1, 0x5d, 0xb0, 0xec, 0x0b, 0x32, 0x7f, 0x21, 0x8a, + 0x1c, 0xeb, 0x5f, 0x37, 0x66, 0x56, 0xfb, 0xbe, 0x1f, 0xa6, 0x23, 0x19, 0xcb, 0x30, 0x61, 0x58, 0x49, 0xfc, 0x07, + 0x1a, 0x4c, 0x6b, 0xe2, 0x5e, 0x31, 0x57, 0x9f, 0x28, 0xf0, 0x8d, 0x6a, 0x63, 0xee, 0x92, 0x3c, 0xdd, 0xe8, 0x65, + 0x50, 0x90, 0x7c, 0xf8, 0xad, 0x90, 0x1c, 0x6a, 0x48, 0x14, 0x79, 0xac, 0xe0, 0x6c, 0x0b, 0x2e, 0x9e, 0x8a, 0x15, + 0x9c, 0x6d, 0xc7, 0xad, 0xc1, 0xd4, 0x37, 0xdb, 0xe0, 0xb3, 0x78, 0x83, 0x02, 0xb4, 0x2c, 0xb0, 0xa0, 0x3c, 0x5a, + 0xd5, 0xbd, 0x14, 0x2b, 0x05, 0x61, 0x2a, 0x88, 0xc7, 0xaa, 0x07, 0xa0, 0xd4, 0x46, 0x2d, 0xc3, 0x97, 0x05, 0x53, + 0x64, 0xb9, 0x04, 0xaa, 0xa9, 0x2b, 0x40, 0x4e, 0xda, 0xdb, 0x3e, 0xdd, 0xdb, 0x03, 0xdb, 0x00, 0x94, 0x38, 0x7f, + 0x10, 0x4e, 0xc5, 0x2c, 0x05, 0x55, 0x2a, 0x33, 0xbf, 0xa1, 0x18, 0x6e, 0x81, 0xc8, 0x32, 0xf8, 0x01, 0x05, 0xd3, + 0x30, 0xcb, 0xd8, 0x5c, 0x95, 0xe9, 0xdf, 0x98, 0x13, 0x43, 0xca, 0x99, 0xd2, 0x09, 0x13, 0xd4, 0x4e, 0x34, 0x9d, + 0x56, 0xd1, 0xf6, 0x6c, 0x4e, 0x13, 0xf1, 0x82, 0x65, 0x82, 0x26, 0xb0, 0xfc, 0x92, 0xe2, 0x60, 0x45, 0x19, 0x82, + 0x03, 0x5b, 0xe9, 0x15, 0x46, 0xd1, 0xbd, 0x5d, 0x44, 0x55, 0x07, 0x1a, 0x87, 0x49, 0x14, 0xab, 0x49, 0xec, 0x7c, + 0x46, 0x93, 0xc3, 0x59, 0xb4, 0xb4, 0xf3, 0x69, 0x4a, 0x65, 0x43, 0x72, 0x77, 0x8f, 0x11, 0x23, 0x09, 0x8c, 0xf4, + 0xbc, 0x57, 0x6b, 0x81, 0x88, 0xf7, 0x96, 0x4d, 0xb0, 0x57, 0x82, 0x85, 0xc5, 0x51, 0xfd, 0x2a, 0x9c, 0x86, 0x6e, + 0x7e, 0xd9, 0x78, 0xa5, 0x6d, 0x93, 0x70, 0x90, 0x74, 0x72, 0xbc, 0xdd, 0xb2, 0x7a, 0x69, 0x24, 0x87, 0x91, 0x16, + 0xec, 0xa1, 0x8c, 0x19, 0x2d, 0x0c, 0x79, 0x21, 0x73, 0x14, 0x77, 0x05, 0xf9, 0x00, 0x77, 0x86, 0x9e, 0x8b, 0x49, + 0xbc, 0x72, 0x35, 0xa6, 0xbd, 0x5b, 0x68, 0xff, 0xbb, 0xc2, 0x7b, 0x87, 0xdf, 0x42, 0x60, 0xf7, 0xa7, 0xb2, 0xf9, + 0x7a, 0x40, 0xf7, 0xa7, 0x12, 0x41, 0x3f, 0x05, 0x6b, 0xed, 0xac, 0x40, 0x6e, 0xcb, 0x3f, 0xf1, 0x1b, 0xae, 0xd1, + 0x96, 0x7e, 0x55, 0x61, 0x24, 0x95, 0x69, 0x29, 0xcf, 0x03, 0x2e, 0xf3, 0xd4, 0x20, 0x5f, 0xae, 0x6a, 0x21, 0x51, + 0x9d, 0x61, 0xa8, 0x74, 0xf8, 0x6d, 0xdb, 0xa3, 0x65, 0x4c, 0xa2, 0xec, 0x8c, 0x37, 0x61, 0x2a, 0x76, 0xe1, 0x94, + 0xf1, 0xb5, 0x7b, 0x78, 0x63, 0x02, 0x1e, 0xb4, 0x87, 0x4d, 0x61, 0x19, 0xdb, 0x99, 0xba, 0x07, 0x64, 0x8f, 0x4f, + 0xb8, 0xd1, 0xdd, 0xaa, 0x56, 0xc6, 0x1b, 0xb0, 0xff, 0x11, 0x1e, 0x9b, 0xcb, 0x71, 0x54, 0x73, 0x60, 0x1a, 0x2c, + 0xf2, 0xc2, 0x29, 0xc0, 0x95, 0xf2, 0x96, 0x22, 0xcc, 0x73, 0x19, 0xe0, 0xfe, 0x16, 0x7f, 0xa7, 0x59, 0xe2, 0xb0, + 0xe0, 0x38, 0x67, 0x0f, 0xe5, 0x88, 0x0a, 0xfc, 0x22, 0x7e, 0x0f, 0x74, 0x2c, 0x29, 0x34, 0x37, 0x54, 0xf4, 0x94, + 0xeb, 0x85, 0x6c, 0x4d, 0x4b, 0xc5, 0xb4, 0x48, 0xa9, 0x91, 0xd3, 0x6c, 0xc8, 0xe3, 0x34, 0x56, 0xb6, 0x28, 0x4e, + 0x55, 0x65, 0x5e, 0xb4, 0x05, 0x8b, 0x65, 0x68, 0x71, 0xb9, 0xf4, 0xaa, 0xa8, 0x26, 0xcc, 0x8a, 0x64, 0x20, 0xcc, + 0xac, 0x8c, 0x8a, 0x8a, 0x66, 0xad, 0xfa, 0x78, 0x68, 0x35, 0xa1, 0xc8, 0xe8, 0xe6, 0x15, 0x38, 0x6c, 0x17, 0x82, + 0xea, 0x6e, 0xfb, 0x14, 0xb0, 0x5a, 0x5d, 0x31, 0x91, 0x85, 0xa1, 0x5f, 0x8b, 0x54, 0xd9, 0x32, 0xa7, 0x75, 0x03, + 0x7e, 0xd1, 0x3d, 0xc9, 0xb2, 0x1a, 0x75, 0xeb, 0xf5, 0x56, 0xb2, 0xd1, 0x53, 0xbe, 0x2d, 0xd9, 0xa8, 0xa2, 0xed, + 0xee, 0x34, 0xd0, 0xfd, 0x69, 0xa9, 0x6a, 0xae, 0xcd, 0x4d, 0x7e, 0xc3, 0x74, 0x4d, 0xa0, 0x4d, 0x85, 0x66, 0xc3, + 0x55, 0x2e, 0xf2, 0x7c, 0x58, 0x5c, 0x26, 0x90, 0xb9, 0x3b, 0x43, 0x45, 0xff, 0xda, 0x6a, 0x94, 0xd7, 0x71, 0xbd, + 0x6f, 0xc9, 0x28, 0xe6, 0xd7, 0x61, 0xfc, 0x0e, 0xe6, 0x2b, 0x2b, 0x9f, 0xdf, 0x45, 0x69, 0x28, 0xa8, 0xe6, 0x2e, + 0x25, 0x0c, 0xdf, 0x5a, 0x30, 0x7c, 0xab, 0xf8, 0x74, 0xd9, 0x1f, 0x2f, 0x5e, 0x14, 0x03, 0x04, 0xc3, 0xdc, 0xb0, + 0x8c, 0x4b, 0xb1, 0x79, 0x8e, 0x55, 0x16, 0x76, 0x59, 0xb0, 0xb0, 0x4b, 0xe1, 0xad, 0x0e, 0xe5, 0x79, 0xdf, 0x6d, + 0x1e, 0x65, 0x9d, 0xb3, 0x7d, 0x57, 0x1e, 0xfc, 0xef, 0x82, 0x7b, 0xfb, 0x58, 0x5c, 0xee, 0xc0, 0x3f, 0x90, 0xe9, + 0x2a, 0x0a, 0xe4, 0xe7, 0x90, 0x76, 0x20, 0x48, 0xc7, 0xba, 0x73, 0x50, 0xca, 0x29, 0x93, 0x08, 0xe4, 0x0d, 0x66, + 0x99, 0xe0, 0x13, 0x3d, 0x66, 0xa6, 0xaf, 0x19, 0xc9, 0x4a, 0x70, 0x45, 0xcb, 0x68, 0x7b, 0x50, 0xbd, 0xc8, 0xb5, + 0xf8, 0xc8, 0x92, 0x28, 0xc8, 0xb0, 0x96, 0x22, 0x59, 0x90, 0xe4, 0xc4, 0x24, 0x1b, 0xaf, 0xd7, 0xe1, 0x21, 0x4b, + 0x58, 0x36, 0xa6, 0xa9, 0xc7, 0xd1, 0x62, 0xdb, 0x64, 0x1c, 0x02, 0x32, 0x6a, 0x32, 0xfc, 0x7d, 0x79, 0xe1, 0xcf, + 0x87, 0xd1, 0xc0, 0x0f, 0x34, 0xa1, 0x62, 0xcc, 0x23, 0x48, 0x4c, 0xf1, 0xa3, 0xe2, 0x46, 0xd3, 0xde, 0xde, 0x8e, + 0xe7, 0x4a, 0xb7, 0x04, 0x5c, 0xfd, 0xb6, 0x6b, 0x50, 0x77, 0x01, 0xd7, 0x73, 0xca, 0xa9, 0x29, 0x5a, 0xd0, 0xd5, + 0x9b, 0x2c, 0xc2, 0xff, 0x48, 0xef, 0x70, 0x8a, 0xf2, 0x3c, 0x50, 0x50, 0xbb, 0x43, 0x46, 0xe3, 0xc8, 0xc5, 0x1f, + 0xe9, 0x5d, 0x50, 0xdc, 0x16, 0x97, 0x97, 0x9b, 0xe5, 0x06, 0xba, 0xfc, 0x26, 0x71, 0x71, 0x39, 0x49, 0xb0, 0xc8, + 0x31, 0x4f, 0xd9, 0x08, 0x88, 0xf3, 0x5b, 0x7a, 0x17, 0xa8, 0xf1, 0x98, 0x75, 0x59, 0x0f, 0x2d, 0x0c, 0xea, 0x7d, + 0xab, 0xd8, 0xde, 0x06, 0x6d, 0x50, 0xf4, 0x64, 0xdf, 0x3e, 0xa9, 0xb4, 0x2b, 0xcd, 0x43, 0x84, 0xf2, 0x87, 0x2e, + 0x05, 0x7f, 0x6d, 0x8b, 0x36, 0x51, 0x49, 0x7d, 0x5d, 0xe9, 0x44, 0xa1, 0x43, 0x99, 0xeb, 0x71, 0xe9, 0xa5, 0xe6, + 0xd4, 0xe9, 0x3b, 0x08, 0x96, 0x23, 0xec, 0x6b, 0xa1, 0x07, 0x0d, 0xbe, 0x57, 0x29, 0x21, 0x65, 0x24, 0xe9, 0x65, + 0xd9, 0xcf, 0xb9, 0xf4, 0x00, 0xef, 0x90, 0xd2, 0x12, 0xca, 0xeb, 0x98, 0xb9, 0x49, 0x17, 0xfd, 0x41, 0x10, 0x6f, + 0x61, 0x96, 0x10, 0xa4, 0x36, 0x16, 0x45, 0x0e, 0x54, 0xa8, 0xe9, 0x4b, 0x65, 0x00, 0xb2, 0xa1, 0xc7, 0xd6, 0xa4, + 0x66, 0x22, 0xa5, 0xa6, 0x6f, 0x61, 0x7c, 0x8b, 0x94, 0xa4, 0x12, 0x19, 0x52, 0x89, 0x94, 0x42, 0x4f, 0x6f, 0xae, + 0x26, 0x21, 0x7b, 0x43, 0x8b, 0xeb, 0x73, 0x6a, 0xcf, 0x93, 0x0a, 0x58, 0x9e, 0x1c, 0x07, 0xe5, 0x01, 0x2c, 0x89, + 0xaa, 0x06, 0xb9, 0x71, 0xe7, 0xa4, 0x26, 0xbf, 0xd5, 0xe3, 0xbe, 0x59, 0x16, 0x31, 0x28, 0xf1, 0xc6, 0x68, 0x91, + 0x7a, 0x63, 0x9c, 0x40, 0x3e, 0x22, 0xcf, 0x0b, 0xf8, 0xa9, 0xbd, 0x1b, 0x95, 0x6c, 0xe5, 0xcd, 0x57, 0xfc, 0x40, + 0x99, 0x17, 0x90, 0xa3, 0x89, 0x53, 0xc3, 0x53, 0x52, 0x4f, 0xde, 0xb5, 0xb3, 0xb6, 0xed, 0x27, 0x9d, 0xa2, 0xa3, + 0x01, 0xfb, 0x41, 0x78, 0x0b, 0x6b, 0x15, 0xf6, 0x5d, 0x6e, 0x7d, 0xe5, 0x4f, 0x07, 0xfb, 0xca, 0x24, 0x52, 0x2f, + 0x23, 0x2b, 0x12, 0xe7, 0xfe, 0x5c, 0xcb, 0x5f, 0x66, 0x34, 0xbd, 0xbb, 0xa0, 0x90, 0xeb, 0xcc, 0xe1, 0xae, 0x6f, + 0xb9, 0x0d, 0x65, 0x9e, 0x7a, 0x37, 0x91, 0xca, 0x4a, 0x5e, 0xbd, 0x04, 0xb8, 0x7a, 0x45, 0x30, 0x97, 0xd1, 0x46, + 0xcb, 0x11, 0xa3, 0x4e, 0x0b, 0xdd, 0x7a, 0x79, 0x92, 0xb6, 0x19, 0xf8, 0xd7, 0x4a, 0x4c, 0xeb, 0x60, 0x01, 0xe6, + 0xf6, 0x85, 0xd4, 0x5e, 0xd6, 0x5f, 0xf5, 0xca, 0x40, 0x11, 0x84, 0xef, 0x92, 0xed, 0x4b, 0xdd, 0x94, 0x35, 0xbb, + 0x7d, 0xa9, 0x95, 0xa0, 0x9f, 0x4c, 0xf9, 0xc1, 0x7a, 0x9e, 0xe2, 0xf2, 0x32, 0xcb, 0x73, 0x94, 0x03, 0x78, 0x3f, + 0xb6, 0x3d, 0xef, 0x47, 0x9d, 0x34, 0xe8, 0x43, 0x2c, 0xf6, 0x22, 0xe6, 0x86, 0x89, 0x97, 0xf3, 0xff, 0xb8, 0x36, + 0xff, 0x8f, 0xd6, 0x95, 0x53, 0x30, 0x8d, 0x46, 0x09, 0x8d, 0x0c, 0xeb, 0x44, 0x8a, 0x00, 0xa5, 0xde, 0x96, 0x09, + 0xf2, 0xf1, 0x2a, 0x00, 0x8d, 0x6b, 0x31, 0xe4, 0x89, 0xa8, 0x0f, 0xc3, 0x09, 0x8b, 0xef, 0x82, 0x19, 0xab, 0x4f, + 0x78, 0xc2, 0xb3, 0x69, 0x38, 0xa0, 0x38, 0xbb, 0xcb, 0x04, 0x9d, 0xd4, 0x67, 0x0c, 0x3f, 0xa7, 0xf1, 0x9c, 0x0a, + 0x36, 0x08, 0xb1, 0x7b, 0x96, 0xb2, 0x30, 0x76, 0x5e, 0x85, 0x69, 0xca, 0x6f, 0x5c, 0xfc, 0x96, 0x5f, 0x73, 0xc1, + 0xf1, 0xeb, 0xdb, 0xbb, 0x11, 0x4d, 0xf0, 0xfb, 0xeb, 0x59, 0x22, 0x66, 0x38, 0x0b, 0x93, 0xac, 0x9e, 0xd1, 0x94, + 0x0d, 0xdb, 0x03, 0x1e, 0xf3, 0xb4, 0x0e, 0x29, 0xdb, 0x13, 0x1a, 0xc4, 0x6c, 0x34, 0x16, 0x4e, 0x14, 0xa6, 0x1f, + 0xdb, 0xf5, 0xfa, 0x34, 0x65, 0x93, 0x30, 0xbd, 0xab, 0xcb, 0x16, 0xc1, 0x97, 0x8d, 0x83, 0xf0, 0xf1, 0xf0, 0xb0, + 0x2d, 0xd2, 0x30, 0xc9, 0x18, 0x6c, 0x53, 0x10, 0xc6, 0xb1, 0x73, 0x70, 0xd4, 0x98, 0x64, 0x3b, 0x2a, 0x90, 0x17, + 0x26, 0x22, 0xbf, 0xc2, 0x1f, 0x01, 0x6e, 0xff, 0x5a, 0x24, 0xf8, 0x7a, 0x26, 0x04, 0x4f, 0x16, 0x83, 0x59, 0x9a, + 0xf1, 0x34, 0x98, 0x72, 0x96, 0x08, 0x9a, 0xb6, 0xaf, 0x79, 0x1a, 0xd1, 0xb4, 0x9e, 0x86, 0x11, 0x9b, 0x65, 0xc1, + 0xe1, 0xf4, 0xb6, 0x0d, 0x9a, 0xc5, 0x28, 0xe5, 0xb3, 0x24, 0xd2, 0x73, 0xb1, 0x64, 0x4c, 0x53, 0x26, 0xec, 0x0a, + 0xf9, 0x0a, 0x93, 0x20, 0x66, 0x09, 0x0d, 0xd3, 0xfa, 0x08, 0x3a, 0x83, 0x59, 0xd4, 0x88, 0xe8, 0x08, 0xa7, 0xa3, + 0xeb, 0xd0, 0x6b, 0xb6, 0x1e, 0x61, 0xf3, 0xbf, 0x7f, 0x84, 0x9c, 0xc6, 0xe6, 0xe2, 0x66, 0xa3, 0xf1, 0x27, 0xd4, + 0x5e, 0x99, 0x45, 0x02, 0x14, 0x34, 0xa7, 0xb7, 0x4e, 0xc6, 0x21, 0xa7, 0x6d, 0x53, 0xcf, 0xf6, 0x34, 0x8c, 0x20, + 0x21, 0x38, 0x68, 0x4d, 0x6f, 0x73, 0x58, 0x5d, 0xa0, 0x92, 0x4c, 0xf5, 0x22, 0xf5, 0xd3, 0xe2, 0xb7, 0x42, 0x7c, + 0xb2, 0x19, 0xe2, 0x96, 0x81, 0xb8, 0xc4, 0x7a, 0x3d, 0x9a, 0xa5, 0x32, 0xb6, 0x1a, 0x34, 0x33, 0x05, 0xc8, 0x98, + 0xcf, 0x69, 0x6a, 0xe0, 0x90, 0x0f, 0xbf, 0x19, 0x8c, 0xd6, 0x66, 0x30, 0x4e, 0x3e, 0x05, 0x46, 0x9a, 0x44, 0x8b, + 0xea, 0xbe, 0x36, 0x53, 0x3a, 0x69, 0x8f, 0x29, 0xd0, 0x53, 0xd0, 0x82, 0xdf, 0x37, 0x2c, 0x12, 0x63, 0xf5, 0x53, + 0x92, 0xf3, 0x8d, 0xaa, 0x3b, 0x6a, 0x34, 0xd4, 0x73, 0xc6, 0x7e, 0xa5, 0x41, 0xd3, 0x87, 0x06, 0xf9, 0x15, 0xfe, + 0x5b, 0x71, 0x99, 0xb7, 0xca, 0x3d, 0xf1, 0xb7, 0xf6, 0x2d, 0x5f, 0x2b, 0x49, 0xb1, 0xbc, 0x11, 0x8d, 0x53, 0x23, + 0x2b, 0x95, 0xf0, 0x01, 0xb7, 0x9d, 0x3c, 0x4f, 0x84, 0x75, 0x8a, 0x5b, 0x9c, 0xac, 0xfb, 0xad, 0xca, 0xbb, 0x08, + 0x20, 0xd2, 0x61, 0x25, 0x1b, 0xf2, 0x76, 0xd2, 0x21, 0x8d, 0x76, 0x52, 0xaf, 0x23, 0x8f, 0x93, 0xb4, 0x97, 0xe8, + 0xf4, 0x3c, 0x8f, 0x75, 0xb9, 0x34, 0xb6, 0x33, 0x14, 0x70, 0xb8, 0x6a, 0xba, 0x5c, 0x96, 0x61, 0x00, 0x26, 0xaf, + 0x6b, 0xfc, 0x4d, 0xe8, 0x06, 0x38, 0xb3, 0x38, 0x79, 0x62, 0x5e, 0xec, 0x92, 0x1a, 0x5e, 0x11, 0xf3, 0x81, 0xc4, + 0x9c, 0x3f, 0x0d, 0xc5, 0x18, 0xbc, 0x14, 0x85, 0xf8, 0x29, 0x93, 0x98, 0xdc, 0x7d, 0x17, 0x75, 0xd3, 0x22, 0xc3, + 0x0d, 0x32, 0xf9, 0xd2, 0x1c, 0x46, 0xf9, 0x4e, 0x10, 0x18, 0x11, 0x7f, 0x43, 0x94, 0x4d, 0x67, 0x2c, 0xba, 0xe1, + 0x43, 0x2d, 0x3a, 0x9a, 0x08, 0x26, 0x73, 0xb7, 0x4d, 0xc4, 0x61, 0x1c, 0x66, 0x97, 0x03, 0x75, 0x57, 0x32, 0x2b, + 0x6f, 0x06, 0x84, 0x12, 0x7a, 0x65, 0xa4, 0xd1, 0x54, 0xda, 0xa3, 0x3f, 0x8a, 0xad, 0xf6, 0x49, 0x7a, 0x9f, 0x7d, + 0x52, 0x2c, 0x3c, 0xe3, 0xb3, 0x74, 0x00, 0xe1, 0x48, 0x2d, 0xf5, 0xd6, 0x1d, 0x37, 0xae, 0x54, 0x31, 0x5c, 0x2c, + 0xac, 0x4c, 0x50, 0x81, 0x99, 0xfd, 0x52, 0x09, 0x2a, 0x43, 0x5e, 0xea, 0xbe, 0x86, 0x16, 0x71, 0x66, 0x49, 0x20, + 0xb3, 0x23, 0x99, 0xd4, 0xe8, 0x25, 0xa4, 0x93, 0xf8, 0xb3, 0x84, 0xfd, 0x32, 0xa3, 0x97, 0x0c, 0x74, 0x4d, 0xe6, + 0xb3, 0x48, 0xc6, 0x9a, 0x40, 0xf6, 0xd5, 0x9b, 0x10, 0xbc, 0x60, 0x91, 0xda, 0x98, 0x44, 0x56, 0xea, 0xdc, 0x26, + 0xb7, 0xee, 0x82, 0xbf, 0x18, 0xb4, 0x03, 0x86, 0x23, 0x3e, 0x09, 0x59, 0x12, 0x48, 0x97, 0x6f, 0x31, 0x58, 0x00, + 0xad, 0x31, 0x8b, 0x82, 0x44, 0x6f, 0x4f, 0x13, 0xf9, 0x1f, 0x38, 0x4b, 0x64, 0xd7, 0xbc, 0xcd, 0x25, 0x42, 0x15, + 0xfa, 0x88, 0x41, 0xf0, 0x99, 0x92, 0x6b, 0x1c, 0x61, 0xbb, 0xba, 0xb8, 0x76, 0x5e, 0xd9, 0x81, 0xc6, 0xca, 0x46, + 0x29, 0x23, 0x80, 0xaf, 0x96, 0x66, 0x3c, 0x15, 0x9e, 0x37, 0xc6, 0x31, 0x22, 0x9d, 0xb1, 0x74, 0x76, 0x9d, 0xc6, + 0xf2, 0x4f, 0xb7, 0xde, 0x0c, 0x9a, 0x85, 0xf9, 0x5e, 0xb9, 0x0d, 0xac, 0x92, 0xa3, 0xf4, 0x8d, 0x52, 0xb9, 0x8c, + 0xe2, 0xb7, 0x5a, 0x6a, 0xf9, 0x5c, 0x2c, 0x17, 0xeb, 0xe3, 0xa6, 0x44, 0x95, 0x57, 0x01, 0x42, 0x06, 0x8b, 0xb6, + 0x4c, 0x85, 0xf2, 0x72, 0xdd, 0x85, 0x2a, 0x79, 0xa5, 0x44, 0xf4, 0xe5, 0xee, 0x22, 0xd5, 0x33, 0xe6, 0x57, 0xcc, + 0x38, 0x99, 0xaa, 0x24, 0x97, 0x6b, 0x8c, 0x58, 0x7a, 0xe8, 0xa6, 0x66, 0x0a, 0x96, 0x3b, 0x92, 0x6e, 0xa4, 0x5b, + 0x5f, 0x3d, 0xd2, 0x94, 0x94, 0xe1, 0xae, 0xb5, 0x01, 0x20, 0x57, 0x6f, 0x13, 0x60, 0x60, 0xb6, 0x66, 0xc2, 0x2c, + 0x01, 0xb4, 0xb1, 0x21, 0x85, 0x8b, 0x34, 0x57, 0xbb, 0x8b, 0xef, 0x44, 0xbe, 0x6f, 0x35, 0x95, 0xbf, 0x59, 0x04, + 0x7f, 0x41, 0x02, 0x2e, 0x94, 0x52, 0x1a, 0xb8, 0x6f, 0x5e, 0x5f, 0xbc, 0x73, 0xf1, 0x35, 0x8f, 0xee, 0x02, 0x57, + 0xa4, 0x33, 0xea, 0xe6, 0xc8, 0x17, 0x63, 0x9a, 0x14, 0x2f, 0xe3, 0xe1, 0x31, 0xf5, 0x63, 0x3e, 0x52, 0x97, 0x32, + 0x57, 0x8d, 0xe4, 0xc1, 0xd5, 0xa9, 0x7c, 0xc9, 0x54, 0xe7, 0x54, 0xa8, 0xd7, 0x7b, 0x89, 0x14, 0x7e, 0x76, 0x20, + 0x84, 0x72, 0xba, 0x2f, 0xc6, 0xf2, 0xe1, 0x02, 0x0e, 0x8c, 0x7c, 0xda, 0x5d, 0xac, 0x11, 0x53, 0x17, 0x86, 0x18, + 0x77, 0xd4, 0x12, 0x32, 0xd9, 0xea, 0x2a, 0x18, 0x5c, 0x5d, 0xe5, 0xa7, 0xfb, 0x30, 0xd6, 0xbe, 0x19, 0x17, 0x20, + 0x34, 0xfd, 0x0b, 0x02, 0x83, 0x97, 0x0d, 0xa5, 0xa4, 0x03, 0x43, 0xc0, 0xbc, 0x51, 0x07, 0x16, 0x09, 0x04, 0x06, + 0xbd, 0xa3, 0xa2, 0x44, 0x9e, 0x58, 0x55, 0xb4, 0x0d, 0x02, 0xd5, 0xb0, 0xa4, 0x7b, 0xe5, 0x4d, 0x2d, 0xf7, 0xd7, + 0x80, 0x14, 0xd9, 0xd0, 0x5d, 0x21, 0xf8, 0x2b, 0x21, 0x3b, 0xdd, 0x57, 0x78, 0xb8, 0xb2, 0x5f, 0x6d, 0xa2, 0x5e, + 0x3b, 0x50, 0x60, 0xab, 0x97, 0x09, 0xfc, 0x51, 0xe0, 0x8f, 0x57, 0xb2, 0xa9, 0x11, 0x46, 0xa0, 0x25, 0x81, 0xd0, + 0x6e, 0x18, 0xad, 0x63, 0xc0, 0xe3, 0x38, 0x9c, 0x66, 0x34, 0x30, 0x3f, 0xb4, 0x5c, 0x02, 0xf1, 0xb6, 0xae, 0x08, + 0xe8, 0xf4, 0x9a, 0x73, 0x50, 0x17, 0xd6, 0xb5, 0x94, 0x79, 0x98, 0x7a, 0xf5, 0xfa, 0xa0, 0x7e, 0x3d, 0x42, 0xb9, + 0x18, 0x2f, 0x6c, 0xa9, 0x76, 0xdc, 0x68, 0xb4, 0x21, 0x17, 0xb2, 0x1e, 0xc6, 0x6c, 0x94, 0x04, 0x31, 0x1d, 0x8a, + 0x5c, 0xc0, 0x2d, 0xb5, 0x85, 0x51, 0x23, 0xfc, 0xd6, 0x51, 0x4a, 0x27, 0x8e, 0x0f, 0xff, 0xde, 0x3f, 0x71, 0x2e, + 0xa2, 0x20, 0x11, 0xe3, 0xba, 0xcc, 0xba, 0x85, 0x3b, 0x03, 0x62, 0x5c, 0x79, 0x5e, 0x58, 0x13, 0x0d, 0x28, 0xa8, + 0x58, 0xb9, 0x48, 0x1d, 0x31, 0xc6, 0x22, 0xb5, 0xdb, 0x25, 0x68, 0xb1, 0xb6, 0x82, 0x75, 0x49, 0x7f, 0x80, 0xf2, + 0x4c, 0x2a, 0xc6, 0xeb, 0x8d, 0x8d, 0xba, 0x54, 0x7d, 0x5a, 0x43, 0x9f, 0xa5, 0xd8, 0xe5, 0xca, 0xb1, 0xbc, 0x50, + 0x3d, 0x1e, 0x82, 0xcc, 0x8a, 0xca, 0x89, 0xed, 0x1e, 0x28, 0x67, 0xc9, 0x74, 0x26, 0x7a, 0xd2, 0xa9, 0x9d, 0xc2, + 0x05, 0x89, 0x3e, 0xb6, 0x4a, 0x00, 0x07, 0xfd, 0x85, 0x02, 0x66, 0x10, 0xc6, 0x03, 0x0f, 0x20, 0x72, 0xea, 0xce, + 0x49, 0x4a, 0x27, 0xa8, 0x3d, 0x61, 0x49, 0x5d, 0xd5, 0x1d, 0x59, 0x6a, 0x89, 0xff, 0x08, 0x9e, 0x72, 0x5f, 0x8e, + 0x86, 0x65, 0xee, 0xea, 0x06, 0x5c, 0x5e, 0xf5, 0xf3, 0xbc, 0x9d, 0x0a, 0xaf, 0xf7, 0xd2, 0x43, 0x7d, 0xfc, 0x8d, + 0xf5, 0x72, 0x16, 0xd7, 0x1c, 0x15, 0x17, 0xb7, 0xd0, 0x96, 0x26, 0xf6, 0x59, 0x90, 0xcd, 0xbe, 0x21, 0xd0, 0xf0, + 0xb9, 0xe7, 0xd2, 0x6c, 0x5a, 0x57, 0xbc, 0xab, 0x2e, 0x49, 0xd6, 0x85, 0xae, 0x48, 0x7b, 0x6a, 0x7f, 0x14, 0x0b, + 0xc9, 0x96, 0xf4, 0x25, 0x0d, 0xe5, 0x4c, 0xe8, 0x17, 0x97, 0x7a, 0xf4, 0xb3, 0x7d, 0x8d, 0x07, 0x55, 0xf8, 0xc9, + 0xd5, 0x59, 0x95, 0xc7, 0x01, 0x5f, 0x2a, 0x5e, 0x60, 0x17, 0xc6, 0x31, 0x4c, 0x78, 0x65, 0xd4, 0x17, 0xfb, 0xa5, + 0x1f, 0x3d, 0xd1, 0xf7, 0x50, 0xae, 0xcf, 0xe9, 0x13, 0xa9, 0x52, 0x5a, 0x6f, 0xcd, 0xdb, 0x11, 0x26, 0x58, 0xa4, + 0xa4, 0x2f, 0x83, 0x70, 0x77, 0x25, 0x2f, 0xba, 0x5d, 0xf2, 0x2e, 0xa5, 0x90, 0x3a, 0x72, 0x41, 0xc4, 0x4d, 0x93, + 0xc8, 0x75, 0xfe, 0x32, 0x88, 0xd9, 0xe0, 0x23, 0x71, 0x77, 0x17, 0x1e, 0x5a, 0xbf, 0xf6, 0x28, 0xb9, 0x82, 0x61, + 0xd8, 0xa8, 0xea, 0x48, 0x4f, 0x7c, 0x8b, 0x17, 0xab, 0xb7, 0xe2, 0xb8, 0x9d, 0xdd, 0x05, 0x30, 0x1e, 0x35, 0x4f, + 0xe7, 0x2a, 0xbf, 0x2c, 0xdf, 0x75, 0x55, 0x42, 0x01, 0x68, 0x56, 0xe5, 0x8e, 0x24, 0x2a, 0xe2, 0x7e, 0x92, 0xd2, + 0x5c, 0x47, 0x31, 0x35, 0x80, 0x53, 0x68, 0xfe, 0xe6, 0x3a, 0x7f, 0x29, 0xca, 0x68, 0xe1, 0xd1, 0x90, 0x29, 0x19, + 0xc4, 0x85, 0xb9, 0xc0, 0x8c, 0xf5, 0x23, 0x2a, 0x42, 0x16, 0xab, 0x2e, 0x6d, 0x63, 0x80, 0xaf, 0xac, 0x68, 0xb9, + 0xcc, 0xaa, 0x6b, 0x61, 0x55, 0x0c, 0xca, 0x95, 0x9d, 0xee, 0x97, 0x70, 0xcb, 0x95, 0xc9, 0x33, 0x69, 0x87, 0x06, + 0xcb, 0x15, 0xaa, 0x3a, 0xe7, 0x2f, 0x03, 0x79, 0x6d, 0x08, 0x00, 0xe4, 0x1a, 0x40, 0x08, 0x5a, 0xab, 0x6b, 0x31, + 0x5e, 0x4c, 0xb8, 0x2f, 0xc2, 0x74, 0x44, 0xc5, 0x0a, 0x62, 0x63, 0x95, 0xa3, 0xda, 0x36, 0x01, 0xea, 0x35, 0x68, + 0xc3, 0x2a, 0xb4, 0x57, 0x80, 0xf4, 0xee, 0xee, 0x82, 0xe5, 0x64, 0x77, 0x41, 0x93, 0x01, 0x8f, 0xe8, 0xfb, 0xb7, + 0xdf, 0xc0, 0x25, 0x47, 0x9e, 0x80, 0x61, 0x31, 0x46, 0x20, 0x38, 0xe5, 0xe6, 0x28, 0x11, 0xc2, 0xa5, 0x08, 0x51, + 0x9c, 0xc0, 0x91, 0x73, 0x49, 0x10, 0x73, 0xd7, 0xe9, 0x2a, 0xc8, 0x69, 0xa4, 0x60, 0x26, 0x89, 0xec, 0xc5, 0xf3, + 0xd3, 0x7d, 0xd5, 0x5a, 0x89, 0x00, 0xd5, 0x08, 0x90, 0x20, 0xcf, 0x69, 0x89, 0x03, 0xc8, 0x6b, 0xb6, 0xf1, 0x10, + 0xb1, 0x79, 0x41, 0x6c, 0xf2, 0x02, 0x55, 0xe7, 0x34, 0x0e, 0xaf, 0x69, 0xdc, 0xd9, 0x5d, 0x24, 0xcb, 0x65, 0x23, + 0x3f, 0xdd, 0x57, 0x8f, 0xce, 0xa9, 0xe4, 0x1b, 0xea, 0x85, 0x97, 0x72, 0x8b, 0xe1, 0x56, 0x22, 0x64, 0x7b, 0x9a, + 0x34, 0xa7, 0x40, 0x0f, 0x90, 0xbb, 0x8e, 0x4c, 0xb0, 0x90, 0x8d, 0x0a, 0x85, 0x28, 0x77, 0x1d, 0x16, 0xad, 0x97, + 0x65, 0x82, 0x4e, 0xa1, 0x74, 0xbc, 0x5c, 0x36, 0x73, 0xd7, 0x99, 0xb0, 0x04, 0x9e, 0x92, 0xe5, 0x52, 0x5e, 0xf8, + 0x9b, 0xb0, 0xc4, 0x6b, 0x00, 0xd9, 0xba, 0xce, 0x24, 0xbc, 0x95, 0x0b, 0x36, 0x35, 0xe1, 0xad, 0xd7, 0xd4, 0x55, + 0x7e, 0x81, 0x9f, 0x0c, 0x28, 0xae, 0xdc, 0xd1, 0x58, 0xef, 0x68, 0x84, 0x67, 0xea, 0x2a, 0x13, 0xf1, 0x22, 0x12, + 0x6f, 0xde, 0xd1, 0xc8, 0xec, 0xe8, 0x6c, 0xcb, 0x8e, 0xce, 0xee, 0xd9, 0xd1, 0x50, 0xef, 0x9e, 0x53, 0xe0, 0x8e, + 0x2f, 0x97, 0xcd, 0x46, 0x89, 0xbd, 0xd3, 0xfd, 0x88, 0xcd, 0x61, 0x37, 0x40, 0xcd, 0x13, 0x6c, 0x42, 0x37, 0x13, + 0x65, 0x15, 0xc5, 0xf4, 0xb3, 0x30, 0x59, 0x62, 0x21, 0xa9, 0x62, 0xc1, 0xa6, 0xeb, 0x22, 0xe6, 0xf6, 0x47, 0x52, + 0x36, 0x03, 0x3c, 0x64, 0x80, 0x87, 0xb1, 0x79, 0x01, 0xa6, 0xe7, 0xbe, 0x73, 0xb1, 0xeb, 0xb8, 0x86, 0xac, 0xaf, + 0xf2, 0x4b, 0x90, 0x11, 0x72, 0x7d, 0x0f, 0xa2, 0x45, 0x68, 0xed, 0x76, 0xb6, 0xd3, 0x1c, 0x84, 0xc7, 0x6f, 0x78, + 0x1a, 0xb9, 0x81, 0x6a, 0xfa, 0x59, 0xa8, 0x9a, 0xb0, 0x44, 0x27, 0x5b, 0x6d, 0xa5, 0xb5, 0xb2, 0xde, 0xa6, 0xb8, + 0xd6, 0xd1, 0x91, 0x6a, 0x31, 0x0d, 0x85, 0xa0, 0x69, 0xa2, 0x29, 0xd7, 0x75, 0xff, 0xbf, 0xa0, 0xc2, 0x0d, 0x7c, + 0x25, 0x34, 0x1b, 0x60, 0x08, 0x50, 0x2b, 0xec, 0x9a, 0xe7, 0x2b, 0xf1, 0xb4, 0x53, 0x6a, 0xb0, 0x77, 0xc8, 0x36, + 0x1a, 0x54, 0x11, 0xd8, 0x30, 0xb3, 0x09, 0x8d, 0x2e, 0x25, 0x83, 0xee, 0x0e, 0xae, 0xb4, 0xc2, 0xba, 0x22, 0xee, + 0xca, 0x0e, 0xd8, 0xfd, 0x79, 0xd6, 0x7a, 0x74, 0x78, 0xee, 0x62, 0xc5, 0xe3, 0xf9, 0x70, 0xe8, 0xa2, 0xdc, 0x79, + 0x58, 0xb7, 0xe6, 0xe1, 0xcf, 0xb3, 0xaf, 0x9f, 0x35, 0xbe, 0x2e, 0x3a, 0x27, 0x40, 0x44, 0x3a, 0xbe, 0x6f, 0x44, + 0x95, 0x05, 0xaf, 0x59, 0xd1, 0x30, 0x4c, 0xb6, 0x2f, 0xa7, 0x67, 0x2f, 0x27, 0x9b, 0x52, 0x1a, 0x01, 0x71, 0xe2, + 0xb5, 0xd2, 0xcb, 0x98, 0xce, 0xa9, 0x79, 0xf3, 0xe0, 0x86, 0xc9, 0x36, 0xf4, 0x18, 0xf0, 0x59, 0x22, 0x74, 0xa2, + 0x83, 0x66, 0xb5, 0xd6, 0x92, 0xae, 0xe4, 0x1a, 0x6c, 0x1b, 0xe1, 0x4e, 0xc9, 0xb9, 0xaa, 0xf4, 0xca, 0xaf, 0xb0, + 0x6b, 0x01, 0xb0, 0x15, 0xb2, 0xee, 0x96, 0xf2, 0xa0, 0x81, 0x1b, 0xdb, 0x60, 0xc3, 0x4d, 0x14, 0xb8, 0x6e, 0xdf, + 0xe0, 0x49, 0xfa, 0x2a, 0x2b, 0x2f, 0x8c, 0xd8, 0x8a, 0xaf, 0x4f, 0x62, 0xe0, 0x3a, 0x85, 0xc1, 0x12, 0x9a, 0x65, + 0x5b, 0x11, 0x50, 0x6c, 0x22, 0x76, 0xcb, 0xd6, 0xee, 0x96, 0x51, 0x70, 0x03, 0xc3, 0x09, 0x93, 0x00, 0x17, 0x11, + 0x53, 0xdd, 0x8a, 0x0e, 0x87, 0x74, 0x50, 0xb8, 0x7a, 0x21, 0xf6, 0x35, 0x64, 0xb1, 0x80, 0x10, 0x90, 0x8c, 0xcd, + 0xb8, 0xaf, 0x78, 0x42, 0x5d, 0x64, 0xb2, 0x39, 0x35, 0xfc, 0x5a, 0xfe, 0x6f, 0x86, 0x47, 0x8d, 0x58, 0x85, 0x45, + 0xcf, 0xb2, 0x5c, 0x1a, 0x37, 0x4f, 0xa5, 0xbc, 0x8a, 0x48, 0x2e, 0xfd, 0x38, 0xdb, 0x0e, 0xd0, 0xc3, 0x8e, 0xc9, + 0xa2, 0xf9, 0xf5, 0x51, 0xb3, 0x91, 0xbb, 0xd8, 0x85, 0xe1, 0x1e, 0x7a, 0x4a, 0x64, 0xaf, 0x03, 0xe8, 0x35, 0x4b, + 0x3e, 0xa7, 0x5f, 0xab, 0xf9, 0xb8, 0xe9, 0x62, 0xf5, 0x22, 0x01, 0x94, 0x17, 0xcc, 0x60, 0x00, 0xce, 0xcf, 0xdf, + 0xbd, 0x94, 0xea, 0xe0, 0x0f, 0x83, 0xe7, 0xb8, 0xd9, 0x70, 0xb1, 0x9b, 0x09, 0x3e, 0xfd, 0x8c, 0x25, 0x1c, 0xb8, + 0xd8, 0x1d, 0xc4, 0x3c, 0xa3, 0xf6, 0x1a, 0x94, 0x3a, 0xfb, 0xfb, 0x17, 0xa1, 0x20, 0x9a, 0xa6, 0x34, 0xcb, 0x1c, + 0x7b, 0x7c, 0x4d, 0x4a, 0x9f, 0x60, 0x98, 0x1b, 0x29, 0x2e, 0xa3, 0x42, 0xe2, 0x45, 0xdd, 0xf1, 0xb7, 0xa9, 0x4a, + 0x95, 0xad, 0x11, 0x9b, 0x14, 0x01, 0x05, 0x63, 0x53, 0xda, 0xd5, 0x27, 0x67, 0xde, 0x70, 0xf4, 0xd4, 0xc4, 0x2a, + 0x26, 0xbc, 0x3e, 0x41, 0xa5, 0x64, 0xc2, 0x92, 0xcb, 0x0d, 0xa5, 0xe1, 0xed, 0x86, 0x52, 0x50, 0xd9, 0x0a, 0xe8, + 0xf4, 0xeb, 0x67, 0x3e, 0x8d, 0xf5, 0x52, 0xf1, 0xb1, 0x41, 0x8c, 0xa4, 0xdf, 0xf2, 0x13, 0x90, 0x5a, 0xdb, 0x20, + 0x47, 0xf8, 0xed, 0xd3, 0x41, 0xc9, 0xe7, 0x4c, 0x57, 0x8c, 0xf2, 0xfb, 0x56, 0x08, 0xa5, 0x75, 0xf0, 0x5f, 0xc7, + 0x9f, 0xb5, 0x56, 0x7a, 0xfb, 0x69, 0x82, 0xb3, 0xb4, 0xaa, 0xdf, 0xb1, 0xf5, 0xfa, 0x1e, 0xfb, 0xea, 0xde, 0x6f, + 0x28, 0xd6, 0x8a, 0x4f, 0xb1, 0xff, 0x83, 0x98, 0x4d, 0x4a, 0x12, 0x58, 0x07, 0x53, 0x6a, 0x3c, 0x90, 0xcc, 0x64, + 0x0f, 0xa2, 0x54, 0x9f, 0x4b, 0xb8, 0xa2, 0x09, 0xef, 0xc1, 0x98, 0xa5, 0xf4, 0x32, 0xe6, 0x37, 0xab, 0xef, 0xf5, + 0xda, 0xde, 0x78, 0xcc, 0x46, 0x63, 0xeb, 0xde, 0x15, 0x25, 0xc5, 0x26, 0xdc, 0x3b, 0x41, 0xfe, 0x2f, 0xff, 0xec, + 0xfb, 0xff, 0xf2, 0xcf, 0x9f, 0x6c, 0x0a, 0xc3, 0xe7, 0x57, 0x58, 0x94, 0xc3, 0x6e, 0x3f, 0x5d, 0x9b, 0x67, 0xaa, + 0xe2, 0x7c, 0x73, 0x9b, 0xb5, 0x4d, 0x80, 0xfa, 0xb5, 0x2d, 0x58, 0x2b, 0x54, 0xa7, 0xcf, 0xf9, 0x2d, 0x80, 0xc1, + 0xba, 0x3e, 0x09, 0x19, 0x34, 0xfa, 0x5d, 0xa0, 0x5d, 0xa1, 0xe0, 0x41, 0x3b, 0xf2, 0xdb, 0x31, 0xfc, 0xa9, 0x35, + 0xfc, 0x4e, 0xf0, 0xb5, 0x7f, 0x62, 0x70, 0x75, 0x55, 0x24, 0xd8, 0xd9, 0x5d, 0xe1, 0x02, 0x7f, 0x77, 0xad, 0x44, + 0x2b, 0x1e, 0x41, 0x03, 0x75, 0xe4, 0xf5, 0x40, 0x32, 0xb8, 0x7a, 0x09, 0x6f, 0xed, 0x39, 0xbd, 0x4e, 0x8d, 0x83, + 0xf7, 0x1e, 0xe1, 0x00, 0x43, 0x54, 0x57, 0x25, 0x07, 0x5d, 0x93, 0x0c, 0x50, 0x0a, 0xe6, 0x06, 0x80, 0x89, 0x07, + 0x57, 0xda, 0xda, 0x3c, 0x57, 0x6e, 0x98, 0x60, 0x95, 0xb4, 0xb5, 0x7b, 0xa6, 0x82, 0x74, 0xec, 0xbc, 0x93, 0xf8, + 0x92, 0x8d, 0x69, 0x69, 0xdd, 0x4b, 0x57, 0x17, 0xd8, 0x11, 0x57, 0xb9, 0x0c, 0xd3, 0xff, 0x75, 0x5b, 0x24, 0xf1, + 0xef, 0x9f, 0x8e, 0x24, 0xf2, 0x07, 0x45, 0x12, 0xff, 0xfe, 0x87, 0x47, 0x12, 0xff, 0x6a, 0x47, 0x12, 0x61, 0x13, + 0x7f, 0x79, 0x50, 0xb4, 0xcf, 0x44, 0x62, 0xf8, 0x4d, 0x46, 0x9a, 0x5a, 0x8d, 0x8e, 0xf9, 0x08, 0x42, 0x7d, 0xff, + 0xf6, 0x91, 0xbb, 0x98, 0x8f, 0xec, 0xb8, 0x1d, 0xbc, 0xb5, 0x15, 0x02, 0x75, 0x6d, 0x13, 0x61, 0xd3, 0xb1, 0xb2, + 0x46, 0x71, 0x23, 0xa5, 0x7e, 0x68, 0xde, 0xa0, 0xe0, 0x06, 0xc5, 0x5b, 0x90, 0x1a, 0xb8, 0x65, 0xa2, 0x69, 0x81, + 0x0c, 0xc4, 0x15, 0x1d, 0x5b, 0x35, 0x73, 0xdd, 0xc2, 0x1e, 0xa1, 0x6d, 0xde, 0xf2, 0xa2, 0x6e, 0xdf, 0x2f, 0xdc, + 0x9f, 0x6f, 0x9b, 0x4f, 0x7a, 0xcd, 0xf6, 0x41, 0x73, 0xe2, 0x06, 0x2e, 0x88, 0x48, 0x59, 0xd0, 0x68, 0x1f, 0x1c, + 0x40, 0xc1, 0x8d, 0x55, 0xd0, 0x82, 0x02, 0x66, 0x15, 0x1c, 0x41, 0xc1, 0xc0, 0x2a, 0x38, 0x86, 0x82, 0xc8, 0x2a, + 0x78, 0x04, 0x05, 0x73, 0x37, 0xef, 0xb1, 0x02, 0xdc, 0x47, 0xa8, 0x8f, 0x95, 0xe5, 0x62, 0xca, 0x1e, 0xe1, 0x26, + 0x84, 0xf0, 0xc2, 0x91, 0xcc, 0x3c, 0x02, 0x87, 0x60, 0xc0, 0xf1, 0xcd, 0x98, 0x26, 0x01, 0x04, 0x51, 0x9f, 0x4a, + 0x19, 0xe3, 0x0b, 0xfe, 0x8e, 0x4d, 0xa8, 0xf9, 0x5e, 0x86, 0xc1, 0x83, 0xe3, 0xa2, 0x5e, 0xa3, 0x9f, 0xb7, 0x8b, + 0x9d, 0x53, 0xb1, 0x3f, 0x9d, 0x85, 0xa2, 0xf6, 0xb2, 0xac, 0x53, 0xd3, 0xd5, 0x8b, 0x3d, 0xdf, 0x12, 0x43, 0xb2, + 0x7c, 0x11, 0xc3, 0x98, 0xdf, 0xd4, 0x6f, 0xdd, 0xce, 0xe6, 0xb8, 0x12, 0x40, 0x54, 0xc4, 0x95, 0xe4, 0x9a, 0x8a, + 0xa7, 0x77, 0xe1, 0xa8, 0xf8, 0xfd, 0x92, 0x66, 0x59, 0x38, 0xd2, 0x2d, 0xb7, 0xc7, 0x91, 0x24, 0x88, 0x76, 0x0c, + 0xc9, 0x00, 0x01, 0xb1, 0x20, 0xd8, 0x2c, 0xb0, 0xe5, 0x75, 0x68, 0x08, 0xb0, 0x53, 0x8d, 0x2a, 0xc9, 0xe9, 0xab, + 0x45, 0x22, 0x1c, 0x95, 0x05, 0xa7, 0xd3, 0x94, 0xca, 0x52, 0x85, 0xe1, 0xfc, 0x74, 0x1f, 0x0a, 0x54, 0xf5, 0x96, + 0xe8, 0x91, 0x71, 0x1c, 0x6c, 0x8f, 0x21, 0x39, 0x26, 0x7a, 0x64, 0xe7, 0xdb, 0x14, 0xc9, 0x36, 0xeb, 0x31, 0x8b, + 0x2f, 0x9b, 0x03, 0xf8, 0x4f, 0x47, 0x44, 0xbe, 0x1c, 0x0e, 0x87, 0xf7, 0x46, 0x93, 0xbe, 0x8c, 0x86, 0xb4, 0x45, + 0x8f, 0xda, 0x90, 0x8b, 0x51, 0xd7, 0x31, 0x88, 0x66, 0x2e, 0x71, 0xb7, 0x78, 0x58, 0x63, 0x08, 0x57, 0x88, 0xf1, + 0xe2, 0xe1, 0x91, 0xa5, 0x7c, 0x9a, 0xd2, 0xc5, 0x24, 0x4c, 0x47, 0x2c, 0x09, 0x1a, 0xb9, 0x3f, 0xd7, 0xa1, 0x98, + 0x2f, 0x4f, 0x4e, 0x4e, 0x72, 0x3f, 0x32, 0x4f, 0x8d, 0x28, 0xca, 0xfd, 0xc1, 0xa2, 0x58, 0x46, 0xa3, 0x31, 0x1c, + 0xe6, 0x3e, 0x33, 0x05, 0x07, 0xad, 0x41, 0x74, 0xd0, 0xca, 0xfd, 0x1b, 0xab, 0x45, 0xee, 0x53, 0xfd, 0x94, 0xd2, + 0xa8, 0x92, 0xd0, 0xf1, 0xa8, 0xd1, 0xc8, 0x7d, 0x45, 0x68, 0x0b, 0x30, 0xc7, 0xd4, 0xcf, 0x20, 0x9c, 0x09, 0x0e, + 0x2c, 0xb9, 0xcd, 0x85, 0xd7, 0xbb, 0xd4, 0x2f, 0xcb, 0x50, 0x1f, 0x96, 0xc8, 0x51, 0x1f, 0xff, 0x62, 0x07, 0x4d, + 0x80, 0x98, 0x65, 0xb0, 0x84, 0x9b, 0x98, 0x4a, 0xa5, 0x1a, 0x28, 0x4b, 0x56, 0xff, 0x42, 0x78, 0x19, 0x4b, 0x01, + 0xfe, 0x03, 0x2d, 0xd5, 0x5b, 0xdd, 0x04, 0xdd, 0xc2, 0xf5, 0x29, 0xfd, 0x24, 0xd7, 0xbf, 0x7b, 0x08, 0xd3, 0xa7, + 0xf4, 0x8f, 0x66, 0xfa, 0xfa, 0xd5, 0xa7, 0x8a, 0xe9, 0x2b, 0xb6, 0x36, 0x11, 0xc4, 0x1d, 0x8c, 0xe9, 0xe0, 0xe3, + 0x35, 0xbf, 0xad, 0xc3, 0x91, 0x48, 0x5d, 0xc9, 0x4f, 0x77, 0x7f, 0x6b, 0xf2, 0x87, 0x19, 0xcc, 0xfa, 0x2e, 0x85, + 0x14, 0x9b, 0xaf, 0x13, 0xe2, 0xbe, 0x36, 0x36, 0x9d, 0x2a, 0x19, 0x0e, 0x89, 0xfb, 0x7a, 0x38, 0x74, 0xcd, 0x95, + 0xbf, 0x50, 0x50, 0xd9, 0xea, 0x55, 0xa5, 0x44, 0xb6, 0xfa, 0xfa, 0x6b, 0xbb, 0xcc, 0x2e, 0xd0, 0x21, 0x17, 0x3b, + 0xbc, 0xa2, 0x6b, 0x22, 0x96, 0xc1, 0x51, 0x83, 0xcf, 0x65, 0x54, 0xdf, 0x39, 0x98, 0x56, 0x5e, 0x0f, 0x5d, 0x00, + 0xbc, 0xe1, 0x9d, 0xd6, 0xab, 0xf7, 0xdd, 0x47, 0xd4, 0xa4, 0xdf, 0x3d, 0xb9, 0xfb, 0x26, 0xf2, 0x26, 0x02, 0xe5, + 0x2c, 0x7b, 0x9d, 0xac, 0xdc, 0x65, 0x51, 0x30, 0x12, 0x62, 0x2f, 0x2b, 0x17, 0x7c, 0x34, 0x8a, 0xe1, 0x83, 0x25, + 0x8b, 0xca, 0x7b, 0x50, 0x55, 0xf7, 0x6e, 0x65, 0xbd, 0x81, 0xdd, 0x51, 0xbf, 0x35, 0x54, 0x7e, 0x3f, 0x49, 0xe5, + 0x40, 0xcf, 0xf5, 0x87, 0x74, 0xa4, 0x39, 0xb8, 0xd0, 0xfc, 0x7f, 0xa1, 0x32, 0x67, 0x05, 0x64, 0x8d, 0xa8, 0x81, + 0xa3, 0x3c, 0xd7, 0x77, 0x0e, 0x22, 0x96, 0x4d, 0xe1, 0xfd, 0x9c, 0xaa, 0x27, 0xfd, 0x14, 0x0b, 0xcf, 0x6e, 0xac, + 0xb8, 0x46, 0x65, 0xbb, 0x72, 0x13, 0xd8, 0x50, 0x8e, 0xe2, 0x89, 0xc8, 0x5d, 0xed, 0x6f, 0x36, 0x48, 0x74, 0x1d, + 0x85, 0x4f, 0x15, 0x71, 0xb1, 0x56, 0x08, 0x4e, 0xdf, 0x62, 0x43, 0x4c, 0x95, 0x29, 0xc8, 0xed, 0xb8, 0x9d, 0xac, + 0x51, 0xd8, 0x92, 0x51, 0x82, 0x6c, 0x1a, 0x26, 0x8a, 0x8d, 0x12, 0x57, 0xf1, 0x83, 0xdd, 0x45, 0xb9, 0xf3, 0xb9, + 0x6b, 0xc0, 0x56, 0xc4, 0xdb, 0x39, 0xdd, 0x87, 0x0e, 0x1d, 0xa7, 0x02, 0x7a, 0xb2, 0x16, 0x5c, 0xf8, 0x44, 0x98, + 0xff, 0xca, 0xcf, 0x6e, 0xb0, 0x9f, 0xdd, 0x38, 0x7f, 0x5e, 0xd4, 0x6f, 0xe8, 0xf5, 0x47, 0x26, 0xea, 0x22, 0x9c, + 0xd6, 0x41, 0xe1, 0x97, 0x4e, 0x41, 0xcd, 0x9e, 0x65, 0xb2, 0x9a, 0xba, 0xb1, 0xdf, 0x9e, 0x65, 0x90, 0x0d, 0x20, + 0xd5, 0xd6, 0x20, 0xe1, 0x09, 0x6d, 0x57, 0x93, 0x12, 0xed, 0xe0, 0xb2, 0xc1, 0x56, 0x7f, 0xc1, 0x21, 0x7b, 0x40, + 0xdc, 0x05, 0x0d, 0xcd, 0xd6, 0x1b, 0x26, 0x72, 0xdc, 0xd8, 0xd8, 0x3e, 0xd0, 0xc8, 0xad, 0x49, 0xe9, 0x95, 0xae, + 0x47, 0xd0, 0xb7, 0x45, 0xc0, 0x3f, 0x95, 0xa2, 0x07, 0xae, 0x44, 0xf3, 0xbf, 0x95, 0xdb, 0xb8, 0x5a, 0x2c, 0x53, + 0xf4, 0x1e, 0x02, 0x59, 0x10, 0x0e, 0x05, 0x4d, 0xf1, 0x43, 0x5a, 0x5e, 0xcb, 0xdb, 0x34, 0x0b, 0x10, 0x33, 0x41, + 0xf3, 0x64, 0x7a, 0xfb, 0xf0, 0xe1, 0xef, 0x5f, 0x7e, 0xae, 0x71, 0x64, 0xde, 0x2e, 0xe3, 0xba, 0x6d, 0x38, 0x08, + 0x71, 0x78, 0x17, 0xb0, 0x44, 0xca, 0xbc, 0x6b, 0xf0, 0x07, 0xb6, 0xa7, 0x5c, 0xe7, 0x9a, 0xa6, 0x34, 0x96, 0x9f, + 0x92, 0xd3, 0x5b, 0x71, 0x70, 0x3c, 0xbd, 0x35, 0xbb, 0xd1, 0x5c, 0xc9, 0x21, 0xfd, 0x43, 0x53, 0x45, 0xb7, 0xe7, + 0xa6, 0x56, 0xd3, 0x1d, 0x8f, 0xa6, 0xb7, 0x6d, 0x25, 0x68, 0xeb, 0xa9, 0x82, 0xaa, 0x31, 0xbd, 0xb5, 0x93, 0x65, + 0xcb, 0x81, 0x1c, 0xff, 0x20, 0x73, 0x68, 0x98, 0xd1, 0x36, 0xbc, 0x3f, 0x9b, 0x0d, 0xc2, 0x58, 0x0b, 0xf3, 0x09, + 0x8b, 0xa2, 0x98, 0xb6, 0x8d, 0xbc, 0x76, 0x9a, 0xc7, 0x90, 0x6b, 0x6a, 0x6f, 0x59, 0x75, 0x57, 0x2c, 0xe4, 0x15, + 0x78, 0x0a, 0xaf, 0x33, 0x1e, 0xc3, 0xc7, 0x2b, 0x36, 0xa2, 0x53, 0x27, 0x61, 0x36, 0x4a, 0xe4, 0xc9, 0xdf, 0xd5, + 0xb5, 0x1c, 0x35, 0xfe, 0xd4, 0x96, 0x1b, 0xde, 0x68, 0x0b, 0x3e, 0x0d, 0xea, 0x07, 0xd5, 0x85, 0x40, 0x55, 0xb1, + 0x04, 0xbc, 0x61, 0x59, 0x18, 0xa4, 0x95, 0xe2, 0xd3, 0x8e, 0xdf, 0xd4, 0x65, 0x72, 0x00, 0x78, 0xd1, 0x73, 0x51, + 0x94, 0x57, 0x17, 0xf3, 0x6f, 0x73, 0x5a, 0x1e, 0x6f, 0x3e, 0x2d, 0x8f, 0xcd, 0x69, 0xb9, 0x9f, 0x62, 0xbf, 0x1c, + 0x36, 0xe1, 0xbf, 0x76, 0xb9, 0xa0, 0xa0, 0xe1, 0x1c, 0x4c, 0x6f, 0x1d, 0xd0, 0xd3, 0xea, 0xad, 0xe9, 0xad, 0x4a, + 0x15, 0x86, 0x98, 0x45, 0x03, 0x92, 0x67, 0x71, 0xc3, 0x81, 0x42, 0xf8, 0xbf, 0x51, 0xa9, 0x6a, 0x1e, 0x42, 0x1d, + 0xf4, 0x3a, 0x5a, 0xaf, 0x6b, 0xdd, 0x7f, 0x68, 0x83, 0x84, 0x0b, 0x2f, 0x30, 0xdc, 0x18, 0xf9, 0x22, 0xbc, 0xbe, + 0xa6, 0x51, 0x30, 0xe4, 0x83, 0x59, 0xf6, 0x4f, 0x1a, 0x7e, 0x8d, 0xc4, 0x7b, 0x8f, 0xf4, 0xca, 0x38, 0xa6, 0xab, + 0x4a, 0x5c, 0x36, 0x23, 0x2c, 0x8a, 0x7d, 0x0a, 0xb2, 0x41, 0x18, 0x53, 0xaf, 0xe5, 0x1f, 0x6e, 0x38, 0x04, 0xff, + 0x2e, 0x7b, 0xb3, 0x71, 0x31, 0xbf, 0x17, 0x19, 0xf7, 0x22, 0xe1, 0xb3, 0x70, 0x60, 0xef, 0x61, 0xe3, 0x64, 0x33, + 0xb8, 0x3d, 0x33, 0x53, 0xdf, 0x08, 0x05, 0x2d, 0x77, 0x22, 0x3a, 0x0c, 0x67, 0xb1, 0xb8, 0x7f, 0xd4, 0x6d, 0x94, + 0xb1, 0x36, 0xea, 0x3d, 0x0c, 0xbd, 0x6c, 0xfb, 0x40, 0x2e, 0xfd, 0xe5, 0xe3, 0x43, 0xf8, 0x4f, 0xe5, 0x3d, 0xdd, + 0x95, 0xba, 0xba, 0xb2, 0x55, 0x41, 0x57, 0xdf, 0xad, 0x28, 0xe3, 0x4a, 0x84, 0x4b, 0x7d, 0xfc, 0xa1, 0xad, 0x41, + 0xab, 0x7c, 0x50, 0x73, 0xad, 0x65, 0x7d, 0x56, 0xeb, 0xcf, 0x1b, 0xfc, 0x81, 0x6d, 0x07, 0x4a, 0x73, 0xad, 0xb6, + 0xd5, 0xdf, 0xd2, 0x5b, 0x6b, 0x6c, 0x30, 0x2e, 0xdb, 0xef, 0x92, 0xbb, 0xc2, 0x44, 0x51, 0x51, 0x48, 0xb0, 0x52, + 0x76, 0x95, 0x95, 0xc2, 0x28, 0xb9, 0x3a, 0xed, 0xde, 0x4e, 0x62, 0x67, 0xae, 0x6e, 0xfd, 0x11, 0xb7, 0xe9, 0x37, + 0x5c, 0x47, 0xc6, 0xbf, 0xe1, 0xed, 0xe3, 0xae, 0xfc, 0x46, 0xab, 0xdb, 0x05, 0x4d, 0x6b, 0x3e, 0x92, 0x9a, 0xdd, + 0x8b, 0xf0, 0x8e, 0xa6, 0x97, 0x2d, 0xd7, 0x01, 0xef, 0x4a, 0x5d, 0xa5, 0x0a, 0xc8, 0x32, 0xa7, 0xe5, 0x3a, 0xb7, + 0x93, 0x38, 0xc9, 0x88, 0x3b, 0x16, 0x62, 0x1a, 0xa8, 0x8f, 0xb8, 0xde, 0x1c, 0xf8, 0x3c, 0x1d, 0xed, 0xb7, 0x1a, + 0x8d, 0x06, 0xbc, 0xc9, 0xd4, 0x75, 0xe6, 0x8c, 0xde, 0x3c, 0xe1, 0xb7, 0xc4, 0x6d, 0x38, 0x0d, 0xa7, 0xd9, 0x3a, + 0x71, 0x9a, 0xad, 0x43, 0xff, 0xf8, 0xc4, 0xed, 0x7c, 0xe1, 0x38, 0xa7, 0x11, 0x1d, 0x66, 0xf0, 0xc3, 0x71, 0x4e, + 0xa5, 0xe2, 0xa5, 0x7e, 0x3b, 0x8e, 0x3f, 0x88, 0xb3, 0x7a, 0xd3, 0x59, 0xe8, 0x47, 0xc7, 0x81, 0xbb, 0x91, 0x81, + 0xf3, 0xe5, 0xb0, 0x35, 0x3c, 0x1c, 0x3e, 0x6e, 0xeb, 0xe2, 0xfc, 0x8b, 0x4a, 0x73, 0xac, 0xfe, 0xb6, 0xac, 0x6e, + 0x99, 0x48, 0xf9, 0x47, 0xaa, 0x73, 0xf1, 0x1c, 0x10, 0x3d, 0x1b, 0xbb, 0xb6, 0xd6, 0x67, 0x6a, 0x9e, 0x5c, 0x0f, + 0x86, 0xad, 0xb2, 0xb9, 0x84, 0x71, 0xbf, 0x00, 0xf2, 0x74, 0xdf, 0x80, 0x7e, 0x6a, 0xa3, 0xa9, 0x59, 0xdf, 0x84, + 0xa8, 0xa6, 0xab, 0xd7, 0x38, 0x32, 0xeb, 0x3b, 0x85, 0x54, 0x7c, 0xa3, 0xab, 0x4a, 0x08, 0x5c, 0x27, 0x22, 0xee, + 0xcb, 0x66, 0xeb, 0x04, 0x37, 0x9b, 0xc7, 0xfe, 0xf1, 0xc9, 0xa0, 0x81, 0x0f, 0xfd, 0xc3, 0xfa, 0x81, 0x7f, 0x8c, + 0x4f, 0xea, 0x27, 0xf8, 0xe4, 0xf9, 0xc9, 0xa0, 0x7e, 0xe8, 0x1f, 0xe2, 0x46, 0xfd, 0x04, 0x0a, 0xeb, 0x27, 0xf5, + 0x93, 0x79, 0xfd, 0xf0, 0x64, 0xd0, 0x90, 0xa5, 0x2d, 0xff, 0xe8, 0xa8, 0xde, 0x6c, 0xf8, 0x47, 0x47, 0xf8, 0xc8, + 0x3f, 0x3e, 0xae, 0x37, 0x0f, 0xfc, 0xe3, 0xe3, 0x17, 0x47, 0x27, 0xfe, 0x01, 0xd4, 0x1d, 0x1c, 0x0c, 0x0e, 0xfc, + 0x66, 0xb3, 0x0e, 0xff, 0xe0, 0x13, 0xbf, 0xa5, 0x7e, 0x34, 0x9b, 0xfe, 0x41, 0x13, 0x37, 0xe2, 0xa3, 0x96, 0x7f, + 0xfc, 0x18, 0xcb, 0x7f, 0x65, 0x33, 0x2c, 0xff, 0x81, 0x61, 0xf0, 0x63, 0xbf, 0x75, 0xac, 0x7e, 0xc9, 0x01, 0xe7, + 0x87, 0x27, 0x3f, 0xb9, 0xfb, 0x5b, 0xd7, 0xd0, 0x54, 0x6b, 0x38, 0x39, 0xf2, 0x0f, 0x0e, 0xf0, 0x61, 0xd3, 0x3f, + 0x39, 0x18, 0xd7, 0x0f, 0x5b, 0xfe, 0xf1, 0xa3, 0x41, 0xbd, 0xe9, 0x3f, 0x7a, 0x84, 0x1b, 0xf5, 0x03, 0xbf, 0x85, + 0x9b, 0xfe, 0xe1, 0x81, 0xfc, 0x71, 0xe0, 0xb7, 0xe6, 0x8f, 0x1e, 0xfb, 0xc7, 0x47, 0xe3, 0x63, 0xff, 0xf0, 0xfb, + 0xc3, 0x13, 0xbf, 0x75, 0x30, 0x3e, 0x38, 0xf6, 0x5b, 0x8f, 0xe6, 0xc7, 0xfe, 0xe1, 0xb8, 0xde, 0x3a, 0xbe, 0xb7, + 0x67, 0xb3, 0xe5, 0x03, 0x8e, 0x64, 0x35, 0x54, 0x60, 0x5d, 0x01, 0xff, 0x8f, 0x65, 0xdf, 0x7f, 0xc7, 0x61, 0xb2, + 0xf5, 0xae, 0x8f, 0xfd, 0x93, 0x47, 0x03, 0xd5, 0x1c, 0x0a, 0xea, 0xa6, 0x05, 0x74, 0x99, 0xd7, 0xd5, 0xb4, 0x72, + 0xb8, 0xba, 0x19, 0xc8, 0xfc, 0xaf, 0x27, 0x9b, 0xd7, 0x61, 0x62, 0x35, 0xef, 0x7f, 0xe8, 0x38, 0xc5, 0x96, 0x9f, + 0xee, 0x8f, 0x14, 0xe9, 0x8f, 0x3a, 0x5f, 0xa8, 0xd7, 0x14, 0x7f, 0x71, 0x85, 0xb3, 0x6d, 0x8e, 0x8f, 0xf4, 0xd3, + 0x8e, 0x8f, 0x84, 0x3e, 0xc4, 0xf3, 0x91, 0xfe, 0xe1, 0x9e, 0x8f, 0x8c, 0xae, 0xb8, 0xbb, 0xef, 0xc4, 0x9a, 0x83, + 0x63, 0xd5, 0x2a, 0x7e, 0x2e, 0xbc, 0x1e, 0x83, 0xef, 0x61, 0xe5, 0xed, 0x3b, 0x78, 0x15, 0xba, 0xed, 0x07, 0xe2, + 0xc0, 0x62, 0xef, 0x84, 0xe2, 0xb1, 0x7c, 0x1b, 0x42, 0xe2, 0x4f, 0x23, 0xe4, 0xfb, 0x87, 0xe0, 0x23, 0xfe, 0xc3, + 0xf1, 0xc1, 0x6d, 0x7c, 0x54, 0x3c, 0xf0, 0xd2, 0xd3, 0x20, 0x3d, 0x05, 0x17, 0xf2, 0xd9, 0x83, 0xbb, 0x40, 0x35, + 0x77, 0x9f, 0x42, 0x51, 0xe6, 0xaa, 0x88, 0xcf, 0xab, 0xcf, 0x09, 0x16, 0xa8, 0x8b, 0x7f, 0xc4, 0xd5, 0x6e, 0x99, + 0xa9, 0x94, 0x3a, 0xfa, 0xa1, 0x10, 0x4a, 0x2d, 0xbf, 0xe1, 0x37, 0x0a, 0x97, 0x0e, 0x5c, 0xf6, 0x24, 0x0b, 0x2e, + 0x42, 0xf8, 0xec, 0x6a, 0xcc, 0x47, 0xf2, 0x03, 0xad, 0xf0, 0x5a, 0x7c, 0xf9, 0xa9, 0x5c, 0xf5, 0x45, 0x82, 0xc0, + 0x75, 0xf5, 0x2b, 0x22, 0xe0, 0x32, 0xe1, 0x77, 0x70, 0xe1, 0xd2, 0xc4, 0x12, 0x26, 0xe0, 0xed, 0x78, 0x49, 0x23, + 0x16, 0x7a, 0xae, 0x37, 0x4d, 0xe9, 0x90, 0xa6, 0x59, 0xbd, 0x72, 0x0b, 0x51, 0x5e, 0x40, 0x44, 0xae, 0xf9, 0xc0, + 0x67, 0x0a, 0xaf, 0x79, 0x26, 0x3d, 0xed, 0x6f, 0x74, 0xb5, 0x01, 0xe6, 0xe6, 0xd8, 0x94, 0xa4, 0x20, 0x6b, 0x4b, + 0xa5, 0xcd, 0x55, 0x5a, 0x5b, 0xd3, 0x6f, 0x1d, 0x21, 0x47, 0x16, 0xc3, 0xeb, 0x73, 0x7f, 0xf4, 0xea, 0x07, 0x8d, + 0x3f, 0x21, 0xab, 0x5b, 0x31, 0x50, 0x5f, 0xbb, 0xdb, 0xd2, 0xf2, 0xc3, 0xc8, 0xd5, 0x2b, 0xa2, 0xae, 0xa2, 0x88, + 0x2f, 0xd5, 0xda, 0xe1, 0x45, 0xbc, 0x3a, 0xb2, 0xab, 0x5e, 0x74, 0x30, 0x64, 0x23, 0xcf, 0xfe, 0xec, 0xad, 0x7a, + 0x3d, 0xaf, 0xfc, 0x5a, 0x36, 0xca, 0xcb, 0x26, 0x29, 0x5a, 0xc8, 0x28, 0x09, 0x4b, 0x9c, 0x74, 0xb9, 0xf4, 0x52, + 0x70, 0x91, 0x13, 0x0b, 0xa7, 0xf0, 0x8c, 0x2a, 0x48, 0x4e, 0x71, 0x01, 0x90, 0x44, 0x30, 0x49, 0xd5, 0xdf, 0xb2, + 0xd8, 0xfc, 0xd0, 0x8e, 0x2f, 0x3f, 0x0e, 0x93, 0x11, 0x50, 0x61, 0x98, 0x8c, 0xd6, 0xdc, 0x6a, 0x2a, 0xd0, 0xb3, + 0x52, 0x5a, 0x0e, 0x55, 0xba, 0xcf, 0xb2, 0x27, 0x77, 0xef, 0xf4, 0x7b, 0xbc, 0x5c, 0xf0, 0x4e, 0xcb, 0xa8, 0x44, + 0xf9, 0xce, 0xe1, 0x1a, 0xf9, 0x4a, 0x7d, 0x48, 0x5e, 0xca, 0x54, 0xd0, 0x27, 0xe0, 0xf2, 0xa7, 0xa3, 0xad, 0x51, + 0xe2, 0x4a, 0xe9, 0x4e, 0x22, 0x3a, 0x67, 0x03, 0x2d, 0xea, 0xb1, 0xa3, 0x2f, 0xc0, 0xd7, 0xe5, 0xd6, 0x90, 0x26, + 0x56, 0xfe, 0x98, 0x41, 0x28, 0x33, 0xd1, 0x49, 0xc2, 0xdd, 0xce, 0x57, 0xc5, 0x57, 0x3c, 0xb7, 0x6d, 0x02, 0x7c, + 0xdd, 0xbe, 0x97, 0xd2, 0xf8, 0x9f, 0xc8, 0x57, 0xf0, 0x7d, 0xfb, 0xaf, 0xfa, 0xf0, 0x69, 0x75, 0x5f, 0x7e, 0xe5, + 0xfe, 0xab, 0xf2, 0x33, 0xf7, 0xc0, 0x08, 0x6b, 0xb7, 0x93, 0x18, 0x4b, 0x8d, 0xe9, 0x01, 0x0a, 0x91, 0x02, 0xd7, + 0x6d, 0x1d, 0xb9, 0x8e, 0xb2, 0x89, 0xe5, 0xef, 0x8e, 0x12, 0xa7, 0x52, 0x09, 0x70, 0x9a, 0x2d, 0xff, 0x68, 0xdc, + 0xf2, 0x1f, 0xcf, 0x1f, 0xf9, 0x27, 0xe3, 0xe6, 0xa3, 0x79, 0x1d, 0xfe, 0xb6, 0xfc, 0xc7, 0x71, 0xbd, 0xe5, 0x3f, + 0x86, 0xff, 0xbf, 0x3f, 0xf4, 0x8f, 0xc6, 0xf5, 0xa6, 0x7f, 0x32, 0x3f, 0xf0, 0x0f, 0x5e, 0x34, 0x5b, 0xfe, 0x81, + 0xd3, 0x74, 0x54, 0x3f, 0x60, 0xd7, 0x8a, 0x3b, 0x7f, 0xb5, 0x72, 0x20, 0x36, 0x04, 0xd1, 0x54, 0xae, 0xa5, 0x8b, + 0xbd, 0xe2, 0x5b, 0x81, 0xfa, 0x7c, 0x6a, 0x67, 0xdd, 0xd3, 0x30, 0x85, 0x0f, 0xb6, 0x54, 0xcf, 0x6e, 0xa5, 0x0e, + 0x57, 0xf8, 0xc5, 0x86, 0x29, 0xe0, 0x84, 0xbb, 0xd8, 0xbe, 0x41, 0x0e, 0xd7, 0xaf, 0xe5, 0xeb, 0xad, 0xcd, 0x5b, + 0xfe, 0xb6, 0x93, 0xb6, 0x6a, 0x68, 0xde, 0x24, 0x28, 0x99, 0x05, 0x93, 0x9f, 0x12, 0x90, 0x93, 0x7c, 0x13, 0xe5, + 0xab, 0xf3, 0x43, 0xca, 0x67, 0xca, 0xad, 0x4b, 0xf4, 0xb4, 0xbc, 0xa8, 0x10, 0x31, 0x78, 0xed, 0x41, 0x9e, 0x1b, + 0xd0, 0x2b, 0x6e, 0xda, 0x12, 0x4b, 0x92, 0x5f, 0xd0, 0xac, 0xeb, 0x42, 0x91, 0x1b, 0xb8, 0xd2, 0xc5, 0xe7, 0x16, + 0x1f, 0xad, 0x29, 0x08, 0xbb, 0x2c, 0xc0, 0xf2, 0xb2, 0x11, 0x9c, 0x5a, 0xc0, 0x8f, 0x8b, 0xf6, 0xf6, 0xb6, 0x9e, + 0x17, 0xa9, 0x40, 0xc2, 0x5a, 0xcb, 0x8f, 0x5d, 0xd8, 0xac, 0xc8, 0xb5, 0x11, 0x5d, 0x8c, 0x2b, 0x51, 0x88, 0x34, + 0x9e, 0xae, 0x69, 0x28, 0xfc, 0x30, 0x51, 0xc9, 0x23, 0x16, 0xc3, 0xc2, 0x4d, 0x7a, 0x80, 0x72, 0x2e, 0x42, 0xeb, + 0x6b, 0xb6, 0xfa, 0x9c, 0x73, 0x11, 0x9a, 0x2b, 0xa1, 0x89, 0xa8, 0xdc, 0x99, 0x18, 0xb7, 0x3a, 0xaf, 0xdf, 0x9d, + 0x39, 0xea, 0x78, 0x9e, 0xee, 0x8f, 0x5b, 0x9d, 0x53, 0xe9, 0x33, 0x51, 0x17, 0xca, 0x88, 0xba, 0x50, 0xe6, 0xe8, + 0xcb, 0x85, 0x10, 0x49, 0xcb, 0xf7, 0xd5, 0xb2, 0xa5, 0xcd, 0xa0, 0xbc, 0xbd, 0x93, 0x59, 0x2c, 0x18, 0xbc, 0xaa, + 0x79, 0x1f, 0xba, 0xd6, 0x61, 0xc3, 0x8a, 0xfc, 0x63, 0xad, 0x1d, 0x5e, 0x8b, 0xc4, 0xf8, 0x86, 0x87, 0x2c, 0xa6, + 0x26, 0xe3, 0x58, 0x0f, 0x55, 0x64, 0xc8, 0xaf, 0xb7, 0xce, 0x66, 0xd7, 0x13, 0x26, 0x5c, 0x93, 0xc7, 0xff, 0x5e, + 0x77, 0x38, 0x95, 0x53, 0x75, 0xae, 0x72, 0xed, 0xbc, 0x36, 0x9f, 0xa5, 0xa9, 0x6e, 0xa9, 0x5e, 0xbd, 0x96, 0x10, + 0x70, 0x33, 0x6c, 0x7c, 0xd0, 0x29, 0xdc, 0xc5, 0x76, 0x5d, 0x7e, 0xba, 0x3f, 0x3e, 0xe8, 0x5c, 0x05, 0x53, 0x3d, + 0xde, 0x0b, 0x3e, 0xda, 0x3c, 0x56, 0xcc, 0x47, 0x5d, 0x79, 0x05, 0x42, 0xdd, 0xb5, 0x35, 0xca, 0x2f, 0x8f, 0xdd, + 0xce, 0xa9, 0x56, 0x06, 0x1c, 0x19, 0x0e, 0x77, 0x8f, 0x1a, 0xe6, 0x56, 0x45, 0xcc, 0x47, 0x70, 0x20, 0x55, 0x17, + 0x6b, 0x92, 0x8a, 0xc7, 0x7d, 0xdc, 0xec, 0x9c, 0x86, 0x8e, 0xe4, 0x2d, 0x92, 0x79, 0x64, 0xc1, 0x3e, 0x74, 0x1e, + 0xf3, 0x09, 0xf5, 0x19, 0xdf, 0xbf, 0xa1, 0xd7, 0xf5, 0x70, 0xca, 0x4a, 0xf7, 0x36, 0x28, 0x1d, 0xc5, 0x94, 0xdc, + 0x78, 0xc4, 0xf5, 0x9d, 0xa3, 0x56, 0xe9, 0x6e, 0x3b, 0x04, 0x9b, 0xc7, 0xb8, 0xe6, 0xa4, 0x4f, 0xce, 0x02, 0x8b, + 0x77, 0x4e, 0xf7, 0xc3, 0x15, 0x8c, 0x48, 0x7e, 0x9f, 0x6b, 0x47, 0x3b, 0x18, 0x36, 0x40, 0x6f, 0xae, 0xa3, 0xc4, + 0x81, 0x71, 0xc8, 0x6b, 0x41, 0x9d, 0xbb, 0x9d, 0x7f, 0xfd, 0x1f, 0xff, 0x4b, 0xfb, 0xd8, 0x4f, 0xf7, 0xc7, 0x4d, + 0x33, 0xd6, 0xca, 0xae, 0xe4, 0xa7, 0x70, 0x6b, 0xb1, 0x0c, 0x0a, 0xd3, 0xdb, 0xfa, 0x28, 0x65, 0x51, 0x7d, 0x1c, + 0xc6, 0x43, 0xb7, 0xb3, 0x1d, 0x9b, 0xf6, 0x75, 0x25, 0x0d, 0x75, 0xb5, 0x08, 0xe8, 0xf5, 0x37, 0x5d, 0xb8, 0x31, + 0xf7, 0x36, 0xe4, 0xd1, 0xb6, 0xaf, 0xdf, 0x94, 0xa7, 0xaf, 0x72, 0x05, 0x27, 0xd5, 0x9f, 0xba, 0xd2, 0x1c, 0x30, + 0xad, 0xdc, 0xbc, 0xc9, 0x5d, 0xa7, 0x08, 0x6a, 0xfd, 0xdf, 0xff, 0xf9, 0x5f, 0xfe, 0x9b, 0x79, 0x84, 0x58, 0xd5, + 0xbf, 0xfe, 0xf7, 0xff, 0xfc, 0x7f, 0xfe, 0xf7, 0x7f, 0x85, 0xdb, 0x1a, 0x3a, 0x9e, 0x25, 0x99, 0x8a, 0x53, 0x06, + 0xb3, 0x14, 0x77, 0x71, 0x20, 0xa1, 0x71, 0xc2, 0x32, 0xc1, 0x06, 0xd5, 0xbb, 0x38, 0x17, 0x72, 0x42, 0x79, 0x32, + 0x35, 0x74, 0xf2, 0x84, 0xe7, 0x25, 0x41, 0x55, 0x50, 0x2e, 0x09, 0x37, 0x3f, 0xdd, 0x07, 0x7c, 0x3f, 0xec, 0xfa, + 0xa2, 0x5f, 0x6c, 0xc7, 0xc2, 0x90, 0x09, 0x94, 0xe4, 0x65, 0xb9, 0x03, 0xb1, 0x95, 0x05, 0x3c, 0x06, 0x2d, 0xab, + 0x58, 0xee, 0x5e, 0xa5, 0x4f, 0xfb, 0xc3, 0x2c, 0x13, 0x6c, 0x08, 0x28, 0x57, 0x7e, 0x62, 0x19, 0xc6, 0xae, 0x83, + 0xae, 0x18, 0xdf, 0xe5, 0x72, 0x14, 0x45, 0xa0, 0x87, 0x27, 0x7f, 0xca, 0xff, 0x32, 0x01, 0x8d, 0xcc, 0xf1, 0x26, + 0xe1, 0xad, 0x36, 0xcf, 0x8f, 0x1b, 0x8d, 0xe9, 0x2d, 0x5a, 0x94, 0x33, 0xe0, 0x6d, 0x93, 0x49, 0x3a, 0xb6, 0x07, + 0x94, 0xf1, 0xef, 0xc2, 0x8d, 0xdd, 0x70, 0xc0, 0x17, 0xee, 0x34, 0xf2, 0xfc, 0xcf, 0x0b, 0xe9, 0x49, 0x65, 0xbf, + 0x42, 0x9c, 0x5a, 0x3b, 0x9d, 0xaf, 0xb9, 0xbd, 0xb8, 0x85, 0xd5, 0xab, 0xa5, 0x7a, 0x8d, 0x9b, 0xeb, 0xb7, 0xf2, + 0xec, 0x38, 0xbb, 0x1d, 0x21, 0x3f, 0x84, 0x98, 0xf7, 0xb8, 0x89, 0xc7, 0xad, 0x45, 0x31, 0xbc, 0x10, 0x7c, 0x62, + 0x07, 0xd6, 0x69, 0x48, 0x07, 0x74, 0x68, 0x9c, 0xf5, 0xba, 0x5e, 0x05, 0xcd, 0xf3, 0xf1, 0xc1, 0x86, 0xb9, 0x34, + 0x48, 0x32, 0xa0, 0xee, 0x34, 0xf2, 0x2f, 0xe1, 0x04, 0x2e, 0x86, 0x31, 0x0f, 0x45, 0x20, 0x09, 0xb6, 0x6d, 0x87, + 0xe7, 0x43, 0xe0, 0x49, 0x7c, 0x61, 0xc1, 0xd3, 0x56, 0x4d, 0xc1, 0x6d, 0x5e, 0xbd, 0x3b, 0x99, 0xfb, 0xb2, 0xbb, + 0x3d, 0x94, 0xf2, 0xba, 0x7d, 0xaf, 0xa3, 0xde, 0xaf, 0x2a, 0xee, 0x2a, 0x2d, 0x90, 0x5a, 0x68, 0x73, 0xbd, 0x92, + 0xeb, 0xaa, 0xf6, 0x27, 0xe1, 0xb9, 0x12, 0x4c, 0x77, 0xb5, 0x95, 0x2c, 0x84, 0x56, 0xaf, 0xc8, 0xf7, 0x85, 0xc9, + 0x14, 0x4e, 0xa7, 0xb2, 0x21, 0x6a, 0x9f, 0xee, 0x2b, 0x4d, 0x17, 0xb8, 0x87, 0x4c, 0xe9, 0x50, 0x19, 0x14, 0xba, + 0x91, 0x3e, 0x0a, 0xea, 0x97, 0xce, 0xad, 0x80, 0x6f, 0xa0, 0x75, 0xfe, 0x1f, 0xa2, 0x48, 0xf6, 0xdd, 0x94, 0x88, + 0x00, 0x00}; + +} // namespace web_server +} // namespace esphome + +#endif +#endif diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h new file mode 100644 index 0000000000..b5d7189137 --- /dev/null +++ b/esphome/components/web_server/server_index_v3.h @@ -0,0 +1,4030 @@ +#pragma once +// Generated from https://github.com/esphome/esphome-webserver + +#ifdef USE_WEBSERVER_LOCAL +#if USE_WEBSERVER_VERSION == 3 + +#include "esphome/core/hal.h" + +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, 0x71, 0x13, 0x60, 0x1a, 0xa0, 0x25, 0x99, + 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, 0xbc, 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, 0x5b, 0xfd, 0x8f, 0x2f, 0xb4, 0xd8, + 0xfb, 0xa5, 0x20, 0xaf, 0x87, 0x7f, 0xb3, 0x4c, 0x27, 0x23, 0x36, 0xe6, 0x82, 0xbd, 0x29, 0xe4, 0x9c, 0x15, 0xfa, + 0xae, 0x0b, 0x99, 0x3f, 0x15, 0x24, 0xe6, 0x58, 0x63, 0x86, 0xc8, 0xa5, 0xde, 0xe3, 0x62, 0x8f, 0xf7, 0x7e, 0x29, + 0x4c, 0xca, 0x92, 0x89, 0xc5, 0x8c, 0x15, 0x74, 0x98, 0xb3, 0x74, 0xbf, 0x85, 0x33, 0x29, 0xc6, 0x7c, 0xb2, 0x28, + 0xbf, 0x6f, 0x0a, 0xae, 0xfd, 0xef, 0x2f, 0x34, 0x5f, 0xb0, 0x94, 0xad, 0x51, 0xca, 0xfb, 0x7a, 0x40, 0x98, 0x69, + 0xf9, 0x73, 0xd5, 0x70, 0xfc, 0x93, 0x69, 0xf2, 0x6e, 0xce, 0xe4, 0x78, 0x4f, 0xef, 0x93, 0x48, 0xdd, 0xcd, 0x86, + 0x32, 0x8f, 0x7a, 0xba, 0x11, 0x45, 0x29, 0x94, 0xc1, 0x0c, 0x75, 0x33, 0x29, 0x94, 0xde, 0x13, 0x9c, 0xdc, 0x70, + 0x31, 0x92, 0x37, 0xf8, 0x46, 0x10, 0xc1, 0x93, 0xab, 0x29, 0x1d, 0xc9, 0x9b, 0xb7, 0x52, 0xea, 0x83, 0x83, 0xd8, + 0x7d, 0xdf, 0x3d, 0xbe, 0xba, 0x22, 0x84, 0x7c, 0x91, 0x7c, 0xb4, 0xd7, 0x5a, 0xad, 0x82, 0xd4, 0x44, 0x50, 0xcd, + 0xbf, 0x30, 0x5b, 0x09, 0x1d, 0x1c, 0x44, 0x74, 0x24, 0xe7, 0x9a, 0x8d, 0xae, 0xf4, 0x5d, 0xce, 0xae, 0xa6, 0x8c, + 0x69, 0x15, 0x71, 0xb1, 0xf7, 0x44, 0x66, 0x8b, 0x19, 0x13, 0x3a, 0x99, 0x17, 0x52, 0x4b, 0x18, 0xd8, 0xc1, 0x41, + 0x54, 0xb0, 0x79, 0x4e, 0x33, 0x06, 0xf9, 0x8f, 0xaf, 0xae, 0xaa, 0x1a, 0x55, 0x21, 0xfc, 0x59, 0x90, 0x2b, 0x33, + 0xf4, 0x18, 0xe1, 0x0f, 0x82, 0x08, 0x76, 0xb3, 0xf7, 0x81, 0xd1, 0xcf, 0xbf, 0xd2, 0x79, 0x37, 0xcb, 0xa9, 0x52, + 0x7b, 0xcf, 0xe4, 0xd2, 0x4c, 0xa3, 0x58, 0x64, 0x5a, 0x16, 0xb1, 0xc6, 0x0c, 0x0b, 0xb4, 0xe4, 0xe3, 0x58, 0x4f, + 0xb9, 0x4a, 0xae, 0xef, 0x65, 0x4a, 0xbd, 0x65, 0x6a, 0x91, 0xeb, 0x7b, 0x64, 0xbf, 0x85, 0xc5, 0x3e, 0x21, 0x9f, + 0x05, 0xd2, 0xd3, 0x42, 0xde, 0xec, 0x3d, 0x2d, 0x0a, 0x59, 0xc4, 0xd1, 0xe3, 0xab, 0x2b, 0x5b, 0x62, 0x8f, 0xab, + 0x3d, 0x21, 0xf5, 0x5e, 0xd9, 0x1e, 0x40, 0x3b, 0xd9, 0xfb, 0x5d, 0xb1, 0xbd, 0x4f, 0x0b, 0xa1, 0xe8, 0x98, 0x3d, + 0xbe, 0xba, 0xfa, 0xb4, 0x27, 0x8b, 0xbd, 0x4f, 0x99, 0x52, 0x9f, 0xf6, 0xb8, 0x50, 0x9a, 0xd1, 0x51, 0x12, 0xa1, + 0xae, 0xe9, 0x2c, 0x53, 0xea, 0x1d, 0xbb, 0xd5, 0x44, 0x63, 0xf3, 0xa9, 0x09, 0x5b, 0x4f, 0x98, 0xde, 0x53, 0xe5, + 0xbc, 0x62, 0xb4, 0xcc, 0x99, 0xde, 0xd3, 0xc4, 0xe4, 0x4b, 0x07, 0x7f, 0x66, 0x3f, 0x75, 0x97, 0x8f, 0xe3, 0x1b, + 0x71, 0x70, 0xa0, 0x4b, 0x40, 0xa3, 0xa5, 0x5b, 0x21, 0xc2, 0xf6, 0x7d, 0xda, 0xc1, 0x01, 0x4b, 0x72, 0x26, 0x26, + 0x7a, 0x4a, 0x08, 0x69, 0x77, 0xc5, 0xc1, 0x41, 0xac, 0xc9, 0x07, 0x91, 0x4c, 0x98, 0x8e, 0x19, 0x42, 0xb8, 0xaa, + 0x7d, 0x70, 0x10, 0x5b, 0x20, 0x48, 0xa2, 0x0d, 0xe0, 0x6a, 0x30, 0x46, 0x89, 0x83, 0xfe, 0xd5, 0x9d, 0xc8, 0xe2, + 0x70, 0xfc, 0x08, 0x8b, 0x83, 0x83, 0x0f, 0x22, 0x51, 0xd0, 0x22, 0xd6, 0x08, 0xad, 0x0b, 0xa6, 0x17, 0x85, 0xd8, + 0xd3, 0x6b, 0x2d, 0xaf, 0x74, 0xc1, 0xc5, 0x24, 0x46, 0x4b, 0x9f, 0x16, 0x54, 0x5c, 0xaf, 0xed, 0x70, 0x7f, 0x2b, + 0x08, 0x27, 0x97, 0xd0, 0xe3, 0x33, 0x19, 0x3b, 0x1c, 0xe4, 0x84, 0x44, 0xca, 0xd4, 0x8d, 0x7a, 0x3c, 0xe5, 0x8d, + 0x28, 0xc2, 0x76, 0x94, 0xf8, 0xb3, 0x40, 0x58, 0x68, 0x40, 0xdd, 0x24, 0x49, 0x34, 0x22, 0x97, 0x4b, 0x0f, 0x16, + 0x1e, 0x4c, 0xb4, 0xc7, 0xfb, 0xad, 0x41, 0xaa, 0x93, 0x82, 0x8d, 0x16, 0x19, 0x8b, 0x63, 0x81, 0x15, 0x96, 0x88, + 0x5c, 0x8a, 0x46, 0x5c, 0x90, 0x4b, 0x58, 0xef, 0xa2, 0xbe, 0xd8, 0x84, 0xec, 0xb7, 0x90, 0x1b, 0x64, 0xe1, 0x47, + 0x08, 0x20, 0x76, 0x03, 0x2a, 0x08, 0x89, 0xc4, 0x62, 0x36, 0x64, 0x45, 0x54, 0x16, 0xeb, 0xd6, 0xf0, 0x62, 0xa1, + 0xd8, 0x5e, 0xa6, 0xd4, 0xde, 0x78, 0x21, 0x32, 0xcd, 0xa5, 0xd8, 0x8b, 0x1a, 0x45, 0x23, 0xb2, 0xf8, 0x50, 0xa2, + 0x43, 0x84, 0xd6, 0x28, 0x56, 0xa8, 0xc1, 0xfb, 0xb2, 0xd1, 0x1e, 0x60, 0x18, 0x25, 0xea, 0xba, 0xf6, 0x1c, 0x04, + 0x18, 0xe6, 0x30, 0xc9, 0x35, 0xfe, 0xd3, 0xee, 0x7c, 0x98, 0xe2, 0x8d, 0xe8, 0xf1, 0x64, 0x7b, 0xa7, 0x10, 0x9d, + 0xcc, 0xe8, 0x3c, 0x66, 0xe4, 0x92, 0x19, 0xec, 0xa2, 0x22, 0x83, 0xb1, 0xd6, 0x16, 0xae, 0xc7, 0x52, 0x96, 0x54, + 0x38, 0x85, 0x52, 0x9d, 0x8c, 0x65, 0xf1, 0x94, 0x66, 0x53, 0xa8, 0x57, 0x62, 0xcc, 0xc8, 0x6f, 0xb8, 0xac, 0x60, + 0x54, 0xb3, 0xa7, 0x39, 0x83, 0xaf, 0x38, 0x32, 0x35, 0x23, 0x84, 0x15, 0x6c, 0xf5, 0x9c, 0xeb, 0x57, 0x52, 0x64, + 0xac, 0xab, 0x02, 0xfc, 0x32, 0x2b, 0xff, 0x50, 0xeb, 0x82, 0x0f, 0x17, 0x9a, 0xc5, 0x91, 0x80, 0x12, 0x11, 0x56, + 0x08, 0x8b, 0x44, 0xb3, 0x5b, 0xfd, 0x58, 0x0a, 0xcd, 0x84, 0x26, 0xcc, 0x43, 0x15, 0xf3, 0x84, 0xce, 0xe7, 0x4c, + 0x8c, 0x1e, 0x4f, 0x79, 0x3e, 0x8a, 0x05, 0x5a, 0xa3, 0x35, 0xfe, 0x5d, 0x10, 0x98, 0x24, 0xb9, 0xe4, 0x29, 0xfc, + 0xf3, 0xed, 0xe9, 0xc4, 0x9a, 0x5c, 0x9a, 0x6d, 0xc1, 0x48, 0x14, 0x75, 0xc7, 0xb2, 0x88, 0xdd, 0x14, 0xf6, 0x80, + 0x74, 0x41, 0x1f, 0x6f, 0x17, 0x39, 0x53, 0x88, 0x35, 0x88, 0x28, 0xd7, 0xd1, 0x41, 0xf8, 0xb7, 0x22, 0x66, 0xb0, + 0x00, 0x1c, 0xa5, 0xdc, 0x90, 0xc0, 0x97, 0xdc, 0x6d, 0xaa, 0x51, 0x49, 0xd4, 0x3e, 0x0a, 0x32, 0xe2, 0x89, 0x2e, + 0x16, 0x4a, 0xb3, 0xd1, 0xbb, 0xbb, 0x39, 0x53, 0xf8, 0xe7, 0x82, 0x7c, 0x14, 0xbd, 0x8f, 0x22, 0x61, 0xb3, 0xb9, + 0xbe, 0xbb, 0x32, 0xd4, 0x3c, 0x8d, 0x22, 0xfc, 0x8f, 0x29, 0x5a, 0x30, 0x9a, 0x01, 0x49, 0x73, 0x20, 0x7b, 0x23, + 0xf3, 0xbb, 0x31, 0xcf, 0xf3, 0xab, 0xc5, 0x7c, 0x2e, 0x0b, 0x8d, 0xb5, 0x20, 0x4b, 0x2d, 0x2b, 0xf8, 0xc0, 0x8a, + 0x2e, 0xd5, 0x0d, 0xd7, 0xd9, 0x34, 0xd6, 0x68, 0x99, 0x51, 0xc5, 0xf6, 0x1e, 0x49, 0x99, 0x33, 0x2a, 0x52, 0x4e, + 0x78, 0xef, 0xe7, 0x22, 0x15, 0x8b, 0x3c, 0xef, 0x0e, 0x0b, 0x46, 0x3f, 0x77, 0x4d, 0xb6, 0x3d, 0x1c, 0x52, 0xf3, + 0xfb, 0x61, 0x51, 0xd0, 0x3b, 0x28, 0x48, 0x08, 0x14, 0xeb, 0xf1, 0xf4, 0xe7, 0xab, 0xd7, 0xaf, 0x12, 0xbb, 0x57, + 0xf8, 0xf8, 0x2e, 0xe6, 0xe5, 0xfe, 0xe3, 0x6b, 0x3c, 0x2e, 0xe4, 0x6c, 0xa3, 0x6b, 0x0b, 0x3a, 0xde, 0xfd, 0xc6, + 0x10, 0x18, 0xe1, 0xfb, 0xb6, 0xe9, 0x70, 0x04, 0xaf, 0x0c, 0xe6, 0x43, 0x26, 0x71, 0xfd, 0xc2, 0x3f, 0xa9, 0x4d, + 0x8e, 0x39, 0xfa, 0xfe, 0x68, 0x75, 0x71, 0xb7, 0x64, 0xc4, 0x8c, 0x73, 0x0e, 0x07, 0x23, 0x8c, 0x31, 0xa3, 0x3a, + 0x9b, 0x2e, 0x99, 0x69, 0x6c, 0xed, 0x47, 0xcc, 0xd6, 0x6b, 0xfc, 0x55, 0x7a, 0xac, 0xd7, 0xfb, 0x84, 0x70, 0x43, + 0xaf, 0x88, 0x5e, 0xad, 0x38, 0x21, 0x1c, 0xe1, 0xb7, 0x9c, 0x2c, 0xa9, 0x9f, 0x10, 0x9c, 0x6c, 0xb0, 0x3d, 0x53, + 0x4b, 0x65, 0xe0, 0x04, 0xfc, 0xc2, 0x0a, 0xcd, 0x8a, 0x54, 0x0b, 0x5c, 0xb0, 0x71, 0x0e, 0xe3, 0xd8, 0x6f, 0xe3, + 0x29, 0x55, 0x8f, 0xa7, 0x54, 0x4c, 0xd8, 0x28, 0xfd, 0x2a, 0xd7, 0x98, 0x09, 0x12, 0x8d, 0xb9, 0xa0, 0x39, 0xff, + 0xca, 0x46, 0x91, 0x3b, 0x17, 0x1e, 0xe9, 0x3d, 0x76, 0xab, 0x99, 0x18, 0xa9, 0xbd, 0xe7, 0xef, 0x7e, 0x7d, 0xe9, + 0x16, 0xb3, 0x76, 0x56, 0xa0, 0xa5, 0x5a, 0xcc, 0x59, 0x11, 0x23, 0xec, 0xce, 0x8a, 0xa7, 0xdc, 0xd0, 0xc9, 0x5f, + 0xe9, 0xdc, 0xa6, 0x70, 0xf5, 0xfb, 0x7c, 0x44, 0x35, 0x7b, 0xc3, 0xc4, 0x88, 0x8b, 0x09, 0xd9, 0x6f, 0xdb, 0xf4, + 0x29, 0x75, 0x19, 0xa3, 0x32, 0xe9, 0xfa, 0xde, 0xd3, 0xdc, 0xcc, 0xbd, 0xfc, 0x5c, 0xc4, 0x68, 0xad, 0x34, 0xd5, + 0x3c, 0xdb, 0xa3, 0xa3, 0xd1, 0x0b, 0xc1, 0x35, 0x37, 0x23, 0x2c, 0x60, 0x89, 0x00, 0x57, 0x99, 0x3d, 0x35, 0xfc, + 0xc8, 0x63, 0x84, 0xe3, 0xd8, 0x9d, 0x05, 0x53, 0xe4, 0xd6, 0xec, 0xe0, 0xa0, 0xa2, 0xfc, 0x3d, 0x96, 0xda, 0x4c, + 0xd2, 0x1f, 0xa0, 0x64, 0xbe, 0x50, 0xb0, 0xd8, 0xbe, 0x0b, 0x38, 0x68, 0xe4, 0x50, 0xb1, 0xe2, 0x0b, 0x1b, 0x95, + 0x08, 0xa2, 0x62, 0xb4, 0xdc, 0xe8, 0xc3, 0x6d, 0x0f, 0x4d, 0xfa, 0x83, 0x6e, 0x48, 0xc2, 0x99, 0x43, 0x76, 0xcb, + 0xa9, 0x70, 0xa6, 0x4a, 0xa2, 0x12, 0xc3, 0x81, 0x5a, 0x12, 0x16, 0x45, 0xfc, 0xfc, 0xe6, 0xb1, 0x00, 0x1e, 0x22, + 0xa4, 0x1c, 0xfe, 0xcc, 0x7d, 0xfa, 0xc5, 0x1c, 0x1e, 0x0a, 0x0b, 0x84, 0xb5, 0x1d, 0xa9, 0x42, 0x68, 0x8d, 0xb0, + 0xf6, 0xc3, 0xb5, 0x44, 0xc9, 0xf3, 0x45, 0x70, 0x6a, 0x93, 0xb7, 0xdc, 0x1c, 0xdb, 0x40, 0xdb, 0xa8, 0x66, 0x07, + 0x07, 0x31, 0x4b, 0x4a, 0xc4, 0x20, 0xfb, 0x6d, 0xb7, 0x48, 0x01, 0xb4, 0xbe, 0x31, 0x6e, 0xe8, 0xd9, 0x30, 0x38, + 0xfb, 0x2c, 0x11, 0xf2, 0x61, 0x96, 0x31, 0xa5, 0x64, 0x71, 0x70, 0xb0, 0x6f, 0xca, 0x97, 0x9c, 0x05, 0x2c, 0xe2, + 0xeb, 0x1b, 0x51, 0x0d, 0x01, 0x55, 0xa7, 0xad, 0xe7, 0x9b, 0x48, 0xc5, 0x37, 0x79, 0x26, 0x24, 0x8d, 0xae, 0xaf, + 0xa3, 0x86, 0xc6, 0x0e, 0x0e, 0x13, 0xe6, 0xbb, 0xbe, 0x7b, 0xc2, 0x2c, 0x5b, 0x68, 0x98, 0x90, 0x2d, 0xd0, 0xec, + 0xe4, 0x07, 0xe3, 0xfa, 0x90, 0xb0, 0xc6, 0x0a, 0xad, 0x83, 0x15, 0xdd, 0xd9, 0xb4, 0xe1, 0x6f, 0xec, 0xd2, 0x2d, + 0x27, 0x86, 0xa7, 0x08, 0xd6, 0xb1, 0xcf, 0x06, 0x6b, 0x6c, 0x60, 0xef, 0x67, 0x23, 0xcd, 0x40, 0xfb, 0x7a, 0xd0, + 0x75, 0xf9, 0x44, 0x59, 0xc8, 0x15, 0xec, 0x9f, 0x05, 0x53, 0xda, 0x22, 0x72, 0xac, 0xb1, 0xc4, 0x70, 0x46, 0x6d, + 0x32, 0x9d, 0x35, 0x96, 0x74, 0xd7, 0xd8, 0x5e, 0xcf, 0xe1, 0x6c, 0x54, 0x80, 0xd4, 0xdf, 0xc7, 0x27, 0x18, 0xab, + 0x46, 0xab, 0xd5, 0x5b, 0xee, 0x5b, 0xa9, 0xd6, 0xb2, 0xe4, 0xd7, 0x36, 0x16, 0x85, 0x09, 0xe4, 0x0e, 0xe7, 0xfd, + 0xb6, 0x1b, 0xbf, 0x18, 0x90, 0xfd, 0x56, 0x89, 0xc5, 0x0e, 0xac, 0x76, 0x3c, 0x16, 0x8a, 0xaf, 0x6d, 0x53, 0xc8, + 0x9c, 0xf5, 0x35, 0x7c, 0x49, 0xa6, 0x5b, 0xb8, 0x3a, 0x25, 0x7d, 0xe0, 0x3a, 0x92, 0xe9, 0xe0, 0x5b, 0xf8, 0xe4, + 0x29, 0x42, 0xac, 0xb7, 0xf3, 0x2a, 0xc2, 0xf1, 0xa5, 0x4e, 0x38, 0x36, 0xa6, 0x11, 0xcd, 0xcb, 0x2a, 0x51, 0x89, + 0x66, 0x6e, 0xab, 0x57, 0x59, 0x58, 0x98, 0xc1, 0x54, 0x53, 0x0a, 0x9a, 0x78, 0x45, 0x67, 0x4c, 0xc5, 0x0c, 0xe1, + 0x6f, 0x15, 0xb0, 0xf8, 0x09, 0x45, 0x06, 0xc1, 0x19, 0xaa, 0xe0, 0x0c, 0x05, 0x76, 0x17, 0x98, 0xb4, 0xfa, 0x96, + 0x53, 0x98, 0xf5, 0xd5, 0xa0, 0xe2, 0xed, 0x82, 0xc9, 0x9b, 0xc3, 0xd9, 0x21, 0xb8, 0x87, 0x9f, 0x4d, 0xb3, 0x40, + 0x33, 0x2c, 0x84, 0x42, 0x78, 0xbf, 0xb5, 0xb9, 0x92, 0xbe, 0x54, 0x35, 0xc7, 0xfe, 0x00, 0xd6, 0xc1, 0x1c, 0x1b, + 0x09, 0x57, 0xe6, 0x6f, 0x6d, 0xab, 0x01, 0xd8, 0xae, 0x00, 0x33, 0x92, 0x71, 0x4e, 0x75, 0xdc, 0x3e, 0x6a, 0x01, + 0x63, 0xfa, 0x85, 0xc1, 0xa9, 0x82, 0xd0, 0xf6, 0x54, 0x58, 0xb2, 0x10, 0x6a, 0xca, 0xc7, 0x3a, 0xfe, 0x5d, 0x18, + 0xa2, 0xc2, 0x72, 0xc5, 0x40, 0xc2, 0x09, 0xd8, 0x63, 0x43, 0x70, 0x7e, 0x17, 0xd0, 0x4f, 0xb7, 0x3c, 0x88, 0xdc, + 0x48, 0x0d, 0xe1, 0x02, 0xf2, 0x50, 0xb1, 0xd6, 0x15, 0x99, 0x29, 0x19, 0x37, 0xe0, 0x1e, 0xdb, 0x3d, 0xdb, 0x62, + 0xea, 0xa8, 0x81, 0x08, 0x38, 0x58, 0x91, 0x86, 0x24, 0xc2, 0x25, 0xea, 0x44, 0xcb, 0x97, 0xf2, 0x86, 0x15, 0x8f, + 0x29, 0x0c, 0x3e, 0xb5, 0xd5, 0xd7, 0xf6, 0x28, 0x30, 0x14, 0x5f, 0x77, 0x3d, 0xbe, 0x5c, 0x9b, 0x89, 0xbf, 0x29, + 0xe4, 0x8c, 0x2b, 0x06, 0x7c, 0x9b, 0x85, 0xbf, 0x80, 0x8d, 0x66, 0x76, 0x24, 0x1c, 0x37, 0xac, 0xc4, 0xaf, 0x87, + 0x2f, 0xeb, 0xf8, 0x75, 0x7d, 0xef, 0xe9, 0xc4, 0x53, 0xc0, 0xfa, 0x3e, 0x46, 0x38, 0x76, 0xe2, 0x45, 0x70, 0xd2, + 0x25, 0x53, 0xe4, 0x8e, 0xf9, 0xd5, 0x4a, 0x07, 0x62, 0x5c, 0x8d, 0x73, 0x64, 0x76, 0xdb, 0xa0, 0x35, 0x1d, 0x8d, + 0x80, 0xc5, 0x2b, 0x64, 0x9e, 0x07, 0x87, 0x15, 0x16, 0xdd, 0xf2, 0x78, 0xba, 0xbe, 0xf7, 0xf4, 0xea, 0x7b, 0x27, + 0x14, 0xe4, 0x87, 0x87, 0x94, 0x1f, 0xa8, 0x18, 0xb1, 0x02, 0xe4, 0xca, 0x60, 0xb5, 0xdc, 0x39, 0xfb, 0x58, 0x0a, + 0xc1, 0x32, 0xcd, 0x46, 0x20, 0xb4, 0x08, 0xa2, 0x93, 0xa9, 0x54, 0xba, 0x4c, 0xac, 0x46, 0x2f, 0x42, 0x21, 0x34, + 0xc9, 0x68, 0x9e, 0xc7, 0x56, 0x40, 0x99, 0xc9, 0x2f, 0x6c, 0xc7, 0xa8, 0xbb, 0xb5, 0x21, 0x97, 0xcd, 0xb0, 0xa0, + 0x19, 0x96, 0xa8, 0x79, 0xce, 0x33, 0x56, 0x1e, 0x5e, 0x57, 0x09, 0x17, 0x23, 0x76, 0x0b, 0x74, 0x04, 0x5d, 0x5e, + 0x5e, 0xb6, 0x70, 0x1b, 0xad, 0x2d, 0xc0, 0x97, 0x5b, 0x80, 0xfd, 0xce, 0xb1, 0x69, 0x05, 0xf1, 0xe5, 0x4e, 0xb2, + 0x86, 0x82, 0xb3, 0x92, 0x7b, 0x41, 0xcb, 0x92, 0x67, 0x84, 0x47, 0x2c, 0x67, 0x9a, 0x79, 0x72, 0x0e, 0xcc, 0xb4, + 0xdd, 0xba, 0x6f, 0x4b, 0xf8, 0x95, 0xe8, 0xe4, 0x77, 0x99, 0x5f, 0x73, 0x55, 0x8a, 0xee, 0xd5, 0xf2, 0x54, 0xd0, + 0xee, 0x69, 0xbb, 0x3c, 0x54, 0x6b, 0x9a, 0x4d, 0xad, 0xc4, 0x1e, 0x6f, 0x4d, 0xa9, 0x6a, 0xc3, 0x91, 0xf6, 0x72, + 0x13, 0xfd, 0x59, 0xb8, 0x61, 0xee, 0x02, 0xc1, 0x95, 0x23, 0x0a, 0x0c, 0x84, 0x40, 0xbb, 0x6c, 0x8f, 0x69, 0x9e, + 0x0f, 0x69, 0xf6, 0xb9, 0x8e, 0xfd, 0x15, 0x1a, 0x90, 0x4d, 0x6a, 0x1c, 0x64, 0x05, 0x24, 0x2b, 0x9c, 0xb7, 0xa7, + 0xd2, 0xb5, 0x8d, 0x12, 0xef, 0xb7, 0x2a, 0xb4, 0xaf, 0x2f, 0xf4, 0x37, 0xb1, 0xdd, 0x8c, 0x48, 0xb8, 0x99, 0xc5, + 0x40, 0x05, 0xfe, 0x25, 0xc6, 0x79, 0x7a, 0xe0, 0xf0, 0x0e, 0x04, 0x8f, 0xf5, 0xc6, 0x40, 0x34, 0x5a, 0xae, 0x47, + 0x5c, 0x7d, 0x1b, 0x02, 0xff, 0x5b, 0x46, 0xf9, 0x24, 0xe8, 0xe1, 0xdf, 0x1d, 0x68, 0x49, 0xe3, 0x1c, 0xe3, 0x5c, + 0x8e, 0xcc, 0x31, 0x14, 0x9e, 0xd0, 0xfc, 0x02, 0xcc, 0x8b, 0xc1, 0xf7, 0xd7, 0x36, 0xcb, 0xf0, 0x65, 0x30, 0x0c, + 0xd5, 0x0d, 0x19, 0x8a, 0x1a, 0x0a, 0x38, 0xa2, 0x2a, 0xcc, 0x99, 0x2b, 0x6b, 0xa2, 0xa4, 0xe3, 0xda, 0xad, 0x38, + 0xee, 0x68, 0x6e, 0x41, 0xe2, 0x38, 0x56, 0x20, 0xcd, 0x79, 0xfe, 0xbe, 0x9a, 0x85, 0xda, 0x9a, 0x85, 0x4a, 0x02, + 0x69, 0x0b, 0x55, 0xc8, 0x1c, 0x54, 0x4f, 0xb5, 0x40, 0x61, 0x29, 0x60, 0x59, 0x13, 0xa0, 0xd0, 0xa8, 0x24, 0xb8, + 0x39, 0xd1, 0xb8, 0x70, 0xa2, 0x8e, 0xc3, 0x35, 0x20, 0x19, 0x55, 0x15, 0x89, 0xec, 0xe6, 0xa8, 0xc9, 0xbe, 0x12, + 0x17, 0x68, 0x83, 0xbf, 0x5f, 0xaf, 0x1d, 0x94, 0x18, 0x72, 0xab, 0x53, 0x63, 0x8c, 0x03, 0xb0, 0x60, 0x49, 0x1c, + 0x33, 0x6c, 0x59, 0x9f, 0x4d, 0xe0, 0x94, 0xed, 0xee, 0x13, 0x22, 0x2b, 0xd8, 0xd4, 0x98, 0x4a, 0xcf, 0x5d, 0x49, + 0x84, 0xa9, 0x67, 0x4b, 0x8b, 0x6a, 0xe2, 0x84, 0x44, 0x5e, 0x3b, 0x11, 0xf5, 0x96, 0x35, 0xe1, 0x30, 0x0d, 0x8a, + 0xad, 0x53, 0x20, 0xaa, 0xc5, 0x2e, 0x78, 0xef, 0xc2, 0x9a, 0x5a, 0x3b, 0x01, 0xc4, 0x8b, 0x1a, 0xc4, 0x03, 0xd0, + 0x4a, 0x4b, 0xbc, 0xe4, 0x80, 0xd0, 0x7a, 0xe5, 0x98, 0xe1, 0xc2, 0x2e, 0xc4, 0x16, 0x14, 0x37, 0xd9, 0x4f, 0x83, + 0x85, 0x20, 0xcb, 0x2a, 0xe0, 0xef, 0xc2, 0x23, 0x22, 0x86, 0xc1, 0x8b, 0xd5, 0x6a, 0x0b, 0xed, 0x76, 0x72, 0xa1, + 0x28, 0xa9, 0xa4, 0xc3, 0xd5, 0xea, 0xab, 0x44, 0xb1, 0xe3, 0x7f, 0x31, 0x43, 0x3d, 0x4f, 0x74, 0x1f, 0xbe, 0x84, + 0x52, 0x86, 0x1d, 0xad, 0x52, 0x4a, 0xc1, 0xa1, 0x8e, 0xb5, 0xf5, 0x85, 0xd2, 0x01, 0xe5, 0x7e, 0xbc, 0x45, 0xc0, + 0x4c, 0xa2, 0x3b, 0xa9, 0xab, 0x29, 0x3f, 0x76, 0x4d, 0x0b, 0x84, 0x50, 0xaa, 0x8c, 0x2c, 0xb3, 0xbf, 0x4b, 0xbe, + 0x3c, 0x38, 0x50, 0x41, 0x43, 0xd7, 0x25, 0xa5, 0xf8, 0x3b, 0x86, 0x53, 0x59, 0xdd, 0x09, 0xc3, 0xbe, 0xfc, 0xed, + 0xcf, 0xa1, 0x2d, 0xe9, 0xb4, 0xd5, 0x05, 0xc1, 0x9c, 0xde, 0x50, 0xae, 0xf7, 0xca, 0x56, 0xac, 0x60, 0x1e, 0x33, + 0xb4, 0x74, 0xdc, 0x46, 0x52, 0x30, 0xe0, 0x1f, 0x81, 0x2c, 0x78, 0x2e, 0xda, 0x22, 0x7e, 0x36, 0x65, 0xa0, 0xca, + 0xf6, 0x8c, 0x44, 0x29, 0x1e, 0xee, 0xbb, 0x83, 0xc4, 0x35, 0xbc, 0x7b, 0xec, 0xeb, 0xcd, 0xea, 0x35, 0x69, 0x60, + 0xce, 0x8a, 0xb1, 0x2c, 0x66, 0x3e, 0x6f, 0xbd, 0xf1, 0xed, 0x88, 0x23, 0x1f, 0xc7, 0x3b, 0xdb, 0x76, 0x22, 0x40, + 0x77, 0x43, 0xf6, 0xae, 0xa4, 0xf6, 0xda, 0x69, 0x5a, 0x1e, 0xc0, 0x56, 0x41, 0xe8, 0x31, 0x53, 0x85, 0x52, 0xbe, + 0x53, 0xaf, 0x76, 0xad, 0xee, 0x64, 0xbf, 0xdd, 0x2d, 0x25, 0x3f, 0x8f, 0x0d, 0x5d, 0xab, 0xe3, 0x70, 0xa7, 0xaa, + 0x5c, 0xe4, 0x23, 0x37, 0x58, 0x81, 0x30, 0x73, 0x78, 0x74, 0xc3, 0xf3, 0xbc, 0x4a, 0xfd, 0x4f, 0x48, 0xbb, 0x72, + 0xa4, 0x5d, 0x7a, 0xd2, 0x0e, 0xa4, 0x02, 0x48, 0xbb, 0x6d, 0xae, 0xaa, 0x2e, 0xb7, 0xb6, 0xa7, 0xb4, 0x44, 0x5d, + 0x19, 0x71, 0x1a, 0xfa, 0x5b, 0xf8, 0x11, 0xa0, 0x92, 0xf9, 0xfa, 0x1c, 0x3b, 0x7d, 0x0c, 0x88, 0x81, 0x56, 0xa7, + 0xc9, 0x42, 0x4d, 0xc5, 0xe7, 0x18, 0x61, 0xb5, 0x66, 0x25, 0x66, 0x3f, 0x7c, 0x0a, 0x4a, 0xbb, 0x60, 0x3a, 0x70, + 0x8e, 0x99, 0xe4, 0xff, 0x88, 0x8f, 0xf2, 0xb3, 0x13, 0x6e, 0x76, 0xca, 0xcf, 0x0e, 0x68, 0x7d, 0x35, 0xbb, 0xd1, + 0xf7, 0xa9, 0xbd, 0x99, 0x9e, 0x28, 0xa7, 0x57, 0xad, 0xf7, 0x6a, 0x15, 0x6f, 0xa4, 0x80, 0x46, 0xdf, 0x49, 0x29, + 0x45, 0xd9, 0x3a, 0xd0, 0x80, 0x10, 0x32, 0x90, 0xb0, 0xb6, 0x93, 0x2e, 0x4f, 0xb9, 0x97, 0xff, 0x4a, 0xcf, 0x63, + 0x14, 0xf7, 0xb6, 0xfe, 0x63, 0x39, 0x9b, 0x03, 0x43, 0xb6, 0x81, 0xd2, 0x13, 0xe6, 0x3a, 0xac, 0xf2, 0xd7, 0x3b, + 0xd2, 0x6a, 0x75, 0xcc, 0x7e, 0xac, 0x61, 0x53, 0x29, 0x35, 0xef, 0xb7, 0xd6, 0x8b, 0x32, 0xa9, 0x24, 0x1c, 0xbb, + 0x74, 0x2b, 0x8f, 0x37, 0x35, 0x33, 0x3e, 0xe3, 0x75, 0x2c, 0x2c, 0x1d, 0x16, 0x40, 0xeb, 0x02, 0xf2, 0xe3, 0xd1, + 0x3d, 0x5c, 0xff, 0x75, 0x05, 0x9c, 0xe5, 0x7a, 0x03, 0x7c, 0xcb, 0xf5, 0xfa, 0x91, 0x76, 0x92, 0x36, 0x7e, 0xb4, + 0x43, 0xee, 0x2d, 0xa1, 0x57, 0x65, 0x3a, 0x99, 0xb1, 0x3f, 0x80, 0xb4, 0x2d, 0x16, 0x92, 0x2c, 0x67, 0x72, 0xc4, + 0xd2, 0x48, 0xce, 0x99, 0x88, 0xd6, 0xa0, 0x67, 0x75, 0x08, 0xf0, 0x8f, 0x88, 0x97, 0x6f, 0xeb, 0xfa, 0xd6, 0xf4, + 0x91, 0x5e, 0x83, 0x2a, 0xec, 0x25, 0xdf, 0xa1, 0x8c, 0x7d, 0xcf, 0x0a, 0x65, 0x78, 0xd2, 0x92, 0xbd, 0x7d, 0xc9, + 0xab, 0x03, 0xea, 0x25, 0x4f, 0xbf, 0x5d, 0xa5, 0x12, 0x48, 0xa2, 0x76, 0x72, 0x96, 0x1c, 0x47, 0xc8, 0x68, 0x8c, + 0x9f, 0x79, 0x8d, 0xf1, 0xa2, 0xd4, 0x18, 0x3f, 0xd7, 0x64, 0xb1, 0xa1, 0x31, 0xfe, 0x43, 0x90, 0xe7, 0xba, 0xf7, + 0xdc, 0x6b, 0xd3, 0xdf, 0xc8, 0x9c, 0x67, 0x77, 0x71, 0x94, 0x73, 0xdd, 0x84, 0xdb, 0xc4, 0x08, 0x2f, 0x6d, 0x06, + 0xa8, 0x1a, 0x8d, 0xbe, 0x7b, 0xed, 0xe5, 0x3f, 0x2c, 0x04, 0x89, 0xee, 0xe5, 0x5c, 0xdf, 0x8b, 0xf0, 0x54, 0x93, + 0x4f, 0xf0, 0xeb, 0xde, 0x32, 0xfe, 0x95, 0xea, 0x69, 0x52, 0x50, 0x31, 0x92, 0xb3, 0x18, 0x35, 0xa2, 0x08, 0x25, + 0xca, 0x08, 0x21, 0x0f, 0xd0, 0xfa, 0xde, 0x27, 0xfc, 0x4a, 0x92, 0xa8, 0x17, 0x35, 0xa6, 0x1a, 0x6b, 0x4a, 0x3e, + 0x5d, 0xdc, 0x5b, 0xbe, 0x92, 0xeb, 0xcb, 0x4f, 0xf8, 0xa9, 0x2e, 0xd5, 0xfa, 0xf8, 0x96, 0x91, 0x18, 0x91, 0xcb, + 0xa7, 0x7e, 0x48, 0x8f, 0xe5, 0xcc, 0x2a, 0xf8, 0x23, 0x84, 0xbf, 0x80, 0x5e, 0xf7, 0x92, 0x57, 0x44, 0xc8, 0xdd, + 0xc1, 0xec, 0x93, 0x48, 0x1a, 0xe5, 0x41, 0x74, 0x70, 0x10, 0xa4, 0x95, 0x2c, 0x04, 0xfe, 0x5b, 0x92, 0x9a, 0xa8, + 0x8e, 0x19, 0x85, 0x96, 0xfe, 0x96, 0x31, 0x47, 0xbe, 0x99, 0xd8, 0x6b, 0xaa, 0xdd, 0x8e, 0xe5, 0x7d, 0xab, 0x7b, + 0x48, 0xb8, 0x66, 0x05, 0xd5, 0xb2, 0x18, 0xa0, 0x90, 0x2d, 0xc1, 0x5f, 0x39, 0xf9, 0xd4, 0xdf, 0xfb, 0x7f, 0xfe, + 0xc7, 0x5f, 0xe3, 0xbf, 0x8a, 0xc1, 0x27, 0x2c, 0x18, 0x39, 0xba, 0x88, 0x7b, 0x69, 0xbc, 0xdf, 0x6c, 0xae, 0xfe, + 0x3a, 0xea, 0xff, 0x37, 0x6d, 0x7e, 0x7d, 0xd8, 0xfc, 0x73, 0x80, 0x56, 0xf1, 0x5f, 0x47, 0xbd, 0xbe, 0xfb, 0xea, + 0xff, 0xf7, 0xe5, 0x5f, 0x6a, 0x70, 0x68, 0x13, 0xef, 0x21, 0x74, 0x34, 0xc1, 0xbf, 0x08, 0x72, 0xd4, 0x6c, 0x5e, + 0x1e, 0x4d, 0xf0, 0x4f, 0x82, 0x1c, 0xc1, 0xdf, 0x3b, 0x4d, 0xde, 0xb2, 0xc9, 0xd3, 0xdb, 0x79, 0xfc, 0xe9, 0x72, + 0x75, 0x6f, 0xf9, 0x95, 0xaf, 0xa1, 0xdd, 0xfe, 0x7f, 0xff, 0xf5, 0x97, 0x8a, 0x7e, 0xbc, 0x24, 0x47, 0x83, 0x06, + 0x8a, 0x4d, 0xf2, 0x21, 0xb1, 0x7f, 0xe2, 0x5e, 0xda, 0xff, 0x6f, 0x37, 0x94, 0xe8, 0xc7, 0xbf, 0x3e, 0x5d, 0x5c, + 0x92, 0xc1, 0x2a, 0x8e, 0x56, 0x3f, 0xa2, 0x15, 0x42, 0xab, 0x7b, 0xe8, 0x13, 0x8e, 0x26, 0x11, 0xc2, 0xbf, 0x09, + 0x72, 0xf4, 0xe3, 0xd1, 0x04, 0xff, 0x29, 0xc8, 0x51, 0x74, 0x34, 0xc1, 0xef, 0x25, 0x39, 0xfa, 0xef, 0xb8, 0x97, + 0x5a, 0x25, 0xdc, 0xca, 0xa8, 0x3f, 0x56, 0x70, 0x13, 0x42, 0x0b, 0x46, 0x57, 0x9a, 0xeb, 0x9c, 0xa1, 0x7b, 0x47, + 0x1c, 0x3f, 0x92, 0x00, 0xac, 0x58, 0x83, 0x92, 0xc6, 0x5c, 0xc2, 0x2e, 0xaf, 0x61, 0xe1, 0x01, 0x83, 0xee, 0xa5, + 0x1c, 0x5b, 0x3d, 0x81, 0x4a, 0xb5, 0xbd, 0xbd, 0x55, 0x70, 0x7d, 0x8b, 0x1f, 0x93, 0x47, 0x32, 0x6e, 0x23, 0xcc, + 0x29, 0xfc, 0xe8, 0x20, 0xfc, 0x41, 0xbb, 0x0b, 0x4f, 0xd8, 0xe6, 0x16, 0xc3, 0x84, 0xb4, 0xfc, 0x4c, 0x84, 0xf0, + 0xcb, 0x1d, 0x99, 0x7a, 0x0a, 0xea, 0x07, 0x84, 0x7f, 0xae, 0x5d, 0x8f, 0xe2, 0xc7, 0x9a, 0x94, 0xc8, 0xf1, 0xae, + 0x60, 0xec, 0x03, 0xcd, 0x3f, 0xb3, 0x22, 0x7e, 0xaa, 0x71, 0xbb, 0xf3, 0x00, 0x1b, 0x55, 0xf5, 0x7e, 0x1b, 0x75, + 0xcb, 0xdb, 0xad, 0xe7, 0xd2, 0xde, 0x27, 0xc0, 0x29, 0x5c, 0xd7, 0xd7, 0xc0, 0xda, 0xef, 0xf3, 0x2d, 0xa5, 0x56, + 0x41, 0x6f, 0x22, 0x54, 0xbf, 0x4a, 0xe5, 0xe2, 0x0b, 0xcd, 0xf9, 0x68, 0x4f, 0xb3, 0xd9, 0x3c, 0xa7, 0x9a, 0xed, + 0xb9, 0x39, 0xef, 0x51, 0x68, 0x28, 0x2a, 0x79, 0x8a, 0x3f, 0x44, 0xb5, 0x69, 0xff, 0x10, 0x49, 0xb5, 0x77, 0x62, + 0xb8, 0xcf, 0x72, 0x7c, 0x89, 0xa0, 0xe5, 0x75, 0xd9, 0xe6, 0x8d, 0x60, 0xb3, 0x0d, 0xca, 0xb2, 0x81, 0x39, 0xbf, + 0x15, 0x86, 0xfb, 0x4d, 0x42, 0x3a, 0xbd, 0xe8, 0x42, 0x7d, 0x99, 0x5c, 0x46, 0x70, 0x93, 0x53, 0x10, 0xc1, 0x8c, + 0xf2, 0x08, 0x4a, 0x50, 0xd2, 0xea, 0xd2, 0x0b, 0xd6, 0xa5, 0x8d, 0x86, 0x67, 0xb3, 0x33, 0xc2, 0xfb, 0xd4, 0xd6, + 0xcf, 0xf1, 0x14, 0x8f, 0x48, 0xb3, 0x8d, 0x17, 0xa4, 0x65, 0xaa, 0x74, 0x17, 0x17, 0x99, 0xeb, 0xe7, 0xe0, 0x20, + 0x2e, 0x92, 0x9c, 0x2a, 0xfd, 0x02, 0x34, 0x02, 0x64, 0x81, 0xa7, 0xa4, 0x48, 0xd8, 0x2d, 0xcb, 0xe2, 0x0c, 0xe1, + 0xa9, 0xa3, 0x41, 0xa8, 0x8b, 0x16, 0x24, 0x28, 0x06, 0x72, 0x06, 0x11, 0xac, 0x37, 0xed, 0xb7, 0x07, 0x84, 0x90, + 0x68, 0xbf, 0xd9, 0x8c, 0x7a, 0x05, 0xf9, 0x45, 0xa4, 0x90, 0x12, 0xb0, 0xd3, 0xe4, 0x27, 0x48, 0xea, 0x04, 0x49, + 0xf1, 0x7b, 0x99, 0x68, 0xa6, 0x74, 0x0c, 0xc9, 0xa0, 0x24, 0x50, 0x1e, 0xc3, 0xa3, 0x8b, 0xa3, 0xa8, 0x01, 0xa9, + 0x06, 0x45, 0x11, 0x2e, 0xc8, 0x9d, 0x46, 0xe9, 0xb4, 0x7f, 0x3c, 0x08, 0xcf, 0x08, 0x9b, 0x0a, 0xfd, 0xdf, 0xe9, + 0xde, 0xb4, 0xdf, 0x32, 0xfd, 0x5f, 0x46, 0xbd, 0xb8, 0x20, 0xca, 0xb2, 0x71, 0x3d, 0x95, 0x0a, 0x66, 0xe6, 0x8b, + 0x52, 0x37, 0x40, 0xd7, 0xf7, 0x88, 0x34, 0x3b, 0x69, 0x3c, 0x0a, 0x67, 0xd2, 0x84, 0x0e, 0x1d, 0x28, 0x70, 0x4e, + 0xa0, 0x3c, 0x2e, 0x08, 0x74, 0x5a, 0x55, 0xbb, 0xd3, 0xa9, 0x4b, 0xf8, 0x31, 0xfa, 0xb1, 0xf7, 0xa7, 0x48, 0x7f, + 0x13, 0x76, 0x04, 0x7f, 0x8a, 0xd5, 0x0a, 0xfe, 0xfe, 0x26, 0x7a, 0x30, 0x2c, 0x93, 0xf6, 0x8b, 0x4b, 0xfb, 0x09, + 0xd2, 0x04, 0x4b, 0xcd, 0x80, 0xb1, 0x2a, 0xf9, 0x31, 0xbb, 0x38, 0x63, 0x62, 0x67, 0x70, 0x70, 0xc0, 0xfb, 0xb4, + 0xd1, 0x1e, 0xc0, 0x8d, 0x40, 0xa1, 0xd5, 0x07, 0xae, 0xa7, 0x71, 0x74, 0x74, 0x19, 0xa1, 0x5e, 0xb4, 0x07, 0xab, + 0xdc, 0x95, 0x0d, 0xe2, 0x60, 0x9d, 0x35, 0x34, 0x4d, 0x47, 0x97, 0xa4, 0xd5, 0x8b, 0x85, 0x25, 0xf2, 0x39, 0xc2, + 0x99, 0xa3, 0xa9, 0x2d, 0x3c, 0x42, 0x0d, 0x21, 0x1a, 0xfe, 0x7b, 0x84, 0x1a, 0x53, 0xdd, 0x18, 0xa3, 0x34, 0x83, + 0xbf, 0xf1, 0x88, 0x10, 0xd2, 0xec, 0x94, 0x15, 0xfd, 0x61, 0x49, 0x51, 0x3a, 0xf6, 0xea, 0xd1, 0xbe, 0xd9, 0x1c, + 0xb2, 0x11, 0xf3, 0x3e, 0x1b, 0xac, 0x56, 0xd1, 0x45, 0xef, 0x32, 0x42, 0x8d, 0xd8, 0xa3, 0xdd, 0x91, 0xc7, 0x3b, + 0x84, 0xb0, 0x18, 0xac, 0xdd, 0x0d, 0xd4, 0x0d, 0xab, 0xdd, 0x36, 0x2d, 0xab, 0xfd, 0x1f, 0x90, 0x05, 0xb6, 0x2e, + 0xe5, 0x1e, 0xcb, 0xdf, 0xce, 0x61, 0xaa, 0x1e, 0xb7, 0x25, 0x69, 0xe1, 0x82, 0x78, 0x75, 0x37, 0x25, 0xba, 0xc2, + 0xff, 0x8c, 0x54, 0xc5, 0x71, 0x3f, 0xc7, 0xd3, 0x01, 0x11, 0xd4, 0xc8, 0x2f, 0x5d, 0xaf, 0x4c, 0x67, 0x39, 0xb9, + 0x61, 0x1b, 0xf7, 0xbf, 0x39, 0xdc, 0xc9, 0x3c, 0xd6, 0x49, 0xb6, 0x28, 0x0a, 0x26, 0xf4, 0x2b, 0x39, 0x72, 0x8c, + 0x1d, 0xcb, 0x41, 0xb6, 0x82, 0x8b, 0x5d, 0x0c, 0x5c, 0x5d, 0xc7, 0xef, 0x94, 0xd1, 0x56, 0xf6, 0x82, 0x8c, 0x2c, + 0xc3, 0x65, 0xae, 0x7b, 0xbb, 0x0b, 0x27, 0x4a, 0xc7, 0x08, 0x8f, 0xdc, 0x3d, 0x70, 0x9c, 0x24, 0xc9, 0x22, 0xc9, + 0x20, 0x1b, 0x3a, 0x50, 0x68, 0x6d, 0xf6, 0x55, 0xac, 0xc8, 0x63, 0x9d, 0x08, 0x76, 0x6b, 0xba, 0x8d, 0x51, 0x75, + 0x88, 0xfb, 0xfd, 0x76, 0x41, 0xbb, 0x86, 0x00, 0xa9, 0x44, 0xc8, 0x11, 0x03, 0x08, 0xc1, 0xdd, 0xbf, 0x4b, 0x9a, + 0x52, 0x15, 0xde, 0x6c, 0x55, 0x03, 0xec, 0x87, 0x2a, 0xef, 0x05, 0xe8, 0x89, 0x0d, 0x7b, 0x56, 0x16, 0xb6, 0xca, + 0x73, 0x84, 0xf8, 0x38, 0x5e, 0x24, 0x70, 0x23, 0x68, 0x30, 0x49, 0x08, 0xb4, 0x5a, 0x2d, 0x42, 0xdc, 0x9a, 0x56, + 0x8a, 0xe9, 0x31, 0x99, 0xf6, 0x8b, 0x46, 0xc3, 0x28, 0xaf, 0x47, 0x16, 0x2f, 0x16, 0x08, 0x8f, 0xcb, 0xbd, 0xe6, + 0xcb, 0xcd, 0x49, 0xbd, 0xab, 0x78, 0x5c, 0x57, 0x02, 0x37, 0x84, 0x40, 0x46, 0xbf, 0xa8, 0xa1, 0x75, 0x3c, 0x21, + 0x47, 0x71, 0x3f, 0xe9, 0xfd, 0xcf, 0x01, 0xea, 0xc5, 0xc9, 0x21, 0x3a, 0xb2, 0xb4, 0x64, 0x8c, 0xba, 0x99, 0xed, + 0x63, 0x69, 0x6e, 0x3f, 0xdb, 0xd8, 0x28, 0x20, 0x53, 0x89, 0x05, 0x9d, 0xb1, 0x74, 0x02, 0xbb, 0xde, 0x23, 0xcf, + 0x1c, 0x03, 0x32, 0xa5, 0x13, 0x47, 0x5b, 0x92, 0xa8, 0x27, 0x69, 0xf9, 0xd5, 0x8b, 0x7a, 0xb4, 0xfa, 0xfa, 0x9f, + 0x51, 0x2f, 0xa3, 0xe9, 0x63, 0xbe, 0x76, 0x4a, 0xf2, 0x5a, 0x1f, 0x67, 0xbe, 0x8f, 0xb5, 0x5d, 0x9c, 0x00, 0x78, + 0x23, 0xb4, 0xad, 0x1d, 0x59, 0xa0, 0x35, 0x1f, 0x97, 0xd4, 0x49, 0x25, 0x9a, 0x4e, 0x00, 0xaa, 0xc1, 0x22, 0xa8, + 0xd0, 0x36, 0x20, 0x98, 0x32, 0x60, 0x8b, 0x47, 0x5a, 0x80, 0xe6, 0xe2, 0xb2, 0x85, 0x96, 0xb5, 0xc2, 0x8e, 0xb3, + 0xaa, 0xdf, 0xc5, 0x97, 0xc4, 0x7b, 0x0c, 0x54, 0xf9, 0x62, 0xd1, 0x1d, 0x37, 0x1a, 0x48, 0x79, 0xfc, 0x1a, 0xf5, + 0xc7, 0x03, 0x7c, 0x0b, 0x28, 0x84, 0x6b, 0x18, 0x85, 0x6b, 0x73, 0xec, 0xb8, 0x39, 0x36, 0x1a, 0x72, 0x8d, 0xba, + 0x41, 0xe5, 0x85, 0xab, 0xbc, 0x5e, 0x5b, 0xc8, 0x6c, 0x62, 0xdc, 0x39, 0x32, 0x29, 0x60, 0x08, 0x46, 0x08, 0x79, + 0x25, 0xd1, 0xce, 0x66, 0xa1, 0x51, 0xa8, 0x6e, 0x76, 0x2f, 0x50, 0x54, 0x7b, 0x7a, 0xc4, 0x00, 0x0b, 0xa8, 0x5a, + 0xaa, 0x91, 0xa7, 0x1a, 0x8f, 0x1a, 0x6d, 0x83, 0xee, 0xcd, 0x76, 0xb7, 0xde, 0xd8, 0xfd, 0xaa, 0x31, 0x3c, 0x6a, + 0x90, 0x69, 0xb5, 0xc3, 0xd7, 0xb2, 0xd1, 0x58, 0xd7, 0xef, 0x4b, 0xfd, 0x26, 0xae, 0xdd, 0x5f, 0x3c, 0xdd, 0x32, + 0xf1, 0xf0, 0xa7, 0x6f, 0x75, 0xde, 0x8a, 0x84, 0x0b, 0xc1, 0x0a, 0x38, 0x61, 0x89, 0xc6, 0x62, 0xbd, 0x2e, 0x4f, + 0xfd, 0xdf, 0xb5, 0xb5, 0x19, 0x23, 0x1c, 0xe8, 0x90, 0x91, 0xda, 0xb0, 0xc4, 0x05, 0xa6, 0x86, 0x8a, 0x10, 0x42, + 0x3e, 0x68, 0x6f, 0x1e, 0xa3, 0x0d, 0x49, 0xca, 0x48, 0x70, 0x76, 0xc7, 0x8a, 0xb0, 0xe4, 0xfa, 0xde, 0x63, 0xf9, + 0x5d, 0x91, 0xae, 0x2f, 0x06, 0xa9, 0x29, 0x96, 0x3b, 0x42, 0x96, 0x93, 0x2f, 0x20, 0xe7, 0x94, 0x17, 0x2c, 0x89, + 0x21, 0x88, 0x4f, 0x78, 0xc1, 0x0c, 0xe3, 0x7e, 0xcf, 0xcb, 0x8d, 0x59, 0x9d, 0xd3, 0xcc, 0x42, 0xed, 0x0f, 0x40, + 0x33, 0x07, 0xe5, 0x90, 0x24, 0x5b, 0xc5, 0xae, 0xef, 0x3d, 0x7c, 0xbd, 0x4b, 0x86, 0x5e, 0xad, 0x9c, 0xf4, 0x9c, + 0x01, 0xeb, 0x83, 0xf3, 0x6a, 0xa8, 0x99, 0xfb, 0x91, 0xc6, 0x99, 0x61, 0xa2, 0xf2, 0x98, 0x03, 0x32, 0x5d, 0xdf, + 0x7b, 0xf8, 0x2e, 0xe6, 0x46, 0x37, 0x85, 0x70, 0x38, 0xef, 0xb8, 0x20, 0x31, 0x25, 0x0c, 0xd9, 0xc9, 0x97, 0x74, + 0xac, 0x08, 0x4e, 0xf7, 0x94, 0x9a, 0x4c, 0x10, 0x3b, 0xfa, 0x62, 0x40, 0x32, 0x07, 0x02, 0x92, 0x21, 0x9c, 0xd5, + 0xe4, 0x3a, 0x62, 0xd6, 0xc0, 0x74, 0x76, 0x05, 0x8b, 0x91, 0x58, 0xf6, 0x10, 0xe1, 0xcc, 0x74, 0xab, 0xd7, 0xf6, + 0x38, 0x51, 0x74, 0xd3, 0xd0, 0xad, 0x92, 0x67, 0xdf, 0x83, 0xe0, 0xe5, 0x3f, 0x5e, 0xb9, 0xb6, 0xcb, 0x84, 0x27, + 0xde, 0x22, 0xed, 0xfa, 0xde, 0xc3, 0x5f, 0x9d, 0x51, 0xda, 0x9c, 0x7a, 0xf2, 0xbf, 0x25, 0xa3, 0x3e, 0xfc, 0x35, + 0xa9, 0x72, 0x4d, 0xe1, 0xeb, 0x7b, 0x0f, 0x7f, 0xdf, 0x55, 0x0c, 0xd2, 0xd7, 0x8b, 0x4a, 0x49, 0x60, 0xc6, 0xb7, + 0x64, 0x79, 0xba, 0x74, 0x67, 0x45, 0x2a, 0xd6, 0xd8, 0x9c, 0x50, 0xa9, 0x5a, 0x97, 0xba, 0x95, 0x27, 0x58, 0x12, + 0x73, 0x95, 0x54, 0x5f, 0x36, 0x87, 0xc6, 0x5c, 0x8a, 0xab, 0x4c, 0xce, 0xd9, 0x37, 0xee, 0x97, 0x9e, 0x6a, 0x94, + 0xf0, 0x19, 0x18, 0xe2, 0x98, 0xb1, 0x0b, 0xbc, 0xdf, 0x42, 0xdd, 0x8d, 0xf3, 0x4c, 0x1a, 0x44, 0x2d, 0xea, 0x87, + 0x0d, 0xa6, 0xa4, 0x85, 0x33, 0xd2, 0xc2, 0x39, 0x51, 0xfd, 0x96, 0x3d, 0x31, 0xba, 0x79, 0xd9, 0xb4, 0x3d, 0x77, + 0x60, 0xbb, 0xe7, 0x76, 0xdf, 0xda, 0x43, 0x79, 0xda, 0xcd, 0x8d, 0xfe, 0xd2, 0x1c, 0xf4, 0x53, 0x83, 0x1a, 0x4f, + 0x58, 0x5c, 0xe0, 0xc2, 0xb4, 0x7c, 0xc5, 0x87, 0x39, 0xd8, 0xa9, 0xc0, 0xcc, 0xb0, 0x46, 0x69, 0x59, 0xb6, 0xed, + 0xca, 0xe6, 0x89, 0x59, 0xab, 0x02, 0xe7, 0x09, 0x90, 0x72, 0x9c, 0x3b, 0xbb, 0x1e, 0xb5, 0x5d, 0xe5, 0xec, 0xe0, + 0x20, 0x76, 0x95, 0x68, 0x5c, 0xf8, 0xfc, 0xea, 0x06, 0xf0, 0xbd, 0xa5, 0x1a, 0x53, 0x64, 0x26, 0xd0, 0x68, 0x64, + 0x83, 0x35, 0xdd, 0x27, 0x24, 0xce, 0xeb, 0x50, 0xf4, 0xa3, 0x37, 0xcc, 0xe0, 0x06, 0x00, 0x1a, 0x8d, 0xf2, 0xba, + 0x77, 0x03, 0x62, 0x4f, 0x35, 0x96, 0xeb, 0x2f, 0x71, 0x69, 0x4d, 0xd4, 0xda, 0xb2, 0xc3, 0xf2, 0xa3, 0x40, 0x22, + 0xc4, 0x5d, 0xe1, 0xe7, 0x13, 0x6c, 0x0d, 0x01, 0xe5, 0x5e, 0x38, 0x1b, 0x08, 0x6c, 0xac, 0xb6, 0x5c, 0x21, 0x4f, + 0xda, 0x3a, 0x28, 0xf5, 0x85, 0xe0, 0x82, 0x0b, 0x0a, 0x35, 0xd6, 0x0e, 0xcb, 0x9f, 0xb0, 0x6d, 0x73, 0x4e, 0xac, + 0x90, 0xd3, 0x96, 0x99, 0x61, 0x18, 0x80, 0x75, 0x4a, 0xc0, 0x3c, 0x27, 0x2f, 0xbf, 0x8d, 0xfa, 0x0f, 0x03, 0xd4, + 0x7f, 0x44, 0x58, 0xb0, 0x0d, 0xac, 0xae, 0x24, 0x91, 0x4e, 0x41, 0xa1, 0x7c, 0xd6, 0xe3, 0x39, 0x01, 0x6d, 0x5c, + 0x1d, 0xaa, 0xb5, 0x2b, 0xca, 0x6f, 0x50, 0x96, 0x70, 0xa7, 0x18, 0x7d, 0x26, 0xf6, 0xf7, 0xc9, 0x71, 0x75, 0x41, + 0x07, 0x5d, 0xef, 0x52, 0x0e, 0x86, 0xa4, 0xf0, 0xe1, 0xef, 0xdf, 0xbf, 0x5b, 0x7d, 0x3c, 0xdf, 0xde, 0xc1, 0x81, + 0x59, 0x29, 0xcc, 0x3a, 0xd8, 0xc0, 0x75, 0x23, 0x53, 0xe8, 0xbf, 0xbc, 0x13, 0xaf, 0x53, 0xa1, 0x8d, 0xcd, 0xe8, + 0x8f, 0x43, 0x18, 0x6d, 0xbb, 0x6d, 0x4a, 0xb0, 0xa0, 0x59, 0xa0, 0x4b, 0xd6, 0xb8, 0x95, 0x16, 0xdf, 0x20, 0x23, + 0x0f, 0x4d, 0x01, 0x26, 0x46, 0xbb, 0xb3, 0x1f, 0xad, 0x1d, 0x9e, 0xd8, 0xa1, 0xa1, 0xa5, 0x21, 0x84, 0x16, 0xef, + 0x01, 0x73, 0xec, 0x11, 0x01, 0x20, 0x7a, 0x69, 0x20, 0x55, 0x81, 0x2c, 0x8a, 0x2a, 0x45, 0xfe, 0xf3, 0x7d, 0x42, + 0x5e, 0x56, 0x8a, 0xcc, 0xb7, 0x95, 0x31, 0x17, 0x20, 0x06, 0x4a, 0xe1, 0x22, 0xa1, 0x4c, 0xb0, 0x97, 0xa1, 0x1f, + 0xb4, 0x2f, 0x6f, 0xa4, 0xcd, 0xa4, 0xe2, 0xc6, 0x83, 0x9b, 0x52, 0xa3, 0xe2, 0xb3, 0xf9, 0x1e, 0x12, 0x1b, 0xb9, + 0xf7, 0x20, 0x97, 0x51, 0x33, 0x48, 0xf8, 0x7e, 0x67, 0x4a, 0xfb, 0x76, 0xd7, 0x9f, 0x37, 0x2d, 0x62, 0x36, 0xd6, + 0x25, 0xe1, 0x42, 0xb1, 0x42, 0x3f, 0x62, 0x63, 0x59, 0xc0, 0xfd, 0x47, 0x09, 0x16, 0xb4, 0xbe, 0x17, 0xe8, 0x00, + 0xcd, 0x04, 0x83, 0x4b, 0x87, 0x8d, 0x19, 0x9a, 0x5f, 0x9f, 0xcd, 0x1d, 0xf8, 0xf5, 0x66, 0xad, 0x97, 0x07, 0x07, + 0x5f, 0x58, 0x05, 0x28, 0x37, 0x4c, 0x33, 0x8c, 0x80, 0x78, 0x59, 0x2e, 0xc7, 0xdd, 0x0c, 0xdf, 0x8b, 0x2b, 0x95, + 0x81, 0x27, 0x1c, 0x21, 0x11, 0x7a, 0x4e, 0xf4, 0x7a, 0xb2, 0x49, 0xef, 0x9d, 0x36, 0x43, 0x84, 0x62, 0x0d, 0x90, + 0x7b, 0x90, 0xcb, 0xad, 0x92, 0x49, 0x55, 0xb6, 0xb6, 0xe5, 0x20, 0x1e, 0x03, 0xb8, 0x62, 0x23, 0xa4, 0x04, 0x68, + 0xb8, 0x5b, 0x68, 0x79, 0x2e, 0x81, 0xfd, 0xc7, 0x2a, 0x01, 0x91, 0x16, 0xd5, 0x36, 0x2e, 0x42, 0xd8, 0x9a, 0xfa, + 0x04, 0xc6, 0x09, 0x0f, 0x9f, 0xef, 0xd2, 0x50, 0x7b, 0xd4, 0x66, 0xe6, 0x0c, 0x82, 0x12, 0x12, 0x95, 0x15, 0x92, + 0x2f, 0xb1, 0x70, 0xdc, 0x9c, 0xbf, 0x87, 0x03, 0x52, 0xac, 0x68, 0x6c, 0xef, 0xb6, 0xe0, 0xf8, 0x28, 0x92, 0x45, + 0x5c, 0xeb, 0xba, 0x5b, 0x98, 0x6a, 0xd8, 0x81, 0x8e, 0x86, 0x70, 0x2a, 0xcc, 0x3d, 0xe1, 0xe3, 0x8a, 0xa4, 0xfe, + 0x6c, 0x4d, 0xb4, 0xb5, 0x27, 0x86, 0x95, 0x69, 0x4a, 0x30, 0xff, 0x9f, 0xad, 0xd5, 0x75, 0x59, 0x08, 0x33, 0x33, + 0x8c, 0x1b, 0xbb, 0x0a, 0x6c, 0x0d, 0x38, 0xb6, 0xfc, 0x5b, 0x06, 0x8b, 0xea, 0x95, 0xe2, 0xa6, 0xd3, 0x80, 0x09, + 0x78, 0x0b, 0xd6, 0x33, 0x9b, 0x5b, 0xff, 0xb9, 0x39, 0x18, 0x05, 0x56, 0x35, 0x02, 0x2f, 0x0d, 0x81, 0x47, 0xc0, + 0xb8, 0x79, 0xd3, 0xf2, 0x9e, 0x33, 0xa2, 0x11, 0xfe, 0xc4, 0x73, 0x78, 0x66, 0x59, 0xee, 0xad, 0x8f, 0x8d, 0x15, + 0x49, 0x05, 0x01, 0xdb, 0x22, 0xec, 0x88, 0xbc, 0x44, 0x58, 0x35, 0x1a, 0x5d, 0x75, 0xc1, 0x2a, 0xad, 0x4a, 0x35, + 0x4c, 0x01, 0xb7, 0xc4, 0x80, 0xf7, 0xb5, 0x13, 0x15, 0x0c, 0x09, 0xbc, 0xf5, 0xb7, 0x02, 0xf5, 0xfd, 0xc3, 0xb7, + 0x71, 0x48, 0xdf, 0xc2, 0xb2, 0xe5, 0x45, 0x2c, 0x4c, 0x29, 0xae, 0xee, 0x70, 0xde, 0x7c, 0xdf, 0x6c, 0x04, 0xc6, + 0xbd, 0xdf, 0xc6, 0x60, 0xe3, 0x86, 0xba, 0xda, 0x92, 0x86, 0x72, 0x13, 0x76, 0x51, 0x65, 0xef, 0x18, 0x76, 0xd6, + 0xd5, 0x95, 0xb4, 0xab, 0x89, 0x5a, 0xaf, 0x15, 0xab, 0x8c, 0x06, 0x36, 0x0c, 0x3b, 0xcd, 0x31, 0xb3, 0xad, 0xc0, + 0x7f, 0x3c, 0x27, 0x1a, 0x07, 0xc8, 0xfa, 0xe6, 0x5b, 0xd7, 0x29, 0xd5, 0x30, 0x61, 0x7b, 0xbb, 0xf3, 0xf1, 0x31, + 0xdf, 0x75, 0x3e, 0x62, 0xe9, 0xb6, 0xbe, 0x39, 0x1b, 0xdb, 0xff, 0xc6, 0xd9, 0xe8, 0xd4, 0xf6, 0xfe, 0x78, 0x04, + 0xee, 0xa4, 0x76, 0x3c, 0xd6, 0xd7, 0x94, 0x48, 0x2c, 0xdc, 0x72, 0x5c, 0x76, 0x56, 0x2b, 0xd1, 0x6f, 0x81, 0xda, + 0x29, 0x8a, 0xe0, 0x67, 0xdb, 0xfe, 0x0c, 0x48, 0xb2, 0xd5, 0x21, 0xc7, 0xa2, 0x14, 0x65, 0x50, 0x02, 0x06, 0xd4, + 0xb1, 0xb1, 0xf5, 0x32, 0x88, 0xed, 0x70, 0xc8, 0x61, 0x39, 0x11, 0xe5, 0xd5, 0x15, 0x8c, 0xd8, 0x1c, 0x1b, 0x4e, + 0xc0, 0x8c, 0x77, 0x5a, 0x15, 0x7a, 0xf1, 0xf3, 0x5f, 0x33, 0xa7, 0xb5, 0x23, 0xc6, 0x72, 0x12, 0x35, 0x2b, 0x06, + 0x37, 0x02, 0xc7, 0x30, 0xee, 0x1b, 0x09, 0xb5, 0x3a, 0xd5, 0x51, 0xed, 0x48, 0xc2, 0x2d, 0x50, 0xbb, 0xed, 0x9b, + 0x73, 0x69, 0xb5, 0xda, 0x79, 0xb0, 0xe0, 0x22, 0xc0, 0xed, 0xe7, 0x44, 0xd7, 0x48, 0x0a, 0x25, 0x4e, 0x82, 0xc2, + 0xb9, 0x41, 0x55, 0x4d, 0x64, 0xbf, 0x35, 0x00, 0x9e, 0xb4, 0x9b, 0x5d, 0xc8, 0x4a, 0x48, 0xce, 0x1a, 0x0d, 0x94, + 0x97, 0x1d, 0xd3, 0xbe, 0x68, 0x64, 0x03, 0xcc, 0x70, 0x66, 0x05, 0x16, 0x38, 0xbd, 0xe2, 0xbc, 0xea, 0xba, 0x9f, + 0x0d, 0x10, 0x2e, 0x56, 0xab, 0xd8, 0x0e, 0x2d, 0x47, 0xab, 0x55, 0x1e, 0x0e, 0xcd, 0xe4, 0x43, 0xc5, 0x97, 0x3d, + 0x4d, 0x5e, 0x9a, 0xf3, 0xf0, 0x25, 0x0c, 0xb2, 0x41, 0xe2, 0xdc, 0xa9, 0x04, 0x73, 0xd0, 0x5c, 0x35, 0x64, 0x3f, + 0x6b, 0xb4, 0x07, 0x01, 0x0d, 0xeb, 0x67, 0x03, 0x92, 0xaf, 0xc1, 0x72, 0x56, 0xb9, 0x03, 0xf3, 0x6f, 0x38, 0xd8, + 0xfe, 0x36, 0xe7, 0x8c, 0x6d, 0x30, 0x5c, 0x93, 0x4d, 0x95, 0x41, 0x89, 0x57, 0x6e, 0x71, 0x7d, 0xb9, 0x9a, 0x81, + 0x45, 0x59, 0x08, 0xbb, 0x6b, 0xe6, 0x1e, 0x08, 0xff, 0x25, 0xb6, 0x4b, 0x5a, 0x1a, 0x71, 0x6f, 0x20, 0xbe, 0xb7, + 0xdd, 0x4e, 0x92, 0x84, 0x16, 0x13, 0x73, 0x25, 0xe2, 0x6f, 0x78, 0xcd, 0x1e, 0x38, 0x76, 0xe3, 0x0c, 0x7a, 0xee, + 0x97, 0x9d, 0x0d, 0x88, 0x1d, 0xbf, 0x67, 0x76, 0xbc, 0xe3, 0x4a, 0x41, 0x77, 0xeb, 0x22, 0xec, 0x60, 0xe8, 0xff, + 0xf2, 0x60, 0x4e, 0xdc, 0x60, 0x2c, 0x9a, 0x6c, 0xc0, 0xed, 0x1b, 0xf0, 0x28, 0xe8, 0x06, 0xdc, 0xbe, 0x0d, 0x5f, + 0x0f, 0xad, 0xec, 0x9b, 0x03, 0x0c, 0xc8, 0x84, 0x1d, 0x69, 0x95, 0x10, 0x0c, 0xf3, 0x74, 0x93, 0x23, 0xb3, 0x64, + 0x15, 0x0e, 0x57, 0x4d, 0x62, 0xb1, 0xb1, 0x17, 0x2a, 0x26, 0x35, 0x10, 0x8c, 0x45, 0xfa, 0x12, 0x85, 0x4a, 0x83, + 0xba, 0x71, 0x0c, 0x60, 0x95, 0xd3, 0xd6, 0xbf, 0x3c, 0x38, 0x00, 0xa1, 0x01, 0x58, 0xbb, 0x24, 0xa3, 0x73, 0xbd, + 0x28, 0x80, 0xbf, 0x52, 0xfe, 0x37, 0x24, 0x83, 0xdb, 0x89, 0x49, 0x83, 0x1f, 0x90, 0x30, 0xa7, 0x4a, 0xf1, 0x2f, + 0x36, 0xcd, 0xfd, 0xc6, 0x05, 0xf1, 0x18, 0xad, 0x2c, 0xa7, 0x28, 0x51, 0x57, 0x3a, 0x74, 0xad, 0x43, 0xee, 0xe9, + 0x17, 0x26, 0xf4, 0x4b, 0xae, 0x34, 0x13, 0x00, 0x80, 0x0a, 0xf1, 0x60, 0x4a, 0x0a, 0xc1, 0xd6, 0xad, 0xd5, 0xa2, + 0xa3, 0xd1, 0x77, 0xab, 0xe8, 0x3a, 0x5b, 0x34, 0xa5, 0x62, 0x94, 0xdb, 0x4e, 0x42, 0x9b, 0x49, 0x6f, 0x27, 0x5a, + 0x96, 0x0c, 0x2d, 0x76, 0x2a, 0xf6, 0xc3, 0xd0, 0xfa, 0x58, 0x10, 0x7f, 0x2e, 0xf8, 0xb3, 0xf4, 0xbb, 0x7c, 0x0c, + 0x5c, 0xa9, 0x7f, 0x63, 0x15, 0xc2, 0x99, 0x60, 0x1d, 0x90, 0xd7, 0xa4, 0x3e, 0x4e, 0x8f, 0x3a, 0xf9, 0x96, 0x72, + 0xa1, 0x34, 0x0a, 0xdb, 0x38, 0x29, 0x0c, 0xa6, 0x9c, 0x7d, 0x5b, 0xe2, 0xfa, 0xd5, 0x1f, 0x23, 0xfe, 0xe8, 0x10, + 0xff, 0x2e, 0x95, 0x46, 0xcb, 0x12, 0xc1, 0x90, 0xdf, 0x91, 0x5a, 0xc1, 0x55, 0x6c, 0xce, 0xf5, 0x73, 0x3d, 0xcb, + 0x37, 0x3c, 0x71, 0xba, 0x5a, 0x95, 0x52, 0x81, 0x8a, 0x6f, 0x18, 0x7e, 0xc2, 0xe0, 0xde, 0xf8, 0x19, 0x0f, 0xaa, + 0x6c, 0xdf, 0x17, 0x3f, 0x0b, 0xee, 0x8b, 0x9f, 0xf1, 0x74, 0xbb, 0x68, 0x70, 0x4f, 0xdc, 0x49, 0xce, 0x93, 0x56, + 0xe4, 0xf9, 0xa8, 0x29, 0xad, 0xfc, 0x2b, 0xed, 0xd6, 0xc0, 0x95, 0x4d, 0x1c, 0x18, 0xe7, 0xd5, 0x45, 0x28, 0xe6, + 0xcc, 0x19, 0x2d, 0x87, 0xff, 0xad, 0x75, 0x72, 0x27, 0x8f, 0xb4, 0x52, 0xc8, 0x1b, 0x5a, 0xe8, 0x7b, 0xb0, 0xe1, + 0x8a, 0x2d, 0x1f, 0x40, 0x4a, 0x40, 0xd9, 0xf6, 0xef, 0x75, 0x11, 0x88, 0xe3, 0xca, 0x3a, 0x1f, 0x85, 0xed, 0x93, + 0xa2, 0xe4, 0xea, 0xea, 0x42, 0xc8, 0xad, 0xd1, 0x12, 0x20, 0x4c, 0xbd, 0x6b, 0x1e, 0x73, 0x34, 0x99, 0xa5, 0xcb, + 0x75, 0xa9, 0x3a, 0x28, 0x2c, 0x57, 0xc7, 0x11, 0x2e, 0xd6, 0xe6, 0x06, 0xfd, 0x15, 0xc7, 0x7f, 0x73, 0x47, 0x23, + 0x7f, 0x2e, 0x29, 0xd0, 0xa3, 0xdd, 0xbe, 0x36, 0x3b, 0x48, 0xa4, 0x9d, 0x43, 0x69, 0x29, 0x00, 0x58, 0x6d, 0xf0, + 0x75, 0xed, 0x71, 0xea, 0x89, 0x74, 0xb3, 0xf9, 0xa6, 0x21, 0x2c, 0x66, 0xa5, 0x05, 0x8f, 0xe9, 0x66, 0x87, 0xe5, + 0xa8, 0x97, 0xc5, 0x75, 0xb9, 0xc7, 0x6a, 0xfd, 0xa2, 0x6f, 0x80, 0xb2, 0x32, 0x44, 0x5b, 0xad, 0xe2, 0x3a, 0xbc, + 0x89, 0x08, 0xae, 0x41, 0x10, 0x16, 0x81, 0x01, 0x47, 0x8d, 0xf1, 0xb6, 0x75, 0x62, 0xb4, 0x69, 0xbf, 0xe4, 0x59, + 0xf7, 0xda, 0x38, 0x42, 0x45, 0x83, 0xad, 0x1e, 0x6a, 0x1e, 0xb0, 0x9d, 0x5d, 0xd9, 0x51, 0x00, 0xa1, 0x29, 0xf5, + 0xc6, 0xb9, 0x95, 0x15, 0xed, 0x0e, 0xf8, 0xa2, 0xef, 0x98, 0xe7, 0x3a, 0xd0, 0x6d, 0xe7, 0x07, 0xb6, 0x4d, 0x4f, + 0xe4, 0xb7, 0x6c, 0x9b, 0x6a, 0x9c, 0xf0, 0x7e, 0x0b, 0x7d, 0xdf, 0x10, 0xd6, 0xf6, 0xb5, 0xbb, 0xc8, 0xff, 0x42, + 0x77, 0x6d, 0x40, 0x4f, 0x0b, 0x66, 0x4f, 0x63, 0x3e, 0xe8, 0xf5, 0xfa, 0xe7, 0xd2, 0x7f, 0xc1, 0xd8, 0x0a, 0xfd, + 0x6c, 0x77, 0x81, 0x13, 0x2b, 0x8d, 0x43, 0x70, 0xfc, 0x8a, 0x93, 0x49, 0x2e, 0x87, 0x34, 0x7f, 0x07, 0x3d, 0x56, + 0xb9, 0xcf, 0xef, 0x46, 0x05, 0xd5, 0xcc, 0xd1, 0x9a, 0x6a, 0x14, 0xaf, 0x78, 0x30, 0x8c, 0x57, 0xdc, 0x52, 0xee, + 0xaa, 0x05, 0xbc, 0x7c, 0x59, 0x36, 0x91, 0xfe, 0xbc, 0x2e, 0x65, 0x30, 0xb5, 0xbb, 0x97, 0x4d, 0x92, 0xc6, 0x4a, + 0x92, 0xc6, 0x54, 0xbc, 0xd9, 0x54, 0x1c, 0xff, 0xfd, 0x8d, 0xc1, 0x6e, 0x93, 0xb9, 0xbf, 0x03, 0x32, 0xf7, 0x37, + 0x4f, 0xbf, 0x5b, 0x2b, 0xa0, 0x78, 0xc7, 0xc9, 0xb1, 0xb1, 0x8c, 0xb1, 0xa3, 0x7e, 0xab, 0xc1, 0xa0, 0x41, 0x93, + 0xcb, 0xc0, 0xdb, 0xa1, 0x3a, 0xbd, 0xbc, 0xfd, 0x51, 0x9c, 0x2d, 0x94, 0x96, 0x33, 0xd7, 0xa8, 0x72, 0x3e, 0x4e, + 0x26, 0x13, 0x14, 0xd8, 0xe6, 0x0e, 0x3f, 0xad, 0xbb, 0x91, 0x2d, 0x3f, 0x73, 0x31, 0x4a, 0x15, 0x76, 0x67, 0x8b, + 0x4a, 0xe5, 0x9a, 0x78, 0x33, 0xe7, 0xed, 0x3c, 0x3c, 0xe6, 0x82, 0xab, 0x29, 0x2b, 0xe2, 0x02, 0x2d, 0xbf, 0xd5, + 0x59, 0x01, 0xb7, 0x39, 0xb6, 0x33, 0x3c, 0x2a, 0x2d, 0x07, 0x74, 0x02, 0xad, 0x81, 0xce, 0x68, 0xc6, 0xf4, 0x54, + 0x8e, 0xc0, 0xf0, 0x25, 0x19, 0x95, 0xee, 0x54, 0x07, 0x07, 0xfb, 0x71, 0x64, 0xf4, 0x17, 0xe0, 0x83, 0x1e, 0xe6, + 0xa0, 0xde, 0x12, 0x1c, 0x83, 0xaa, 0xae, 0x19, 0x5a, 0xb2, 0x4d, 0x1f, 0x1a, 0x9d, 0x7c, 0x66, 0x77, 0x98, 0xa3, + 0xf5, 0x3a, 0xb5, 0xa3, 0x8e, 0xc6, 0x9c, 0xe5, 0xa3, 0x08, 0x7f, 0x66, 0x77, 0x69, 0xe9, 0xb6, 0x6e, 0xbc, 0xac, + 0xcd, 0x22, 0x46, 0xf2, 0x46, 0x44, 0xb8, 0xea, 0x24, 0x5d, 0xae, 0xb1, 0x2c, 0xf8, 0x04, 0x70, 0xf4, 0x17, 0x76, + 0x97, 0xba, 0xf6, 0x02, 0x57, 0x41, 0xb4, 0xf4, 0xa0, 0x4f, 0x82, 0xe4, 0x70, 0x19, 0x9c, 0xc0, 0xd1, 0x37, 0x75, + 0x07, 0xa4, 0x56, 0xae, 0x12, 0x21, 0x11, 0x5a, 0xff, 0xbb, 0x53, 0xc1, 0x8b, 0xf0, 0x9c, 0xd3, 0x35, 0x8b, 0xdb, + 0x8d, 0x4a, 0x0c, 0x2a, 0x54, 0x16, 0x24, 0x1f, 0x63, 0xee, 0x77, 0x9f, 0xf3, 0x7e, 0x08, 0x74, 0x66, 0x0b, 0xea, + 0x1a, 0x4d, 0x47, 0xe6, 0x17, 0xaa, 0xee, 0xa0, 0x66, 0xba, 0xaa, 0xb8, 0xf7, 0x31, 0x06, 0xc0, 0x83, 0xb5, 0x0c, + 0x35, 0x0e, 0xa1, 0x6b, 0x6f, 0xa6, 0x3a, 0xa6, 0x24, 0x5e, 0xfa, 0x39, 0xa4, 0x3c, 0x04, 0xa3, 0x5e, 0x03, 0x1a, + 0x3a, 0x04, 0xb3, 0x96, 0x87, 0x7c, 0x1c, 0x8b, 0xad, 0x33, 0x54, 0x9a, 0x33, 0x34, 0x09, 0x40, 0xfe, 0x8d, 0x33, + 0x93, 0x19, 0x68, 0x18, 0xde, 0xd2, 0x1c, 0x80, 0x6e, 0x75, 0x1d, 0x0e, 0x85, 0x2b, 0x5a, 0x3a, 0xef, 0xd9, 0x45, + 0x97, 0xb5, 0x61, 0xc5, 0xa6, 0x1d, 0xb4, 0x4e, 0x61, 0x4a, 0xcc, 0x16, 0x58, 0x7b, 0xbd, 0x0f, 0xf7, 0x76, 0xb5, + 0x71, 0x91, 0xf8, 0x69, 0x11, 0x0f, 0x93, 0x98, 0xa2, 0x25, 0x8f, 0x29, 0x96, 0x60, 0x07, 0x59, 0xac, 0xcb, 0xf1, + 0xb3, 0x70, 0x39, 0x6a, 0x56, 0xd2, 0xbb, 0x1d, 0x0c, 0x81, 0xcb, 0xd7, 0x60, 0x1b, 0x8a, 0xb9, 0x27, 0x2c, 0x3c, + 0x36, 0x9e, 0x7e, 0xc1, 0xba, 0xcd, 0xed, 0x82, 0xf8, 0x15, 0x18, 0xd3, 0x78, 0x19, 0xcc, 0x22, 0x74, 0x2a, 0x77, + 0x0e, 0x87, 0xee, 0x9a, 0xb0, 0x32, 0x5e, 0x8d, 0x15, 0xd9, 0x38, 0x7a, 0xbe, 0x6f, 0xe3, 0xf9, 0xcf, 0x82, 0x15, + 0x77, 0x57, 0x0c, 0x6c, 0xac, 0x25, 0xb8, 0x1b, 0x57, 0xcb, 0x50, 0x19, 0xc8, 0xf7, 0xa4, 0x61, 0x5d, 0xd6, 0xf8, + 0xbb, 0x51, 0x31, 0xd6, 0xe6, 0x9e, 0x32, 0xd0, 0xd6, 0xd8, 0xed, 0xc2, 0xbe, 0xe9, 0xba, 0xc9, 0xba, 0x46, 0x11, + 0x57, 0x41, 0xda, 0xdd, 0x2d, 0xe0, 0x22, 0xf4, 0x87, 0xed, 0xab, 0xc1, 0xa6, 0xea, 0x06, 0x92, 0xe0, 0xda, 0x4f, + 0x7e, 0x7b, 0xaa, 0xbb, 0xac, 0x75, 0xbf, 0x3d, 0xd5, 0xda, 0x65, 0xa1, 0x31, 0x24, 0xc2, 0xae, 0x9f, 0xd2, 0x7f, + 0x5a, 0xac, 0xd7, 0x68, 0x0d, 0xc3, 0x7b, 0xcf, 0xbb, 0x71, 0xfc, 0xde, 0x5b, 0x28, 0x26, 0x70, 0x91, 0x7b, 0x95, + 0x4b, 0x4f, 0xc8, 0xab, 0x11, 0xbc, 0xe7, 0x5b, 0x43, 0x78, 0xcf, 0x03, 0xa7, 0x57, 0x90, 0x9a, 0x26, 0x82, 0x8d, + 0x3c, 0xfd, 0x44, 0x16, 0x09, 0x0d, 0x1f, 0xf7, 0x9a, 0x13, 0xa1, 0x3f, 0xa5, 0xc0, 0x7f, 0xe1, 0xe1, 0x42, 0x6b, + 0x29, 0x30, 0x17, 0xf3, 0x85, 0xc6, 0xca, 0x8c, 0x7e, 0x39, 0x96, 0x42, 0x37, 0xc7, 0x74, 0xc6, 0xf3, 0xbb, 0x74, + 0xc1, 0x9b, 0x33, 0x29, 0xa4, 0x9a, 0xd3, 0x8c, 0x61, 0x75, 0xa7, 0x34, 0x9b, 0x35, 0x17, 0x1c, 0x3f, 0x67, 0xf9, + 0x17, 0xa6, 0x79, 0x46, 0xf1, 0x5b, 0x39, 0x94, 0x5a, 0xe2, 0xd7, 0xb7, 0x77, 0x13, 0x26, 0xf0, 0xef, 0xc3, 0x85, + 0xd0, 0x0b, 0xac, 0xa8, 0x50, 0x4d, 0xc5, 0x0a, 0x3e, 0xee, 0x36, 0x9b, 0xf3, 0x82, 0xcf, 0x68, 0x71, 0xd7, 0xcc, + 0x64, 0x2e, 0x8b, 0xf4, 0xbf, 0x5a, 0xc7, 0xf4, 0xc1, 0xf8, 0xa4, 0xab, 0x0b, 0x2a, 0x14, 0x87, 0x85, 0x49, 0x69, + 0x9e, 0xef, 0x1d, 0x9f, 0xb6, 0x66, 0x6a, 0xdf, 0x5e, 0xf8, 0x51, 0xa1, 0xd7, 0x9f, 0xf0, 0x07, 0x09, 0xa3, 0x4c, + 0x86, 0x5a, 0xb8, 0x41, 0x2e, 0xb3, 0x45, 0xa1, 0x64, 0x91, 0xce, 0x25, 0x17, 0x9a, 0x15, 0xdd, 0xa1, 0x2c, 0x46, + 0xac, 0x68, 0x16, 0x74, 0xc4, 0x17, 0x2a, 0x3d, 0x99, 0xdf, 0x76, 0xeb, 0x3d, 0xd8, 0xfc, 0x54, 0x48, 0xc1, 0xba, + 0xc0, 0x6f, 0x4c, 0x0a, 0xb9, 0x10, 0x23, 0x37, 0x8c, 0x85, 0x50, 0x4c, 0x77, 0xe7, 0x74, 0x04, 0x76, 0xc0, 0xe9, + 0xf9, 0xfc, 0xb6, 0x6b, 0x66, 0x7d, 0xc3, 0xf8, 0x64, 0xaa, 0xd3, 0xd3, 0x56, 0xcb, 0x7e, 0x2b, 0xfe, 0x95, 0xa5, + 0xed, 0x4e, 0xd2, 0x39, 0x9d, 0xdf, 0x02, 0x07, 0xaf, 0x59, 0xd1, 0x04, 0x58, 0x40, 0xa5, 0x76, 0xd2, 0x7a, 0x70, + 0x7c, 0x1f, 0x32, 0xc0, 0xc6, 0xa1, 0x69, 0x26, 0x04, 0xc6, 0xee, 0xe9, 0x62, 0x3e, 0x67, 0x05, 0x78, 0xd1, 0x77, + 0x67, 0xb4, 0x98, 0x70, 0xd1, 0x2c, 0x4c, 0xa3, 0xcd, 0xf3, 0xf9, 0xed, 0x1a, 0xe6, 0x93, 0x5a, 0xb3, 0x55, 0x37, + 0x2d, 0xf7, 0xb5, 0x0c, 0x86, 0x68, 0x62, 0xd2, 0xa4, 0xc5, 0x64, 0x48, 0xe3, 0x76, 0xe7, 0x3e, 0xf6, 0xff, 0x4b, + 0x3a, 0x28, 0x00, 0x5b, 0x73, 0xb4, 0x28, 0xcc, 0x2d, 0x6a, 0xda, 0x56, 0xb6, 0xd9, 0xa9, 0xfc, 0xc2, 0x0a, 0xdf, + 0xaa, 0xf9, 0x58, 0x6e, 0xcd, 0xfb, 0x3f, 0x69, 0xf4, 0x13, 0x9e, 0x50, 0x58, 0x03, 0x83, 0x1c, 0x7d, 0x23, 0x0f, + 0xc2, 0x4c, 0x07, 0xcb, 0x1b, 0x3e, 0xd2, 0xd3, 0xb4, 0xdd, 0x6a, 0xfd, 0x50, 0xad, 0x58, 0x77, 0x6a, 0x41, 0xd7, + 0x2e, 0xd8, 0xac, 0xb6, 0x8e, 0x33, 0x5a, 0x62, 0xdb, 0x72, 0x2e, 0xdd, 0x92, 0x17, 0x2c, 0x37, 0xd1, 0x64, 0xd6, + 0x0e, 0xe5, 0xb6, 0xc6, 0xc9, 0xc5, 0x94, 0x15, 0x5c, 0x77, 0xeb, 0x5f, 0x55, 0xc7, 0xdb, 0xab, 0xbf, 0xb6, 0x72, + 0xe8, 0xd2, 0xd6, 0x70, 0x97, 0x9e, 0x8f, 0xe1, 0x63, 0x7b, 0xf5, 0xbf, 0xd0, 0x22, 0xde, 0x40, 0x4c, 0x1c, 0xd6, + 0x40, 0xeb, 0x60, 0xce, 0x05, 0x98, 0x64, 0x0e, 0xf0, 0x37, 0xa0, 0x90, 0xd1, 0x3c, 0x8b, 0x61, 0x44, 0x7b, 0xcd, + 0xbd, 0xe3, 0x82, 0xcd, 0x90, 0x07, 0x44, 0x72, 0xff, 0xb4, 0x60, 0xb3, 0x75, 0x62, 0xaa, 0x2f, 0x0d, 0x8a, 0xd0, + 0x9c, 0x4f, 0x44, 0x9a, 0x31, 0x40, 0xdf, 0x75, 0xc2, 0x84, 0xe6, 0xfa, 0xae, 0x59, 0xc8, 0x9b, 0xe5, 0x88, 0xab, + 0x79, 0x4e, 0xef, 0xd2, 0x71, 0xce, 0x6e, 0xbb, 0xa6, 0x54, 0x93, 0x6b, 0x36, 0x53, 0xae, 0x6c, 0x17, 0xd2, 0x9b, + 0x23, 0x6b, 0x36, 0x01, 0xd0, 0x93, 0x37, 0x9b, 0xfb, 0x27, 0x39, 0x56, 0x7b, 0x8c, 0x2a, 0xd6, 0x94, 0x0b, 0xbd, + 0xd7, 0x52, 0xdd, 0x19, 0x17, 0x4d, 0x37, 0x90, 0x93, 0xd6, 0xfc, 0xb6, 0xbb, 0x0d, 0xf9, 0xa0, 0xff, 0x84, 0xdd, + 0xce, 0xa9, 0x18, 0xb1, 0xd1, 0x32, 0xa8, 0xd6, 0x81, 0x7a, 0x61, 0xa9, 0x54, 0xe8, 0x69, 0xd3, 0xd8, 0x7a, 0xc5, + 0x1d, 0x81, 0xbe, 0x81, 0x5a, 0x0f, 0x5a, 0xd8, 0xfe, 0x7f, 0xd2, 0x46, 0x61, 0xe5, 0x3d, 0x08, 0xbb, 0xc4, 0xc7, + 0x77, 0x4d, 0xf8, 0xbb, 0x04, 0xdf, 0x22, 0x9e, 0xd1, 0xdc, 0x41, 0x64, 0xc6, 0x47, 0xa3, 0xbc, 0x36, 0xa2, 0xcb, + 0xa0, 0xb3, 0x36, 0x5a, 0xc2, 0xfc, 0xd3, 0xd6, 0x5e, 0x6b, 0xcf, 0xcc, 0xc5, 0x6d, 0xf3, 0x93, 0x93, 0xfb, 0xc7, + 0x0f, 0x58, 0x37, 0xe7, 0x82, 0xd5, 0xa6, 0xfa, 0x5d, 0x50, 0x87, 0x0d, 0x77, 0x5c, 0xc3, 0xed, 0xbd, 0xf6, 0xde, + 0x49, 0xeb, 0x07, 0xbf, 0x5b, 0x73, 0x36, 0xd6, 0x69, 0xfb, 0x6c, 0x7e, 0x5b, 0xdf, 0xbe, 0xe7, 0xbe, 0xe9, 0x9b, + 0x82, 0xce, 0x53, 0x21, 0xe1, 0x4f, 0x17, 0x36, 0xd9, 0x38, 0x97, 0x37, 0xe9, 0x94, 0x8f, 0x46, 0x4c, 0xd8, 0x02, + 0x65, 0x22, 0xcb, 0x73, 0x3e, 0x57, 0xdc, 0xae, 0x86, 0xc3, 0xdd, 0xd3, 0x0d, 0xa8, 0x86, 0x03, 0x3a, 0x0e, 0x06, + 0x74, 0x5a, 0x0d, 0xa8, 0xea, 0x3f, 0x1c, 0x61, 0x67, 0x63, 0xae, 0xa6, 0x54, 0xb7, 0x86, 0x49, 0x7f, 0x2f, 0x94, + 0x06, 0x98, 0x7b, 0x23, 0x0d, 0x43, 0xc5, 0x9b, 0x43, 0xa6, 0x6f, 0x18, 0x13, 0xdf, 0x1e, 0xc4, 0x65, 0x2a, 0x45, + 0x7e, 0x67, 0x3f, 0x97, 0x61, 0x97, 0x74, 0xa1, 0xe5, 0x3a, 0x19, 0x72, 0x41, 0x8b, 0xbb, 0x6b, 0xc5, 0x84, 0x92, + 0xc5, 0xb5, 0x1c, 0x8f, 0x97, 0xdf, 0x22, 0x2d, 0xf7, 0xd1, 0x3a, 0x51, 0x5c, 0x4c, 0x72, 0x66, 0x89, 0x92, 0x41, + 0x04, 0x47, 0xcc, 0x6d, 0xbb, 0xa6, 0xc9, 0xda, 0xa0, 0xd7, 0x49, 0x96, 0xf3, 0x19, 0xd5, 0xcc, 0xc0, 0x39, 0x20, + 0x35, 0x6e, 0xf2, 0x69, 0xbb, 0x35, 0xbf, 0xdd, 0x6b, 0xed, 0xd9, 0x3f, 0x55, 0x69, 0xd8, 0x46, 0x41, 0x61, 0xdf, + 0x24, 0x17, 0x06, 0x3f, 0x0c, 0x38, 0xcc, 0x2e, 0x32, 0xab, 0x67, 0xd6, 0x2e, 0x80, 0x1d, 0xcc, 0xae, 0xd6, 0xd4, + 0xa5, 0xa3, 0x4b, 0xb6, 0xc5, 0xd3, 0xd6, 0x0f, 0xf5, 0xdc, 0x9c, 0x0e, 0x59, 0xbe, 0xb4, 0x1b, 0xd5, 0x03, 0xd7, + 0x6d, 0xd5, 0x70, 0x99, 0x03, 0x92, 0x61, 0x40, 0x34, 0x48, 0xd3, 0xe6, 0x0d, 0x1b, 0x7e, 0xe6, 0xda, 0x6e, 0x99, + 0xa6, 0xba, 0x01, 0xe7, 0x1d, 0x33, 0xa6, 0x39, 0x2b, 0x96, 0xfe, 0x38, 0x6a, 0xd5, 0x08, 0xe8, 0x95, 0x30, 0x07, + 0xa1, 0xa6, 0xc3, 0x26, 0x84, 0x32, 0x63, 0xc5, 0x72, 0xd7, 0xe4, 0x66, 0xf4, 0xd6, 0xa1, 0xd8, 0x83, 0xd6, 0x0f, + 0xb5, 0xc3, 0xec, 0xa4, 0xd5, 0xf2, 0x07, 0x5d, 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, 0xaa, 0x81, 0xb8, 0x39, 0xae, 0xd7, 0x66, 0xb2, 0x00, 0x58, 0xca, 0x05, 0x9c, 0x62, 0xf6, 0x24, 0x87, + 0x3e, 0x94, 0x04, 0xb3, 0xf3, 0x9d, 0x8d, 0xd6, 0x87, 0xd5, 0xda, 0xab, 0x06, 0x06, 0xff, 0xac, 0x3f, 0x55, 0x7c, + 0xf0, 0x0b, 0x16, 0xc8, 0x21, 0xbc, 0x91, 0x9c, 0xae, 0x5a, 0x4e, 0xf6, 0x18, 0xe9, 0x4a, 0x24, 0x32, 0x9e, 0x1b, + 0x33, 0x7a, 0x6b, 0x5d, 0x38, 0x66, 0x5c, 0x80, 0x81, 0x18, 0xc2, 0x3a, 0x30, 0xa5, 0x9f, 0x86, 0x0d, 0x8d, 0x74, + 0x0c, 0x0d, 0x1f, 0x76, 0x92, 0xd3, 0x53, 0x84, 0x5b, 0xb8, 0x73, 0x7a, 0x1a, 0xc8, 0x3e, 0x63, 0xbd, 0xab, 0xe8, + 0xae, 0x92, 0x72, 0x47, 0xc9, 0x23, 0xd3, 0xe8, 0x51, 0xbb, 0xd5, 0xc2, 0xc6, 0x4d, 0xbd, 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, 0x68, 0x8b, 0xa8, 0xd5, 0xee, 0x1c, + 0x9f, 0x9c, 0x9e, 0xdd, 0x3f, 0x7f, 0xf0, 0xf0, 0xd1, 0xe3, 0x27, 0x4f, 0x9f, 0x45, 0x03, 0x3c, 0x34, 0x1e, 0x26, + 0x4a, 0xf4, 0xf9, 0x41, 0xfb, 0x74, 0x80, 0xaf, 0xfd, 0x67, 0xcc, 0x0f, 0x3a, 0x27, 0x2d, 0x74, 0x79, 0x79, 0x32, + 0x68, 0x94, 0xb9, 0xef, 0x8d, 0x63, 0x4b, 0x95, 0x45, 0x08, 0x89, 0x21, 0x07, 0xe1, 0x3b, 0x53, 0xef, 0x3d, 0x8b, + 0x79, 0x52, 0xa0, 0x83, 0x03, 0xf3, 0x63, 0xe2, 0x7f, 0x0c, 0xfd, 0x0f, 0x1a, 0x2c, 0xd2, 0x2d, 0x8d, 0x9d, 0x67, + 0xb3, 0x2e, 0xfd, 0x0a, 0x4a, 0x93, 0x9d, 0x3d, 0xee, 0x8c, 0xe7, 0xff, 0x2b, 0xb2, 0xc6, 0x31, 0xe4, 0xc4, 0x2a, + 0xa0, 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, 0xd5, 0x96, 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, 0x3e, 0xfa, 0x4a, 0xef, 0x7d, + 0x29, 0xf5, 0x24, 0xfc, 0xa2, 0x73, 0x7a, 0xda, 0x03, 0x0c, 0x33, 0xb6, 0xb7, 0x1e, 0x46, 0x37, 0x01, 0x8c, 0xee, + 0xe0, 0x77, 0x6f, 0x48, 0xd3, 0x6b, 0x5a, 0x02, 0xa9, 0x17, 0xfd, 0x57, 0xd4, 0xd0, 0x06, 0xe6, 0xe6, 0xcf, 0xc4, + 0xfe, 0x19, 0xa2, 0xc6, 0x17, 0x0a, 0xe0, 0x06, 0xcd, 0x8f, 0x57, 0xdd, 0x9a, 0x1e, 0x3f, 0x53, 0x70, 0xa5, 0x99, + 0xaa, 0x9c, 0xf6, 0x56, 0xd3, 0x9b, 0xe1, 0x6a, 0xaa, 0xbe, 0xa0, 0xbf, 0xe2, 0xbf, 0xd4, 0x61, 0xdc, 0x6f, 0x36, + 0x12, 0xf6, 0xd7, 0x08, 0x7c, 0x76, 0x7a, 0xe9, 0x88, 0x4d, 0x50, 0xaf, 0xff, 0x97, 0xc2, 0x83, 0x46, 0x90, 0xf1, + 0xc3, 0x76, 0x0a, 0x78, 0xf4, 0x6c, 0x26, 0xc6, 0x3f, 0xa0, 0x1e, 0xea, 0xfd, 0xa5, 0x0e, 0xff, 0x42, 0xf7, 0x8e, + 0xaa, 0xb9, 0xfc, 0x2e, 0xdd, 0x16, 0xae, 0xc2, 0xfc, 0x1c, 0x96, 0x5b, 0x98, 0xe1, 0x76, 0x93, 0x41, 0x50, 0x34, + 0x70, 0xf9, 0x26, 0xb1, 0x6c, 0xf0, 0xa3, 0xe3, 0x16, 0xfa, 0xa1, 0xdd, 0x01, 0x25, 0x46, 0x53, 0x1c, 0x6e, 0x6f, + 0xfa, 0xa2, 0x79, 0x8c, 0x1f, 0x34, 0x0b, 0xdc, 0x46, 0xb8, 0xd9, 0xf6, 0xda, 0xe5, 0xbe, 0x8a, 0x5b, 0x08, 0xab, + 0xf8, 0x1c, 0xfe, 0x39, 0x41, 0x83, 0x6a, 0x43, 0x5e, 0xd1, 0xcd, 0xde, 0xc1, 0x3f, 0x95, 0xc4, 0xaa, 0xc1, 0x8f, + 0xce, 0x5a, 0xe8, 0x87, 0x33, 0xd3, 0x11, 0x3b, 0xd4, 0x3b, 0xba, 0x92, 0xf8, 0xa4, 0x29, 0xa1, 0xa3, 0x56, 0xd9, + 0x8f, 0x88, 0x4f, 0x11, 0x16, 0xf1, 0x31, 0xfc, 0xd3, 0x0e, 0xfb, 0xf9, 0x75, 0xab, 0x1f, 0x33, 0xef, 0x36, 0x4e, + 0x4e, 0xad, 0xbb, 0xab, 0xb2, 0x77, 0xcf, 0x0d, 0x76, 0xd9, 0x36, 0x97, 0x66, 0xed, 0x23, 0xf8, 0x40, 0x58, 0x1f, + 0x12, 0x85, 0xd9, 0x21, 0xf8, 0xc9, 0x82, 0x79, 0x88, 0xba, 0x38, 0xee, 0xaa, 0x46, 0x03, 0x89, 0xbe, 0x1a, 0x1c, + 0x92, 0x76, 0x53, 0x37, 0x19, 0x86, 0xdf, 0x0d, 0x52, 0x06, 0x5f, 0x13, 0x55, 0xaf, 0x8f, 0x5d, 0xaf, 0xf6, 0x86, + 0xdd, 0x63, 0x07, 0x21, 0x44, 0xf5, 0x62, 0xdd, 0x64, 0xe8, 0x48, 0x34, 0x62, 0x7d, 0xc1, 0x7a, 0x67, 0x69, 0x0b, + 0x19, 0xec, 0x54, 0xbd, 0x98, 0x35, 0x39, 0xa4, 0x77, 0xd2, 0x98, 0x37, 0x35, 0xfc, 0x3a, 0x09, 0x66, 0x21, 0x00, + 0xef, 0x2a, 0xaf, 0x9f, 0xe2, 0xa8, 0x73, 0x7a, 0x8a, 0x05, 0xe1, 0xc9, 0xc4, 0xfc, 0x52, 0x84, 0x27, 0x43, 0xf3, + 0x4b, 0x92, 0x12, 0x5e, 0xb6, 0x77, 0x5c, 0x90, 0x60, 0x55, 0x4d, 0x0a, 0x85, 0x05, 0x2d, 0xd0, 0x51, 0xc7, 0x5f, + 0xbf, 0xe3, 0xa9, 0x9f, 0x03, 0xa8, 0x1b, 0x0a, 0x63, 0x79, 0x29, 0x9b, 0x05, 0xce, 0x09, 0xbd, 0x4c, 0x4e, 0x7b, + 0xd3, 0xa3, 0xb8, 0xd3, 0x94, 0xcd, 0x02, 0xa5, 0xd3, 0x23, 0x53, 0x13, 0x67, 0xe4, 0x31, 0xb5, 0xad, 0xe1, 0x29, + 0xdc, 0x99, 0x66, 0x24, 0x3b, 0x3c, 0x6b, 0x35, 0x92, 0x53, 0x84, 0xfb, 0xd9, 0xaa, 0x85, 0xf3, 0xd5, 0xaa, 0x85, + 0x69, 0xb0, 0x0c, 0x8f, 0x85, 0x07, 0x48, 0xa9, 0x11, 0xdb, 0x8c, 0x81, 0xd3, 0xe3, 0xb1, 0x86, 0xfb, 0x7f, 0x0d, + 0x5e, 0x35, 0x1a, 0xfc, 0x7d, 0x52, 0xee, 0x2e, 0xde, 0x90, 0x89, 0x02, 0x38, 0x0e, 0x75, 0x64, 0xaf, 0x85, 0x5f, + 0x57, 0x6f, 0xc1, 0x29, 0xe2, 0xdf, 0x25, 0xb6, 0x69, 0x41, 0x31, 0xba, 0x5d, 0xec, 0x57, 0xba, 0x55, 0xec, 0xcd, + 0x8e, 0x62, 0x57, 0xdb, 0xc5, 0x3e, 0xca, 0x40, 0xa3, 0xc7, 0x7f, 0x38, 0x3e, 0x6b, 0x35, 0x8e, 0x01, 0x59, 0x8f, + 0xcf, 0x5a, 0x55, 0xa1, 0x7b, 0xb4, 0x5a, 0x2b, 0x4d, 0x3e, 0x53, 0xeb, 0xd3, 0xc0, 0xbd, 0x73, 0xb5, 0x59, 0x38, + 0xeb, 0xda, 0x5d, 0xfa, 0x71, 0xf7, 0x4f, 0xc1, 0x66, 0x44, 0x18, 0x6a, 0xa7, 0xfb, 0x67, 0x83, 0xde, 0x94, 0xc5, + 0x0d, 0x48, 0x45, 0xe9, 0x58, 0xbb, 0x5f, 0xa8, 0xbc, 0x3a, 0xfe, 0x28, 0x21, 0xa9, 0x33, 0x40, 0x58, 0x92, 0x86, + 0xee, 0x1f, 0x0f, 0xcc, 0x79, 0x57, 0xc0, 0xef, 0x13, 0xf3, 0xbb, 0x54, 0xdc, 0x38, 0xc7, 0x87, 0xe9, 0xcd, 0x30, + 0xea, 0x09, 0xf2, 0x9a, 0xc6, 0xc6, 0x96, 0x1d, 0xa5, 0x65, 0x86, 0xfa, 0x02, 0x19, 0x6f, 0xca, 0x0c, 0x41, 0x5e, + 0x0b, 0xf7, 0x1b, 0x2f, 0x8b, 0x14, 0xec, 0x5a, 0xf0, 0x24, 0x05, 0x9b, 0x16, 0x3c, 0x4c, 0x05, 0xf8, 0x5d, 0xd0, + 0x94, 0x05, 0xd6, 0xf2, 0x0f, 0x9d, 0xa6, 0xcc, 0xdc, 0xee, 0x12, 0x83, 0xa5, 0x5d, 0x06, 0x27, 0xc5, 0x47, 0x19, + 0xc3, 0xdf, 0x86, 0x46, 0x98, 0x41, 0x9b, 0x0c, 0x61, 0x9e, 0x14, 0x04, 0xd2, 0x30, 0x4f, 0x26, 0x84, 0x41, 0x93, + 0x3c, 0x19, 0x12, 0xd6, 0xef, 0x04, 0x68, 0xf2, 0xd4, 0xc0, 0x0e, 0x80, 0xc3, 0xeb, 0x17, 0xe6, 0xda, 0x36, 0x0e, + 0x37, 0xf1, 0xd0, 0x84, 0x20, 0x5c, 0xc5, 0x30, 0x0b, 0xd8, 0x9c, 0xe6, 0x67, 0xa7, 0x0a, 0x33, 0xc9, 0x13, 0x6a, + 0xa8, 0xf7, 0x27, 0x20, 0xab, 0xf1, 0xbd, 0x25, 0x5b, 0xe3, 0xbd, 0x7b, 0x4b, 0xb1, 0xfe, 0x01, 0xfe, 0x28, 0xfb, + 0x07, 0x98, 0x87, 0x84, 0xa2, 0x35, 0xfa, 0x94, 0x42, 0xb1, 0x1d, 0xa5, 0xd0, 0x27, 0xef, 0x76, 0xa7, 0xc8, 0xf2, + 0x36, 0x8d, 0x46, 0xb4, 0xf8, 0x1c, 0xe1, 0x3f, 0xd3, 0x28, 0x07, 0x6e, 0x31, 0xc2, 0x1f, 0xd3, 0xa8, 0x60, 0x11, + 0xfe, 0x23, 0x8d, 0x86, 0xf9, 0x22, 0xc2, 0x1f, 0xd2, 0x68, 0x52, 0x44, 0xf8, 0x3d, 0x28, 0x45, 0x47, 0x7c, 0x31, + 0x8b, 0xf0, 0xef, 0x69, 0xa4, 0x8c, 0xd7, 0x01, 0x7e, 0x98, 0x46, 0x8c, 0x45, 0xf8, 0x5d, 0x1a, 0xc9, 0x3c, 0xc2, + 0x57, 0x69, 0x24, 0x8b, 0x08, 0x3f, 0x4a, 0xa3, 0x82, 0x46, 0xf8, 0x71, 0x1a, 0x41, 0xa1, 0x49, 0x84, 0x9f, 0xa4, + 0x11, 0xb4, 0xac, 0x22, 0xfc, 0x36, 0x8d, 0xb8, 0x88, 0xf0, 0x6f, 0x69, 0xa4, 0x17, 0xc5, 0x3f, 0x0b, 0xc9, 0x55, + 0x84, 0x9f, 0xa6, 0xd1, 0x94, 0x47, 0xf8, 0x4d, 0x1a, 0x15, 0x32, 0xc2, 0xaf, 0xd3, 0x88, 0xe6, 0x11, 0x7e, 0x95, + 0x46, 0x39, 0x8b, 0xf0, 0xaf, 0x69, 0x34, 0x62, 0x11, 0x7e, 0x99, 0x46, 0x77, 0x2c, 0xcf, 0x65, 0x84, 0x9f, 0xa5, + 0x11, 0x13, 0x11, 0xfe, 0x25, 0x8d, 0xb2, 0x69, 0x84, 0x7f, 0x4a, 0x23, 0x5a, 0x7c, 0x56, 0x11, 0x7e, 0x9e, 0x46, + 0x8c, 0x46, 0xf8, 0x85, 0xed, 0x68, 0x12, 0xe1, 0x9f, 0xd3, 0xe8, 0x66, 0x1a, 0xad, 0xb1, 0x52, 0x64, 0xf9, 0x9a, + 0x67, 0xec, 0x0f, 0x96, 0x46, 0xe3, 0xd6, 0xf8, 0x7c, 0x3c, 0x8e, 0x30, 0x15, 0x9a, 0xff, 0xb3, 0x60, 0x37, 0x4f, + 0x35, 0x24, 0x52, 0x36, 0x1c, 0xdd, 0x8f, 0x30, 0xfd, 0x67, 0x41, 0xd3, 0x68, 0x3c, 0x36, 0x05, 0xfe, 0x59, 0xd0, + 0x19, 0x2d, 0xde, 0xb2, 0x34, 0xba, 0x3f, 0x1e, 0x8f, 0x47, 0x27, 0x11, 0xa6, 0x5f, 0x17, 0x1f, 0x4d, 0x0b, 0xa6, + 0xc0, 0x90, 0xf1, 0x09, 0xd4, 0x3d, 0x1d, 0x9f, 0x8e, 0xb2, 0x08, 0x0f, 0xb9, 0xfa, 0x67, 0x01, 0xdf, 0x63, 0x76, + 0x92, 0x9d, 0x44, 0x78, 0x98, 0xd3, 0xec, 0x73, 0x1a, 0xb5, 0xcc, 0x2f, 0xf1, 0x0b, 0x1b, 0xbd, 0x9e, 0x49, 0x73, + 0x65, 0x30, 0x66, 0xc3, 0x6c, 0x14, 0x61, 0x33, 0x98, 0x31, 0xfc, 0xfd, 0xc2, 0xdf, 0x31, 0x9d, 0x46, 0xe7, 0xb4, + 0x33, 0x64, 0x9d, 0x08, 0x0f, 0xdf, 0xdc, 0x88, 0x34, 0xa2, 0xa7, 0x1d, 0xda, 0xa1, 0x11, 0x1e, 0x2e, 0x8a, 0xfc, + 0xee, 0x46, 0xca, 0x11, 0x00, 0x61, 0x78, 0x7e, 0x7e, 0x3f, 0xc2, 0x19, 0xfd, 0x55, 0x43, 0xed, 0xd3, 0xf1, 0x03, + 0x46, 0x5b, 0x11, 0xfe, 0x85, 0x16, 0xfa, 0xe3, 0x42, 0xb9, 0x81, 0xb6, 0x20, 0x45, 0x66, 0xef, 0x40, 0x9d, 0x1e, + 0x8d, 0x3a, 0x67, 0x0f, 0xda, 0x2c, 0xc2, 0xd9, 0xd5, 0x6b, 0xe8, 0xed, 0xfe, 0xf8, 0xb4, 0x05, 0x1f, 0x02, 0xe4, + 0x52, 0x56, 0x40, 0x23, 0x67, 0x27, 0x0f, 0x4e, 0xd9, 0xc8, 0x24, 0x2a, 0x9e, 0x7f, 0x36, 0xb3, 0x3f, 0x87, 0xf9, + 0x64, 0x05, 0x9f, 0x29, 0x29, 0xd2, 0x68, 0x94, 0xb5, 0x4f, 0x8e, 0x21, 0xe1, 0x8e, 0x0a, 0x0f, 0x9c, 0x5b, 0xa8, + 0x7a, 0x3e, 0x8c, 0xf0, 0xad, 0x4d, 0x3d, 0x1f, 0x9a, 0x8f, 0xc9, 0xbb, 0x5f, 0xc5, 0x9b, 0x51, 0x1a, 0x0d, 0xcf, + 0xcf, 0xcf, 0x5a, 0x90, 0xf0, 0x81, 0xde, 0xa5, 0x11, 0x7d, 0x00, 0xff, 0x41, 0xf6, 0xc7, 0x67, 0xd0, 0x21, 0x8c, + 0xf0, 0x76, 0xf2, 0x31, 0xcc, 0xf9, 0x3c, 0xa5, 0x9f, 0x79, 0x1a, 0x0d, 0x47, 0xc3, 0xfb, 0x67, 0x50, 0x6f, 0x46, + 0x27, 0xcf, 0x34, 0x85, 0x76, 0x5b, 0x2d, 0xd3, 0xf2, 0x3b, 0xfe, 0x85, 0x99, 0xea, 0xa7, 0xa7, 0x67, 0xc3, 0x0e, + 0x8c, 0xe0, 0x0a, 0x14, 0x2a, 0x30, 0x9e, 0xf3, 0xcc, 0x34, 0x78, 0x95, 0x3d, 0x1d, 0xa5, 0xd1, 0x83, 0x07, 0xc7, + 0x9d, 0x2c, 0x8b, 0xf0, 0xed, 0xc7, 0x91, 0xad, 0x6d, 0xf2, 0x14, 0xc0, 0x3e, 0x8d, 0xd8, 0x83, 0x07, 0x67, 0xf7, + 0x29, 0x7c, 0x3f, 0x37, 0x6d, 0x9d, 0x8f, 0x87, 0xd9, 0x39, 0xb4, 0xf5, 0x3b, 0x4c, 0xe7, 0xe4, 0xfc, 0x78, 0x64, + 0xfa, 0xfa, 0xdd, 0x8c, 0xba, 0x33, 0x3e, 0x19, 0x9f, 0x98, 0x4c, 0x33, 0xd4, 0xf2, 0xf3, 0x37, 0x96, 0x46, 0x19, + 0x1b, 0xb5, 0x23, 0x7c, 0xeb, 0x16, 0xee, 0xc1, 0x49, 0xab, 0x35, 0x3a, 0x8e, 0xf0, 0xe8, 0xe1, 0x7c, 0xfe, 0xd6, + 0x40, 0xb0, 0x7d, 0xf2, 0xc0, 0x7e, 0xab, 0xcf, 0x77, 0xd0, 0xf4, 0xd0, 0x00, 0x6d, 0xc4, 0x67, 0xa6, 0xe5, 0xb3, + 0x07, 0xf0, 0x9f, 0xf9, 0x36, 0x4d, 0x97, 0xdf, 0x72, 0x34, 0xb1, 0x8b, 0xd2, 0x66, 0x0f, 0x5a, 0x50, 0x63, 0xcc, + 0x3f, 0x0e, 0x0b, 0x0e, 0x68, 0x34, 0xec, 0xc0, 0xff, 0x45, 0x78, 0x9c, 0x5f, 0xbd, 0x76, 0x38, 0x3b, 0x1e, 0xd3, + 0x71, 0x2b, 0xc2, 0x63, 0xf9, 0x51, 0xe9, 0x0f, 0x0f, 0x45, 0x1a, 0x75, 0x3a, 0xe7, 0x43, 0x53, 0x66, 0xf1, 0x8b, + 0xe2, 0x06, 0x8f, 0x5b, 0xa6, 0x95, 0x09, 0x7d, 0xab, 0x86, 0x57, 0x12, 0x56, 0x12, 0xfe, 0x8b, 0xf0, 0x04, 0xb4, + 0x70, 0xae, 0x95, 0x73, 0xbb, 0x1d, 0x26, 0xef, 0x0c, 0x6a, 0x8e, 0xee, 0x03, 0xbc, 0xfc, 0x32, 0x8e, 0x28, 0x3d, + 0xed, 0xb4, 0x22, 0x6c, 0x46, 0x7d, 0xde, 0x82, 0xff, 0x22, 0x6c, 0x21, 0x67, 0xe0, 0x3a, 0xf9, 0xf8, 0xec, 0xe5, + 0x4d, 0x1a, 0xd1, 0xd1, 0x78, 0x0c, 0x4b, 0x62, 0x26, 0xe3, 0x8b, 0x4d, 0xa5, 0x60, 0x77, 0xbf, 0xde, 0xb8, 0xed, + 0x62, 0x12, 0xb4, 0x83, 0xce, 0xd9, 0x83, 0xe1, 0x49, 0x84, 0xdf, 0x8e, 0x38, 0x15, 0xb0, 0x4a, 0xd9, 0xe8, 0x34, + 0x3b, 0xcd, 0x4c, 0xc2, 0x44, 0xa6, 0xd1, 0x09, 0x2c, 0x79, 0x27, 0xc2, 0xfc, 0xcb, 0xd5, 0x9d, 0x45, 0x37, 0xa8, + 0xed, 0x10, 0x64, 0xdc, 0x62, 0x67, 0xe7, 0x59, 0x84, 0x73, 0xfa, 0xe5, 0xd9, 0xaf, 0x45, 0x1a, 0xb1, 0x33, 0x76, + 0x36, 0xa6, 0xfe, 0xfb, 0x0f, 0x35, 0x35, 0x35, 0x5a, 0xe3, 0x53, 0x48, 0xba, 0x11, 0x66, 0xac, 0xf7, 0xb3, 0xb1, + 0xc1, 0x90, 0x57, 0x33, 0x29, 0xb2, 0xa7, 0xe3, 0xb1, 0xb4, 0x58, 0x4c, 0x61, 0x13, 0xfe, 0x09, 0xd0, 0xa6, 0xa3, + 0xd1, 0x39, 0x3b, 0x8b, 0xf0, 0x9f, 0x76, 0x97, 0xb8, 0x09, 0xfc, 0x69, 0x31, 0x9b, 0xb9, 0xdd, 0xfe, 0xa7, 0x05, + 0x0a, 0xcc, 0x77, 0x4c, 0xc7, 0x74, 0xd4, 0x89, 0xf0, 0x9f, 0x06, 0x2e, 0xa3, 0x63, 0xf8, 0x0f, 0x0a, 0x40, 0x67, + 0x0f, 0x5a, 0x8c, 0x3d, 0x68, 0x99, 0xaf, 0x30, 0xcf, 0xcd, 0x7c, 0x78, 0x96, 0xb5, 0x23, 0xfc, 0xa7, 0x43, 0xc7, + 0xf1, 0x98, 0xb6, 0x00, 0x1d, 0xff, 0x74, 0xe8, 0xd8, 0x69, 0x0d, 0x3b, 0xd4, 0x7c, 0x5b, 0xac, 0x39, 0xbf, 0x9f, + 0x31, 0x98, 0xdc, 0x9f, 0x16, 0x21, 0xef, 0xdf, 0x3f, 0x3f, 0x7f, 0xf0, 0x00, 0x3e, 0x4d, 0xdb, 0xe5, 0xa7, 0xd2, + 0x0f, 0x73, 0x83, 0x64, 0xad, 0xec, 0x04, 0xe8, 0xe4, 0x9f, 0x66, 0x8c, 0xe3, 0xf1, 0x98, 0xb5, 0x22, 0x9c, 0xf3, + 0x19, 0xb3, 0x98, 0x60, 0x7f, 0x9b, 0x8e, 0x8e, 0x3b, 0xd9, 0xe8, 0xb8, 0x13, 0xe1, 0xfc, 0xed, 0x33, 0x33, 0x9b, + 0x16, 0xcc, 0xde, 0x6f, 0x39, 0x8f, 0x35, 0x33, 0xfa, 0x06, 0x06, 0x09, 0x2b, 0x0d, 0x95, 0xdf, 0x07, 0xf4, 0xf0, + 0xec, 0x2c, 0x1b, 0xc1, 0x40, 0xdf, 0x43, 0xb7, 0x00, 0xc6, 0xf7, 0x76, 0xf3, 0x0d, 0xe9, 0xe9, 0x29, 0x4c, 0xf7, + 0xfd, 0x7c, 0x51, 0xcc, 0x5f, 0xa5, 0xd1, 0x83, 0xe3, 0xfb, 0xad, 0xd1, 0x30, 0xc2, 0xef, 0xdd, 0x04, 0x8f, 0xb3, + 0xe1, 0xf1, 0xfd, 0x76, 0x84, 0xdf, 0x9b, 0xfd, 0x76, 0x7f, 0x78, 0x76, 0x0e, 0xe7, 0xc6, 0x7b, 0x35, 0x2f, 0xde, + 0x4e, 0x4c, 0x81, 0x31, 0x7d, 0x00, 0xcd, 0xfe, 0x66, 0x76, 0xe3, 0xa8, 0x0d, 0x1b, 0xf9, 0xbd, 0xd9, 0x64, 0x06, + 0x4f, 0xee, 0xb7, 0x4f, 0xcf, 0x4f, 0x23, 0x3c, 0xe3, 0x23, 0x01, 0x04, 0xde, 0x6c, 0x94, 0x07, 0xed, 0x07, 0xf7, + 0x5b, 0x11, 0x9e, 0xbd, 0xd5, 0xd9, 0x47, 0x3a, 0x33, 0xd4, 0x78, 0x0c, 0x30, 0x9b, 0x71, 0xa5, 0xef, 0xde, 0x28, + 0x47, 0x8f, 0x59, 0x3b, 0xc2, 0x33, 0x99, 0x65, 0x54, 0xbd, 0xb5, 0x09, 0xc3, 0xd3, 0x08, 0x0b, 0xfa, 0x85, 0xfe, + 0x2d, 0xfd, 0x66, 0x1a, 0x31, 0x3a, 0x32, 0x69, 0x06, 0x87, 0x23, 0xfc, 0x6e, 0x04, 0x97, 0x7e, 0x69, 0x34, 0x1e, + 0x8d, 0x4f, 0x01, 0x3c, 0x40, 0x80, 0x2c, 0x76, 0x03, 0x34, 0xe0, 0x6b, 0xf4, 0x68, 0x98, 0x46, 0x67, 0xc3, 0x73, + 0xd6, 0x39, 0x8e, 0x70, 0x49, 0x8d, 0xe8, 0x29, 0xe4, 0x9b, 0xcf, 0x8f, 0x66, 0x4b, 0x9d, 0xd8, 0x04, 0x03, 0xa0, + 0x11, 0xbd, 0xdf, 0x1a, 0x9d, 0x45, 0x78, 0xfe, 0x9a, 0xf9, 0x3d, 0xc6, 0x18, 0x3b, 0x07, 0x58, 0x42, 0x92, 0x41, + 0xa0, 0xf3, 0xf1, 0xf0, 0xc1, 0xb9, 0xf9, 0x06, 0x30, 0xd0, 0x31, 0x63, 0x00, 0xa4, 0xf9, 0x6b, 0x56, 0x02, 0x62, + 0x34, 0xbc, 0xdf, 0x02, 0xfa, 0x32, 0xa7, 0x73, 0x7a, 0x47, 0x6f, 0x9e, 0xce, 0xcd, 0x9c, 0xc6, 0xa3, 0xd3, 0x08, + 0xcf, 0x9f, 0xff, 0x32, 0x5f, 0x8c, 0xc7, 0x66, 0x42, 0x74, 0xf8, 0x20, 0xc2, 0x73, 0x56, 0x2c, 0x60, 0x8d, 0xce, + 0x4f, 0x8f, 0xc7, 0x11, 0x76, 0x68, 0x98, 0xb5, 0xb2, 0x21, 0xdc, 0x6a, 0x2e, 0x66, 0x69, 0x34, 0x1a, 0xd1, 0xd6, + 0x08, 0xee, 0x38, 0xe5, 0xcd, 0xaf, 0x85, 0x45, 0x23, 0x66, 0xf0, 0xc1, 0xad, 0x21, 0xcc, 0x17, 0xe0, 0xf1, 0x71, + 0xc8, 0xb2, 0x8c, 0xba, 0xc4, 0xb3, 0xb3, 0xe3, 0x63, 0xc0, 0x3d, 0x3b, 0x43, 0x8b, 0x20, 0x6f, 0xd4, 0xdd, 0xb0, + 0x90, 0x70, 0x74, 0x01, 0x51, 0x05, 0xb2, 0xfa, 0xe6, 0xee, 0xb5, 0xa1, 0xab, 0xed, 0xb3, 0x07, 0xb0, 0x00, 0x8a, + 0x8e, 0x46, 0xaf, 0xec, 0xe1, 0x76, 0x3e, 0x3c, 0x39, 0x6d, 0x1f, 0x47, 0xd8, 0x6f, 0x04, 0x7a, 0xde, 0xba, 0xdf, + 0x81, 0x12, 0x62, 0x74, 0x67, 0x4b, 0x8c, 0x4f, 0xe8, 0xc9, 0x59, 0x2b, 0xc2, 0x7e, 0x6b, 0xb0, 0xf3, 0xe1, 0xe9, + 0x7d, 0xf8, 0x54, 0x53, 0x96, 0xe7, 0x06, 0xbf, 0x4f, 0x01, 0x2e, 0x8a, 0x3f, 0x13, 0x34, 0x8d, 0x68, 0xeb, 0xb4, + 0xd3, 0x19, 0xc1, 0x67, 0xfe, 0x85, 0x15, 0x69, 0x94, 0xb5, 0xe0, 0xbf, 0x08, 0x07, 0x3b, 0x89, 0x0d, 0x23, 0x6c, + 0xf0, 0xee, 0x8c, 0x9e, 0x9a, 0xbd, 0xef, 0x76, 0x55, 0xeb, 0xbc, 0x05, 0x1b, 0xd6, 0x6d, 0x2a, 0xf7, 0xa5, 0x84, + 0xbc, 0x71, 0x24, 0x96, 0x46, 0x38, 0x40, 0xd0, 0xf1, 0xfd, 0x71, 0x84, 0xfd, 0x8e, 0x3b, 0x39, 0x3b, 0xef, 0x00, + 0x29, 0xd3, 0x40, 0x28, 0x46, 0x9d, 0xe1, 0x09, 0x90, 0x26, 0xcd, 0x5e, 0x5b, 0x3c, 0x89, 0xb0, 0x7e, 0xaa, 0xf4, + 0xab, 0x34, 0x1a, 0x9d, 0x0f, 0xc7, 0xa3, 0xf3, 0x08, 0x6b, 0x39, 0xa3, 0x5a, 0x1a, 0x0a, 0x78, 0x7c, 0x72, 0x3f, + 0xc2, 0x06, 0xcd, 0x5b, 0xac, 0x35, 0x6a, 0x45, 0xd8, 0x1d, 0x25, 0x8c, 0x9d, 0x77, 0x60, 0x5a, 0x3f, 0x3f, 0xd7, + 0x80, 0xcb, 0x23, 0x36, 0x3c, 0x8e, 0x70, 0x49, 0xef, 0x0d, 0x21, 0x82, 0x2f, 0x35, 0x93, 0x9f, 0x1d, 0xeb, 0x01, + 0xa4, 0xce, 0x6f, 0x78, 0x58, 0x86, 0x97, 0x37, 0x16, 0x8d, 0xa8, 0xd9, 0xe2, 0xc1, 0xad, 0xef, 0x13, 0x1a, 0x7b, + 0xb6, 0x9d, 0x93, 0xe5, 0x1a, 0x97, 0xc1, 0x54, 0x3f, 0xb3, 0x3b, 0x15, 0x2b, 0x65, 0x38, 0xd9, 0x20, 0x05, 0x1c, + 0x1e, 0x9c, 0xfb, 0x80, 0xf3, 0x10, 0x05, 0x41, 0x52, 0x90, 0x56, 0x57, 0x5c, 0x78, 0xaf, 0xd5, 0xae, 0x80, 0x10, + 0x0b, 0x90, 0x5e, 0x10, 0x4a, 0x34, 0x44, 0xa2, 0xb1, 0xc2, 0xa4, 0x37, 0xe6, 0x37, 0x32, 0xa5, 0xb4, 0xee, 0x01, + 0x94, 0x50, 0x1f, 0x83, 0x1e, 0xae, 0xa4, 0x21, 0x4a, 0x13, 0xea, 0x4a, 0x62, 0x22, 0x4a, 0xbf, 0x10, 0x3a, 0x56, + 0xaa, 0x5f, 0x0c, 0x70, 0xfb, 0x0c, 0x61, 0x88, 0xd5, 0x40, 0xfa, 0xf2, 0xf2, 0xb2, 0x7d, 0x76, 0x60, 0x84, 0xbe, + 0xcb, 0xcb, 0x73, 0xfb, 0x03, 0xfe, 0x1d, 0x54, 0xf1, 0xb1, 0x61, 0x7c, 0x8f, 0x58, 0xa0, 0xd1, 0x33, 0xfc, 0xf5, + 0x23, 0xb6, 0x5a, 0xc5, 0x8f, 0x18, 0x81, 0x19, 0xe3, 0x47, 0x2c, 0x31, 0x77, 0x24, 0xd6, 0x13, 0x87, 0xf4, 0x41, + 0x73, 0xd6, 0xc2, 0x10, 0xb5, 0xdd, 0x73, 0xde, 0x8f, 0x58, 0x9f, 0xd7, 0xbd, 0xb8, 0xab, 0x50, 0xc9, 0x07, 0x07, + 0xcb, 0x22, 0xd5, 0x56, 0x4c, 0xd0, 0x56, 0x4c, 0xd0, 0x56, 0x4c, 0xd0, 0x55, 0xb0, 0xfa, 0x93, 0x1e, 0x48, 0x29, + 0x46, 0xd9, 0xe2, 0x78, 0xea, 0x77, 0xa0, 0xf6, 0x00, 0xed, 0x64, 0xaf, 0x52, 0x76, 0x94, 0xba, 0x8a, 0x9d, 0x0a, + 0x8c, 0x9d, 0x89, 0x4e, 0xdb, 0x71, 0xf4, 0xef, 0xa8, 0x3b, 0x5e, 0xd6, 0xc4, 0xb2, 0x77, 0x3b, 0xc5, 0x32, 0x58, + 0x49, 0x23, 0x9a, 0xed, 0xdb, 0xb8, 0x1f, 0xba, 0x7f, 0xdf, 0x08, 0x66, 0x55, 0x30, 0xba, 0x06, 0x24, 0x75, 0x41, + 0x0a, 0x39, 0x37, 0x52, 0x5a, 0x81, 0xd2, 0x91, 0x8e, 0x0b, 0xd0, 0x50, 0x7a, 0x05, 0x65, 0x19, 0x33, 0xb5, 0x61, + 0x00, 0xa2, 0xac, 0x8c, 0x66, 0x65, 0xb5, 0x53, 0x10, 0x5d, 0x40, 0x13, 0x66, 0x24, 0x16, 0x68, 0x40, 0x98, 0x06, + 0x84, 0xab, 0x0c, 0xe2, 0x8c, 0xcb, 0x3e, 0x31, 0xd9, 0xca, 0x64, 0xab, 0x32, 0x5b, 0xfa, 0x6c, 0x2b, 0x24, 0x4a, + 0x93, 0x2d, 0xcb, 0x6c, 0x90, 0xd9, 0xf0, 0x24, 0x55, 0x78, 0x98, 0x4a, 0x2b, 0xaa, 0x55, 0xb2, 0xd5, 0x5b, 0x1a, + 0x6a, 0x73, 0x0f, 0x0e, 0xe2, 0x52, 0x4e, 0x32, 0x6a, 0xe2, 0x7b, 0x4b, 0x9e, 0x14, 0x46, 0x06, 0xe2, 0xc9, 0xc4, + 0xfd, 0x1d, 0xae, 0x37, 0x65, 0xa5, 0x62, 0x32, 0xfc, 0x46, 0x49, 0xf4, 0xc9, 0x2b, 0x51, 0x1f, 0x71, 0x13, 0x6d, + 0xe7, 0x82, 0x24, 0xad, 0xd6, 0x71, 0xfb, 0xb8, 0x75, 0xde, 0xe3, 0x87, 0xed, 0x4e, 0xf2, 0xa0, 0x93, 0x1a, 0x45, + 0xc4, 0x5c, 0xde, 0x80, 0x02, 0xe6, 0xa8, 0x93, 0x9c, 0xa0, 0xc3, 0x76, 0xd2, 0x3a, 0x3d, 0x6d, 0xc2, 0x3f, 0xf8, + 0xbd, 0x2e, 0xab, 0x9d, 0xb4, 0x4e, 0x4e, 0x7b, 0xfc, 0x68, 0xa3, 0x52, 0xcc, 0x1b, 0x50, 0x10, 0x1d, 0x99, 0x4a, + 0x18, 0xea, 0x57, 0xcb, 0xfb, 0x6c, 0x4b, 0xcf, 0xf3, 0x5e, 0xc7, 0xd2, 0xaa, 0xe2, 0x00, 0xaa, 0xfe, 0x6b, 0x62, + 0x80, 0xe8, 0xbf, 0x86, 0x65, 0x44, 0xdc, 0x65, 0x01, 0xa2, 0xf6, 0x23, 0x1e, 0x8b, 0x06, 0x3b, 0x8c, 0x6d, 0xbe, + 0x86, 0xba, 0x4d, 0x88, 0x52, 0x87, 0x27, 0x2e, 0x57, 0x85, 0xb9, 0x13, 0x84, 0x9a, 0x0a, 0x72, 0x87, 0x2e, 0x57, + 0x86, 0xb9, 0x43, 0x84, 0x9a, 0x12, 0x72, 0x69, 0xca, 0x13, 0x0a, 0x39, 0x3a, 0xa1, 0x4d, 0x03, 0xc9, 0x6a, 0x51, + 0x9e, 0x33, 0x3f, 0x6c, 0x3e, 0x86, 0xe5, 0x31, 0x04, 0xc5, 0x09, 0xd2, 0x02, 0x5e, 0x32, 0x29, 0xb5, 0x39, 0x2d, + 0x5c, 0xaa, 0x71, 0x20, 0xa3, 0x01, 0xff, 0x1c, 0x32, 0xf3, 0xbc, 0x45, 0xab, 0x77, 0x7c, 0xd6, 0x4a, 0xdb, 0xe0, + 0x92, 0x0d, 0xb2, 0xb6, 0xb0, 0xb2, 0xb6, 0xf0, 0xb2, 0xb6, 0xf0, 0xb2, 0x36, 0x08, 0xf0, 0x41, 0xdf, 0xff, 0xc8, + 0x9a, 0x99, 0x0b, 0x2f, 0x6d, 0x66, 0xac, 0x51, 0x44, 0xac, 0x57, 0xab, 0xe5, 0x1a, 0x2c, 0x9a, 0x2a, 0x95, 0xbb, + 0xaa, 0xd4, 0x9f, 0xcb, 0x22, 0x6d, 0xe1, 0x49, 0x0a, 0x5a, 0xee, 0x16, 0xa6, 0x66, 0x73, 0x7b, 0xaa, 0xb0, 0x19, + 0x2d, 0xa7, 0xe7, 0xd5, 0xc9, 0x97, 0xe4, 0xd8, 0x68, 0x8f, 0x97, 0x45, 0xca, 0x2d, 0xcd, 0xe0, 0x96, 0x66, 0x70, + 0x4b, 0x33, 0xa0, 0x11, 0x5c, 0x16, 0x36, 0x65, 0x13, 0x4a, 0xe0, 0x4a, 0xa0, 0x7f, 0x3c, 0x80, 0x60, 0x81, 0xb1, + 0x26, 0x66, 0xd4, 0x1b, 0x9d, 0xb7, 0x21, 0x38, 0x9a, 0x2d, 0xa9, 0x13, 0x6a, 0x7c, 0xc4, 0xcb, 0x31, 0x7f, 0xad, + 0xa1, 0x7d, 0x02, 0x2f, 0xd7, 0x3c, 0xd4, 0x71, 0x0b, 0x4c, 0x44, 0xa2, 0x22, 0xea, 0x19, 0xb2, 0x90, 0x1a, 0x9d, + 0x8d, 0x33, 0xfd, 0xfe, 0xbc, 0xe1, 0x71, 0x6b, 0x29, 0x41, 0xf8, 0x5e, 0xc3, 0x67, 0x56, 0x85, 0x00, 0x28, 0x2d, + 0x5b, 0x9d, 0x59, 0x9a, 0x3d, 0x12, 0xba, 0x60, 0x9e, 0xee, 0x63, 0x4b, 0xf5, 0x04, 0x91, 0xca, 0x38, 0x47, 0x92, + 0x2a, 0x3a, 0x32, 0x38, 0x0b, 0x93, 0x5b, 0x6a, 0x5c, 0x67, 0x5e, 0xd8, 0x3f, 0x5f, 0x69, 0xe0, 0x5b, 0x58, 0x4c, + 0x86, 0xde, 0x25, 0xf7, 0xda, 0xc4, 0x10, 0x22, 0xfb, 0xfb, 0xd6, 0x72, 0xdc, 0x7c, 0x6d, 0x9a, 0x8e, 0x9b, 0x44, + 0x93, 0x0d, 0x3b, 0xd4, 0xaf, 0xd1, 0x3f, 0xde, 0x33, 0xae, 0x98, 0x0c, 0x51, 0x40, 0xb3, 0x0d, 0x58, 0x65, 0x05, + 0x2c, 0xe5, 0xea, 0x95, 0x0e, 0x93, 0xd0, 0xbb, 0x19, 0xf3, 0xba, 0x98, 0x0c, 0x77, 0x3e, 0x71, 0x62, 0x7b, 0xec, + 0xbd, 0xa5, 0x41, 0x0f, 0x5e, 0xb5, 0x3d, 0x65, 0xb7, 0xdf, 0xab, 0x73, 0xb3, 0xb3, 0x8e, 0xca, 0xbf, 0x57, 0xe7, + 0xe9, 0xae, 0x3a, 0x33, 0x7e, 0x1b, 0xfb, 0xbd, 0xa3, 0x03, 0x35, 0xb6, 0x31, 0x47, 0x9a, 0x0c, 0x21, 0x26, 0x3d, + 0xfc, 0xb5, 0x91, 0x63, 0xba, 0x9e, 0x84, 0xc3, 0x2a, 0xc8, 0x5e, 0x72, 0x9a, 0x32, 0x4c, 0x49, 0xe7, 0xb0, 0x30, + 0xb1, 0x63, 0x44, 0x42, 0x9b, 0x2a, 0xa1, 0x38, 0x27, 0x71, 0x4c, 0x0f, 0x33, 0x88, 0x80, 0xd3, 0xee, 0xd1, 0x34, + 0xa6, 0x8d, 0x0c, 0x1d, 0xc5, 0xed, 0x06, 0x3d, 0xcc, 0x10, 0x6a, 0xb4, 0x41, 0x67, 0x2a, 0x49, 0xbb, 0x99, 0x43, + 0x4c, 0x4c, 0x43, 0x8a, 0xf3, 0x43, 0x91, 0x14, 0x0d, 0x79, 0xa8, 0x92, 0xa2, 0x91, 0x9c, 0x62, 0x91, 0x4c, 0xca, + 0xe4, 0x89, 0x49, 0x9e, 0xd8, 0xe4, 0x61, 0x99, 0x3c, 0x34, 0xc9, 0x43, 0x9b, 0x4c, 0x49, 0x71, 0x28, 0x12, 0xda, + 0x88, 0xdb, 0xcd, 0x02, 0x1d, 0xc2, 0x08, 0xfc, 0xe8, 0x89, 0x08, 0x43, 0x91, 0xaf, 0x8d, 0x2d, 0xcf, 0x5c, 0xe6, + 0x2e, 0x38, 0x68, 0x05, 0xa4, 0xd2, 0xc1, 0x0a, 0xea, 0x3c, 0x0b, 0xc0, 0x84, 0xb5, 0xfd, 0xe3, 0x43, 0xdf, 0xad, + 0xb3, 0x5c, 0x8a, 0xc0, 0x81, 0x0c, 0x6c, 0xde, 0x3f, 0x3b, 0xb7, 0x19, 0x80, 0xea, 0x9a, 0xe6, 0xf3, 0x29, 0xdd, + 0xf2, 0xd2, 0x2d, 0x26, 0x43, 0xb7, 0xb3, 0xca, 0x66, 0x18, 0x2d, 0x6c, 0x48, 0xe9, 0xba, 0x3f, 0x25, 0x80, 0xda, + 0xfb, 0x70, 0x26, 0xd4, 0x28, 0xc9, 0x6d, 0x8d, 0x49, 0xc1, 0xee, 0x54, 0x46, 0x73, 0x16, 0x57, 0x07, 0x70, 0x35, + 0x4c, 0x46, 0x5e, 0x80, 0xe5, 0x7d, 0x71, 0x98, 0x1c, 0x37, 0x74, 0x32, 0x39, 0x4c, 0x4e, 0x1f, 0x34, 0x74, 0x32, + 0x3c, 0x4c, 0xda, 0xed, 0x0a, 0x67, 0x93, 0x82, 0xe8, 0x64, 0x42, 0x34, 0x68, 0x0c, 0x6d, 0xa3, 0x72, 0x4e, 0xc1, + 0x94, 0xec, 0xdf, 0x18, 0x46, 0xc3, 0x0d, 0x43, 0xb0, 0x89, 0x8d, 0xae, 0xb9, 0x35, 0x86, 0xb0, 0x9b, 0xce, 0xe9, + 0x69, 0x53, 0x27, 0x05, 0xd6, 0x76, 0x25, 0x9b, 0x3a, 0x99, 0x60, 0x6d, 0x97, 0xaf, 0xa9, 0x93, 0xa1, 0x6d, 0xca, + 0xe8, 0x00, 0x99, 0x08, 0x80, 0xf5, 0x9c, 0x05, 0x90, 0xef, 0x78, 0x67, 0x98, 0x35, 0x68, 0x0d, 0xbf, 0x57, 0xae, + 0xe9, 0x0b, 0x2a, 0xaa, 0xc1, 0xa4, 0x88, 0x7d, 0xab, 0x68, 0xbb, 0x6a, 0x92, 0xfd, 0xeb, 0xb2, 0x65, 0xb3, 0x85, + 0xd4, 0xf5, 0x82, 0x0f, 0x6b, 0x18, 0xe2, 0x4a, 0xb9, 0x83, 0xfb, 0x15, 0x25, 0x31, 0xc4, 0xd0, 0x33, 0xa7, 0x10, + 0x27, 0x5e, 0x8f, 0x0c, 0x49, 0xbc, 0xd1, 0x58, 0xa3, 0x38, 0x38, 0x6f, 0x9f, 0x86, 0x54, 0x75, 0x2b, 0xb0, 0x1e, + 0x21, 0xd1, 0x42, 0x58, 0xd3, 0xcb, 0x51, 0x14, 0xb0, 0x20, 0x4e, 0xbb, 0x5b, 0x3b, 0x20, 0x0e, 0x0e, 0x36, 0xcf, + 0x0b, 0xff, 0xc4, 0xc1, 0xd6, 0xb3, 0x06, 0x95, 0xdd, 0x9e, 0x7f, 0x78, 0xc9, 0x5a, 0xf4, 0xf2, 0x00, 0x51, 0x7c, + 0x88, 0xab, 0xfb, 0x86, 0xc2, 0xf7, 0xab, 0xf8, 0x7e, 0x2e, 0xa7, 0x79, 0x66, 0x32, 0x4c, 0x5f, 0x83, 0x60, 0x6c, + 0x6f, 0xc2, 0x09, 0x95, 0x36, 0x89, 0xff, 0xb2, 0xe3, 0xa0, 0x13, 0xf7, 0x30, 0x4c, 0xd8, 0xe8, 0xdf, 0xa1, 0x05, + 0x70, 0x05, 0x1b, 0xe7, 0xfb, 0xbd, 0x5a, 0xd5, 0x9e, 0x01, 0xb2, 0x8f, 0xcd, 0xa0, 0x83, 0x03, 0xae, 0x9e, 0x81, + 0xd1, 0x32, 0x8b, 0x1b, 0xe1, 0xe1, 0xfb, 0x4f, 0xed, 0xb4, 0xfe, 0xdb, 0x9c, 0xab, 0x69, 0x70, 0xd0, 0x3d, 0xac, + 0xe5, 0xef, 0x5c, 0x89, 0x9e, 0x4e, 0xb9, 0x5b, 0xeb, 0xbf, 0x2b, 0x93, 0xf0, 0xad, 0x07, 0xa9, 0x0e, 0x0e, 0x78, + 0x15, 0x16, 0x2a, 0xfa, 0x21, 0x42, 0x3d, 0x23, 0x83, 0x3c, 0xcb, 0x25, 0x85, 0x1b, 0x51, 0xb8, 0x62, 0x48, 0x1b, + 0xfc, 0x48, 0xe3, 0x3f, 0xe4, 0xff, 0xa7, 0x46, 0x0e, 0x75, 0xda, 0xe0, 0x81, 0x00, 0x16, 0xb2, 0x42, 0x55, 0x40, + 0x46, 0x03, 0xe9, 0xd0, 0xc2, 0x1b, 0x95, 0x87, 0x39, 0x9d, 0xcf, 0xf3, 0x3b, 0xf3, 0x26, 0x57, 0xc0, 0x51, 0x55, + 0x17, 0x4d, 0x2e, 0x1a, 0x1e, 0x2e, 0x80, 0xa7, 0x07, 0xdc, 0x43, 0xc6, 0x9b, 0xb5, 0xbc, 0xdc, 0x16, 0x08, 0x24, + 0x33, 0x45, 0x64, 0xb3, 0xdd, 0x55, 0x97, 0x20, 0x97, 0x35, 0x9b, 0x48, 0xbb, 0x20, 0xe1, 0x98, 0x83, 0x4c, 0xa6, + 0xac, 0xc7, 0xea, 0x9e, 0x2d, 0x08, 0x92, 0x9b, 0x34, 0x22, 0xdb, 0xee, 0x52, 0x7c, 0x1c, 0x03, 0x1a, 0x21, 0x2b, + 0xf0, 0x85, 0xc2, 0x22, 0x07, 0xae, 0xb3, 0xf0, 0x1d, 0x7f, 0xa3, 0xa5, 0xa2, 0xaf, 0x06, 0x03, 0x5c, 0x98, 0x67, + 0x28, 0xca, 0xf9, 0x14, 0x2a, 0x78, 0xd6, 0x28, 0x10, 0x51, 0xf8, 0x6a, 0xb5, 0x0f, 0xaf, 0x06, 0xb9, 0x36, 0xc1, + 0xc5, 0xd5, 0xfd, 0xac, 0x5e, 0x08, 0x81, 0x71, 0x30, 0xd2, 0x32, 0x17, 0x85, 0x4e, 0xde, 0x64, 0x17, 0xa2, 0xdb, + 0x68, 0x30, 0x13, 0xd0, 0x89, 0x40, 0xf4, 0x36, 0xf0, 0x3f, 0x84, 0x3f, 0x36, 0x46, 0x93, 0x62, 0x36, 0xd2, 0x1d, + 0x84, 0xe0, 0xae, 0x25, 0xac, 0x56, 0xca, 0x46, 0x52, 0x31, 0x39, 0x36, 0xa6, 0x4a, 0xd9, 0x4f, 0x19, 0xb2, 0xb5, + 0x32, 0xe3, 0xe0, 0x6e, 0xab, 0xbf, 0xad, 0xf6, 0xf3, 0x1e, 0xb7, 0xd7, 0x78, 0xdc, 0xc4, 0x27, 0x30, 0x80, 0x5a, + 0x6e, 0x6c, 0x70, 0x6b, 0x4f, 0x1f, 0x5b, 0xe3, 0x5f, 0xb6, 0x09, 0x41, 0x51, 0xfa, 0xe3, 0xdb, 0x9b, 0x5b, 0x1f, + 0x9f, 0x50, 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, 0xb1, 0x47, + 0x01, 0x2f, 0x18, 0x0d, 0xca, 0x10, 0x56, 0x66, 0xe3, 0x37, 0xdb, 0x55, 0x63, 0xef, 0x69, 0x85, 0x77, 0xb0, 0x3c, + 0xa6, 0xf1, 0x2d, 0xa7, 0xcf, 0x3e, 0x07, 0x78, 0xb3, 0x3e, 0x1f, 0x74, 0xdf, 0xc4, 0x0a, 0x1d, 0x1c, 0xbc, 0x89, + 0x25, 0xea, 0x5d, 0x31, 0x73, 0xe7, 0x06, 0x5e, 0xdf, 0x7d, 0x6e, 0x86, 0x2f, 0x03, 0x04, 0xb8, 0x62, 0x9b, 0x92, + 0xcd, 0x5b, 0x13, 0x63, 0x23, 0x85, 0x18, 0xde, 0x10, 0x49, 0xd8, 0x81, 0x04, 0x7a, 0x7d, 0x13, 0x42, 0xbb, 0xcb, + 0x08, 0x03, 0x16, 0xbe, 0xf4, 0xc9, 0x63, 0xc9, 0x8c, 0x15, 0x13, 0x56, 0xac, 0x56, 0xef, 0xa9, 0xf5, 0xb3, 0xdb, + 0x08, 0x09, 0xa9, 0xba, 0x8d, 0x06, 0x35, 0xe3, 0x07, 0xf1, 0x81, 0x0e, 0xf0, 0xfe, 0x9b, 0xb8, 0x40, 0x08, 0x2c, + 0x8c, 0xb8, 0x58, 0x78, 0x9f, 0xb2, 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, 0x88, 0xae, 0x03, 0x60, 0x3c, 0xa2, 0x01, 0x91, 0xd8, + 0x05, 0x64, 0x61, 0x81, 0xac, 0x3c, 0x90, 0x85, 0x01, 0xb2, 0x42, 0xbd, 0x39, 0x04, 0x47, 0x52, 0x28, 0xdd, 0xa2, + 0xe8, 0xf5, 0x30, 0x9e, 0xce, 0x45, 0x04, 0x73, 0x13, 0x49, 0xc2, 0x2d, 0x07, 0xb8, 0x8b, 0x38, 0xaf, 0x43, 0x45, + 0x96, 0x51, 0x64, 0x22, 0xda, 0xe2, 0x5b, 0xf3, 0x27, 0xb9, 0xc5, 0x77, 0xf6, 0xc7, 0x5d, 0xa0, 0x4c, 0x7a, 0x5e, + 0xd3, 0x36, 0x70, 0x17, 0xff, 0x2d, 0x4a, 0x22, 0x40, 0x6b, 0x17, 0xcc, 0x50, 0xd4, 0xdf, 0x77, 0x53, 0x36, 0xec, + 0x84, 0x68, 0x10, 0x85, 0x45, 0x40, 0x3a, 0xff, 0xfa, 0x2b, 0x42, 0x3d, 0x01, 0x51, 0x83, 0xdc, 0xc9, 0xd6, 0x6c, + 0xa3, 0x46, 0x94, 0x44, 0x69, 0xec, 0x83, 0x52, 0xc0, 0xce, 0x88, 0xa2, 0xe0, 0x6d, 0x97, 0x72, 0x18, 0x1f, 0x6a, + 0xc3, 0x30, 0x83, 0xaa, 0xc2, 0x6c, 0x5c, 0x2e, 0x37, 0x83, 0x1a, 0x19, 0xa8, 0x0a, 0x13, 0x51, 0x06, 0xd9, 0x07, + 0xcf, 0x18, 0x61, 0x07, 0x07, 0xac, 0x2f, 0x06, 0xc1, 0x0b, 0x66, 0xd5, 0x75, 0xb8, 0x0e, 0x17, 0x2e, 0xa6, 0x10, + 0x55, 0x7e, 0xb5, 0xb2, 0x7f, 0xc9, 0x07, 0x23, 0xcd, 0xc0, 0x53, 0x74, 0xc1, 0x19, 0x2b, 0x76, 0xcb, 0x62, 0x89, + 0x96, 0xbf, 0x83, 0x65, 0x9f, 0x8b, 0x11, 0xc8, 0xdd, 0x54, 0xdb, 0x1e, 0xea, 0x73, 0xa3, 0x51, 0x08, 0x22, 0xf4, + 0x56, 0x47, 0x1a, 0x9e, 0xeb, 0x30, 0xaf, 0x16, 0x69, 0x37, 0x53, 0x65, 0xc0, 0x54, 0x38, 0x52, 0x12, 0xb0, 0x52, + 0x37, 0x74, 0x12, 0x7e, 0xd4, 0xa9, 0xa4, 0x63, 0x21, 0x01, 0x0a, 0x1c, 0x99, 0xcb, 0x79, 0x13, 0x10, 0x9f, 0xa1, + 0x1d, 0x44, 0x2e, 0x30, 0xa1, 0xa9, 0xcb, 0x96, 0x2e, 0x72, 0x55, 0x34, 0x93, 0x0b, 0xc5, 0x16, 0x73, 0x38, 0xdf, + 0xcb, 0xb4, 0x2c, 0xe7, 0xd9, 0xe7, 0x7a, 0x0a, 0x18, 0x44, 0xde, 0xea, 0x19, 0x13, 0x8b, 0xc8, 0xcd, 0xf3, 0xab, + 0x15, 0xf7, 0xdf, 0xbc, 0xc0, 0x8f, 0x48, 0xe7, 0xf0, 0x2b, 0xfe, 0x48, 0xc9, 0xa3, 0xc6, 0x57, 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, 0x43, 0xc9, 0xd7, 0xa3, 0xf6, 0x79, 0x0b, 0x7f, 0x20, 0x5f, 0x8f, 0x3a, 0xf8, 0x56, 0x93, + 0xaf, 0x47, 0x27, 0x38, 0x57, 0xe4, 0xeb, 0x61, 0xe7, 0xe8, 0x18, 0x2f, 0xb4, 0x6d, 0x32, 0x97, 0x93, 0x76, 0x0b, + 0xff, 0xe3, 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, 0x36, 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, 0x36, 0x94, 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, 0x4f, 0xa1, 0xbd, 0x4a, 0x4f, 0x38, 0x75, 0x6c, 0xbb, 0x2b, 0x2e, 0x98, 0xd1, 0xc3, 0xf2, 0x1f, 0x00, 0xb5, + 0x8d, 0xfb, 0x4a, 0xb9, 0x71, 0xdc, 0x2f, 0x7e, 0x24, 0x50, 0x2d, 0x00, 0x4d, 0xcc, 0x56, 0x2d, 0x04, 0x4c, 0xa3, + 0xc9, 0x06, 0x73, 0xa0, 0x44, 0xc9, 0x42, 0xfb, 0x20, 0xfa, 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, 0x05, 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, + 0xa1, 0x10, 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, 0xfa, 0x1a, 0xe4, 0x8f, 0x94, 0xb7, 0xf7, 0xf8, 0x3b, + 0xa0, 0xe4, 0x36, 0x78, 0x57, 0x1b, 0xfb, 0xb8, 0x6b, 0xdd, 0x10, 0x20, 0x87, 0x1a, 0x1d, 0x99, 0x87, 0x13, 0xbb, + 0x48, 0x1f, 0x92, 0x76, 0x0b, 0x82, 0xa5, 0xed, 0xa0, 0x7c, 0x3f, 0x6d, 0xc0, 0x54, 0x27, 0xb7, 0x4d, 0xa0, 0xd5, + 0xf0, 0x96, 0xd2, 0x5d, 0x93, 0x27, 0x77, 0x58, 0x05, 0x38, 0xc3, 0x0e, 0x59, 0x43, 0x1c, 0x0a, 0xe4, 0x82, 0xcc, + 0xda, 0x0d, 0xa0, 0xa9, 0xe8, 0xd8, 0x37, 0xfd, 0xbc, 0x71, 0xd4, 0x45, 0x33, 0x39, 0x3d, 0xfc, 0x7a, 0x70, 0x10, + 0xcb, 0x06, 0x79, 0x84, 0xf0, 0x92, 0x82, 0xcd, 0x36, 0xf8, 0xb8, 0x71, 0xcb, 0xc4, 0xa7, 0x2a, 0xa0, 0x8e, 0x0b, + 0x55, 0x3b, 0xd6, 0xaa, 0xce, 0xca, 0xdd, 0xe0, 0xc7, 0xd4, 0x41, 0x8d, 0x20, 0xcd, 0x8e, 0xae, 0x13, 0x42, 0xf9, + 0xb7, 0x9a, 0x43, 0x1a, 0x6c, 0xcb, 0xc6, 0x47, 0x8a, 0x7e, 0x78, 0xd4, 0xfc, 0x1a, 0x94, 0xa9, 0x99, 0x26, 0x3d, + 0x6a, 0x3c, 0x42, 0x3f, 0x3c, 0x0a, 0x5c, 0x0a, 0x79, 0xc5, 0x9e, 0x78, 0x6e, 0xe4, 0x37, 0xcb, 0x95, 0xfe, 0x06, + 0x92, 0x7d, 0x41, 0x7e, 0x03, 0x2c, 0xa7, 0xe4, 0xb7, 0x58, 0x36, 0x21, 0xd4, 0x22, 0xf9, 0x2d, 0x2e, 0xe0, 0x47, + 0x4e, 0x7e, 0x8b, 0x01, 0xdb, 0xf1, 0xd4, 0xfc, 0x28, 0x4a, 0x60, 0x80, 0x1b, 0x9b, 0xb4, 0xde, 0x6c, 0xc5, 0x6a, + 0x25, 0x0e, 0x0e, 0xa4, 0xfd, 0x45, 0x2f, 0xb3, 0x83, 0x83, 0xfc, 0x62, 0x1a, 0xd8, 0xde, 0xea, 0x5d, 0xf4, 0xc5, + 0x20, 0x14, 0x0e, 0x4c, 0xd3, 0x78, 0x0d, 0xaf, 0x6a, 0x94, 0x15, 0x1a, 0x68, 0x1e, 0x77, 0xee, 0x9f, 0x9d, 0x63, + 0xf8, 0xf7, 0x7e, 0x50, 0xf0, 0xe7, 0x92, 0xef, 0x22, 0x6d, 0xd6, 0x3c, 0xab, 0xea, 0x5c, 0x06, 0xf8, 0x8c, 0x19, + 0x6a, 0x8a, 0x83, 0x03, 0x7e, 0x11, 0xe0, 0x32, 0x66, 0xa8, 0x11, 0x58, 0xec, 0x3d, 0x2c, 0xed, 0xc9, 0x0c, 0xd7, + 0x04, 0x8f, 0xe8, 0xf2, 0x7e, 0x31, 0xb8, 0xd0, 0x8e, 0x9a, 0x84, 0xa1, 0xb6, 0x15, 0x69, 0xb9, 0x4d, 0xd6, 0x15, + 0x4d, 0x75, 0xd9, 0xee, 0x22, 0x49, 0x54, 0x43, 0x5c, 0x5e, 0xb6, 0x31, 0xa8, 0xe4, 0x7b, 0x8a, 0xc8, 0x54, 0x10, + 0xef, 0x0e, 0xb8, 0xcc, 0x65, 0xaa, 0xf0, 0x94, 0xa7, 0xc2, 0xcb, 0xd9, 0xaf, 0xbd, 0xf5, 0xb4, 0x71, 0xd0, 0x34, + 0x3d, 0x33, 0x2c, 0x7a, 0xaa, 0x74, 0x2c, 0x84, 0x4d, 0xaa, 0x06, 0xf0, 0x46, 0x61, 0x89, 0x79, 0xcc, 0x7a, 0xd3, + 0x31, 0x88, 0x01, 0xad, 0x1a, 0x6d, 0xc8, 0x84, 0xcf, 0x75, 0xaa, 0x60, 0xa0, 0xa6, 0xf0, 0x05, 0x90, 0xa9, 0xac, + 0x32, 0xcc, 0xf6, 0x0d, 0x43, 0x01, 0x01, 0x05, 0x2e, 0x09, 0x0b, 0x24, 0x78, 0xb8, 0xfd, 0x08, 0x08, 0x47, 0x9d, + 0x5c, 0xd8, 0xc9, 0x5d, 0x28, 0xe8, 0x4e, 0x0c, 0x2e, 0x74, 0x17, 0x89, 0x46, 0xc3, 0x71, 0xdb, 0x97, 0xc2, 0x0c, + 0xa2, 0xd9, 0x1e, 0x5c, 0xb2, 0x2e, 0x52, 0xcd, 0x66, 0x69, 0x00, 0x79, 0xd9, 0x5a, 0xad, 0xd4, 0x85, 0x6f, 0xa4, + 0xe7, 0xcf, 0x71, 0xc3, 0x77, 0x79, 0xc1, 0xf3, 0x37, 0x49, 0xfa, 0x11, 0x50, 0x55, 0xe0, 0xb3, 0xe5, 0x3c, 0xc2, + 0x91, 0x79, 0xbe, 0x0e, 0xfe, 0x9a, 0x67, 0xc7, 0x22, 0x1c, 0xb9, 0x17, 0xed, 0xa2, 0x41, 0x35, 0x58, 0x9e, 0x95, + 0xc1, 0xd8, 0x79, 0x72, 0x0d, 0x8c, 0x83, 0xfe, 0x5b, 0xa1, 0x65, 0xf5, 0x3b, 0xc9, 0x5d, 0x58, 0x12, 0xe5, 0x1f, + 0x59, 0x73, 0xa3, 0x5a, 0xef, 0x76, 0x04, 0xe5, 0x38, 0xf2, 0x55, 0xe1, 0xb1, 0x82, 0xef, 0xbc, 0xf2, 0xd8, 0x76, + 0x8f, 0x8b, 0x2f, 0xcb, 0x1e, 0x80, 0xf3, 0x5e, 0xaf, 0x11, 0xfe, 0x4d, 0xee, 0x7c, 0x69, 0x38, 0xba, 0x96, 0xe2, + 0x09, 0xd5, 0x34, 0x6a, 0xbc, 0x31, 0x86, 0x6f, 0x56, 0xce, 0xea, 0x7e, 0x6b, 0x1c, 0xec, 0xdf, 0xea, 0x1e, 0x02, + 0x45, 0xd4, 0x1e, 0x45, 0xb2, 0xb2, 0xaf, 0x09, 0x0f, 0x22, 0x03, 0xd3, 0xb7, 0x1d, 0xf0, 0xf0, 0x63, 0xa4, 0xe0, + 0xbe, 0x6c, 0xf9, 0x24, 0x0a, 0x11, 0x58, 0x6b, 0x4e, 0xd3, 0x90, 0x62, 0xfb, 0x30, 0x0e, 0xb6, 0x6b, 0x14, 0x72, + 0xdd, 0x63, 0x55, 0x27, 0xa6, 0x55, 0x37, 0x46, 0xea, 0x60, 0x9b, 0x2c, 0x38, 0xab, 0x7a, 0x37, 0x12, 0x4a, 0xf5, + 0x7e, 0x9c, 0x79, 0x03, 0xb4, 0xd9, 0x36, 0x8f, 0x2a, 0xdb, 0x57, 0xe6, 0x14, 0x18, 0xf2, 0xee, 0x97, 0xc1, 0xb1, + 0x2e, 0xe1, 0xd8, 0x8d, 0x03, 0xc8, 0x4a, 0x72, 0xb9, 0x74, 0x2f, 0xc0, 0xf1, 0xbe, 0x1c, 0xac, 0xcb, 0xf7, 0xe0, + 0x02, 0x3c, 0xa8, 0x46, 0x2a, 0xb2, 0x90, 0x33, 0xf0, 0x8f, 0x29, 0xd6, 0xf4, 0x43, 0xfc, 0x2b, 0x1c, 0xf0, 0x15, + 0x92, 0xa6, 0x56, 0xfd, 0x04, 0xef, 0x34, 0x81, 0xc2, 0xdb, 0xd6, 0xfd, 0x53, 0x86, 0x0e, 0xb1, 0x75, 0x9d, 0x8a, + 0xf5, 0x39, 0xad, 0x2b, 0x56, 0xca, 0xc2, 0x01, 0xd5, 0x8a, 0xd1, 0x3a, 0x75, 0xfe, 0xa9, 0xee, 0x71, 0xa7, 0x87, + 0x02, 0x7c, 0x63, 0xb8, 0x14, 0xcf, 0x0a, 0x88, 0xd6, 0x15, 0xea, 0xd3, 0x7e, 0x96, 0xe1, 0xeb, 0xc5, 0x7d, 0xb8, + 0x27, 0x2c, 0x79, 0xce, 0xf2, 0xc5, 0x6d, 0x58, 0x20, 0x05, 0x14, 0x4a, 0x61, 0xb1, 0x5a, 0xc5, 0x02, 0x02, 0x36, + 0xfc, 0xe9, 0x42, 0xf8, 0xba, 0xb7, 0x3a, 0x8c, 0xfe, 0x0e, 0xea, 0x62, 0xaf, 0x1e, 0x31, 0x26, 0xac, 0x28, 0xbc, + 0x74, 0x52, 0x59, 0xd0, 0xd7, 0xae, 0x3e, 0x44, 0x35, 0xe5, 0x5e, 0x6c, 0xf4, 0xbd, 0xef, 0xf8, 0x8c, 0xc9, 0x05, + 0x3c, 0x92, 0x84, 0x19, 0x51, 0x4c, 0xfb, 0x6f, 0xa0, 0x20, 0xf0, 0xd2, 0x0e, 0x0f, 0xf1, 0x11, 0xf8, 0x2a, 0x4f, + 0xeb, 0x64, 0xe6, 0x9f, 0xde, 0x88, 0x4c, 0x68, 0xcc, 0xa8, 0x17, 0x81, 0x17, 0x11, 0x88, 0x50, 0x84, 0x44, 0x4c, + 0x8c, 0xa2, 0x5e, 0x64, 0x5c, 0xb2, 0x22, 0xb0, 0x1a, 0x03, 0x25, 0x77, 0x84, 0xe7, 0xaa, 0x22, 0x62, 0x61, 0x4d, + 0x1d, 0x54, 0x62, 0xa9, 0x31, 0xd3, 0x3e, 0xea, 0x54, 0x20, 0x2c, 0xb2, 0x4d, 0x41, 0x59, 0x6f, 0xa8, 0x0b, 0xb0, + 0x24, 0xc6, 0xf4, 0x96, 0x27, 0xd7, 0xc0, 0xcd, 0xb1, 0x91, 0x2b, 0xba, 0xe4, 0x57, 0xa0, 0x9e, 0x4e, 0x0b, 0x7c, + 0x6d, 0x18, 0xb6, 0x51, 0x4a, 0xd7, 0x84, 0xe3, 0x8c, 0x14, 0x09, 0xbd, 0x85, 0x18, 0x16, 0x33, 0x2e, 0xd2, 0x1c, + 0xcf, 0xe8, 0x6d, 0x3a, 0xc5, 0x33, 0x2e, 0x9e, 0xd8, 0x65, 0x4f, 0x47, 0x90, 0xe4, 0x3f, 0x16, 0x6b, 0x62, 0x9e, + 0xe0, 0xfa, 0x5d, 0xb1, 0xe2, 0x11, 0xf0, 0x2a, 0x2a, 0x46, 0xdd, 0x91, 0xb1, 0x29, 0xe7, 0xba, 0x32, 0x5e, 0x7f, + 0xad, 0x63, 0x8a, 0x33, 0x9c, 0xa3, 0x24, 0x97, 0x98, 0xf5, 0x44, 0xfa, 0x1a, 0xe2, 0x57, 0x67, 0xd8, 0x3e, 0xdf, + 0xc5, 0x6f, 0x59, 0xfe, 0x4c, 0x16, 0xef, 0xcd, 0x96, 0xcf, 0x11, 0x14, 0x02, 0x17, 0x15, 0xd1, 0x84, 0xdb, 0xbd, + 0x45, 0x4f, 0x56, 0x4d, 0xd1, 0x5b, 0xdb, 0x94, 0x1b, 0xe2, 0x14, 0x02, 0xff, 0x26, 0x53, 0xde, 0x68, 0x63, 0xd6, + 0x6b, 0x7d, 0xa7, 0xd1, 0x29, 0x2a, 0x4b, 0x22, 0x0c, 0x6b, 0xd5, 0x54, 0xa9, 0x24, 0xa2, 0xa9, 0x9c, 0x84, 0xb7, + 0x34, 0xc0, 0x4e, 0x15, 0xce, 0xe4, 0x42, 0xe8, 0x54, 0x06, 0x78, 0x43, 0xab, 0xcd, 0xb5, 0xbc, 0xb5, 0x10, 0xd3, + 0xf8, 0xce, 0xfe, 0x60, 0xf8, 0xda, 0xa8, 0xf8, 0xdf, 0x82, 0x61, 0x8f, 0x4a, 0x05, 0xc0, 0x0f, 0x0c, 0x67, 0x01, + 0x72, 0x96, 0x9f, 0xbc, 0x05, 0xf0, 0x59, 0x16, 0xf2, 0x0e, 0x52, 0x99, 0x49, 0xbd, 0x83, 0x54, 0x06, 0xa9, 0xc6, + 0x73, 0x7d, 0x5f, 0x54, 0xca, 0xa2, 0xb0, 0x41, 0xa2, 0x70, 0xa9, 0x0e, 0x96, 0x44, 0x24, 0xd0, 0xae, 0x11, 0xe5, + 0x66, 0x5c, 0x40, 0x08, 0x43, 0x68, 0xdc, 0x7e, 0xd3, 0x5b, 0xf8, 0xbe, 0xb3, 0xf9, 0xcc, 0xe7, 0xdf, 0xd9, 0x7c, + 0xd3, 0x91, 0xc7, 0xf8, 0xfa, 0x6d, 0xa7, 0xb1, 0x8c, 0x97, 0x0e, 0x6b, 0x3f, 0x94, 0x0f, 0xc6, 0xb4, 0xcc, 0xc3, + 0xdc, 0xa4, 0x8d, 0x27, 0x01, 0x52, 0x36, 0x2b, 0x1e, 0xae, 0x83, 0xdb, 0xad, 0xc3, 0x98, 0x37, 0x49, 0x1b, 0xa1, + 0x43, 0x27, 0x5c, 0x89, 0xd8, 0x48, 0x4e, 0x87, 0x8f, 0x8e, 0xe0, 0xee, 0x65, 0xa6, 0x36, 0x7c, 0xa5, 0x6c, 0xb5, + 0x66, 0xbb, 0x75, 0xc8, 0x77, 0x56, 0x69, 0xb4, 0xf1, 0x8c, 0x91, 0x25, 0x78, 0xa0, 0xd1, 0xc2, 0xaa, 0x1a, 0xc0, + 0x65, 0xf5, 0x85, 0xf8, 0x6d, 0x41, 0x47, 0xe6, 0xfb, 0xd0, 0xa6, 0xbc, 0x5e, 0x68, 0x9f, 0xd4, 0xe4, 0x30, 0x88, + 0x0e, 0x72, 0x25, 0x83, 0x9c, 0x98, 0x1f, 0x91, 0xe4, 0x14, 0x5d, 0xb4, 0x7b, 0xc9, 0xe9, 0x21, 0x3f, 0xe4, 0x29, + 0xf0, 0xb0, 0x71, 0xd3, 0x57, 0x68, 0xb6, 0x7d, 0x9d, 0xc7, 0x8b, 0x21, 0xcf, 0x5c, 0xf3, 0x55, 0x07, 0x65, 0xaa, + 0x9d, 0x23, 0x64, 0x01, 0x8a, 0xf9, 0x5e, 0x82, 0xec, 0x7a, 0x37, 0x87, 0x3c, 0x85, 0x7e, 0xa0, 0x56, 0xc7, 0xd6, + 0x2a, 0x07, 0xf7, 0xdb, 0x02, 0x10, 0xcc, 0x77, 0x54, 0x9b, 0x8b, 0x4d, 0x6f, 0xc6, 0x55, 0x67, 0x87, 0xbc, 0x1a, + 0x61, 0x58, 0x66, 0xbb, 0x3f, 0x3f, 0xb5, 0xaa, 0xcb, 0xc3, 0x00, 0x22, 0xbf, 0x2d, 0xb8, 0x08, 0x3b, 0x0d, 0xbb, + 0x75, 0x39, 0x61, 0xa7, 0xf5, 0x59, 0x06, 0x45, 0xb6, 0x7b, 0xdd, 0x9a, 0x69, 0x7d, 0xb6, 0x57, 0xe0, 0x48, 0x08, + 0x93, 0x32, 0x2b, 0x9d, 0xc1, 0x15, 0xfa, 0xe1, 0x07, 0xe4, 0x5a, 0x7f, 0xbd, 0xd0, 0x3e, 0xbf, 0x44, 0x04, 0xc8, + 0xae, 0xba, 0x2e, 0xab, 0x43, 0x1f, 0x65, 0x13, 0x5f, 0x0f, 0x79, 0xb0, 0x72, 0x4f, 0x6f, 0xe7, 0x32, 0xf5, 0xf8, + 0xda, 0x6b, 0xa5, 0x5b, 0xc8, 0x09, 0xc4, 0xc3, 0x75, 0x17, 0x96, 0x05, 0x39, 0xbb, 0xb9, 0x85, 0x92, 0xe1, 0xc4, + 0x7d, 0xe9, 0x0f, 0xcc, 0x5e, 0x37, 0xf0, 0x8b, 0xe4, 0x14, 0xa6, 0xbe, 0xd9, 0xc3, 0x61, 0x07, 0xfa, 0x30, 0x70, + 0xd8, 0x6c, 0xd0, 0x67, 0x56, 0x10, 0x79, 0xcc, 0x0b, 0x8b, 0x67, 0x97, 0xa4, 0xdd, 0xe3, 0xa9, 0xdb, 0x4c, 0x46, + 0x34, 0x6a, 0x37, 0x79, 0x30, 0x33, 0xc0, 0x2f, 0x57, 0x36, 0x2c, 0xe2, 0xd7, 0x29, 0x80, 0x92, 0x2f, 0x56, 0xad, + 0x4f, 0x05, 0xaf, 0x7a, 0xc3, 0xe9, 0x66, 0xba, 0x5f, 0x37, 0xb8, 0xdd, 0xf5, 0xf0, 0x84, 0x07, 0x5f, 0x2c, 0x5a, + 0xfb, 0x89, 0x4f, 0x80, 0x03, 0x4a, 0x5a, 0xf7, 0x4f, 0xc1, 0x85, 0xb2, 0x84, 0xe5, 0x76, 0xb9, 0xd9, 0x56, 0x39, + 0x0b, 0x47, 0x5b, 0x32, 0xe0, 0x0e, 0x36, 0x21, 0x0a, 0x1d, 0x1c, 0x76, 0x70, 0xd2, 0x6e, 0x77, 0x4e, 0x71, 0x72, + 0x72, 0x0a, 0x03, 0x6d, 0x24, 0xa7, 0x87, 0x33, 0x65, 0x01, 0x18, 0xe4, 0xac, 0x5d, 0xbb, 0x8f, 0x20, 0x38, 0x54, + 0x28, 0x5e, 0xf3, 0xc3, 0x38, 0x6e, 0x27, 0xf7, 0x5b, 0xed, 0xd3, 0xf3, 0x06, 0x00, 0xa8, 0xe9, 0x3e, 0x5c, 0x8d, + 0xd7, 0x0b, 0x5d, 0xaf, 0x52, 0x22, 0x7c, 0xbd, 0x5a, 0xc3, 0x57, 0x6b, 0xb4, 0xd7, 0xd5, 0x14, 0x7c, 0x55, 0x27, + 0x9c, 0xdb, 0x22, 0x5e, 0x69, 0x13, 0x6e, 0x8b, 0xd8, 0x0e, 0x24, 0x06, 0xe9, 0x3c, 0x39, 0xed, 0x9c, 0x22, 0x3b, + 0x16, 0xed, 0xf0, 0xa3, 0xdc, 0x27, 0x5b, 0x45, 0x1a, 0x1a, 0x90, 0xa4, 0x9c, 0x9d, 0x5c, 0x80, 0x44, 0xcd, 0xc9, + 0x65, 0xbb, 0x39, 0x63, 0x89, 0x9f, 0x80, 0x49, 0x85, 0xe5, 0x2c, 0x57, 0xc1, 0x25, 0x05, 0x80, 0xb8, 0x00, 0xe3, + 0xa2, 0xfb, 0xa7, 0xbd, 0xfb, 0xc9, 0xe9, 0x59, 0xc7, 0x12, 0x3d, 0x7e, 0xd1, 0xa9, 0xa5, 0x99, 0xa9, 0x27, 0xa7, + 0x26, 0x0d, 0xba, 0x4e, 0xee, 0x9f, 0x42, 0x19, 0x97, 0x12, 0x96, 0x82, 0xa0, 0x16, 0x55, 0x31, 0x88, 0x64, 0x91, + 0xd6, 0x72, 0xcf, 0x6a, 0xd9, 0xe7, 0x27, 0xc7, 0xf7, 0x4f, 0x43, 0xa8, 0x95, 0xb3, 0x30, 0x0b, 0xed, 0x26, 0xe2, + 0x67, 0x07, 0x4b, 0x8b, 0x0e, 0x93, 0xd3, 0x74, 0x6b, 0x82, 0x76, 0xd3, 0x1c, 0x1a, 0x1c, 0x08, 0x14, 0x8e, 0x4f, + 0x85, 0xd3, 0x97, 0x04, 0xf7, 0x63, 0x95, 0xa1, 0x49, 0xa8, 0x70, 0xf6, 0xf7, 0x94, 0xc1, 0xbb, 0x95, 0xe1, 0x55, + 0xe5, 0x63, 0x2a, 0xbe, 0x50, 0xf5, 0x86, 0x42, 0xa4, 0x0e, 0x31, 0x88, 0x5c, 0x1c, 0xf1, 0x7a, 0xee, 0x4f, 0xe0, + 0x22, 0xcc, 0x04, 0x5c, 0x68, 0x7a, 0x25, 0x68, 0xc5, 0x0b, 0x0c, 0x43, 0x87, 0x5a, 0x33, 0xac, 0x1e, 0x4f, 0x9d, + 0x49, 0x41, 0xa8, 0xdb, 0x7a, 0xce, 0xbf, 0x57, 0x2e, 0x29, 0xaf, 0xb2, 0x93, 0x53, 0x94, 0xb8, 0xcb, 0xf2, 0xa4, + 0x8d, 0x92, 0xc0, 0x84, 0xc4, 0x1d, 0xc9, 0x59, 0x46, 0xfa, 0xd1, 0x6d, 0x84, 0xa3, 0xbb, 0x08, 0x47, 0xd6, 0x87, + 0xf9, 0x03, 0xf8, 0x71, 0x47, 0x38, 0xb2, 0xae, 0xcc, 0x11, 0x8e, 0x34, 0x13, 0x10, 0xc0, 0x2b, 0x1a, 0xe0, 0x1c, + 0x4a, 0x1b, 0xcf, 0xea, 0xb2, 0xf4, 0x63, 0xff, 0x55, 0xba, 0x5e, 0xdb, 0x94, 0x40, 0xca, 0x9c, 0x9a, 0x1d, 0x6a, + 0x1f, 0xa0, 0x8e, 0xa8, 0x67, 0xd6, 0x23, 0x0c, 0x02, 0x08, 0xbd, 0xf3, 0x0f, 0xd8, 0x55, 0xb1, 0x3f, 0xd8, 0x31, + 0xac, 0x34, 0xb8, 0xa2, 0x47, 0xe1, 0x19, 0x16, 0xe1, 0xb1, 0xf0, 0x85, 0x41, 0xac, 0xf0, 0xbf, 0x73, 0x29, 0xe7, + 0xfe, 0xb7, 0x96, 0xe5, 0x2f, 0x78, 0xf6, 0xc4, 0x59, 0xb4, 0x80, 0xe5, 0x96, 0x0d, 0x35, 0x34, 0x64, 0xf5, 0x11, + 0x5c, 0x8f, 0x5d, 0x38, 0x38, 0x90, 0x08, 0xaf, 0x8d, 0x40, 0xe5, 0xe5, 0xc3, 0x6b, 0x1b, 0x9a, 0xc8, 0x7c, 0x42, + 0x6c, 0x32, 0x08, 0x3f, 0x2c, 0xe1, 0x42, 0x63, 0x52, 0x30, 0xa5, 0x22, 0x1b, 0xb3, 0x2f, 0x92, 0xc2, 0x3f, 0xc2, + 0xe8, 0x53, 0xc6, 0x22, 0x32, 0x1d, 0xd6, 0x67, 0x6b, 0xc5, 0xe1, 0x5c, 0x16, 0x2a, 0xb5, 0x2f, 0xb2, 0x78, 0x30, + 0xce, 0xcb, 0xe7, 0x0e, 0xd3, 0x3c, 0x5b, 0x63, 0x7b, 0x87, 0x5d, 0x16, 0x72, 0x57, 0xda, 0x61, 0xa9, 0x2c, 0x5b, + 0x7f, 0x6b, 0x42, 0xaa, 0x36, 0xa3, 0x60, 0xa2, 0xd5, 0x80, 0xaa, 0xc0, 0x1d, 0x50, 0xd8, 0x06, 0x7f, 0x49, 0x97, + 0x65, 0xc9, 0x74, 0x59, 0x2e, 0xc3, 0x49, 0xab, 0xb5, 0x5e, 0xe3, 0x82, 0x99, 0x20, 0x34, 0x3b, 0x4b, 0x40, 0xbe, + 0x9a, 0xca, 0x9b, 0x20, 0x57, 0xa5, 0xe5, 0x2c, 0xcd, 0x12, 0x45, 0x81, 0x11, 0x6c, 0xb4, 0xc6, 0x5f, 0xb8, 0xe2, + 0x00, 0x4f, 0x37, 0xbb, 0xa1, 0x94, 0x39, 0xa3, 0x10, 0xab, 0x2c, 0x68, 0x72, 0x8d, 0xa7, 0x7c, 0xc4, 0x76, 0xb7, + 0x09, 0x66, 0xcc, 0xff, 0x5e, 0x8b, 0x1e, 0x81, 0x2c, 0xbb, 0x67, 0x50, 0x07, 0x16, 0x71, 0x05, 0x1d, 0x84, 0x32, + 0xf8, 0x28, 0xc4, 0xcd, 0x9c, 0xde, 0xc9, 0x85, 0x06, 0xb8, 0x2c, 0xb4, 0x7c, 0xe3, 0xc2, 0x21, 0xec, 0xb7, 0xb0, + 0x8f, 0x8c, 0xb0, 0x84, 0x90, 0x01, 0x2d, 0x6c, 0x23, 0x62, 0xb4, 0xb0, 0x0b, 0x54, 0xd0, 0xc2, 0x26, 0x3c, 0x45, + 0x6b, 0x5d, 0xc6, 0x10, 0xbb, 0x2e, 0x9f, 0xae, 0xac, 0x36, 0xc1, 0xc2, 0x49, 0x87, 0x9a, 0xe8, 0xe0, 0xf6, 0x90, + 0x11, 0xde, 0xf8, 0xf9, 0xea, 0xf5, 0x2b, 0x17, 0x21, 0x9a, 0x8f, 0xc1, 0x65, 0xd3, 0xa9, 0xc6, 0xae, 0xcd, 0x9b, + 0x4f, 0x71, 0xa5, 0x28, 0xb5, 0xc2, 0x29, 0xb4, 0xfc, 0x42, 0xe8, 0x3c, 0xb1, 0x97, 0x17, 0xcf, 0x64, 0x31, 0xa3, + 0xf6, 0xc6, 0x08, 0x5f, 0x2b, 0xf7, 0xc8, 0xbb, 0x79, 0x47, 0xa6, 0x9a, 0xe4, 0xbb, 0xcd, 0xab, 0x88, 0x45, 0x66, + 0xe4, 0x57, 0xd0, 0x06, 0x98, 0xca, 0xe5, 0x1b, 0xbd, 0x05, 0x71, 0x71, 0xf6, 0x03, 0xf2, 0xf2, 0xd6, 0x52, 0x97, + 0x28, 0x6a, 0x70, 0x83, 0x9f, 0xac, 0xe0, 0x59, 0x70, 0x5d, 0x68, 0xd8, 0x23, 0x27, 0x5e, 0x44, 0xad, 0xa8, 0xfe, + 0xc6, 0xad, 0x51, 0x25, 0xf8, 0x18, 0xad, 0x49, 0x2e, 0x41, 0xf4, 0x28, 0x9f, 0xd3, 0xe3, 0x20, 0x9a, 0xf8, 0xbb, + 0xe7, 0xcb, 0xb6, 0xa7, 0xb3, 0x79, 0xa5, 0x4e, 0x2c, 0xaf, 0x4c, 0xc0, 0xc3, 0xd1, 0x3e, 0x58, 0x83, 0x70, 0x90, + 0xc8, 0x4a, 0xed, 0xa1, 0xcf, 0x45, 0xdd, 0x38, 0xbf, 0x68, 0xb3, 0xe6, 0xc9, 0x6a, 0x95, 0x5f, 0xb6, 0x59, 0xfb, + 0xd4, 0x3e, 0x6f, 0x17, 0xa9, 0x0c, 0x68, 0x2e, 0x1f, 0xf3, 0x2c, 0x02, 0xed, 0xec, 0x38, 0x33, 0xe1, 0x14, 0x7c, + 0x40, 0x66, 0xb2, 0xd0, 0x55, 0x5f, 0x12, 0x8c, 0x4b, 0x89, 0xd5, 0xe3, 0x17, 0xa8, 0xd7, 0x4e, 0xb7, 0x5d, 0xa5, + 0x9b, 0xed, 0xc3, 0xe0, 0xc2, 0xa5, 0x40, 0xb8, 0x03, 0x21, 0x0f, 0x40, 0xbf, 0xbb, 0x14, 0x60, 0x1a, 0x04, 0xa8, + 0xac, 0x40, 0xa4, 0xe5, 0xb3, 0xc5, 0xec, 0x59, 0x41, 0xcd, 0x32, 0x3c, 0xe1, 0x13, 0xae, 0x55, 0x4a, 0x41, 0xba, + 0xdd, 0x95, 0xbe, 0xde, 0x2d, 0x41, 0x65, 0xb5, 0x38, 0xb7, 0x89, 0xe6, 0xd9, 0x67, 0xe5, 0x16, 0x0e, 0x61, 0xb3, + 0xb2, 0x02, 0x67, 0x68, 0x8d, 0x73, 0x39, 0xa1, 0x05, 0xd7, 0xd3, 0xd9, 0xbf, 0xb5, 0x3a, 0xac, 0xaf, 0x07, 0xe6, + 0xc2, 0x0a, 0x40, 0x42, 0xc5, 0x68, 0xb5, 0xe2, 0x47, 0xdf, 0xbf, 0x4f, 0xf2, 0x3e, 0xe1, 0x6d, 0xdc, 0xc1, 0xc7, + 0xf8, 0x14, 0xb7, 0x5b, 0xb8, 0x7d, 0x0a, 0x57, 0xf7, 0x59, 0xbe, 0x18, 0x31, 0x15, 0xc3, 0x3b, 0x67, 0xfa, 0x32, + 0x39, 0x3f, 0x2c, 0xa3, 0xfb, 0xeb, 0x22, 0x71, 0xe8, 0x12, 0x04, 0x99, 0x77, 0xd1, 0xf9, 0xa2, 0x28, 0x0c, 0x0d, + 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, 0x7e, 0x5d, 0x48, 0x2a, 0x8c, 0x10, 0x0d, 0x47, 0x98, 0x0b, 0x86, 0xb2, 0xdf, 0xc2, + 0x72, 0x3c, 0x56, 0x4c, 0xc3, 0xd1, 0x51, 0xb0, 0x2f, 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, 0x8c, 0xc4, 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, 0x28, 0x0d, 0xca, 0xb7, 0x31, 0xdc, 0xc3, 0xc2, 0x2b, 0x99, 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, 0x47, 0x3f, 0x7d, 0x7b, 0x57, 0xba, 0x90, 0x9f, 0x1d, 0x48, 0x5a, 0x41, 0x8a, + 0x9d, 0x4e, 0xd0, 0xd9, 0x31, 0x0e, 0x46, 0x0e, 0xf4, 0xfc, 0xea, 0xb3, 0x85, 0xb5, 0xff, 0xfd, 0xa6, 0x2c, 0x68, + 0xe2, 0xe9, 0x94, 0x13, 0xca, 0xfc, 0xf9, 0xf9, 0x86, 0x27, 0x15, 0x2a, 0xb8, 0xd7, 0xb2, 0x60, 0x4f, 0xdb, 0x80, + 0x9a, 0x33, 0xfa, 0xb7, 0xfd, 0x61, 0x63, 0xf8, 0x94, 0x5a, 0xb6, 0xac, 0x90, 0x4a, 0x3d, 0xb4, 0x69, 0xf6, 0xe8, + 0x81, 0x23, 0xf2, 0x25, 0x74, 0x01, 0xbc, 0xfe, 0xa8, 0x90, 0x73, 0x83, 0x08, 0xee, 0xb7, 0x1b, 0xb7, 0xf1, 0x15, + 0x00, 0x6f, 0x87, 0xbd, 0xea, 0x9f, 0x16, 0xb0, 0xbf, 0x51, 0x59, 0xd2, 0x8f, 0xb7, 0x63, 0x8f, 0xff, 0x42, 0x42, + 0x74, 0x76, 0x8b, 0x87, 0x89, 0x43, 0xa7, 0x92, 0x35, 0x2b, 0x7f, 0x6e, 0x95, 0x04, 0x0c, 0xab, 0x17, 0x0c, 0xd9, + 0xb8, 0xad, 0xe2, 0x36, 0xf3, 0x3f, 0xa8, 0x60, 0xb0, 0xe0, 0x5b, 0x23, 0xa9, 0x58, 0x16, 0xbf, 0x7d, 0xea, 0xfc, + 0x57, 0x9d, 0xe3, 0xda, 0xd7, 0xb5, 0x17, 0x39, 0x87, 0x26, 0x1a, 0x72, 0x84, 0x0e, 0x0e, 0x36, 0x32, 0xe8, 0x18, + 0x00, 0x8f, 0x1c, 0xfb, 0xe5, 0x97, 0xcf, 0xb3, 0x63, 0x46, 0xf3, 0x58, 0x44, 0x21, 0x73, 0xe7, 0xb9, 0x39, 0x3b, + 0x91, 0x27, 0x54, 0x4d, 0x7d, 0x61, 0x80, 0xe3, 0xa3, 0xad, 0x54, 0xc0, 0xf7, 0x68, 0xbd, 0x63, 0x02, 0x1b, 0xfc, + 0x96, 0x9d, 0xd4, 0xae, 0x82, 0x7e, 0x81, 0x96, 0xbb, 0x98, 0xca, 0x8d, 0x05, 0x8e, 0x36, 0x27, 0xb2, 0x73, 0xe8, + 0x1b, 0x75, 0x4a, 0xd6, 0xe3, 0xc9, 0x6e, 0xa3, 0x2f, 0x29, 0x76, 0x25, 0x57, 0xb4, 0x6d, 0xc8, 0xaa, 0x57, 0x79, + 0x75, 0x65, 0xea, 0x54, 0x5d, 0xf3, 0x56, 0x96, 0x36, 0xa5, 0x5d, 0x92, 0xbd, 0xdb, 0x62, 0xe1, 0x55, 0x78, 0xa3, + 0x51, 0x5e, 0x84, 0x82, 0x3d, 0x96, 0x18, 0x74, 0x39, 0x81, 0xeb, 0x85, 0xd5, 0x2a, 0x86, 0x3f, 0xbb, 0xc6, 0xb0, + 0xcb, 0x74, 0xe9, 0x03, 0xdf, 0xe0, 0x57, 0x82, 0xc0, 0xc0, 0xce, 0x0e, 0x12, 0xac, 0xbb, 0xdc, 0xa0, 0xe1, 0x38, + 0xf1, 0x5f, 0xf0, 0x2c, 0xb5, 0xf6, 0x2e, 0x07, 0x93, 0xec, 0x1b, 0x4f, 0xd9, 0x95, 0xac, 0x65, 0x2d, 0xaa, 0xfc, + 0x86, 0x04, 0x43, 0xec, 0xa6, 0x74, 0x8e, 0x5b, 0x49, 0x1b, 0x45, 0xae, 0x58, 0x85, 0xfe, 0xdf, 0x2a, 0x92, 0xd9, + 0xcc, 0xff, 0x3a, 0x3b, 0x3b, 0x73, 0x29, 0xce, 0xe6, 0x4f, 0x19, 0x0f, 0x38, 0x93, 0xc0, 0xbe, 0xf0, 0x8c, 0x19, + 0x1d, 0xf2, 0x5b, 0x18, 0x0a, 0x11, 0xe4, 0x52, 0x38, 0x76, 0x09, 0x5e, 0x55, 0x04, 0xca, 0x03, 0xec, 0xdf, 0x93, + 0x8d, 0x72, 0xfe, 0x59, 0x26, 0x1f, 0xb6, 0xb8, 0x6c, 0x90, 0x7d, 0x31, 0x9f, 0x7d, 0x6b, 0x26, 0x03, 0x2f, 0x11, + 0x44, 0xd8, 0xfe, 0x36, 0x2c, 0xad, 0xb3, 0x94, 0xc1, 0x91, 0x96, 0x8b, 0x6c, 0x6a, 0x35, 0xff, 0xee, 0xc3, 0x94, + 0x75, 0x4f, 0xfa, 0x40, 0xe4, 0x2e, 0xb2, 0x74, 0xd1, 0x37, 0xa3, 0x1f, 0xcb, 0x40, 0x9b, 0x7b, 0xaf, 0xd8, 0x82, + 0xfd, 0x88, 0xf7, 0xaa, 0x14, 0xf8, 0x78, 0x58, 0x70, 0x9a, 0xff, 0x88, 0xf7, 0xaa, 0x80, 0x9b, 0xe0, 0x0a, 0x69, + 0x62, 0x56, 0x62, 0xf3, 0x7c, 0x75, 0x1a, 0x09, 0xa0, 0xa0, 0x79, 0x64, 0x0e, 0xb2, 0xe7, 0x2e, 0x46, 0x63, 0xd2, + 0xc1, 0x2e, 0x38, 0x98, 0x8d, 0xbc, 0x6a, 0x03, 0x96, 0x43, 0xdc, 0xba, 0x72, 0x36, 0xe6, 0xeb, 0xd1, 0xc6, 0x82, + 0x18, 0x65, 0x32, 0xb9, 0x7c, 0xce, 0xe3, 0xad, 0xc5, 0x42, 0x61, 0xb5, 0x60, 0x81, 0x6a, 0x55, 0xaa, 0xf4, 0xb0, + 0xf8, 0x76, 0xc1, 0x2c, 0x28, 0x62, 0xb6, 0xde, 0xc3, 0x5b, 0xae, 0x08, 0x48, 0xc9, 0x2e, 0x09, 0x5e, 0x20, 0x37, + 0x98, 0xea, 0x1f, 0x9c, 0x07, 0x42, 0xcf, 0x94, 0x8e, 0xb0, 0xc9, 0x53, 0x10, 0x49, 0x6c, 0xbf, 0x85, 0x1d, 0x6b, + 0xf4, 0x42, 0x78, 0x21, 0x05, 0xce, 0x55, 0xd3, 0xc4, 0x8c, 0x72, 0x13, 0x5d, 0xec, 0xa1, 0x9a, 0xb3, 0x4c, 0x5b, + 0x04, 0xd8, 0x77, 0x68, 0x28, 0xc5, 0x73, 0x03, 0x0a, 0xf3, 0x74, 0xb6, 0x4b, 0x79, 0x0c, 0x8b, 0x17, 0xa4, 0x00, + 0x51, 0xe3, 0x62, 0x52, 0xd6, 0x99, 0xe7, 0x8b, 0x09, 0x17, 0x15, 0x32, 0x14, 0x4c, 0xcd, 0xa5, 0x80, 0x97, 0x2b, + 0xca, 0x22, 0x86, 0x0e, 0xd5, 0xf0, 0xdd, 0x92, 0xb0, 0xb2, 0x8e, 0x39, 0xa6, 0xb8, 0xa8, 0x6a, 0x00, 0x73, 0xf1, + 0x30, 0x7c, 0xe2, 0x5e, 0xbd, 0x16, 0xef, 0xe4, 0xbc, 0xca, 0xf7, 0x34, 0xce, 0x07, 0x88, 0x77, 0x76, 0xc3, 0x68, + 0x6d, 0x1e, 0x97, 0x0a, 0xb6, 0xef, 0x07, 0x5e, 0x3d, 0xb8, 0xb6, 0x36, 0xcf, 0x53, 0x95, 0x59, 0x43, 0x56, 0xbe, + 0xc5, 0x50, 0xb5, 0x57, 0xaf, 0x2a, 0x85, 0xad, 0x08, 0x50, 0x29, 0xf8, 0x68, 0x2b, 0xff, 0x89, 0xb6, 0xf9, 0xf6, + 0x1c, 0x2a, 0xc3, 0x03, 0x79, 0x32, 0x54, 0xf5, 0x80, 0x8b, 0xf2, 0x43, 0x00, 0x8b, 0x1f, 0x99, 0x38, 0xbd, 0xbb, + 0x2e, 0x90, 0x39, 0x53, 0xb1, 0xc4, 0xcb, 0x3e, 0x1d, 0xa4, 0x56, 0x1e, 0x4a, 0x25, 0xd8, 0xf6, 0xdc, 0x14, 0x5c, + 0xfb, 0x80, 0xc0, 0xb8, 0xcf, 0x06, 0xe9, 0xb2, 0x1e, 0x34, 0xd8, 0x86, 0x2d, 0xf6, 0xe6, 0x9c, 0x26, 0xca, 0x2e, + 0x1d, 0xe0, 0x9c, 0x80, 0xed, 0xb1, 0x67, 0x4f, 0xdf, 0xc4, 0x19, 0xea, 0xd5, 0x39, 0xfc, 0xe5, 0x1a, 0xe7, 0x38, + 0x43, 0xe9, 0xc3, 0x18, 0x2e, 0xb0, 0xd6, 0x18, 0xc0, 0x97, 0x59, 0x52, 0x05, 0x1e, 0xa9, 0x99, 0x91, 0x58, 0xdd, + 0x45, 0x20, 0x5a, 0xea, 0xf0, 0x76, 0x9c, 0xf9, 0xb0, 0xdb, 0x86, 0x7b, 0x7d, 0x66, 0x84, 0xc3, 0x49, 0x16, 0xd7, + 0xce, 0x19, 0x4e, 0x2e, 0xf7, 0x79, 0xed, 0xc4, 0x04, 0x6b, 0xef, 0xf0, 0x54, 0x01, 0x3d, 0x1a, 0x9c, 0x2a, 0x96, + 0x86, 0x40, 0xcc, 0x04, 0xf0, 0x66, 0x0e, 0x8f, 0xb6, 0x00, 0xe7, 0xa3, 0x35, 0x0e, 0xbe, 0xd2, 0x5a, 0x57, 0x9b, + 0x4a, 0x94, 0xf5, 0x1a, 0xf7, 0xa7, 0x19, 0x1e, 0x65, 0x78, 0x9e, 0x0d, 0x82, 0xe3, 0x66, 0x96, 0x85, 0x26, 0x5d, + 0xab, 0xd5, 0x53, 0x67, 0x46, 0x88, 0xec, 0x4f, 0x4b, 0x7f, 0x50, 0x0f, 0x10, 0x3e, 0x85, 0x2c, 0xa0, 0x25, 0x3d, + 0xf7, 0xb7, 0x61, 0x5f, 0xe5, 0x46, 0x8d, 0x98, 0x27, 0x96, 0x8c, 0xf4, 0xfc, 0x8f, 0x32, 0xcb, 0xb6, 0xd6, 0x88, + 0xe6, 0xb7, 0x7b, 0x51, 0xc3, 0xb7, 0x17, 0x68, 0xd9, 0x4a, 0xb3, 0x1d, 0x40, 0x14, 0x6b, 0x9c, 0xa4, 0x83, 0x35, + 0x92, 0xab, 0x55, 0x6c, 0x53, 0x08, 0x4f, 0x66, 0x8c, 0xaa, 0x45, 0x61, 0x1e, 0xaa, 0x8b, 0x15, 0x4a, 0x0c, 0xbf, + 0x8b, 0x9d, 0x8d, 0x28, 0xbc, 0x0b, 0x27, 0xc1, 0x70, 0x23, 0x16, 0x44, 0xd6, 0x44, 0xee, 0x61, 0x56, 0x59, 0x06, + 0x09, 0x22, 0x8c, 0xc8, 0x6f, 0xaf, 0x4b, 0x85, 0x7d, 0x0a, 0xcf, 0xfe, 0x31, 0xbe, 0x80, 0x70, 0xf3, 0x36, 0xa1, + 0xc5, 0x90, 0x4e, 0x80, 0x8d, 0x85, 0x38, 0x84, 0x5b, 0x09, 0xab, 0x55, 0x7f, 0xd0, 0x15, 0x86, 0x3c, 0xbb, 0x87, + 0xfa, 0xca, 0x86, 0x76, 0x37, 0x00, 0x57, 0xdd, 0x96, 0x9a, 0x6b, 0xa3, 0xfb, 0xa1, 0xe6, 0x2d, 0x31, 0xee, 0x92, + 0xdc, 0x73, 0x20, 0xd5, 0x8b, 0xdf, 0x35, 0x0b, 0x70, 0x13, 0xba, 0x0a, 0x8f, 0xf0, 0xc2, 0xda, 0x70, 0x9a, 0x87, + 0xa3, 0xa8, 0x79, 0x2f, 0x0a, 0x9e, 0xa9, 0x26, 0xac, 0x9f, 0x0d, 0xf0, 0xc8, 0x87, 0x15, 0xdf, 0x7f, 0x1b, 0x8f, + 0x10, 0x2a, 0x88, 0x81, 0xa9, 0x75, 0xd9, 0x1e, 0x55, 0x76, 0xfb, 0x26, 0xd3, 0x30, 0x0c, 0xc6, 0x88, 0x79, 0x14, + 0x1a, 0x31, 0xe7, 0x8d, 0x06, 0x5a, 0x90, 0x11, 0x18, 0x31, 0x2f, 0x82, 0xd6, 0x16, 0xf6, 0x51, 0xd1, 0xa0, 0xbd, + 0x05, 0x42, 0x5d, 0x0e, 0x34, 0x4d, 0xc3, 0xf3, 0x21, 0xd5, 0xf3, 0xed, 0xfe, 0x31, 0xab, 0xa3, 0x0e, 0x28, 0x12, + 0xc6, 0x97, 0x7e, 0x12, 0xd6, 0x35, 0xdc, 0x8e, 0x7b, 0x6c, 0xc6, 0xed, 0x6c, 0x1b, 0x54, 0x5f, 0xf6, 0xb3, 0xc1, + 0xa0, 0x2b, 0xbd, 0x95, 0x44, 0x0b, 0x8f, 0xab, 0x07, 0x47, 0xaa, 0xc5, 0xfb, 0xa2, 0x37, 0xaf, 0xbc, 0xb9, 0x7f, + 0xc7, 0x74, 0xf3, 0x3c, 0x06, 0x0e, 0x68, 0x1f, 0xee, 0x87, 0xaa, 0xf8, 0x60, 0x47, 0x1d, 0x88, 0x82, 0x96, 0xb6, + 0x6a, 0x02, 0xa9, 0x35, 0xb3, 0x8b, 0x75, 0x53, 0xa1, 0x43, 0x01, 0x61, 0xc8, 0x54, 0xd5, 0xdd, 0x9d, 0x0a, 0x54, + 0x43, 0x1c, 0x4e, 0xfd, 0xc7, 0xd6, 0x88, 0x35, 0x8e, 0x3a, 0xa3, 0xc8, 0x18, 0x49, 0xda, 0xe5, 0x83, 0x37, 0x86, + 0xc0, 0x4a, 0xc0, 0xc7, 0x7a, 0x36, 0x49, 0xc6, 0x90, 0xe0, 0x2d, 0xcb, 0xb4, 0xe1, 0x43, 0xb8, 0x43, 0x50, 0x9e, + 0xd8, 0xa0, 0xb4, 0xae, 0x92, 0x85, 0x5c, 0xd5, 0xe5, 0x75, 0x80, 0x9e, 0x77, 0xe5, 0x6f, 0x6c, 0x38, 0xb2, 0x60, + 0x60, 0xd9, 0xd6, 0x3e, 0x01, 0x8f, 0x7c, 0x5c, 0x21, 0x88, 0x5f, 0x0a, 0x9d, 0x98, 0xb8, 0xd8, 0x57, 0xb0, 0x41, + 0xf1, 0x1c, 0x1c, 0x04, 0x9d, 0x04, 0x87, 0xc1, 0xbb, 0xcc, 0x6a, 0x92, 0x0d, 0x6e, 0xcd, 0x48, 0x3c, 0x5f, 0xad, + 0x5a, 0xe8, 0xf0, 0x1f, 0xf3, 0xf4, 0xf3, 0xb8, 0x54, 0xb8, 0x8f, 0x2b, 0x85, 0x3b, 0x58, 0x02, 0x92, 0x71, 0xa0, + 0x6b, 0xc7, 0x32, 0x54, 0xa3, 0x43, 0x54, 0xf2, 0x17, 0x10, 0xa3, 0xda, 0x1d, 0x4b, 0xa0, 0x67, 0xdf, 0x2a, 0x60, + 0x75, 0xed, 0x65, 0x09, 0x64, 0x04, 0x77, 0xbf, 0x09, 0x8c, 0x0a, 0xd1, 0xf8, 0xfc, 0x99, 0xd7, 0x23, 0x78, 0xe2, + 0xfc, 0xb9, 0x66, 0x86, 0x75, 0x2f, 0xe8, 0x8d, 0x69, 0x3e, 0x1e, 0xe3, 0xe6, 0xd8, 0x82, 0xf3, 0xa8, 0x03, 0x3f, + 0x2d, 0x44, 0x8f, 0x3a, 0xd8, 0xa5, 0xe2, 0x71, 0x09, 0xe4, 0x10, 0x3d, 0x9d, 0x81, 0x14, 0xb0, 0xd2, 0xb1, 0xd5, + 0x22, 0x4d, 0xd0, 0x6a, 0x35, 0xb9, 0x20, 0x2d, 0x84, 0x96, 0xea, 0x86, 0xeb, 0x6c, 0x0a, 0x3e, 0xd2, 0xa0, 0x18, + 0x78, 0x43, 0xf5, 0x34, 0x46, 0x78, 0x8c, 0x96, 0x23, 0x36, 0xa6, 0x8b, 0x5c, 0xa7, 0xaa, 0xc7, 0x13, 0x1b, 0xb8, + 0x97, 0xd9, 0x48, 0x70, 0x47, 0x1d, 0x3c, 0x31, 0xfc, 0xe5, 0x23, 0x63, 0x0e, 0x52, 0x64, 0x26, 0x79, 0x62, 0x12, + 0x30, 0x4f, 0xb2, 0x5c, 0x2a, 0x66, 0x9b, 0xe9, 0x5a, 0xdb, 0x72, 0x08, 0xfd, 0x1d, 0xe9, 0x82, 0x1b, 0x2b, 0xca, + 0x28, 0x9d, 0x12, 0xd5, 0x53, 0x47, 0x9d, 0x74, 0x82, 0x79, 0x02, 0x9c, 0xde, 0x3b, 0x19, 0xb3, 0x46, 0x79, 0x2b, + 0x3a, 0x43, 0x87, 0x53, 0x2c, 0xaa, 0x4b, 0xd4, 0x19, 0x3a, 0x9c, 0x20, 0x3c, 0x6b, 0x90, 0x5c, 0x81, 0xc7, 0x30, + 0x17, 0xff, 0x47, 0xca, 0x7f, 0x73, 0xd8, 0x10, 0x62, 0xfa, 0x2d, 0xec, 0x14, 0x36, 0x8a, 0xd2, 0x9c, 0x80, 0xd7, + 0x62, 0xfb, 0x0c, 0x67, 0x64, 0xd2, 0xcc, 0x7d, 0xc0, 0x3d, 0xd3, 0x4a, 0xe3, 0x56, 0xa3, 0xc3, 0x0c, 0x8f, 0x36, + 0x93, 0x62, 0x33, 0xd7, 0x66, 0x9e, 0x66, 0x70, 0xbe, 0x57, 0xa3, 0x70, 0xe5, 0x17, 0x9b, 0x49, 0x61, 0x79, 0x07, + 0xdc, 0xe6, 0x08, 0x8b, 0x26, 0xc5, 0x39, 0x9e, 0x35, 0xbf, 0xe2, 0x59, 0xf3, 0x43, 0x99, 0xd1, 0x58, 0x60, 0x01, + 0xc1, 0xfb, 0x20, 0x11, 0xcf, 0xaa, 0xe4, 0x11, 0x16, 0x0d, 0x53, 0x1e, 0xcf, 0x1a, 0x55, 0xe9, 0xe6, 0x02, 0x8b, + 0x86, 0x29, 0xdd, 0xf8, 0x80, 0x67, 0x8d, 0xaf, 0xff, 0x62, 0xd2, 0x51, 0x0a, 0xe8, 0x32, 0x47, 0xcb, 0xcc, 0x0e, + 0xf1, 0xea, 0xb7, 0xb7, 0xef, 0xda, 0xd7, 0x9d, 0xc3, 0x09, 0xf6, 0xeb, 0x97, 0x19, 0x1c, 0xcb, 0x74, 0xcc, 0x9a, + 0x00, 0xd1, 0x0c, 0x77, 0x0e, 0xa7, 0xb8, 0x73, 0x98, 0xb9, 0xa6, 0xd6, 0xb3, 0x06, 0xb9, 0xd5, 0x21, 0x14, 0x75, + 0x94, 0x86, 0xf0, 0xf1, 0x93, 0x4d, 0x27, 0xa8, 0x06, 0x4a, 0x74, 0x38, 0xa9, 0x81, 0x0a, 0xbe, 0x17, 0xb5, 0xef, + 0xaa, 0x5e, 0x85, 0x41, 0x16, 0x4a, 0x28, 0x5c, 0x73, 0x03, 0x9e, 0x5a, 0x8a, 0x81, 0x4c, 0x98, 0x62, 0x81, 0xf2, + 0x1d, 0x50, 0x18, 0xe5, 0x89, 0x19, 0x7a, 0x30, 0x1d, 0x93, 0xf8, 0xff, 0xf3, 0x64, 0xca, 0xa1, 0x97, 0x5b, 0x66, + 0x6b, 0x7a, 0x6e, 0x32, 0xe1, 0xf0, 0x81, 0xc7, 0xfa, 0xbf, 0x76, 0xa0, 0xd8, 0x80, 0x14, 0xff, 0x5f, 0x3a, 0xba, + 0x10, 0x8c, 0x90, 0x15, 0xa5, 0x85, 0x43, 0xfc, 0xef, 0x0f, 0x2b, 0xe8, 0xbe, 0xd8, 0xea, 0xbe, 0x30, 0xdd, 0x87, + 0x4d, 0x1b, 0x55, 0x4e, 0x5a, 0x55, 0xb2, 0xe4, 0xbf, 0x4e, 0xb7, 0xb6, 0x40, 0x23, 0x6a, 0xf4, 0x6c, 0x12, 0x36, + 0xb8, 0xdf, 0x4e, 0x77, 0x20, 0xf3, 0x9a, 0xdb, 0x97, 0x48, 0xe1, 0xf0, 0x0d, 0xee, 0x54, 0x2f, 0x5b, 0xe0, 0xbd, + 0xa9, 0x8c, 0xbe, 0x32, 0x0e, 0x2d, 0x07, 0xe9, 0xa6, 0x29, 0xb7, 0x31, 0x96, 0x4e, 0x4e, 0xb1, 0x71, 0x45, 0x84, + 0x4a, 0xb7, 0x97, 0xa0, 0x14, 0x1f, 0xeb, 0x26, 0x33, 0x5f, 0x17, 0x3a, 0x31, 0x97, 0x50, 0x0d, 0xf3, 0x79, 0x77, + 0xa9, 0x13, 0x2d, 0xe7, 0x36, 0xef, 0xee, 0x02, 0xfa, 0x04, 0x0d, 0x6b, 0x23, 0xb0, 0xdb, 0x67, 0x85, 0xd3, 0xef, + 0x54, 0x87, 0x60, 0x78, 0x00, 0x39, 0xd2, 0x62, 0xfb, 0xc0, 0xa6, 0x35, 0xec, 0xba, 0x68, 0x96, 0x89, 0xb6, 0xd5, + 0xa6, 0xc9, 0xb5, 0x7b, 0x98, 0xcf, 0x43, 0x9e, 0x82, 0x17, 0x56, 0x3f, 0xbe, 0x83, 0xdd, 0xb8, 0xad, 0x31, 0x12, + 0x75, 0x25, 0x53, 0x09, 0xfd, 0xe4, 0x16, 0xb3, 0xe4, 0xce, 0x78, 0x31, 0x2a, 0xe3, 0xef, 0x63, 0xe2, 0xf2, 0x47, + 0x95, 0x24, 0x07, 0x96, 0xfd, 0x0d, 0x96, 0xdc, 0x82, 0x79, 0x62, 0x59, 0x4d, 0x62, 0x9d, 0xdc, 0x05, 0x8b, 0x28, + 0x4d, 0x23, 0x6b, 0xc3, 0x80, 0x9a, 0x66, 0xac, 0x7a, 0x70, 0x1f, 0x02, 0x3d, 0xf4, 0xca, 0x52, 0xda, 0x75, 0x96, + 0xd6, 0xba, 0xd7, 0xa6, 0xfb, 0xcd, 0x01, 0x05, 0x7c, 0x61, 0xc0, 0x35, 0xfd, 0xab, 0x49, 0x24, 0x43, 0xf6, 0x95, + 0xb3, 0xe2, 0xf1, 0xa2, 0x30, 0x98, 0x26, 0x7a, 0x3a, 0xc9, 0xe6, 0x6d, 0x30, 0xd5, 0xcb, 0xe6, 0x9d, 0x5b, 0xec, + 0xbe, 0xef, 0xec, 0xf7, 0x1d, 0x16, 0x3d, 0x66, 0x32, 0x52, 0x66, 0x8a, 0xf9, 0xef, 0x3b, 0xfb, 0x7d, 0x87, 0xb7, + 0x07, 0x73, 0xe3, 0x2f, 0x14, 0x4b, 0x76, 0x86, 0x4b, 0x30, 0x21, 0x0f, 0xb8, 0x9b, 0x5a, 0x96, 0x09, 0x02, 0x5b, + 0x4b, 0x80, 0x38, 0x9f, 0x4f, 0xe3, 0x8a, 0x57, 0x43, 0xc0, 0x7d, 0x7a, 0xd7, 0xf6, 0x2a, 0x15, 0x78, 0x4c, 0xd0, + 0x88, 0x98, 0xd8, 0x36, 0xe6, 0x15, 0x31, 0xe0, 0xf2, 0x88, 0x2e, 0xf5, 0x24, 0x09, 0xf0, 0xaa, 0x46, 0xe5, 0x6d, + 0x8a, 0x94, 0x5f, 0x24, 0xc8, 0xf1, 0xc5, 0x1e, 0x51, 0xc5, 0x00, 0x56, 0x65, 0x49, 0x9f, 0x40, 0xea, 0xf9, 0xc1, + 0x44, 0x3f, 0x6f, 0x22, 0x8f, 0x7d, 0x4f, 0xf7, 0x33, 0xd3, 0xd3, 0x42, 0x2e, 0x26, 0x53, 0xf0, 0xa1, 0x05, 0x96, + 0xa1, 0x30, 0xf5, 0x2a, 0x5b, 0xff, 0x9a, 0xe4, 0x26, 0x80, 0xc2, 0xe9, 0xa6, 0x4c, 0x68, 0xa6, 0x17, 0x34, 0x37, + 0x96, 0xa4, 0x5c, 0x4c, 0x1e, 0xc9, 0xdb, 0x97, 0x80, 0xdd, 0x94, 0xe8, 0xc6, 0x8e, 0xbc, 0xb7, 0xb0, 0x03, 0x70, + 0x46, 0xd8, 0xae, 0x8a, 0x0f, 0x15, 0xe8, 0xfc, 0x71, 0x4e, 0xd8, 0xae, 0xaa, 0x4f, 0x98, 0xcd, 0x9e, 0x92, 0x8d, + 0xe1, 0xf6, 0xe2, 0xac, 0x91, 0xa3, 0xa3, 0x4e, 0x9a, 0x77, 0x3d, 0x31, 0xb0, 0x00, 0x0d, 0x80, 0xbb, 0xb5, 0x3d, + 0xcb, 0xbb, 0x1b, 0x02, 0x7a, 0x97, 0x4c, 0xda, 0xeb, 0x72, 0x93, 0xb2, 0x5a, 0x75, 0x2a, 0x2a, 0x58, 0xe0, 0x69, + 0xb0, 0x17, 0xa8, 0xfd, 0xda, 0x41, 0x71, 0xae, 0xb2, 0x4d, 0xd3, 0xf3, 0xb2, 0xef, 0xee, 0x8e, 0x45, 0xc6, 0x36, + 0xed, 0xed, 0x0e, 0x22, 0x61, 0x39, 0x61, 0x1d, 0x70, 0xc2, 0x55, 0xed, 0x80, 0x00, 0x5d, 0x07, 0x22, 0x37, 0x96, + 0x64, 0xb9, 0xae, 0x8c, 0xee, 0x03, 0xbf, 0x5b, 0x4a, 0xa4, 0x1b, 0x6d, 0x49, 0x30, 0x7d, 0x82, 0x51, 0xd3, 0x99, + 0x27, 0xa0, 0x6b, 0xaf, 0x1b, 0x6f, 0x8a, 0xb6, 0xfe, 0xad, 0x65, 0x6c, 0xb6, 0x87, 0x89, 0xa1, 0x0c, 0x62, 0xa0, + 0xf7, 0x11, 0xef, 0x36, 0x1a, 0x19, 0x02, 0x85, 0x4c, 0x36, 0xc0, 0x32, 0xf1, 0x5a, 0xf4, 0x83, 0x03, 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, 0x77, 0x2e, 0x36, 0x1b, 0x02, 0x12, 0x73, 0x98, 0xa0, 0x68, 0x38, 0x37, 0xc6, 0x58, 0x25, 0x95, 0x96, 0xb5, + 0x26, 0x31, 0x07, 0xbe, 0x74, 0xe1, 0xba, 0x2f, 0x6f, 0x53, 0x86, 0xef, 0x52, 0x81, 0x6f, 0xc0, 0x93, 0x26, 0x95, + 0xd8, 0x3d, 0x5e, 0x50, 0xac, 0x89, 0xee, 0x7a, 0xf6, 0xb6, 0x80, 0x75, 0x36, 0x7b, 0x44, 0x04, 0xbf, 0xab, 0x5f, + 0x6d, 0xf0, 0xdd, 0xc2, 0x5f, 0xc1, 0xfa, 0x39, 0x38, 0x49, 0xb1, 0x68, 0xc8, 0x66, 0xe1, 0x8e, 0x0c, 0x28, 0x57, + 0xf1, 0xcb, 0x61, 0xea, 0x56, 0x31, 0x5c, 0xfb, 0xf8, 0x8a, 0x3f, 0x6c, 0xb4, 0xdb, 0x50, 0x65, 0x71, 0xbb, 0x37, + 0x45, 0x43, 0x56, 0x4d, 0xef, 0xc8, 0xdc, 0x48, 0xa9, 0x7f, 0x7d, 0xc0, 0xad, 0xad, 0xf6, 0xfd, 0x34, 0xdf, 0x7a, + 0x74, 0xae, 0x9a, 0xf6, 0xa9, 0xb5, 0x22, 0x38, 0xf8, 0xd9, 0xc2, 0xcd, 0xad, 0x01, 0x07, 0xf0, 0xf3, 0x77, 0x34, + 0x8f, 0x33, 0x88, 0x4e, 0x6f, 0x35, 0xe3, 0xab, 0xf8, 0xaf, 0x51, 0x23, 0xee, 0xa5, 0x7f, 0x25, 0x7f, 0x8d, 0x1a, + 0xa8, 0x87, 0xe2, 0xf9, 0xed, 0x8a, 0xcd, 0x56, 0x10, 0x6c, 0xed, 0xde, 0x11, 0x7e, 0x1d, 0x96, 0xe4, 0x9a, 0xe6, + 0x3c, 0x5b, 0xb9, 0x87, 0xf7, 0x56, 0xee, 0x55, 0xa2, 0x95, 0x79, 0x4b, 0x6a, 0x15, 0xcb, 0x61, 0x0e, 0x81, 0x85, + 0xe3, 0xbd, 0x66, 0xaf, 0xdf, 0x6a, 0x3e, 0x18, 0xd8, 0x7f, 0x4d, 0x84, 0x7b, 0x54, 0x8b, 0xd8, 0xf6, 0x66, 0x63, + 0xeb, 0xc7, 0x60, 0xd8, 0x01, 0xa1, 0xc0, 0x41, 0x2e, 0x7d, 0x9c, 0x21, 0xeb, 0x7b, 0xb2, 0x5a, 0x31, 0x17, 0xcd, + 0xda, 0x69, 0xf0, 0xcb, 0xd8, 0x4c, 0x87, 0xed, 0xa4, 0xd3, 0xf5, 0x62, 0x2c, 0x69, 0x40, 0xa4, 0x69, 0xcc, 0x20, + 0x90, 0xd4, 0xd2, 0x70, 0x58, 0xf3, 0xdb, 0x28, 0xad, 0xee, 0x8f, 0x20, 0xe5, 0x87, 0x28, 0xe5, 0x47, 0x04, 0x02, + 0x68, 0x5b, 0xe6, 0xa8, 0x6c, 0xc8, 0xfb, 0x2e, 0xdd, 0x33, 0xce, 0x0c, 0x0d, 0xbe, 0x5a, 0xb5, 0xaa, 0x61, 0x8a, + 0xa2, 0x3e, 0xcc, 0xe5, 0x1a, 0x0b, 0xf2, 0x06, 0x74, 0xcd, 0x8a, 0x88, 0x5e, 0xe8, 0x2a, 0x0f, 0xef, 0x0e, 0x63, + 0x49, 0xc0, 0x49, 0xbf, 0x27, 0x7a, 0x05, 0xb9, 0x7c, 0x18, 0x83, 0x8f, 0x19, 0xe6, 0x7d, 0xdd, 0x2f, 0x06, 0x03, + 0x94, 0x3a, 0xa7, 0xb3, 0xd4, 0x44, 0x5c, 0x09, 0xfc, 0x92, 0x0b, 0xf0, 0x4b, 0x56, 0x88, 0xf5, 0x8b, 0x01, 0xb9, + 0x97, 0xc5, 0x12, 0x9c, 0xf2, 0x77, 0xf8, 0x3c, 0x3e, 0x0c, 0x0d, 0x4c, 0xcd, 0xb0, 0xcc, 0x45, 0x36, 0x58, 0xcc, + 0x59, 0x4b, 0x20, 0xb8, 0x19, 0x70, 0x97, 0xda, 0x90, 0x68, 0xac, 0x81, 0xa2, 0xdb, 0x28, 0x34, 0x33, 0x7a, 0xba, + 0xd5, 0x46, 0x3f, 0x72, 0x78, 0x61, 0xae, 0x61, 0x2c, 0x02, 0x99, 0xcb, 0x55, 0x8f, 0xfd, 0xe5, 0x87, 0xcd, 0x0a, + 0x83, 0x57, 0x64, 0x3a, 0x74, 0xc7, 0x31, 0xe3, 0xab, 0x3c, 0x71, 0x0c, 0x41, 0x26, 0x96, 0x4a, 0x37, 0x1c, 0x13, + 0x57, 0xd2, 0x67, 0x62, 0xc8, 0x76, 0xc3, 0x33, 0x73, 0xa1, 0x9b, 0xed, 0x1f, 0xce, 0xed, 0x9c, 0x13, 0x6e, 0xb4, + 0x92, 0x46, 0x1b, 0xf5, 0xcc, 0x50, 0x55, 0x17, 0xcc, 0xef, 0xa1, 0xd3, 0xd2, 0x62, 0xe7, 0xea, 0xdd, 0x0d, 0x5f, + 0xc1, 0x2b, 0xe3, 0x6f, 0xb1, 0x2a, 0xb4, 0x22, 0xc3, 0xed, 0x16, 0xf2, 0xe6, 0x4c, 0x0f, 0xbd, 0x22, 0x17, 0xaa, + 0xc3, 0x5f, 0xd4, 0x15, 0xe6, 0x61, 0xcc, 0xa8, 0x21, 0x3c, 0xfa, 0xbd, 0xce, 0x40, 0xf9, 0x07, 0x13, 0x93, 0x39, + 0x4b, 0x6e, 0x68, 0x21, 0xe2, 0x1f, 0x5f, 0x08, 0x13, 0xab, 0x6a, 0x0f, 0x06, 0xb2, 0x67, 0x2a, 0xee, 0xc1, 0xad, + 0x09, 0x1f, 0x73, 0x36, 0x4a, 0xf7, 0xa2, 0x1f, 0x1b, 0xa2, 0xf1, 0x63, 0xf4, 0x23, 0xb8, 0x3b, 0xbb, 0x57, 0x18, + 0xcb, 0xb8, 0x10, 0xfe, 0x1e, 0xeb, 0x61, 0xa9, 0x52, 0xc6, 0xda, 0xeb, 0x96, 0xc3, 0x0b, 0xa9, 0x37, 0x59, 0xfc, + 0xd0, 0x11, 0x6b, 0x9b, 0x82, 0x75, 0x48, 0x49, 0xe1, 0xd9, 0x15, 0x73, 0xab, 0xc5, 0xdc, 0xa5, 0x96, 0xf0, 0xd7, + 0x57, 0x0f, 0x4b, 0x15, 0x34, 0x1c, 0x84, 0xae, 0xb4, 0x85, 0x04, 0x18, 0xb8, 0x94, 0x3e, 0x9d, 0xee, 0x4c, 0x22, + 0xb3, 0x2c, 0x86, 0x77, 0x0f, 0x2a, 0x98, 0xff, 0xce, 0x36, 0xc2, 0xaa, 0xc0, 0xe5, 0x4a, 0x15, 0xf5, 0x52, 0x12, + 0x08, 0x40, 0x5f, 0x7a, 0x0f, 0xca, 0x8b, 0xa2, 0xdb, 0x68, 0x48, 0xd0, 0xc2, 0x52, 0x73, 0xad, 0x8a, 0xe9, 0x7e, + 0xf8, 0x7a, 0x60, 0xf0, 0xe1, 0x1d, 0xd2, 0x36, 0x9e, 0xf0, 0xa4, 0x84, 0xda, 0x1d, 0xb4, 0x0f, 0x56, 0xd9, 0x41, + 0xf9, 0xb7, 0x31, 0x45, 0x36, 0xbf, 0xcf, 0x7e, 0xa0, 0xae, 0xc3, 0x81, 0x2b, 0x58, 0xf5, 0x52, 0x46, 0xc1, 0x80, + 0x95, 0x53, 0xa0, 0xf6, 0x4e, 0x32, 0x9a, 0x4d, 0x19, 0xa8, 0xfb, 0x6d, 0xd1, 0x6a, 0x6e, 0x4f, 0xea, 0x7e, 0x43, + 0xc6, 0xd9, 0x47, 0x18, 0x67, 0x1f, 0x05, 0x5e, 0x2c, 0x92, 0xfc, 0x21, 0x63, 0x8d, 0x63, 0xd5, 0x14, 0xe8, 0xa8, + 0x03, 0xdc, 0x19, 0x38, 0xf0, 0x80, 0x2d, 0xca, 0xc1, 0x01, 0x75, 0x16, 0xf7, 0xb4, 0x91, 0x79, 0x6f, 0x4f, 0xa8, + 0x5d, 0xc4, 0x02, 0x37, 0x6b, 0x66, 0x5a, 0xd0, 0x5a, 0x61, 0x9c, 0xc7, 0x03, 0xde, 0xe6, 0x59, 0x2d, 0x7e, 0xc2, + 0x86, 0x35, 0x55, 0xfd, 0x06, 0x9a, 0xa3, 0x5a, 0x90, 0x9b, 0x27, 0xc6, 0x5b, 0x95, 0xf4, 0xa3, 0x68, 0x60, 0x39, + 0x15, 0x62, 0x48, 0x46, 0xbf, 0x35, 0x08, 0x6e, 0xb5, 0x57, 0x2b, 0xee, 0x11, 0x5f, 0xd4, 0xbc, 0xd5, 0xcc, 0x2d, + 0x00, 0x2d, 0xe2, 0xa8, 0xbc, 0x37, 0x89, 0xc0, 0xfb, 0xb6, 0x8c, 0x90, 0xb6, 0xec, 0xdb, 0x27, 0x22, 0x4b, 0xc5, + 0xe6, 0x3b, 0x3a, 0x19, 0xa4, 0x91, 0x1d, 0x51, 0x84, 0xaf, 0x4b, 0x48, 0xc2, 0x55, 0xd2, 0xb5, 0xca, 0xe4, 0x9c, + 0xa9, 0x94, 0xe3, 0xeb, 0x42, 0x4a, 0x7d, 0x65, 0xbf, 0x24, 0xae, 0xee, 0x64, 0x04, 0xbe, 0x9e, 0x30, 0xfd, 0x8e, + 0x16, 0x13, 0x06, 0x7e, 0x45, 0xfe, 0x76, 0x2c, 0xa5, 0xe4, 0xf2, 0x89, 0x88, 0xfb, 0x14, 0xc3, 0xfb, 0xa6, 0x03, + 0xac, 0x4d, 0x08, 0x94, 0x12, 0x17, 0xe1, 0x82, 0xe8, 0x4d, 0x21, 0x6f, 0xef, 0xe2, 0x02, 0x3b, 0x07, 0xc0, 0xd2, + 0x69, 0x12, 0xe0, 0x5f, 0x3e, 0xe6, 0x63, 0x35, 0xe6, 0xd4, 0xe8, 0xfa, 0xdd, 0xef, 0xe4, 0x1a, 0xe8, 0x6d, 0xe9, + 0x28, 0xd8, 0x6f, 0x0d, 0x20, 0x17, 0xee, 0xc2, 0xe0, 0xe2, 0x2b, 0xac, 0x2d, 0x0b, 0xe3, 0x8d, 0x05, 0xd0, 0xfb, + 0x3b, 0x03, 0x0b, 0x36, 0xcc, 0x31, 0x85, 0xc7, 0x61, 0x27, 0x4c, 0x07, 0x51, 0x41, 0x9e, 0x94, 0xcf, 0x7f, 0xd6, + 0x6a, 0xbf, 0x65, 0x63, 0xb8, 0xc3, 0x48, 0xbe, 0x5d, 0x38, 0x71, 0xe0, 0x01, 0x99, 0x26, 0xb3, 0xcd, 0xbe, 0xf1, + 0x91, 0x47, 0x5e, 0x8f, 0xe3, 0x5d, 0x2d, 0x85, 0xf9, 0x66, 0x45, 0xd7, 0x18, 0x42, 0x51, 0x84, 0xfd, 0x7e, 0x51, + 0x31, 0x45, 0x95, 0x41, 0x1b, 0x34, 0x2c, 0x6f, 0xc4, 0x2f, 0x70, 0xc6, 0xd0, 0x7a, 0x21, 0x7b, 0x47, 0x67, 0x1d, + 0xce, 0x1c, 0x66, 0x4c, 0x09, 0x8c, 0x4a, 0xcb, 0x82, 0x4e, 0xc0, 0xd1, 0xb9, 0xfa, 0x20, 0x2a, 0xae, 0x8e, 0x15, + 0x80, 0x27, 0x99, 0xc2, 0x3f, 0xf9, 0x26, 0x58, 0xf7, 0x5b, 0x35, 0xc3, 0xd4, 0x5f, 0xf4, 0xb6, 0x6b, 0xf9, 0x32, + 0xc4, 0x91, 0x36, 0x86, 0xd0, 0x3a, 0xb7, 0x77, 0x80, 0x22, 0x2e, 0xe8, 0x45, 0xaa, 0xf1, 0xb5, 0x5a, 0x0c, 0xcd, + 0xfa, 0x1a, 0xd7, 0x31, 0x6d, 0x10, 0xc5, 0xba, 0x6b, 0xe2, 0xeb, 0xea, 0xb5, 0x55, 0x95, 0x2a, 0x38, 0x83, 0x04, + 0xc2, 0xaa, 0xbc, 0x6c, 0x48, 0x25, 0xb9, 0x34, 0x9d, 0x4a, 0xd3, 0x69, 0x85, 0x50, 0x2e, 0x3d, 0x29, 0xef, 0x5f, + 0x21, 0x84, 0x81, 0x29, 0xb3, 0x03, 0xab, 0xd4, 0x16, 0x56, 0xc1, 0xab, 0x17, 0x1b, 0x58, 0x25, 0xe1, 0x78, 0x2e, + 0xd1, 0xa8, 0xa8, 0x70, 0xc8, 0x90, 0xbe, 0x10, 0x8b, 0x20, 0x01, 0xb0, 0xe8, 0x5d, 0xe6, 0xf2, 0xbe, 0x87, 0x43, + 0x61, 0x4f, 0x32, 0x09, 0xa7, 0x9b, 0xd0, 0x1c, 0x9e, 0xe1, 0x55, 0x3d, 0x8f, 0x10, 0xb0, 0xf4, 0x1c, 0xc3, 0xf3, + 0xcb, 0xdf, 0x7f, 0xf6, 0xd5, 0x59, 0x90, 0xa7, 0xff, 0x12, 0x25, 0xa1, 0xb1, 0xff, 0x1c, 0x0f, 0x1d, 0x12, 0x86, + 0x03, 0xdf, 0x1c, 0x61, 0x85, 0x83, 0x5b, 0x45, 0x7c, 0x06, 0x77, 0xf8, 0x58, 0x87, 0x1e, 0x00, 0x96, 0x50, 0x1c, + 0x82, 0x7c, 0x03, 0xc5, 0x0c, 0x0e, 0x68, 0xb2, 0x0c, 0x2f, 0x70, 0xc1, 0x6a, 0xa1, 0xbc, 0xbf, 0x6d, 0x79, 0x29, + 0xad, 0x76, 0xc9, 0x6b, 0xcc, 0x81, 0xca, 0xcf, 0xf0, 0xc2, 0x57, 0x98, 0x77, 0xa1, 0xdd, 0x17, 0xbe, 0x76, 0x40, + 0x4f, 0x21, 0x60, 0xa4, 0xfb, 0xbd, 0x26, 0xdc, 0x53, 0xf4, 0x32, 0x17, 0x87, 0x6d, 0x07, 0xdd, 0x0b, 0xcc, 0xd5, + 0x55, 0x95, 0x35, 0x07, 0x53, 0x68, 0x70, 0x50, 0x85, 0x33, 0x02, 0x73, 0xf5, 0xa2, 0x2c, 0x38, 0x07, 0xf1, 0xbe, + 0x27, 0x4c, 0x4e, 0x19, 0x0d, 0xe0, 0x45, 0x56, 0x3e, 0x3a, 0xd5, 0xe3, 0xe0, 0x32, 0x6e, 0xd8, 0xc4, 0x17, 0xc2, + 0xa7, 0x02, 0x2b, 0x69, 0x8d, 0x43, 0x23, 0x3a, 0xa2, 0x73, 0x30, 0xdb, 0x00, 0x0a, 0xee, 0xce, 0x87, 0x8d, 0x85, + 0x0a, 0x9e, 0xbe, 0xad, 0xbd, 0x54, 0x4d, 0x88, 0x33, 0x69, 0x0a, 0xee, 0xb6, 0x0d, 0x32, 0x78, 0xf3, 0xdb, 0x7f, + 0x2b, 0x2c, 0x12, 0x0c, 0xa8, 0xd4, 0x24, 0x41, 0x78, 0x82, 0xd2, 0x48, 0xb7, 0x72, 0x33, 0x81, 0x74, 0x22, 0x6a, + 0x46, 0xdd, 0x1b, 0xe7, 0xab, 0xa3, 0x06, 0xa2, 0xa2, 0x06, 0x2a, 0xa0, 0x06, 0xb2, 0xbe, 0xfd, 0x0b, 0x58, 0x08, + 0x1b, 0xa1, 0x4a, 0x04, 0x01, 0x11, 0xe6, 0xda, 0xf0, 0x01, 0x45, 0x12, 0x42, 0xde, 0x00, 0x2a, 0xa6, 0xe4, 0x25, + 0x18, 0x8d, 0xc3, 0xeb, 0x3d, 0xe0, 0x7e, 0x69, 0x19, 0x06, 0xcf, 0x29, 0x98, 0xfc, 0xb7, 0x3e, 0x1f, 0xaa, 0x97, + 0xab, 0x83, 0x10, 0x7e, 0x01, 0xb1, 0x22, 0x1c, 0x7f, 0xf1, 0x0b, 0x90, 0x4d, 0x85, 0xe5, 0xc1, 0x81, 0x04, 0x81, + 0x1f, 0xa2, 0x08, 0x07, 0x3c, 0xc3, 0xcb, 0x6c, 0x83, 0xe8, 0xf9, 0x59, 0xa9, 0x6a, 0x56, 0x32, 0x98, 0x55, 0xe1, + 0x69, 0x1c, 0x5d, 0x13, 0x06, 0x82, 0x0b, 0xb5, 0xfb, 0x06, 0x21, 0x50, 0xb6, 0xdc, 0x18, 0xba, 0xf4, 0x14, 0xcc, + 0x47, 0xe3, 0xe8, 0x2d, 0x83, 0x07, 0x7c, 0x8d, 0xc9, 0x3f, 0xd3, 0x2c, 0xd3, 0x86, 0x79, 0x6c, 0x04, 0x4e, 0xea, + 0x14, 0x25, 0x7f, 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, 0xbf, 0xd6, 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, 0x55, 0xe6, + 0x1f, 0x2a, 0x0b, 0x6e, 0x12, 0xd4, 0xe6, 0x22, 0x76, 0x59, 0x17, 0x31, 0x52, 0x5b, 0xdc, 0x1d, 0x42, 0xfc, 0xab, + 0xad, 0x28, 0x06, 0x3c, 0xa9, 0xf8, 0xe7, 0x18, 0x75, 0x21, 0x14, 0xb5, 0xf5, 0xb0, 0x01, 0x4a, 0xbb, 0x5c, 0x57, + 0x62, 0x64, 0x48, 0x20, 0xdf, 0xba, 0xf0, 0x82, 0xe6, 0x24, 0x52, 0x20, 0x27, 0x07, 0x51, 0x49, 0xb3, 0x0d, 0x61, + 0xae, 0xbb, 0x85, 0x63, 0xe6, 0x6a, 0x83, 0x16, 0xf1, 0x0b, 0x60, 0x67, 0xb8, 0x91, 0x2c, 0x1d, 0xf8, 0x54, 0x0d, + 0x7c, 0x7e, 0xcd, 0x0d, 0x45, 0x51, 0xa8, 0xf7, 0xce, 0x3e, 0x32, 0x07, 0xbf, 0xd3, 0x40, 0x7c, 0xa4, 0x4e, 0x47, + 0xb2, 0x11, 0x6a, 0xcd, 0xd9, 0xf1, 0xb2, 0xcd, 0x08, 0x83, 0xc2, 0x46, 0xef, 0xab, 0x90, 0x55, 0xec, 0xec, 0x54, + 0x04, 0x73, 0xfa, 0xa2, 0x2a, 0xe7, 0x54, 0x6e, 0x19, 0xd5, 0x52, 0xd3, 0x00, 0x11, 0xae, 0x7c, 0x22, 0x79, 0x9f, + 0x99, 0xf0, 0x0f, 0x06, 0xe3, 0xea, 0x91, 0xc2, 0xdf, 0xef, 0x8a, 0x1d, 0xb2, 0x1d, 0x1d, 0x6e, 0x23, 0x68, 0x5e, + 0xa8, 0xe0, 0x01, 0x47, 0x25, 0x4b, 0x88, 0x14, 0xb9, 0xdc, 0x57, 0x35, 0x53, 0xb6, 0xeb, 0x08, 0x21, 0xa4, 0x3d, + 0xce, 0xba, 0xa1, 0xd5, 0x43, 0x8f, 0x54, 0x51, 0x0e, 0xb7, 0x68, 0xae, 0x0b, 0x50, 0x61, 0x04, 0xd2, 0xe5, 0x67, + 0x76, 0x97, 0x4a, 0x88, 0x5e, 0xbe, 0x76, 0x21, 0x8c, 0x9d, 0x95, 0x25, 0x2e, 0xcc, 0xa8, 0x6d, 0x18, 0x5d, 0xb7, + 0x31, 0x9c, 0x0d, 0x8c, 0x99, 0x06, 0x25, 0x2d, 0x08, 0x75, 0xdd, 0xa5, 0x17, 0x99, 0x09, 0xf4, 0x98, 0x13, 0xda, + 0x60, 0x78, 0x4a, 0x34, 0x58, 0x36, 0x15, 0x60, 0xc1, 0xb7, 0x2c, 0x52, 0x6b, 0xb3, 0xc9, 0xe2, 0x8f, 0x3a, 0x36, + 0x4f, 0xfb, 0xe5, 0x15, 0xf3, 0x5c, 0x38, 0xea, 0xf6, 0x3c, 0xf3, 0xf1, 0xe8, 0x9e, 0xbe, 0xb9, 0x7a, 0xf1, 0xf2, + 0xf5, 0xab, 0xd5, 0xaa, 0xcd, 0x9a, 0xed, 0x13, 0xfc, 0x93, 0x2e, 0xe3, 0xc1, 0x96, 0x51, 0x80, 0x0e, 0x0e, 0xf6, + 0xb9, 0x71, 0xe1, 0xf9, 0xcc, 0xe7, 0x10, 0x37, 0x48, 0x0f, 0x70, 0x56, 0x94, 0x31, 0x41, 0x6e, 0xa3, 0x5e, 0x74, + 0x17, 0x81, 0x12, 0xaa, 0x22, 0x7f, 0x1f, 0x36, 0x67, 0xbf, 0x07, 0x81, 0x89, 0xa0, 0x3e, 0x44, 0x00, 0x81, 0x78, + 0xa5, 0xb8, 0x20, 0xcc, 0x27, 0x40, 0x14, 0xef, 0x05, 0x70, 0xa6, 0x26, 0x6a, 0xd5, 0x42, 0xc5, 0x05, 0x90, 0x44, + 0x1b, 0x8e, 0x92, 0x1e, 0x99, 0x00, 0xde, 0x10, 0x94, 0xd2, 0xfe, 0xea, 0xe6, 0xce, 0x5d, 0x2a, 0x47, 0xbd, 0x56, + 0x9a, 0xe3, 0xa9, 0xfb, 0x9c, 0xc2, 0xe7, 0xb4, 0xeb, 0x4f, 0x07, 0x71, 0x98, 0xe3, 0x05, 0x11, 0x87, 0xfe, 0x59, + 0xc4, 0xe5, 0xbc, 0x60, 0x5f, 0xb8, 0x5c, 0xa8, 0x74, 0x79, 0x9b, 0xca, 0xe4, 0xb6, 0x39, 0x3a, 0x8c, 0x8b, 0xe4, + 0xb6, 0xa9, 0x92, 0x5b, 0x84, 0xef, 0x52, 0x99, 0xdc, 0xd9, 0x94, 0xbb, 0xa6, 0x82, 0x9b, 0x2f, 0x2c, 0xe0, 0x50, + 0xb4, 0x45, 0x1b, 0x8b, 0xcd, 0xa2, 0x36, 0xc5, 0x15, 0x0d, 0x30, 0xf8, 0xf7, 0x1d, 0x1b, 0x3f, 0x0c, 0x5f, 0x82, + 0x4b, 0x93, 0x26, 0xf2, 0x13, 0x48, 0x3f, 0xad, 0xca, 0xc0, 0x7d, 0x4a, 0x5a, 0xdd, 0xe9, 0x85, 0x68, 0xb6, 0xbb, + 0x8d, 0xc6, 0x14, 0xf6, 0x6e, 0x46, 0x72, 0x5f, 0x6c, 0xda, 0x30, 0xf1, 0x75, 0xf6, 0xb3, 0xd5, 0x6a, 0x3f, 0x47, + 0x66, 0xc3, 0x4d, 0x58, 0xac, 0xfb, 0xd3, 0x01, 0x6e, 0xe1, 0xe7, 0x19, 0x42, 0x4b, 0xd6, 0x9f, 0x0e, 0x08, 0xeb, + 0x4f, 0x1b, 0xed, 0x81, 0x35, 0xb4, 0x33, 0x5b, 0x71, 0x0d, 0x21, 0x34, 0xa7, 0x83, 0x23, 0x53, 0x52, 0xba, 0x7c, + 0xfb, 0x45, 0xab, 0x80, 0x7e, 0xaa, 0x16, 0xbc, 0x4c, 0xe2, 0x0e, 0xf4, 0x45, 0x2f, 0xec, 0xd3, 0xad, 0x05, 0x39, + 0x3e, 0xaa, 0x5c, 0xed, 0x29, 0xc2, 0xa6, 0x27, 0x75, 0x58, 0x1c, 0x9a, 0x66, 0x5c, 0x97, 0xd2, 0x7d, 0x87, 0x9a, + 0x91, 0x8f, 0x0e, 0x16, 0x80, 0x20, 0x15, 0x3c, 0xb2, 0xc2, 0x85, 0x53, 0x0a, 0xe1, 0xe2, 0xa0, 0xb2, 0x05, 0x93, + 0x9c, 0xb4, 0xba, 0xb9, 0xb1, 0xf4, 0xcf, 0x5d, 0x44, 0x53, 0x8a, 0x29, 0xc9, 0x7c, 0xc9, 0xdc, 0x80, 0x85, 0x6e, + 0x52, 0x9e, 0x29, 0xe8, 0x95, 0x06, 0x78, 0x44, 0x20, 0x1e, 0x52, 0xb7, 0x30, 0x06, 0x5e, 0xf1, 0xb4, 0x59, 0xf4, + 0xd9, 0x00, 0x1d, 0x1d, 0x63, 0xda, 0xff, 0x94, 0xcd, 0xdb, 0xf0, 0x58, 0xe0, 0xa7, 0x01, 0x99, 0x36, 0x65, 0x99, + 0x20, 0x20, 0x61, 0xd4, 0x94, 0x87, 0xb0, 0x97, 0x10, 0xce, 0x6c, 0xc5, 0xac, 0xcf, 0x06, 0xcd, 0x69, 0x59, 0xb1, + 0xe3, 0x2b, 0x36, 0x64, 0x99, 0x60, 0x2b, 0x36, 0x5c, 0xc5, 0xf0, 0x75, 0x06, 0x03, 0x82, 0x10, 0x00, 0x0c, 0x00, + 0xa0, 0x51, 0x10, 0xcd, 0x17, 0x2b, 0xe2, 0x37, 0xbb, 0xbd, 0xc7, 0x6f, 0x81, 0x05, 0x5a, 0x6d, 0xff, 0xef, 0x42, + 0x19, 0xb0, 0xa7, 0x2c, 0x4c, 0xcc, 0xdc, 0xc2, 0xaa, 0xe8, 0x00, 0x2a, 0x25, 0xc2, 0x14, 0x06, 0x32, 0xfb, 0x99, + 0x81, 0x5a, 0xa0, 0x35, 0xc8, 0xfb, 0x7a, 0xd0, 0xcc, 0xe0, 0x88, 0x81, 0x77, 0x68, 0xc8, 0xd4, 0x18, 0x13, 0xc6, + 0x39, 0x4c, 0x31, 0x33, 0xe0, 0x99, 0xa6, 0xad, 0xb5, 0x34, 0xb2, 0x5c, 0x2f, 0xef, 0xfd, 0xa3, 0x63, 0xd5, 0x2f, + 0x9a, 0xed, 0x01, 0xda, 0x27, 0xc4, 0x7e, 0x0c, 0x60, 0x93, 0xb9, 0xd4, 0x86, 0xf9, 0x3e, 0xea, 0xa4, 0xf6, 0x13, + 0xfe, 0x0c, 0xd6, 0x66, 0x07, 0x80, 0x8e, 0x0c, 0x9b, 0xf5, 0x97, 0x35, 0x95, 0xd7, 0xc7, 0x9d, 0x51, 0x2a, 0x77, + 0xbd, 0x3b, 0x1d, 0x68, 0x8a, 0x43, 0x6f, 0x3d, 0x5c, 0x3e, 0xd4, 0x43, 0xc0, 0x8c, 0xc1, 0xdc, 0x32, 0xa3, 0xef, + 0x85, 0x48, 0x2e, 0x88, 0xc4, 0xd2, 0x60, 0x0d, 0x83, 0xbd, 0x75, 0x70, 0x60, 0xaa, 0xb1, 0x06, 0x3c, 0x4f, 0x8a, + 0x40, 0x30, 0xf0, 0x11, 0x94, 0x01, 0x4d, 0x94, 0xb9, 0x0d, 0x27, 0x1f, 0x99, 0xfb, 0x85, 0xcb, 0xdb, 0xc7, 0xc2, + 0x69, 0x5b, 0xcd, 0xf5, 0x78, 0x59, 0xe0, 0xae, 0xbc, 0x97, 0xb4, 0x0a, 0x6e, 0x64, 0x6f, 0xf2, 0x94, 0xb9, 0x5b, + 0xf7, 0xa5, 0x3a, 0xbb, 0x9b, 0xe9, 0x94, 0xcd, 0x74, 0xb6, 0x9b, 0x09, 0x35, 0x33, 0xdf, 0xb2, 0x8a, 0x34, 0x27, + 0x6b, 0xa2, 0xe6, 0x54, 0xfc, 0x44, 0xe7, 0xa0, 0x1d, 0xe5, 0xf6, 0x5e, 0x15, 0x4e, 0xae, 0x9c, 0x5c, 0xee, 0xe7, + 0x86, 0xb8, 0x22, 0x73, 0xa1, 0x0e, 0x01, 0x5e, 0x5e, 0x94, 0x8f, 0x0f, 0x70, 0x29, 0x7e, 0x95, 0x23, 0x17, 0xe5, + 0x54, 0x48, 0x2d, 0x05, 0x8b, 0x90, 0x41, 0x55, 0x17, 0x03, 0x7b, 0x69, 0xf7, 0x9e, 0xe8, 0xf1, 0x7e, 0x15, 0x31, + 0x6f, 0x60, 0x9e, 0xfb, 0xf8, 0x9e, 0xa6, 0xd8, 0xa9, 0x89, 0x33, 0xf2, 0x21, 0x8b, 0x73, 0x90, 0xcd, 0xfa, 0xd5, + 0x6b, 0xbf, 0x8d, 0x36, 0x2e, 0x9a, 0xb1, 0xe8, 0x99, 0x27, 0x4e, 0x7e, 0x28, 0x8c, 0x71, 0x80, 0x75, 0xf4, 0x47, + 0x98, 0x5a, 0xb0, 0x67, 0x89, 0xa7, 0xd0, 0xc9, 0xad, 0x4d, 0xbb, 0x0b, 0xd3, 0xee, 0x4c, 0x5a, 0x07, 0xca, 0x01, + 0x69, 0x76, 0x65, 0x3a, 0x77, 0xfe, 0xfb, 0x0e, 0x5e, 0xba, 0x5d, 0x43, 0x24, 0xee, 0xf9, 0x23, 0x63, 0x0c, 0xf1, + 0x06, 0x6c, 0x44, 0xd5, 0xc1, 0xc1, 0x1f, 0xce, 0xfb, 0xb6, 0x92, 0xfb, 0xbe, 0x15, 0x0e, 0x6c, 0x83, 0xa9, 0x74, + 0x79, 0x23, 0x99, 0x2d, 0xc0, 0xae, 0x73, 0xff, 0x1b, 0xf1, 0xf0, 0x45, 0xc8, 0xb4, 0x58, 0x57, 0xf1, 0x57, 0x72, + 0x54, 0x7a, 0x88, 0x6a, 0x88, 0x40, 0x5a, 0x59, 0x97, 0x86, 0xa6, 0xa3, 0x57, 0x53, 0x3a, 0x92, 0x37, 0x6f, 0xa5, + 0xd4, 0x03, 0xfb, 0x22, 0xb7, 0x4e, 0xe0, 0xd1, 0xc2, 0x1a, 0x43, 0x73, 0x57, 0x7a, 0x27, 0xd9, 0x80, 0xa8, 0xf5, + 0x71, 0x87, 0x92, 0x48, 0x2c, 0xaa, 0xbb, 0x10, 0x0e, 0x77, 0x21, 0x98, 0x97, 0x41, 0xdb, 0x20, 0x76, 0xbb, 0x0b, + 0xda, 0x06, 0x4e, 0xdd, 0x36, 0x70, 0x7b, 0x30, 0x58, 0xd8, 0xfb, 0xf0, 0x72, 0x2c, 0xc7, 0xc2, 0x5f, 0x93, 0xd9, + 0x07, 0x80, 0x40, 0xed, 0xc3, 0x8a, 0x27, 0x0e, 0x04, 0x89, 0x33, 0x1c, 0x7d, 0xcf, 0xd9, 0x8d, 0xb5, 0x1c, 0x9e, + 0xcd, 0x17, 0x9a, 0x8d, 0xcc, 0x1d, 0x35, 0xa8, 0xf8, 0xea, 0x7e, 0x5e, 0x3f, 0x65, 0x35, 0xdd, 0xf8, 0x3d, 0x08, + 0x23, 0xe1, 0x94, 0x1d, 0x46, 0x21, 0x61, 0x83, 0x59, 0x95, 0xf1, 0xda, 0x7e, 0x83, 0x78, 0x0f, 0xda, 0x84, 0x13, + 0x2c, 0x6a, 0x17, 0x54, 0x11, 0xb6, 0xf1, 0xc6, 0x82, 0x28, 0x0f, 0x6f, 0xb6, 0x8c, 0xa6, 0x97, 0x6b, 0x08, 0x74, + 0xdc, 0x8b, 0x9a, 0x51, 0x83, 0xa5, 0x2e, 0x28, 0xb3, 0x8f, 0x30, 0xae, 0x2e, 0x4e, 0x4c, 0x9c, 0xf6, 0x52, 0xaf, + 0xfe, 0x5b, 0x06, 0x06, 0xf8, 0x02, 0xbc, 0xc4, 0xc2, 0xe8, 0xae, 0x7d, 0xdd, 0x80, 0xfa, 0xb2, 0xc1, 0x06, 0x68, + 0xb5, 0x6a, 0x95, 0xcf, 0x40, 0xb9, 0x6b, 0x2e, 0x61, 0xaf, 0xb9, 0x84, 0xbb, 0xe6, 0x12, 0xfe, 0x9a, 0x4b, 0x98, + 0x6b, 0x2e, 0xe1, 0xaf, 0xb9, 0x3c, 0x08, 0xff, 0x0c, 0xe2, 0x38, 0xc6, 0x1c, 0xe2, 0x2a, 0x6a, 0x1b, 0x19, 0x0f, + 0x2e, 0x3c, 0xf7, 0x59, 0xa2, 0xca, 0xe5, 0x0f, 0x63, 0xc8, 0x6d, 0xd9, 0x4a, 0x18, 0xb7, 0x29, 0xa6, 0x20, 0x72, + 0xfa, 0xc1, 0x41, 0xe9, 0xee, 0x0c, 0x3e, 0xea, 0x29, 0xc7, 0x4b, 0xeb, 0x44, 0xfb, 0x07, 0xe8, 0xe4, 0xcd, 0xaf, + 0x8f, 0xa9, 0x5c, 0x13, 0xe1, 0x4c, 0xee, 0xf7, 0xdb, 0x9e, 0x52, 0xfc, 0x99, 0x99, 0xf0, 0xe4, 0x3c, 0xd1, 0x46, + 0x04, 0x41, 0x88, 0x12, 0x85, 0x33, 0x22, 0xed, 0x7e, 0xf7, 0xae, 0xf0, 0x46, 0x15, 0xe5, 0xcd, 0x4a, 0x1e, 0xe7, + 0xe0, 0xc4, 0x6e, 0xac, 0x30, 0x50, 0x17, 0x5c, 0x08, 0x32, 0x93, 0xf0, 0x47, 0x33, 0xb7, 0xe4, 0x2c, 0x2b, 0x93, + 0x3e, 0x36, 0x73, 0x43, 0xc0, 0x0a, 0xb2, 0xef, 0x61, 0xb6, 0xbc, 0x4d, 0xe9, 0xff, 0xcb, 0xde, 0xbb, 0x2e, 0xb7, + 0x8d, 0x64, 0xe9, 0xa2, 0xaf, 0x22, 0x31, 0x6c, 0x16, 0x60, 0x26, 0x29, 0xca, 0x7b, 0xcf, 0x44, 0x1c, 0x50, 0x29, + 0x86, 0x2f, 0xe5, 0x2e, 0x77, 0x97, 0x2f, 0x6d, 0xb9, 0xab, 0xab, 0x9a, 0xc1, 0xc3, 0x82, 0x80, 0xa4, 0x00, 0x17, + 0x08, 0xb0, 0x00, 0x50, 0x22, 0x4d, 0xe2, 0xdd, 0x77, 0xac, 0xb5, 0xf2, 0x0a, 0x82, 0xb2, 0x7b, 0x66, 0xcf, 0xaf, + 0x73, 0xfe, 0xd8, 0x62, 0x22, 0x91, 0xc8, 0x7b, 0xae, 0x5c, 0x97, 0xef, 0x63, 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, 0x4b, 0x35, 0x39, 0x42, 0x44, 0xbb, 0x08, 0x07, 0x00, 0x71, 0xa7, 0xa9, 0xac, 0x43, 0x0d, 0xd0, + 0x07, 0x04, 0xd6, 0xa1, 0x6f, 0x33, 0x80, 0x83, 0x3e, 0xda, 0x3c, 0x8b, 0x40, 0x5e, 0xf7, 0xee, 0xd9, 0x3b, 0xb6, + 0xf3, 0xf9, 0xf5, 0x2a, 0xf5, 0xee, 0xd1, 0x21, 0xf8, 0x7c, 0xec, 0x4f, 0x2f, 0x03, 0x83, 0x0b, 0xcd, 0xde, 0x3d, + 0x13, 0x6c, 0xc7, 0x76, 0xcf, 0x10, 0xa9, 0xa8, 0x3b, 0xff, 0xf0, 0xd2, 0x44, 0xcf, 0x3b, 0x2f, 0xdc, 0xf1, 0x25, + 0x80, 0x07, 0xb2, 0x18, 0x50, 0x7c, 0x96, 0xde, 0x3f, 0x59, 0x02, 0x6a, 0xf2, 0x5b, 0xbe, 0xf6, 0xbe, 0x52, 0xea, + 0x02, 0xfe, 0x1c, 0x50, 0xfa, 0x24, 0xe7, 0xde, 0xdd, 0xf0, 0xd6, 0xbf, 0x78, 0x0e, 0xce, 0x13, 0xab, 0xe1, 0x02, + 0xfe, 0x2a, 0xf8, 0xd0, 0xbb, 0x1b, 0x60, 0x62, 0xc9, 0x87, 0xde, 0x6a, 0x00, 0xa9, 0x0a, 0x17, 0x12, 0x63, 0x1f, + 0x7e, 0x0d, 0x72, 0x86, 0x7f, 0xfc, 0xa6, 0x31, 0x58, 0x7f, 0x0d, 0x0a, 0x8d, 0xc6, 0x5a, 0xaa, 0x90, 0xa5, 0x58, + 0x9c, 0x09, 0xb0, 0x09, 0xc7, 0xdd, 0xbe, 0x58, 0xd5, 0x66, 0x2d, 0xe8, 0xcf, 0x47, 0x7c, 0x8f, 0xc6, 0xea, 0xaa, + 0x9c, 0x8b, 0xf2, 0x13, 0xd2, 0xa7, 0x3a, 0x3e, 0x46, 0xc5, 0xa6, 0xee, 0x4e, 0xa7, 0x5a, 0x75, 0xa4, 0xfd, 0xa6, + 0x5c, 0x83, 0x1d, 0xaf, 0x93, 0x23, 0x4b, 0xe1, 0x59, 0x87, 0x9d, 0x97, 0x4e, 0x89, 0x0e, 0xc3, 0x78, 0xb7, 0x55, + 0xcf, 0x18, 0xca, 0x73, 0x83, 0x31, 0x5d, 0xf0, 0x88, 0x5f, 0x0f, 0x72, 0x19, 0x1a, 0xf3, 0x11, 0xd9, 0x30, 0x94, + 0x0f, 0x2d, 0x32, 0x24, 0x44, 0xbc, 0x87, 0x4a, 0xc0, 0xb6, 0x05, 0x65, 0x52, 0xc0, 0x59, 0x34, 0xf8, 0xad, 0xf6, + 0x72, 0xe0, 0x3d, 0x88, 0xfc, 0x46, 0xba, 0x94, 0x4b, 0x6c, 0x74, 0xe2, 0x58, 0x16, 0xda, 0x79, 0x5c, 0x7f, 0x1d, + 0x83, 0xfa, 0xbd, 0xd2, 0x6f, 0x50, 0xce, 0xfe, 0x24, 0x59, 0xa7, 0x8d, 0x27, 0xc6, 0xbf, 0x5c, 0xe5, 0x9f, 0xa2, + 0xa5, 0x1e, 0xfe, 0x3f, 0x63, 0x0a, 0xa5, 0x7f, 0x95, 0x96, 0xd1, 0x66, 0xb5, 0x14, 0xa5, 0xc8, 0x23, 0x71, 0xf2, + 0xb5, 0xc8, 0xce, 0xe5, 0x3b, 0x9f, 0x42, 0xbf, 0x00, 0xb4, 0xec, 0x13, 0x64, 0xf4, 0x4b, 0x26, 0xf8, 0xf0, 0xa5, + 0x76, 0xae, 0xcd, 0xf9, 0x78, 0x92, 0x5f, 0x59, 0x7b, 0xb7, 0xe3, 0x45, 0x62, 0x14, 0x63, 0xb9, 0xaf, 0xba, 0x59, + 0x39, 0x51, 0xc9, 0x81, 0x91, 0xae, 0xc9, 0x5e, 0xae, 0x64, 0xdd, 0x4e, 0xb7, 0x12, 0x88, 0xa8, 0x02, 0xef, 0x31, + 0xae, 0x62, 0x1f, 0xc1, 0x74, 0xdd, 0x71, 0x19, 0xed, 0x78, 0xcf, 0x78, 0x75, 0xa2, 0xac, 0xe0, 0x76, 0x23, 0xda, + 0x13, 0x3a, 0xfa, 0x69, 0x52, 0x5b, 0x16, 0x0e, 0x40, 0xee, 0x12, 0xc6, 0xb2, 0x21, 0x58, 0x31, 0x28, 0x7d, 0xbd, + 0xa6, 0x64, 0x59, 0x80, 0x45, 0x67, 0x97, 0x11, 0x88, 0x61, 0xdd, 0x34, 0x27, 0x74, 0xbc, 0x74, 0x71, 0xde, 0x6b, + 0x15, 0x29, 0x78, 0x46, 0x8b, 0x8e, 0xb9, 0xe9, 0x48, 0x37, 0x46, 0x7b, 0xfb, 0xd2, 0x20, 0xa4, 0x78, 0xfe, 0xc0, + 0x56, 0xeb, 0xe2, 0x22, 0xf1, 0x0a, 0x99, 0x68, 0x41, 0x2c, 0x45, 0x60, 0xc6, 0x0b, 0x4d, 0x23, 0x4c, 0x50, 0xa6, + 0x04, 0x8b, 0xd6, 0xe8, 0xd0, 0xfe, 0xb0, 0x84, 0xdd, 0x63, 0x8c, 0x00, 0x81, 0x2a, 0xd3, 0x97, 0xb0, 0x35, 0x61, + 0x36, 0x75, 0xb1, 0x01, 0xda, 0x2a, 0x86, 0x06, 0x61, 0x6d, 0x88, 0xf9, 0x94, 0xe6, 0x77, 0xff, 0xc4, 0x62, 0x6c, + 0x4f, 0x20, 0xb6, 0x77, 0xbb, 0x26, 0x61, 0xba, 0xd7, 0xe2, 0xc6, 0x7a, 0xb9, 0x3d, 0xe5, 0x98, 0xda, 0xb1, 0x36, + 0x6a, 0xc7, 0x5a, 0xea, 0x1d, 0x6b, 0xad, 0x77, 0xac, 0xbb, 0x86, 0x7f, 0xcc, 0xbc, 0x98, 0x25, 0xa0, 0xdf, 0x5d, + 0x71, 0xd5, 0x20, 0x68, 0xc6, 0x86, 0xdd, 0xc2, 0x6f, 0x89, 0xb5, 0x5b, 0xfa, 0x17, 0x4b, 0xb6, 0x30, 0x7d, 0xa0, + 0x5b, 0x07, 0x58, 0x46, 0xd4, 0xe4, 0x7b, 0xe4, 0xdd, 0x74, 0x56, 0x14, 0x6e, 0x4f, 0x6c, 0xe1, 0xb3, 0x77, 0xe6, + 0xcd, 0xfb, 0x67, 0x11, 0xe4, 0xde, 0x71, 0xef, 0x7e, 0xf8, 0xce, 0xbf, 0xd0, 0x2d, 0x90, 0x93, 0x59, 0xce, 0x40, + 0xea, 0x88, 0xcf, 0x10, 0xad, 0xec, 0x29, 0xdf, 0x09, 0xb9, 0xb3, 0xad, 0x9f, 0xdd, 0xbb, 0xdb, 0xda, 0xdd, 0xb3, + 0x7b, 0x56, 0x8d, 0x28, 0x56, 0x9c, 0xa6, 0x48, 0x98, 0x45, 0x1b, 0xe0, 0xa9, 0x97, 0xef, 0x77, 0xec, 0x98, 0xc3, + 0xdd, 0xb3, 0x8e, 0x8e, 0x97, 0x73, 0xc0, 0xee, 0xfe, 0xa3, 0x4d, 0xd8, 0x58, 0xe9, 0x5a, 0x85, 0x0e, 0x77, 0xcf, + 0x32, 0x8d, 0xe7, 0x70, 0x24, 0x9f, 0x8e, 0x35, 0x36, 0x08, 0xea, 0xfa, 0x9c, 0x41, 0xed, 0xd8, 0x7d, 0x4d, 0xd8, + 0x65, 0xc7, 0xbc, 0xd6, 0x35, 0x6f, 0xaf, 0x3c, 0x15, 0x1b, 0x02, 0x3a, 0x7c, 0xad, 0x6e, 0x90, 0x7f, 0x09, 0x9c, + 0x22, 0x00, 0xe4, 0x70, 0xbc, 0xe4, 0xb1, 0xef, 0xd3, 0x2c, 0xad, 0x77, 0xa8, 0xb5, 0xa8, 0x2c, 0xcb, 0xb0, 0xf6, + 0x7e, 0xd0, 0x8a, 0x61, 0xa9, 0xe9, 0x9f, 0x8e, 0x03, 0xb7, 0xb3, 0xdd, 0xca, 0xd8, 0x65, 0x3c, 0x2b, 0x2e, 0x5e, + 0x9e, 0x16, 0xca, 0xb5, 0x9b, 0xb7, 0xf1, 0x9b, 0x56, 0x4b, 0x96, 0xd6, 0x7a, 0xc8, 0x4b, 0xcb, 0x22, 0x02, 0x01, + 0x0c, 0x47, 0xca, 0x2e, 0x96, 0x70, 0x8f, 0xb0, 0xba, 0x07, 0xa1, 0x64, 0x5e, 0xb8, 0x78, 0xce, 0x62, 0x48, 0x04, + 0xd8, 0xee, 0x50, 0xb1, 0x2d, 0x5c, 0x3c, 0x67, 0x1b, 0x5e, 0xf4, 0xfb, 0x99, 0xea, 0x14, 0xb2, 0xee, 0x2c, 0xf9, + 0x46, 0x35, 0xc7, 0x1a, 0x6a, 0xb6, 0x36, 0xc9, 0xd6, 0x38, 0xb7, 0x15, 0x1f, 0x77, 0x6d, 0xc5, 0xc7, 0xca, 0x5a, + 0x97, 0xee, 0xf5, 0x1e, 0xd5, 0x05, 0xb0, 0xf5, 0xdf, 0x1e, 0xaf, 0x5c, 0xcf, 0x67, 0x04, 0xf0, 0xb5, 0xe0, 0xe3, + 0xc9, 0x02, 0xbd, 0x4a, 0x16, 0xfe, 0xed, 0x40, 0x8d, 0xbf, 0xd3, 0xb9, 0x0b, 0x80, 0xae, 0xa4, 0xbc, 0x02, 0xf2, + 0x0e, 0x72, 0xcc, 0x2d, 0xbb, 0xf2, 0xfe, 0xe4, 0x3b, 0xec, 0x1d, 0xaf, 0x67, 0x8b, 0x39, 0xdb, 0x81, 0x53, 0x41, + 0x32, 0xb0, 0x97, 0x15, 0xdb, 0x05, 0xb1, 0x9d, 0xf0, 0x1b, 0x01, 0x53, 0xbe, 0x80, 0x20, 0xae, 0xe0, 0x16, 0xe2, + 0xf0, 0xe4, 0x9f, 0x83, 0xfb, 0xd6, 0x66, 0x7d, 0xcf, 0xac, 0xce, 0x09, 0xd6, 0xcc, 0xea, 0xc1, 0x60, 0xd9, 0x4c, + 0x56, 0xfd, 0xbe, 0xb7, 0xd3, 0x8e, 0x4f, 0x77, 0x52, 0x27, 0x76, 0x5a, 0xab, 0xb5, 0x60, 0xef, 0xa4, 0xd6, 0xc5, + 0x18, 0x7a, 0x80, 0xf8, 0xe9, 0x76, 0xc0, 0xef, 0x3b, 0xd6, 0x96, 0xf7, 0x8e, 0x2d, 0xd8, 0x0e, 0x2e, 0x41, 0x4d, + 0x7b, 0xd9, 0x9f, 0x54, 0x2e, 0x68, 0xc7, 0x2e, 0x89, 0x87, 0x33, 0x66, 0x95, 0x32, 0xb3, 0x4e, 0xaa, 0x2b, 0xd1, + 0x19, 0xd3, 0x59, 0xeb, 0xf9, 0x5c, 0xcd, 0x27, 0x85, 0x06, 0xf5, 0x3b, 0x27, 0x3e, 0xa2, 0xa2, 0xf3, 0x04, 0xb6, + 0x96, 0x15, 0xc4, 0x6a, 0x9f, 0x83, 0xb5, 0x56, 0xbb, 0xf4, 0x7b, 0xf9, 0x80, 0xdb, 0x94, 0xc3, 0x3a, 0x30, 0xa8, + 0x39, 0xb1, 0xa2, 0x1e, 0xb3, 0x1d, 0xe3, 0xe6, 0xa7, 0x97, 0x3f, 0x38, 0x61, 0xc9, 0x8a, 0xd5, 0xfe, 0xf4, 0xe5, + 0x33, 0x4f, 0x7f, 0xa7, 0xf6, 0x2f, 0x84, 0x1f, 0x8c, 0xff, 0x5d, 0xbb, 0xaf, 0xb5, 0x18, 0x95, 0xad, 0x72, 0x84, + 0xc6, 0xdd, 0x4a, 0x9a, 0x2c, 0x3f, 0x0b, 0x4f, 0x58, 0x0b, 0x9e, 0xe5, 0x7a, 0x89, 0x66, 0x05, 0xac, 0xb0, 0x96, + 0x49, 0xb8, 0xc2, 0x58, 0x2d, 0x6d, 0xf5, 0x2d, 0x9a, 0xe6, 0xf8, 0x70, 0xae, 0x0d, 0xca, 0x94, 0xb3, 0x33, 0x62, + 0x35, 0x5c, 0x86, 0xa5, 0x09, 0x45, 0xc8, 0xee, 0xed, 0xe0, 0xc6, 0x4e, 0x59, 0x4a, 0x19, 0xce, 0x31, 0x98, 0xf0, + 0x48, 0x8c, 0xaa, 0x7c, 0x7f, 0x5f, 0x52, 0xe4, 0xb4, 0x2d, 0x07, 0x55, 0x08, 0xfb, 0x48, 0xa2, 0x04, 0x6e, 0x45, + 0x5a, 0x28, 0x52, 0x16, 0x7f, 0x3b, 0x40, 0x17, 0x78, 0x01, 0x75, 0x35, 0xea, 0xf6, 0x87, 0x23, 0x1e, 0x3e, 0x32, + 0xf5, 0x81, 0x11, 0x4b, 0x02, 0xb5, 0xbd, 0xc8, 0xd2, 0x3b, 0x50, 0xe1, 0xf7, 0x70, 0x35, 0x11, 0xfb, 0xb9, 0x25, + 0x45, 0x45, 0x36, 0xd2, 0x1b, 0x5a, 0x83, 0x47, 0x68, 0x4d, 0x79, 0xe9, 0xa4, 0xda, 0xa4, 0xf3, 0x8e, 0x90, 0x63, + 0xf5, 0xad, 0x25, 0x8c, 0x76, 0x45, 0x2f, 0xee, 0x1d, 0xbd, 0xe7, 0xe9, 0xaa, 0xe7, 0xfe, 0xc4, 0x15, 0xf3, 0xe4, + 0x36, 0x02, 0x75, 0x2b, 0xa8, 0x6e, 0x1f, 0x54, 0x82, 0x05, 0x4b, 0xda, 0x7d, 0xfc, 0x76, 0xd6, 0x0e, 0x44, 0x65, + 0xac, 0xd2, 0xb7, 0x24, 0x61, 0x4f, 0x0c, 0x3a, 0x85, 0xaa, 0xdc, 0xee, 0x8e, 0xb6, 0xc0, 0x75, 0xcc, 0x52, 0xf4, + 0xc2, 0x16, 0xb9, 0x5b, 0xfe, 0xdd, 0x73, 0x45, 0xce, 0x7e, 0x09, 0x08, 0x4e, 0xcd, 0x37, 0xc4, 0x97, 0x23, 0x3c, + 0xaa, 0x6e, 0x81, 0xe3, 0xf4, 0x1d, 0xc0, 0x3f, 0x1c, 0x2e, 0x41, 0x13, 0x10, 0x0b, 0xd6, 0x4b, 0xe3, 0x1e, 0xeb, + 0xc5, 0xc5, 0xe6, 0x2e, 0xc9, 0x37, 0xe0, 0xcc, 0x40, 0xa9, 0x96, 0x7e, 0xe0, 0x58, 0x2d, 0xa0, 0xc2, 0xc1, 0xec, + 0xa4, 0x5e, 0x58, 0x46, 0x3d, 0xa6, 0xcf, 0xcf, 0x60, 0xef, 0x08, 0x09, 0x80, 0xfb, 0x65, 0x1f, 0x90, 0x80, 0x87, + 0xce, 0xec, 0x80, 0x70, 0xc2, 0x2c, 0xaa, 0x02, 0x89, 0xe4, 0x48, 0x3f, 0x7b, 0xcc, 0x44, 0xf2, 0x07, 0xb3, 0x9e, + 0x73, 0x4a, 0xf4, 0x58, 0x4f, 0x1d, 0x21, 0x3d, 0xd6, 0xb3, 0x8e, 0x88, 0x1e, 0xeb, 0x59, 0xc7, 0x47, 0x8f, 0xf5, + 0xcc, 0xb1, 0xd3, 0x83, 0xc0, 0x04, 0x88, 0x3c, 0x60, 0x3d, 0x9a, 0x4c, 0x3d, 0xc5, 0x3d, 0x40, 0x34, 0x08, 0xac, + 0x27, 0x85, 0xf3, 0x1e, 0x20, 0x8f, 0x91, 0x58, 0x1d, 0xf4, 0xfe, 0x63, 0xfc, 0xb4, 0x67, 0x64, 0xe4, 0x71, 0xeb, + 0xb0, 0xfa, 0x5f, 0xff, 0x09, 0x01, 0x70, 0x78, 0x36, 0xf5, 0x2e, 0xc7, 0x90, 0x55, 0x96, 0x11, 0x48, 0x7e, 0x62, + 0xf0, 0xe5, 0x0b, 0x80, 0xaa, 0xcf, 0x74, 0xad, 0x26, 0x47, 0xed, 0x31, 0x87, 0xae, 0x18, 0x00, 0xb6, 0x61, 0x89, + 0xaa, 0x5a, 0xd8, 0x84, 0xc5, 0xed, 0x67, 0x18, 0xcd, 0x65, 0xd3, 0x0b, 0x1a, 0xa8, 0x47, 0x08, 0x7e, 0x69, 0x3d, + 0xb4, 0xd6, 0x32, 0xe5, 0xd0, 0xb5, 0x51, 0x54, 0xd9, 0x50, 0x97, 0xb0, 0x5a, 0x8b, 0xa8, 0x26, 0x8a, 0x94, 0x4b, + 0x46, 0x51, 0x2c, 0x55, 0xb0, 0xcf, 0xc4, 0x1d, 0x44, 0xcd, 0xd3, 0x56, 0x5b, 0x05, 0xfb, 0x3b, 0x40, 0x58, 0x0b, + 0x6b, 0x21, 0x9d, 0x41, 0xed, 0x9d, 0x7e, 0xa4, 0xfc, 0xe5, 0x85, 0xdc, 0xce, 0x2d, 0x14, 0xe1, 0xf6, 0x1c, 0x94, + 0x37, 0x75, 0x55, 0x2a, 0xa2, 0xd1, 0x12, 0x28, 0x65, 0x4e, 0x10, 0x59, 0x80, 0x00, 0x8e, 0x1b, 0x08, 0x7c, 0x5e, + 0xe3, 0x13, 0x68, 0x14, 0x02, 0xf9, 0x81, 0x55, 0xb8, 0xf6, 0x90, 0x96, 0x5a, 0x23, 0xa2, 0x44, 0xfc, 0xe8, 0xea, + 0x39, 0xb6, 0xaf, 0x9e, 0xc6, 0xda, 0x52, 0x9a, 0x20, 0x7e, 0x62, 0xb1, 0x85, 0x98, 0x20, 0xaa, 0x43, 0x74, 0x04, + 0xcb, 0x09, 0x21, 0x0a, 0x7f, 0x08, 0xfd, 0xd4, 0xc0, 0x5f, 0xb2, 0x65, 0x91, 0xd7, 0x04, 0x8b, 0x59, 0x31, 0x40, + 0xab, 0x22, 0xf0, 0x4c, 0x67, 0x4b, 0x65, 0x4e, 0xf3, 0xe8, 0xc8, 0x0e, 0xce, 0xbb, 0x0e, 0xf6, 0xd2, 0x97, 0xb1, + 0x93, 0x65, 0xd3, 0xa8, 0x8d, 0x0d, 0x91, 0xf0, 0x8a, 0xfc, 0x55, 0x96, 0x1a, 0xe7, 0xc8, 0x5c, 0xae, 0xef, 0xba, + 0xb8, 0xbb, 0xa3, 0x6d, 0xc2, 0x2a, 0x44, 0xa8, 0xdb, 0x86, 0xca, 0xa5, 0x30, 0x1b, 0x9b, 0xa6, 0x01, 0xbe, 0x50, + 0x54, 0x2a, 0x55, 0xa9, 0xad, 0x54, 0x72, 0xc2, 0xbb, 0xbe, 0xa9, 0x45, 0xea, 0x8a, 0x60, 0x1b, 0x33, 0xd4, 0x43, + 0xb9, 0x51, 0x63, 0xdf, 0x76, 0xac, 0xd2, 0x3b, 0x4c, 0x90, 0x33, 0xf2, 0x22, 0x07, 0x17, 0x25, 0x05, 0x99, 0xab, + 0x21, 0xcc, 0x1f, 0x35, 0x7c, 0x5a, 0x58, 0xee, 0xa1, 0x04, 0xcc, 0x8e, 0x1a, 0x5e, 0x46, 0x08, 0x44, 0x5c, 0x2a, + 0xfb, 0x8a, 0x89, 0xdf, 0x53, 0x30, 0x4b, 0x26, 0x74, 0x2f, 0x62, 0x61, 0x84, 0x36, 0x3e, 0x49, 0x92, 0xa9, 0xa7, + 0x29, 0xb8, 0x91, 0xcb, 0x30, 0x47, 0x23, 0xb4, 0xe4, 0x23, 0x07, 0xd2, 0xd7, 0x72, 0x2a, 0xc1, 0x47, 0xd4, 0x29, + 0xe0, 0x78, 0x7e, 0x5e, 0x58, 0x3f, 0x59, 0x2e, 0x31, 0x97, 0xb5, 0xf9, 0x2f, 0x3b, 0x3a, 0x06, 0xbb, 0x3c, 0x4d, + 0x1c, 0x57, 0xff, 0x51, 0x95, 0x14, 0x0f, 0x3f, 0xa7, 0x39, 0xa0, 0x08, 0x66, 0xf6, 0x14, 0xe3, 0x63, 0x9f, 0x65, + 0x0a, 0xf8, 0xdb, 0xf5, 0xd6, 0x92, 0x89, 0x5d, 0xd2, 0x6e, 0xae, 0x8c, 0x5f, 0x6a, 0xc3, 0x8e, 0x83, 0x73, 0x03, + 0x50, 0x9c, 0x35, 0x3a, 0x2c, 0xaf, 0x75, 0xdb, 0xaa, 0x50, 0x81, 0x5a, 0xff, 0x7b, 0xb7, 0x30, 0xe5, 0x6d, 0x5e, + 0x2a, 0x6f, 0xf3, 0xd0, 0x04, 0x08, 0x44, 0x66, 0xc8, 0xb3, 0xa6, 0x63, 0x92, 0xb8, 0x77, 0xa4, 0xa4, 0x7d, 0x47, + 0x8a, 0x1f, 0xbd, 0x23, 0x21, 0xdf, 0x12, 0x3a, 0xb2, 0x2f, 0x39, 0x39, 0x81, 0x32, 0x83, 0xbd, 0xbc, 0x66, 0xb2, + 0x7f, 0x40, 0x7b, 0xe1, 0x5c, 0x96, 0x57, 0xfc, 0x9d, 0xf0, 0xd6, 0xfe, 0x74, 0x7d, 0xda, 0x55, 0xf5, 0xf6, 0x1b, + 0x33, 0xf3, 0x70, 0x28, 0x0e, 0x87, 0xca, 0x04, 0xed, 0x2e, 0xb8, 0x18, 0xe4, 0xec, 0xde, 0x8d, 0x8f, 0x7f, 0xc7, + 0x51, 0xc4, 0x56, 0xca, 0x23, 0xe9, 0x42, 0x25, 0x86, 0x97, 0x06, 0x1e, 0x66, 0xc7, 0xc7, 0x93, 0xdd, 0xd5, 0xfd, + 0x64, 0x30, 0xd8, 0xa9, 0xbe, 0xdd, 0xf2, 0x7a, 0xb6, 0x9b, 0xb3, 0x07, 0x7e, 0x3b, 0xdd, 0x06, 0xfb, 0x06, 0xb6, + 0xdd, 0xdd, 0x95, 0x38, 0x1c, 0x76, 0xd7, 0x7c, 0xe1, 0xef, 0x1f, 0x10, 0xd0, 0x99, 0x9f, 0x8f, 0xdb, 0x18, 0x3f, + 0x37, 0x6d, 0x57, 0xad, 0x1d, 0xc0, 0xd3, 0xff, 0xe8, 0xdd, 0xcc, 0x96, 0x73, 0x9f, 0x3d, 0xe1, 0x0f, 0xe0, 0x9f, + 0x8f, 0x9b, 0x24, 0x52, 0x9f, 0x68, 0x97, 0xc9, 0x1b, 0x70, 0x20, 0xdf, 0xf9, 0xec, 0x2d, 0x7f, 0x98, 0x2d, 0xe7, + 0xbc, 0x38, 0x1c, 0x3e, 0x4c, 0x43, 0x24, 0x6b, 0x0a, 0x2b, 0x62, 0x49, 0xf1, 0xfc, 0x20, 0x3c, 0x7e, 0x2f, 0x22, + 0x43, 0xa4, 0xe5, 0xde, 0x1d, 0xb2, 0x1b, 0x16, 0xf9, 0x01, 0x7c, 0x90, 0xed, 0xfc, 0x89, 0xac, 0x29, 0xdd, 0x2f, + 0x9e, 0xf8, 0x87, 0x03, 0xfd, 0xf5, 0xd6, 0x3f, 0x1c, 0x3e, 0xb0, 0x07, 0x04, 0x47, 0xe7, 0x3b, 0xe8, 0x1f, 0x7d, + 0xeb, 0x80, 0xaa, 0x0c, 0xdf, 0xcd, 0x36, 0x73, 0xff, 0x7a, 0xc5, 0xee, 0x80, 0x0b, 0x45, 0x79, 0xa1, 0xdd, 0xb0, + 0x07, 0xf4, 0x3a, 0x23, 0x27, 0xa2, 0xd9, 0x6e, 0xee, 0xb3, 0x18, 0x9f, 0xab, 0xfb, 0x62, 0xf2, 0xcd, 0xfb, 0xe2, + 0x8e, 0x6d, 0xbb, 0xef, 0x8b, 0xf2, 0x4d, 0x77, 0xfd, 0x6c, 0xd9, 0x8e, 0x3d, 0xc0, 0x0c, 0x7b, 0xc7, 0x6f, 0x9a, + 0x63, 0xc7, 0xd8, 0x6f, 0xde, 0x18, 0x01, 0x94, 0xd9, 0x82, 0xc5, 0x82, 0x83, 0x52, 0xad, 0xda, 0x96, 0x44, 0x5e, + 0xe9, 0x40, 0xb5, 0x19, 0xc1, 0x7d, 0xb5, 0x90, 0x33, 0xcf, 0x0c, 0xf4, 0x6d, 0x85, 0x68, 0xe1, 0xb0, 0x01, 0x7f, + 0xa3, 0xad, 0x63, 0x0c, 0xd3, 0xac, 0x66, 0xda, 0x16, 0x75, 0xf9, 0x7d, 0xef, 0x99, 0xfc, 0x46, 0x06, 0xb6, 0x10, + 0x49, 0xe1, 0x38, 0xbe, 0x78, 0x7e, 0xc2, 0x7f, 0xd5, 0xf2, 0xa8, 0xd5, 0x7e, 0xa1, 0xd4, 0xa7, 0xaf, 0xe8, 0x88, + 0x26, 0xee, 0x45, 0x5b, 0x86, 0x35, 0xca, 0x9a, 0x5a, 0x3a, 0x0c, 0xe3, 0x1a, 0xf6, 0xe5, 0x81, 0x43, 0xdf, 0x01, + 0x81, 0xb6, 0x4a, 0xa5, 0x40, 0x0b, 0xc7, 0x30, 0x0a, 0xb3, 0x90, 0xf2, 0xb8, 0x30, 0x4b, 0x79, 0x8f, 0x05, 0x5a, + 0xdc, 0xaa, 0x7b, 0x4c, 0x6d, 0xb7, 0x20, 0xc2, 0xea, 0x2d, 0xe3, 0xfc, 0xb2, 0x51, 0x85, 0xdb, 0x02, 0x14, 0x45, + 0x50, 0x06, 0x7b, 0x92, 0xdb, 0x16, 0x4a, 0x9a, 0x8d, 0xc2, 0x5a, 0xdc, 0x15, 0xe5, 0xae, 0xd7, 0xb0, 0x05, 0x5e, + 0x50, 0xf5, 0x13, 0xc2, 0xb6, 0xec, 0x59, 0x87, 0x72, 0x91, 0xfe, 0x5b, 0x96, 0x9e, 0xef, 0xb7, 0xe6, 0xfc, 0x4f, + 0x5f, 0xd1, 0x47, 0xe5, 0xbf, 0x7f, 0x49, 0x3f, 0x19, 0x2c, 0x23, 0xa7, 0xd4, 0xcb, 0x68, 0x74, 0x9b, 0xe6, 0x84, + 0xb1, 0xe5, 0xeb, 0xa7, 0xdf, 0x21, 0x53, 0x90, 0x1c, 0x4a, 0xa9, 0xca, 0xc9, 0x1e, 0xfa, 0xc2, 0xeb, 0x3e, 0xcc, + 0x04, 0x03, 0x10, 0x5e, 0xa3, 0x4d, 0x35, 0x61, 0x12, 0x8f, 0xae, 0xe0, 0xff, 0x46, 0x10, 0x83, 0xf6, 0x89, 0xa2, + 0x8e, 0x6d, 0x23, 0x5d, 0xb7, 0x9d, 0x83, 0xe4, 0x4e, 0x5d, 0xf9, 0xa3, 0x72, 0xf2, 0xef, 0x68, 0x88, 0xbc, 0xe2, + 0x0a, 0xb1, 0xb2, 0xe0, 0x12, 0x8b, 0xa1, 0x22, 0x05, 0xb8, 0x86, 0x20, 0x52, 0x16, 0x25, 0x85, 0x5b, 0x0e, 0xaa, + 0x22, 0x00, 0xe3, 0x6a, 0x75, 0xd4, 0x89, 0xf0, 0x71, 0x6b, 0x2d, 0x42, 0xb0, 0xa2, 0x51, 0x2b, 0x6b, 0x05, 0xbe, + 0x20, 0x7d, 0xe9, 0x50, 0x10, 0xd3, 0xa3, 0x90, 0xaa, 0xd2, 0xa1, 0x40, 0x9a, 0x43, 0xc5, 0x37, 0x06, 0x1b, 0x45, + 0x45, 0x7a, 0xfe, 0xd2, 0xa4, 0xe4, 0xd2, 0x98, 0xf1, 0x51, 0x94, 0x91, 0xc8, 0xeb, 0xf0, 0x4e, 0x4c, 0x0b, 0xe4, + 0x1b, 0x3d, 0x7e, 0x10, 0x5c, 0xc2, 0xbb, 0x21, 0xf7, 0x0a, 0xb0, 0x25, 0x60, 0x07, 0xb8, 0x57, 0x66, 0x94, 0xeb, + 0xb4, 0xae, 0xdf, 0x5a, 0x0f, 0xc5, 0x30, 0x7c, 0x66, 0x09, 0x6c, 0x47, 0xeb, 0xe8, 0x48, 0x0f, 0x1f, 0xfe, 0xd7, + 0x55, 0xcd, 0x51, 0xa7, 0x72, 0x39, 0x3b, 0x9e, 0xb0, 0x14, 0x31, 0x83, 0xee, 0xaf, 0xdb, 0x57, 0x02, 0xe8, 0x76, + 0x59, 0xcc, 0xb3, 0xd1, 0x4e, 0xfe, 0x2d, 0xdd, 0x58, 0x51, 0xda, 0xc4, 0xbb, 0xac, 0x37, 0xf6, 0x87, 0xa3, 0xff, + 0x78, 0xf6, 0x75, 0x42, 0xa8, 0x3a, 0x1b, 0xb6, 0xd6, 0x71, 0x2e, 0xff, 0xeb, 0x3f, 0xc7, 0x64, 0x05, 0x41, 0x41, + 0x58, 0x76, 0x8a, 0x89, 0x0a, 0x46, 0x91, 0x62, 0xcd, 0xc7, 0x93, 0x35, 0xea, 0x84, 0xd7, 0xfe, 0x52, 0xeb, 0x84, + 0x89, 0x91, 0x95, 0xca, 0x5f, 0xb3, 0x8a, 0xdd, 0xa9, 0xcc, 0x02, 0x32, 0x0f, 0xf2, 0xc9, 0xda, 0x68, 0x30, 0x57, + 0xbc, 0x9e, 0xad, 0xe7, 0x52, 0xf9, 0x0c, 0xa6, 0x9c, 0xe5, 0xe0, 0x64, 0x29, 0xec, 0x9e, 0x04, 0x8a, 0xd6, 0x0c, + 0x5d, 0xfb, 0x53, 0x6c, 0xd5, 0xeb, 0xb4, 0xaa, 0x01, 0x1e, 0x10, 0x62, 0x60, 0xa8, 0xbd, 0x5a, 0x78, 0x68, 0x2d, + 0x80, 0xb5, 0x3f, 0x2a, 0xfd, 0x60, 0x3c, 0x59, 0xf2, 0x05, 0xf2, 0x2f, 0x47, 0x8e, 0xda, 0xbd, 0xdf, 0xf7, 0xee, + 0x41, 0x0a, 0x8e, 0x5c, 0x0b, 0x05, 0x12, 0x01, 0x2d, 0xf8, 0xc6, 0x57, 0x3e, 0x18, 0xef, 0x50, 0x5b, 0x0d, 0x0a, + 0x6a, 0x47, 0xb7, 0x3c, 0x76, 0xf4, 0xce, 0xf7, 0x27, 0xf4, 0xd5, 0x0b, 0x2d, 0x1c, 0x7f, 0xe3, 0x8c, 0x5c, 0xb3, + 0x55, 0x87, 0x1c, 0xd1, 0x4c, 0x3a, 0x84, 0x88, 0x15, 0x5b, 0xb3, 0x77, 0xa4, 0x72, 0xee, 0x1c, 0xb2, 0xd3, 0x47, + 0xa8, 0xd2, 0x6b, 0x3d, 0xbe, 0x9d, 0x28, 0xdd, 0xed, 0xf1, 0x6e, 0xf2, 0x3d, 0x9b, 0x88, 0x18, 0x0c, 0x68, 0x83, + 0x70, 0x46, 0xd6, 0x21, 0x52, 0xe9, 0x00, 0x21, 0x70, 0x4c, 0x40, 0xd3, 0x7f, 0x7d, 0x4b, 0xa2, 0x80, 0x23, 0x6d, + 0x84, 0xac, 0x65, 0x87, 0x43, 0x0e, 0x1a, 0xe5, 0xe6, 0x0f, 0xaf, 0x50, 0xa7, 0x39, 0x30, 0x4f, 0x97, 0xb0, 0xe7, + 0xe0, 0x91, 0x5e, 0x1c, 0x1f, 0xe9, 0xff, 0x1d, 0x4d, 0xd4, 0xf8, 0xdf, 0xd7, 0x44, 0x29, 0x2d, 0x92, 0xa3, 0x5a, + 0xfa, 0x2e, 0x75, 0x14, 0x5c, 0xe4, 0x1d, 0xb5, 0x90, 0x3d, 0xcb, 0xc6, 0x8d, 0x6a, 0xde, 0xff, 0xaf, 0x95, 0xf9, + 0xff, 0x9a, 0x56, 0x86, 0x29, 0xd9, 0xb1, 0x54, 0x33, 0x0f, 0xb4, 0x8a, 0x61, 0xf6, 0x33, 0x49, 0x88, 0x0c, 0x97, + 0x06, 0xfc, 0xa8, 0x82, 0x7d, 0x9c, 0x56, 0xeb, 0x2c, 0xdc, 0xa1, 0x12, 0xf5, 0x56, 0xdc, 0xa5, 0xf9, 0x8b, 0xfa, + 0x5f, 0xa2, 0x2c, 0x60, 0x6a, 0xdf, 0x95, 0x69, 0x1c, 0x90, 0x85, 0x3f, 0x0b, 0x4b, 0x9c, 0xdc, 0xd8, 0xc6, 0x9f, + 0xe5, 0x78, 0xda, 0xaf, 0x3a, 0x33, 0x0f, 0x24, 0x50, 0x03, 0xf1, 0x47, 0xce, 0x65, 0x65, 0xf1, 0x80, 0xd0, 0xcd, + 0x3f, 0x96, 0x65, 0x51, 0x7a, 0xbd, 0xcf, 0x49, 0x5a, 0x9d, 0xad, 0x44, 0x9d, 0x14, 0xb1, 0x82, 0xb2, 0x49, 0x01, + 0x46, 0x1f, 0x56, 0x9e, 0x88, 0x83, 0x33, 0x04, 0x6a, 0x38, 0xab, 0x93, 0x10, 0x80, 0x86, 0x15, 0xc2, 0xfe, 0x19, + 0xb4, 0xf0, 0x2c, 0x8c, 0xc3, 0x35, 0xc0, 0xe4, 0xa4, 0xd5, 0xd9, 0xba, 0x2c, 0xee, 0xd3, 0x58, 0xc4, 0xa3, 0x9e, + 0xa2, 0x64, 0x79, 0x93, 0xbb, 0x72, 0xae, 0xbf, 0xff, 0x83, 0x02, 0xd8, 0x0d, 0x98, 0x6d, 0x0b, 0xec, 0x00, 0x20, + 0x41, 0x81, 0x6c, 0xa1, 0x4e, 0xa3, 0x33, 0xb5, 0x54, 0xe0, 0x3d, 0xd7, 0x03, 0xfc, 0x4d, 0x0e, 0x58, 0xc6, 0x75, + 0x21, 0x03, 0x46, 0x10, 0xc0, 0x08, 0x1c, 0x94, 0x80, 0xa1, 0x33, 0xc4, 0x6d, 0x55, 0xce, 0x5a, 0x68, 0xae, 0x74, + 0x5b, 0x72, 0xd3, 0x28, 0x67, 0x2b, 0x11, 0x40, 0x5f, 0xdd, 0x94, 0x38, 0x5d, 0x2e, 0x5b, 0x49, 0xd8, 0xb7, 0x1f, + 0xda, 0xa9, 0x22, 0x8f, 0x8f, 0xd2, 0x90, 0x57, 0xe0, 0x49, 0xc6, 0x91, 0x24, 0x4a, 0x04, 0x6f, 0xf2, 0xc6, 0x8c, + 0xc3, 0x8b, 0x36, 0xe5, 0xd4, 0xde, 0xac, 0x17, 0x80, 0xf3, 0x04, 0x6d, 0x19, 0x60, 0x2c, 0x60, 0x70, 0x2e, 0xc4, + 0x92, 0xa7, 0x08, 0x7e, 0xe9, 0x44, 0x0a, 0xe3, 0x2e, 0x87, 0x61, 0x1e, 0x14, 0xbd, 0x4b, 0xea, 0x8f, 0x7e, 0x1f, + 0xb5, 0xc9, 0x60, 0x08, 0x2a, 0x01, 0x54, 0xd6, 0x0d, 0x12, 0x03, 0xab, 0xd2, 0x42, 0xe2, 0x12, 0xe2, 0x65, 0xbe, + 0x9a, 0xd6, 0x51, 0xf0, 0xa1, 0x9e, 0x10, 0xc2, 0x09, 0xc6, 0x87, 0xb8, 0x01, 0x02, 0x06, 0xab, 0xb8, 0xc0, 0x20, + 0x79, 0x2e, 0xd1, 0xfd, 0xf1, 0x7c, 0xc7, 0x00, 0x57, 0xce, 0x7b, 0xaa, 0x5d, 0x3d, 0xb0, 0x97, 0xab, 0x74, 0xc9, + 0x08, 0x61, 0xc5, 0xff, 0x45, 0xe4, 0x7d, 0x3b, 0x4c, 0x40, 0x6d, 0x23, 0x7f, 0x0c, 0x12, 0x73, 0x99, 0x28, 0x82, + 0x78, 0x94, 0x15, 0x2c, 0x49, 0x83, 0xcd, 0x28, 0x49, 0x41, 0xa3, 0x89, 0x31, 0x64, 0x2a, 0xb4, 0x43, 0xd2, 0x68, + 0x36, 0x26, 0xfb, 0x18, 0xf2, 0x1a, 0x2e, 0x16, 0x0b, 0xbc, 0xef, 0x67, 0xa1, 0x3a, 0xd8, 0x96, 0xe6, 0x10, 0x70, + 0x92, 0x60, 0x4f, 0x5d, 0x91, 0x92, 0x30, 0x1b, 0x7d, 0x0a, 0x39, 0x37, 0xa0, 0xe3, 0xa4, 0x31, 0x54, 0x1f, 0x98, + 0x84, 0x57, 0x11, 0x3a, 0x29, 0x2b, 0x84, 0x05, 0xdc, 0x37, 0x32, 0x1a, 0xad, 0xa4, 0x41, 0xe0, 0x6d, 0x86, 0xad, + 0xc0, 0x26, 0x34, 0xfc, 0x45, 0xe6, 0x61, 0x5a, 0xcd, 0x4a, 0x30, 0xe7, 0x1b, 0xa8, 0xc4, 0x78, 0xb2, 0xbc, 0xe2, + 0x1b, 0x17, 0x2b, 0x31, 0x99, 0x2d, 0xe7, 0x93, 0xb5, 0xa4, 0x9a, 0xcb, 0xbd, 0x35, 0xcb, 0xd8, 0x12, 0xf6, 0x0f, + 0x03, 0x43, 0xe9, 0xc0, 0x8e, 0xa6, 0x9a, 0x36, 0x09, 0x30, 0x99, 0xce, 0x39, 0x1f, 0x5e, 0x22, 0x9a, 0xac, 0x4e, + 0xdd, 0xc9, 0x54, 0xb5, 0x83, 0x6b, 0x72, 0x26, 0xa7, 0x47, 0xea, 0xa9, 0xd6, 0xbd, 0xe4, 0xa3, 0xed, 0xb0, 0x1a, + 0x6d, 0xfd, 0x00, 0xdc, 0x3a, 0x85, 0x9d, 0xbe, 0x1b, 0x56, 0xa3, 0x9d, 0xaf, 0x61, 0x77, 0x49, 0x21, 0x50, 0xfd, + 0x59, 0xd6, 0x64, 0x2e, 0x5e, 0x17, 0x0f, 0x5e, 0xc1, 0x9e, 0xfb, 0x03, 0xfd, 0xab, 0x64, 0xcf, 0x7d, 0x9b, 0xc9, + 0xf5, 0xcf, 0xb4, 0x6b, 0x34, 0x66, 0x3a, 0x5e, 0xbb, 0x02, 0x2b, 0x34, 0x40, 0x7e, 0xc1, 0x8e, 0xf6, 0x36, 0x07, + 0x81, 0x00, 0xdd, 0x4b, 0x70, 0x14, 0x05, 0x44, 0x4d, 0xab, 0xca, 0xa3, 0xd3, 0xbd, 0xbf, 0xc7, 0x37, 0x42, 0xc0, + 0x26, 0x4f, 0xad, 0x7b, 0xcb, 0xd8, 0x3f, 0x1c, 0x20, 0x84, 0x5e, 0x4e, 0xbf, 0xd1, 0x96, 0xd5, 0xa3, 0x1d, 0xcb, + 0x7d, 0xc3, 0xa8, 0xa7, 0x60, 0x0c, 0x43, 0x17, 0x56, 0x31, 0x92, 0x67, 0x40, 0xd6, 0xf8, 0x0d, 0xa2, 0x0b, 0x58, + 0xf4, 0x7a, 0xaf, 0x8f, 0x68, 0x10, 0x01, 0x95, 0x5e, 0xf3, 0x97, 0x22, 0x9f, 0xab, 0x42, 0xf4, 0xde, 0x5b, 0x3b, + 0x6f, 0x66, 0x24, 0xcb, 0xa4, 0x91, 0x6a, 0xb7, 0xb2, 0x58, 0x57, 0xde, 0xec, 0x84, 0x74, 0x31, 0xc7, 0x50, 0x19, + 0x3c, 0x0e, 0x40, 0xe9, 0xf9, 0x97, 0xd0, 0x2b, 0x19, 0x32, 0xcd, 0x12, 0xcd, 0xec, 0xae, 0xf1, 0x27, 0xab, 0xd4, + 0x8b, 0x11, 0x31, 0x1b, 0xd8, 0x42, 0xdc, 0x16, 0x95, 0x6e, 0x8b, 0x42, 0xd9, 0xa2, 0x48, 0x1f, 0x6a, 0x67, 0xba, + 0x33, 0x0b, 0x9f, 0x55, 0xa6, 0x7d, 0x6f, 0x33, 0x33, 0x36, 0x40, 0xdb, 0x45, 0xf8, 0x06, 0x3a, 0x50, 0x21, 0xe4, + 0x3f, 0x22, 0x22, 0x12, 0x01, 0xbb, 0x9c, 0xba, 0x13, 0x9b, 0x0e, 0xc9, 0x3c, 0xc4, 0xac, 0x50, 0xa3, 0xbc, 0xe4, + 0xc9, 0xd1, 0x80, 0x54, 0x84, 0xba, 0xdd, 0xef, 0x9f, 0x2f, 0x5d, 0x50, 0xfb, 0x35, 0xc5, 0x8e, 0xd1, 0x4d, 0x01, + 0xe7, 0x82, 0x47, 0x79, 0xcf, 0xbd, 0x73, 0x40, 0x73, 0x6c, 0x4f, 0x91, 0x35, 0xe0, 0xf4, 0xb6, 0x0b, 0x01, 0xb6, + 0xcf, 0x9a, 0xad, 0xfd, 0xc9, 0xea, 0x2a, 0x9a, 0x7a, 0x25, 0x9f, 0xe9, 0x2e, 0x4a, 0xdc, 0x2e, 0x8a, 0x65, 0x17, + 0x6d, 0x1a, 0x08, 0x76, 0x5c, 0xf9, 0x01, 0xf0, 0x86, 0x46, 0xfd, 0x7e, 0xd9, 0xea, 0xd9, 0x93, 0xaf, 0x1d, 0xf7, + 0x6c, 0xe6, 0xb3, 0xd2, 0xf4, 0xec, 0xaf, 0xa9, 0xdb, 0xb3, 0x72, 0xb2, 0x17, 0x9d, 0x93, 0x7d, 0x3a, 0x9b, 0x07, + 0x82, 0xcb, 0x9d, 0xfb, 0x3c, 0x9f, 0xea, 0x69, 0x57, 0xf9, 0x41, 0x6b, 0x88, 0xcc, 0x17, 0x3e, 0x57, 0xdd, 0xeb, + 0x0a, 0x16, 0xb0, 0x04, 0x77, 0xeb, 0xa5, 0xf9, 0xaf, 0xd8, 0xfd, 0xbd, 0xa0, 0x97, 0xe6, 0xbf, 0xd1, 0x9f, 0x14, + 0xc0, 0x01, 0x68, 0x4c, 0xed, 0x16, 0x78, 0x88, 0xa1, 0x82, 0xc2, 0xdd, 0xac, 0x9c, 0x7b, 0x35, 0xc0, 0x61, 0x92, + 0xbe, 0xa1, 0xd5, 0x2b, 0x2d, 0x76, 0xbd, 0x4c, 0xf6, 0x0a, 0xf0, 0x50, 0x85, 0x3c, 0x3c, 0x1c, 0xa2, 0x8e, 0x61, + 0x07, 0x75, 0x04, 0x0c, 0x7b, 0x08, 0x8d, 0x2d, 0xf0, 0x7c, 0xfc, 0x9c, 0xf1, 0xbd, 0x00, 0xb5, 0x11, 0xc2, 0xe3, + 0xd5, 0xa2, 0x0c, 0xb1, 0x65, 0x6f, 0x91, 0x4a, 0xea, 0x67, 0x81, 0x28, 0xa3, 0x55, 0x40, 0x5b, 0xed, 0x31, 0x4b, + 0xe3, 0x0d, 0x84, 0x8a, 0xa5, 0x3e, 0x86, 0xd0, 0xc0, 0xe1, 0x77, 0x38, 0x80, 0x04, 0x5f, 0x72, 0x4d, 0x36, 0xf7, + 0x36, 0xbf, 0xa7, 0x7d, 0xfe, 0x70, 0x38, 0xbf, 0x44, 0x50, 0xba, 0x14, 0x3e, 0x52, 0x89, 0xa8, 0x9e, 0xe2, 0xa6, + 0x84, 0x6c, 0x96, 0xac, 0xf4, 0x83, 0x5f, 0xd5, 0x2f, 0x00, 0x90, 0x85, 0x40, 0x9b, 0xc8, 0xec, 0x4f, 0x67, 0x2a, + 0xba, 0x00, 0x38, 0xc4, 0x1f, 0x3f, 0x41, 0xf4, 0x0d, 0x2d, 0xd3, 0xf2, 0x71, 0xc2, 0x43, 0xd0, 0xda, 0x92, 0x4e, + 0x22, 0x56, 0x0a, 0x6c, 0x88, 0x84, 0xef, 0xf7, 0xcf, 0x63, 0x49, 0x07, 0x1a, 0xb5, 0xba, 0x37, 0x6e, 0x75, 0xaf, + 0x7c, 0x5d, 0x77, 0x72, 0xe3, 0x83, 0xa2, 0x7d, 0x36, 0x6f, 0x54, 0xbe, 0xef, 0xeb, 0x9c, 0xdd, 0xe9, 0xde, 0x91, + 0x73, 0xe2, 0xfb, 0x7b, 0x08, 0x45, 0x0f, 0x4d, 0x91, 0x65, 0x49, 0x18, 0xd0, 0x5a, 0xbb, 0xf6, 0x2c, 0xa3, 0x83, + 0xd7, 0xbe, 0x21, 0x44, 0xe4, 0x29, 0x3e, 0x09, 0xb9, 0xc5, 0xf1, 0x41, 0x81, 0xfe, 0x99, 0xf1, 0x67, 0x4e, 0xfc, + 0xb0, 0xd5, 0x2f, 0x80, 0x73, 0xd3, 0xbd, 0x77, 0x27, 0x66, 0x3d, 0x86, 0x52, 0x36, 0xfe, 0xef, 0xf7, 0x89, 0x2c, + 0xd0, 0xe9, 0x88, 0x86, 0x81, 0xe0, 0x2e, 0xaa, 0xff, 0x7b, 0xc5, 0xeb, 0x9e, 0xb5, 0x3a, 0x5f, 0x7e, 0xea, 0xf4, + 0xa4, 0x57, 0x2f, 0xe3, 0x1e, 0x50, 0xa1, 0x03, 0x84, 0xf3, 0xba, 0xdf, 0xb0, 0xdd, 0x77, 0xbf, 0xbc, 0x3b, 0x7a, + 0x19, 0xd8, 0xa4, 0x48, 0x6c, 0x2b, 0xf9, 0xac, 0x07, 0x0a, 0xbf, 0x1e, 0xeb, 0xd5, 0xc5, 0xba, 0xc7, 0x7a, 0xa8, + 0x05, 0x44, 0x0f, 0x0b, 0x50, 0xff, 0xf5, 0xec, 0xd3, 0x50, 0x38, 0xc8, 0xc6, 0xa9, 0x02, 0x45, 0x16, 0xfc, 0x5a, + 0x8c, 0xd6, 0x05, 0x01, 0x22, 0x5b, 0x42, 0x5a, 0x75, 0x32, 0x7b, 0x5c, 0x6a, 0x49, 0x06, 0xdf, 0x04, 0x64, 0x76, + 0x60, 0xe5, 0x04, 0xa5, 0xe3, 0xd6, 0x80, 0x2b, 0x5b, 0x3c, 0xda, 0xed, 0x4f, 0x83, 0xec, 0xac, 0x39, 0x69, 0xb4, + 0x0f, 0xfb, 0x34, 0x0f, 0x10, 0x88, 0x64, 0x2a, 0x82, 0x5c, 0x73, 0x6f, 0x49, 0x1f, 0x1d, 0xce, 0x79, 0x21, 0xff, + 0x9c, 0x4a, 0x1d, 0xe2, 0x50, 0x62, 0x0d, 0x04, 0x2a, 0xcf, 0x50, 0xe5, 0xb0, 0x41, 0x8e, 0x7f, 0x76, 0x24, 0x33, + 0x89, 0xc9, 0x22, 0x77, 0x6b, 0xa6, 0xc2, 0x0f, 0x04, 0x1f, 0xb3, 0x9c, 0x03, 0x17, 0xd8, 0x6c, 0xee, 0xab, 0x29, + 0x2e, 0xae, 0xc0, 0x1f, 0x53, 0xf8, 0x15, 0x4f, 0x61, 0xa7, 0xdd, 0xaf, 0x8b, 0x2a, 0x45, 0xdd, 0x46, 0x61, 0x51, + 0xc9, 0x82, 0x69, 0x0d, 0x69, 0xa2, 0xc3, 0xe8, 0x0f, 0x72, 0x06, 0x0a, 0x42, 0x7e, 0xd9, 0x34, 0xc0, 0x48, 0x25, + 0x97, 0x07, 0x55, 0x12, 0x78, 0x01, 0xb6, 0x41, 0xc5, 0xd6, 0x05, 0x04, 0xd9, 0x26, 0x45, 0x99, 0x7e, 0x2d, 0xf2, + 0x3a, 0xcc, 0x82, 0x6a, 0x94, 0x56, 0x3f, 0xe9, 0x9f, 0xc0, 0xbc, 0x4d, 0xc5, 0xa8, 0x56, 0x31, 0xf9, 0x8d, 0x7e, + 0xbf, 0x18, 0xb4, 0x3e, 0x64, 0xf0, 0xd1, 0x6b, 0xd3, 0xe0, 0x4f, 0x4e, 0x83, 0x1d, 0x26, 0x1a, 0x01, 0x90, 0xcc, + 0xa9, 0x25, 0x0f, 0x45, 0x7f, 0x04, 0x39, 0xd6, 0xa8, 0x72, 0x0a, 0x06, 0xeb, 0x3f, 0x1e, 0xed, 0xc0, 0xd4, 0x8b, + 0xa3, 0x2d, 0xd9, 0x41, 0x2b, 0xdf, 0x00, 0xf7, 0x6b, 0x64, 0x8b, 0x59, 0x0e, 0xd0, 0xec, 0x35, 0x22, 0xe3, 0x93, + 0x17, 0xc0, 0x98, 0xad, 0xb3, 0x30, 0x12, 0x71, 0x30, 0x56, 0x8d, 0x19, 0x33, 0x30, 0x70, 0x81, 0xae, 0x65, 0x52, + 0x92, 0x86, 0x74, 0x30, 0x60, 0xa5, 0x6c, 0xe1, 0x80, 0x17, 0xcd, 0x71, 0x3b, 0xde, 0xb4, 0x68, 0x3c, 0xb0, 0x5d, + 0x6c, 0x7f, 0xff, 0xb2, 0xd8, 0xbe, 0x0b, 0xb7, 0xa4, 0x57, 0xc8, 0x59, 0x42, 0x3f, 0x7f, 0x92, 0x7d, 0xd6, 0x70, + 0x72, 0x2a, 0x34, 0x43, 0x4b, 0x91, 0x50, 0x8a, 0x77, 0x7a, 0x52, 0x60, 0x2c, 0x63, 0xe1, 0xef, 0x81, 0x73, 0xba, + 0x50, 0x44, 0xee, 0xc0, 0x71, 0x7c, 0x03, 0x15, 0x8c, 0x1a, 0x0e, 0x5e, 0xc6, 0xb0, 0x2d, 0x8a, 0x59, 0x48, 0x38, + 0x85, 0x70, 0xb1, 0xca, 0xfa, 0x7d, 0xf9, 0x8b, 0xba, 0xe8, 0x22, 0x93, 0x75, 0x9f, 0x84, 0x23, 0x33, 0x96, 0x53, + 0x2f, 0x24, 0xcf, 0x7b, 0x9e, 0x4c, 0x93, 0x67, 0x79, 0x10, 0x01, 0xe4, 0x73, 0x78, 0x1f, 0xa6, 0x19, 0x58, 0xa5, + 0x49, 0xf9, 0x11, 0x4a, 0x5f, 0x7c, 0x5e, 0xf9, 0x81, 0xce, 0x9e, 0x9b, 0x64, 0x78, 0xb3, 0x6a, 0xbd, 0x49, 0xad, + 0xeb, 0xe2, 0x01, 0xff, 0xea, 0x0c, 0x36, 0xce, 0x75, 0x26, 0x38, 0xf0, 0x22, 0xa9, 0xf5, 0x9a, 0xf1, 0xeb, 0x0c, + 0xd7, 0xa5, 0x6a, 0xa3, 0x8f, 0x42, 0x74, 0x0e, 0x99, 0x0a, 0x50, 0x28, 0xd2, 0xfe, 0x41, 0xa9, 0x95, 0x49, 0xa5, + 0x8d, 0x04, 0xd0, 0x3d, 0x4c, 0x1a, 0x6c, 0x31, 0x94, 0xb1, 0x34, 0x89, 0x72, 0xa7, 0x41, 0x5c, 0xd9, 0x9f, 0x2b, + 0x89, 0x43, 0xcb, 0x22, 0xf9, 0xf7, 0xae, 0xa7, 0xaf, 0x90, 0xba, 0x93, 0x05, 0x32, 0x63, 0xbc, 0xc8, 0xe3, 0xcf, + 0x40, 0x98, 0x0d, 0xda, 0xa8, 0x28, 0x84, 0x90, 0x0d, 0x62, 0xd0, 0x78, 0x91, 0xc7, 0x2f, 0x15, 0x8d, 0x87, 0x7c, + 0x14, 0xf9, 0xea, 0xaf, 0x52, 0xff, 0x15, 0xfa, 0xcc, 0x04, 0x8f, 0x50, 0x4d, 0xf4, 0xef, 0x9e, 0xcf, 0xee, 0x41, + 0x6d, 0x18, 0x85, 0x99, 0x29, 0xbf, 0xf2, 0x4d, 0x71, 0xf6, 0xfa, 0x2b, 0xba, 0xca, 0xb6, 0xee, 0x47, 0x9f, 0x8e, + 0x08, 0xac, 0x8d, 0xd1, 0x15, 0x37, 0x06, 0x90, 0xc3, 0xe4, 0xfd, 0x8a, 0xd2, 0x72, 0x48, 0x83, 0xd0, 0x41, 0x43, + 0xd0, 0x2b, 0x89, 0x3e, 0x90, 0x58, 0xc4, 0x18, 0x5e, 0x88, 0x67, 0xa4, 0x26, 0x13, 0x0d, 0xf1, 0x8a, 0xd8, 0x0f, + 0xd1, 0x92, 0x53, 0x13, 0xdd, 0x08, 0x53, 0x0c, 0x24, 0x76, 0x06, 0xc9, 0x49, 0x52, 0x2b, 0xbf, 0x78, 0x26, 0x09, + 0x4b, 0xec, 0x3c, 0xc4, 0x60, 0x52, 0x4b, 0x77, 0x7a, 0x53, 0xa5, 0xf7, 0x47, 0x5a, 0x0e, 0xda, 0x07, 0x60, 0x97, + 0x92, 0xde, 0x3f, 0x29, 0x14, 0xf1, 0x31, 0x8c, 0x63, 0x08, 0xdf, 0x22, 0xaa, 0x2b, 0x70, 0xae, 0x15, 0x68, 0xac, + 0x06, 0x1e, 0x9a, 0x59, 0x35, 0x1f, 0x72, 0xfa, 0xa9, 0xb4, 0xfc, 0x31, 0xa2, 0xb1, 0xd1, 0xba, 0x39, 0x1c, 0xf6, + 0xb4, 0xea, 0xa5, 0x73, 0xd0, 0x65, 0x33, 0x89, 0x89, 0x1b, 0x48, 0xd7, 0x8f, 0x7e, 0x33, 0x61, 0x2f, 0xa2, 0x42, + 0x2e, 0x85, 0xa0, 0xa0, 0xd5, 0x81, 0xc0, 0xa1, 0xf0, 0x16, 0x65, 0xbe, 0x88, 0x69, 0x03, 0x61, 0xf0, 0xf9, 0x81, + 0xfc, 0x7c, 0x53, 0x90, 0x8a, 0x1d, 0xeb, 0xda, 0xef, 0x6f, 0x4a, 0x0f, 0xf0, 0xe4, 0x4c, 0x92, 0xa7, 0xcd, 0x10, + 0x56, 0x04, 0xd0, 0x98, 0xd5, 0x64, 0x71, 0xc2, 0x95, 0x39, 0xfc, 0x54, 0x79, 0x25, 0x4b, 0x99, 0x3a, 0x4f, 0xf5, + 0x02, 0x88, 0x3a, 0xde, 0xa0, 0x15, 0xa9, 0x5f, 0xa1, 0xb3, 0xd7, 0xac, 0x84, 0x8c, 0x87, 0xe7, 0x9c, 0xa7, 0xa3, + 0x07, 0x96, 0xf0, 0x08, 0xff, 0x4a, 0x26, 0xfa, 0xf0, 0x7b, 0xe0, 0x70, 0x33, 0x4e, 0x78, 0xe4, 0x36, 0x7b, 0x5f, + 0x85, 0x2b, 0xb8, 0x99, 0x16, 0x80, 0xe4, 0x16, 0x24, 0x4d, 0x40, 0x09, 0x89, 0x4c, 0xc8, 0xac, 0x29, 0xf9, 0xa5, + 0xa5, 0x6d, 0xb0, 0x86, 0x49, 0xe7, 0x01, 0x2f, 0x5a, 0x7d, 0xb4, 0x9a, 0x68, 0x97, 0x59, 0x3e, 0x1f, 0xe2, 0x0c, + 0xd5, 0x1c, 0x77, 0x67, 0xf0, 0x73, 0xc0, 0x2b, 0x56, 0x35, 0xe9, 0x68, 0x37, 0xe0, 0xc2, 0x93, 0xeb, 0x3c, 0x1d, + 0x6d, 0xf1, 0x97, 0xdc, 0x1f, 0x00, 0x3a, 0x98, 0xba, 0x04, 0xfe, 0x54, 0x6d, 0x35, 0x95, 0xfa, 0xa5, 0xb5, 0x5f, + 0xd7, 0x9d, 0xd5, 0xca, 0x3d, 0xeb, 0x32, 0xb4, 0x47, 0x86, 0x9c, 0x31, 0x03, 0xfe, 0x9c, 0xb1, 0xe4, 0xcf, 0x19, + 0x2b, 0xfe, 0x9c, 0x71, 0x63, 0x64, 0x00, 0x25, 0xb8, 0x97, 0xfc, 0x7a, 0x8f, 0x98, 0x21, 0x56, 0x83, 0x4a, 0x60, + 0x65, 0x29, 0xe7, 0x3e, 0x72, 0x8a, 0x29, 0xa7, 0x0c, 0x2f, 0x9d, 0xce, 0xdc, 0x81, 0x9c, 0x07, 0x33, 0x77, 0x98, + 0x9c, 0xf5, 0x29, 0x8e, 0xa5, 0x31, 0x29, 0x2a, 0x48, 0xe7, 0x74, 0xb8, 0x79, 0x75, 0x9c, 0x27, 0x2c, 0xe3, 0xe3, + 0xf6, 0x99, 0x02, 0x21, 0xb6, 0x78, 0x86, 0x44, 0x4a, 0xd5, 0x2c, 0xb7, 0xf9, 0xc3, 0xa1, 0x1e, 0x3d, 0xe8, 0x9d, + 0x1e, 0x7e, 0x25, 0xec, 0x97, 0xcc, 0xb3, 0x4f, 0x10, 0xc0, 0x24, 0x91, 0x67, 0x12, 0x8e, 0x7e, 0x2c, 0x47, 0x7f, + 0xd3, 0xf0, 0xf7, 0x19, 0xaa, 0xbb, 0x43, 0x60, 0x62, 0xcb, 0x0e, 0x1c, 0x82, 0xd3, 0x55, 0x25, 0x12, 0x70, 0xb0, + 0xd9, 0xb0, 0x48, 0xef, 0xf1, 0x10, 0xe7, 0x83, 0xc2, 0x47, 0x68, 0x98, 0xd1, 0xfb, 0xfd, 0x8d, 0xf0, 0x2a, 0xd9, + 0xca, 0xc3, 0x21, 0xb1, 0xee, 0xc2, 0x8e, 0x3e, 0x8e, 0xf6, 0x28, 0xa1, 0xf6, 0xa3, 0x5a, 0x6f, 0x2a, 0xf5, 0x20, + 0x37, 0xbb, 0x90, 0x18, 0x54, 0x2c, 0xd5, 0xa7, 0x57, 0xaa, 0x0f, 0x35, 0xeb, 0xfc, 0xae, 0x8e, 0xfb, 0x54, 0x8c, + 0xd6, 0x72, 0x42, 0x80, 0xeb, 0x20, 0xd1, 0xe8, 0x00, 0x18, 0x67, 0x9b, 0x2d, 0x2f, 0xb5, 0x75, 0xa2, 0x74, 0x1c, + 0xe7, 0xfa, 0x38, 0x3e, 0x1c, 0xa4, 0x98, 0x71, 0x79, 0x24, 0x66, 0x5c, 0x36, 0x00, 0x6f, 0xd6, 0x79, 0x50, 0x1f, + 0x0e, 0x97, 0x74, 0x29, 0x32, 0x9d, 0x6d, 0x94, 0x9f, 0xf5, 0xe8, 0xe1, 0x59, 0x82, 0xe6, 0xde, 0x0a, 0x7b, 0x2f, + 0x92, 0xed, 0x99, 0xac, 0x53, 0x2f, 0x23, 0x9f, 0x5e, 0xb8, 0x67, 0x97, 0x5c, 0xfd, 0xb0, 0xfa, 0x7a, 0xfa, 0xab, + 0xf0, 0x22, 0x56, 0xd1, 0x6e, 0x5d, 0x32, 0x61, 0x6f, 0x29, 0x95, 0xb4, 0xca, 0xcb, 0xa7, 0x1b, 0x3f, 0xc0, 0xcc, + 0xb4, 0xa7, 0x0f, 0xb2, 0x11, 0xd5, 0x9f, 0x95, 0xa8, 0x95, 0x61, 0xb2, 0x70, 0x5e, 0x32, 0xf5, 0x64, 0xc0, 0x63, + 0x56, 0xf2, 0x48, 0x76, 0x7a, 0x63, 0x10, 0x04, 0xb0, 0xce, 0x49, 0xab, 0xce, 0x38, 0x1a, 0xad, 0x2a, 0x17, 0xa7, + 0xab, 0x5c, 0x60, 0xb8, 0xdd, 0x9a, 0x6d, 0x54, 0x9d, 0xe5, 0xa6, 0x56, 0x29, 0xdf, 0x01, 0x7c, 0x2c, 0xab, 0x5c, + 0xd0, 0x31, 0x65, 0xea, 0xbc, 0x81, 0x60, 0x6c, 0x55, 0xe3, 0xc2, 0xa9, 0x71, 0xc1, 0x23, 0x6a, 0x77, 0xd3, 0xd4, + 0xa3, 0x2d, 0xb0, 0x94, 0x8e, 0x76, 0xbc, 0x44, 0x95, 0xc2, 0xdf, 0x04, 0xdf, 0x87, 0x71, 0xfc, 0xb2, 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, + 0x17, 0x59, 0xb8, 0x13, 0x25, 0x38, 0x72, 0xc9, 0xbf, 0x0e, 0x07, 0xad, 0xb2, 0x54, 0x47, 0xfa, 0x6c, 0xff, 0x35, + 0x18, 0x33, 0x74, 0x69, 0x02, 0x96, 0x8d, 0x91, 0xfc, 0xab, 0x69, 0xe6, 0x0d, 0x93, 0x35, 0x53, 0x38, 0x0e, 0x0d, + 0x23, 0xa4, 0x01, 0xdd, 0x06, 0xb5, 0xe1, 0xc9, 0x7c, 0x53, 0x95, 0x5f, 0xdd, 0x91, 0x6a, 0x3f, 0x18, 0x5e, 0x4e, + 0xc4, 0x39, 0x5d, 0x92, 0xd4, 0x53, 0x09, 0x25, 0x21, 0xd8, 0xa5, 0x0f, 0xe4, 0xc4, 0x0a, 0xc8, 0x5a, 0xc6, 0xf2, + 0x5b, 0x3d, 0x20, 0xf4, 0x9f, 0x76, 0xeb, 0x85, 0xfe, 0xd3, 0x34, 0x5b, 0xa8, 0xeb, 0x0f, 0x93, 0xfb, 0x8e, 0x5e, + 0x7f, 0x70, 0x78, 0xa7, 0xae, 0x2a, 0xae, 0xe2, 0x51, 0x6d, 0x98, 0xe4, 0x46, 0x59, 0xb8, 0x2b, 0x36, 0xb5, 0x5a, + 0x9e, 0x8e, 0xc3, 0x08, 0xcc, 0x08, 0x0a, 0x90, 0x75, 0xdd, 0x46, 0xc4, 0xb0, 0x92, 0xcb, 0x84, 0x7c, 0x42, 0x40, + 0x16, 0xa5, 0xc6, 0xf9, 0xb8, 0x05, 0x2a, 0x11, 0x0c, 0x4e, 0x43, 0x6b, 0xd5, 0x4d, 0x7e, 0x52, 0xd9, 0xd8, 0x1d, + 0x90, 0x43, 0x92, 0xc9, 0xe2, 0x6e, 0x74, 0x2b, 0x96, 0x45, 0x29, 0x7e, 0xc6, 0x7a, 0xb8, 0x66, 0x0b, 0xf7, 0x19, + 0x10, 0xda, 0x4f, 0x94, 0xf6, 0x26, 0xd2, 0x04, 0xdd, 0x77, 0x6c, 0x05, 0x20, 0x03, 0x28, 0xea, 0x6a, 0xb7, 0x3e, + 0xe7, 0xe7, 0x48, 0x9a, 0xe1, 0x30, 0xba, 0x7d, 0x7a, 0x17, 0xdc, 0x0d, 0x2e, 0x51, 0x2b, 0x7d, 0xc9, 0xe2, 0x16, + 0x06, 0xd5, 0xde, 0x2c, 0xe1, 0xa0, 0x66, 0xd6, 0xda, 0x08, 0x04, 0x93, 0x3d, 0x14, 0x54, 0xcc, 0x15, 0xec, 0x83, + 0x82, 0xb5, 0xe4, 0x75, 0x70, 0xb8, 0xb5, 0x2f, 0x2b, 0xc5, 0xc5, 0xf3, 0x8b, 0xa4, 0x75, 0x61, 0x29, 0x2f, 0x9e, + 0x37, 0x60, 0x70, 0x39, 0xc2, 0xa6, 0xaa, 0xfc, 0xc9, 0x06, 0x40, 0xb7, 0x22, 0x8a, 0x78, 0x51, 0x0a, 0xdb, 0x56, + 0x3e, 0x73, 0xc2, 0x06, 0x1b, 0xf6, 0x00, 0xf7, 0xca, 0xa0, 0x64, 0x70, 0x21, 0xc6, 0xed, 0x66, 0x17, 0xe0, 0x0a, + 0x86, 0xc2, 0xd8, 0x9a, 0xbf, 0xc9, 0xbc, 0x48, 0x09, 0xb8, 0x19, 0xa2, 0x7c, 0x6d, 0xe0, 0x64, 0xd2, 0x93, 0x6b, + 0xc9, 0x62, 0xc0, 0x82, 0x06, 0xdf, 0x51, 0xeb, 0xef, 0x4c, 0xfe, 0x8d, 0xa7, 0x87, 0x7e, 0xf0, 0x25, 0xf3, 0x96, + 0x3e, 0x7b, 0x53, 0xc9, 0x68, 0x4d, 0x12, 0xe5, 0xd5, 0xc3, 0x25, 0xc8, 0x0d, 0xcb, 0xd1, 0x03, 0x5b, 0x82, 0x38, + 0xb1, 0x1c, 0x25, 0x94, 0xd1, 0x15, 0xee, 0x55, 0x66, 0xcb, 0x44, 0x20, 0xc5, 0x81, 0xa5, 0x94, 0x7b, 0x8b, 0x75, + 0xb0, 0xc4, 0xfd, 0x89, 0xe4, 0x02, 0x4a, 0x1e, 0x40, 0xb9, 0x52, 0x40, 0xc0, 0xa7, 0x03, 0x28, 0x5f, 0xca, 0x8b, + 0xf0, 0x27, 0x4e, 0xd4, 0x60, 0x39, 0x7a, 0x68, 0xd8, 0x4f, 0x5e, 0x68, 0xd9, 0x1f, 0xee, 0xb4, 0xa6, 0x61, 0xc5, + 0xef, 0x60, 0x5a, 0x4c, 0xdc, 0xbe, 0x5c, 0xd9, 0x55, 0xf1, 0xd9, 0x4a, 0x9d, 0xdd, 0xd4, 0x90, 0x84, 0x7d, 0x43, + 0x56, 0x01, 0x0e, 0x56, 0x45, 0xdc, 0xb3, 0x2c, 0xf7, 0x61, 0xf4, 0xe7, 0x26, 0x2d, 0x85, 0x85, 0x2a, 0xe9, 0xef, + 0x9b, 0x52, 0x20, 0x95, 0x89, 0x4e, 0xb4, 0x10, 0x5c, 0x81, 0x41, 0xe0, 0x5e, 0xe4, 0x35, 0x00, 0xc6, 0x80, 0x4b, + 0x81, 0xb2, 0x6c, 0x4b, 0x08, 0xa9, 0xee, 0x67, 0xa0, 0xb6, 0x13, 0xf7, 0x69, 0x44, 0xd6, 0x42, 0xf4, 0x55, 0x30, + 0x66, 0xce, 0x4b, 0xe9, 0x16, 0x9b, 0xae, 0x36, 0xab, 0x1b, 0x74, 0x2e, 0x6d, 0xb9, 0xf9, 0x09, 0x5b, 0xac, 0x15, + 0x28, 0x9b, 0x90, 0xb4, 0x9d, 0xf3, 0x1c, 0x65, 0x13, 0x5a, 0xda, 0x7b, 0xea, 0x51, 0xa1, 0x3a, 0xd9, 0x7a, 0xa9, + 0x9a, 0x5a, 0x84, 0xd5, 0xe2, 0xa2, 0xf2, 0x03, 0xd0, 0x4d, 0xa5, 0xd5, 0x8b, 0xba, 0x46, 0x53, 0xa8, 0xd5, 0xc2, + 0x71, 0xa3, 0x9d, 0x4d, 0x97, 0xe9, 0x1d, 0xe2, 0xac, 0x4a, 0x3b, 0xf4, 0xcb, 0x4c, 0xbb, 0x5e, 0x76, 0xf4, 0x9b, + 0x71, 0x75, 0x81, 0x0b, 0xb1, 0x01, 0x9f, 0x73, 0x7f, 0x79, 0xbd, 0xe7, 0x71, 0xcf, 0x3f, 0x1c, 0x90, 0x3d, 0xa9, + 0xfd, 0xa1, 0xfa, 0xd8, 0x15, 0x0c, 0x59, 0x18, 0xa5, 0xfe, 0x22, 0xe5, 0xbd, 0x27, 0x38, 0xee, 0x5f, 0xaa, 0x1e, + 0xfb, 0x29, 0xe3, 0xfb, 0xba, 0xd8, 0x44, 0x09, 0x45, 0x35, 0xf4, 0x56, 0xc5, 0xa6, 0x12, 0x71, 0xf1, 0x90, 0xf7, + 0x18, 0x26, 0xc3, 0x58, 0xc8, 0x54, 0xf8, 0x53, 0xa6, 0x82, 0x47, 0x08, 0x25, 0x6e, 0xd6, 0x3d, 0xd2, 0x6e, 0x42, + 0x9c, 0x52, 0x2d, 0x4a, 0x99, 0x8c, 0x7f, 0xeb, 0x27, 0x50, 0x9e, 0x53, 0xb4, 0x4c, 0x3f, 0x2a, 0x5c, 0xa6, 0x6f, + 0xd6, 0xc7, 0xa5, 0x67, 0x22, 0xd4, 0x99, 0x8b, 0x4d, 0xad, 0xd3, 0x31, 0x76, 0x4a, 0xa7, 0x36, 0xec, 0x6b, 0xa5, + 0xb8, 0xac, 0x28, 0xfc, 0x1b, 0x89, 0xac, 0x7a, 0x46, 0x1c, 0xff, 0x67, 0xd6, 0x3e, 0xc3, 0x2a, 0xf0, 0xcb, 0x40, + 0xde, 0x2f, 0x00, 0x3e, 0xae, 0xeb, 0x32, 0xbd, 0xdd, 0x00, 0x6d, 0x08, 0x0d, 0x7f, 0xcf, 0x47, 0x06, 0x4c, 0xf7, + 0x11, 0xce, 0x90, 0x1e, 0xea, 0x9c, 0xd3, 0x59, 0x99, 0xce, 0xb9, 0x0a, 0x6b, 0x09, 0xf6, 0x72, 0xd2, 0xe4, 0x72, + 0x5d, 0x82, 0x9a, 0x09, 0xdc, 0x3e, 0xb4, 0x47, 0x84, 0x50, 0x9b, 0xb2, 0x9a, 0x5e, 0x42, 0xcd, 0x3b, 0x39, 0xed, + 0x68, 0x52, 0x82, 0xab, 0x86, 0xce, 0xca, 0xf5, 0x5f, 0x87, 0x43, 0xef, 0x36, 0x2b, 0xa2, 0x3f, 0x7a, 0xe8, 0xef, + 0xb8, 0xbd, 0x49, 0xbf, 0x42, 0xb4, 0x8c, 0xf5, 0x37, 0x64, 0x40, 0xc7, 0x93, 0xe1, 0x6d, 0xb1, 0xed, 0xb1, 0xaf, + 0xa8, 0xc1, 0xd2, 0xd7, 0x8f, 0x3f, 0x40, 0x42, 0xd5, 0xb5, 0x2f, 0x2c, 0x9e, 0x30, 0x4f, 0x89, 0xb6, 0x85, 0x0f, + 0x61, 0xa1, 0x5f, 0x21, 0x32, 0x12, 0xc2, 0x4d, 0x65, 0xf7, 0x28, 0x69, 0x17, 0xfa, 0xd2, 0xd7, 0xb2, 0xaf, 0x7c, + 0xe7, 0x02, 0x60, 0x65, 0x9f, 0xdb, 0x70, 0x4f, 0xfa, 0x53, 0xaa, 0x0f, 0xdb, 0xdf, 0x92, 0x05, 0x14, 0x5a, 0x58, + 0x4f, 0xe5, 0xec, 0x5c, 0x97, 0x3c, 0xcd, 0xa6, 0xfb, 0x35, 0xec, 0x51, 0xf7, 0xe8, 0x35, 0x15, 0x9c, 0x5f, 0x9a, + 0xd1, 0xfb, 0x87, 0xa1, 0x50, 0x1d, 0x75, 0xee, 0x20, 0xeb, 0xd2, 0xba, 0xe4, 0xfc, 0x66, 0xe5, 0x8e, 0xc2, 0xfc, + 0x3e, 0x04, 0xcf, 0xb0, 0xee, 0xdd, 0xc5, 0x79, 0xef, 0xcf, 0xd6, 0x1c, 0xf9, 0x29, 0x9b, 0xa5, 0x88, 0x45, 0x32, + 0x07, 0xab, 0x1f, 0xfa, 0x79, 0xec, 0xb7, 0x41, 0x0e, 0xc7, 0x4d, 0x03, 0x3a, 0x6c, 0xc8, 0xac, 0x7d, 0x89, 0xc0, + 0xa9, 0x46, 0x90, 0xa6, 0x26, 0xa8, 0x59, 0x1e, 0x22, 0xb1, 0x5d, 0xca, 0xb6, 0x41, 0xae, 0xbb, 0x60, 0x9a, 0x23, + 0xed, 0x19, 0xbc, 0x6f, 0xd2, 0x24, 0x15, 0x9a, 0x45, 0xda, 0x2a, 0x19, 0xff, 0x8e, 0xb4, 0x99, 0x92, 0x3d, 0xb6, + 0x06, 0xde, 0x4b, 0x50, 0x4e, 0x86, 0x29, 0x86, 0xef, 0xf8, 0x7a, 0xe7, 0x31, 0xf7, 0x9c, 0x63, 0xb6, 0x49, 0xd9, + 0x11, 0x4c, 0x92, 0x8d, 0x6f, 0x28, 0xde, 0xf0, 0xc3, 0x6d, 0x25, 0x4a, 0x00, 0xbd, 0x2c, 0xf8, 0xb5, 0xb4, 0xb9, + 0x42, 0xb7, 0xbb, 0x77, 0x94, 0xc2, 0x2f, 0x79, 0x79, 0x38, 0x6c, 0x53, 0x2f, 0x84, 0xce, 0x17, 0xf1, 0x7b, 0x30, + 0x87, 0x31, 0xc4, 0x66, 0x04, 0x08, 0x73, 0x7c, 0x40, 0x1d, 0xac, 0x1f, 0x01, 0x68, 0x9c, 0x40, 0x01, 0x46, 0x5f, + 0x6d, 0x0b, 0xfa, 0x96, 0x17, 0x17, 0x11, 0xa2, 0x46, 0x01, 0x26, 0x4a, 0x9a, 0xc5, 0x30, 0x1c, 0xe8, 0xfc, 0xbe, + 0xb9, 0xad, 0x4b, 0x81, 0x43, 0xef, 0x58, 0x86, 0xff, 0xf6, 0x3f, 0xd6, 0x96, 0x56, 0x95, 0xed, 0xd6, 0x38, 0xcd, + 0xfc, 0x6f, 0xb7, 0x85, 0xbe, 0xff, 0x4a, 0x28, 0x9e, 0x77, 0xbc, 0x6e, 0xbf, 0x87, 0xe8, 0x7d, 0xdd, 0xca, 0xbb, + 0x52, 0xbb, 0x61, 0xa6, 0xfc, 0x21, 0xcd, 0xe3, 0xe2, 0x61, 0x14, 0xb7, 0x8e, 0xbc, 0x49, 0x7a, 0xce, 0xf9, 0xfb, + 0xaa, 0xdf, 0xf7, 0xde, 0x03, 0x19, 0xef, 0x2b, 0x61, 0x1c, 0x31, 0x89, 0x83, 0x6f, 0x2f, 0x46, 0xd1, 0xa6, 0x84, + 0x0d, 0xb9, 0x7d, 0x5a, 0x82, 0x66, 0xa6, 0xdf, 0x47, 0x89, 0xd2, 0x9a, 0xef, 0x7f, 0x91, 0xf3, 0xfd, 0x95, 0x90, + 0x37, 0x2b, 0xf9, 0xe1, 0xa3, 0x15, 0x06, 0xbe, 0xc7, 0xe9, 0x57, 0xd1, 0x63, 0x77, 0xa5, 0x0f, 0xdf, 0x95, 0x96, + 0x3e, 0xab, 0xa8, 0xbf, 0xa3, 0xa2, 0xe6, 0x95, 0x18, 0x11, 0xf1, 0x20, 0x68, 0x67, 0xdb, 0xa5, 0x76, 0x2d, 0x41, + 0xbb, 0x60, 0x53, 0xd8, 0xbf, 0x1e, 0x1d, 0xf2, 0x7e, 0xff, 0x53, 0xee, 0xb5, 0x78, 0xdd, 0x75, 0x68, 0xca, 0x5f, + 0x0b, 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, 0x13, 0xd6, 0x4d, 0x4f, 0xab, 0xd0, 0xf2, + 0x35, 0x04, 0xeb, 0x22, 0xcb, 0x38, 0x8a, 0x99, 0x00, 0x60, 0xf3, 0x09, 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, 0xb7, 0x54, 0xf5, 0x88, 0x40, 0xaf, 0xca, 0xb4, 0x8a, 0x8a, 0x3c, 0x17, + 0x11, 0x22, 0x54, 0x4b, 0xe7, 0x70, 0xe8, 0xc7, 0x7e, 0x1f, 0x07, 0xc2, 0xbc, 0xf8, 0xd7, 0xc7, 0xba, 0xf2, 0xaf, + 0x05, 0xae, 0x95, 0x14, 0x38, 0x15, 0x35, 0x42, 0x84, 0xf0, 0xfe, 0x04, 0x9e, 0xd5, 0xd4, 0xf7, 0x1b, 0xcb, 0x44, + 0xf7, 0x8f, 0x0c, 0x28, 0x7f, 0x40, 0xbe, 0xae, 0xa4, 0x38, 0x53, 0x27, 0x8f, 0x89, 0x33, 0x0e, 0x40, 0xcc, 0xb7, + 0x25, 0x1a, 0x8d, 0xfd, 0x0f, 0x48, 0x30, 0x54, 0x3f, 0xd8, 0xe9, 0xa6, 0xde, 0x3f, 0x33, 0x89, 0xa3, 0xe8, 0xd3, + 0x36, 0x79, 0x2c, 0x59, 0x1a, 0x2d, 0x1c, 0xbd, 0x47, 0x0c, 0xe3, 0x70, 0x3a, 0x1f, 0x93, 0x6c, 0x63, 0xb2, 0x0a, + 0x20, 0x9d, 0xcc, 0xd4, 0x31, 0xa5, 0x8e, 0xc6, 0xb9, 0x5e, 0x50, 0x85, 0x1e, 0xeb, 0x92, 0xe7, 0x60, 0x3d, 0xf9, + 0xd1, 0x2b, 0xfd, 0xa9, 0x90, 0x73, 0xd8, 0x48, 0x04, 0x85, 0x1f, 0xe0, 0x6a, 0xb0, 0x52, 0xc0, 0x60, 0xea, 0x5b, + 0xf8, 0x9a, 0x78, 0x8e, 0x82, 0x47, 0x61, 0x17, 0x63, 0x6b, 0xe5, 0x3b, 0x9f, 0x14, 0x94, 0x7b, 0x56, 0xcc, 0x79, + 0x05, 0x9c, 0xcb, 0xa0, 0x10, 0xa6, 0xe3, 0x59, 0xfe, 0xcf, 0x24, 0xaf, 0x27, 0x36, 0x04, 0xc8, 0xe0, 0x4f, 0x89, + 0xd3, 0xd2, 0x1d, 0xba, 0xf3, 0xd0, 0xb3, 0x88, 0xc3, 0x46, 0x4f, 0xd6, 0x65, 0xb1, 0x4d, 0x51, 0x2f, 0x61, 0x7e, + 0x20, 0x3f, 0x6f, 0xc9, 0xf7, 0x21, 0x8a, 0xb7, 0xc1, 0xaf, 0x19, 0x8b, 0x05, 0xfe, 0xf5, 0xb7, 0x8c, 0xd1, 0x44, + 0x0b, 0xfe, 0x95, 0x35, 0x48, 0x54, 0xfc, 0xd7, 0x6c, 0x02, 0xb0, 0x8e, 0x5c, 0x7d, 0xf8, 0x94, 0x18, 0x6f, 0xcd, + 0x86, 0x47, 0xbe, 0x59, 0x81, 0x4e, 0x7d, 0xee, 0xae, 0x6c, 0x4f, 0x55, 0xe3, 0x6f, 0xa9, 0xae, 0x46, 0xaa, 0xaa, + 0xf1, 0xb7, 0x94, 0xaa, 0xf1, 0x5b, 0x46, 0xf1, 0x3b, 0x95, 0xcf, 0x90, 0x39, 0xd9, 0xc4, 0x24, 0x9d, 0xbe, 0x37, + 0x9c, 0xd8, 0x65, 0xbf, 0x79, 0x9b, 0xc8, 0x4c, 0xa4, 0x90, 0x7b, 0x03, 0xd0, 0xf6, 0xbb, 0xdc, 0x70, 0x4a, 0x9c, + 0x9f, 0x7b, 0xb8, 0x62, 0xd3, 0xea, 0x15, 0x2d, 0x58, 0x60, 0xf3, 0x32, 0xcb, 0x53, 0x24, 0xb0, 0x6d, 0xca, 0xac, + 0x3f, 0xe7, 0x1e, 0x40, 0x30, 0x93, 0x9a, 0x00, 0x90, 0x16, 0xa2, 0x52, 0x88, 0xfc, 0x15, 0xce, 0xea, 0x73, 0xde, + 0xdb, 0xe4, 0x31, 0x91, 0x56, 0xf7, 0xfa, 0xfd, 0xf4, 0x2c, 0xcd, 0x29, 0xa8, 0xe1, 0x38, 0xeb, 0xf4, 0x65, 0x16, + 0xd4, 0x89, 0x5c, 0xa5, 0x7f, 0x77, 0x83, 0xbc, 0x8c, 0xef, 0xeb, 0xb6, 0xe7, 0x4f, 0xd4, 0xdf, 0x3b, 0xeb, 0x6f, + 0x0b, 0x04, 0x77, 0x72, 0xec, 0x27, 0xab, 0x52, 0x9e, 0x18, 0x97, 0xf6, 0x9e, 0xdf, 0xd4, 0x45, 0x91, 0xd5, 0xe9, + 0xfa, 0xa3, 0xd4, 0xd3, 0xe8, 0xbe, 0xd8, 0x83, 0x31, 0x78, 0x07, 0x80, 0x67, 0x3a, 0x34, 0x40, 0xfa, 0x9e, 0x91, + 0x87, 0xfb, 0xdc, 0x92, 0x9f, 0x54, 0xd6, 0x26, 0x09, 0x2b, 0x8a, 0xcd, 0x30, 0x46, 0x28, 0x19, 0xa7, 0xb1, 0xf5, + 0xfb, 0x7d, 0xf5, 0xf7, 0x0e, 0xa3, 0xa8, 0xa8, 0xb8, 0x63, 0x34, 0x2a, 0xab, 0x7a, 0xb4, 0x1d, 0x1c, 0x0e, 0xe7, + 0xb9, 0x8d, 0xa3, 0xad, 0x57, 0xc0, 0xde, 0x0a, 0x95, 0xb2, 0x57, 0x22, 0x2c, 0x3f, 0x5c, 0xf9, 0xfd, 0x3e, 0xfc, + 0x2b, 0x23, 0x2d, 0x3c, 0x7f, 0x8a, 0xbf, 0x16, 0x75, 0x81, 0xe1, 0x19, 0xb4, 0x46, 0x2b, 0x08, 0x26, 0xf8, 0x7b, + 0x07, 0xea, 0xa5, 0x95, 0xf6, 0x09, 0x74, 0x2b, 0xd0, 0x83, 0x7a, 0xe8, 0xd3, 0xa4, 0x7d, 0x21, 0x51, 0xb7, 0xb7, + 0x3a, 0x8d, 0xfe, 0xa8, 0xe0, 0x72, 0x0a, 0x93, 0xc3, 0x0d, 0x7d, 0x5a, 0x85, 0xdb, 0xcf, 0xf0, 0xf4, 0x67, 0xa0, + 0xdc, 0x3a, 0x1c, 0x72, 0x10, 0x5b, 0xc0, 0xcd, 0x63, 0x15, 0x7e, 0x29, 0x4a, 0x19, 0x51, 0x1f, 0x4f, 0x0b, 0xd0, + 0xde, 0x05, 0xe8, 0x80, 0xa5, 0x41, 0xbc, 0x42, 0xf2, 0x9c, 0x8d, 0x00, 0x96, 0x1d, 0x58, 0xce, 0x32, 0x4e, 0x61, + 0x9e, 0xe5, 0xb5, 0x5a, 0x69, 0x67, 0x65, 0xe2, 0xd5, 0x2c, 0x03, 0x67, 0x81, 0x8b, 0xca, 0x67, 0x99, 0x56, 0x3d, + 0x55, 0x09, 0xfa, 0xbc, 0x92, 0x13, 0x5c, 0x09, 0x4e, 0x36, 0x20, 0xbf, 0x00, 0x49, 0x9a, 0x52, 0xd6, 0x94, 0xd7, + 0x97, 0x74, 0x43, 0x46, 0xcf, 0x79, 0xcf, 0x8b, 0x86, 0xa1, 0x7f, 0xe1, 0x95, 0x10, 0xbe, 0x89, 0xdb, 0x36, 0x4a, + 0x61, 0x7f, 0x11, 0x58, 0x7c, 0xc2, 0x7e, 0xf4, 0x96, 0xfe, 0x74, 0x1c, 0x84, 0x43, 0xe4, 0x86, 0x8a, 0x39, 0xb0, + 0xa7, 0x01, 0x8b, 0x4d, 0x7c, 0xb5, 0x99, 0xc4, 0x83, 0x81, 0xaf, 0x33, 0x16, 0xb3, 0x18, 0x68, 0x90, 0xe3, 0xc1, + 0xe5, 0x5c, 0x9f, 0x10, 0xfa, 0x61, 0x44, 0xe5, 0xa8, 0x40, 0xe7, 0x20, 0x1a, 0x2c, 0x01, 0x4f, 0xbd, 0x95, 0x0d, + 0x92, 0x8c, 0x49, 0x26, 0x71, 0xad, 0x49, 0xaa, 0xc3, 0x09, 0xad, 0x03, 0x1d, 0x57, 0x17, 0xd0, 0xf9, 0xb8, 0xee, + 0x7d, 0xbc, 0x1a, 0x2e, 0xa8, 0xf4, 0x0b, 0x31, 0xf0, 0xea, 0xe9, 0x38, 0xb8, 0xa4, 0x5b, 0xe1, 0x62, 0x15, 0x6e, + 0x7f, 0x96, 0x0f, 0x1c, 0x77, 0x54, 0xd2, 0x10, 0x18, 0xbc, 0x3d, 0x74, 0x37, 0x33, 0x34, 0xd4, 0x49, 0xfb, 0x30, + 0x0e, 0xe5, 0x10, 0xab, 0x56, 0x5c, 0x48, 0x6f, 0x04, 0xdf, 0x2e, 0x14, 0x63, 0xd9, 0xd8, 0xa5, 0xa1, 0x28, 0xfc, + 0x15, 0xc0, 0x0e, 0xb5, 0xbf, 0x52, 0xc9, 0xc7, 0xc8, 0xa8, 0xa6, 0x81, 0x8e, 0x01, 0x58, 0xb2, 0x34, 0x91, 0x54, + 0x91, 0x46, 0xe2, 0x8f, 0xcc, 0x58, 0x47, 0x4d, 0xd7, 0x17, 0x4c, 0x55, 0x8b, 0xa4, 0xdb, 0x99, 0xc4, 0x72, 0x22, + 0x49, 0x6d, 0xf7, 0x11, 0x31, 0x18, 0xf8, 0x60, 0x23, 0xa6, 0x99, 0x08, 0x47, 0x3c, 0x2a, 0x91, 0x45, 0x97, 0xdf, + 0x46, 0x99, 0xb4, 0x7d, 0x59, 0x91, 0x2d, 0x08, 0xa6, 0x27, 0xd1, 0x07, 0x49, 0xca, 0xa9, 0x48, 0xa4, 0x19, 0x21, + 0xc0, 0x8f, 0x27, 0xe5, 0x95, 0xfe, 0x1c, 0x34, 0xad, 0x04, 0x2f, 0x19, 0x24, 0x8f, 0xc4, 0xcf, 0xa4, 0x60, 0x16, + 0x63, 0xd5, 0x60, 0x80, 0xe5, 0x54, 0xcf, 0x1c, 0x93, 0xf4, 0x5f, 0x3a, 0x9d, 0xb0, 0x5f, 0x78, 0xb9, 0xad, 0xe5, + 0x4d, 0x73, 0xef, 0x85, 0x57, 0xb1, 0x54, 0xc3, 0x32, 0xe8, 0xbf, 0x26, 0xda, 0x05, 0x5b, 0x5b, 0xc6, 0x84, 0x55, + 0x3f, 0x80, 0xb4, 0x47, 0xba, 0xbc, 0x6a, 0x98, 0x33, 0xc1, 0xa3, 0x0b, 0x6b, 0x1e, 0x44, 0x17, 0xc2, 0x47, 0x2e, + 0xbb, 0x49, 0x72, 0x35, 0x9e, 0xf8, 0xe1, 0x60, 0xa0, 0x00, 0x68, 0x69, 0x9d, 0x14, 0x83, 0xf0, 0x99, 0x90, 0x03, + 0x69, 0x74, 0x54, 0x05, 0x58, 0x2c, 0xb3, 0xab, 0x72, 0x92, 0x0d, 0x06, 0x3e, 0x88, 0x8d, 0x89, 0xdd, 0xd0, 0x6c, + 0xee, 0xb3, 0x13, 0x05, 0x59, 0x6d, 0x0e, 0x5b, 0x33, 0xdd, 0x02, 0x03, 0x80, 0x41, 0x44, 0xb0, 0xdc, 0xe7, 0x46, + 0x3e, 0xa2, 0x4e, 0x4f, 0x61, 0x04, 0x04, 0xbf, 0x9c, 0x08, 0x44, 0x2e, 0x12, 0xa8, 0x07, 0x98, 0x09, 0x30, 0xa3, + 0x8a, 0xe1, 0x25, 0xb0, 0x8b, 0xe7, 0xe6, 0x15, 0x83, 0xfe, 0x45, 0x93, 0x2c, 0xd1, 0x54, 0xe2, 0x68, 0x8c, 0x9c, + 0x4a, 0x63, 0x64, 0x40, 0xec, 0xe2, 0xf8, 0xf7, 0x94, 0x1e, 0x05, 0x29, 0xfb, 0x52, 0x19, 0xe2, 0x70, 0x14, 0x5f, + 0xc1, 0xaa, 0x71, 0x38, 0xd4, 0xe6, 0xf5, 0x74, 0x56, 0xcf, 0x07, 0x22, 0x80, 0xff, 0x86, 0x82, 0xfd, 0xa2, 0xa9, + 0xc8, 0x0d, 0x52, 0xe7, 0xe1, 0x90, 0x82, 0x7c, 0xaa, 0x9b, 0xfc, 0xb2, 0x72, 0xf7, 0xd3, 0xd9, 0xdc, 0x9a, 0xa3, + 0x17, 0x35, 0xae, 0x5b, 0xab, 0x1b, 0x0a, 0x89, 0xd6, 0x34, 0x29, 0xae, 0xaa, 0x49, 0x31, 0xe0, 0xb9, 0x2f, 0x54, + 0x17, 0x5b, 0x23, 0x58, 0xf8, 0x73, 0x0b, 0x84, 0xc9, 0xb8, 0x17, 0x1f, 0x2d, 0xe4, 0x94, 0x76, 0x6d, 0xb5, 0xdb, + 0x56, 0x36, 0xa4, 0x68, 0x3e, 0xbc, 0x84, 0x5d, 0x3a, 0x45, 0xb4, 0xed, 0x92, 0xe0, 0x0b, 0xd0, 0xb2, 0xba, 0x10, + 0x79, 0x4c, 0xbf, 0x42, 0x7e, 0x29, 0x86, 0xff, 0x29, 0xdd, 0x9b, 0x53, 0x1b, 0xe4, 0x00, 0xb6, 0x7b, 0x0f, 0xb7, + 0x63, 0xf4, 0x40, 0x06, 0x6f, 0x84, 0x9c, 0x73, 0x7e, 0x39, 0xb5, 0x66, 0x4c, 0x34, 0x2c, 0x58, 0x39, 0x8c, 0xfc, + 0x00, 0x19, 0x2f, 0xa7, 0xc0, 0xca, 0x7e, 0x54, 0xc4, 0xa5, 0x3f, 0x8c, 0xfc, 0x8b, 0xe7, 0x41, 0xc6, 0xbd, 0x68, + 0xd8, 0xf1, 0x05, 0xd8, 0xab, 0x2f, 0x9e, 0xb3, 0x68, 0xc0, 0xab, 0xab, 0x7a, 0x9a, 0x05, 0xc3, 0x8c, 0x45, 0x57, + 0xc5, 0x10, 0x7c, 0x68, 0xaf, 0xcb, 0x41, 0xe8, 0xfb, 0x66, 0xe7, 0xd0, 0xdd, 0x90, 0xc8, 0x23, 0xec, 0x27, 0x70, + 0xdb, 0xd5, 0x12, 0x33, 0x98, 0x6c, 0xee, 0x22, 0x66, 0xb0, 0xe5, 0x2f, 0x9e, 0x1b, 0x2e, 0xa1, 0xea, 0x5a, 0x6a, + 0x36, 0x0a, 0x34, 0x27, 0x57, 0x68, 0x4e, 0x56, 0x42, 0x2d, 0xf9, 0xa4, 0xc2, 0x09, 0x3b, 0x9f, 0xe4, 0xca, 0x6e, + 0x34, 0xc6, 0xc0, 0x45, 0x7b, 0x6e, 0x0b, 0x23, 0x33, 0x9d, 0xa5, 0x68, 0xc0, 0xc2, 0x33, 0x71, 0x4a, 0x63, 0x40, + 0xfb, 0x72, 0x60, 0x69, 0x43, 0x7e, 0x92, 0x33, 0x03, 0x6d, 0x43, 0x4a, 0xa3, 0x66, 0xe0, 0xcf, 0xd4, 0x84, 0xf9, + 0x15, 0xac, 0x44, 0x10, 0xd5, 0x05, 0x98, 0x24, 0x39, 0x19, 0x8d, 0x94, 0x95, 0x48, 0xce, 0x01, 0xef, 0x13, 0x78, + 0xb2, 0x88, 0x6d, 0xed, 0x4f, 0xe9, 0x7f, 0x75, 0xf8, 0x5c, 0xfa, 0xcf, 0x04, 0xb0, 0x90, 0x4b, 0x83, 0xc8, 0x40, + 0xe1, 0x90, 0x9a, 0x4a, 0xc4, 0x89, 0xe3, 0x19, 0xf8, 0x06, 0x2e, 0xd0, 0x14, 0xd0, 0x1f, 0xd4, 0x8c, 0x22, 0xb2, + 0xf0, 0x57, 0xcf, 0x6e, 0xea, 0x46, 0xcf, 0x33, 0xe7, 0x35, 0x68, 0x66, 0x20, 0xa4, 0xc7, 0xa9, 0x7a, 0x1b, 0x12, + 0x9d, 0x97, 0x97, 0xfa, 0x65, 0x42, 0x24, 0x2b, 0x22, 0x4f, 0xdf, 0xe7, 0x60, 0x1e, 0x51, 0x84, 0x0e, 0xae, 0xcc, + 0xc3, 0xe1, 0x5c, 0x50, 0xf8, 0x8e, 0xf2, 0x7c, 0xc0, 0x69, 0x16, 0x25, 0xa0, 0x0d, 0x64, 0xb9, 0x29, 0x73, 0x9d, + 0xb4, 0x4c, 0xdd, 0x7b, 0xb0, 0x12, 0x54, 0xe8, 0xe6, 0x14, 0x14, 0xca, 0x48, 0x50, 0x4a, 0xab, 0x41, 0x28, 0xd5, + 0x61, 0x11, 0x44, 0x0e, 0x59, 0x08, 0xb8, 0x99, 0x8a, 0x46, 0x4b, 0x1a, 0x1e, 0xe1, 0xdc, 0x40, 0x21, 0x00, 0x89, + 0x3d, 0x55, 0x94, 0x71, 0x39, 0x04, 0x7c, 0x94, 0x70, 0x88, 0xb3, 0x26, 0x6d, 0x79, 0x0e, 0xe2, 0x58, 0x2e, 0xf9, + 0xba, 0x42, 0x30, 0x88, 0xd0, 0x67, 0xc8, 0x9f, 0x2c, 0xe7, 0xdf, 0xad, 0xc3, 0xb4, 0x23, 0x7c, 0xd8, 0xd5, 0x16, + 0x5c, 0xcc, 0x6e, 0xe7, 0x13, 0x88, 0x6f, 0xb9, 0x9d, 0x1f, 0x63, 0x88, 0x2c, 0xfc, 0xc1, 0xdd, 0x50, 0x72, 0x45, + 0xa1, 0xcb, 0x7a, 0x44, 0x8a, 0xec, 0xe9, 0x9a, 0x23, 0x08, 0x0e, 0xb4, 0x6a, 0x90, 0xa1, 0x91, 0xf8, 0xe2, 0x39, + 0x64, 0x0d, 0xd6, 0xfc, 0x4b, 0x45, 0xce, 0xea, 0xfe, 0x64, 0x03, 0xd5, 0x24, 0x93, 0xb5, 0xa2, 0x72, 0xfe, 0x76, + 0x55, 0x96, 0x27, 0xab, 0x32, 0x5c, 0x0d, 0xba, 0xaa, 0xb2, 0xe4, 0x48, 0x6d, 0x80, 0xd6, 0x74, 0x85, 0x18, 0x0a, + 0x59, 0x83, 0xa5, 0x55, 0x95, 0x35, 0xf5, 0x09, 0x04, 0xfa, 0x00, 0xcb, 0xa8, 0xd9, 0x4f, 0x87, 0xff, 0x0c, 0xfe, + 0xa9, 0x42, 0x96, 0xea, 0xb4, 0xce, 0xc4, 0xaf, 0xc1, 0x92, 0xe1, 0x1f, 0xbf, 0x05, 0x6b, 0xc0, 0x12, 0x20, 0xcb, + 0xdd, 0xc6, 0x46, 0xeb, 0x95, 0x57, 0x88, 0xaf, 0xb5, 0xbe, 0xe8, 0xb7, 0x6e, 0x13, 0xb5, 0x02, 0x8c, 0x50, 0x68, + 0x11, 0x60, 0xab, 0x07, 0xee, 0x29, 0xf8, 0x81, 0x18, 0xce, 0x35, 0x69, 0x4d, 0x9d, 0xf0, 0x3a, 0x1b, 0x47, 0x22, + 0xaa, 0xb7, 0x70, 0x71, 0xaf, 0xb7, 0x16, 0x7f, 0xa3, 0x02, 0x01, 0x90, 0xc5, 0x14, 0x6b, 0xe7, 0x0d, 0xe9, 0x95, + 0x61, 0x27, 0xa1, 0xf7, 0x86, 0x9d, 0x40, 0x5e, 0x1c, 0x76, 0x0a, 0x5d, 0xa2, 0xed, 0x14, 0xa9, 0x89, 0xb6, 0x93, + 0x16, 0xab, 0xb0, 0x84, 0xe0, 0x57, 0xed, 0xad, 0xa3, 0x6c, 0x5f, 0x64, 0x09, 0xd3, 0x16, 0x30, 0xca, 0xad, 0xfa, + 0xcc, 0x29, 0x62, 0xa5, 0xec, 0x9d, 0x4e, 0xaa, 0xdc, 0x45, 0x3e, 0xb7, 0x9a, 0x22, 0x93, 0x5f, 0x1e, 0xb7, 0x48, + 0x3e, 0xf9, 0xb9, 0xdd, 0x30, 0x99, 0xfe, 0xe9, 0xe8, 0x0b, 0xe8, 0x8a, 0xec, 0xf4, 0x09, 0x04, 0x64, 0x2a, 0xa8, + 0x56, 0xb7, 0x8a, 0x69, 0xde, 0xae, 0xb2, 0xdb, 0x0b, 0x25, 0x86, 0xd3, 0xd9, 0x49, 0x78, 0xb4, 0x19, 0x32, 0x70, + 0x08, 0x02, 0x85, 0x50, 0x51, 0x0c, 0x8f, 0x40, 0xad, 0x91, 0x7c, 0x80, 0x1f, 0xed, 0x4e, 0x05, 0x91, 0xda, 0x4d, + 0xc5, 0x8d, 0x93, 0x9b, 0xae, 0x97, 0x02, 0xb5, 0x4e, 0xc9, 0x0a, 0xa0, 0x84, 0xa8, 0x3f, 0x8b, 0x6d, 0xfd, 0x0a, + 0xae, 0xd8, 0x7c, 0xdf, 0x28, 0x7a, 0x72, 0x7d, 0x8a, 0xba, 0x15, 0x57, 0xa7, 0x69, 0xab, 0x39, 0x76, 0x9c, 0x21, + 0x07, 0xcf, 0x0a, 0x82, 0xed, 0xa8, 0x44, 0xf9, 0xae, 0xdd, 0x74, 0x4c, 0x6c, 0xf5, 0xcf, 0xa2, 0xda, 0xdc, 0x41, + 0x45, 0x44, 0x7c, 0x94, 0xdd, 0x3c, 0x69, 0xbf, 0x83, 0x3d, 0xd6, 0x6a, 0x10, 0xd9, 0x67, 0x70, 0x95, 0xeb, 0xb4, + 0xc8, 0x6d, 0x19, 0x9c, 0x7f, 0x78, 0xb5, 0xab, 0xb0, 0xc9, 0xb1, 0xae, 0xae, 0x66, 0xaa, 0x93, 0x8a, 0x0d, 0x8c, + 0x35, 0xad, 0xa5, 0x9a, 0xc7, 0x90, 0x74, 0x57, 0x16, 0x67, 0x55, 0xd2, 0x4d, 0xcf, 0x8d, 0x33, 0x85, 0x18, 0x38, + 0x5b, 0x8d, 0x96, 0x33, 0x0c, 0xd1, 0xf5, 0x61, 0x96, 0xf8, 0xad, 0x9e, 0x72, 0x9f, 0x87, 0x5b, 0xbf, 0xab, 0x17, + 0x9c, 0x4c, 0xf6, 0x93, 0xe3, 0xdc, 0xed, 0x22, 0xed, 0x27, 0xbe, 0x0d, 0xf3, 0xaf, 0x6f, 0x10, 0x77, 0xa2, 0xfe, + 0x47, 0x05, 0x40, 0x83, 0x9b, 0x3c, 0x96, 0x28, 0xf5, 0x7b, 0x55, 0xfd, 0xa0, 0x66, 0xaa, 0xa6, 0x81, 0x60, 0x4e, + 0xa5, 0x80, 0x3f, 0xdc, 0x2e, 0x5c, 0xf1, 0x88, 0x1b, 0x16, 0xc6, 0x3f, 0xbd, 0x9a, 0x9d, 0x0a, 0x2a, 0x03, 0x37, + 0xe3, 0x3f, 0x3d, 0xc1, 0x4e, 0x61, 0xad, 0x80, 0xac, 0xf0, 0xa7, 0x97, 0x3f, 0xf2, 0x7e, 0xc5, 0xff, 0xf4, 0xaa, + 0x47, 0xde, 0x47, 0x9c, 0x97, 0x3f, 0x91, 0xd4, 0x09, 0x51, 0x5d, 0xfe, 0x24, 0x4c, 0xb1, 0x55, 0x9a, 0xbf, 0x26, + 0x85, 0x4f, 0xf0, 0x05, 0xf8, 0x0e, 0x57, 0xe1, 0xd6, 0xfc, 0x06, 0x8f, 0x1d, 0x8b, 0x6d, 0x97, 0xfa, 0x02, 0xca, + 0x11, 0x58, 0x44, 0x6e, 0xbf, 0x5d, 0xd9, 0xaf, 0x16, 0x46, 0x19, 0x63, 0xf7, 0x25, 0x2b, 0x51, 0x3a, 0xeb, 0xf7, + 0x0b, 0x29, 0x18, 0xd9, 0x85, 0x35, 0xda, 0xa3, 0x54, 0xbd, 0xfa, 0x2e, 0xac, 0xa3, 0x24, 0xcd, 0xef, 0x64, 0xf4, + 0x91, 0x0c, 0x3b, 0xd2, 0x57, 0x52, 0xa2, 0xbd, 0x56, 0x61, 0x39, 0x9a, 0xfd, 0xba, 0xe4, 0x40, 0x79, 0xdd, 0x0a, + 0xca, 0x57, 0x4d, 0x00, 0xbd, 0x52, 0xed, 0x33, 0xd0, 0x0a, 0x0a, 0x4b, 0xe5, 0xc1, 0x4a, 0x9c, 0x8b, 0x3e, 0x2b, + 0x0e, 0x07, 0x75, 0x31, 0x24, 0x14, 0xa8, 0x12, 0x27, 0xa1, 0x11, 0xcf, 0xe1, 0x42, 0x28, 0xae, 0x73, 0x8c, 0xad, + 0xc8, 0x81, 0x03, 0x19, 0x7e, 0x40, 0xe0, 0xbd, 0xec, 0x5f, 0xc1, 0x60, 0x98, 0xe0, 0x46, 0x46, 0x9d, 0x9c, 0xb3, + 0x3f, 0x31, 0x30, 0x83, 0x7a, 0x52, 0xbb, 0xcf, 0xee, 0x55, 0x60, 0x2f, 0x9c, 0x01, 0xed, 0xdd, 0x18, 0xfd, 0xac, + 0x8a, 0xb5, 0x93, 0xfe, 0xb9, 0x58, 0x43, 0x32, 0x1d, 0x16, 0x47, 0xdb, 0x34, 0x3c, 0x92, 0x27, 0xc7, 0xf1, 0xa6, + 0x7f, 0x38, 0x8c, 0xf1, 0xe3, 0x28, 0xbf, 0xb6, 0x80, 0x57, 0x71, 0x0b, 0x69, 0x2c, 0x52, 0xf4, 0x0e, 0xc4, 0x1c, + 0x8a, 0x5e, 0xb2, 0xdf, 0x32, 0x5e, 0x4e, 0x04, 0xa5, 0x24, 0xb1, 0xe1, 0x1d, 0xe9, 0x69, 0x5a, 0x8f, 0xb6, 0x32, + 0x60, 0xbf, 0x1e, 0xed, 0xe8, 0x2f, 0x50, 0x3c, 0x5a, 0xf8, 0x4b, 0xfa, 0xbb, 0xb8, 0x9b, 0x7b, 0xce, 0x37, 0x8d, + 0xef, 0x88, 0x0b, 0x14, 0x6b, 0x76, 0x7f, 0x4d, 0x4b, 0x67, 0x1d, 0x08, 0x0e, 0x78, 0x8b, 0x5d, 0xb4, 0xef, 0x37, + 0xae, 0xd3, 0xd3, 0xfe, 0x7b, 0xb7, 0x46, 0xf9, 0xde, 0x3f, 0x24, 0xca, 0xc1, 0xfe, 0xb5, 0x8b, 0xe6, 0x6f, 0x3f, + 0x65, 0x48, 0x2a, 0x34, 0x37, 0xd8, 0x4e, 0xb6, 0x08, 0x6b, 0x63, 0x1c, 0x54, 0xec, 0xae, 0x0c, 0x23, 0x60, 0x50, + 0xc7, 0xfe, 0x47, 0x9f, 0x4d, 0x1b, 0xb2, 0x0f, 0x00, 0x95, 0xab, 0x10, 0xb0, 0x07, 0xe0, 0x44, 0x23, 0xdc, 0x00, + 0xb7, 0x1a, 0x2d, 0xe9, 0xa0, 0x6e, 0x0b, 0x06, 0xa2, 0x25, 0x6c, 0xe4, 0x6d, 0x57, 0xa7, 0x6f, 0x08, 0x1f, 0x6a, + 0x27, 0xa5, 0x43, 0xf9, 0x9b, 0xe7, 0xec, 0xbf, 0x77, 0x58, 0x53, 0x53, 0x6e, 0x00, 0x33, 0x67, 0x25, 0xf2, 0x0a, + 0xa1, 0x53, 0xe4, 0xf7, 0xaa, 0xae, 0xc4, 0x70, 0x59, 0x8b, 0xb2, 0x33, 0xbb, 0x75, 0xa2, 0x77, 0x4e, 0x41, 0x2d, + 0x95, 0x0d, 0x72, 0x92, 0x6a, 0xf3, 0x91, 0xb5, 0x82, 0x12, 0x75, 0x8d, 0x02, 0xc7, 0xa7, 0x5c, 0xbb, 0xff, 0x77, + 0xce, 0x04, 0x35, 0xdb, 0xa8, 0xee, 0xaf, 0xf5, 0x53, 0x55, 0x93, 0x58, 0x80, 0xcb, 0x49, 0x9a, 0x77, 0x3c, 0xc2, + 0xea, 0x1f, 0x27, 0x4b, 0x11, 0xe8, 0x75, 0x44, 0xbb, 0x12, 0x90, 0xa0, 0x9d, 0x9c, 0x85, 0x8a, 0x40, 0x81, 0xbe, + 0xfe, 0x72, 0x93, 0x66, 0xb1, 0x5c, 0xcd, 0xf6, 0x30, 0x51, 0x16, 0xeb, 0x21, 0x82, 0x9c, 0x99, 0x3a, 0xd8, 0xef, + 0x69, 0x46, 0xb3, 0xf0, 0xca, 0x94, 0xe0, 0x52, 0x5c, 0x45, 0x45, 0x0e, 0x3e, 0x87, 0xf8, 0xc2, 0xe7, 0x42, 0x6e, + 0x10, 0xd1, 0xf4, 0xa5, 0x44, 0xb5, 0x23, 0x05, 0x72, 0x28, 0xf9, 0x09, 0xf1, 0x97, 0xac, 0x8d, 0x71, 0xbf, 0x74, + 0xaa, 0xfd, 0x4a, 0x21, 0xb8, 0xff, 0x6c, 0x8b, 0x8d, 0x2a, 0x4f, 0xf4, 0xe8, 0x53, 0xac, 0xff, 0xc9, 0x02, 0x4a, + 0x75, 0xdf, 0x06, 0xa7, 0xe2, 0x51, 0xb8, 0xa9, 0x8b, 0x1b, 0x84, 0x16, 0x28, 0x47, 0x55, 0xb1, 0x29, 0x23, 0xe2, + 0x84, 0xdd, 0xd4, 0x45, 0x4f, 0x73, 0xa0, 0x53, 0x87, 0xa5, 0x89, 0x3c, 0x11, 0xda, 0x2d, 0xe8, 0x9e, 0xe6, 0x58, + 0x89, 0x17, 0xb2, 0x74, 0x90, 0x75, 0x22, 0x4d, 0xa8, 0xdc, 0xd5, 0x55, 0x47, 0xa5, 0x52, 0x37, 0xbc, 0x49, 0x35, + 0xe3, 0xef, 0xd2, 0xfc, 0x89, 0x65, 0xbf, 0x69, 0xfd, 0x56, 0xab, 0xbd, 0xb1, 0x7a, 0x54, 0xb2, 0xe6, 0x38, 0x9b, + 0x90, 0x94, 0x3e, 0x61, 0xbb, 0x99, 0x74, 0xad, 0x03, 0x4f, 0x82, 0xcb, 0xa1, 0x27, 0xa0, 0x62, 0xd0, 0xc4, 0xdb, + 0x5d, 0xa0, 0x1e, 0x81, 0x67, 0xa0, 0x7c, 0xa2, 0xd6, 0x01, 0x3f, 0xaf, 0xb5, 0x3c, 0x65, 0x84, 0x61, 0xb5, 0xb3, + 0x68, 0x39, 0x38, 0xef, 0x14, 0x81, 0x6b, 0x57, 0x02, 0xcf, 0x87, 0xea, 0xbd, 0x10, 0x30, 0xdc, 0x3f, 0x17, 0x2a, + 0x9b, 0xdd, 0x0c, 0xe7, 0x51, 0xe3, 0xf4, 0x40, 0x7b, 0xdb, 0xb5, 0x1e, 0xea, 0x5d, 0xb7, 0x73, 0x5b, 0xe9, 0xde, + 0xaf, 0x9d, 0x4c, 0xba, 0x80, 0xd6, 0xe6, 0xb3, 0xef, 0xec, 0x4a, 0xeb, 0xa6, 0xe7, 0xec, 0xc1, 0xd6, 0x2d, 0xd1, + 0xb9, 0x20, 0x9a, 0xfc, 0x7e, 0xe0, 0x59, 0xdb, 0x8e, 0x7e, 0x9b, 0x76, 0x6c, 0x73, 0x0f, 0x75, 0xaf, 0xa0, 0xd6, + 0x1b, 0x9a, 0xf7, 0xcf, 0x5c, 0xdb, 0x8e, 0xaf, 0x7e, 0x5d, 0x77, 0xb8, 0xce, 0x9b, 0xe0, 0xb8, 0xe9, 0xda, 0x56, + 0x3b, 0xfb, 0xb9, 0xbb, 0xb7, 0x16, 0x51, 0x98, 0x65, 0x3f, 0x15, 0xc5, 0x1f, 0x95, 0xbe, 0x23, 0xd0, 0xd1, 0x9d, + 0x17, 0x75, 0xba, 0xdc, 0x7d, 0x24, 0x8c, 0x27, 0xaf, 0x3e, 0x22, 0xba, 0xf5, 0x7d, 0xe6, 0x7e, 0x05, 0xb8, 0x11, + 0xdc, 0x41, 0xb4, 0x77, 0x4b, 0x7d, 0x52, 0xab, 0xaf, 0xf5, 0xda, 0x79, 0x7a, 0x7e, 0xd3, 0xb9, 0xfd, 0xee, 0x9b, + 0xa3, 0xad, 0xf7, 0xb8, 0xb0, 0x56, 0x96, 0x9e, 0xaa, 0x82, 0xbd, 0x59, 0x9e, 0xaa, 0x82, 0xc9, 0x03, 0xaf, 0xd9, + 0x2f, 0x68, 0x70, 0xa5, 0xa3, 0x8d, 0xf7, 0x44, 0x0d, 0xdc, 0xa2, 0xb0, 0x74, 0xf8, 0x25, 0x37, 0x93, 0x57, 0xb8, + 0xbf, 0x54, 0xe4, 0x62, 0xdf, 0x39, 0xa3, 0x3b, 0x33, 0xeb, 0x5e, 0x55, 0xb8, 0x5a, 0x90, 0xab, 0x03, 0x5b, 0xcb, + 0x2e, 0x0e, 0x37, 0x2c, 0xa2, 0x00, 0x81, 0x98, 0x5e, 0xa9, 0xb5, 0x3f, 0xa2, 0x41, 0xc8, 0x07, 0x03, 0xbf, 0xc0, + 0x60, 0x55, 0xa0, 0xf0, 0x81, 0x22, 0xf9, 0x6b, 0x4f, 0xc0, 0x2e, 0x9e, 0x01, 0xba, 0x15, 0x9b, 0x15, 0x23, 0x44, + 0xc8, 0x64, 0x39, 0xab, 0xe9, 0x0c, 0xf2, 0xa9, 0x2f, 0xbe, 0xb3, 0x55, 0xa7, 0xf3, 0xb6, 0xa6, 0xca, 0xa9, 0x43, + 0xa1, 0xbb, 0x9b, 0xba, 0x73, 0xeb, 0x22, 0x4f, 0x1d, 0x42, 0xae, 0x54, 0xac, 0xc4, 0x34, 0xd4, 0x3c, 0x49, 0x33, + 0xea, 0x2f, 0xf6, 0x7e, 0xaf, 0x51, 0x38, 0xe5, 0x4f, 0xc7, 0xa0, 0x0a, 0x57, 0x35, 0xc4, 0xb1, 0x54, 0xc5, 0x23, + 0x1b, 0x04, 0x9a, 0x57, 0xb7, 0x2a, 0x69, 0x42, 0x26, 0x37, 0xc2, 0xa7, 0x26, 0xa5, 0x3c, 0x4d, 0x9b, 0xb4, 0x52, + 0xa4, 0x0e, 0x3e, 0xa8, 0x53, 0x8d, 0xe7, 0x66, 0x75, 0x0d, 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, 0x7a, 0x97, 0x40, 0x8a, 0xf4, + 0x2f, 0x5d, 0xf2, 0xd4, 0x61, 0x6c, 0x20, 0xc6, 0xac, 0x98, 0x19, 0xfd, 0x0f, 0xee, 0x92, 0xfe, 0x24, 0x04, 0xc0, + 0x4d, 0x34, 0x85, 0x4e, 0x9d, 0x27, 0x17, 0x79, 0xb0, 0xbc, 0xf0, 0xd0, 0x8a, 0x11, 0x0f, 0xfe, 0xf3, 0x3a, 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, 0x53, 0xe5, 0x41, 0xff, 0xd7, 0x99, 0xb0, 0xd4, 0x7e, 0x7a, 0x3a, + 0x80, 0x0a, 0xde, 0x57, 0xbc, 0x8d, 0x88, 0xef, 0x13, 0x3f, 0x8b, 0x07, 0x9b, 0x67, 0x1b, 0xb0, 0xd6, 0x3d, 0xc9, + 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, 0xa8, 0x20, 0xd4, 0xb9, 0x88, 0xbe, 0x31, + 0xe5, 0x5b, 0x42, 0xcd, 0xbe, 0xb1, 0xa4, 0x94, 0xee, 0x35, 0xf4, 0x26, 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, 0x27, 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, 0x32, 0x2d, 0xab, 0x3a, 0xc8, 0x58, 0x16, 0x56, 0x80, 0xab, 0x43, 0xeb, + 0x07, 0xe1, 0xb2, 0x70, 0xfe, 0x40, 0x08, 0x62, 0xf7, 0x6a, 0x5b, 0xf2, 0x5c, 0xcd, 0xe1, 0x67, 0xcf, 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, 0xb3, 0x58, 0x5e, 0x87, 0xee, 0x9e, 0x6d, + 0x94, 0xcf, 0xa0, 0x6e, 0xb5, 0x31, 0x26, 0xb6, 0x9b, 0x2f, 0xe7, 0xfa, 0xed, 0x60, 0xe9, 0xdb, 0x41, 0x73, 0x4e, + 0xd9, 0x77, 0xba, 0xec, 0x95, 0x5d, 0x36, 0xf5, 0xdc, 0x51, 0xd1, 0x6a, 0x0c, 0xe8, 0x0d, 0x2c, 0x58, 0x9f, 0x8b, + 0x34, 0x5b, 0x95, 0xaa, 0x04, 0xbc, 0x30, 0x56, 0xec, 0xce, 0x6f, 0x64, 0x86, 0x24, 0xcc, 0xe3, 0x4c, 0xbc, 0xa3, + 0x7b, 0x2d, 0x4c, 0x8e, 0x63, 0x91, 0x4c, 0x09, 0x9d, 0xd2, 0x9d, 0x6d, 0xe8, 0x5c, 0x85, 0x51, 0x44, 0x6b, 0x25, + 0x95, 0x46, 0x02, 0x53, 0x33, 0x40, 0xc9, 0x5c, 0x81, 0x53, 0xba, 0xdc, 0xff, 0x8e, 0xc4, 0x38, 0xf3, 0x45, 0xc9, + 0x0c, 0xe8, 0x96, 0x5f, 0x17, 0xeb, 0x56, 0x8a, 0x8c, 0x30, 0x6f, 0x8e, 0xdb, 0xeb, 0xfa, 0x10, 0xc8, 0xd5, 0xb2, + 0x47, 0xd1, 0x38, 0x28, 0x74, 0xb8, 0x54, 0x09, 0xb0, 0x2f, 0x12, 0x3f, 0x23, 0x6c, 0x69, 0x0f, 0xe4, 0xf6, 0xe8, + 0x4c, 0x98, 0x73, 0x4e, 0xca, 0xb2, 0x73, 0x69, 0x06, 0x97, 0x13, 0x57, 0x82, 0x8b, 0xf4, 0xb6, 0x3d, 0x4d, 0x5a, + 0xda, 0x3e, 0x36, 0x9c, 0xa3, 0xa1, 0x6d, 0xd0, 0x1d, 0xfb, 0x43, 0x73, 0xb1, 0x88, 0xad, 0x8b, 0xc5, 0xb0, 0x33, + 0xfb, 0xd1, 0x62, 0x01, 0x72, 0x00, 0x38, 0xea, 0x36, 0x7c, 0xcc, 0x96, 0xc0, 0x69, 0x35, 0xcd, 0xa6, 0xde, 0x86, + 0x57, 0xcf, 0x54, 0x4f, 0x2f, 0x79, 0xfe, 0x4c, 0x98, 0xb1, 0xd8, 0xf0, 0xfc, 0x99, 0x75, 0xe4, 0x54, 0xcf, 0x84, + 0x12, 0xad, 0x0b, 0x68, 0x06, 0x5e, 0x53, 0xc0, 0x88, 0x25, 0x93, 0x29, 0x55, 0xe4, 0x71, 0x6f, 0xba, 0x51, 0x83, + 0x17, 0x14, 0x0e, 0x81, 0x94, 0x4e, 0xbf, 0x78, 0xce, 0xf4, 0x7b, 0x17, 0xcf, 0x3b, 0x64, 0x6d, 0xc3, 0x74, 0xb9, + 0x19, 0x26, 0x83, 0xd2, 0x7f, 0x66, 0x26, 0xc6, 0x85, 0x35, 0x49, 0x00, 0xf1, 0x6f, 0xec, 0x77, 0x48, 0xe1, 0xe6, + 0xfd, 0xe5, 0x30, 0x7e, 0xe4, 0xfd, 0x18, 0xd9, 0x93, 0x34, 0x43, 0xac, 0x99, 0x54, 0xc8, 0xdd, 0x57, 0xeb, 0x1f, + 0x13, 0xbb, 0xc9, 0x1e, 0x58, 0x00, 0x62, 0x6b, 0xda, 0xea, 0x96, 0xf7, 0xfb, 0x9e, 0x29, 0x02, 0xfc, 0xa0, 0xfc, + 0xa3, 0x3b, 0x43, 0x32, 0x28, 0xbb, 0x6e, 0x08, 0xf1, 0xa0, 0x6c, 0x9a, 0xf6, 0x7a, 0xdb, 0x3b, 0xf3, 0x58, 0x5d, + 0xa7, 0x9d, 0xc5, 0xd5, 0x22, 0x83, 0xb4, 0xfa, 0x90, 0x1d, 0x67, 0xf6, 0xd9, 0xd1, 0x52, 0xe9, 0x7e, 0x1f, 0x22, + 0xe2, 0x8e, 0xb2, 0xb6, 0xdf, 0x6e, 0xc1, 0x35, 0x1c, 0x0d, 0x42, 0x57, 0xf6, 0x76, 0x19, 0x6d, 0x5c, 0x88, 0xe3, + 0x9e, 0xe9, 0x7c, 0xc1, 0x97, 0x47, 0x69, 0xe7, 0xc1, 0xa9, 0x9e, 0xe8, 0x73, 0xd3, 0x5d, 0x65, 0x72, 0xad, 0xc3, + 0x6a, 0x0c, 0x6a, 0xb3, 0xb0, 0x85, 0xbb, 0xb0, 0x8d, 0x0e, 0x5a, 0xfb, 0xb2, 0xe0, 0x9f, 0x32, 0x00, 0x5f, 0x7a, + 0xb6, 0x6c, 0x7b, 0x4d, 0x5a, 0xbd, 0x91, 0x51, 0x88, 0x2d, 0x6d, 0xaf, 0x3e, 0x1d, 0xe5, 0xe3, 0xe6, 0x84, 0xe2, + 0x42, 0x8e, 0xf2, 0xa3, 0xd7, 0x10, 0x75, 0xad, 0xeb, 0xb8, 0x58, 0x74, 0xb8, 0x71, 0xd5, 0x6d, 0x37, 0xae, 0x1f, + 0x11, 0x6f, 0x8d, 0x36, 0x29, 0xd4, 0xca, 0xd8, 0x11, 0xbc, 0x2c, 0x1f, 0x0e, 0x99, 0x18, 0x0e, 0x25, 0x64, 0xea, + 0x63, 0xf7, 0x86, 0xa6, 0x7d, 0x7e, 0xda, 0xfa, 0x11, 0x4b, 0x8d, 0xa3, 0xd8, 0xf0, 0x4e, 0xdf, 0x79, 0x6c, 0x8d, + 0x2b, 0xf9, 0x32, 0x98, 0xed, 0x0a, 0xaa, 0xad, 0xf1, 0x86, 0xbd, 0x9c, 0xbf, 0xac, 0xa4, 0x92, 0xbf, 0xfd, 0x19, + 0xae, 0xe1, 0xad, 0x2d, 0x1d, 0x34, 0xd5, 0x2c, 0x67, 0xb9, 0xbe, 0x17, 0x1c, 0x7f, 0xdc, 0xbd, 0x22, 0x18, 0xfc, + 0x9e, 0x8e, 0x82, 0x5c, 0x2c, 0xd5, 0x1a, 0x50, 0x90, 0x8e, 0xec, 0x98, 0xca, 0x02, 0xc3, 0x00, 0xde, 0x90, 0x01, + 0xf2, 0x98, 0xc2, 0xdd, 0x50, 0xe1, 0x85, 0xbf, 0x54, 0x64, 0x97, 0xc0, 0xb6, 0x66, 0x7c, 0xcc, 0x70, 0x07, 0x21, + 0xff, 0x08, 0x76, 0xc7, 0x56, 0xec, 0x96, 0x2d, 0x18, 0x92, 0x8d, 0xe3, 0x30, 0xc6, 0x7c, 0x3c, 0x89, 0xaf, 0xc4, + 0x24, 0x1e, 0xf0, 0x08, 0x1d, 0x23, 0xd6, 0xbc, 0x9e, 0xc5, 0x72, 0x00, 0xd9, 0x1d, 0x57, 0x3a, 0x20, 0x84, 0xc6, + 0x86, 0x96, 0xbc, 0x29, 0x0c, 0x2e, 0x76, 0xec, 0x33, 0x12, 0xc9, 0x38, 0x04, 0x8b, 0x56, 0x35, 0xb0, 0x30, 0xb1, + 0x5b, 0x5e, 0xcc, 0x56, 0x73, 0xfc, 0xe7, 0x70, 0x40, 0x00, 0xec, 0x60, 0xdf, 0xb0, 0xbb, 0x08, 0x91, 0xde, 0x16, + 0xfc, 0xce, 0xf2, 0x74, 0x61, 0xf7, 0xfc, 0x1d, 0x1f, 0xb3, 0xf3, 0x1f, 0x3d, 0x88, 0x9c, 0x3d, 0xff, 0x04, 0x68, + 0x88, 0xf7, 0xfc, 0x36, 0xf5, 0x2a, 0x76, 0x4b, 0x14, 0x84, 0xb7, 0xe0, 0x0c, 0x74, 0x0f, 0x11, 0xb0, 0xef, 0xf8, + 0x02, 0x63, 0xc5, 0xce, 0xd2, 0xa5, 0x87, 0x19, 0xa1, 0xf6, 0x74, 0xbe, 0xac, 0xd5, 0x24, 0xdc, 0x5c, 0x2d, 0x27, + 0x83, 0xc1, 0xc6, 0xdf, 0xf1, 0x35, 0xf0, 0xc1, 0x9c, 0xff, 0xe8, 0xed, 0xa8, 0x5c, 0xf8, 0xcf, 0xeb, 0x2c, 0x79, + 0xe7, 0xb3, 0x77, 0x03, 0xbe, 0x00, 0xbc, 0x25, 0x74, 0xe0, 0xba, 0xf7, 0x99, 0xc4, 0x6b, 0x7b, 0xa7, 0xaf, 0x11, + 0x48, 0xe4, 0x0b, 0xc0, 0x88, 0x89, 0xf9, 0xfd, 0x0e, 0x22, 0x30, 0x12, 0xf0, 0x6d, 0xd5, 0x1e, 0xf1, 0x5b, 0x6e, + 0x00, 0xbf, 0x32, 0x9f, 0x3d, 0xf0, 0x50, 0xff, 0x4c, 0x7c, 0x76, 0xc3, 0x3f, 0xf0, 0x6b, 0x4f, 0x4a, 0xd2, 0xe5, + 0xec, 0xc3, 0x1c, 0xae, 0x87, 0x52, 0x9e, 0x0e, 0xe9, 0x67, 0x63, 0x30, 0x80, 0x50, 0xc8, 0xbc, 0xf1, 0x80, 0x35, + 0x29, 0xc4, 0xbf, 0x80, 0x6f, 0x47, 0x09, 0x9b, 0x37, 0xde, 0xd6, 0xd7, 0xf2, 0xe6, 0x8d, 0xf7, 0xe0, 0x53, 0x14, + 0x60, 0x15, 0x94, 0xb2, 0xc0, 0x2a, 0x08, 0x1b, 0x6d, 0x84, 0x31, 0x70, 0xf5, 0xae, 0x31, 0xd4, 0xf5, 0x1c, 0xb1, + 0x6d, 0xa5, 0xef, 0xc3, 0xf7, 0x90, 0x01, 0x1f, 0xbc, 0x29, 0x4a, 0xa2, 0xcf, 0xa9, 0x29, 0x92, 0xd6, 0x3d, 0xf7, + 0x5b, 0xeb, 0x8e, 0xd6, 0x94, 0xfa, 0xc8, 0xd5, 0xf8, 0x70, 0xa8, 0xaf, 0x85, 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, 0xd9, 0x72, 0x9c, + 0xaa, 0xda, 0xbf, 0x24, 0x49, 0xb5, 0xab, 0xb4, 0x9c, 0xde, 0xdb, 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, 0xeb, 0xf1, 0xb4, 0x26, 0x96, 0xd4, 0xae, 0xc0, 0x18, 0x3d, + 0xae, 0x8a, 0xda, 0xa7, 0xbe, 0x86, 0x50, 0xa4, 0x5a, 0x33, 0xc7, 0x1a, 0x37, 0x46, 0xc4, 0x1d, 0x56, 0xae, 0x9d, + 0xda, 0xeb, 0x00, 0x2c, 0xaf, 0xc6, 0x05, 0x61, 0x93, 0x1c, 0x3b, 0x17, 0xb0, 0x1a, 0x0d, 0xa9, 0x76, 0xc3, 0xad, + 0x97, 0x9d, 0xdf, 0x3c, 0x4e, 0x6c, 0x6d, 0x84, 0x5b, 0x0a, 0x28, 0xa3, 0xfc, 0xc6, 0x72, 0xc2, 0xee, 0x54, 0xef, + 0x48, 0xd5, 0x8e, 0x38, 0x71, 0x01, 0xcb, 0x0d, 0x4f, 0xad, 0xbe, 0x89, 0xc1, 0x89, 0x50, 0xb5, 0xd2, 0xe1, 0x4e, + 0x26, 0x10, 0xf7, 0xab, 0xfb, 0xba, 0x57, 0x82, 0x9f, 0x84, 0xbc, 0x7e, 0xcb, 0x3b, 0x00, 0xac, 0xf8, 0x90, 0x17, + 0xd3, 0xc2, 0xd1, 0xba, 0x0c, 0xca, 0x00, 0x11, 0x9a, 0x01, 0xd0, 0xc9, 0xd5, 0x41, 0x94, 0x06, 0xae, 0xb8, 0x43, + 0x84, 0x9f, 0x46, 0xcf, 0xf2, 0xeb, 0xf0, 0x59, 0x35, 0x0d, 0x2f, 0xf2, 0x20, 0xba, 0xa8, 0x82, 0xe8, 0x59, 0x75, + 0x15, 0x3e, 0xcb, 0xa7, 0xd1, 0x45, 0x1e, 0x84, 0x17, 0x55, 0x63, 0xdf, 0xb5, 0xbb, 0x7b, 0x42, 0xde, 0x76, 0xf5, + 0x47, 0xce, 0x95, 0x3d, 0x65, 0x7a, 0x7e, 0x5e, 0xeb, 0x95, 0xda, 0x6d, 0xae, 0xd7, 0xa8, 0x99, 0xfa, 0x28, 0xfb, + 0x8b, 0x6d, 0x2c, 0x3c, 0x9a, 0x43, 0xe8, 0x33, 0xd2, 0x62, 0xee, 0x71, 0xae, 0x37, 0x7b, 0x52, 0x18, 0x18, 0x31, + 0xa9, 0x64, 0xe4, 0xf4, 0x02, 0x17, 0xa1, 0x0a, 0x31, 0xac, 0xa5, 0xab, 0x7d, 0xd6, 0xa5, 0x37, 0x50, 0xd7, 0x14, + 0xfb, 0x1a, 0x32, 0xf0, 0xa2, 0xe9, 0x65, 0x30, 0x06, 0xe4, 0x08, 0xbc, 0xe3, 0xb3, 0x25, 0x1c, 0x98, 0x6b, 0x80, + 0xbe, 0x79, 0xd4, 0xd7, 0xe5, 0x8e, 0xaf, 0x55, 0xdf, 0x4c, 0xd7, 0x23, 0xa5, 0xfc, 0x58, 0xf1, 0xbb, 0x8b, 0xe7, + 0xec, 0x96, 0x6b, 0x54, 0x94, 0x5f, 0xf4, 0x62, 0xbd, 0x07, 0xae, 0xba, 0x5f, 0xe0, 0x36, 0x8b, 0xc7, 0xae, 0x3c, + 0x60, 0xd9, 0x96, 0x3d, 0xb0, 0x1b, 0xf6, 0x81, 0x3d, 0x61, 0x6f, 0xd9, 0x7b, 0x56, 0x23, 0x44, 0x79, 0xa9, 0xa4, + 0x3c, 0x7f, 0xc1, 0x6f, 0xa5, 0xed, 0x51, 0xc2, 0x92, 0x3d, 0xd8, 0x76, 0x9a, 0xe1, 0x86, 0x7d, 0xe0, 0x8b, 0xe1, + 0x8a, 0xbd, 0x85, 0x6c, 0x28, 0x14, 0x0f, 0x56, 0xac, 0x86, 0x2b, 0x2c, 0x65, 0xd0, 0xa7, 0x61, 0x69, 0x09, 0x8b, + 0xa6, 0x50, 0x94, 0xa2, 0xdf, 0xf2, 0x9a, 0xb0, 0xd3, 0x6a, 0x2c, 0x44, 0x7e, 0x68, 0xb8, 0x62, 0x0f, 0x7c, 0x31, + 0x58, 0xb1, 0x0f, 0xda, 0x46, 0x34, 0xd8, 0xb8, 0xc5, 0x11, 0x98, 0x95, 0x2e, 0x4c, 0x0a, 0xd4, 0x5b, 0xfb, 0x26, + 0xb8, 0x61, 0x37, 0x58, 0xbf, 0x27, 0x58, 0x34, 0xca, 0xfc, 0x83, 0x15, 0x7b, 0xcf, 0x25, 0x86, 0x9a, 0x5b, 0x9e, + 0x74, 0x0c, 0xd5, 0x05, 0xd2, 0x15, 0xe1, 0x09, 0xa7, 0x17, 0xd9, 0x7b, 0x2c, 0x83, 0xbe, 0x32, 0x5c, 0xb1, 0x2d, + 0xd6, 0xee, 0xc6, 0x18, 0xb7, 0xac, 0xea, 0x49, 0x50, 0x60, 0x94, 0x55, 0x4a, 0xcb, 0xc5, 0x11, 0xcb, 0xa6, 0x8e, + 0x1a, 0xd4, 0x86, 0x01, 0x7d, 0x30, 0xfa, 0x0f, 0x5f, 0xbf, 0xfb, 0xd1, 0x2b, 0xf5, 0xcd, 0xf7, 0x17, 0xc7, 0xbb, + 0xb2, 0x44, 0xef, 0xca, 0x5f, 0x79, 0x39, 0xfb, 0x65, 0x3e, 0xd1, 0xb5, 0xa4, 0x4d, 0x86, 0xdc, 0x4d, 0x67, 0xbf, + 0x74, 0xf8, 0x5b, 0xfe, 0xea, 0xfb, 0x8d, 0xd5, 0xc7, 0xea, 0xbb, 0xba, 0x7b, 0x1f, 0x06, 0x9b, 0xc6, 0xa9, 0xf8, + 0xee, 0x74, 0xc5, 0xb1, 0x9d, 0xb5, 0xf6, 0xce, 0xfc, 0x1f, 0xae, 0xf5, 0x16, 0xc7, 0xee, 0x86, 0x6f, 0x87, 0x1b, + 0x7b, 0x18, 0xe4, 0xf7, 0xa5, 0xe2, 0x38, 0xab, 0xf9, 0x0b, 0xaf, 0x53, 0x92, 0x05, 0x54, 0xa3, 0xcf, 0x46, 0x1a, + 0xba, 0x64, 0x26, 0xa6, 0x21, 0xbe, 0xc8, 0x00, 0x9d, 0x0b, 0xc4, 0xb3, 0x7b, 0x3e, 0x9e, 0xdc, 0x5f, 0xc5, 0x93, + 0xfb, 0x01, 0xff, 0x6c, 0x5a, 0xd0, 0x5e, 0x70, 0xf7, 0x3e, 0xfb, 0x95, 0x17, 0xf6, 0x92, 0xfc, 0xc5, 0x67, 0x5f, + 0x85, 0xbb, 0x4a, 0x7f, 0xf1, 0xd9, 0x7b, 0xc1, 0x7f, 0x1d, 0x69, 0xb2, 0x0c, 0xf6, 0xbe, 0xe6, 0xbf, 0x8e, 0x90, + 0xf5, 0x83, 0x7d, 0x11, 0xfc, 0x2b, 0xf8, 0x7f, 0x57, 0x09, 0x5a, 0xc6, 0xbf, 0xd4, 0xea, 0xe7, 0x07, 0x19, 0x9b, + 0x03, 0x6f, 0x42, 0x2b, 0xe8, 0xcd, 0xdb, 0x5a, 0xfe, 0x24, 0x2e, 0x8e, 0x54, 0x3d, 0x35, 0x1c, 0xb4, 0x58, 0xcc, + 0xa2, 0x3e, 0x4a, 0xa7, 0xf2, 0x26, 0xef, 0x78, 0x26, 0x2d, 0xcc, 0xf7, 0x10, 0x0e, 0xfc, 0xce, 0x86, 0x29, 0xd8, + 0x71, 0xdc, 0x0c, 0xde, 0xb1, 0xf7, 0xc2, 0x67, 0xd9, 0x74, 0xcb, 0x6f, 0xf8, 0x13, 0xfe, 0x9e, 0xef, 0x82, 0x07, + 0xfe, 0x81, 0xbf, 0xe5, 0x75, 0xcd, 0x77, 0x6c, 0x29, 0x21, 0x4f, 0xeb, 0xed, 0x65, 0xb0, 0x65, 0xf5, 0xee, 0x32, + 0x78, 0x60, 0xf5, 0xf6, 0x79, 0x70, 0xc3, 0xea, 0xdd, 0xf3, 0xe0, 0x03, 0xdb, 0x5e, 0x06, 0x4f, 0xd8, 0xee, 0x32, + 0x78, 0xcb, 0xb6, 0xcf, 0x83, 0xf7, 0x6c, 0xf7, 0x3c, 0xa8, 0x15, 0xd2, 0xc3, 0x7b, 0x21, 0x99, 0x4e, 0xde, 0xd7, + 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, 0x60, 0x51, 0x37, 0xad, 0x93, 0xd9, 0xf1, 0x4e, + 0x8c, 0x3b, 0xbc, 0x13, 0x17, 0x6c, 0xd9, 0x74, 0xba, 0xea, 0x9c, 0x3e, 0x0f, 0xf4, 0x11, 0xa0, 0xf7, 0xfe, 0x4a, + 0x7a, 0xd0, 0x14, 0x0d, 0xcf, 0x95, 0xee, 0xb8, 0xb5, 0xdf, 0x87, 0xd6, 0x7e, 0xcf, 0xa4, 0x22, 0x2d, 0x62, 0x51, + 0x59, 0x54, 0x15, 0xf2, 0x89, 0x07, 0x99, 0xd6, 0xaa, 0x25, 0x8c, 0xd4, 0x99, 0x80, 0x49, 0x5f, 0xd0, 0x61, 0x90, + 0x93, 0x5d, 0x81, 0x2d, 0xf9, 0x66, 0x90, 0xb0, 0x35, 0x8f, 0xa7, 0xc3, 0x24, 0x58, 0xb2, 0x3b, 0x3e, 0xec, 0x16, + 0x0b, 0x56, 0x2a, 0x8c, 0x49, 0x5f, 0x9f, 0x8e, 0x76, 0x77, 0xde, 0x5b, 0xa5, 0x71, 0x9c, 0x09, 0xd4, 0xb9, 0x55, + 0x7a, 0x9b, 0xdf, 0x3a, 0xbb, 0xfa, 0x5a, 0xed, 0xf2, 0x20, 0x30, 0xfc, 0x0a, 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, 0xcd, 0xab, 0xbf, 0x75, 0xbc, 0xba, 0xf1, 0x27, 0x0f, 0xfc, 0x33, 0x46, 0x27, 0x60, 0x22, 0xdb, 0xf1, 0xcf, + 0xa3, 0x6d, 0xe3, 0x94, 0x27, 0xf7, 0xf2, 0xff, 0x2b, 0x05, 0xda, 0xbb, 0x79, 0x65, 0x6f, 0x8a, 0x5b, 0xde, 0xb1, + 0x97, 0x2f, 0xad, 0x3d, 0xd1, 0x20, 0x94, 0x7c, 0xe6, 0x6e, 0x50, 0x34, 0xec, 0x89, 0xbf, 0xf0, 0x6a, 0xf6, 0x79, + 0x3e, 0xd9, 0xf2, 0xe3, 0x1d, 0xf1, 0x73, 0xc7, 0x8e, 0xf8, 0x8b, 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, 0xbe, + 0x0a, 0x39, 0x6d, 0x6f, 0x90, 0xc4, 0x89, 0x8e, 0x8a, 0xf7, 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, 0xbe, 0xdb, + 0xb7, 0x47, 0x85, 0xb5, 0xa9, 0x88, 0xb7, 0x5b, 0x0c, 0xc2, 0x3a, 0x99, 0x59, 0xe6, 0x92, 0x2f, 0xbd, 0xaf, 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, 0x3d, 0x1f, 0x66, 0x75, 0xeb, 0xda, 0x71, 0xf1, 0x7c, + 0xf8, 0x0b, 0xe4, 0x0d, 0xba, 0x1e, 0x9a, 0x22, 0x5a, 0xe5, 0x77, 0x14, 0x7d, 0xa2, 0xe4, 0xa0, 0xe3, 0x09, 0xd4, + 0x0e, 0xb9, 0x70, 0xdf, 0x3f, 0xe3, 0xa0, 0xe8, 0xc0, 0x52, 0xfb, 0xfd, 0xf3, 0xcf, 0x44, 0x28, 0x0d, 0xe3, 0xfd, + 0x32, 0x8c, 0xfe, 0x88, 0xcb, 0x62, 0x0d, 0x47, 0xec, 0x00, 0x3e, 0xf7, 0x4c, 0x5f, 0xc3, 0xee, 0x7c, 0xdf, 0x0f, + 0xbc, 0x2d, 0xbf, 0x61, 0xef, 0xb9, 0x77, 0x39, 0x7c, 0xeb, 0x3f, 0x7b, 0x02, 0xf2, 0x13, 0x8c, 0xcb, 0x17, 0x0c, + 0x89, 0xed, 0x28, 0x46, 0xad, 0xc3, 0x2f, 0x35, 0xc4, 0x6a, 0x7d, 0x46, 0xea, 0x2e, 0x48, 0xff, 0xa8, 0x90, 0xfd, + 0x84, 0xc0, 0x6a, 0x92, 0x3e, 0x05, 0x26, 0xf1, 0x6d, 0x0d, 0x09, 0xa4, 0x69, 0x81, 0x18, 0x1c, 0x28, 0x3e, 0x15, + 0xfc, 0xfd, 0xf0, 0x0b, 0xc9, 0x7f, 0x8b, 0x9a, 0x8f, 0xe1, 0x6f, 0x18, 0x9a, 0x49, 0xf5, 0x90, 0xd6, 0x51, 0xe2, + 0xd5, 0x70, 0xea, 0x85, 0x95, 0x50, 0x27, 0x43, 0x90, 0x8a, 0x21, 0x17, 0xe2, 0xe2, 0xf9, 0xe4, 0xb6, 0x14, 0xe1, + 0x1f, 0x13, 0x7c, 0x26, 0x57, 0x9a, 0x7c, 0x46, 0x4f, 0x1a, 0x59, 0xc0, 0x83, 0x7c, 0x5f, 0xf6, 0x6a, 0xb0, 0xa8, + 0x87, 0xfc, 0xb6, 0x76, 0xdf, 0x97, 0x73, 0x82, 0x1e, 0xd9, 0x0f, 0x68, 0x0e, 0x06, 0x6a, 0x06, 0x52, 0x86, 0xe0, + 0x16, 0x2e, 0xfd, 0x9e, 0x2a, 0xc8, 0x97, 0xdf, 0xfb, 0x22, 0x64, 0xe0, 0xca, 0x82, 0x30, 0xe5, 0x52, 0x21, 0x05, + 0x8e, 0xdb, 0x7a, 0xf0, 0x45, 0xa3, 0x93, 0x48, 0xf0, 0x29, 0x01, 0x49, 0xd2, 0xf2, 0x40, 0xd2, 0x88, 0xe9, 0x40, + 0x5c, 0x28, 0x4d, 0xb3, 0x92, 0x22, 0x0e, 0xb1, 0xab, 0xbe, 0x43, 0xc2, 0xb3, 0xe0, 0x03, 0x83, 0xb5, 0x23, 0x45, + 0x8b, 0xf7, 0xc6, 0x74, 0xac, 0xc3, 0x86, 0xee, 0x64, 0x71, 0xbf, 0x4a, 0xea, 0x34, 0x12, 0x57, 0xbe, 0x0a, 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, 0xf4, 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, 0xfb, 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, 0x6a, 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, 0xa3, 0xa7, 0xf5, 0x48, 0x53, 0x4c, 0x82, 0x93, 0x2e, 0xfe, 0x02, 0x6c, 0x28, + 0x4f, 0x72, 0x73, 0x40, 0x0e, 0xa0, 0x72, 0x29, 0x4a, 0xa5, 0x0c, 0xfe, 0x59, 0xdd, 0x91, 0x6d, 0xd5, 0x7f, 0xa7, + 0x81, 0x0c, 0xee, 0x40, 0xdf, 0xf6, 0x42, 0x6b, 0x47, 0x3b, 0x57, 0xb6, 0xa6, 0x6d, 0x99, 0xe6, 0x31, 0xb2, 0xd8, + 0x00, 0xf2, 0x89, 0x74, 0x0e, 0x44, 0x5e, 0x13, 0x8d, 0x77, 0x76, 0xcd, 0xc7, 0x53, 0xf1, 0x98, 0xbc, 0x57, 0xf9, + 0xbe, 0xb9, 0xd7, 0x07, 0x63, 0xec, 0x5b, 0x50, 0x26, 0x3e, 0x5a, 0x6d, 0xad, 0x4b, 0xac, 0xb7, 0x4a, 0x93, 0xe8, + 0x86, 0x2b, 0xe8, 0x38, 0x12, 0x37, 0x88, 0xc1, 0x31, 0xe3, 0xb5, 0x55, 0x96, 0xbe, 0xc2, 0x32, 0xd7, 0x31, 0x4b, + 0x86, 0x4c, 0xea, 0x3c, 0x51, 0xf0, 0xe4, 0xe7, 0x09, 0xc9, 0x88, 0xa8, 0xd9, 0x96, 0xa3, 0x94, 0x9b, 0x16, 0x70, + 0x99, 0x91, 0x01, 0x7c, 0x93, 0x26, 0x00, 0xe5, 0xf2, 0x25, 0x48, 0xa5, 0x21, 0x82, 0x6b, 0xb6, 0x97, 0x8c, 0x6e, + 0x1d, 0xad, 0x83, 0x2a, 0xc9, 0xdc, 0xc1, 0xb9, 0x9d, 0x45, 0x4a, 0xbd, 0xf9, 0x08, 0xc3, 0x4e, 0x3e, 0x86, 0x75, + 0x82, 0xdf, 0x06, 0xd4, 0xa4, 0xcf, 0x85, 0x17, 0x8d, 0x00, 0x4d, 0x7d, 0xa7, 0xca, 0xf8, 0x5c, 0x78, 0xd9, 0x68, + 0xcb, 0x32, 0x4a, 0xa1, 0xba, 0x60, 0x76, 0x6b, 0xba, 0x10, 0xf3, 0xaa, 0x1a, 0x68, 0x83, 0xdc, 0xae, 0x63, 0x06, + 0x34, 0x6a, 0xbb, 0xf2, 0xc8, 0x02, 0xdc, 0x9a, 0x89, 0xc0, 0xc8, 0xf9, 0x0f, 0xf9, 0x2b, 0x15, 0xce, 0xd3, 0xef, + 0x87, 0xde, 0x7e, 0x1b, 0x44, 0xa3, 0xed, 0x25, 0xdb, 0x05, 0xd1, 0x68, 0x77, 0xd9, 0x30, 0xfa, 0xfd, 0x9c, 0x7e, + 0x3f, 0x6f, 0x40, 0x55, 0x22, 0x4c, 0xc4, 0xbd, 0x7e, 0xa3, 0x96, 0xaf, 0xd4, 0xfa, 0x9d, 0x5a, 0xbe, 0x54, 0xc3, + 0x5b, 0x7b, 0x12, 0x09, 0x22, 0x4b, 0x63, 0xf3, 0x20, 0xd9, 0x52, 0x2d, 0x95, 0x8e, 0x51, 0x65, 0x44, 0x2d, 0x9d, + 0xcd, 0xb1, 0x62, 0xa4, 0x9d, 0x83, 0x92, 0x01, 0x99, 0x16, 0x57, 0x35, 0xa6, 0x9b, 0x15, 0x2d, 0x31, 0x19, 0x61, + 0x65, 0x5b, 0xde, 0x6e, 0x52, 0x35, 0x9d, 0x93, 0x9b, 0x5b, 0xa5, 0xdc, 0xdc, 0x0a, 0x9e, 0x7f, 0x43, 0xb7, 0x5c, + 0x72, 0xed, 0x65, 0x36, 0x2d, 0x94, 0x6e, 0x19, 0xd7, 0x60, 0x6b, 0xdf, 0x04, 0xb2, 0xcc, 0x47, 0x8a, 0x1a, 0xdb, + 0x8b, 0x46, 0xf9, 0x06, 0xd9, 0x8a, 0x18, 0x75, 0xca, 0x82, 0xf1, 0xb7, 0x3b, 0x7a, 0x20, 0x03, 0x55, 0x55, 0x6d, + 0x1c, 0xdc, 0x59, 0xe9, 0x0f, 0xcb, 0x8b, 0xe7, 0x2c, 0xb1, 0xd2, 0xc9, 0x85, 0x2a, 0xf4, 0x07, 0x21, 0xba, 0xa9, + 0x6c, 0x38, 0x38, 0xd4, 0xc5, 0x56, 0x06, 0x84, 0x1e, 0xa6, 0xf7, 0x36, 0x56, 0xb2, 0xdc, 0x35, 0xe5, 0x8b, 0x19, + 0x4f, 0x38, 0x8e, 0xbe, 0x5c, 0x2d, 0xc2, 0x5a, 0x2d, 0xb2, 0x13, 0xe0, 0xa1, 0xb5, 0x5a, 0x0a, 0xb9, 0x5a, 0x84, + 0x33, 0xd3, 0x85, 0x9a, 0xe9, 0x19, 0x28, 0x20, 0x85, 0x9a, 0xe5, 0x09, 0xc0, 0xc2, 0x0b, 0x33, 0xc3, 0x85, 0x99, + 0xe1, 0x38, 0xa4, 0xc6, 0xff, 0x41, 0xef, 0x75, 0xee, 0xb9, 0xe5, 0x6e, 0x74, 0x1a, 0xf1, 0xed, 0x68, 0x83, 0x39, + 0x3e, 0x08, 0x27, 0x10, 0x2a, 0x58, 0x22, 0x56, 0x8f, 0x46, 0xd8, 0x51, 0x43, 0xe5, 0x68, 0xbf, 0x2c, 0x2c, 0xc9, + 0x92, 0xb0, 0x24, 0xf7, 0x6a, 0x9c, 0x4b, 0xcb, 0xc5, 0xab, 0x24, 0x10, 0x89, 0x8c, 0x97, 0xd2, 0x04, 0x9f, 0xf0, + 0x72, 0x64, 0xa4, 0xe6, 0xc9, 0x22, 0xf5, 0x72, 0x96, 0xb1, 0x31, 0x62, 0x18, 0x85, 0x7e, 0x53, 0xf5, 0xfb, 0x69, + 0xe9, 0xe5, 0xd4, 0xce, 0xcf, 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, 0x3e, 0x71, 0xdd, 0x59, 0x6c, 0x60, + 0x82, 0xc3, 0x01, 0x10, 0x0f, 0xa6, 0x5e, 0x34, 0xe0, 0xa5, 0x9a, 0x33, 0x9f, 0xbc, 0x9c, 0x60, 0x32, 0x40, 0x55, + 0x31, 0x70, 0xca, 0x7a, 0x26, 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, 0x6b, 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, 0xcb, 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, 0x5f, 0x48, 0xa7, 0x6c, 0x84, 0x62, 0x2c, 0xd3, 0x78, 0xf4, 0x95, + 0x0d, 0x11, 0xda, 0xea, 0x05, 0x9a, 0x98, 0xa0, 0x0e, 0xf0, 0x88, 0xfe, 0x1a, 0x7d, 0x35, 0x14, 0x2a, 0x5d, 0x8d, + 0xd4, 0x35, 0x3b, 0xe7, 0xfc, 0x6b, 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, 0xa4, 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, 0xc6, 0xe3, 0x9b, 0x1b, 0x3d, 0xf9, 0xa0, + 0x07, 0xb7, 0xb7, 0xb7, 0xaf, 0x7b, 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, 0x6b, 0xed, 0x09, 0xda, 0x4a, 0x1a, 0x08, 0xf5, 0x7c, 0x9d, 0xde, 0x51, 0x14, 0x8f, 0x33, 0x13, 0x4f, 0x55, + 0x60, 0xec, 0x5b, 0x3b, 0x82, 0x82, 0xa4, 0xe9, 0x3a, 0xe0, 0x30, 0x8d, 0x4e, 0x58, 0xfc, 0x53, 0xfa, 0x50, 0x5e, + 0xd4, 0x0a, 0x9c, 0xe4, 0xef, 0xc2, 0x45, 0x24, 0xb1, 0xd0, 0x2f, 0x09, 0x80, 0x44, 0x06, 0xaf, 0x46, 0xc5, 0x5a, + 0xa8, 0x00, 0x39, 0x45, 0xe9, 0xad, 0xe2, 0xe3, 0x52, 0x94, 0x2a, 0xa5, 0x32, 0x37, 0x2a, 0x05, 0x84, 0xb5, 0x81, + 0xa3, 0x0b, 0xf8, 0x02, 0x82, 0xd6, 0x72, 0xb7, 0xb6, 0x3d, 0x6f, 0x64, 0x3e, 0x33, 0xcd, 0xd3, 0xea, 0xa3, 0xfa, + 0xfb, 0xc3, 0x12, 0xc3, 0x6c, 0x3c, 0xfd, 0x7d, 0x9b, 0x21, 0xdc, 0xfc, 0x0d, 0x43, 0x74, 0x07, 0xe0, 0x98, 0xa5, + 0x3d, 0x14, 0xb2, 0x60, 0x82, 0x35, 0x54, 0xe5, 0x29, 0x9f, 0xbd, 0x7c, 0x72, 0x0b, 0x68, 0x6a, 0xe8, 0xe2, 0x46, + 0xa7, 0xba, 0x2a, 0x41, 0xf8, 0xbe, 0x2b, 0xd4, 0x63, 0x73, 0xc0, 0xa9, 0x01, 0xa0, 0x58, 0xe4, 0xb5, 0x1e, 0xdb, + 0x3f, 0xe8, 0x8d, 0x7a, 0x03, 0xc4, 0xd3, 0x39, 0x2f, 0xfc, 0x23, 0xfa, 0x75, 0xea, 0xcf, 0xb8, 0x10, 0x44, 0xbd, + 0x9e, 0x84, 0xf7, 0xe2, 0x2c, 0x8d, 0x83, 0xb3, 0xde, 0xc0, 0x5c, 0x04, 0x8a, 0xb3, 0x34, 0x3f, 0x03, 0xb1, 0x1c, + 0xe1, 0x11, 0x6b, 0x76, 0x07, 0x88, 0x81, 0xa5, 0x0e, 0x49, 0x56, 0x1d, 0xdb, 0xef, 0xbf, 0x19, 0x19, 0xde, 0x74, + 0x44, 0x84, 0xd1, 0xbf, 0x2b, 0x10, 0xa0, 0x60, 0x99, 0xd9, 0xce, 0x4c, 0xba, 0xda, 0xb3, 0x7a, 0xde, 0x6c, 0xf2, + 0xae, 0xde, 0xb1, 0x9a, 0x96, 0x53, 0xd3, 0x2a, 0xab, 0x69, 0x93, 0x1c, 0x6a, 0x26, 0xfa, 0x7d, 0x8d, 0x8f, 0x9a, + 0xcf, 0x01, 0x97, 0x0d, 0x93, 0xdf, 0xcc, 0xaa, 0x79, 0xbf, 0xef, 0xc9, 0x47, 0xf0, 0x0b, 0x89, 0xcb, 0xdc, 0x1a, + 0xcb, 0xa7, 0xaf, 0x89, 0xcf, 0xcc, 0x20, 0x1e, 0xdd, 0x1d, 0x41, 0x7d, 0xdd, 0x08, 0xaf, 0x63, 0xae, 0xb0, 0x99, + 0x98, 0xbe, 0x81, 0xc1, 0xf3, 0x84, 0x0f, 0xde, 0x72, 0xf4, 0x37, 0xd2, 0x99, 0x29, 0x58, 0xc8, 0xb9, 0x3f, 0x79, + 0x83, 0xd0, 0xc9, 0x88, 0xf4, 0xa0, 0xd3, 0x09, 0x1a, 0xb2, 0xdf, 0x5f, 0x41, 0x67, 0xb6, 0x52, 0x29, 0x5b, 0x15, + 0x95, 0xe9, 0xba, 0x2e, 0xca, 0x0a, 0x3a, 0x96, 0x7e, 0xde, 0x0a, 0x99, 0x59, 0x3f, 0xb3, 0x90, 0x9f, 0x56, 0x12, + 0x6b, 0xca, 0xb6, 0x4f, 0xd4, 0x06, 0x69, 0xd6, 0x85, 0xea, 0x02, 0xe7, 0xce, 0xda, 0xeb, 0x8d, 0x50, 0xff, 0x9c, + 0x8f, 0xd6, 0xc5, 0xda, 0x03, 0x97, 0x98, 0x59, 0x3a, 0x57, 0x1c, 0x1a, 0xb9, 0x3f, 0xfa, 0x52, 0xa4, 0x39, 0xe5, + 0x01, 0x1a, 0x44, 0x31, 0xb7, 0xdf, 0x02, 0xe9, 0x87, 0xde, 0x02, 0xd9, 0x47, 0xe7, 0x9c, 0xbc, 0x01, 0x70, 0x3a, + 0x44, 0xc4, 0xad, 0x48, 0xd0, 0xb1, 0x6a, 0x78, 0x6b, 0xe1, 0x9e, 0xf6, 0xd2, 0xb8, 0x97, 0xe6, 0x67, 0x69, 0xbf, + 0x6f, 0x00, 0x34, 0x53, 0x44, 0x86, 0xc7, 0x19, 0xb9, 0x48, 0x5a, 0x08, 0xa6, 0xb4, 0xff, 0x6a, 0x0c, 0x09, 0x02, + 0x01, 0xff, 0xbb, 0xf0, 0x9e, 0x00, 0xda, 0x26, 0x6d, 0xc0, 0x55, 0x8f, 0xe9, 0xc0, 0x6c, 0xc9, 0xd9, 0xaa, 0xb3, + 0x01, 0x28, 0xa7, 0x4a, 0xeb, 0x29, 0x8f, 0x6b, 0x8a, 0x88, 0x54, 0x59, 0xa8, 0xdf, 0x58, 0x4f, 0x26, 0xab, 0x5c, + 0x64, 0xc8, 0x51, 0x99, 0xbe, 0xd6, 0x8c, 0x10, 0xbb, 0xf4, 0xf3, 0x05, 0x2c, 0xd9, 0xf8, 0x13, 0x4e, 0xde, 0x12, + 0x20, 0x6d, 0x67, 0xed, 0xaa, 0xda, 0xe5, 0xb8, 0xb5, 0x9b, 0x03, 0x92, 0xaf, 0x37, 0x1a, 0x8d, 0xb4, 0x9f, 0x9c, + 0x80, 0xa1, 0xea, 0xa9, 0xa5, 0xd0, 0x63, 0xb5, 0xc2, 0xd6, 0xed, 0xc8, 0x65, 0x96, 0x0c, 0xe6, 0x0b, 0xe3, 0xf8, + 0x95, 0xf9, 0xe8, 0xe3, 0xa5, 0xb2, 0x76, 0x1d, 0xf1, 0xf5, 0x1f, 0x65, 0xb5, 0xbe, 0xe7, 0x5d, 0xd5, 0x04, 0x7c, + 0x51, 0xc5, 0x96, 0x7e, 0xc7, 0x7b, 0xb2, 0x77, 0xf1, 0xb5, 0x1b, 0xec, 0x92, 0xef, 0x79, 0x8b, 0x3a, 0xcf, 0x57, + 0xbe, 0x6e, 0x54, 0xe9, 0xf6, 0x5e, 0xb2, 0xc0, 0xb5, 0x77, 0xd4, 0x34, 0xd6, 0x33, 0x3f, 0x7a, 0x58, 0x84, 0x6c, + 0xe7, 0x63, 0xef, 0xab, 0xe6, 0xe9, 0x59, 0x43, 0x6f, 0x52, 0x43, 0x1f, 0x7b, 0x51, 0xb6, 0x4f, 0x4d, 0x23, 0x7a, + 0x0d, 0x1b, 0xfa, 0xd8, 0x5b, 0x72, 0x72, 0x48, 0x30, 0x38, 0x35, 0xe6, 0x8f, 0x0f, 0xa7, 0x33, 0xfc, 0x1d, 0x03, + 0x2a, 0x31, 0x99, 0x4f, 0x8f, 0x69, 0x47, 0x01, 0x66, 0x54, 0xe9, 0xed, 0xd3, 0x03, 0xdb, 0xf1, 0xb2, 0x1e, 0x5a, + 0x7a, 0xf7, 0xe4, 0xe8, 0x76, 0xbc, 0xaa, 0xc6, 0x97, 0x72, 0xc8, 0xf3, 0x7c, 0x36, 0x1a, 0x8d, 0x84, 0x41, 0xe7, + 0xae, 0xf4, 0x06, 0x56, 0x20, 0x83, 0x8b, 0xea, 0x43, 0xb9, 0xf4, 0x76, 0xea, 0xd0, 0xae, 0xfc, 0x49, 0x7e, 0x38, + 0x14, 0x23, 0x73, 0x8c, 0x03, 0xce, 0x49, 0xa1, 0xe4, 0x28, 0x59, 0x4b, 0x10, 0x9d, 0xd2, 0x78, 0x2a, 0xeb, 0xb5, + 0x15, 0x91, 0x57, 0x23, 0xe4, 0x43, 0xf0, 0x93, 0x07, 0x6a, 0xf1, 0x6b, 0x2d, 0x88, 0x3d, 0xf6, 0xa9, 0x52, 0x3a, + 0xc4, 0xab, 0x02, 0x42, 0x84, 0x01, 0x6f, 0xa0, 0x1d, 0x94, 0xe0, 0xb0, 0xc3, 0x7d, 0x44, 0x84, 0xe8, 0xd7, 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, 0x17, 0x89, 0xa4, + 0x21, 0xda, 0x75, 0x1e, 0x11, 0x0b, 0x00, 0xc9, 0xe2, 0xb3, 0x79, 0x43, 0x51, 0xef, 0x26, 0x7c, 0x8b, 0xee, 0xb2, + 0xd8, 0xef, 0x6f, 0xf3, 0xb4, 0xee, 0xe9, 0x50, 0x19, 0x7c, 0x41, 0xaa, 0x09, 0xf0, 0x68, 0x7f, 0x6d, 0x8e, 0x57, + 0xaf, 0x36, 0x47, 0xca, 0x42, 0x95, 0xa8, 0xdf, 0x62, 0x35, 0xeb, 0x21, 0x22, 0x77, 0x96, 0x19, 0x7b, 0x7b, 0xc1, + 0x2b, 0x39, 0xab, 0x62, 0xbb, 0x1c, 0x5f, 0x11, 0xd6, 0x56, 0x12, 0xa0, 0xa3, 0xf5, 0x58, 0x9b, 0x62, 0xe4, 0x57, + 0x0a, 0x09, 0xb8, 0xe8, 0xd8, 0x5a, 0x28, 0x36, 0x5e, 0x80, 0xbe, 0x64, 0x67, 0x1a, 0x60, 0xbd, 0xd1, 0xab, 0x88, + 0xdb, 0xf2, 0x91, 0x0a, 0x6f, 0x72, 0x53, 0x65, 0x56, 0x36, 0x8b, 0x76, 0x3f, 0x55, 0xbc, 0x42, 0xdc, 0x7a, 0xa3, + 0xf6, 0x28, 0x40, 0xed, 0xa1, 0x85, 0x32, 0x40, 0x97, 0xa6, 0x19, 0x00, 0x32, 0x00, 0xc8, 0x54, 0x11, 0x9f, 0x09, + 0x50, 0x69, 0xab, 0x1b, 0x05, 0x4e, 0xa4, 0xd7, 0xc0, 0xb8, 0xc0, 0x4a, 0x1f, 0xd9, 0xc8, 0x60, 0xb1, 0x45, 0x80, + 0x5b, 0x8e, 0xf4, 0x61, 0x1a, 0x4e, 0xb6, 0xd1, 0x1c, 0x26, 0x69, 0x7e, 0x1f, 0x66, 0xa9, 0x84, 0x96, 0xf8, 0x51, + 0xd6, 0x18, 0xb1, 0x80, 0xf4, 0x7d, 0x7a, 0x51, 0x64, 0x31, 0x41, 0xc2, 0x59, 0x4f, 0x1d, 0x40, 0x35, 0x39, 0xd7, + 0x9a, 0x56, 0xcf, 0x6a, 0x93, 0x87, 0x2c, 0xd0, 0xd9, 0x83, 0x31, 0xa9, 0xe5, 0x86, 0x1e, 0xd9, 0x5f, 0x39, 0x9e, + 0x11, 0xbe, 0xeb, 0x19, 0x4e, 0xfd, 0x77, 0x53, 0x03, 0x29, 0x53, 0x02, 0x08, 0x32, 0x38, 0x9a, 0x10, 0xca, 0xd3, + 0x31, 0x99, 0xda, 0xfc, 0x08, 0x84, 0x23, 0x82, 0x57, 0xf0, 0xdc, 0xd0, 0xba, 0xe5, 0xc6, 0xce, 0x22, 0x4f, 0x13, + 0x40, 0x16, 0x2f, 0xf8, 0x16, 0x90, 0x39, 0xf5, 0xaa, 0x90, 0x3d, 0x7b, 0x2e, 0xa6, 0xb3, 0x79, 0xf0, 0x90, 0xd0, + 0xfe, 0xc5, 0x84, 0xdf, 0x74, 0x57, 0xc9, 0x95, 0xa9, 0x75, 0x6f, 0xa2, 0xc7, 0x5c, 0xee, 0xf4, 0x69, 0xc5, 0x31, + 0xe2, 0x19, 0xac, 0x02, 0x72, 0xce, 0x86, 0xfc, 0xfa, 0x1c, 0xb0, 0x5b, 0x56, 0xc2, 0x8b, 0xf8, 0x75, 0x28, 0xab, + 0x05, 0xc8, 0x8f, 0x9c, 0x47, 0xe6, 0x97, 0xaf, 0xb6, 0x43, 0x39, 0xa7, 0x28, 0xa2, 0xe5, 0xd4, 0xb4, 0xa4, 0x90, + 0x1d, 0x7a, 0x0a, 0x26, 0x53, 0x5b, 0xfe, 0xde, 0x26, 0x2e, 0xc9, 0x37, 0x93, 0xc8, 0xbe, 0x0e, 0xb0, 0x66, 0xad, + 0xba, 0x87, 0x6e, 0x08, 0x06, 0x88, 0x8c, 0x50, 0x66, 0x73, 0x7d, 0xb7, 0x1e, 0x0c, 0x14, 0xcc, 0xaf, 0xa0, 0x9b, + 0x16, 0x9d, 0xe2, 0x00, 0x39, 0x6b, 0x5d, 0xa3, 0x52, 0x55, 0x1c, 0x3a, 0xcc, 0xbb, 0x65, 0x55, 0x76, 0x59, 0x7a, + 0x21, 0x48, 0x8d, 0xba, 0x0a, 0x16, 0x29, 0x15, 0x51, 0xbc, 0x27, 0xbf, 0x06, 0x26, 0x9e, 0x59, 0x39, 0x4a, 0xe3, + 0x39, 0x20, 0x06, 0x29, 0x20, 0x4e, 0xf9, 0x15, 0xa0, 0x89, 0x2e, 0xa2, 0x30, 0x7b, 0x1b, 0x57, 0x41, 0x6d, 0x35, + 0xfd, 0xde, 0x81, 0x8c, 0x3d, 0xaf, 0xfb, 0xfd, 0x94, 0x18, 0xfd, 0x30, 0x0a, 0x03, 0xff, 0x1e, 0x4f, 0xf7, 0x4d, + 0x90, 0x9a, 0x57, 0x1e, 0xe0, 0x15, 0x5d, 0x6e, 0x6d, 0xca, 0x15, 0x8d, 0x0b, 0x7f, 0x8d, 0xe0, 0xf0, 0xa9, 0xa3, + 0xd8, 0x6e, 0x53, 0xe5, 0xd4, 0xc6, 0x60, 0x10, 0xc2, 0x7d, 0x2b, 0xe3, 0xf7, 0x89, 0x97, 0xcf, 0xa2, 0x39, 0x28, + 0x4a, 0x33, 0xcd, 0x17, 0x52, 0x48, 0x37, 0x01, 0xfa, 0x68, 0x10, 0x6a, 0x75, 0xe5, 0x1f, 0x89, 0x97, 0xaa, 0x69, + 0x6d, 0x9e, 0x62, 0x8d, 0x02, 0x31, 0x8b, 0xe6, 0x0d, 0xcb, 0xe8, 0x90, 0x54, 0x97, 0x4b, 0xd3, 0x8c, 0x3f, 0xac, + 0x66, 0xa8, 0x56, 0x1c, 0x35, 0x41, 0x8d, 0xd2, 0x0d, 0x5c, 0x00, 0xff, 0x46, 0x77, 0x1c, 0xd5, 0x28, 0x52, 0x34, + 0xe0, 0x13, 0xc4, 0x88, 0x35, 0x9b, 0x27, 0xac, 0x35, 0x75, 0xcd, 0xe8, 0xf7, 0x65, 0xc8, 0x90, 0x49, 0x42, 0x9e, + 0x3e, 0x5c, 0xae, 0x9f, 0x48, 0x75, 0x01, 0xfc, 0xca, 0x15, 0x9b, 0xf5, 0x7a, 0x73, 0x80, 0xeb, 0x85, 0xf5, 0x0b, + 0x1b, 0x57, 0x70, 0x7e, 0x49, 0xf0, 0xbb, 0xea, 0x47, 0x98, 0x65, 0x50, 0x05, 0x64, 0xfc, 0xb1, 0x90, 0xce, 0x73, + 0x17, 0x93, 0xfa, 0xcd, 0x48, 0x5d, 0x50, 0x66, 0xe9, 0xdc, 0xe2, 0x04, 0x01, 0xe7, 0x61, 0xf5, 0x04, 0x92, 0x7d, + 0xf9, 0xd8, 0xa7, 0x19, 0x05, 0xaa, 0x23, 0xc0, 0x67, 0xb3, 0x7e, 0x08, 0xfb, 0x07, 0x44, 0x16, 0xea, 0x6f, 0xde, + 0xc8, 0x59, 0x43, 0xf2, 0x40, 0xaa, 0xb9, 0x8f, 0xe1, 0xd4, 0x58, 0xe0, 0x4b, 0x8b, 0xde, 0x54, 0xf0, 0x9a, 0x90, + 0xb9, 0x17, 0x68, 0xed, 0x5b, 0xc0, 0x11, 0x22, 0xb8, 0x8c, 0x52, 0x9c, 0xf6, 0x76, 0xbd, 0x00, 0xb9, 0xcd, 0x2d, + 0xc8, 0xeb, 0x77, 0x2e, 0x7e, 0x71, 0x8a, 0xf4, 0x2c, 0xba, 0xc0, 0x40, 0x17, 0x64, 0xde, 0xf8, 0x67, 0x05, 0x2b, + 0x17, 0xd0, 0x7b, 0xa9, 0x58, 0xc9, 0xc9, 0xb6, 0x53, 0x7f, 0x94, 0xca, 0x7e, 0x7b, 0x66, 0x4d, 0xe0, 0x57, 0x89, + 0xfd, 0x12, 0x99, 0x7c, 0xd3, 0x63, 0x93, 0xaf, 0x0c, 0x8b, 0x4e, 0x2d, 0x83, 0x73, 0x7a, 0x64, 0x70, 0xee, 0xed, + 0xac, 0xda, 0x84, 0x30, 0x14, 0x24, 0x81, 0xa6, 0x4b, 0x0f, 0xeb, 0xa6, 0x3f, 0x3f, 0x69, 0x51, 0x6d, 0xd5, 0xbe, + 0x75, 0x3f, 0x0e, 0xb1, 0x8b, 0x5f, 0x25, 0x9e, 0x21, 0x22, 0xf5, 0x81, 0x0e, 0x4c, 0x06, 0x4f, 0x5c, 0xf6, 0xfb, + 0x50, 0xd8, 0x6c, 0x3c, 0x1f, 0xd5, 0xc5, 0xcf, 0xc5, 0x03, 0xa0, 0x3a, 0x54, 0x60, 0x97, 0x43, 0x19, 0xca, 0x88, + 0x4d, 0x6d, 0xb9, 0xe7, 0xf7, 0x57, 0x61, 0x0e, 0xf2, 0x8e, 0x86, 0xc7, 0x39, 0x03, 0x31, 0x0c, 0xbe, 0xfe, 0xc3, + 0x93, 0x7d, 0xda, 0xfc, 0x70, 0x06, 0xdf, 0x1d, 0x9d, 0x7d, 0x44, 0xba, 0x9b, 0xb3, 0x75, 0x59, 0xdc, 0xa7, 0xb1, + 0x38, 0xfb, 0x01, 0x52, 0x7f, 0x38, 0x2b, 0xca, 0xb3, 0x1f, 0x54, 0x65, 0x7e, 0x38, 0xa3, 0x05, 0x37, 0xfa, 0xdd, + 0x9a, 0x78, 0xff, 0xa8, 0x34, 0x03, 0xda, 0x12, 0x22, 0xb3, 0xb4, 0xfa, 0x11, 0x94, 0x88, 0x8a, 0x1f, 0x55, 0x46, + 0xb5, 0x5a, 0x3b, 0xce, 0x87, 0x44, 0x23, 0x65, 0xd3, 0x84, 0xc4, 0xd5, 0x12, 0xd6, 0xa1, 0x9e, 0x9d, 0x36, 0xdf, + 0x8e, 0xf3, 0x40, 0x1d, 0x10, 0x39, 0xbf, 0xce, 0x47, 0x5b, 0xfa, 0x1a, 0x7c, 0xeb, 0x70, 0xc8, 0x47, 0x3b, 0xf3, + 0xd3, 0x27, 0x6b, 0xa5, 0x8c, 0x3b, 0x52, 0xe4, 0x42, 0xc8, 0x19, 0xb7, 0xed, 0x31, 0xe0, 0x00, 0xf0, 0x0f, 0x07, + 0xfa, 0xbd, 0x93, 0xbf, 0xd5, 0x6e, 0x69, 0xd5, 0xf3, 0x63, 0x8b, 0x3b, 0xe3, 0x75, 0x6d, 0x88, 0xda, 0xf6, 0x12, + 0x5b, 0x7a, 0xdf, 0x34, 0xa8, 0x29, 0xa2, 0x9f, 0xb0, 0x9a, 0x58, 0xc5, 0x61, 0x41, 0x4a, 0x48, 0x62, 0x38, 0x46, + 0x3b, 0xf4, 0x38, 0x5d, 0x2c, 0x3d, 0xb9, 0xef, 0xf0, 0x72, 0xeb, 0xfb, 0x80, 0xa4, 0x55, 0x38, 0xff, 0xe8, 0x85, + 0x06, 0x1e, 0xbd, 0xc8, 0xab, 0x22, 0x13, 0x23, 0x41, 0xa3, 0xfc, 0x96, 0xc4, 0x99, 0x33, 0xac, 0xc5, 0x99, 0x02, + 0x0b, 0x0b, 0x09, 0xa0, 0xbb, 0x28, 0x29, 0x3d, 0x38, 0x7b, 0xb2, 0x2f, 0x9b, 0xdf, 0x09, 0x1e, 0x62, 0xb4, 0x00, + 0x46, 0x9c, 0x5d, 0xbb, 0xbc, 0x87, 0xb0, 0xcc, 0xbd, 0xdf, 0xdf, 0xde, 0xe5, 0x05, 0x84, 0x68, 0x9e, 0x49, 0xc5, + 0x6a, 0x79, 0x06, 0x8c, 0x79, 0x22, 0x3e, 0x0b, 0x2b, 0x39, 0x0d, 0xaa, 0x8e, 0x62, 0xf5, 0x36, 0x9e, 0x7b, 0x40, + 0xf1, 0xfd, 0x21, 0x01, 0x2e, 0x77, 0x9f, 0xbd, 0x51, 0xae, 0xa9, 0xa4, 0x47, 0x9e, 0x63, 0xb4, 0x64, 0x02, 0x14, + 0xcf, 0x10, 0x27, 0x29, 0xac, 0x9e, 0x9b, 0x20, 0x15, 0xf9, 0xfa, 0x84, 0xe2, 0x8b, 0xe6, 0x51, 0xd4, 0xb0, 0x90, + 0x25, 0x70, 0x3c, 0x24, 0xb3, 0x6c, 0x8e, 0x2c, 0xe5, 0x69, 0x7b, 0x8a, 0x74, 0x74, 0x62, 0x89, 0xdf, 0xd6, 0xfc, + 0x7a, 0x91, 0x8a, 0xc0, 0xa4, 0x9d, 0xad, 0xcc, 0xbd, 0x10, 0x86, 0x2a, 0xe1, 0xde, 0xeb, 0x7a, 0x16, 0xca, 0x4d, + 0xd1, 0xaa, 0x98, 0x3d, 0x4c, 0x89, 0x19, 0xa6, 0x58, 0x7f, 0x61, 0xc3, 0x6f, 0x12, 0x2f, 0x06, 0xc3, 0xf5, 0x92, + 0x97, 0xb3, 0x8d, 0x59, 0x08, 0x87, 0xc3, 0x66, 0x52, 0xcc, 0x96, 0x10, 0xe6, 0xba, 0x9c, 0x1f, 0x0e, 0x5d, 0x2d, + 0x5b, 0x0b, 0x0f, 0x1e, 0xaa, 0x16, 0x6e, 0x1a, 0x96, 0xc3, 0xcf, 0x64, 0x16, 0x63, 0xfb, 0x1a, 0x9f, 0xd9, 0x9f, + 0x2f, 0xba, 0x67, 0x09, 0x92, 0x6f, 0xac, 0x81, 0x76, 0x6c, 0xd6, 0xee, 0x70, 0x35, 0x02, 0x92, 0xd2, 0xdd, 0xe8, + 0xef, 0xca, 0x4e, 0x9e, 0x12, 0xe4, 0x8e, 0x56, 0x60, 0xbf, 0xfb, 0xc6, 0x9f, 0x68, 0xb1, 0x07, 0xed, 0x36, 0xb6, + 0x84, 0xa8, 0xa6, 0x3d, 0x97, 0x2b, 0xc5, 0xd2, 0xbc, 0x95, 0x36, 0x7a, 0x3e, 0xac, 0xcf, 0x7d, 0x23, 0x07, 0x0a, + 0xc6, 0x88, 0xa7, 0xd6, 0x41, 0x34, 0x9b, 0x03, 0x0d, 0x06, 0x9a, 0x47, 0x78, 0x6a, 0xa1, 0x83, 0x32, 0x6b, 0xc3, + 0x7e, 0x92, 0x9c, 0x2c, 0x8f, 0xc3, 0xb7, 0xf0, 0x2f, 0x9f, 0x61, 0x93, 0x98, 0x62, 0x7b, 0xfc, 0xad, 0x52, 0x54, + 0x78, 0x6c, 0x41, 0x5c, 0x6b, 0x37, 0xa2, 0x36, 0x54, 0x0e, 0xff, 0x12, 0xf6, 0x11, 0xf6, 0x1b, 0x9a, 0x20, 0x0c, + 0x76, 0xfd, 0x99, 0x40, 0x88, 0x58, 0x88, 0x17, 0xfc, 0xad, 0x92, 0x54, 0x74, 0xc2, 0x67, 0x8b, 0x12, 0x78, 0xeb, + 0x30, 0xa0, 0x4f, 0x28, 0x52, 0x0a, 0x61, 0x68, 0x26, 0xf4, 0x8e, 0xfe, 0x1b, 0xb1, 0x93, 0x4d, 0x72, 0x2b, 0xe4, + 0x03, 0x49, 0x25, 0xc1, 0x04, 0x2b, 0x2f, 0x94, 0x2f, 0xdc, 0x0b, 0xa5, 0xd6, 0x5a, 0xd0, 0xfa, 0xe5, 0x4f, 0x12, + 0xcf, 0xe0, 0xef, 0x81, 0x8c, 0x41, 0xb7, 0x11, 0xd5, 0x24, 0xc7, 0xf4, 0x51, 0x3a, 0xcf, 0x40, 0x05, 0x74, 0xb6, + 0xce, 0xc2, 0x7a, 0x59, 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, 0xbf, 0xeb, 0xcd, 0xed, 0xc9, 0xbe, 0x6e, 0x7e, 0x67, 0xbd, 0x1e, 0x6c, 0x0d, + 0x32, 0xf1, 0x85, 0x62, 0xa1, 0xb2, 0x0a, 0xb1, 0x82, 0xb4, 0xff, 0x25, 0xbc, 0xdf, 0xe1, 0xad, 0x11, 0x9a, 0x95, + 0xf1, 0x30, 0x1f, 0x3d, 0xd9, 0x8b, 0xe6, 0xf7, 0xce, 0xb2, 0xad, 0x5c, 0x95, 0xcc, 0xf6, 0xfb, 0x51, 0xd2, 0x9c, + 0x3d, 0x5e, 0x23, 0xa9, 0x03, 0x7c, 0xbc, 0x3e, 0xc3, 0x47, 0x2a, 0xa1, 0xd4, 0x82, 0xaa, 0x06, 0xad, 0x8f, 0xfd, + 0xde, 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, 0x64, 0xaf, 0x84, 0xfa, 0x28, 0x57, 0x7b, 0xa6, 0xe1, 0x00, + 0x4d, 0xc0, 0xa6, 0x0d, 0xfe, 0x16, 0xb8, 0x97, 0xc1, 0x99, 0x69, 0x9f, 0x86, 0x11, 0x70, 0x9a, 0x43, 0xcc, 0x9f, + 0xdf, 0xf5, 0xa0, 0x82, 0x07, 0x1d, 0xe9, 0xaf, 0xeb, 0x59, 0x81, 0x67, 0xee, 0x89, 0xe7, 0x6f, 0x4e, 0xa4, 0x17, + 0x39, 0x3c, 0xd0, 0x34, 0x88, 0x19, 0x7f, 0x51, 0x96, 0xe1, 0x6e, 0xb4, 0x2c, 0x8b, 0x95, 0x17, 0xe9, 0x7d, 0x3c, + 0x93, 0x62, 0x20, 0x31, 0x63, 0x66, 0x74, 0x15, 0xeb, 0x38, 0x87, 0x71, 0x6f, 0x4f, 0xc2, 0x0a, 0xed, 0x9f, 0x25, + 0xf6, 0xba, 0x00, 0x2c, 0x87, 0xac, 0x41, 0x2b, 0xbc, 0xd3, 0xed, 0xed, 0x1e, 0x97, 0xec, 0x28, 0x6e, 0x00, 0xfd, + 0xac, 0x86, 0x96, 0x09, 0x6a, 0x99, 0x75, 0x27, 0x93, 0x29, 0x92, 0xcb, 0xb7, 0x61, 0x6f, 0x58, 0x91, 0xcf, 0x1b, + 0xb9, 0x3d, 0xbc, 0x0f, 0x57, 0x22, 0xd6, 0x16, 0x74, 0xd2, 0x91, 0x71, 0xb8, 0x17, 0x9a, 0x1b, 0xe9, 0xfe, 0x49, + 0x95, 0x84, 0xa5, 0x88, 0xe1, 0x16, 0xc8, 0xf6, 0x6a, 0x5b, 0x09, 0x4a, 0xe0, 0x83, 0xfd, 0x58, 0x8a, 0x65, 0xba, + 0x15, 0x80, 0xeb, 0xc0, 0xff, 0x94, 0x88, 0x84, 0xee, 0xce, 0x43, 0x14, 0x6b, 0xe4, 0x7d, 0x83, 0x68, 0xec, 0xaf, + 0x41, 0x4e, 0x03, 0x32, 0x91, 0x62, 0x24, 0x0b, 0x06, 0x3e, 0x80, 0x9c, 0xaf, 0xc1, 0x24, 0x37, 0xcd, 0x3d, 0x3f, + 0xc8, 0x75, 0x07, 0xd3, 0x3e, 0xe8, 0x5e, 0x5c, 0x6b, 0x96, 0x83, 0x57, 0x4c, 0xc4, 0xff, 0x56, 0x7b, 0x25, 0xcb, + 0x59, 0xe6, 0x37, 0xe6, 0xa2, 0x93, 0xc1, 0x55, 0x43, 0xf8, 0xc5, 0x2c, 0x9b, 0xf3, 0x68, 0x96, 0xe9, 0xa8, 0xff, + 0xa2, 0x39, 0x2a, 0x05, 0xe0, 0xd4, 0xf1, 0x02, 0xac, 0xa1, 0xaf, 0x74, 0xd3, 0x8a, 0x47, 0x1a, 0x63, 0x14, 0x54, + 0xe8, 0x20, 0xf4, 0xb7, 0x1a, 0x90, 0x36, 0x98, 0xa4, 0x49, 0xa8, 0x7c, 0x70, 0x41, 0x37, 0xcc, 0xcb, 0x95, 0xcb, + 0x55, 0x93, 0xaa, 0xe5, 0x97, 0x23, 0xea, 0xbb, 0x5a, 0x72, 0xa9, 0x36, 0x9f, 0x1a, 0x65, 0x8d, 0x20, 0x93, 0xa3, + 0xf4, 0xfb, 0x94, 0x0b, 0xb7, 0x32, 0x26, 0xeb, 0xc3, 0xc1, 0x2b, 0xb8, 0xa9, 0xf1, 0xeb, 0x9c, 0x08, 0x45, 0xed, + 0x21, 0x11, 0xb6, 0x76, 0x2b, 0x74, 0xef, 0x71, 0xa3, 0x34, 0x8f, 0xb2, 0x4d, 0x2c, 0x2a, 0xaf, 0x97, 0x80, 0xb5, + 0xb8, 0x07, 0xbc, 0xa8, 0xb4, 0xf4, 0x2b, 0x56, 0x00, 0x7a, 0x80, 0x14, 0x36, 0x7e, 0x44, 0x06, 0xac, 0x8f, 0x5e, + 0xea, 0xf7, 0xfb, 0xc6, 0x94, 0xff, 0xe1, 0x21, 0x07, 0x92, 0x42, 0x51, 0xd6, 0x3b, 0x98, 0x40, 0x70, 0xed, 0x24, + 0xed, 0x59, 0xcd, 0xaf, 0xd7, 0xb5, 0x07, 0xfc, 0x56, 0xbe, 0x45, 0x62, 0xf5, 0xda, 0xbe, 0xd8, 0xec, 0xd3, 0xea, + 0xc6, 0x68, 0x1c, 0x04, 0x4b, 0xab, 0xb7, 0x5a, 0xe5, 0x90, 0x37, 0xbc, 0x02, 0x91, 0xca, 0xba, 0xba, 0x56, 0xce, + 0xd5, 0xb5, 0xe0, 0xc8, 0x25, 0x5b, 0xf2, 0x1c, 0xfe, 0x0b, 0xb9, 0x57, 0x1e, 0x0e, 0x85, 0xdf, 0xef, 0xa7, 0x33, + 0xd2, 0xca, 0x02, 0x7b, 0xda, 0xba, 0xf6, 0x42, 0xff, 0x70, 0xf8, 0x11, 0xbc, 0x46, 0xfc, 0xc3, 0xa1, 0xec, 0xf7, + 0x3f, 0x99, 0x9b, 0xcc, 0xf9, 0x58, 0x29, 0x65, 0x2f, 0x51, 0xe9, 0xfe, 0x36, 0xe1, 0xbd, 0xff, 0x3d, 0xfa, 0xdf, + 0xa3, 0xcb, 0x9e, 0x0a, 0x01, 0x4b, 0xf8, 0x0c, 0x6f, 0xe8, 0x4c, 0x5d, 0xce, 0x99, 0x74, 0x77, 0x57, 0x7e, 0xe8, + 0x3d, 0x0d, 0x15, 0xdf, 0x9b, 0x9b, 0x36, 0xfe, 0x5a, 0x1d, 0x69, 0x12, 0x3a, 0x2e, 0xfa, 0x87, 0xc3, 0xe7, 0x44, + 0xeb, 0xd3, 0x52, 0xa5, 0x4f, 0x53, 0x38, 0x4a, 0x86, 0x18, 0xd7, 0x2d, 0x4c, 0x07, 0xf6, 0xe3, 0xe6, 0xab, 0xe4, + 0xc5, 0x59, 0x0a, 0xd7, 0xde, 0x7c, 0x96, 0xce, 0xa7, 0x60, 0x5d, 0x19, 0xe6, 0xb3, 0x7a, 0x1e, 0x40, 0xea, 0x10, + 0xd2, 0xac, 0x69, 0xf8, 0x97, 0xca, 0x15, 0xbc, 0xb5, 0xc7, 0xbb, 0x81, 0x8b, 0x52, 0x47, 0xfa, 0xa4, 0x8d, 0xa6, + 0x4b, 0x2a, 0xf9, 0x4f, 0x22, 0x8f, 0x31, 0x66, 0xe3, 0x35, 0xf1, 0x7e, 0x16, 0xf9, 0xab, 0x02, 0xb0, 0x8b, 0x00, + 0x0c, 0x39, 0x9d, 0x3b, 0x92, 0xf8, 0xcf, 0xc9, 0xf7, 0x7f, 0x4c, 0x97, 0xf6, 0xb1, 0x2c, 0xee, 0x4a, 0x51, 0x55, + 0x47, 0xa5, 0xed, 0x6c, 0xb9, 0x1e, 0x98, 0x44, 0xfb, 0x7d, 0xc9, 0x24, 0x9a, 0x62, 0x28, 0x0a, 0xdc, 0x1a, 0x7b, + 0xd3, 0x94, 0x2b, 0xc6, 0xea, 0x91, 0xb1, 0x7e, 0xbe, 0xdc, 0xbd, 0x8d, 0xbd, 0xd4, 0x0f, 0x52, 0x10, 0x84, 0x35, + 0x94, 0x52, 0x8a, 0x7c, 0x70, 0x3e, 0xc3, 0x54, 0xa2, 0xd6, 0xa5, 0x54, 0xf9, 0xc3, 0x48, 0xf3, 0x61, 0x0a, 0x7a, + 0xd9, 0xbf, 0x57, 0x30, 0xff, 0x75, 0x7b, 0xb0, 0x3e, 0xad, 0xcb, 0x34, 0xaa, 0x88, 0x2a, 0x2f, 0x4c, 0xb5, 0x09, + 0x44, 0xf0, 0x6b, 0x61, 0xf1, 0xfd, 0xfa, 0xe4, 0x48, 0xd0, 0x98, 0xc9, 0xf2, 0xe9, 0xc8, 0xfd, 0xc2, 0xbe, 0x72, + 0x1d, 0xcf, 0xff, 0xdc, 0xcc, 0xff, 0x01, 0x3a, 0x43, 0x16, 0xd7, 0xdc, 0x32, 0x58, 0xe0, 0xec, 0x97, 0xae, 0x1e, + 0xf0, 0x37, 0xf3, 0xc4, 0x35, 0xd0, 0x31, 0x5f, 0xa3, 0xab, 0x62, 0x3a, 0x2b, 0x06, 0xc0, 0x65, 0xeb, 0x37, 0xd6, + 0x9c, 0x78, 0x63, 0x51, 0x5e, 0xc9, 0x05, 0xa1, 0xaf, 0xab, 0x30, 0x1b, 0x57, 0xc5, 0xa6, 0x12, 0xc5, 0xa6, 0xee, + 0x91, 0x5a, 0x36, 0x9f, 0xd6, 0xb6, 0x42, 0xf6, 0xaf, 0xa2, 0xc5, 0xe0, 0x65, 0x58, 0x27, 0xa3, 0x2c, 0x5d, 0x4f, + 0x81, 0x5f, 0x2f, 0x80, 0xb3, 0xc8, 0xbc, 0xf2, 0xd5, 0xd9, 0x03, 0xb6, 0x68, 0x3c, 0x05, 0x72, 0x54, 0xfa, 0x23, + 0x6f, 0x8c, 0x4e, 0x4f, 0xf4, 0xfb, 0xf9, 0x94, 0x62, 0xbe, 0xfe, 0x0a, 0xf0, 0x5c, 0xb5, 0x5c, 0x80, 0xbe, 0x0c, + 0x75, 0x50, 0x89, 0x52, 0x2b, 0x86, 0x11, 0x0b, 0x7f, 0x15, 0x48, 0xe4, 0x4c, 0x81, 0xcd, 0x2a, 0x4a, 0x42, 0x25, + 0x2a, 0x25, 0x5b, 0x13, 0xd4, 0xd2, 0xfb, 0xa2, 0xac, 0xf7, 0x15, 0x38, 0x4a, 0x46, 0xda, 0x2c, 0x27, 0xcd, 0xb8, + 0x02, 0x65, 0x2e, 0xfa, 0xc1, 0xfe, 0x55, 0x79, 0x7e, 0x23, 0xf3, 0x59, 0xee, 0x3b, 0x3a, 0xa7, 0xed, 0xb8, 0x40, + 0x99, 0x5b, 0x4e, 0x5b, 0x2d, 0x79, 0x4c, 0xde, 0xb3, 0x60, 0xdb, 0x7f, 0x91, 0x20, 0xc5, 0x22, 0xcc, 0x27, 0x54, + 0xd9, 0xfc, 0x1d, 0x42, 0x6d, 0x71, 0x60, 0x8f, 0x5d, 0x98, 0x88, 0xff, 0x16, 0x2c, 0x89, 0x61, 0x56, 0x8a, 0x30, + 0xde, 0x81, 0xf7, 0xcf, 0xa6, 0x12, 0xa3, 0x33, 0x74, 0x72, 0x3f, 0x7b, 0x48, 0xeb, 0xe4, 0xec, 0xed, 0xeb, 0xb3, + 0x1f, 0x7a, 0x83, 0x62, 0x94, 0xc6, 0x83, 0xde, 0x0f, 0x67, 0xab, 0x0d, 0xa0, 0x65, 0x8a, 0xb3, 0x98, 0x4c, 0x69, + 0x22, 0x3e, 0x23, 0xc3, 0xe0, 0x59, 0x9d, 0x88, 0x33, 0x9a, 0x98, 0xee, 0x6b, 0x94, 0x26, 0xdf, 0x8e, 0xc2, 0x1c, + 0x5e, 0x2e, 0xc5, 0xa6, 0x12, 0x31, 0xd8, 0x29, 0xd5, 0x3c, 0xcb, 0xdb, 0x67, 0x71, 0x3e, 0xea, 0x90, 0x55, 0x3a, + 0xf0, 0xb7, 0x27, 0xd2, 0xae, 0x4a, 0x57, 0x40, 0xe8, 0x01, 0x70, 0xd2, 0x95, 0x3f, 0x0f, 0x07, 0x91, 0x40, 0xa8, + 0x05, 0x73, 0x32, 0x8d, 0xe8, 0x86, 0xf4, 0x0a, 0xfb, 0x0c, 0xcc, 0x42, 0x4a, 0xf3, 0xe0, 0xe6, 0x6a, 0x31, 0x74, + 0x57, 0xac, 0x1c, 0x85, 0xd5, 0x5a, 0x44, 0x35, 0xb2, 0x1e, 0x83, 0xf3, 0x0e, 0x44, 0x00, 0x28, 0x72, 0xf0, 0x8c, + 0x47, 0xfd, 0x7e, 0xa4, 0x82, 0x72, 0x12, 0xfa, 0x45, 0xa1, 0x5f, 0x1a, 0x8e, 0x32, 0xe6, 0xef, 0x43, 0xcd, 0x11, + 0x50, 0x6f, 0x79, 0xa8, 0xe8, 0x02, 0x70, 0x39, 0x47, 0xcc, 0x38, 0xef, 0x71, 0x17, 0x98, 0x53, 0x51, 0x50, 0xa8, + 0xeb, 0x60, 0xa9, 0x00, 0xe8, 0x4d, 0x7d, 0xa4, 0xe7, 0xa4, 0x49, 0xd0, 0x78, 0x6e, 0xe0, 0xf1, 0x6a, 0xb8, 0xa8, + 0x56, 0x42, 0xea, 0x2d, 0x74, 0x4a, 0x55, 0x87, 0x40, 0x20, 0xf0, 0x7f, 0x78, 0xfb, 0x16, 0xee, 0xb6, 0x6d, 0x6c, + 0xdd, 0xbf, 0x62, 0xf1, 0xa6, 0x2a, 0x11, 0x41, 0xb2, 0xe4, 0x24, 0x9d, 0x29, 0x65, 0x58, 0xc7, 0xcd, 0xa3, 0x4d, + 0xa7, 0x89, 0xd3, 0x38, 0xed, 0x74, 0xaa, 0xab, 0xe3, 0xd2, 0x24, 0x6c, 0xb1, 0xa1, 0x01, 0x95, 0xa4, 0xfc, 0x88, + 0xc4, 0xff, 0x7e, 0xd7, 0xde, 0x78, 0x92, 0xa2, 0x9d, 0xcc, 0xdc, 0x73, 0xef, 0xca, 0x5a, 0xb1, 0x08, 0x82, 0x78, + 0x63, 0x63, 0x63, 0x3f, 0xbe, 0xdd, 0x8c, 0x19, 0x76, 0xcb, 0x5d, 0x8e, 0x64, 0x5d, 0x14, 0x5c, 0xec, 0x04, 0x86, + 0x6e, 0xc6, 0x25, 0x33, 0x07, 0x57, 0x33, 0xac, 0x93, 0x8a, 0x02, 0xec, 0xea, 0x02, 0x64, 0x2f, 0x0c, 0x75, 0xdd, + 0xcc, 0x96, 0xeb, 0xc0, 0xd7, 0xa5, 0x0b, 0x5f, 0x52, 0xf0, 0x72, 0x25, 0x45, 0x99, 0x5d, 0xf3, 0x9f, 0xec, 0xcb, + 0x66, 0x2c, 0x29, 0xb4, 0x23, 0x7d, 0xd3, 0xee, 0x8e, 0x16, 0xe3, 0xd8, 0x72, 0x7c, 0x4b, 0xa5, 0x3b, 0x3d, 0xaa, + 0x5e, 0x08, 0x6d, 0x9d, 0x6b, 0x99, 0xa5, 0x29, 0x17, 0xaf, 0x45, 0x9a, 0x25, 0x5e, 0x72, 0xac, 0x63, 0x55, 0xbb, + 0x20, 0x58, 0x2e, 0x4c, 0xf2, 0x8b, 0xac, 0xc4, 0xd8, 0xc1, 0x8d, 0x46, 0xb5, 0xa2, 0x4e, 0x99, 0x18, 0x18, 0xf2, + 0x3d, 0x06, 0xdf, 0x66, 0x65, 0x02, 0x0c, 0x3f, 0x26, 0xea, 0x4b, 0x7a, 0x0a, 0x01, 0x1f, 0x54, 0x68, 0xee, 0x17, + 0x1c, 0xc1, 0xaf, 0xad, 0xca, 0x1c, 0x98, 0x6c, 0xad, 0x82, 0x44, 0xdc, 0xbb, 0x6c, 0xae, 0x17, 0xd1, 0x42, 0xdd, + 0x85, 0x7a, 0xf1, 0x76, 0xdb, 0x4b, 0x14, 0x1d, 0x70, 0xf2, 0xd3, 0xe0, 0x55, 0x9c, 0xe5, 0x3c, 0xdd, 0xab, 0xe4, + 0x9e, 0xda, 0x50, 0x7b, 0xca, 0x99, 0x03, 0x76, 0xde, 0xd7, 0xd5, 0x9e, 0x5e, 0xd3, 0x7b, 0xba, 0x9d, 0x7b, 0x70, + 0xc1, 0xc0, 0x9d, 0x7b, 0x99, 0x5d, 0x73, 0xb1, 0x07, 0xca, 0x40, 0x6b, 0x3c, 0x50, 0x97, 0xd5, 0x48, 0x4d, 0x8c, + 0x8e, 0x61, 0x9d, 0xe8, 0x83, 0x39, 0xa0, 0xdf, 0x43, 0x58, 0xfb, 0xd6, 0xdb, 0x95, 0x3e, 0x68, 0x03, 0xfa, 0xd3, + 0xd2, 0xf4, 0x41, 0x07, 0x8e, 0x57, 0xd1, 0x81, 0x1b, 0x43, 0xaa, 0x41, 0x5b, 0x8d, 0xac, 0x02, 0xc5, 0x1b, 0xde, + 0xe2, 0xdd, 0xb9, 0x96, 0x6c, 0xbc, 0x97, 0x88, 0xf1, 0x95, 0x89, 0x2a, 0xce, 0xc4, 0xb1, 0x97, 0xca, 0x6b, 0xed, + 0x24, 0x23, 0x8c, 0x6f, 0x59, 0x49, 0xfd, 0x1d, 0x62, 0x6e, 0x91, 0xe6, 0x30, 0x78, 0x19, 0x56, 0x64, 0xc6, 0xfb, + 0x7d, 0x39, 0x93, 0x51, 0x39, 0x13, 0xfb, 0x65, 0xa4, 0xc0, 0xda, 0xee, 0x13, 0x01, 0x3d, 0x28, 0x01, 0xf2, 0x05, + 0x40, 0xd5, 0x43, 0xc2, 0x9f, 0x87, 0xa4, 0x3e, 0x9d, 0x42, 0x9f, 0x42, 0x5b, 0xaf, 0xb8, 0x82, 0x78, 0x55, 0x37, + 0x46, 0xb6, 0x51, 0x41, 0x8b, 0xc7, 0xf2, 0xac, 0x36, 0x8c, 0xcd, 0xa9, 0xf5, 0xaf, 0x37, 0x1b, 0x4c, 0xd9, 0x5c, + 0xa8, 0x55, 0x18, 0x92, 0xe8, 0xa6, 0xf4, 0x22, 0x89, 0x58, 0xd8, 0xac, 0xd6, 0xe6, 0x37, 0x61, 0x40, 0x32, 0x91, + 0xe2, 0x7e, 0xb6, 0xc4, 0xb9, 0x8b, 0xc7, 0xf3, 0xaa, 0xaf, 0xb5, 0xb4, 0xc8, 0xb4, 0xf9, 0x4e, 0x5f, 0x86, 0x34, + 0x15, 0x35, 0xa4, 0x51, 0x67, 0x06, 0xdd, 0xb7, 0xcb, 0x5b, 0x56, 0x23, 0x4c, 0x80, 0x57, 0x3a, 0x83, 0x6e, 0x34, + 0x1e, 0x88, 0x65, 0x35, 0x2a, 0xd6, 0x42, 0x20, 0xf0, 0x30, 0xe4, 0x98, 0x59, 0x42, 0x92, 0x7d, 0xe2, 0xdf, 0xa9, + 0x38, 0x0b, 0x45, 0x7c, 0x63, 0x90, 0xbd, 0x2b, 0xeb, 0xda, 0x5d, 0x47, 0x7e, 0x4e, 0x2c, 0xac, 0xf6, 0x1f, 0x9a, + 0x47, 0xad, 0x71, 0x16, 0xd0, 0xd6, 0xb4, 0xba, 0xe1, 0x70, 0x8f, 0xea, 0x58, 0x94, 0x06, 0x9b, 0xd8, 0x23, 0xcb, + 0x45, 0xeb, 0x98, 0x41, 0x03, 0xfa, 0xdb, 0xec, 0x6a, 0x7d, 0x85, 0x00, 0x6e, 0x25, 0xb2, 0x4e, 0x52, 0xf9, 0x97, + 0xb4, 0x47, 0x5d, 0xdb, 0x53, 0xf9, 0xdf, 0xb6, 0xa9, 0x72, 0x68, 0x31, 0xe5, 0xb1, 0x9b, 0xb3, 0x40, 0x75, 0x24, + 0x88, 0x02, 0xb5, 0xf5, 0x82, 0xa9, 0x77, 0xca, 0x14, 0x1d, 0x20, 0xd0, 0x85, 0x39, 0xc3, 0xbe, 0xe0, 0x88, 0x31, + 0x4b, 0x25, 0x06, 0x53, 0x1f, 0x63, 0x54, 0xd3, 0x5a, 0x01, 0xba, 0x7e, 0xba, 0x81, 0x3f, 0x51, 0x51, 0xa3, 0xa1, + 0xd6, 0x48, 0x0a, 0x45, 0x13, 0x15, 0x8a, 0x2c, 0x2d, 0x74, 0x5c, 0x85, 0x4e, 0x22, 0x61, 0x09, 0x68, 0x98, 0x10, + 0x9d, 0x54, 0xe0, 0xad, 0x01, 0x9c, 0xf9, 0xb8, 0x28, 0xd7, 0x85, 0x36, 0x98, 0xfb, 0x21, 0xbe, 0xe6, 0xaf, 0x5f, + 0x38, 0xa3, 0xfa, 0x96, 0xb5, 0xbe, 0xa7, 0x05, 0xf9, 0x21, 0xe4, 0x14, 0x1d, 0x98, 0xd8, 0xd1, 0x06, 0x8d, 0x31, + 0xca, 0x5a, 0x47, 0xbd, 0x38, 0xd1, 0xa1, 0x58, 0xb4, 0x09, 0xde, 0x03, 0x9e, 0x22, 0xda, 0xf0, 0x50, 0x18, 0xab, + 0x6a, 0x7c, 0x2a, 0x59, 0x4b, 0x0f, 0x56, 0xf0, 0x74, 0x9d, 0xf0, 0x10, 0xf4, 0x48, 0x84, 0x1d, 0x85, 0xc5, 0x3c, + 0x5e, 0xc0, 0x71, 0x52, 0x10, 0x50, 0x3b, 0xe8, 0x2b, 0xf8, 0x7c, 0x81, 0xee, 0xaf, 0x12, 0x3d, 0xc0, 0xd0, 0x82, + 0xb8, 0x19, 0x05, 0x75, 0x74, 0x15, 0xaf, 0x1a, 0x2a, 0x12, 0x3e, 0x2f, 0xc0, 0x76, 0x48, 0xa9, 0xa7, 0x40, 0x0b, + 0x95, 0x28, 0xfd, 0x30, 0xf0, 0x1d, 0x1a, 0x03, 0x5b, 0xeb, 0x00, 0x0d, 0xfd, 0x8c, 0x69, 0x6a, 0x9d, 0xa1, 0xf2, + 0x99, 0x77, 0xcf, 0x8c, 0x96, 0x33, 0x8b, 0xc6, 0xa0, 0x6f, 0xa3, 0x29, 0x8a, 0x73, 0xf2, 0x59, 0x50, 0xc4, 0x69, + 0x16, 0xe7, 0xe0, 0xb7, 0x19, 0x17, 0x98, 0x31, 0x89, 0x2b, 0x7e, 0x29, 0x0b, 0xd0, 0x76, 0xe7, 0x2a, 0xb5, 0xae, + 0x41, 0x40, 0xf6, 0x03, 0x58, 0xbd, 0x34, 0x74, 0x54, 0xce, 0xbb, 0x4b, 0x9b, 0x42, 0xc4, 0x22, 0x04, 0x9b, 0x66, + 0xba, 0x64, 0xc7, 0xa1, 0xd2, 0xe6, 0x40, 0xa8, 0x23, 0x34, 0xee, 0x9f, 0x86, 0xb1, 0xd5, 0x14, 0x5b, 0xbb, 0xb7, + 0xed, 0xf6, 0xb7, 0xd2, 0x4b, 0xa7, 0x39, 0xe9, 0x31, 0xf6, 0x5b, 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, 0x83, 0x7b, 0x61, 0xa7, 0xd4, 0xf6, 0xa0, 0x76, 0x44, 0x09, 0xfd, 0x07, 0x87, 0x8b, 0xc4, + 0x77, 0x52, 0x87, 0x00, 0x44, 0x8b, 0x90, 0x33, 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, 0x1f, 0x65, 0x18, 0xa0, 0x4f, 0x52, + 0x00, 0x26, 0x83, 0x29, 0xbf, 0x05, 0x89, 0xd2, 0x99, 0xba, 0x21, 0xfd, 0x22, 0x0a, 0x7e, 0xc1, 0x0b, 0x2e, 0x12, + 0x57, 0x80, 0xe5, 0x1d, 0x6c, 0xaf, 0xa3, 0x8a, 0x2a, 0x4c, 0x5e, 0xd3, 0xe3, 0x88, 0x1b, 0xef, 0x3f, 0xd3, 0x63, + 0x8b, 0xd9, 0x6a, 0x1d, 0x1b, 0x7c, 0xe6, 0x18, 0x5c, 0xd0, 0xb5, 0xc4, 0xd6, 0x50, 0x0d, 0x2b, 0x02, 0x03, 0x17, + 0x70, 0x10, 0x96, 0x28, 0x8e, 0xad, 0xe4, 0x15, 0x69, 0x48, 0x69, 0x1f, 0x18, 0x8e, 0x36, 0xc9, 0xf1, 0x6d, 0x96, + 0xdd, 0x04, 0xce, 0x17, 0x9d, 0x93, 0x66, 0xc2, 0xda, 0xe0, 0x7d, 0xde, 0x9c, 0x5f, 0xf7, 0x0f, 0x09, 0x55, 0x71, + 0x6f, 0x78, 0x3b, 0xee, 0x8d, 0x13, 0x7e, 0xcd, 0xc5, 0x42, 0x87, 0x6a, 0x31, 0x97, 0x2c, 0xbf, 0xb5, 0xde, 0x2d, + 0x49, 0x6a, 0x05, 0xb4, 0xcf, 0xb2, 0xa0, 0x26, 0x02, 0x40, 0xfe, 0xf0, 0x17, 0x08, 0x9d, 0xe1, 0x6f, 0x8f, 0xc1, + 0x15, 0x29, 0xbc, 0x77, 0x08, 0x84, 0x35, 0xdd, 0xdc, 0xa9, 0x0d, 0xf8, 0x62, 0xdc, 0x9f, 0x31, 0xf5, 0xf4, 0xdb, + 0x4c, 0xee, 0xea, 0xba, 0x3d, 0xb2, 0x0c, 0x1f, 0xe1, 0x4a, 0x01, 0xdc, 0x4c, 0xf8, 0x8b, 0x61, 0x26, 0xd5, 0x27, + 0x80, 0xa9, 0xa6, 0x83, 0xfb, 0x04, 0x81, 0x01, 0x54, 0xa2, 0xc5, 0xe8, 0x5a, 0x39, 0xa2, 0x19, 0xb8, 0x35, 0xdd, + 0x0a, 0xe3, 0xad, 0x07, 0x2d, 0xf4, 0x4c, 0xc3, 0x89, 0xff, 0xa0, 0x99, 0x57, 0x05, 0x04, 0xd0, 0xca, 0x08, 0xde, + 0x5a, 0x1f, 0xcd, 0x11, 0xe2, 0x13, 0x96, 0x44, 0x13, 0x16, 0xcf, 0x14, 0x3f, 0x26, 0x74, 0xd3, 0xd4, 0x36, 0x7d, + 0x40, 0xfa, 0x8b, 0x6b, 0xd6, 0x4f, 0x59, 0xd6, 0xbe, 0x3d, 0x54, 0xbc, 0x98, 0x36, 0xe3, 0x20, 0x26, 0xaa, 0x18, + 0xff, 0x0b, 0xee, 0x4b, 0xad, 0x00, 0x91, 0xb9, 0xab, 0x9e, 0x7e, 0xbf, 0x99, 0x2d, 0x07, 0x42, 0xe5, 0x77, 0x06, + 0x49, 0x9f, 0x0e, 0xed, 0x07, 0x36, 0x89, 0xda, 0x42, 0xcf, 0x1f, 0x97, 0xba, 0x89, 0x97, 0xd7, 0xa6, 0x46, 0xb4, + 0x42, 0x86, 0xca, 0xd6, 0x01, 0xeb, 0xfb, 0x87, 0x70, 0x77, 0x51, 0xd3, 0x50, 0xeb, 0x9e, 0xbb, 0x16, 0x05, 0x27, + 0xfe, 0x00, 0x63, 0x71, 0x21, 0xa9, 0x75, 0x3c, 0x26, 0xfd, 0x68, 0x21, 0x93, 0x1b, 0x75, 0x75, 0x72, 0xa6, 0x98, + 0x27, 0x70, 0x01, 0x2e, 0xdb, 0xfe, 0x8a, 0x4a, 0x5d, 0xca, 0xed, 0x15, 0xa5, 0xe9, 0x21, 0x6d, 0xaf, 0xe2, 0xbc, + 0x2d, 0xb8, 0xe0, 0x5f, 0x28, 0xb8, 0xb0, 0x0e, 0xd6, 0x1d, 0x77, 0xca, 0x9e, 0xf0, 0x44, 0x99, 0xd6, 0x06, 0x77, + 0xdd, 0x60, 0x4c, 0x8c, 0xfd, 0xee, 0x92, 0x27, 0x1f, 0x91, 0x05, 0xff, 0x2e, 0x13, 0xe0, 0x99, 0xec, 0x5e, 0xa9, + 0xfc, 0x3f, 0xf8, 0x57, 0x5b, 0xfb, 0xce, 0x9a, 0x7f, 0x7a, 0xd6, 0xc3, 0x9d, 0xc3, 0xe4, 0xc7, 0xea, 0x0c, 0xe8, + 0xe6, 0x4a, 0xa6, 0x1c, 0x90, 0x01, 0xac, 0x45, 0x32, 0x1a, 0xf0, 0xa1, 0x95, 0x65, 0xdb, 0x77, 0x5a, 0x5d, 0x10, + 0xee, 0x25, 0x70, 0xd3, 0xfb, 0x6b, 0x33, 0x33, 0xa7, 0x6b, 0x25, 0x9a, 0x2e, 0x8d, 0xad, 0x65, 0xa9, 0xc2, 0x78, + 0xbf, 0xf7, 0x24, 0x9b, 0xe6, 0x87, 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, 0x7f, 0x72, 0xcd, 0x8b, 0x8b, 0x5c, 0xde, 0x00, 0x1c, 0x32, 0xa9, 0x8d, + 0xc2, 0xf2, 0x0a, 0xdc, 0xf9, 0xd1, 0x71, 0x9c, 0x89, 0x51, 0x8e, 0x71, 0x5b, 0x11, 0x29, 0x59, 0x27, 0xce, 0x00, + 0x0f, 0xd9, 0x9f, 0x34, 0x1d, 0xda, 0xb5, 0xc0, 0xf0, 0xbe, 0xc0, 0x5d, 0xe5, 0xec, 0x68, 0x93, 0xdb, 0x45, 0xdf, + 0x9c, 0x61, 0xdd, 0x91, 0xd2, 0xda, 0x58, 0x74, 0xdd, 0xc1, 0x5a, 0x33, 0x68, 0x8b, 0x50, 0xf2, 0x21, 0x77, 0xd2, + 0x7e, 0x0a, 0x68, 0x70, 0x96, 0xa5, 0xb7, 0xd6, 0x2a, 0x7f, 0xa3, 0x85, 0x38, 0x51, 0x4c, 0x9d, 0xf8, 0x26, 0x4a, + 0xf4, 0xf9, 0x99, 0x18, 0x37, 0x10, 0x48, 0xfd, 0x01, 0xe3, 0x6b, 0x14, 0x61, 0x02, 0xd7, 0x81, 0x28, 0xb6, 0x27, + 0x6a, 0x63, 0x39, 0x82, 0x4e, 0x08, 0xf1, 0x0e, 0xca, 0x30, 0x56, 0x17, 0x07, 0xda, 0x60, 0xe9, 0xeb, 0xd6, 0x3a, + 0x37, 0x84, 0xc2, 0x38, 0x81, 0x29, 0x06, 0x49, 0x9d, 0x75, 0x96, 0x09, 0xaa, 0xec, 0x98, 0x74, 0xde, 0x07, 0xe8, + 0xee, 0x5a, 0x34, 0xc5, 0xd7, 0x9d, 0x3b, 0xe8, 0x3e, 0xae, 0x5f, 0x6b, 0x91, 0x1b, 0xfc, 0x79, 0x4b, 0x84, 0x45, + 0xe0, 0xac, 0x35, 0xf9, 0xaa, 0x11, 0x0e, 0x4c, 0x49, 0xa6, 0x61, 0x2f, 0x51, 0x36, 0xdd, 0xdb, 0x6d, 0xaf, 0x77, + 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, 0xc2, 0x09, 0x9d, 0x54, 0xc8, 0x55, 0xa7, 0x2e, 0xd8, 0x5c, 0xf1, 0x6a, 0x29, 0xd3, 0x48, 0x50, 0xb4, + 0x39, 0x8f, 0x4a, 0x9a, 0xc8, 0xb5, 0xa8, 0x22, 0x59, 0xa3, 0x5e, 0xd4, 0x6a, 0x0c, 0x10, 0x90, 0xe9, 0xac, 0xe9, + 0x41, 0x15, 0xcc, 0x86, 0x32, 0x92, 0xd3, 0xf7, 0x60, 0x69, 0x8f, 0x1c, 0x6b, 0x7d, 0x5f, 0x9d, 0x2d, 0xbe, 0xd5, + 0x13, 0x82, 0x29, 0xcc, 0x1e, 0x88, 0x08, 0xd7, 0x34, 0x86, 0x9c, 0x76, 0x89, 0xcb, 0x9a, 0x6e, 0x09, 0xf7, 0x70, + 0xbb, 0x92, 0x1d, 0xb9, 0x79, 0xd2, 0xdc, 0x5c, 0xc1, 0x8e, 0x8a, 0xf9, 0x18, 0xb4, 0x5f, 0x52, 0x5d, 0xbb, 0x34, + 0xb7, 0x1e, 0x0f, 0x02, 0x1a, 0x0c, 0x0a, 0xc3, 0xbf, 0x4e, 0x8c, 0x87, 0x27, 0x0d, 0x08, 0x92, 0x72, 0x11, 0x8e, + 0x7d, 0x23, 0xfa, 0xc9, 0x54, 0x1e, 0x72, 0xb4, 0x78, 0x87, 0x56, 0x27, 0x10, 0xd0, 0x4b, 0x84, 0x92, 0x18, 0x55, + 0xa1, 0x11, 0x41, 0x79, 0x5a, 0xfe, 0x52, 0x55, 0x87, 0x80, 0x42, 0xda, 0x57, 0x14, 0xca, 0x36, 0x89, 0xa1, 0x19, + 0x7e, 0x39, 0x9f, 0x2c, 0xf4, 0x0c, 0x0c, 0xe4, 0xfc, 0x60, 0xa1, 0x67, 0x61, 0x20, 0xe7, 0x4f, 0x16, 0xb5, 0x5b, + 0x07, 0x9a, 0x80, 0x78, 0x2e, 0x1c, 0x9d, 0x94, 0x56, 0x65, 0x0b, 0xe8, 0xe6, 0x21, 0x82, 0xfe, 0x0f, 0x7b, 0x08, + 0x3a, 0xb9, 0xd0, 0x8e, 0xdc, 0x80, 0xb6, 0x43, 0x12, 0xd8, 0x2b, 0x26, 0x15, 0x26, 0x16, 0xd1, 0x21, 0x1b, 0x83, + 0x21, 0xb6, 0xfa, 0xe0, 0x90, 0x8d, 0xa7, 0x3e, 0x09, 0x02, 0x46, 0xf7, 0x07, 0x03, 0x0e, 0x7e, 0x8b, 0x57, 0xe9, + 0xa3, 0x8d, 0x40, 0x37, 0x7d, 0x77, 0x37, 0xf4, 0x2e, 0xae, 0xe0, 0x54, 0xed, 0xee, 0x49, 0xe8, 0x26, 0xd3, 0x8e, + 0xd5, 0x6b, 0x88, 0x1b, 0xf2, 0x2b, 0xa3, 0xd1, 0xc8, 0xa6, 0x84, 0x84, 0x18, 0xce, 0xa1, 0x99, 0xd3, 0x72, 0xf9, + 0xea, 0xd6, 0xb3, 0x01, 0x19, 0x66, 0x7a, 0xcb, 0x64, 0xfd, 0x00, 0x65, 0xd5, 0x63, 0x68, 0x87, 0xde, 0x23, 0xc7, + 0x0f, 0x0f, 0xbe, 0xc9, 0xf8, 0x99, 0xc3, 0xb5, 0x87, 0x73, 0xe1, 0xbb, 0xac, 0x19, 0x99, 0x43, 0xe7, 0xd9, 0xc7, + 0xf1, 0x1e, 0xc6, 0xc9, 0xe7, 0x59, 0x28, 0x6f, 0xbc, 0xa6, 0xff, 0x51, 0xe9, 0xcd, 0x0e, 0x87, 0x9c, 0xae, 0x60, + 0xc5, 0xcd, 0xaa, 0xd0, 0xf0, 0xb3, 0xc8, 0x1b, 0x47, 0xbc, 0x26, 0x51, 0xd5, 0x7d, 0xde, 0xdb, 0x88, 0xa5, 0x1d, + 0xe3, 0x00, 0xe0, 0x44, 0xad, 0x1a, 0x76, 0xa5, 0x71, 0xad, 0x0e, 0x62, 0x44, 0x4a, 0xd8, 0x2a, 0x71, 0x24, 0x94, + 0xbf, 0x01, 0x08, 0x8b, 0xa1, 0x38, 0xde, 0x1a, 0xd6, 0x07, 0xd8, 0x0f, 0x5d, 0xa0, 0x69, 0x4e, 0xa9, 0x66, 0x00, + 0x90, 0x04, 0xfc, 0xd1, 0xd3, 0x4d, 0x43, 0x65, 0x9b, 0xe7, 0xa1, 0x65, 0x75, 0x05, 0x0f, 0xf4, 0xd4, 0x95, 0x0c, + 0x8c, 0xab, 0x3a, 0xf6, 0x36, 0xf7, 0xb7, 0x47, 0xab, 0xc8, 0x77, 0x36, 0xa9, 0x69, 0x16, 0x40, 0x8a, 0xc6, 0xa5, + 0x2f, 0xf4, 0x74, 0x02, 0xb4, 0x5e, 0x5b, 0x2a, 0xda, 0xef, 0xa3, 0x18, 0x35, 0x2e, 0x14, 0x58, 0x85, 0x09, 0x0a, + 0x87, 0x08, 0x23, 0x84, 0x7e, 0x5f, 0x86, 0x1b, 0x5f, 0x90, 0x41, 0x34, 0x5c, 0x8b, 0x0e, 0x45, 0xe4, 0x78, 0xd1, + 0xb6, 0x54, 0xd5, 0x9c, 0x34, 0x6d, 0x09, 0xbc, 0x89, 0x0c, 0xd8, 0xce, 0x3f, 0x6d, 0x88, 0x5c, 0x85, 0x0b, 0x18, + 0xbe, 0x23, 0xae, 0x05, 0xd1, 0x4d, 0x6d, 0xea, 0x6d, 0xd8, 0x21, 0x3a, 0x9a, 0xe2, 0xd1, 0x21, 0xf7, 0xdc, 0x3d, + 0xb7, 0x45, 0x7c, 0xf3, 0x19, 0x72, 0xd7, 0x74, 0xf6, 0x52, 0x84, 0x41, 0xdd, 0xb2, 0x81, 0x62, 0x1d, 0x3a, 0x41, + 0x01, 0x06, 0x70, 0xf9, 0x04, 0x74, 0x6c, 0x30, 0xa8, 0x08, 0x3e, 0x29, 0x6c, 0x9b, 0x06, 0xf9, 0x23, 0xde, 0x0d, + 0x1d, 0x5e, 0x5b, 0xf2, 0x40, 0xbc, 0xc2, 0x3e, 0x53, 0xc2, 0xfd, 0x0b, 0x0a, 0xba, 0xa3, 0xbc, 0x5c, 0x15, 0xae, + 0x4a, 0x03, 0x50, 0x65, 0xc7, 0x73, 0xad, 0x29, 0x69, 0x01, 0x2b, 0x25, 0x75, 0xe7, 0x37, 0xc1, 0x71, 0x4b, 0xa6, + 0xc2, 0xb7, 0xea, 0x46, 0x95, 0x87, 0x12, 0x45, 0x3a, 0xf6, 0x6c, 0xe7, 0x60, 0x0d, 0x80, 0xa7, 0xb0, 0xbd, 0x38, + 0x13, 0xf0, 0xb9, 0xd3, 0x2e, 0x5b, 0xe6, 0x12, 0x28, 0xea, 0x87, 0x71, 0x5e, 0x76, 0x7c, 0xb9, 0x3b, 0xda, 0xde, + 0x43, 0x6f, 0xc4, 0xc6, 0x78, 0x7d, 0x19, 0x35, 0xfd, 0xe2, 0x19, 0xae, 0x2c, 0x05, 0x79, 0xa0, 0xa9, 0x1e, 0x61, + 0x74, 0x08, 0x4c, 0x53, 0x7e, 0xc4, 0xc6, 0xd3, 0xe1, 0xd0, 0x90, 0x41, 0xaf, 0x99, 0x18, 0x0a, 0xec, 0x0b, 0x68, + 0x9d, 0x99, 0xb8, 0xc6, 0xa7, 0xed, 0x2b, 0x68, 0x75, 0x8b, 0x32, 0xb9, 0x33, 0x30, 0x7c, 0xa0, 0x25, 0x53, 0x30, + 0x55, 0x78, 0x43, 0xa4, 0x92, 0x7d, 0x5a, 0x5a, 0x87, 0x7d, 0xbb, 0x50, 0x68, 0xa1, 0x89, 0x5f, 0x65, 0x88, 0x9f, + 0xba, 0xce, 0xfc, 0xdb, 0xb4, 0x4f, 0x0d, 0x62, 0xe1, 0x48, 0x0c, 0x22, 0x7e, 0x71, 0xaa, 0x6c, 0x27, 0x84, 0x8a, + 0x8d, 0x87, 0xae, 0x75, 0xe3, 0x48, 0xaa, 0x30, 0x94, 0x42, 0xe3, 0xa9, 0xe1, 0xbe, 0x17, 0x3a, 0x7c, 0x1d, 0x66, + 0x71, 0x9b, 0x35, 0x92, 0x1a, 0xe3, 0x54, 0x98, 0x38, 0x95, 0x72, 0x15, 0x09, 0x0c, 0x94, 0x67, 0x0b, 0x83, 0x00, + 0x93, 0x98, 0x64, 0x6c, 0x2d, 0x84, 0x09, 0x63, 0xe7, 0x0a, 0xd3, 0xd4, 0x45, 0xea, 0x37, 0x03, 0x93, 0x05, 0x0d, + 0xf9, 0x3d, 0x1a, 0xad, 0xa9, 0x9a, 0x02, 0x0c, 0xe3, 0x28, 0xd5, 0xf8, 0xb7, 0x08, 0xb5, 0x19, 0x06, 0x00, 0xb6, + 0x79, 0x27, 0x33, 0x51, 0xbd, 0x16, 0x08, 0x81, 0xe6, 0xec, 0xa7, 0xe2, 0x6a, 0x67, 0x16, 0x8c, 0xa2, 0xdd, 0x5e, + 0xf9, 0x7c, 0xe0, 0x84, 0xf2, 0x58, 0x5d, 0xa0, 0x5e, 0xc9, 0xe2, 0x8d, 0x4c, 0x79, 0x2b, 0x44, 0xe6, 0x9e, 0x64, + 0x1f, 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, 0xcf, 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, 0x9d, 0xff, 0xa0, 0xb0, 0x06, 0x95, 0x11, 0x97, 0x11, + 0xe5, 0x99, 0x03, 0xdd, 0x34, 0x29, 0xe2, 0xf4, 0x6c, 0x15, 0x17, 0x25, 0x4f, 0xa1, 0x52, 0x4d, 0xdd, 0xa2, 0xde, + 0x04, 0xec, 0x0d, 0x91, 0x24, 0x59, 0x4b, 0x63, 0x2b, 0x76, 0x69, 0x90, 0x9e, 0x7b, 0x23, 0x2e, 0xbd, 0xaa, 0xd0, + 0x90, 0x96, 0x7a, 0x67, 0xa1, 0x92, 0xf9, 0x2b, 0xfe, 0x33, 0xa8, 0x15, 0xe8, 0x68, 0x93, 0x62, 0x3c, 0x07, 0x46, + 0x7c, 0x37, 0x98, 0xd5, 0x03, 0xc4, 0x45, 0x13, 0x94, 0x7a, 0x47, 0xec, 0xf8, 0xb9, 0xc9, 0xc3, 0xbb, 0x90, 0x73, + 0x06, 0x9f, 0x3e, 0xcc, 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, 0xbb, 0x2f, 0xe1, 0xc0, 0xa6, + 0xb6, 0x82, 0x1e, 0xaf, 0x2b, 0x79, 0x79, 0xa9, 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, 0x3e, 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, 0x67, 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, 0x4c, 0x9b, 0x01, 0x20, + 0x0b, 0x4a, 0xb9, 0xe3, 0xa7, 0x54, 0x3a, 0x58, 0xa2, 0x68, 0x7b, 0x39, 0x7d, 0xa3, 0x63, 0xe3, 0x87, 0xf4, 0x5c, + 0xc0, 0x76, 0x21, 0xbf, 0x75, 0xaf, 0x5e, 0xa2, 0x22, 0xb5, 0x6d, 0xd6, 0x03, 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, 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, 0x9b, 0xa7, 0xaf, 0xaf, 0xe2, 0x4b, 0x83, 0xa2, 0xd4, 0xb0, 0x88, 0x51, 0xa6, 0x7d, 0x95, + 0x84, 0xc1, 0xfb, 0xe5, 0xfd, 0x4f, 0x2a, 0x4b, 0xed, 0xf7, 0x60, 0x63, 0x45, 0x55, 0xbf, 0x94, 0xbc, 0x68, 0x0a, + 0xb0, 0xee, 0xb3, 0x44, 0x81, 0xdc, 0xef, 0x6d, 0x9a, 0xf9, 0x26, 0x6a, 0xdc, 0x6c, 0x58, 0x6f, 0x5c, 0xb7, 0x4b, + 0x6d, 0xc9, 0x8e, 0xac, 0x44, 0xce, 0x2c, 0x06, 0x33, 0x7e, 0x54, 0x18, 0x94, 0x86, 0x0d, 0xaa, 0x52, 0xf1, 0x7b, + 0x23, 0x82, 0x53, 0xc7, 0xaa, 0xc2, 0x98, 0x06, 0xcc, 0xb6, 0xa2, 0xd6, 0xa0, 0x0e, 0x4a, 0x69, 0x6b, 0x02, 0xb2, + 0xfd, 0xc6, 0x0a, 0x6a, 0x7e, 0xff, 0xcb, 0x18, 0xf2, 0x35, 0xa5, 0xa0, 0x92, 0x80, 0x9d, 0x41, 0xa3, 0xa7, 0x4a, + 0x18, 0x48, 0x41, 0xf0, 0x04, 0x28, 0x5f, 0x44, 0x8d, 0xd5, 0x6e, 0x5f, 0x9d, 0x1a, 0xa3, 0x2d, 0x20, 0xb4, 0x90, + 0x1e, 0x5d, 0xf6, 0x71, 0x1b, 0xeb, 0x40, 0xe2, 0xc1, 0x09, 0xb6, 0x73, 0x75, 0x8d, 0x46, 0x42, 0xf3, 0x87, 0x46, + 0x03, 0x5e, 0xd3, 0x0a, 0x14, 0xea, 0x39, 0x8e, 0x86, 0xce, 0x0e, 0x29, 0x88, 0xd8, 0xa0, 0x85, 0x7d, 0xf7, 0x7c, + 0x68, 0xf6, 0xf5, 0x3c, 0x59, 0x90, 0x9a, 0x4a, 0xf7, 0xb9, 0x5b, 0x42, 0xd6, 0xaa, 0x43, 0x59, 0x79, 0x80, 0xe3, + 0x85, 0x92, 0xf9, 0x3b, 0x4c, 0x6a, 0x94, 0xc6, 0x84, 0xc6, 0x88, 0x05, 0x2c, 0x09, 0xda, 0xeb, 0x81, 0xfa, 0x65, + 0x10, 0x2a, 0x9c, 0xe9, 0x89, 0xc4, 0xa7, 0x94, 0xab, 0x4f, 0x0b, 0x52, 0x4f, 0x0b, 0xe6, 0x40, 0x2f, 0x7d, 0x2b, + 0xbf, 0xb2, 0xf1, 0xd1, 0xee, 0xde, 0x35, 0x17, 0xd6, 0x31, 0xc4, 0xc5, 0x16, 0x7e, 0x73, 0x6a, 0x0a, 0xc0, 0x86, + 0xc7, 0xba, 0x2c, 0xdf, 0xa8, 0x89, 0xcc, 0xe2, 0x90, 0x44, 0x20, 0xd9, 0x6e, 0x6e, 0x6e, 0x23, 0xd8, 0xf6, 0x16, + 0x6a, 0x43, 0xfd, 0xe5, 0x6d, 0xf7, 0x7b, 0x86, 0x97, 0x7b, 0x72, 0xef, 0xa6, 0x0d, 0xe5, 0x0f, 0xf7, 0xaf, 0x92, + 0xff, 0xab, 0x4a, 0xee, 0xb7, 0xca, 0xac, 0xdb, 0xe2, 0xfd, 0xae, 0xe3, 0x96, 0x63, 0x34, 0x08, 0xac, 0x29, 0x30, + 0x90, 0x9e, 0x34, 0xa6, 0x89, 0x8e, 0xae, 0xcc, 0x98, 0xc1, 0xa3, 0x0b, 0xd0, 0x1c, 0xa6, 0xf3, 0x3c, 0x06, 0xe0, + 0x00, 0xff, 0xc8, 0x23, 0xd4, 0x3f, 0x9d, 0xe7, 0xc1, 0x59, 0x30, 0x28, 0x07, 0x81, 0xfe, 0xc4, 0x35, 0x27, 0x58, + 0x80, 0xce, 0x2d, 0x66, 0x10, 0x77, 0xd2, 0x9a, 0x39, 0xc4, 0x87, 0xc9, 0x74, 0x30, 0x88, 0xc9, 0x06, 0x40, 0xfa, + 0xe2, 0x85, 0x75, 0x0e, 0x2a, 0xf4, 0x82, 0x6c, 0xd5, 0x5d, 0x34, 0x2b, 0xf6, 0xaa, 0x9d, 0xe6, 0xfd, 0x7e, 0x3e, + 0x2f, 0x07, 0x41, 0xa3, 0xc2, 0xc2, 0x78, 0xff, 0xd1, 0xe6, 0x97, 0x46, 0x27, 0x4d, 0x30, 0x62, 0xed, 0x31, 0xaa, + 0x57, 0x3c, 0xcd, 0x68, 0xe3, 0x76, 0xac, 0x94, 0x2f, 0x20, 0x8a, 0x07, 0x86, 0xac, 0x95, 0x77, 0xe7, 0xe0, 0x75, + 0xb9, 0xf1, 0xe6, 0x88, 0x02, 0xec, 0xa6, 0x30, 0x4e, 0x6a, 0x2e, 0xba, 0xa8, 0x89, 0x67, 0xb0, 0xd3, 0xd5, 0x5b, + 0x89, 0x56, 0xe3, 0xbd, 0x78, 0xd7, 0x6c, 0xfc, 0xad, 0xdc, 0xd3, 0x65, 0xee, 0x5d, 0x00, 0xe2, 0xec, 0x5e, 0x5c, + 0xed, 0x61, 0xa9, 0x7b, 0xc1, 0xc0, 0x22, 0x87, 0xb4, 0xab, 0xd5, 0x43, 0x11, 0xa9, 0xf3, 0x18, 0x0c, 0x98, 0x4c, + 0x43, 0x6a, 0x32, 0xed, 0x15, 0x0a, 0xd2, 0xc6, 0x5a, 0x0b, 0x68, 0xc3, 0x61, 0xb1, 0x63, 0x37, 0xec, 0x4e, 0xb7, + 0x0e, 0x85, 0x12, 0x06, 0xb2, 0xae, 0x9b, 0x87, 0x5a, 0xc3, 0x13, 0x41, 0x0f, 0xaa, 0xd1, 0x7e, 0x7a, 0x28, 0x4f, + 0xda, 0x63, 0x01, 0x2e, 0x7a, 0xf8, 0xf2, 0xa5, 0xc0, 0x8b, 0xf6, 0x0e, 0xf2, 0x9c, 0xf9, 0x54, 0xf9, 0x20, 0x36, + 0xdc, 0x32, 0x7c, 0x68, 0x1f, 0xdf, 0x0a, 0x64, 0x52, 0x77, 0x34, 0xb5, 0xb5, 0x3b, 0x1a, 0xc7, 0x04, 0xfa, 0x4d, + 0x39, 0x4a, 0x99, 0x98, 0x5a, 0x96, 0xec, 0xa8, 0x97, 0x2b, 0x6f, 0xa8, 0x94, 0x1d, 0x2d, 0xdb, 0x9c, 0x5f, 0xda, + 0x48, 0xe8, 0xf7, 0xb5, 0x3b, 0x10, 0xbe, 0x51, 0xeb, 0x0d, 0x79, 0xd9, 0x10, 0xb1, 0x1c, 0x62, 0x06, 0x8e, 0x17, + 0x52, 0xb9, 0x76, 0x17, 0x4d, 0x55, 0xdd, 0xce, 0x56, 0x2e, 0x68, 0x89, 0xb7, 0x52, 0x60, 0x15, 0xa9, 0xd3, 0xeb, + 0xa9, 0xc4, 0xfb, 0x3e, 0x8a, 0xed, 0x47, 0xc0, 0x36, 0x36, 0x8e, 0xc6, 0xc6, 0x2d, 0x62, 0x83, 0xaf, 0xa2, 0x8a, + 0x16, 0x1c, 0x20, 0xb8, 0xdb, 0x92, 0x5a, 0x9a, 0x39, 0xc4, 0x7d, 0xc5, 0x03, 0xb4, 0xef, 0xe2, 0x88, 0x53, 0x01, + 0xb6, 0x75, 0xad, 0x73, 0x56, 0xcb, 0x01, 0x9b, 0x89, 0x9e, 0x7f, 0x5a, 0x35, 0x12, 0x31, 0xac, 0xb2, 0x91, 0xb2, + 0x42, 0x7b, 0x50, 0xba, 0x84, 0x8b, 0x2f, 0xc0, 0xcb, 0xf6, 0xfd, 0xca, 0xee, 0xb3, 0x25, 0xf6, 0x0f, 0xf3, 0xaa, + 0x09, 0x1e, 0x79, 0x8d, 0xb7, 0xf7, 0x30, 0xf1, 0xa5, 0x52, 0x08, 0xaf, 0x52, 0x1a, 0x4a, 0x00, 0x06, 0x49, 0x50, + 0xc3, 0x95, 0xb6, 0xcd, 0x20, 0x95, 0x31, 0xec, 0x6e, 0xf5, 0x56, 0xff, 0xa7, 0x55, 0xb8, 0xa8, 0x64, 0x31, 0x26, + 0x81, 0xce, 0xa9, 0x96, 0x9b, 0xc0, 0x82, 0x67, 0xbb, 0xe4, 0x08, 0x14, 0x76, 0x02, 0xb8, 0xa1, 0x84, 0xfd, 0x82, + 0xb7, 0xa1, 0x9c, 0xbd, 0xb2, 0x92, 0x27, 0xb7, 0x2f, 0xa9, 0xa0, 0x09, 0x99, 0x0a, 0xbb, 0x7f, 0x5b, 0x1b, 0xf6, + 0x45, 0x28, 0x47, 0x52, 0xe0, 0xe2, 0xa0, 0x73, 0x00, 0xfb, 0x83, 0x5c, 0xc6, 0xe6, 0x33, 0xe9, 0xf7, 0xd5, 0xfb, + 0xe7, 0x79, 0x96, 0x7c, 0xdc, 0x79, 0x6f, 0x78, 0x9a, 0x25, 0x03, 0x2a, 0x11, 0x53, 0xeb, 0xaa, 0x18, 0x2e, 0xb5, + 0x8b, 0x71, 0x83, 0x64, 0xc4, 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, 0x70, 0xaf, 0x4c, 0x48, 0xaa, 0x8d, 0x03, 0x2c, 0x62, 0x5d, 0x7f, 0x0c, + 0x0b, 0x00, 0x6a, 0x35, 0x48, 0xaf, 0xf4, 0x15, 0xa1, 0x2a, 0x09, 0xc1, 0xe8, 0x44, 0xc2, 0xcb, 0x80, 0xc6, 0x99, + 0x49, 0xb4, 0xb0, 0xc1, 0x01, 0x7d, 0x51, 0x99, 0x44, 0x63, 0x43, 0x1e, 0x50, 0x6e, 0xd3, 0x00, 0x06, 0x1f, 0x24, + 0x49, 0xf4, 0xf5, 0xd2, 0x24, 0x81, 0xa0, 0x04, 0xe5, 0x1b, 0xf4, 0xe7, 0xd2, 0xf3, 0xb1, 0xfc, 0xdd, 0x3b, 0x94, + 0x7e, 0x08, 0x0b, 0x90, 0x29, 0xea, 0x8a, 0x69, 0xc6, 0x8e, 0xb2, 0x6e, 0x63, 0x12, 0xcf, 0xd3, 0xee, 0xb6, 0x50, + 0x2e, 0x5d, 0xe0, 0x57, 0x96, 0x21, 0x8e, 0xf5, 0xf3, 0x78, 0xc5, 0x8e, 0x43, 0xae, 0xf1, 0xd2, 0x9f, 0xc7, 0x2b, + 0x9c, 0x21, 0x5a, 0xb5, 0x12, 0x88, 0xf2, 0x5f, 0xb5, 0x81, 0x43, 0xdc, 0x27, 0x18, 0xe4, 0xa2, 0xf2, 0x1e, 0x08, + 0xe4, 0x6d, 0x05, 0x11, 0x69, 0x66, 0xd7, 0x61, 0x44, 0xaa, 0x9d, 0x24, 0xf3, 0xe5, 0x8f, 0x32, 0x13, 0xde, 0x37, + 0xf0, 0xd8, 0x6c, 0x96, 0x4d, 0x31, 0x5f, 0xa8, 0x60, 0x0e, 0xee, 0x13, 0x15, 0x97, 0xa2, 0xf2, 0x9f, 0xb0, 0x0b, + 0x5e, 0x8c, 0x07, 0xaf, 0xd7, 0x08, 0xb0, 0x5f, 0xf9, 0x4f, 0xde, 0x98, 0xbd, 0xb5, 0x6e, 0x7c, 0x99, 0x89, 0xf8, + 0xc0, 0x47, 0xb7, 0x94, 0x8f, 0xee, 0xbc, 0x4c, 0x7f, 0x36, 0xa0, 0x44, 0x46, 0x65, 0xc5, 0x57, 0x2b, 0x9e, 0xce, + 0x6e, 0x93, 0x28, 0x1b, 0x55, 0x5c, 0xc0, 0xf4, 0x82, 0xe3, 0x5d, 0xb2, 0x3e, 0xcf, 0x92, 0xd7, 0x10, 0x7b, 0x60, + 0x25, 0x15, 0x16, 0x3f, 0x2c, 0x33, 0xb5, 0x98, 0x85, 0xac, 0xa4, 0xe0, 0xc1, 0xec, 0x3a, 0x89, 0xde, 0x2e, 0x3d, + 0x24, 0x35, 0x33, 0x65, 0x9b, 0xda, 0x11, 0x6a, 0xe3, 0xeb, 0x48, 0x37, 0xda, 0x02, 0x00, 0xee, 0xd9, 0x22, 0x8d, + 0x24, 0x13, 0xc3, 0x49, 0xcd, 0xb8, 0x49, 0x2f, 0x30, 0x35, 0xae, 0x59, 0x45, 0x13, 0x67, 0x21, 0x03, 0x7a, 0x7f, + 0xc0, 0xcb, 0xc1, 0xe7, 0x0c, 0xee, 0x3f, 0x68, 0x0d, 0x5c, 0x1e, 0x16, 0xfd, 0xbe, 0x3c, 0x2c, 0xb6, 0xdb, 0xf2, + 0x28, 0xee, 0xf7, 0xe5, 0x51, 0x6c, 0xf8, 0x07, 0xa5, 0xd8, 0x36, 0xe6, 0x06, 0x09, 0xcd, 0x25, 0x44, 0x2d, 0x1a, + 0xc1, 0x1f, 0x9a, 0xe5, 0x5c, 0x44, 0xf9, 0x61, 0xd2, 0xef, 0xf7, 0x96, 0x33, 0x31, 0xc8, 0x87, 0x49, 0x94, 0x0f, + 0x13, 0xcf, 0x09, 0xf1, 0x57, 0xcf, 0x09, 0x51, 0xd1, 0xc0, 0x15, 0x9c, 0x19, 0x80, 0x28, 0xe0, 0xd3, 0x3f, 0xaa, + 0x6b, 0x29, 0x74, 0x2d, 0xb1, 0xaa, 0x25, 0xd1, 0x15, 0xd4, 0xec, 0xba, 0x08, 0x4b, 0x2c, 0x85, 0x2e, 0xd9, 0x9f, + 0x4b, 0xe0, 0x89, 0x72, 0x5e, 0x6d, 0x80, 0x81, 0x8d, 0xf0, 0xce, 0x61, 0xc2, 0x49, 0xac, 0x6b, 0x40, 0x3b, 0xdd, + 0xd4, 0xf4, 0x82, 0xae, 0xe8, 0x25, 0xf2, 0xb3, 0x17, 0x60, 0xb0, 0x74, 0xc8, 0xf2, 0xe9, 0x60, 0x70, 0x41, 0x56, + 0xac, 0x9c, 0x87, 0xf1, 0x20, 0x5c, 0xcf, 0xf2, 0xe1, 0x45, 0x74, 0x41, 0xc8, 0x57, 0xc5, 0x82, 0xf6, 0x56, 0xa3, + 0xf2, 0x63, 0x06, 0xe1, 0xfd, 0xd2, 0x59, 0x98, 0x99, 0x38, 0x1f, 0xab, 0xd1, 0x2d, 0x5d, 0x41, 0xfc, 0x1a, 0xb8, + 0x91, 0x90, 0x08, 0x3a, 0x72, 0x49, 0x57, 0x74, 0x4d, 0xa5, 0x99, 0x61, 0x8c, 0xd6, 0x6d, 0x8f, 0x93, 0x04, 0x1c, + 0x93, 0x5d, 0xf1, 0xd1, 0x58, 0x15, 0xde, 0xf5, 0x1d, 0xa1, 0xbd, 0x5e, 0xe2, 0x06, 0xe9, 0xbb, 0xf6, 0x20, 0x01, + 0x23, 0x32, 0x52, 0x03, 0x65, 0x46, 0x46, 0x52, 0x33, 0xa9, 0x38, 0x24, 0xb1, 0x3f, 0x24, 0x6a, 0x1c, 0x12, 0x7f, + 0x1c, 0x72, 0x3d, 0x0e, 0xc8, 0xdd, 0x2f, 0xd9, 0x98, 0xa6, 0x6c, 0x4c, 0xd7, 0x6a, 0x54, 0xe8, 0x15, 0x3d, 0xd7, + 0xd4, 0xf1, 0x8c, 0xbd, 0x81, 0x03, 0x7b, 0x10, 0xe6, 0xb3, 0x78, 0xf8, 0x26, 0x7a, 0x43, 0xc8, 0x57, 0x92, 0x5e, + 0xab, 0x4b, 0x19, 0x04, 0x42, 0xbc, 0x02, 0xe7, 0x52, 0x17, 0xea, 0xe4, 0xca, 0xec, 0x38, 0x7c, 0xba, 0x6c, 0x3c, + 0x9d, 0x43, 0x44, 0x1f, 0xb4, 0x52, 0xe9, 0xf7, 0xc3, 0x0b, 0x56, 0xce, 0xcf, 0xc2, 0x31, 0x01, 0x1c, 0x1e, 0x3d, + 0x9c, 0x17, 0xa3, 0x5b, 0x7a, 0x31, 0xba, 0x23, 0x60, 0xe1, 0x35, 0x9e, 0xae, 0x0f, 0x59, 0x3c, 0x1d, 0x0c, 0xd6, + 0x48, 0xd5, 0x55, 0xee, 0x35, 0x59, 0xd0, 0x0b, 0x9c, 0x08, 0x02, 0x0c, 0x7d, 0x26, 0xd6, 0x86, 0x86, 0xbf, 0x61, + 0xf0, 0xf1, 0x1d, 0xbb, 0x18, 0xdd, 0xd1, 0x5b, 0xf6, 0x66, 0x3b, 0x9e, 0x02, 0x33, 0xb5, 0x9a, 0x85, 0x77, 0x87, + 0x97, 0xb3, 0x4b, 0x76, 0x17, 0xdd, 0x1d, 0x41, 0x43, 0xaf, 0xd8, 0x1d, 0x02, 0x2e, 0xa5, 0x8f, 0x97, 0x83, 0x37, + 0x64, 0x7f, 0x30, 0x48, 0x49, 0x14, 0x5e, 0x87, 0x5e, 0x2b, 0xdf, 0xd0, 0x3b, 0x42, 0x57, 0xec, 0x16, 0x47, 0xe3, + 0x92, 0xe1, 0x07, 0xe7, 0xec, 0xae, 0xbe, 0x0e, 0xbd, 0xdd, 0x9c, 0x88, 0x4e, 0x10, 0x23, 0xf4, 0x35, 0x70, 0x34, + 0xcb, 0x85, 0x99, 0x80, 0x27, 0x73, 0x91, 0xd1, 0xa2, 0xd0, 0x0c, 0xc4, 0x59, 0x09, 0x88, 0x25, 0x51, 0xf7, 0x9b, + 0x8d, 0xce, 0x60, 0x39, 0xf7, 0xfb, 0xbd, 0xca, 0xd0, 0x03, 0x44, 0xce, 0xec, 0xa4, 0x07, 0x3d, 0x9f, 0x1e, 0xe0, + 0x27, 0x7a, 0xd5, 0x20, 0x4e, 0xe6, 0x77, 0xcb, 0xe8, 0x57, 0x8f, 0x3e, 0xfc, 0xd0, 0x4d, 0x79, 0x44, 0xfe, 0xef, + 0x53, 0x9e, 0x32, 0x8f, 0xde, 0x54, 0x1e, 0x08, 0x9e, 0xb7, 0x26, 0x95, 0x46, 0xa2, 0x1a, 0x9d, 0xad, 0x62, 0xd0, + 0x46, 0xa2, 0xb6, 0x41, 0x3f, 0xa1, 0x85, 0x15, 0x44, 0xc8, 0x39, 0x78, 0x01, 0x06, 0xa9, 0x10, 0x2a, 0x47, 0x2d, + 0x4a, 0x34, 0x04, 0xc9, 0x65, 0xc9, 0x55, 0xf8, 0x1c, 0x42, 0xd5, 0xe9, 0xe3, 0x4c, 0x84, 0x0d, 0x3d, 0x0e, 0x7d, + 0x00, 0xf8, 0x5f, 0x76, 0xc8, 0x45, 0xc9, 0x2f, 0xf1, 0x6c, 0x6e, 0x13, 0x8c, 0x82, 0x25, 0xa2, 0x19, 0xda, 0x06, + 0xb1, 0x1f, 0x4b, 0x82, 0xf5, 0x48, 0x1a, 0x8f, 0x4a, 0x73, 0x44, 0xf8, 0x51, 0x7c, 0x14, 0x3d, 0x8d, 0x0d, 0x89, + 0xe4, 0x48, 0x22, 0xf9, 0x00, 0x08, 0x27, 0x41, 0x7f, 0x71, 0xd7, 0x64, 0xd7, 0x42, 0x62, 0xd0, 0x9f, 0x96, 0x4c, + 0xcb, 0xee, 0x55, 0x8f, 0x7d, 0x45, 0x90, 0x3b, 0xa6, 0xff, 0xf2, 0xfa, 0xf0, 0xaf, 0x25, 0xce, 0xa0, 0xf5, 0x7c, + 0x51, 0x9d, 0x99, 0x79, 0x83, 0x1b, 0x79, 0x5d, 0xd6, 0xae, 0xcb, 0x17, 0x7c, 0x8f, 0xdf, 0x56, 0x5c, 0xa4, 0xe5, + 0xde, 0xcf, 0x55, 0x1b, 0xcf, 0xa9, 0x5c, 0xaf, 0x5c, 0x9c, 0x15, 0x65, 0x9c, 0xea, 0x49, 0x5d, 0x8c, 0x35, 0x6c, + 0xc3, 0xef, 0x11, 0x75, 0x25, 0x2d, 0x47, 0x4f, 0x29, 0x57, 0xcd, 0x94, 0x8b, 0x75, 0x9e, 0xff, 0xb4, 0x93, 0x8a, + 0x53, 0xdc, 0x4c, 0x41, 0xaa, 0xd4, 0x72, 0x01, 0xd5, 0x73, 0xd4, 0x72, 0xb7, 0x34, 0x3b, 0xc0, 0xb9, 0x6d, 0xaa, + 0x8f, 0x95, 0xd9, 0x85, 0x97, 0xdc, 0xb8, 0x3f, 0x99, 0x32, 0x2c, 0x18, 0x85, 0x36, 0xab, 0xae, 0xb4, 0x7d, 0xa1, + 0x75, 0x1a, 0x86, 0x2b, 0x3f, 0x5e, 0x40, 0xba, 0x80, 0x71, 0xbc, 0x28, 0x99, 0x18, 0xb7, 0x47, 0x6f, 0x05, 0xf1, + 0x25, 0x5b, 0x81, 0xf4, 0xfb, 0x3d, 0xe1, 0xed, 0xba, 0x8e, 0xb6, 0x7b, 0xe2, 0x94, 0x51, 0xb9, 0x8a, 0xc5, 0xf7, + 0xf1, 0xca, 0x40, 0x26, 0xab, 0xe3, 0xb1, 0x31, 0xa6, 0xd3, 0x7f, 0x24, 0xa1, 0x5f, 0x08, 0x05, 0x9f, 0xf5, 0xd2, + 0xca, 0x93, 0xdb, 0xc3, 0x32, 0xae, 0xd1, 0x2b, 0x71, 0xa5, 0xfb, 0x66, 0xa4, 0x90, 0x7a, 0xe4, 0xab, 0xa6, 0x80, + 0xde, 0x8c, 0x7d, 0x33, 0x15, 0xe6, 0xed, 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, 0x59, 0x49, 0xbe, 0x2b, 0x8d, 0xb9, 0xf4, 0xa9, 0xd2, 0xc4, 0x70, + 0xb2, 0x18, 0x71, 0x91, 0x2e, 0xea, 0xcc, 0xae, 0x85, 0xcf, 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, 0x7e, 0x40, 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, 0x30, 0x39, 0x4f, 0x17, 0x74, 0xc5, 0xe4, 0x7c, 0x8d, 0x37, 0xa1, 0x0b, 0x38, 0x21, 0xc9, 0x26, 0x56, + 0x0a, 0xd8, 0x0b, 0xbc, 0xbc, 0xe1, 0x99, 0xaa, 0x69, 0xd9, 0xa5, 0xe2, 0x00, 0xe3, 0xf3, 0x32, 0x0c, 0xcb, 0xe1, + 0x05, 0x58, 0x4b, 0xec, 0x87, 0xab, 0x39, 0x5f, 0xa8, 0xdf, 0x10, 0x75, 0x3e, 0x09, 0x15, 0xbb, 0x60, 0xf7, 0x02, + 0x99, 0x5e, 0xcd, 0xf9, 0x42, 0x8d, 0x84, 0x2e, 0xf8, 0xca, 0x1a, 0x9b, 0xc4, 0x9e, 0xa0, 0x65, 0x16, 0xcf, 0xc7, + 0x8b, 0x28, 0xae, 0x61, 0x19, 0x9e, 0xaa, 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, 0x2f, 0x73, 0x8f, 0x57, 0x12, 0x06, 0x3f, 0xa4, + 0x66, 0x93, 0xcc, 0xdb, 0x2b, 0xf6, 0x3d, 0x74, 0xd4, 0xa3, 0xd6, 0x78, 0x5f, 0xbd, 0xe0, 0x14, 0x62, 0x94, 0x50, + 0x74, 0x12, 0x0c, 0xe0, 0x76, 0x09, 0x29, 0xee, 0x06, 0xbb, 0x69, 0x5e, 0xf3, 0xa2, 0xe0, 0x7c, 0x5d, 0x55, 0x81, + 0x1f, 0xd0, 0x70, 0xbe, 0xd8, 0x0d, 0x61, 0x38, 0xa6, 0xad, 0x6b, 0x18, 0x84, 0x19, 0xc3, 0x48, 0x08, 0x5e, 0xff, + 0xa2, 0x27, 0x34, 0x89, 0x57, 0xdf, 0xf1, 0x4f, 0x19, 0x2f, 0x14, 0x91, 0x06, 0x11, 0x52, 0x37, 0xf1, 0x8d, 0x4c, + 0x93, 0x02, 0x0a, 0x01, 0x46, 0x01, 0x95, 0xd8, 0xd0, 0x54, 0xfc, 0xad, 0x16, 0x1f, 0xfc, 0xd4, 0x74, 0x3c, 0x1a, + 0xd7, 0xad, 0xce, 0xa8, 0xa0, 0x33, 0xd0, 0xa3, 0x56, 0xd4, 0xd3, 0xa0, 0x95, 0x60, 0x1a, 0x69, 0xde, 0xba, 0x87, + 0xc0, 0x2b, 0xd3, 0xe2, 0x9d, 0x07, 0x74, 0x73, 0xe6, 0x83, 0x27, 0x8f, 0xe9, 0x99, 0x43, 0x4f, 0xae, 0xd8, 0x51, + 0xd5, 0x43, 0xed, 0xbd, 0x19, 0xa1, 0xa0, 0xdf, 0xc7, 0x14, 0xe8, 0x46, 0x50, 0x7b, 0x57, 0xf7, 0x1f, 0xcb, 0x5d, + 0x0e, 0xdf, 0x71, 0x96, 0x1b, 0xc0, 0x52, 0x91, 0xb5, 0x02, 0x8f, 0x02, 0xd4, 0xa5, 0x32, 0x84, 0x2d, 0xe6, 0x70, + 0xa8, 0xec, 0x56, 0xad, 0x86, 0x92, 0x1c, 0x96, 0x23, 0x70, 0x08, 0x5d, 0x97, 0x83, 0x72, 0xb4, 0xcc, 0xaa, 0xf7, + 0xf8, 0x5b, 0xb3, 0x0e, 0x49, 0x76, 0x1f, 0xeb, 0xc0, 0x2d, 0xeb, 0x30, 0xfd, 0x68, 0x90, 0x02, 0xd0, 0x64, 0x23, + 0x70, 0x09, 0xc0, 0x7b, 0xfb, 0x8f, 0x08, 0xb5, 0x32, 0xbd, 0x97, 0xb1, 0x50, 0xdf, 0x37, 0x92, 0xa0, 0x84, 0x66, + 0x42, 0xe5, 0x58, 0x0a, 0xde, 0x79, 0xa4, 0x73, 0x52, 0x67, 0xe2, 0x3d, 0x88, 0xd3, 0xc2, 0x07, 0xf6, 0x16, 0x04, + 0xe7, 0x2c, 0xe8, 0x1d, 0xde, 0x66, 0xb5, 0xd4, 0x46, 0x0f, 0x14, 0xc0, 0xef, 0x06, 0x77, 0x08, 0xf2, 0xd5, 0x18, + 0xae, 0x95, 0xbc, 0x09, 0xf9, 0xb0, 0xa0, 0x07, 0x64, 0x60, 0x9f, 0xc5, 0x30, 0xa6, 0x07, 0xe4, 0xd0, 0x3e, 0x4b, + 0x37, 0x80, 0x03, 0xa9, 0x47, 0x95, 0x1e, 0x40, 0x83, 0x7e, 0xb3, 0x2d, 0xb2, 0x24, 0xeb, 0xc7, 0xd2, 0x28, 0x62, + 0xa0, 0x4a, 0x10, 0x51, 0x8b, 0x7f, 0x3d, 0x98, 0xeb, 0x0e, 0x73, 0x81, 0x30, 0x07, 0x03, 0x0e, 0xe2, 0x36, 0x08, + 0xcd, 0x01, 0xb3, 0xb9, 0x8d, 0x04, 0xbd, 0xb3, 0x86, 0x99, 0x1d, 0xfd, 0xe1, 0x56, 0x82, 0x6f, 0xb2, 0xd6, 0xa8, + 0xf3, 0xe2, 0x10, 0x08, 0x82, 0x37, 0x85, 0xaa, 0xf6, 0xaa, 0x07, 0x36, 0xde, 0xaa, 0x1f, 0xdb, 0xed, 0x78, 0x2a, + 0xdc, 0xb5, 0x5f, 0x50, 0x38, 0xf9, 0x94, 0xfc, 0xeb, 0xbd, 0xc9, 0xe0, 0xc0, 0xc8, 0xf0, 0xa5, 0xb7, 0x7f, 0xe1, + 0x6b, 0x2d, 0xdd, 0x13, 0x83, 0x92, 0x3c, 0x3e, 0x50, 0xf4, 0xef, 0x5e, 0x59, 0xf9, 0xd4, 0x4e, 0xff, 0x76, 0x6b, + 0xd6, 0xe7, 0xe1, 0x68, 0xb2, 0xdd, 0xf6, 0xe2, 0x4a, 0x7b, 0xac, 0xe9, 0x05, 0x81, 0xce, 0xf5, 0x64, 0xff, 0x00, + 0xa2, 0x22, 0x34, 0xe3, 0x6e, 0x96, 0x0d, 0x89, 0x8c, 0x1f, 0xa7, 0xb3, 0x6c, 0x08, 0x76, 0xb8, 0x17, 0x95, 0xb8, + 0x1c, 0xb5, 0x36, 0x38, 0x3d, 0x4b, 0x42, 0x08, 0xe5, 0x80, 0x95, 0xdd, 0xaa, 0x3f, 0x77, 0xca, 0x4c, 0x48, 0x4d, + 0x56, 0xb7, 0x53, 0xba, 0x87, 0x69, 0xbe, 0x67, 0x46, 0x70, 0xc0, 0xbd, 0xfd, 0x55, 0x7f, 0x0c, 0x93, 0x4c, 0x93, + 0x53, 0x24, 0xbf, 0x48, 0x4f, 0x21, 0x69, 0x87, 0x9e, 0x2a, 0x02, 0x38, 0xa1, 0xf6, 0x63, 0xf8, 0x0d, 0xe3, 0xfe, + 0x5d, 0xf3, 0xb5, 0x9b, 0x8a, 0xe8, 0x29, 0xc5, 0x32, 0x35, 0x39, 0x4d, 0xb2, 0x22, 0x81, 0xa8, 0x8d, 0xaa, 0x19, + 0xd1, 0x13, 0x17, 0xf3, 0x51, 0x11, 0x3e, 0xaf, 0xd6, 0xff, 0x19, 0xc2, 0x67, 0x14, 0x6e, 0x00, 0x97, 0x57, 0x5c, + 0x9e, 0x87, 0xcf, 0x9e, 0xd2, 0xbd, 0xc9, 0x37, 0x07, 0x74, 0xef, 0xe0, 0xc9, 0x33, 0x02, 0xb0, 0x68, 0x97, 0xe7, + 0xe1, 0xc1, 0xb3, 0x67, 0x74, 0xef, 0xdb, 0x6f, 0xe9, 0xde, 0xe4, 0xc9, 0x41, 0x23, 0x6d, 0xf2, 0xec, 0x5b, 0xba, + 0xf7, 0xcd, 0xd3, 0x46, 0xda, 0xc1, 0xf8, 0x19, 0xdd, 0xfb, 0xfb, 0x37, 0x26, 0xed, 0x6f, 0x90, 0xed, 0xdb, 0x03, + 0xfc, 0xcf, 0xa4, 0x4d, 0x9e, 0x3d, 0xa1, 0x7b, 0x93, 0x31, 0x54, 0xf2, 0xcc, 0x55, 0x32, 0x9e, 0xc0, 0xc7, 0x4f, + 0xe0, 0xbf, 0xbf, 0x91, 0x60, 0x41, 0x2b, 0xc9, 0x72, 0x81, 0xfa, 0x33, 0x14, 0x71, 0xa2, 0x6a, 0x22, 0xe1, 0x21, + 0x66, 0x56, 0xdf, 0xc4, 0x61, 0x40, 0x5c, 0x3a, 0x14, 0x44, 0xf7, 0xc6, 0xa3, 0x67, 0x24, 0xf0, 0xe1, 0xe9, 0x6e, + 0x7c, 0x90, 0xb1, 0x5c, 0xcc, 0xb3, 0xaf, 0x72, 0x13, 0x5b, 0xc1, 0x03, 0xb0, 0xfa, 0xe8, 0xe7, 0xaa, 0xe4, 0x3c, + 0xfb, 0xaa, 0x92, 0xbb, 0xb9, 0x7e, 0x6b, 0x01, 0xca, 0xfb, 0xab, 0x96, 0xdd, 0x14, 0x2a, 0x74, 0x5a, 0x6b, 0xf4, + 0xd9, 0x47, 0x4c, 0x1f, 0x0c, 0xbc, 0x1b, 0xf6, 0x3f, 0x76, 0xca, 0x69, 0x7d, 0xa3, 0x51, 0xa8, 0x51, 0x79, 0x48, + 0xd8, 0x11, 0x14, 0x3d, 0x18, 0x00, 0x4f, 0xe0, 0xe1, 0xbe, 0xfd, 0x9b, 0x65, 0x7c, 0xec, 0x28, 0xe3, 0x67, 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, 0xc5, 0x5e, 0xca, 0x67, 0x15, 0xfb, 0xc7, 0x12, 0xd5, 0x5b, 0x51, 0xe3, 0x8d, + 0xcc, 0x66, 0x15, 0xfb, 0xde, 0xbc, 0x01, 0x6e, 0x86, 0xfd, 0xa6, 0x9e, 0xfc, 0xc0, 0x19, 0x5c, 0xda, 0xf6, 0x28, + 0x13, 0x23, 0xc0, 0x0a, 0xc8, 0xc0, 0x81, 0x07, 0x40, 0x07, 0xfd, 0xd1, 0xde, 0x6e, 0x55, 0x4a, 0xb3, 0xcf, 0x16, + 0x06, 0xd0, 0x30, 0x6f, 0x13, 0x57, 0xf6, 0xef, 0x0d, 0x79, 0x09, 0x0a, 0xb7, 0x9a, 0xe5, 0xed, 0x14, 0x86, 0x10, + 0x82, 0x3f, 0x2e, 0x19, 0x00, 0x0e, 0x04, 0x18, 0x8c, 0xb5, 0x0c, 0xa8, 0xd9, 0xf2, 0xd1, 0x86, 0x2b, 0xf5, 0x24, + 0x70, 0x06, 0x17, 0xb2, 0x48, 0xf8, 0x89, 0x16, 0xfb, 0xa3, 0xf5, 0xa3, 0xef, 0xdb, 0xe3, 0xc1, 0xda, 0xf7, 0xf8, + 0x48, 0x7f, 0xd6, 0xb8, 0x0e, 0x6c, 0x5a, 0xbe, 0xf1, 0xa2, 0xb6, 0x12, 0x8f, 0x12, 0x78, 0x03, 0x13, 0x91, 0xc2, + 0x20, 0xd5, 0x02, 0xc7, 0xa0, 0xbc, 0xb1, 0x10, 0x4b, 0xd5, 0xd5, 0x0d, 0xb6, 0x20, 0x32, 0x04, 0x0f, 0xb7, 0xdf, + 0x97, 0x2a, 0x70, 0x54, 0xbf, 0xcf, 0xa5, 0xef, 0xf6, 0x64, 0xec, 0xc8, 0x71, 0xea, 0xa7, 0xc2, 0xc1, 0x7f, 0x93, + 0xba, 0x36, 0x96, 0x2b, 0x29, 0xb3, 0x2c, 0x0b, 0x3b, 0x0a, 0xb5, 0xdc, 0xa3, 0xf2, 0x20, 0xf9, 0x42, 0x0e, 0x91, + 0x2c, 0x30, 0x0a, 0x05, 0x19, 0x4e, 0xa8, 0x18, 0xad, 0x45, 0xb9, 0xcc, 0x2e, 0xaa, 0x70, 0xa3, 0x14, 0xca, 0x9c, + 0xa2, 0x6f, 0x37, 0x38, 0x90, 0x90, 0x28, 0x2b, 0xdf, 0xc6, 0x6f, 0x43, 0x04, 0xab, 0xe3, 0xda, 0x16, 0x8a, 0x7b, + 0xfb, 0x93, 0xa7, 0x5d, 0xfc, 0x91, 0x71, 0x01, 0x75, 0xb1, 0x98, 0x86, 0x13, 0x1b, 0xfb, 0xc6, 0x7d, 0x61, 0x35, + 0x3d, 0x00, 0xf5, 0x5d, 0x2a, 0x31, 0x82, 0xfa, 0xca, 0xd8, 0xc7, 0xf6, 0x18, 0x93, 0x33, 0x88, 0x35, 0xac, 0x72, + 0x66, 0xaa, 0x6f, 0x84, 0x1d, 0x01, 0x70, 0x23, 0xb4, 0x46, 0x47, 0x26, 0xa9, 0x42, 0x3c, 0x2f, 0x55, 0xf8, 0xd6, + 0x8c, 0xd0, 0x31, 0x78, 0x53, 0xd9, 0x46, 0x66, 0xd2, 0x17, 0x0c, 0x9a, 0x63, 0x5b, 0x47, 0x61, 0xb5, 0x95, 0x65, + 0x47, 0x00, 0x37, 0x90, 0x1d, 0x9a, 0x8b, 0xe7, 0xac, 0x9a, 0x67, 0x8b, 0xc8, 0x04, 0x05, 0x5c, 0x0a, 0xcb, 0xa0, + 0x7d, 0xba, 0x47, 0xb6, 0xe3, 0x10, 0xba, 0xe1, 0x3e, 0x82, 0xf1, 0xb4, 0x9b, 0x82, 0x15, 0x44, 0x23, 0xc4, 0xc3, + 0x8c, 0x59, 0x7c, 0xaf, 0x34, 0xe5, 0xa9, 0x6a, 0x09, 0x04, 0x8e, 0x42, 0xa8, 0x8b, 0x5d, 0xa3, 0x04, 0x97, 0xa9, + 0x11, 0xcc, 0x60, 0xc7, 0x8e, 0xd4, 0x76, 0xc9, 0x39, 0x1d, 0xaa, 0x29, 0x2d, 0xf5, 0x94, 0x6a, 0x5f, 0x43, 0x31, + 0x2f, 0xd1, 0x43, 0x0f, 0x5c, 0x0f, 0xb4, 0x43, 0x5e, 0x49, 0x27, 0x26, 0x82, 0x4e, 0xab, 0x4d, 0xd8, 0xb9, 0x91, + 0x6e, 0x59, 0x8d, 0xbc, 0x63, 0x68, 0x76, 0xc4, 0x4b, 0x3f, 0x50, 0x17, 0x40, 0x84, 0xdc, 0xdb, 0x22, 0x73, 0x44, + 0xb3, 0xac, 0x7c, 0x05, 0x65, 0x71, 0xc4, 0xd6, 0x15, 0x70, 0x2d, 0x05, 0x93, 0x4b, 0x1e, 0xf1, 0x14, 0x11, 0x01, + 0x8f, 0x95, 0x76, 0x7d, 0xa7, 0x25, 0x84, 0x66, 0x29, 0x10, 0x37, 0x17, 0xc5, 0xb9, 0xb6, 0x81, 0x2c, 0x80, 0xbe, + 0xfd, 0x94, 0x5d, 0x79, 0xe1, 0x60, 0x37, 0x57, 0x99, 0x78, 0xc1, 0x2f, 0x32, 0xc1, 0x53, 0x04, 0xbb, 0xba, 0x35, + 0x0f, 0xdc, 0xb1, 0x6d, 0x60, 0xf9, 0xf6, 0x1d, 0x2c, 0x98, 0x32, 0xd4, 0x4a, 0x89, 0x4c, 0x44, 0x02, 0x32, 0xfb, + 0xcc, 0xdd, 0x9b, 0x4c, 0xbc, 0x89, 0x6f, 0xc1, 0x9b, 0xa2, 0xc1, 0x4f, 0x8f, 0xce, 0xf1, 0x4b, 0x44, 0x12, 0x85, + 0x18, 0xb6, 0x18, 0x11, 0x0b, 0x91, 0x63, 0xc7, 0x84, 0x72, 0x25, 0x68, 0x6d, 0x0d, 0x81, 0x17, 0x7f, 0x5a, 0x75, + 0xef, 0x2a, 0x13, 0xc6, 0x3e, 0xe3, 0x2a, 0xbe, 0x65, 0xa5, 0x02, 0xb3, 0xc0, 0x38, 0xf7, 0x6d, 0x29, 0xc9, 0x55, + 0x26, 0x8c, 0x80, 0xe4, 0x2a, 0xbe, 0xa5, 0x4d, 0x19, 0x87, 0xb6, 0xa2, 0xf3, 0xe2, 0xfc, 0xee, 0x0e, 0xbf, 0xc4, + 0x50, 0x2b, 0xe3, 0x7e, 0x1f, 0x24, 0x66, 0xd2, 0x36, 0x65, 0x26, 0x23, 0xa9, 0xd1, 0x42, 0x2a, 0xca, 0x07, 0x13, + 0xb2, 0xbb, 0x52, 0x2d, 0x23, 0x6a, 0xbf, 0x0a, 0xc5, 0x6c, 0x1c, 0x4d, 0x08, 0x9d, 0x74, 0xac, 0x77, 0xd3, 0x5a, + 0xc8, 0x34, 0x7a, 0x16, 0x79, 0x3e, 0x9d, 0x05, 0xab, 0xa6, 0xc5, 0x21, 0xe3, 0xd3, 0x62, 0x30, 0x20, 0xda, 0xa5, + 0x70, 0x83, 0xf5, 0x80, 0x29, 0x8d, 0x8b, 0xb7, 0x66, 0x5a, 0xfd, 0x4a, 0xaa, 0x90, 0xf4, 0x9e, 0x01, 0x49, 0x26, + 0x5d, 0xb0, 0x5b, 0x90, 0x28, 0x7a, 0xfe, 0x77, 0x6a, 0x0b, 0xee, 0x7a, 0x30, 0x36, 0xa3, 0xfb, 0x7a, 0xc6, 0x7f, + 0xa8, 0x6d, 0x41, 0xd4, 0xa7, 0x92, 0xf5, 0x3a, 0x12, 0x55, 0xc8, 0x45, 0xf8, 0xd9, 0xd1, 0x10, 0x43, 0x54, 0x7b, + 0x2c, 0x10, 0xeb, 0xab, 0x73, 0x5e, 0xe0, 0xf4, 0x33, 0x77, 0xb9, 0x82, 0x6d, 0x41, 0x2b, 0x43, 0xa3, 0xde, 0xc6, + 0x6f, 0x23, 0x7b, 0x59, 0xd0, 0x45, 0xbe, 0x40, 0x21, 0x6b, 0x1e, 0x86, 0xd5, 0xb0, 0x3d, 0x88, 0x64, 0xbf, 0x3d, + 0x09, 0x8d, 0xc6, 0xc0, 0x02, 0xd9, 0xa1, 0x11, 0xb8, 0x08, 0xad, 0xfc, 0xed, 0x10, 0x5c, 0xb8, 0x2c, 0x22, 0xcb, + 0x50, 0xc7, 0x6f, 0x6a, 0x37, 0x41, 0xf5, 0x0a, 0x9d, 0xa6, 0xb0, 0x2a, 0x65, 0x92, 0x0f, 0xbf, 0x5e, 0xc9, 0x02, + 0x33, 0x79, 0x5d, 0xf6, 0xe8, 0x6b, 0xbb, 0xbd, 0x03, 0x53, 0xb0, 0xee, 0x93, 0xf7, 0xf5, 0xe3, 0xce, 0x9e, 0x80, + 0x51, 0xac, 0xca, 0xd1, 0x14, 0x52, 0x6a, 0x1f, 0x94, 0xfa, 0x63, 0xb8, 0x14, 0x9a, 0x63, 0xb7, 0x80, 0x49, 0xc0, + 0x3e, 0x43, 0xaa, 0xc7, 0xb4, 0x63, 0x9f, 0xa3, 0x0d, 0x2c, 0x09, 0x38, 0xfc, 0xa3, 0x4c, 0xd6, 0xfe, 0xd5, 0x5d, + 0xa4, 0xcd, 0x90, 0x2d, 0xf3, 0x05, 0xf0, 0xf9, 0xb0, 0x6b, 0xa3, 0x12, 0x65, 0x13, 0x91, 0xa4, 0xb0, 0xe5, 0x31, + 0x48, 0x7b, 0x14, 0xd3, 0x55, 0xc1, 0x93, 0x0c, 0xa5, 0x14, 0x89, 0xf6, 0x09, 0xce, 0xe1, 0x0d, 0xee, 0x47, 0x15, + 0x10, 0x5e, 0x85, 0x9c, 0x8e, 0x52, 0xaa, 0x2d, 0x60, 0x14, 0xf5, 0x00, 0x51, 0x5e, 0x06, 0x72, 0xbc, 0xed, 0x76, + 0x42, 0x57, 0x6c, 0x39, 0x9c, 0x50, 0x24, 0x25, 0x97, 0x58, 0xee, 0x15, 0xe8, 0x3c, 0xce, 0x59, 0xef, 0x25, 0x60, + 0x11, 0x9c, 0xc1, 0xdf, 0x98, 0xd0, 0x6b, 0xf8, 0x9b, 0x13, 0xfa, 0x86, 0x85, 0x57, 0xc3, 0x4b, 0xb2, 0x1f, 0xa6, + 0x83, 0x89, 0x12, 0x8c, 0xdd, 0xb1, 0x65, 0x19, 0xaa, 0xc4, 0xd5, 0xfe, 0x05, 0x79, 0x7c, 0x41, 0x6f, 0xe9, 0x0d, + 0x3d, 0xa5, 0x27, 0x40, 0xf8, 0xef, 0x0e, 0x27, 0x7c, 0x38, 0x79, 0xda, 0xef, 0xf7, 0xce, 0xfb, 0xfd, 0xde, 0x99, + 0x31, 0xa0, 0xd0, 0xbb, 0xe8, 0xb2, 0xa6, 0xfa, 0xd7, 0x55, 0xbd, 0x98, 0x9e, 0xa8, 0x8d, 0x9b, 0xf0, 0x2c, 0x0f, + 0xaf, 0xf6, 0xef, 0xc8, 0x10, 0x1f, 0x2f, 0x72, 0x29, 0x8b, 0xf0, 0x72, 0xff, 0x8e, 0xd0, 0x93, 0x23, 0xd0, 0x9b, + 0x62, 0x7d, 0x27, 0x8f, 0xef, 0x74, 0x6d, 0x84, 0xbe, 0x0c, 0x13, 0xd8, 0x26, 0xb7, 0xcc, 0xde, 0xb5, 0x27, 0x63, + 0x88, 0x65, 0x72, 0xe7, 0x95, 0x77, 0xf7, 0xf8, 0x96, 0xec, 0xdf, 0x82, 0xa7, 0xa8, 0x25, 0x7f, 0xb3, 0xf0, 0x86, + 0xb5, 0x6a, 0x78, 0x7c, 0x47, 0x4f, 0x5b, 0x8d, 0x78, 0x7c, 0x47, 0xa2, 0xf0, 0x86, 0x5d, 0xd2, 0x53, 0x76, 0x45, + 0xe8, 0x79, 0xbf, 0x7f, 0xd6, 0xef, 0xcb, 0x7e, 0xff, 0x1f, 0x71, 0x18, 0xc6, 0xc3, 0x82, 0xec, 0x4b, 0x7a, 0xb7, + 0x3f, 0xe1, 0x4f, 0xc8, 0x2c, 0xd4, 0xcd, 0x57, 0x0b, 0xce, 0xaa, 0xbc, 0x55, 0xae, 0x3b, 0x0a, 0xd6, 0x0a, 0x77, + 0x4c, 0x3d, 0x9d, 0xd0, 0x1b, 0x56, 0xd0, 0x53, 0x16, 0x93, 0xe8, 0x1a, 0x5a, 0x71, 0x3e, 0x2b, 0xa2, 0x1b, 0x7a, + 0xca, 0xce, 0x66, 0x71, 0x74, 0x4a, 0x4f, 0x58, 0x3e, 0x9c, 0x40, 0xde, 0xd3, 0xe1, 0x0d, 0xd9, 0x3f, 0x21, 0x51, + 0x78, 0xa2, 0x7f, 0xdf, 0xd1, 0x4b, 0x1e, 0x9e, 0x50, 0xaf, 0x9a, 0x13, 0x62, 0xaa, 0x6f, 0xd4, 0x7e, 0x42, 0x22, + 0x7f, 0x30, 0x4f, 0xac, 0x3d, 0xcd, 0x23, 0x47, 0x1b, 0xd3, 0x32, 0x04, 0x7d, 0x73, 0x19, 0xde, 0x10, 0x32, 0x6d, + 0x8e, 0x1d, 0x0c, 0xe8, 0xec, 0x51, 0x94, 0x10, 0x7a, 0xe3, 0x97, 0x7a, 0x83, 0x63, 0x68, 0x46, 0x48, 0xa5, 0x9d, + 0x62, 0x1a, 0xae, 0x83, 0xd7, 0x1a, 0xac, 0xe3, 0xbc, 0xdf, 0x0f, 0xd7, 0xfd, 0x3e, 0x44, 0xba, 0x2f, 0x66, 0x26, + 0xb6, 0x9b, 0x23, 0x9b, 0xf4, 0x06, 0xb4, 0xff, 0xaf, 0x07, 0x03, 0xe8, 0x8c, 0x57, 0x52, 0x78, 0x33, 0x78, 0xfd, + 0xf8, 0x8e, 0xa8, 0x3a, 0x0a, 0x2a, 0x64, 0x58, 0xd0, 0x37, 0x34, 0x03, 0xc0, 0xaf, 0xd7, 0x83, 0x01, 0x89, 0xcc, + 0x67, 0x64, 0xfa, 0xfa, 0xf0, 0x64, 0x3a, 0x18, 0xbc, 0x36, 0xdb, 0xe4, 0x2d, 0xbb, 0xa7, 0x14, 0x58, 0x7f, 0x67, + 0xfd, 0xfe, 0xdb, 0xa3, 0x98, 0x9c, 0x17, 0x3c, 0xfe, 0x38, 0x6d, 0xb6, 0xe5, 0xad, 0x8b, 0xaa, 0x76, 0xd6, 0xef, + 0xaf, 0xfb, 0xfd, 0x53, 0xc0, 0x2e, 0x9a, 0x39, 0x5f, 0x4f, 0x90, 0xb6, 0xcc, 0x1d, 0x45, 0xd2, 0x24, 0x87, 0xc6, + 0xd0, 0xb6, 0x58, 0xb5, 0x6d, 0xd6, 0x91, 0x81, 0xc5, 0x51, 0xb3, 0xa2, 0xb8, 0x26, 0x51, 0xd8, 0x3b, 0xdb, 0x6e, + 0x4f, 0x19, 0x63, 0x31, 0x01, 0xe9, 0x87, 0xff, 0xfa, 0xb4, 0x6e, 0xc4, 0x10, 0x13, 0x12, 0x99, 0xcd, 0xcd, 0xd2, + 0x1e, 0x02, 0x11, 0x87, 0x4d, 0xff, 0xde, 0xdc, 0xcb, 0x45, 0xed, 0xf8, 0xd6, 0xdf, 0x01, 0x84, 0x48, 0xb2, 0x90, + 0xcf, 0x70, 0x0c, 0xca, 0x0c, 0x80, 0xcc, 0x23, 0x35, 0xf3, 0x12, 0x40, 0x80, 0xc9, 0x76, 0x3b, 0x1a, 0x8f, 0x27, + 0xb4, 0x60, 0xa3, 0xbf, 0x3d, 0x7b, 0x5c, 0x3d, 0x0e, 0x83, 0x60, 0x90, 0x91, 0x96, 0x9e, 0xc2, 0x2e, 0xd6, 0x6a, + 0x1f, 0x8c, 0xe0, 0x35, 0xfb, 0x78, 0x9d, 0x7d, 0x31, 0xfb, 0x88, 0x84, 0xb5, 0xc1, 0x38, 0x72, 0x91, 0xb6, 0xf4, + 0x76, 0xf7, 0x30, 0x98, 0x5c, 0xa4, 0x9f, 0x61, 0x3b, 0x7d, 0xfe, 0xcd, 0x83, 0xf1, 0x84, 0x83, 0xd1, 0x5d, 0x14, + 0xf4, 0x99, 0xb6, 0xdd, 0x56, 0xfe, 0x25, 0xf0, 0x2d, 0xa6, 0x82, 0x8e, 0xcd, 0xb2, 0x70, 0x83, 0x8a, 0xa8, 0xa3, + 0x65, 0x50, 0xd5, 0xca, 0x76, 0x0e, 0xa8, 0x25, 0x56, 0x65, 0xe2, 0x16, 0x18, 0x86, 0x0c, 0x75, 0xb9, 0xc7, 0xd5, + 0xef, 0xbc, 0x90, 0x06, 0x3e, 0xc3, 0x89, 0x08, 0x3d, 0x6e, 0x8d, 0xfb, 0xdc, 0x9a, 0xf8, 0x0c, 0xb7, 0x56, 0x22, + 0x89, 0x35, 0xb0, 0xa4, 0xe6, 0x72, 0x94, 0xb0, 0xa3, 0x92, 0xf1, 0x59, 0x19, 0x25, 0x34, 0x86, 0x07, 0xc9, 0xc4, + 0x4c, 0x46, 0x09, 0xda, 0x27, 0xba, 0x08, 0x83, 0x7f, 0x01, 0x66, 0x3f, 0xcd, 0xe1, 0xaf, 0x24, 0xd3, 0xe4, 0x10, + 0x02, 0x42, 0x1c, 0x8e, 0x67, 0x71, 0x38, 0x26, 0x51, 0x72, 0x04, 0x4f, 0xf0, 0x5f, 0x11, 0x8e, 0x49, 0xad, 0xef, + 0x30, 0x52, 0x5d, 0x6e, 0x13, 0x06, 0x70, 0x65, 0xe3, 0xd9, 0x24, 0xb2, 0xd2, 0x5d, 0xf9, 0x78, 0x34, 0x7e, 0x46, + 0xa6, 0x71, 0x28, 0x07, 0x09, 0xa1, 0xe0, 0xdd, 0x1b, 0x96, 0xc3, 0x44, 0xc3, 0xb3, 0x01, 0x9b, 0x57, 0x3a, 0x36, + 0x4f, 0xc2, 0x09, 0x08, 0xc3, 0x84, 0x1c, 0xeb, 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, + 0x7d, 0xd1, 0xa0, 0xfc, 0xa6, 0x41, 0xfb, 0x82, 0x0c, 0x26, 0xb4, 0x3c, 0x9a, 0xf0, 0x27, 0x10, 0xc0, 0xa3, 0x11, + 0xf1, 0x4b, 0xe1, 0xc4, 0x40, 0x78, 0x15, 0x64, 0xa0, 0xd2, 0x5a, 0x35, 0x66, 0x64, 0x2b, 0xde, 0x83, 0x30, 0x29, + 0x7b, 0x37, 0x72, 0x9d, 0xa7, 0x10, 0x15, 0x6c, 0x9d, 0x57, 0x7b, 0x97, 0x60, 0xc9, 0x1e, 0x57, 0x10, 0x27, 0x6c, + 0xbd, 0x02, 0xec, 0xdc, 0x47, 0x9b, 0xb2, 0xde, 0x53, 0xdf, 0xed, 0x61, 0xcb, 0xe1, 0x55, 0x25, 0xf7, 0x26, 0xe3, + 0xf1, 0x78, 0xf4, 0x07, 0x1c, 0x1d, 0x40, 0x68, 0x49, 0x64, 0xf8, 0x64, 0x80, 0xc6, 0x5d, 0x57, 0xdc, 0x1b, 0x17, + 0x8a, 0xb2, 0xd2, 0xc9, 0x84, 0x80, 0xf8, 0xd9, 0xf4, 0x0d, 0xf6, 0x15, 0xd7, 0xf1, 0x4f, 0x76, 0x3f, 0x31, 0x2b, + 0x5a, 0xad, 0xd4, 0xd1, 0xbb, 0x93, 0xd3, 0xd7, 0x1f, 0x5e, 0xff, 0xfa, 0xf2, 0xec, 0xf5, 0xdb, 0x57, 0xaf, 0xdf, + 0xbe, 0xfe, 0xf0, 0xaf, 0x07, 0x18, 0x6c, 0xdf, 0x56, 0xc4, 0x8e, 0xbd, 0x77, 0x8f, 0xf1, 0x6a, 0xf1, 0x85, 0xb3, + 0x07, 0xee, 0x16, 0x0b, 0xb0, 0x09, 0x86, 0x5b, 0x10, 0x54, 0x33, 0x1a, 0x95, 0xbe, 0x27, 0x20, 0xa3, 0x51, 0x21, + 0x1b, 0x0f, 0x2b, 0xb6, 0x42, 0x2e, 0xde, 0x31, 0x1c, 0x7c, 0x64, 0x7f, 0x2b, 0xce, 0x84, 0xdb, 0xd1, 0xd6, 0xac, + 0x08, 0xf8, 0x7c, 0xad, 0x45, 0xe5, 0x71, 0x21, 0x6a, 0x6f, 0xdb, 0xe7, 0x90, 0x50, 0x8f, 0xc8, 0x75, 0xf0, 0xbe, + 0x0d, 0xb2, 0xc7, 0x47, 0xde, 0x93, 0xf2, 0x0c, 0xf5, 0x39, 0x1a, 0x3e, 0x6a, 0x3c, 0xa3, 0x13, 0x73, 0x6d, 0x74, + 0xa8, 0x67, 0x05, 0xec, 0x6f, 0x25, 0xc6, 0x86, 0x68, 0x0f, 0x29, 0x62, 0x7d, 0x38, 0xdd, 0xef, 0xee, 0xcd, 0xe8, + 0x7b, 0x38, 0x7e, 0x94, 0x6a, 0x02, 0x69, 0x51, 0xa0, 0x74, 0x65, 0xc8, 0x6d, 0xcf, 0xc2, 0xc2, 0xfc, 0x0c, 0x1b, + 0x04, 0xd0, 0x5e, 0x76, 0x2c, 0x09, 0x34, 0x8b, 0xd7, 0xba, 0xfe, 0x79, 0xf9, 0x32, 0xd1, 0xce, 0x17, 0xdf, 0x42, + 0x88, 0x61, 0xff, 0x8a, 0xd0, 0x98, 0x70, 0x37, 0xc9, 0xee, 0xd2, 0x62, 0xee, 0x55, 0x57, 0x31, 0x1e, 0x77, 0xf7, + 0x5c, 0x29, 0x9a, 0xb7, 0x2e, 0xb0, 0x07, 0x6a, 0x5e, 0xc7, 0x4b, 0x16, 0x02, 0x36, 0xe3, 0xbe, 0x5d, 0x24, 0xce, + 0xef, 0x9d, 0x4e, 0xc8, 0xfe, 0xc1, 0x94, 0x0f, 0x59, 0x49, 0xc5, 0x80, 0x95, 0xf5, 0x0e, 0x35, 0xe7, 0x6d, 0x42, + 0x2e, 0x76, 0x69, 0xb8, 0x18, 0xf2, 0x87, 0x2e, 0x49, 0x1f, 0x78, 0xc3, 0xa1, 0xda, 0x36, 0x17, 0x43, 0x9a, 0x72, + 0xba, 0x4b, 0x65, 0x40, 0x88, 0x74, 0x15, 0x57, 0xa4, 0xd6, 0x47, 0x55, 0xea, 0x24, 0x1d, 0xd7, 0xd9, 0xe6, 0x33, + 0x97, 0x6c, 0x75, 0xbb, 0xf6, 0xaf, 0xd5, 0xed, 0x0b, 0x33, 0x90, 0xbf, 0x3f, 0x11, 0xd5, 0xc4, 0x40, 0x74, 0x01, + 0x15, 0xfc, 0x13, 0xbc, 0x3c, 0x79, 0xa4, 0x15, 0xa0, 0xf7, 0x9d, 0x1d, 0x5d, 0x7b, 0xbc, 0x31, 0x8b, 0xad, 0x25, + 0xce, 0x59, 0xe5, 0x3b, 0xcb, 0xab, 0xb2, 0x15, 0xba, 0x8e, 0x60, 0xbf, 0x87, 0x1d, 0x7d, 0xf7, 0xb6, 0x01, 0x10, + 0xa5, 0xb0, 0x72, 0x67, 0xbf, 0xf0, 0xce, 0x7e, 0x61, 0xcf, 0x7e, 0xbb, 0x09, 0x94, 0x0f, 0x2b, 0xb4, 0xec, 0x95, + 0x14, 0x95, 0x69, 0xf2, 0xb8, 0xa9, 0xcb, 0x42, 0x5a, 0xcc, 0xf7, 0x2d, 0xed, 0x7a, 0x3a, 0xa6, 0x12, 0xd5, 0x23, + 0x3f, 0x60, 0xab, 0xf6, 0x4b, 0xf2, 0xf0, 0x3d, 0xf3, 0x7f, 0xf6, 0x06, 0x79, 0xdf, 0xdd, 0xee, 0xff, 0xe6, 0x42, + 0x07, 0xb7, 0xb5, 0x54, 0x78, 0xea, 0xea, 0xb8, 0xc0, 0xbb, 0x5a, 0xfa, 0xf0, 0x5d, 0xed, 0x5d, 0xa6, 0x97, 0x5d, + 0x05, 0xa8, 0x41, 0x62, 0x7d, 0xc5, 0x8b, 0x2c, 0xa9, 0xad, 0x42, 0xe3, 0x84, 0x43, 0x68, 0x0f, 0xef, 0xe0, 0x02, + 0x39, 0x2c, 0x21, 0xf4, 0x63, 0x65, 0x04, 0x80, 0x3e, 0x8b, 0x7d, 0xc2, 0xc3, 0x8c, 0x0c, 0x7c, 0x89, 0x5f, 0x29, + 0x7d, 0x71, 0xf1, 0xfe, 0x4e, 0x66, 0x82, 0x5e, 0x25, 0x36, 0xbb, 0x94, 0xed, 0x98, 0x1f, 0xfe, 0x17, 0x18, 0x0d, + 0xc2, 0x6b, 0x4b, 0xb6, 0x2f, 0x3a, 0x66, 0xb9, 0x82, 0xa3, 0xb6, 0x74, 0x65, 0x96, 0xad, 0xeb, 0x67, 0x35, 0xcc, + 0xf4, 0x99, 0x72, 0x02, 0xb2, 0x2f, 0xe4, 0xee, 0xa7, 0xba, 0x62, 0x41, 0x8e, 0x26, 0xe3, 0x29, 0x11, 0x83, 0x41, + 0x2b, 0xf9, 0x10, 0x93, 0x87, 0xc3, 0x1d, 0xe6, 0x52, 0xe8, 0x7e, 0x78, 0x7d, 0x80, 0xfa, 0x1a, 0x5b, 0x92, 0x6c, + 0x2a, 0xf6, 0x17, 0x98, 0xc5, 0x02, 0x71, 0x74, 0xf0, 0x8b, 0xf3, 0x05, 0x80, 0x2c, 0xc3, 0x32, 0xd3, 0xc2, 0xa2, + 0x32, 0x55, 0x3e, 0xb2, 0x05, 0x93, 0x87, 0xe3, 0x99, 0xdf, 0x73, 0xc7, 0xe0, 0x10, 0x12, 0x4d, 0xac, 0xf1, 0x8b, + 0x9f, 0x05, 0xe3, 0x38, 0x94, 0x47, 0xb2, 0xf1, 0x5d, 0x49, 0xa2, 0xb1, 0x31, 0x55, 0xd6, 0x57, 0x89, 0x6a, 0x98, + 0x90, 0xc7, 0x05, 0xd9, 0x2f, 0xe8, 0xd2, 0x1f, 0x4b, 0x4c, 0xdf, 0x8f, 0xf7, 0x27, 0x63, 0xf2, 0x38, 0x7e, 0x3c, + 0x31, 0x70, 0xc3, 0x7e, 0x8e, 0x7c, 0xb8, 0x24, 0xfb, 0xcd, 0x2a, 0xc1, 0x14, 0xd5, 0xf4, 0xcc, 0xaf, 0x24, 0x19, + 0x2c, 0x07, 0xe9, 0xe3, 0x56, 0x5e, 0xac, 0x55, 0x8f, 0xf7, 0xfa, 0x90, 0x4f, 0x89, 0x68, 0xdc, 0x18, 0xd6, 0xf4, + 0x2a, 0xfe, 0x53, 0x16, 0x51, 0x29, 0x01, 0x91, 0x10, 0xd4, 0xdb, 0xd9, 0x45, 0x96, 0xc4, 0x22, 0x8d, 0xd2, 0x9a, + 0xd0, 0xf4, 0x88, 0x4d, 0xc6, 0xb3, 0x94, 0xa5, 0x87, 0x93, 0x67, 0xb3, 0xc9, 0xb3, 0xe8, 0x60, 0x1c, 0xa5, 0x83, + 0x01, 0x24, 0x1f, 0x8c, 0xc1, 0xc5, 0x0e, 0x7e, 0xb3, 0x03, 0x18, 0xba, 0x23, 0x64, 0x09, 0x0b, 0x68, 0xda, 0x97, + 0x35, 0x49, 0x0f, 0xe7, 0x85, 0xea, 0x49, 0x7c, 0x4b, 0xd7, 0x9e, 0x83, 0x8b, 0xdf, 0xc2, 0x0b, 0xd7, 0xc2, 0x8b, + 0xdd, 0x16, 0x0a, 0x13, 0x37, 0x45, 0xfe, 0xff, 0xb8, 0x61, 0xdc, 0x77, 0x97, 0x30, 0x8b, 0xeb, 0x3a, 0x1b, 0xad, + 0x0a, 0x59, 0x49, 0xb8, 0x4d, 0x28, 0x51, 0xd8, 0x28, 0x5e, 0xad, 0x72, 0xed, 0x22, 0x36, 0xaf, 0x28, 0x80, 0xbb, + 0x40, 0x9c, 0x62, 0x60, 0xa1, 0x8d, 0x81, 0xdc, 0x27, 0x5e, 0x48, 0x66, 0xd5, 0x3e, 0xe6, 0x1e, 0xf9, 0x67, 0x08, + 0xc6, 0xa8, 0xe2, 0x68, 0x3c, 0x53, 0x58, 0x17, 0x9f, 0x93, 0xf7, 0xfe, 0x1b, 0x47, 0x91, 0x3d, 0x9a, 0x41, 0x4f, + 0x10, 0x39, 0x8f, 0x38, 0x7b, 0x32, 0x79, 0x19, 0xb8, 0x9f, 0xc1, 0x4a, 0x7f, 0xdd, 0x6d, 0xc6, 0xda, 0xf6, 0xe8, + 0x5e, 0x18, 0xa1, 0xe8, 0x27, 0x7c, 0x67, 0xea, 0x05, 0x5c, 0x42, 0x35, 0xb0, 0xeb, 0xcb, 0x4b, 0x5e, 0x02, 0x88, + 0x50, 0x26, 0xfa, 0xfd, 0xde, 0x9f, 0x06, 0x9a, 0xb4, 0xe4, 0xc5, 0x9b, 0x4c, 0x58, 0x67, 0x1c, 0x68, 0x2a, 0x50, + 0xff, 0x8f, 0x95, 0x7d, 0xa6, 0x63, 0x32, 0xf3, 0x1f, 0x87, 0x13, 0x12, 0x35, 0x5f, 0x93, 0xcf, 0x9c, 0xa6, 0x9f, + 0xb9, 0xa2, 0xfd, 0x07, 0x32, 0x73, 0xc3, 0x21, 0x43, 0xfd, 0xa5, 0x63, 0x9e, 0x8c, 0x5e, 0x27, 0x66, 0x47, 0x82, + 0x55, 0x33, 0x88, 0xc2, 0x5e, 0xc0, 0x83, 0xba, 0x96, 0xc5, 0x53, 0x98, 0x7d, 0x50, 0x23, 0x8a, 0x43, 0x36, 0x9e, + 0x85, 0x32, 0x9c, 0x80, 0x7d, 0xef, 0x64, 0x0c, 0xf7, 0x01, 0x19, 0x7e, 0xac, 0x42, 0xec, 0x1c, 0xa4, 0x7d, 0xac, + 0x50, 0x31, 0x01, 0x10, 0x81, 0x90, 0xb7, 0xdf, 0x97, 0x2a, 0x09, 0x5f, 0x97, 0x98, 0x52, 0xa8, 0x0f, 0xfe, 0x13, + 0xa9, 0xba, 0x63, 0xfa, 0xd5, 0xfa, 0xf1, 0x67, 0x42, 0xf1, 0xe9, 0x2e, 0x25, 0xbe, 0x85, 0xe0, 0xce, 0x12, 0x74, + 0x10, 0x15, 0x9a, 0xb1, 0x3d, 0xcc, 0xef, 0x8a, 0xfb, 0xf9, 0x5d, 0xf1, 0xff, 0x8e, 0xdf, 0x15, 0x0f, 0x31, 0x86, + 0x95, 0x85, 0x86, 0x9f, 0x05, 0xe3, 0x20, 0xfa, 0xcf, 0xf9, 0xc4, 0x7b, 0x79, 0xea, 0xab, 0x4c, 0x4c, 0xef, 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, 0x97, + 0x71, 0x91, 0x55, 0xcb, 0xab, 0x2c, 0x41, 0xa6, 0x0b, 0x5e, 0x7c, 0x31, 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, 0xe5, 0x7e, 0x93, 0x81, 0x41, 0xba, 0x34, 0x27, + 0xc2, 0xd8, 0x71, 0x3b, 0x45, 0xda, 0x8a, 0x72, 0xc6, 0xd9, 0x7b, 0x75, 0xa5, 0x00, 0x03, 0xbc, 0xcd, 0x4d, 0x74, + 0x9e, 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, 0xfd, 0x03, 0xb4, 0x36, 0xad, 0x06, 0x7c, 0xff, 0xa0, 0x8e, 0xb2, 0x43, 0xc8, 0x72, 0xe4, 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, 0xa7, 0x7d, 0x19, 0x39, 0xc0, 0xe9, 0x84, + 0x8d, 0xa7, 0xc9, 0xa1, 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, 0xc9, 0xc4, 0x25, 0x78, 0x0c, 0x0f, 0xe0, 0x4b, 0x70, 0x93, 0x4b, 0xd9, + 0x6f, 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, 0xcf, 0xaa, 0x69, 0x68, 0x9d, 0x70, 0x2c, 0x2e, 0x73, 0x88, 0xa2, 0x32, + 0x88, 0xc1, 0x1d, 0xc9, 0xe3, 0xf3, 0x1e, 0x89, 0xf0, 0x82, 0x80, 0x5b, 0x59, 0x2c, 0xc3, 0x15, 0x5d, 0x8e, 0x6e, + 0xe9, 0x7a, 0x74, 0x43, 0xc7, 0x74, 0xf2, 0xf7, 0x31, 0x58, 0x64, 0xeb, 0xd4, 0x3b, 0xba, 0x1e, 0x2d, 0xe9, 0xb7, + 0x63, 0x7a, 0xf0, 0x37, 0x30, 0xe1, 0xc3, 0xc3, 0x84, 0x5e, 0x80, 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, 0xf9, 0xb0, 0x1a, 0x9d, 0x93, 0x66, 0xd3, 0x5f, 0x57, 0xfc, 0xaa, 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, + 0x47, 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, 0x5c, 0x7d, + 0x74, 0xee, 0x57, 0x76, 0x0e, 0x2e, 0xa4, 0x83, 0xc4, 0xbf, 0x4a, 0xe5, 0x69, 0xfb, 0x3a, 0xd8, 0x58, 0x55, 0x74, + 0xc3, 0x6f, 0xab, 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, 0x43, 0x44, 0x65, 0x28, 0x6f, 0xf0, 0x4f, 0x0c, 0x9e, 0xcc, 0x56, 0x69, 0x98, 0x8c, 0xee, 0x68, 0x3c, 0x5a, + 0x22, 0x1c, 0x0c, 0x5b, 0xa7, 0x0a, 0x6f, 0xfd, 0x02, 0xd2, 0x6f, 0x69, 0x3c, 0xba, 0xa1, 0xa9, 0xb5, 0x39, 0x35, + 0x50, 0x57, 0xbd, 0x31, 0xbd, 0x8d, 0xe0, 0xf5, 0x5d, 0xb4, 0xa4, 0xb0, 0x95, 0x8e, 0xf3, 0xec, 0x52, 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, 0x07, 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, 0xde, 0x3f, 0xd8, 0x99, 0xef, 0x62, 0xf6, 0x69, 0xbf, 0x8c, 0xc6, 0xca, 0x82, + 0x37, 0xb7, 0xc4, 0x6e, 0xc9, 0xc6, 0xd3, 0xe5, 0x61, 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, 0xc4, 0x47, 0x0c, 0x5c, 0x14, 0xe9, 0xb8, 0x9d, + 0xae, 0x88, 0x8b, 0xdd, 0x63, 0x16, 0xe2, 0x24, 0x61, 0xae, 0x59, 0x36, 0x64, 0x55, 0x84, 0x09, 0xba, 0x30, 0x30, + 0xcb, 0x1b, 0xb2, 0x6a, 0xff, 0x00, 0x22, 0xb5, 0xda, 0x32, 0x56, 0x5d, 0x65, 0x7c, 0x0b, 0x40, 0xd6, 0x8c, 0xb1, + 0x83, 0xbf, 0x8d, 0x67, 0xea, 0x9b, 0x28, 0xe4, 0x47, 0x07, 0x7f, 0x83, 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, 0xc1, 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, 0x0d, 0x70, 0x32, 0x9b, 0xdb, 0x68, 0x49, 0xef, 0xa2, 0x94, 0xde, 0x44, 0x6b, 0xba, + 0x8c, 0x2e, 0x8c, 0x89, 0x71, 0x52, 0xc3, 0x39, 0x00, 0xad, 0x02, 0x48, 0x3c, 0xf5, 0xeb, 0x3d, 0x4f, 0xaa, 0x70, + 0x49, 0x53, 0x70, 0x1b, 0xf6, 0xed, 0x33, 0xaf, 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, 0xdb, 0x28, + 0xa1, 0x77, 0x51, 0xee, 0x91, 0xb5, 0x65, 0xcd, 0xe4, 0xf4, 0x2c, 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, 0x9e, 0x16, 0x6d, 0x6f, 0x57, + 0x18, 0x4d, 0x30, 0xf6, 0x44, 0xfb, 0x3c, 0x52, 0x8e, 0xe2, 0x22, 0x09, 0xb3, 0xd1, 0xad, 0x3a, 0xcf, 0x69, 0x36, + 0xba, 0xd3, 0xbf, 0x2a, 0x3a, 0xa6, 0xdf, 0xe9, 0x80, 0x36, 0x4a, 0xfa, 0xd6, 0x71, 0x36, 0xa0, 0xf5, 0x62, 0x69, + 0xfc, 0xaf, 0xe5, 0xe8, 0x96, 0xca, 0xd1, 0x9d, 0x6f, 0x49, 0x35, 0x99, 0x16, 0x87, 0x02, 0x0d, 0xa9, 0x3a, 0xbf, + 0x2f, 0x80, 0x9f, 0x2b, 0x8d, 0xef, 0xb4, 0xf9, 0xde, 0x6b, 0xff, 0x79, 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, 0xda, 0xd4, 0xe8, 0x15, 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, 0xd7, 0x91, 0xe6, 0xf1, 0x16, 0x1d, 0x3e, 0xbb, 0xd6, 0x12, 0x73, 0x27, 0x51, + 0xf1, 0x7c, 0x0a, 0xd8, 0xea, 0x45, 0x76, 0xa5, 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, 0xdf, 0x3f, 0x18, 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, 0xb9, 0xdf, 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, 0x17, + 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, 0xf8, + 0x3e, 0x03, 0x89, 0xa9, 0x27, 0x79, 0xa8, 0x1a, 0xcd, 0xd1, 0xc9, 0xb3, 0x38, 0xd5, 0xf8, 0xfc, 0x4a, 0x76, 0xd6, + 0xbc, 0x5b, 0x8d, 0x29, 0xfe, 0x23, 0x75, 0xfb, 0xce, 0x65, 0x68, 0xa2, 0xbf, 0x96, 0x07, 0x2d, 0x85, 0x05, 0xc7, + 0x6d, 0xe3, 0xaf, 0xdf, 0x66, 0x0e, 0x31, 0x2c, 0x5d, 0x0e, 0x6f, 0x42, 0x87, 0xee, 0xae, 0xb2, 0x33, 0xd7, 0x07, + 0xd4, 0xa9, 0x8b, 0x75, 0x1b, 0x50, 0xb2, 0xe4, 0xdd, 0x3a, 0x3d, 0xb1, 0xd2, 0x77, 0xfb, 0xe1, 0xce, 0x3c, 0x6a, + 0x76, 0x77, 0xbb, 0x9d, 0x90, 0xb6, 0x7d, 0x30, 0xde, 0x97, 0xb0, 0x10, 0xe7, 0x1d, 0xb6, 0xf7, 0x73, 0x58, 0x3d, + 0xe6, 0x83, 0xdf, 0x71, 0x9c, 0x61, 0xf4, 0x33, 0x65, 0xe8, 0xf3, 0xaa, 0x90, 0x57, 0xaa, 0x53, 0xbe, 0xd0, 0xad, + 0x65, 0xea, 0xfd, 0x36, 0x7e, 0xdb, 0x0a, 0x10, 0xe3, 0x75, 0xc5, 0x4a, 0xf1, 0x86, 0x56, 0x18, 0xd7, 0xc0, 0x6d, + 0x72, 0xa8, 0xa5, 0x5a, 0x20, 0xea, 0xf2, 0x93, 0xc7, 0x3c, 0x32, 0xea, 0x4c, 0xf8, 0xee, 0x31, 0xf7, 0xa5, 0x6b, + 0xbb, 0x4d, 0xfc, 0x5c, 0xd3, 0xf6, 0x77, 0x07, 0xba, 0xa3, 0x75, 0x0f, 0x37, 0xcf, 0xe6, 0xe7, 0x91, 0xf9, 0x62, + 0x80, 0xcd, 0xda, 0x65, 0x5c, 0x76, 0x0c, 0xf7, 0xbd, 0xe9, 0xc1, 0x58, 0x40, 0x20, 0x31, 0x43, 0x2f, 0x03, 0x17, + 0xb8, 0xc0, 0x5d, 0x61, 0xc0, 0x10, 0xd7, 0xb4, 0xe4, 0x4c, 0x5b, 0xd9, 0xfa, 0xc8, 0xdb, 0xa8, 0x10, 0xac, 0xeb, + 0x8e, 0x9b, 0x24, 0x87, 0xe0, 0x84, 0x2d, 0xf7, 0xbe, 0xf6, 0xda, 0x19, 0xfe, 0x73, 0x20, 0x9c, 0x5b, 0xa2, 0x67, + 0xd4, 0xf6, 0x58, 0xab, 0x7b, 0x0d, 0xaf, 0x72, 0x17, 0x79, 0xd6, 0x6f, 0xe6, 0xa5, 0x61, 0x5f, 0xf0, 0x5a, 0x0a, + 0x0e, 0x8d, 0xed, 0x56, 0xb8, 0xc5, 0xe2, 0x1d, 0xad, 0x56, 0xd6, 0xda, 0x6a, 0xaf, 0x95, 0x8a, 0xde, 0xbf, 0xe6, + 0x38, 0x71, 0x96, 0xc2, 0xf6, 0xc3, 0x87, 0x0b, 0x76, 0x4d, 0x00, 0x83, 0x16, 0x93, 0x05, 0x4a, 0x50, 0xc9, 0x5a, + 0xd5, 0x6e, 0xa7, 0xc4, 0x2f, 0xf7, 0x8b, 0x2e, 0xb3, 0x9d, 0xc7, 0xaf, 0x9b, 0xb4, 0xcf, 0x7c, 0x8e, 0x7e, 0x98, + 0xdf, 0x59, 0x27, 0x25, 0x67, 0x18, 0xd7, 0xf2, 0xff, 0xab, 0xe8, 0x65, 0x91, 0xa5, 0xd1, 0xc6, 0xf0, 0x60, 0x36, + 0xd4, 0xa6, 0x0f, 0x8d, 0x51, 0xb9, 0x65, 0xa3, 0x88, 0x68, 0x75, 0x0b, 0x82, 0x19, 0xc5, 0x7d, 0x89, 0x36, 0xaf, + 0x54, 0x59, 0x78, 0x87, 0xcf, 0x6c, 0xf4, 0x86, 0xed, 0x09, 0xa1, 0x7c, 0xf7, 0xb4, 0x30, 0xab, 0x96, 0x8a, 0x06, + 0xdb, 0x25, 0xbc, 0x8b, 0x51, 0xa5, 0x9f, 0x30, 0xd9, 0xb2, 0x60, 0xaa, 0xff, 0xdf, 0x17, 0x59, 0xda, 0xa6, 0xe8, + 0xc0, 0x74, 0x36, 0x7d, 0x3a, 0xe9, 0x06, 0xd7, 0x19, 0xb0, 0x88, 0x60, 0x4b, 0x85, 0xe3, 0x51, 0x6a, 0x37, 0x48, + 0x98, 0x08, 0x6e, 0xa2, 0x5e, 0x76, 0xb4, 0x4c, 0xc9, 0xaa, 0x80, 0xe7, 0x57, 0xae, 0x32, 0x1d, 0x47, 0x43, 0xbf, + 0x7f, 0x95, 0x9a, 0xd0, 0xaf, 0xd4, 0x4b, 0x55, 0x9c, 0x87, 0x51, 0x75, 0xa8, 0x30, 0x46, 0x4b, 0x9a, 0xc2, 0x31, + 0x98, 0x5d, 0x84, 0x29, 0x5e, 0xce, 0x36, 0x09, 0xfb, 0x82, 0x81, 0x5c, 0x6a, 0x83, 0x7a, 0x4d, 0x89, 0xd6, 0xac, + 0xbd, 0x99, 0x53, 0x42, 0x2f, 0x58, 0xe9, 0xdf, 0x85, 0xd6, 0x20, 0x50, 0x94, 0xcd, 0x94, 0xe9, 0xb9, 0x6e, 0xe7, + 0x05, 0x4d, 0x68, 0x41, 0x57, 0xa4, 0x06, 0x7d, 0xaf, 0x93, 0xb3, 0xa3, 0x93, 0x9d, 0x99, 0xf5, 0x98, 0x15, 0xc3, + 0xc9, 0x34, 0x86, 0x6b, 0x5a, 0xec, 0xae, 0x69, 0xcb, 0xe6, 0x8d, 0xab, 0xb1, 0x71, 0x1a, 0xb4, 0x0b, 0xa4, 0x6d, + 0x9a, 0xdb, 0x4f, 0x3d, 0x6e, 0x7f, 0x5d, 0xb3, 0xe5, 0xb4, 0xb7, 0xde, 0x6e, 0x7b, 0x29, 0xd8, 0x88, 0x7a, 0x7c, + 0xfc, 0x5a, 0x49, 0xd7, 0x2d, 0x97, 0x9f, 0xc2, 0xb3, 0xc7, 0xd7, 0x2f, 0x7d, 0x70, 0x39, 0x5a, 0xb5, 0xb9, 0xfb, + 0xe5, 0x2e, 0xb2, 0xdc, 0x17, 0x0d, 0x2d, 0xd7, 0x33, 0xd4, 0x24, 0xcf, 0x46, 0x7b, 0x87, 0x5a, 0xb0, 0x9c, 0x75, + 0x13, 0x9e, 0x18, 0xec, 0xd8, 0xab, 0xc6, 0xe6, 0xa8, 0xcc, 0x25, 0xab, 0x41, 0x02, 0x7d, 0x92, 0x67, 0x9a, 0xfe, + 0x41, 0x86, 0xf9, 0xe8, 0x96, 0xe6, 0x80, 0x2b, 0x56, 0xd9, 0x4b, 0x06, 0xa9, 0xab, 0xf6, 0x12, 0x57, 0xbe, 0xc2, + 0x21, 0xd9, 0xe0, 0x93, 0x61, 0xaa, 0x3e, 0xbb, 0xe4, 0xc1, 0xff, 0xdb, 0xaa, 0x55, 0x7a, 0x6e, 0x92, 0x1b, 0x8e, + 0x7f, 0x9d, 0xb4, 0x7d, 0x4c, 0x0c, 0x12, 0xf0, 0xd4, 0x2e, 0x86, 0x6a, 0x54, 0x15, 0xb1, 0x28, 0x73, 0x13, 0x73, + 0xec, 0xde, 0xae, 0xa1, 0x83, 0x32, 0xf8, 0x75, 0xc3, 0x27, 0xe6, 0x0e, 0x6c, 0x05, 0x3a, 0x3a, 0xd1, 0x5c, 0x86, + 0x99, 0xb9, 0x0c, 0xd3, 0xae, 0xad, 0x02, 0xc3, 0xab, 0xb6, 0x4a, 0xa2, 0x5c, 0x8d, 0x7a, 0xdc, 0xcc, 0x52, 0xb3, + 0x17, 0x79, 0xf7, 0x9a, 0xf4, 0x24, 0xfe, 0x74, 0xe9, 0xc9, 0xeb, 0x61, 0x40, 0xe4, 0x97, 0x2c, 0x0d, 0xd7, 0x28, + 0x08, 0x4e, 0xad, 0x76, 0x20, 0xcd, 0x47, 0x80, 0xcc, 0x8f, 0xd3, 0xf0, 0x9d, 0x16, 0xe7, 0x90, 0x8d, 0xd2, 0x38, + 0xb1, 0xa5, 0x51, 0x0f, 0xc1, 0x9d, 0xf7, 0x8a, 0xc7, 0x10, 0xf8, 0xf0, 0x03, 0x6e, 0x06, 0x15, 0xdd, 0x96, 0x98, + 0x28, 0x6d, 0x1e, 0x75, 0xcb, 0x47, 0x0d, 0xa1, 0x92, 0x95, 0xe1, 0x25, 0xd0, 0xde, 0x1d, 0x81, 0x51, 0xe5, 0x04, + 0x32, 0xc3, 0x62, 0xff, 0x60, 0x98, 0x2a, 0x41, 0xd1, 0x50, 0x0e, 0x97, 0x28, 0x07, 0xc4, 0x24, 0x10, 0x18, 0x15, + 0x83, 0x54, 0x57, 0xa6, 0x5e, 0x0c, 0x52, 0x7d, 0xab, 0x22, 0xf5, 0x59, 0x16, 0x56, 0x54, 0xb7, 0x88, 0x8e, 0xe9, + 0x50, 0xd2, 0xa5, 0xd9, 0xa9, 0xb9, 0x96, 0x5e, 0xa8, 0xe5, 0xf8, 0x5c, 0xa7, 0xc1, 0x28, 0x9e, 0xba, 0x14, 0xfd, + 0x56, 0xed, 0x67, 0xff, 0x2d, 0xa6, 0xd4, 0x88, 0x4d, 0xed, 0x2d, 0x62, 0x58, 0xb5, 0x1f, 0xb2, 0x2a, 0x07, 0xed, + 0x2e, 0x28, 0x1b, 0x2b, 0xe3, 0x3c, 0xdf, 0x08, 0x66, 0x0e, 0xda, 0xc6, 0xaa, 0xe9, 0x43, 0x6f, 0xc4, 0xa8, 0xbd, + 0x31, 0xd5, 0xb8, 0x27, 0xf0, 0xd3, 0x06, 0x4d, 0xf7, 0x22, 0xcf, 0x51, 0x8f, 0xbc, 0xfb, 0x9f, 0x39, 0xb2, 0x33, + 0xf9, 0x2c, 0x96, 0x49, 0xdd, 0x3e, 0x26, 0xc1, 0x42, 0xd5, 0x31, 0xba, 0x70, 0x23, 0x53, 0xda, 0xcf, 0x9d, 0xe9, + 0x47, 0x3c, 0x93, 0x87, 0xed, 0xd0, 0xa8, 0x2f, 0x0d, 0x6b, 0x49, 0x11, 0xf5, 0x05, 0xbd, 0x35, 0xd5, 0xd1, 0x01, + 0xf5, 0x3a, 0x02, 0xab, 0x2b, 0xda, 0xa0, 0x06, 0x60, 0x32, 0xae, 0x6d, 0x6d, 0x3e, 0x07, 0x53, 0x5b, 0x55, 0xc1, + 0x33, 0xba, 0x2b, 0x94, 0xee, 0x4d, 0xea, 0xba, 0x35, 0xc4, 0x16, 0x30, 0x20, 0x70, 0xa3, 0xa7, 0xa6, 0x3f, 0x68, + 0xa2, 0x02, 0xd0, 0xa0, 0x71, 0x3b, 0xd3, 0x39, 0x12, 0xfd, 0x4e, 0x6d, 0xda, 0x66, 0xaa, 0x57, 0x95, 0x0f, 0xa0, + 0xe2, 0xcf, 0xd2, 0xd9, 0x85, 0x19, 0xb1, 0x00, 0xc6, 0x3d, 0x70, 0xa6, 0x7a, 0xc7, 0x19, 0x58, 0x4f, 0xe4, 0x79, + 0x56, 0xf2, 0x44, 0x0a, 0x98, 0x11, 0x79, 0x75, 0x25, 0x05, 0x0c, 0x83, 0x1a, 0x00, 0xb4, 0x68, 0x2e, 0xa3, 0x09, + 0x7f, 0x52, 0xd3, 0xfb, 0xf2, 0xf0, 0x27, 0x3a, 0xd7, 0x37, 0xe3, 0x1a, 0x0c, 0x95, 0xd7, 0x15, 0xdf, 0xc9, 0xf4, + 0x0d, 0x7f, 0xea, 0x65, 0x5a, 0xca, 0x75, 0xb1, 0x93, 0xe5, 0xc9, 0x37, 0xfc, 0x99, 0xce, 0x73, 0xf0, 0xb4, 0xa6, + 0x69, 0x7c, 0xb7, 0x93, 0xe5, 0xef, 0xdf, 0x3c, 0xb5, 0x79, 0x9e, 0x8c, 0x6b, 0x7a, 0xc3, 0xf9, 0x47, 0x97, 0x69, + 0xa2, 0xab, 0x1a, 0x3f, 0xfd, 0xbb, 0xcd, 0xf5, 0xb4, 0xa6, 0x57, 0x52, 0x54, 0xcb, 0x9d, 0xa2, 0x0e, 0xbe, 0x39, + 0xf8, 0x3b, 0xff, 0xc6, 0x74, 0xef, 0xa0, 0xa6, 0x7f, 0xad, 0xe3, 0xa2, 0xe2, 0xc5, 0x4e, 0x71, 0x7f, 0xfb, 0xfb, + 0xdf, 0x9f, 0xda, 0x8c, 0x4f, 0x6b, 0x7a, 0xc7, 0xe3, 0x8e, 0xb6, 0x4f, 0x9e, 0x3d, 0xe5, 0x7f, 0xab, 0x6b, 0xfa, + 0x0b, 0xf3, 0x83, 0xa3, 0x1e, 0x67, 0x9e, 0x1e, 0x3e, 0x91, 0x4d, 0xd4, 0x80, 0xa1, 0x87, 0x06, 0x90, 0x4b, 0xab, + 0xa6, 0xb9, 0xc7, 0x2b, 0x17, 0xdc, 0xbe, 0xcf, 0xe2, 0x34, 0x5e, 0xc1, 0x41, 0xb0, 0x41, 0xe3, 0xac, 0x02, 0x38, + 0x55, 0xe0, 0x3d, 0xa3, 0x92, 0x66, 0xa5, 0xfc, 0x27, 0xe7, 0x1f, 0x61, 0xd0, 0x10, 0xd2, 0x46, 0x45, 0x06, 0x3a, + 0x59, 0xe9, 0xc8, 0x46, 0xe8, 0xbf, 0xd9, 0x8c, 0x83, 0xe3, 0xc3, 0xe8, 0xf5, 0xfb, 0x61, 0xc1, 0x44, 0x58, 0x10, + 0x42, 0xff, 0x0c, 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, 0x86, + 0x87, 0x12, 0x7c, 0xe2, 0x01, 0x3e, 0x7a, 0x4c, 0x66, 0x5c, 0x5d, 0x6b, 0x4f, 0x2e, 0xc2, 0x82, 0x06, 0xba, 0xed, + 0x10, 0x74, 0x20, 0xf2, 0x5f, 0x80, 0xa7, 0xc0, 0xc0, 0x87, 0x85, 0x5d, 0xca, 0x5d, 0x7f, 0xf5, 0x5f, 0x0c, 0xeb, + 0xe8, 0xc2, 0x8f, 0xfe, 0x62, 0x5d, 0xd8, 0x33, 0x32, 0x95, 0x87, 0xe5, 0x70, 0x32, 0x1d, 0x0c, 0xa4, 0x8b, 0xe3, + 0x76, 0x9c, 0xcd, 0x7f, 0x99, 0xcb, 0xc5, 0x02, 0x75, 0xdf, 0x38, 0xaf, 0x33, 0xfd, 0x37, 0xd2, 0xce, 0x07, 0x6f, + 0x8e, 0x7f, 0x3b, 0x3b, 0x3d, 0x7e, 0x05, 0xce, 0x07, 0x1f, 0x5e, 0x7e, 0xff, 0xf2, 0xbd, 0x0a, 0xee, 0xae, 0xe6, + 0xbc, 0xdf, 0x77, 0x52, 0x9f, 0x90, 0x0f, 0x2b, 0xb2, 0x1f, 0xc6, 0x8f, 0x0b, 0x65, 0xf4, 0x40, 0x0e, 0x99, 0x85, + 0x42, 0x86, 0x2a, 0x6a, 0xfb, 0xbb, 0x1c, 0x4e, 0x3c, 0x30, 0x8b, 0xbb, 0x86, 0x08, 0xd7, 0x6f, 0xb9, 0x0d, 0xb2, + 0x26, 0x8f, 0xbc, 0x7e, 0x70, 0x32, 0x95, 0x8e, 0x2d, 0x2c, 0x18, 0x94, 0x0d, 0x6d, 0x3a, 0xce, 0xe6, 0xc5, 0xc2, + 0xb6, 0xcb, 0x2d, 0x90, 0x51, 0x9a, 0x5d, 0x5c, 0x84, 0x0a, 0xba, 0xfa, 0x08, 0x34, 0x00, 0xa6, 0x51, 0x85, 0x6b, + 0x11, 0x9f, 0xf9, 0xe5, 0x47, 0x63, 0xaf, 0x79, 0xb7, 0xa8, 0x7b, 0x32, 0xcd, 0xaa, 0x1a, 0x03, 0x3a, 0x98, 0x50, + 0xee, 0x06, 0xdd, 0x04, 0x93, 0x51, 0x6d, 0xf9, 0x65, 0x5e, 0x2d, 0x4c, 0x73, 0xdc, 0x30, 0x54, 0x5e, 0xc9, 0x6b, + 0xd9, 0x40, 0x64, 0x20, 0x19, 0x86, 0x3d, 0x1a, 0xa3, 0x48, 0x7d, 0x6f, 0xd7, 0x3b, 0x7e, 0x93, 0x4b, 0x88, 0xa6, + 0x98, 0x81, 0x74, 0xfe, 0x58, 0x28, 0xe7, 0x72, 0xc9, 0xf8, 0x5c, 0x2c, 0x8e, 0xc0, 0xed, 0x7c, 0x2e, 0x16, 0x11, + 0x06, 0xe5, 0xcb, 0x20, 0x56, 0x09, 0xd8, 0xbd, 0x38, 0x08, 0xdf, 0x4e, 0x68, 0x03, 0xbb, 0x81, 0x24, 0x1b, 0x94, + 0x76, 0xa5, 0x21, 0xca, 0x9d, 0xf2, 0x68, 0x83, 0xc8, 0x43, 0xac, 0x9a, 0x57, 0x6d, 0x4f, 0x36, 0x73, 0x31, 0xc1, + 0x55, 0x16, 0x33, 0x39, 0x8d, 0x0f, 0x59, 0x31, 0x8d, 0xa1, 0x94, 0x38, 0x4d, 0xc3, 0x98, 0x4e, 0xa8, 0x20, 0x24, + 0x61, 0x7c, 0x1e, 0x2f, 0x68, 0x82, 0x52, 0x82, 0x10, 0x42, 0x7e, 0x8c, 0xd0, 0x36, 0x07, 0x96, 0xbc, 0xdd, 0x7e, + 0x9e, 0x7e, 0x6e, 0xc7, 0x70, 0x19, 0x15, 0xa1, 0x1b, 0x74, 0xd6, 0xf0, 0x6f, 0x44, 0x05, 0x8d, 0xb1, 0x62, 0x08, + 0x02, 0x5e, 0x60, 0x54, 0xc2, 0x82, 0xc4, 0xac, 0x82, 0x28, 0x02, 0xe5, 0x3c, 0x5e, 0xb0, 0x82, 0x36, 0x6d, 0x4e, + 0x63, 0x6d, 0x12, 0xd4, 0x73, 0x58, 0x6a, 0x7b, 0x52, 0xa9, 0x10, 0x7b, 0x7c, 0x26, 0xa2, 0x6b, 0x6d, 0x68, 0x00, + 0x28, 0x50, 0x4a, 0x2e, 0x7e, 0xf3, 0xe5, 0x1e, 0x6e, 0x0a, 0xfa, 0x9f, 0x6d, 0x4c, 0xb4, 0xb3, 0x5c, 0x1d, 0x7a, + 0xf3, 0x05, 0x8d, 0xf3, 0x1c, 0x42, 0xb1, 0x19, 0x04, 0x72, 0x91, 0x55, 0x10, 0xd1, 0xe2, 0x2e, 0x30, 0x21, 0xe1, + 0xa0, 0x4d, 0xbf, 0x40, 0x6a, 0x43, 0x4c, 0xae, 0x3c, 0x31, 0xb0, 0xdb, 0x2a, 0x41, 0xc0, 0x91, 0x9e, 0x67, 0x9f, + 0x9a, 0x18, 0x6b, 0x9a, 0x9a, 0x99, 0x78, 0x1b, 0x0a, 0xd1, 0xa0, 0x05, 0xd1, 0x0c, 0xde, 0x3f, 0x57, 0x1c, 0xaf, + 0x3a, 0xf0, 0x03, 0xde, 0xb9, 0x38, 0xf3, 0x6a, 0xe6, 0x11, 0x39, 0xf5, 0x51, 0x8e, 0xe8, 0x97, 0x3c, 0xac, 0x46, + 0x3a, 0x19, 0x63, 0x25, 0x71, 0xd0, 0xdb, 0x60, 0xc1, 0x9c, 0xd0, 0x15, 0x0f, 0x2d, 0x1f, 0xff, 0x0a, 0x99, 0x8c, + 0x92, 0x1a, 0x2b, 0xba, 0xd2, 0x62, 0xc4, 0x79, 0x0d, 0xb3, 0x34, 0x59, 0xd1, 0xc5, 0x42, 0x93, 0x66, 0xa1, 0x4c, + 0x03, 0x7c, 0x02, 0x2d, 0x46, 0xee, 0xa1, 0xa6, 0x0d, 0x84, 0x86, 0xdd, 0x21, 0xe0, 0x23, 0xf7, 0xd0, 0xe1, 0xff, + 0xe7, 0xd9, 0x05, 0x22, 0xed, 0xcd, 0x4d, 0x64, 0x3c, 0x52, 0x37, 0x70, 0x50, 0x8c, 0x8f, 0x7d, 0x33, 0xf1, 0x0b, + 0x67, 0xf4, 0x21, 0xa9, 0x7c, 0x87, 0x0f, 0x96, 0x3f, 0xde, 0xd4, 0xcc, 0xca, 0x08, 0xd6, 0xc3, 0x76, 0x8b, 0x0b, + 0xa2, 0xed, 0x02, 0x48, 0x3d, 0xe3, 0xd5, 0xc2, 0x37, 0x5e, 0x8d, 0xef, 0x31, 0x5e, 0x75, 0x67, 0x6a, 0x98, 0x93, + 0x0d, 0xea, 0xb3, 0x94, 0x3c, 0x3f, 0x47, 0x99, 0x60, 0xd3, 0xe5, 0xac, 0xa4, 0x2a, 0x95, 0xd0, 0x5e, 0xec, 0x67, + 0x8c, 0x6f, 0x09, 0xc6, 0x59, 0x71, 0x18, 0x09, 0x54, 0xa5, 0x92, 0x3a, 0xec, 0x15, 0xa0, 0x1e, 0x83, 0xf7, 0x06, + 0x43, 0xd4, 0xc8, 0xd8, 0x4d, 0x1b, 0x08, 0x0d, 0x8d, 0xf5, 0x68, 0xcf, 0x5a, 0x8f, 0x6e, 0xb7, 0x95, 0xf1, 0xb7, + 0x93, 0xeb, 0x22, 0x41, 0x54, 0x61, 0x35, 0x9a, 0x00, 0x6f, 0x9a, 0xd8, 0xdb, 0x92, 0x53, 0x5a, 0x60, 0xf8, 0xec, + 0x3f, 0xc3, 0xd2, 0xa9, 0x24, 0x4a, 0x32, 0x2b, 0xa3, 0x81, 0x3b, 0x07, 0x5f, 0xc4, 0x15, 0xac, 0x01, 0x88, 0xe4, + 0x88, 0x1e, 0xae, 0x7f, 0x86, 0xd2, 0x65, 0x96, 0x64, 0x26, 0x21, 0x33, 0x17, 0x69, 0x3b, 0xeb, 0x60, 0xe2, 0x4c, + 0x6a, 0xbd, 0xb1, 0x90, 0x43, 0x83, 0xfc, 0x00, 0xca, 0x10, 0x87, 0x4f, 0x3e, 0x98, 0x50, 0xa9, 0x42, 0xa9, 0x36, + 0xba, 0xd9, 0x0d, 0xbc, 0xf2, 0x21, 0xbb, 0xe2, 0x65, 0x15, 0x5f, 0xad, 0x8c, 0x25, 0x31, 0x67, 0xf7, 0xb9, 0xed, + 0x51, 0x61, 0x5e, 0xbd, 0x7d, 0xf9, 0xfd, 0x71, 0xe3, 0xd5, 0x2e, 0xe2, 0x68, 0x08, 0xb6, 0x15, 0x63, 0x8c, 0xde, + 0xe2, 0xd3, 0x60, 0xa2, 0x5c, 0x23, 0xd0, 0xbb, 0x14, 0xf4, 0xdb, 0x5f, 0xea, 0x09, 0x78, 0xc5, 0xf5, 0xf2, 0x4b, + 0x3e, 0x02, 0x96, 0xa8, 0xd0, 0xb3, 0xc2, 0xdc, 0xac, 0xcc, 0xee, 0xed, 0x56, 0x64, 0xa6, 0x5d, 0x69, 0x64, 0x20, + 0x5e, 0x6d, 0x87, 0xb1, 0x70, 0xe9, 0x9a, 0x6e, 0x07, 0xbb, 0x5a, 0x7a, 0x96, 0xc8, 0xdb, 0x6d, 0x09, 0x1d, 0xb2, + 0x03, 0xee, 0xbd, 0x8c, 0x6f, 0xe1, 0x65, 0xe9, 0x75, 0xb3, 0x19, 0x3c, 0x01, 0xcc, 0x84, 0x0b, 0x67, 0x59, 0x1c, + 0x33, 0x9e, 0x84, 0x2a, 0x36, 0x57, 0x43, 0xe4, 0xad, 0x08, 0xad, 0xd9, 0x5f, 0xa1, 0x18, 0x81, 0xdd, 0xc9, 0xe9, + 0xc7, 0x6c, 0x35, 0x5b, 0x02, 0x6a, 0xfe, 0x55, 0x26, 0x80, 0xe6, 0xda, 0xb5, 0x60, 0x9b, 0x42, 0x9b, 0xeb, 0xfa, + 0x79, 0xbc, 0x8a, 0x13, 0x50, 0xdd, 0x80, 0xb7, 0xc8, 0x9d, 0x16, 0x5d, 0x19, 0x74, 0x51, 0xfa, 0x40, 0x39, 0x96, + 0x14, 0x3a, 0xfa, 0xde, 0x13, 0xea, 0xdc, 0x33, 0x80, 0x4b, 0x1a, 0x35, 0x4f, 0xb5, 0x94, 0xb1, 0x00, 0x58, 0xe8, + 0x60, 0xa6, 0xc8, 0x56, 0x74, 0x6b, 0x30, 0x29, 0xe0, 0xad, 0x01, 0xfe, 0x10, 0x59, 0xa5, 0xee, 0x8a, 0x65, 0x58, + 0x7a, 0xf6, 0xd7, 0xfd, 0x7e, 0xec, 0xd9, 0x5f, 0x5f, 0x68, 0x5a, 0x17, 0xb7, 0x1b, 0x40, 0x6a, 0x0c, 0x20, 0x72, + 0xac, 0x07, 0xc2, 0x44, 0x14, 0x6b, 0xfa, 0xfe, 0x1d, 0x9b, 0x2c, 0x0a, 0x84, 0x7e, 0xa7, 0x5e, 0x4f, 0x4a, 0x02, + 0x3a, 0xb5, 0x8a, 0x1d, 0x0d, 0xb4, 0xd9, 0x07, 0x04, 0x44, 0xf5, 0x33, 0xb2, 0xf9, 0x42, 0x39, 0x17, 0xab, 0xf0, + 0xe1, 0x63, 0x0a, 0x01, 0x85, 0x3b, 0x6a, 0x74, 0xde, 0x86, 0x48, 0xa0, 0xac, 0x50, 0xc4, 0x9a, 0x17, 0x6b, 0x49, + 0xc8, 0x7c, 0xbc, 0x40, 0xc1, 0x95, 0x03, 0x76, 0xe5, 0x6c, 0x32, 0x2c, 0x23, 0xce, 0xc2, 0xfb, 0xbf, 0x99, 0x2c, + 0x08, 0x6a, 0xae, 0xfc, 0x40, 0x8e, 0x3b, 0x99, 0x1a, 0x7b, 0xaa, 0x51, 0x83, 0x60, 0x32, 0x82, 0xc0, 0x70, 0xc3, + 0x2f, 0xf8, 0xf8, 0x60, 0x41, 0x40, 0x45, 0x66, 0xcd, 0x42, 0xcc, 0x8b, 0xc3, 0x27, 0x80, 0x1a, 0x33, 0x3a, 0x78, + 0x36, 0xe5, 0x0c, 0x0e, 0x51, 0x3a, 0x06, 0x19, 0xad, 0x80, 0xdf, 0x42, 0xfd, 0x6e, 0x9d, 0xf8, 0x3e, 0xf4, 0xab, + 0xa0, 0x17, 0x31, 0x30, 0x1c, 0xd1, 0x64, 0x3f, 0xe4, 0x83, 0xc9, 0x00, 0xb4, 0x25, 0xde, 0xee, 0x6b, 0x69, 0xc5, + 0xcd, 0xe9, 0xd2, 0xe9, 0xfe, 0x49, 0x9b, 0x20, 0x89, 0x54, 0xb2, 0x52, 0x11, 0x03, 0x08, 0x65, 0xa9, 0xb6, 0xc9, + 0x12, 0x2c, 0x2b, 0xcc, 0x92, 0xe6, 0x06, 0x25, 0x71, 0x77, 0x33, 0x70, 0x8c, 0x9a, 0x75, 0x1c, 0x96, 0x2d, 0x37, + 0x6a, 0x80, 0xcf, 0x49, 0x58, 0x61, 0x6f, 0x38, 0x33, 0xe9, 0x9d, 0xe9, 0x70, 0x75, 0xcc, 0xd9, 0x1b, 0x8e, 0x60, + 0x1c, 0x09, 0xde, 0x78, 0xe8, 0x92, 0x69, 0xa8, 0xc8, 0x94, 0x71, 0x30, 0xed, 0x01, 0xee, 0x3d, 0x07, 0xe3, 0x30, + 0x36, 0xa8, 0x2c, 0xa9, 0x4f, 0xbd, 0xbb, 0x10, 0x08, 0xd2, 0x5a, 0x2f, 0xf3, 0x19, 0x9e, 0x9e, 0x11, 0xca, 0xfe, + 0x90, 0xc3, 0x17, 0x60, 0x47, 0x41, 0x8e, 0x26, 0xfc, 0xd9, 0xe3, 0xdd, 0x40, 0x55, 0x7c, 0x10, 0xec, 0xc5, 0x22, + 0xdd, 0x0b, 0x06, 0x02, 0x7e, 0x15, 0x7c, 0xaf, 0x92, 0x72, 0xef, 0x22, 0x2e, 0xf6, 0xe2, 0x55, 0x5c, 0x54, 0x7b, + 0x37, 0x59, 0xb5, 0xdc, 0x33, 0x1d, 0x02, 0x68, 0xde, 0x60, 0x10, 0x0f, 0x82, 0xbd, 0x60, 0x50, 0x98, 0xa9, 0x5d, + 0xb1, 0xb2, 0x71, 0x9c, 0x99, 0x10, 0x65, 0x41, 0x33, 0x40, 0x58, 0xe3, 0x34, 0x00, 0x3e, 0x75, 0xcd, 0x52, 0x7a, + 0x81, 0xe1, 0x06, 0xc4, 0x74, 0x0d, 0x7d, 0x00, 0x1e, 0x79, 0x4d, 0x63, 0x58, 0x02, 0x17, 0x83, 0x01, 0x59, 0x43, + 0xe4, 0x82, 0x35, 0xb5, 0x41, 0x1c, 0xc2, 0xb5, 0xb2, 0xd3, 0xde, 0x05, 0x66, 0xda, 0x6e, 0x01, 0x51, 0x79, 0x42, + 0xfa, 0x7d, 0xfb, 0x0d, 0xf5, 0x2f, 0xd8, 0x4b, 0xb0, 0xbf, 0x2a, 0xaa, 0x30, 0x91, 0x4a, 0xf3, 0x7d, 0xc9, 0x8e, + 0x06, 0x2a, 0xe2, 0xf0, 0x8e, 0x23, 0x45, 0x1b, 0x95, 0xcb, 0xb2, 0x27, 0xcb, 0x86, 0xaf, 0xc4, 0x15, 0x77, 0x7e, + 0x5c, 0x95, 0x94, 0x79, 0x95, 0xad, 0x14, 0xfb, 0x37, 0xe3, 0x9a, 0xfb, 0x03, 0xeb, 0xcf, 0xe6, 0x2b, 0xb8, 0xb6, + 0x7a, 0xef, 0x9a, 0x5c, 0x23, 0x72, 0x96, 0x50, 0x2e, 0xa9, 0x6d, 0x1e, 0xde, 0xd2, 0xf7, 0xf9, 0xd5, 0xb7, 0x99, + 0x4e, 0xe3, 0xb3, 0x0a, 0x0b, 0x17, 0xa2, 0x15, 0xc1, 0xa1, 0x21, 0x17, 0xcd, 0x23, 0xc0, 0x5c, 0xfb, 0x6c, 0x05, + 0x05, 0xa9, 0xcf, 0x2a, 0xf4, 0x6e, 0x85, 0x84, 0x57, 0x9a, 0x5d, 0x7a, 0x18, 0x48, 0x19, 0xb7, 0x87, 0x96, 0x30, + 0x69, 0x79, 0x11, 0xde, 0x7b, 0xcd, 0x4d, 0xee, 0x45, 0x88, 0xd1, 0x8b, 0x3c, 0x3b, 0x01, 0x63, 0xdd, 0x25, 0x3b, + 0x1b, 0x9e, 0xf8, 0x0d, 0xcf, 0x59, 0x8b, 0x46, 0xd3, 0x25, 0x4b, 0xfa, 0xfd, 0x18, 0x4c, 0xbc, 0x53, 0x96, 0xc3, + 0xaf, 0x7c, 0x41, 0xd7, 0x0c, 0x30, 0xc5, 0xe8, 0x05, 0x24, 0xa4, 0x88, 0x44, 0xb2, 0x56, 0x27, 0xc9, 0x67, 0xba, + 0x0b, 0xc0, 0xe8, 0x17, 0xb3, 0x34, 0x5a, 0xde, 0x6b, 0x66, 0x81, 0xe4, 0x19, 0xfa, 0xae, 0x83, 0xed, 0x8d, 0x7d, + 0x90, 0x72, 0x7e, 0x28, 0xa6, 0x83, 0x01, 0x27, 0x1a, 0x6e, 0xbc, 0x54, 0xe2, 0x5a, 0xdd, 0xe2, 0x8e, 0x61, 0x2c, + 0xf5, 0x6d, 0x11, 0x83, 0x03, 0x76, 0xd1, 0xca, 0x6e, 0x1f, 0x60, 0x5f, 0x39, 0xde, 0xa5, 0xca, 0xee, 0xf4, 0x98, + 0x69, 0x2e, 0x5b, 0x4d, 0x3a, 0xa9, 0xb8, 0x9f, 0xc8, 0x37, 0xb9, 0x83, 0x2e, 0x97, 0x63, 0xcd, 0x5b, 0x0e, 0x40, + 0x45, 0x3f, 0x52, 0x54, 0xf7, 0x0b, 0x1c, 0x61, 0x1e, 0xac, 0xdb, 0x7c, 0xb2, 0x6f, 0x0a, 0x1c, 0x22, 0x4f, 0xda, + 0x68, 0x0a, 0xe8, 0xde, 0xc5, 0xe3, 0xae, 0x7e, 0x5b, 0xba, 0x0b, 0x94, 0x68, 0xa7, 0xe2, 0x86, 0x1f, 0x13, 0x75, + 0x3a, 0xd3, 0x86, 0xd0, 0xbf, 0x32, 0xe2, 0xfe, 0xd2, 0xb8, 0x8a, 0x37, 0xbd, 0xcb, 0x67, 0x1c, 0xea, 0xec, 0x86, + 0x50, 0x00, 0xae, 0xda, 0xd3, 0xa9, 0x1b, 0x43, 0x7a, 0xa5, 0x44, 0xb7, 0xc1, 0xc1, 0xee, 0xf5, 0x19, 0x47, 0xd1, + 0x8f, 0x51, 0x23, 0xdf, 0x44, 0xe2, 0xb1, 0x1c, 0xc4, 0x8f, 0x0b, 0xba, 0x8c, 0xc4, 0xe3, 0x62, 0x10, 0x3f, 0x96, + 0x75, 0xbd, 0x7b, 0xae, 0xdc, 0xdf, 0x47, 0xe4, 0x59, 0x77, 0xf6, 0x52, 0x09, 0x1b, 0x03, 0xcf, 0xae, 0x05, 0x84, + 0x53, 0xf0, 0x44, 0xb6, 0x96, 0x3e, 0x74, 0x6e, 0xf7, 0xb1, 0x65, 0x92, 0x20, 0xe8, 0x79, 0x9b, 0x4d, 0xa2, 0xd8, + 0xd9, 0xe6, 0xd1, 0x87, 0x53, 0x20, 0xa1, 0xdb, 0x6d, 0xb3, 0xae, 0xd6, 0x80, 0x62, 0x1a, 0x8e, 0xf9, 0x7e, 0x31, + 0xba, 0xf1, 0xdd, 0xf5, 0xf7, 0x8b, 0xd1, 0x92, 0x0c, 0x27, 0x66, 0xf2, 0xe3, 0xa3, 0xf1, 0x2c, 0x8e, 0x26, 0x75, + 0xc7, 0x69, 0xa1, 0xf1, 0x4f, 0xbd, 0x5b, 0x28, 0x02, 0xa7, 0x62, 0x04, 0x47, 0x4e, 0x85, 0x72, 0x52, 0x6a, 0x60, + 0xf8, 0xef, 0x55, 0x3b, 0xda, 0xb4, 0x37, 0x71, 0x95, 0x2c, 0x33, 0x71, 0xa9, 0xc3, 0x87, 0xeb, 0xe8, 0xe2, 0x36, + 0xa0, 0x9d, 0x77, 0x99, 0x76, 0xfc, 0x3a, 0x69, 0xd0, 0x13, 0x57, 0x33, 0x03, 0x6e, 0xdd, 0x8f, 0xd0, 0x0c, 0x81, + 0xd1, 0xf2, 0xfc, 0x1d, 0x62, 0x6e, 0xff, 0xaa, 0x6c, 0x7e, 0x15, 0xed, 0x73, 0x64, 0xa4, 0x6c, 0x93, 0x91, 0x0a, + 0x8c, 0x30, 0xa5, 0x48, 0xe2, 0x2a, 0x84, 0x40, 0xf6, 0x5f, 0x52, 0x5c, 0x8b, 0xa5, 0xf7, 0x1a, 0x84, 0x09, 0xb6, + 0x0b, 0xda, 0xaf, 0x6e, 0xe7, 0xb6, 0xd2, 0x62, 0x8f, 0xd4, 0xf7, 0xb9, 0xb3, 0x5d, 0xd1, 0xe4, 0xef, 0xcb, 0x06, + 0xb4, 0x01, 0x44, 0x79, 0x5f, 0x1f, 0x95, 0xc0, 0xc9, 0x88, 0x1b, 0x4a, 0x8c, 0x5e, 0xd0, 0xd5, 0x89, 0xdc, 0xb3, + 0x53, 0xf3, 0xa6, 0x62, 0xa6, 0xe2, 0xca, 0x37, 0x7b, 0xe6, 0x3f, 0x18, 0x0a, 0x2a, 0xc0, 0xc0, 0xdb, 0x9c, 0xf1, + 0xe8, 0x40, 0x77, 0x63, 0x74, 0x5a, 0xb0, 0x59, 0x50, 0x97, 0x75, 0xd3, 0xc6, 0x83, 0x46, 0x1c, 0x14, 0xc5, 0xaa, + 0x50, 0x23, 0xe1, 0x89, 0x40, 0xc0, 0x94, 0x5d, 0xf1, 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, + 0x3d, 0x01, 0xbb, 0xd8, 0x60, 0x25, 0xcb, 0x00, 0x7c, 0x2f, 0xe8, 0x66, 0x25, 0xcb, 0x48, 0xaa, 0xee, 0xc7, 0x35, + 0x96, 0xa0, 0xd2, 0x0a, 0x95, 0x96, 0xd4, 0x58, 0x10, 0xf8, 0xaa, 0xea, 0xf2, 0x21, 0xd9, 0x55, 0xa0, 0x9e, 0x3a, + 0x6a, 0xc0, 0x29, 0x50, 0x55, 0x60, 0x41, 0x12, 0x54, 0x86, 0xae, 0x0a, 0x4c, 0x2b, 0x30, 0xcd, 0x54, 0xe1, 0xa2, + 0xcc, 0x0e, 0xa5, 0x59, 0x2f, 0xf9, 0x2c, 0x1e, 0x84, 0xc9, 0x30, 0x26, 0x8f, 0x11, 0x6a, 0x7f, 0x3f, 0x8f, 0x62, + 0x2d, 0x97, 0x5c, 0x39, 0xbf, 0xf8, 0x9b, 0xcf, 0xd8, 0xeb, 0x9e, 0x61, 0xb0, 0x00, 0x67, 0x69, 0x7b, 0x95, 0x89, + 0x77, 0xb2, 0x15, 0x1c, 0x07, 0xb3, 0x28, 0x87, 0x55, 0x4f, 0x8e, 0x68, 0x2e, 0x72, 0xed, 0x5d, 0x84, 0xc8, 0x41, + 0x66, 0x8f, 0x01, 0x76, 0x23, 0x7c, 0x1d, 0x5a, 0x9b, 0x5b, 0x5d, 0x21, 0xfe, 0x46, 0x89, 0xc4, 0x4f, 0x52, 0x7e, + 0x5c, 0xaf, 0x54, 0xae, 0xca, 0xe0, 0xb1, 0xea, 0x66, 0xf0, 0x4c, 0xfb, 0x1e, 0x6b, 0xff, 0xd6, 0x76, 0x73, 0xbc, + 0xf7, 0xe0, 0x41, 0xeb, 0x7f, 0xeb, 0x49, 0x08, 0xed, 0x95, 0x93, 0xd4, 0x1d, 0x35, 0x7a, 0x66, 0xb2, 0x46, 0x54, + 0xc2, 0xd4, 0xee, 0x54, 0x8e, 0x81, 0x9a, 0x0e, 0xe0, 0x5a, 0xa2, 0x26, 0xe8, 0x49, 0xc1, 0xc6, 0x70, 0xc4, 0x59, + 0x1c, 0xb4, 0xc3, 0x18, 0xc5, 0xcb, 0xb9, 0x12, 0x2f, 0xe7, 0x47, 0x8c, 0x03, 0xb4, 0x16, 0x20, 0xd5, 0x6b, 0xd8, + 0xcf, 0x5c, 0xc1, 0x02, 0x9b, 0x3b, 0xdf, 0x81, 0x05, 0x32, 0xc4, 0xc9, 0xe6, 0x38, 0xd9, 0xe3, 0x5a, 0xcf, 0xbd, + 0xc0, 0xc7, 0x49, 0xbd, 0xf0, 0xea, 0x2a, 0xdb, 0x75, 0x2d, 0x59, 0x39, 0x2f, 0x06, 0x13, 0x08, 0xca, 0x52, 0xce, + 0x8b, 0xe1, 0x64, 0x41, 0x73, 0xf8, 0xb1, 0x68, 0xa0, 0x43, 0x2c, 0x07, 0x09, 0x5c, 0x3a, 0x7b, 0x0c, 0x78, 0x43, + 0xa9, 0xc5, 0xdd, 0x58, 0x47, 0x8e, 0x75, 0x14, 0xfb, 0x61, 0x0c, 0xb8, 0xb2, 0x4e, 0xe0, 0x7d, 0xff, 0xf5, 0xb1, + 0x09, 0xc8, 0xaa, 0x5d, 0xe1, 0xd5, 0x28, 0x77, 0x5d, 0x69, 0xf4, 0x25, 0xa5, 0x27, 0xbc, 0xe0, 0xa9, 0x64, 0xbb, + 0xed, 0x19, 0x38, 0x5b, 0xe2, 0x21, 0xf1, 0x8e, 0x11, 0xbd, 0x98, 0x36, 0x32, 0x73, 0x02, 0x67, 0xb6, 0xbb, 0x6c, + 0x63, 0x7e, 0xec, 0x00, 0x07, 0x8b, 0x20, 0x24, 0x6e, 0x08, 0xc3, 0xc4, 0x8e, 0xca, 0xa1, 0x16, 0xc2, 0x75, 0x2d, + 0xbc, 0x8e, 0xd3, 0x32, 0x06, 0x17, 0x69, 0x6d, 0x9b, 0x78, 0x0f, 0x5d, 0xf7, 0xfc, 0x98, 0x5b, 0x1d, 0xa3, 0x2d, + 0xa4, 0xdf, 0x8e, 0x4e, 0xef, 0x39, 0x0c, 0x40, 0xd3, 0x83, 0x59, 0xd5, 0x3e, 0x93, 0xb8, 0x39, 0xed, 0x04, 0x21, + 0x11, 0x88, 0xa2, 0x74, 0x46, 0x98, 0xfe, 0x9d, 0xe6, 0xb2, 0x8a, 0x56, 0x0f, 0xf2, 0xcc, 0x21, 0xcf, 0x42, 0x6f, + 0x7b, 0xd0, 0xaa, 0xb9, 0x1b, 0x8c, 0x13, 0xb7, 0xdb, 0x3b, 0xff, 0x6f, 0x59, 0xd7, 0x56, 0x6b, 0xc4, 0xe3, 0x76, + 0xf5, 0x83, 0xc6, 0x5e, 0xed, 0xa9, 0x18, 0x30, 0x2b, 0xe9, 0x9d, 0x51, 0x25, 0x2f, 0x32, 0x5e, 0xe2, 0x49, 0xb5, + 0x6a, 0xf8, 0x78, 0xdf, 0x64, 0x23, 0xf3, 0x40, 0xa6, 0x80, 0x78, 0x7e, 0x93, 0x1a, 0xf5, 0x71, 0x8a, 0x12, 0xf0, + 0x77, 0x3a, 0xbe, 0x11, 0xfd, 0x68, 0x5f, 0x5c, 0xf2, 0xea, 0xe4, 0x46, 0x98, 0x17, 0x2f, 0xac, 0xce, 0x9f, 0xbe, + 0x29, 0x7c, 0xe8, 0x70, 0xd4, 0xde, 0x41, 0x91, 0x25, 0x13, 0x47, 0x13, 0x23, 0x6b, 0x13, 0xb3, 0x8f, 0x0a, 0x2e, + 0x26, 0xaa, 0xd0, 0xb3, 0xce, 0x9e, 0x30, 0x05, 0xe8, 0x1b, 0xc7, 0xa8, 0x64, 0x0c, 0x0b, 0x06, 0xea, 0x34, 0x25, + 0x44, 0x0f, 0xc5, 0x0c, 0xe3, 0x15, 0x03, 0x28, 0x4c, 0xa1, 0x40, 0x14, 0x9d, 0x7d, 0x38, 0xd0, 0x84, 0x7e, 0xff, + 0x26, 0xd5, 0x19, 0x68, 0x59, 0x4f, 0x0b, 0x10, 0xd5, 0x41, 0xb4, 0x55, 0x5e, 0x84, 0x3f, 0x2e, 0x69, 0x99, 0xd1, + 0xa5, 0xa0, 0xa9, 0xa0, 0x49, 0x46, 0x2f, 0xb8, 0x12, 0x15, 0x5f, 0x08, 0xa6, 0x68, 0xbb, 0x21, 0xec, 0x3f, 0x36, + 0xe8, 0x7a, 0x2b, 0xd6, 0x1a, 0xda, 0x9d, 0x20, 0x23, 0x34, 0x5f, 0xe8, 0x20, 0x64, 0xa8, 0x9c, 0x84, 0xae, 0x55, + 0x1a, 0xaf, 0xc0, 0x25, 0xd3, 0x6c, 0xb4, 0x8c, 0xcb, 0x30, 0xb0, 0x5f, 0x05, 0x16, 0x93, 0x03, 0x93, 0x4e, 0xd7, + 0xe7, 0xcf, 0xe5, 0xd5, 0x4a, 0x0a, 0x2e, 0x2a, 0x05, 0xd1, 0x6f, 0x70, 0xdf, 0x4d, 0x5c, 0x75, 0xd6, 0xac, 0x95, + 0x3e, 0xf4, 0xad, 0xcf, 0xda, 0xb8, 0x2f, 0x0c, 0x8e, 0xc1, 0xce, 0x47, 0xc4, 0x40, 0x1a, 0x54, 0xba, 0xc5, 0xa1, + 0x09, 0xd0, 0xa5, 0x43, 0x0a, 0x59, 0x32, 0x95, 0xa9, 0x12, 0x54, 0x7c, 0xe3, 0xf7, 0x52, 0x56, 0xa3, 0xbf, 0xd6, + 0xbc, 0xb8, 0x3b, 0xe5, 0x39, 0xc7, 0x31, 0x0a, 0x92, 0x58, 0x5c, 0xc7, 0x65, 0x40, 0x7c, 0xcb, 0xab, 0xe0, 0x20, + 0x35, 0x61, 0x63, 0x76, 0xaa, 0x46, 0xad, 0x57, 0x81, 0xbe, 0x32, 0xca, 0x37, 0x06, 0x43, 0x13, 0x51, 0x05, 0x7d, + 0xaf, 0xd5, 0x3d, 0xad, 0x6e, 0x58, 0x40, 0xfc, 0xb9, 0xd2, 0x0b, 0xb5, 0x5e, 0x37, 0x63, 0x6e, 0x98, 0x08, 0x41, + 0xa3, 0x27, 0xf5, 0xa2, 0xf6, 0xdc, 0xd2, 0x54, 0x64, 0xdc, 0x68, 0x93, 0xf3, 0x4b, 0x90, 0xf1, 0x39, 0x73, 0xa1, + 0x49, 0x5d, 0x53, 0x05, 0x55, 0x18, 0x6d, 0x6e, 0x1b, 0xe9, 0xf4, 0x0e, 0xdc, 0xd9, 0x8c, 0xd9, 0x91, 0x76, 0x69, + 0xac, 0x69, 0xc1, 0xcb, 0x95, 0x14, 0x25, 0x84, 0x71, 0xee, 0x8d, 0xe9, 0x55, 0x9c, 0x89, 0x2a, 0xce, 0xc4, 0x71, + 0xb9, 0xe2, 0x49, 0xf5, 0x1e, 0x6e, 0x71, 0xca, 0xea, 0xa6, 0x2e, 0xe1, 0x4a, 0x97, 0xec, 0x61, 0x30, 0x35, 0x15, + 0xf7, 0xd8, 0x71, 0x92, 0xd5, 0x1f, 0xd1, 0x52, 0x62, 0x2c, 0x54, 0x5d, 0x7c, 0x7c, 0x5e, 0xca, 0x7c, 0x5d, 0x81, + 0x76, 0xf7, 0xa2, 0x8a, 0x0e, 0x9e, 0xae, 0x6e, 0xa7, 0xea, 0x06, 0x13, 0x3d, 0x3d, 0x58, 0xdd, 0xf6, 0xb2, 0xab, + 0x95, 0x2c, 0xaa, 0x58, 0x54, 0x53, 0x85, 0x48, 0x96, 0xc4, 0x79, 0x12, 0x4e, 0xc6, 0xe3, 0xaf, 0xf6, 0x86, 0x7b, + 0x90, 0x81, 0x4c, 0x3f, 0x0d, 0x95, 0xcb, 0xd1, 0x70, 0x32, 0x1e, 0x4f, 0xa5, 0xba, 0xdb, 0x45, 0xa3, 0x49, 0x8d, + 0xf5, 0x0c, 0x13, 0x3d, 0x33, 0x23, 0x7e, 0xbb, 0x8a, 0x45, 0x0a, 0xf1, 0xeb, 0x74, 0xf1, 0x07, 0x4f, 0xc7, 0x8d, + 0xf2, 0xed, 0xa7, 0xcf, 0xea, 0x3f, 0x6a, 0x13, 0xd6, 0xda, 0xb4, 0xfb, 0xf9, 0x1f, 0x87, 0x6a, 0xbe, 0x8f, 0x0e, + 0xf7, 0xf5, 0x8f, 0x3f, 0xea, 0x7a, 0xfa, 0xa6, 0x08, 0xe7, 0xff, 0x0a, 0xd5, 0x7c, 0x1e, 0x17, 0x45, 0x7c, 0x57, + 0x43, 0x24, 0x4f, 0xe1, 0xbc, 0x49, 0xa8, 0xb7, 0x0d, 0xe8, 0x01, 0x99, 0x5e, 0x08, 0x06, 0xdf, 0xbc, 0xaf, 0xc2, + 0x80, 0x97, 0xab, 0x21, 0x17, 0x55, 0x56, 0xdd, 0x0d, 0x31, 0x4f, 0x80, 0x9f, 0x1a, 0xde, 0xec, 0x79, 0x61, 0x88, + 0xcd, 0x45, 0xc1, 0xf9, 0x27, 0x1e, 0x2a, 0xe3, 0xe8, 0x31, 0x1a, 0x47, 0x8f, 0xa9, 0x1a, 0x8c, 0xc9, 0x37, 0x54, + 0x77, 0x66, 0xf2, 0x0d, 0x98, 0x20, 0x65, 0xed, 0x6f, 0x94, 0x71, 0x62, 0x34, 0xa6, 0xd7, 0xaf, 0xf2, 0x6c, 0x05, + 0x4c, 0xf0, 0x52, 0xff, 0xa8, 0x09, 0x7d, 0xcf, 0xdb, 0xd9, 0x47, 0xa3, 0xd1, 0xf3, 0x82, 0x8e, 0x46, 0xa3, 0x8f, + 0x59, 0x4d, 0xe8, 0x4a, 0x74, 0xbc, 0x7f, 0xcf, 0xe9, 0xb9, 0x4c, 0xef, 0xa2, 0x20, 0xa0, 0xcb, 0x2c, 0x4d, 0xb9, + 0x50, 0x65, 0x9d, 0xa6, 0xed, 0xbc, 0xaa, 0x85, 0x08, 0xfc, 0xa3, 0xdb, 0x88, 0x10, 0x44, 0x84, 0x9e, 0xec, 0xf4, + 0x6c, 0x34, 0x1a, 0x9d, 0xa6, 0xa6, 0x5a, 0xc7, 0x90, 0xbf, 0x41, 0xf3, 0x01, 0x67, 0x97, 0x0f, 0xd6, 0x37, 0x26, + 0xda, 0xc9, 0xfe, 0x7f, 0x0f, 0x67, 0xf3, 0xf1, 0xf0, 0xdb, 0xd1, 0xe2, 0xf1, 0x3e, 0x0d, 0x02, 0x1f, 0xb4, 0x3a, + 0xd4, 0xd6, 0x1c, 0xd3, 0xf2, 0x70, 0x3c, 0x25, 0xe5, 0x80, 0x3d, 0xb5, 0xbe, 0x34, 0x5f, 0x3d, 0x05, 0x24, 0x52, + 0x14, 0xa1, 0x06, 0x4e, 0xfa, 0x87, 0x57, 0x91, 0xd7, 0x02, 0xf0, 0xd1, 0x6c, 0x24, 0x03, 0xa3, 0x05, 0x1c, 0x47, + 0x50, 0x5e, 0x6d, 0x4d, 0x23, 0x7a, 0x8c, 0x65, 0x26, 0x2a, 0xe8, 0x78, 0x5a, 0xde, 0x64, 0x55, 0xb2, 0xc4, 0xc0, + 0x46, 0x71, 0xc9, 0x83, 0xaf, 0x82, 0xa8, 0x64, 0x07, 0xcf, 0xa6, 0x0a, 0xde, 0x17, 0x93, 0x52, 0x7e, 0x09, 0x89, + 0xdf, 0x8e, 0x11, 0x02, 0x95, 0x68, 0x8f, 0x45, 0xac, 0xf1, 0x55, 0x2e, 0x63, 0xf0, 0xe0, 0x2c, 0x35, 0xcf, 0x62, + 0x4f, 0x02, 0x6b, 0x7f, 0xd1, 0x6a, 0x8e, 0x84, 0xe6, 0x84, 0x92, 0xc9, 0xfd, 0x92, 0xca, 0xaf, 0x26, 0xe8, 0x15, + 0x04, 0x6e, 0xd5, 0x11, 0x1c, 0x77, 0xd6, 0xb2, 0x41, 0x2f, 0x9f, 0x94, 0xed, 0xcf, 0xff, 0x77, 0x49, 0x17, 0x83, + 0x7d, 0x37, 0x34, 0x27, 0xda, 0x7d, 0xb5, 0x42, 0x46, 0xa9, 0x0a, 0x9f, 0xa7, 0xc4, 0x1a, 0x9f, 0x72, 0x76, 0xb4, + 0x31, 0xdd, 0x19, 0x55, 0x45, 0x76, 0x15, 0x12, 0xdd, 0x2b, 0x07, 0x8a, 0x19, 0x44, 0xd9, 0x08, 0xd7, 0x0f, 0x58, + 0x8b, 0x78, 0x9d, 0xbc, 0xe6, 0x45, 0x95, 0x25, 0xea, 0xfd, 0x75, 0xe3, 0x3d, 0x50, 0x03, 0xd5, 0xa0, 0x77, 0x05, + 0x83, 0x79, 0x3e, 0x29, 0x00, 0xb4, 0xb3, 0xe4, 0xc5, 0x35, 0xf7, 0xe9, 0x46, 0x10, 0xd4, 0xae, 0x99, 0x97, 0x8d, + 0x60, 0x13, 0xf0, 0xd5, 0xbb, 0x02, 0x30, 0x37, 0x42, 0x90, 0x9a, 0x42, 0x28, 0x1c, 0xb8, 0xc0, 0x57, 0x55, 0x91, + 0x9d, 0xaf, 0x2b, 0x8e, 0xc1, 0x3e, 0xbc, 0xb8, 0x89, 0xca, 0x09, 0x8f, 0x87, 0x01, 0xfe, 0x08, 0xa8, 0x0a, 0xb8, + 0x61, 0x3c, 0xec, 0xe0, 0x85, 0xfa, 0xe5, 0xde, 0xa8, 0x3d, 0xc2, 0xde, 0xa4, 0x21, 0x04, 0xd7, 0xc1, 0x87, 0x00, + 0x96, 0x14, 0xa1, 0x27, 0x78, 0xaa, 0x86, 0xc1, 0x45, 0x9e, 0xad, 0x74, 0x52, 0x35, 0xea, 0x68, 0x3e, 0x94, 0xda, + 0x91, 0x1c, 0x50, 0x2f, 0x3d, 0xc6, 0xf4, 0x42, 0xa5, 0xab, 0xa2, 0x9c, 0x11, 0xca, 0x3b, 0x3d, 0x31, 0x2e, 0x4c, + 0x1f, 0x87, 0xc8, 0x2f, 0xef, 0x0a, 0x15, 0xfa, 0x85, 0x2f, 0x00, 0xfc, 0x0a, 0x6e, 0xf7, 0xbb, 0xf1, 0x5d, 0x54, + 0xf6, 0x33, 0xce, 0xf6, 0xff, 0x7b, 0x1e, 0x0f, 0x3f, 0x8d, 0x87, 0xdf, 0x2e, 0x06, 0xe1, 0xd0, 0xfe, 0x24, 0x8f, + 0x1f, 0xed, 0xd3, 0x57, 0xdc, 0x72, 0x25, 0xb0, 0xf0, 0x1b, 0xc1, 0x6d, 0xd4, 0x4a, 0x08, 0xa2, 0x00, 0x6f, 0x14, + 0x6e, 0x35, 0x4e, 0x00, 0xe0, 0x2f, 0xf8, 0xaf, 0x00, 0x8d, 0x84, 0xdc, 0x45, 0x03, 0xf4, 0x03, 0xea, 0xf7, 0xd1, + 0x93, 0x86, 0x81, 0x1c, 0x88, 0x27, 0x54, 0x0c, 0x14, 0xa2, 0xcb, 0x98, 0x28, 0xd8, 0x5f, 0x9b, 0x7d, 0xbb, 0xed, + 0xb5, 0x25, 0x3f, 0xf8, 0xa5, 0x9f, 0x69, 0x62, 0xe6, 0x1d, 0x6e, 0x28, 0x2b, 0xb9, 0x0a, 0x11, 0x1b, 0x4f, 0xff, + 0xca, 0x19, 0xc4, 0x9a, 0xbc, 0xce, 0xc0, 0x87, 0xc1, 0x7e, 0x31, 0x9e, 0x01, 0xdb, 0x00, 0x77, 0x9c, 0x82, 0x5f, + 0x64, 0xe0, 0xd6, 0x2c, 0x62, 0xbc, 0x60, 0xdb, 0x25, 0xd1, 0xef, 0xf7, 0xf2, 0x2c, 0xcc, 0x35, 0xce, 0x72, 0x5e, + 0x1b, 0xb1, 0x3b, 0xea, 0x84, 0x41, 0xdc, 0xae, 0x87, 0x60, 0xa8, 0x86, 0xa0, 0xe8, 0x68, 0x8b, 0xab, 0xd7, 0xd6, + 0x53, 0x98, 0xde, 0xaa, 0xfa, 0x8a, 0xd1, 0x9f, 0x32, 0x13, 0x58, 0x48, 0xbb, 0xe6, 0x58, 0xd7, 0x1c, 0x23, 0xed, + 0xe9, 0xf7, 0x45, 0x83, 0xfc, 0x74, 0x16, 0x1e, 0x04, 0xaa, 0x54, 0xb9, 0x53, 0x16, 0xe5, 0xb6, 0x34, 0x6f, 0x0c, + 0x6b, 0x9a, 0x67, 0x36, 0xae, 0xcb, 0xac, 0xd7, 0x0b, 0x43, 0x74, 0x68, 0xc4, 0x52, 0xb1, 0x36, 0x08, 0xef, 0x63, + 0x12, 0x46, 0x57, 0x20, 0xab, 0x0b, 0xcf, 0x38, 0x41, 0xbe, 0x0c, 0x4c, 0xd6, 0x54, 0xb5, 0x5e, 0x4e, 0x78, 0x6c, + 0xe4, 0xcb, 0x46, 0xd0, 0x20, 0x2f, 0x29, 0xea, 0x4d, 0xdc, 0x8e, 0x7d, 0xd4, 0x42, 0x6a, 0xdc, 0xd4, 0xd3, 0x9e, + 0x26, 0x15, 0x3d, 0xd6, 0xab, 0xd4, 0x2f, 0xb0, 0x2c, 0xb0, 0xe4, 0x83, 0xd0, 0x9e, 0xa6, 0x15, 0x98, 0xe1, 0xda, + 0x66, 0x30, 0xf4, 0xc3, 0xa1, 0x2d, 0x42, 0x67, 0xd4, 0xb6, 0x84, 0xb0, 0x6d, 0x83, 0xb0, 0xf2, 0x9e, 0xc8, 0x57, + 0x4f, 0x3d, 0x46, 0x38, 0xe4, 0x66, 0x33, 0x8b, 0x06, 0x86, 0xf9, 0x95, 0x6c, 0x36, 0x4f, 0x37, 0xd7, 0x8b, 0x8a, + 0x29, 0x60, 0xbb, 0xad, 0x04, 0xc1, 0xbf, 0x1f, 0xb3, 0x19, 0xfe, 0xcd, 0xfa, 0xfd, 0x5e, 0x88, 0xbf, 0x38, 0x06, + 0xef, 0x99, 0x8b, 0x05, 0xfb, 0x08, 0x32, 0x15, 0x12, 0x61, 0xaa, 0x32, 0x7e, 0x63, 0x15, 0x58, 0xc0, 0x99, 0x0f, + 0x54, 0x2e, 0xcc, 0x64, 0x2f, 0x2f, 0xae, 0x21, 0xc7, 0xad, 0x71, 0xca, 0x46, 0x59, 0xa2, 0x5c, 0x17, 0xb2, 0x51, + 0x9c, 0x67, 0x71, 0xc9, 0xcb, 0xed, 0x56, 0x1f, 0x8e, 0x49, 0xc1, 0x81, 0x3d, 0x55, 0x54, 0xaa, 0x64, 0x1d, 0xa9, + 0x6e, 0xfc, 0x65, 0x58, 0xe0, 0x3e, 0xe5, 0xf3, 0xc2, 0xd0, 0x88, 0x3d, 0xb8, 0xbc, 0x33, 0x75, 0x2b, 0xed, 0x85, + 0x05, 0x34, 0xaf, 0x24, 0x64, 0x83, 0xa9, 0x9e, 0x45, 0x6b, 0xcc, 0xc4, 0xbc, 0x58, 0x40, 0x18, 0x99, 0x62, 0x01, + 0x36, 0x53, 0x5c, 0x80, 0x17, 0x49, 0x0c, 0x30, 0x71, 0x31, 0x99, 0x42, 0x3c, 0x73, 0x55, 0x4e, 0xbc, 0x30, 0xf7, + 0xcb, 0xc4, 0x21, 0x65, 0xc0, 0xab, 0xda, 0xa0, 0x89, 0xd9, 0x86, 0xa3, 0x4e, 0x90, 0x13, 0x93, 0xdf, 0x4f, 0x15, + 0x84, 0xb8, 0x13, 0x47, 0xc2, 0x65, 0xc5, 0x76, 0xe1, 0x65, 0x07, 0x62, 0x8c, 0x1a, 0x9c, 0xf2, 0x33, 0x83, 0xa3, + 0x31, 0x38, 0x37, 0xde, 0x09, 0x52, 0x84, 0x31, 0xd9, 0x48, 0x76, 0x25, 0x43, 0x31, 0x8f, 0x17, 0xa0, 0xac, 0x8b, + 0x17, 0x60, 0x59, 0x63, 0x0c, 0x30, 0x41, 0x5e, 0xc5, 0xbd, 0xd0, 0x4f, 0x14, 0x57, 0x88, 0xf4, 0xac, 0x5c, 0x1f, + 0x15, 0xed, 0xd0, 0x17, 0x78, 0xbd, 0x57, 0xe6, 0xb8, 0x59, 0x8f, 0x05, 0x12, 0x1b, 0x02, 0xc6, 0x46, 0x3a, 0x4d, + 0xb5, 0xd6, 0xbd, 0x31, 0xf3, 0xc0, 0xa7, 0xd9, 0x48, 0xc8, 0xea, 0xec, 0x02, 0x44, 0x28, 0x3e, 0x1a, 0x3c, 0xf2, + 0x8b, 0xb8, 0xb3, 0xcc, 0x5b, 0xdb, 0xa2, 0x92, 0x1d, 0x6d, 0x00, 0xa4, 0x4f, 0x47, 0x8b, 0x52, 0x72, 0x8a, 0x92, + 0xd4, 0x6e, 0x53, 0xc0, 0x4a, 0xf2, 0x17, 0x30, 0x04, 0x1b, 0xdb, 0x13, 0x4e, 0xa7, 0x08, 0xf1, 0x89, 0xa6, 0x88, + 0xac, 0x18, 0x96, 0x14, 0xc7, 0xb6, 0x44, 0xd4, 0x4f, 0x5b, 0x96, 0x1d, 0x0c, 0x13, 0x15, 0xf7, 0x45, 0xea, 0x51, + 0xa2, 0x20, 0xa0, 0x7a, 0xc8, 0x41, 0x62, 0x6b, 0x1b, 0x08, 0x0f, 0xc8, 0x23, 0x7a, 0x63, 0xfd, 0x7d, 0xd6, 0x79, + 0x76, 0xa1, 0x39, 0x2a, 0xd7, 0xbb, 0xc2, 0x8c, 0x11, 0x9e, 0x64, 0x06, 0x26, 0xdf, 0x3b, 0xcf, 0x8c, 0x9a, 0xa2, + 0xe7, 0xe1, 0x93, 0x1d, 0x63, 0x44, 0xba, 0x7b, 0x06, 0xdd, 0x04, 0xaf, 0xea, 0xb0, 0xd1, 0xae, 0x14, 0x84, 0x84, + 0xa9, 0x45, 0x13, 0xb3, 0x9e, 0x35, 0xa0, 0xde, 0x6e, 0x7b, 0x7a, 0xae, 0xee, 0x9f, 0xbb, 0xed, 0xb6, 0x87, 0xdd, + 0x7a, 0x91, 0x76, 0x5b, 0x81, 0x57, 0xea, 0x83, 0xf6, 0xf8, 0x73, 0x37, 0xfe, 0xdc, 0x20, 0x79, 0x94, 0x8e, 0x66, + 0xda, 0xfa, 0x20, 0x1c, 0x6e, 0x7a, 0xd7, 0x68, 0xd2, 0xf7, 0x59, 0x28, 0xe9, 0x4a, 0x34, 0xaa, 0xab, 0x9d, 0x49, + 0xe5, 0x83, 0xeb, 0xff, 0xe1, 0x55, 0x80, 0x47, 0x9c, 0xda, 0xd9, 0xf7, 0x36, 0xa8, 0x68, 0xb4, 0x85, 0x23, 0x45, + 0xe8, 0x01, 0x49, 0xb8, 0xaf, 0x65, 0x2d, 0x6e, 0xf3, 0x34, 0x7b, 0x98, 0x3e, 0xbd, 0x4e, 0x7d, 0xab, 0x7b, 0xb7, + 0xcc, 0x32, 0x73, 0xe0, 0x55, 0x14, 0x07, 0x34, 0xea, 0xa2, 0x7d, 0x57, 0x59, 0x59, 0x82, 0x97, 0x07, 0x5c, 0x9f, + 0x4f, 0xb9, 0x0f, 0x37, 0x77, 0x59, 0x35, 0x37, 0xe9, 0x69, 0x36, 0xcf, 0x16, 0xdb, 0x6d, 0x88, 0x7f, 0xbb, 0x5a, + 0xe4, 0x68, 0xf2, 0x1c, 0x74, 0x78, 0x18, 0xb9, 0x87, 0xe9, 0xc6, 0x79, 0x9b, 0xff, 0x93, 0x68, 0x38, 0x09, 0x1c, + 0x03, 0xbd, 0x98, 0x3d, 0x02, 0x19, 0x8c, 0x71, 0xea, 0x17, 0x33, 0xbd, 0x66, 0x20, 0xfa, 0x96, 0x88, 0x00, 0x47, + 0x17, 0x1b, 0x89, 0x46, 0x16, 0x9c, 0xd4, 0x04, 0x2c, 0x36, 0x6d, 0x79, 0x1f, 0x2c, 0x6d, 0xab, 0x8a, 0x3b, 0x6f, + 0x49, 0x73, 0x5c, 0x07, 0xce, 0xb6, 0xdf, 0x0c, 0xb1, 0x29, 0xbb, 0x5a, 0x20, 0xf7, 0xcb, 0x6b, 0xda, 0x1b, 0xd7, + 0x09, 0xcc, 0xda, 0xa6, 0xb6, 0x8c, 0x9f, 0x2d, 0xfd, 0x27, 0x3d, 0xb8, 0xca, 0xf8, 0x69, 0x6e, 0xac, 0x12, 0xec, + 0xbe, 0xf1, 0x7c, 0x07, 0x20, 0x1c, 0x9b, 0x4f, 0x8f, 0x4f, 0x33, 0x8f, 0x1e, 0x03, 0xd1, 0x31, 0x1f, 0x95, 0xee, + 0x23, 0xbb, 0x7b, 0xfd, 0x00, 0x78, 0xf3, 0xaa, 0x5d, 0xd0, 0xbc, 0x5c, 0x40, 0x20, 0x51, 0xaf, 0xbc, 0xc2, 0xf2, + 0x99, 0x31, 0xbb, 0x04, 0x32, 0x54, 0x10, 0x08, 0x54, 0xdd, 0x75, 0x2e, 0xc4, 0xaa, 0xc3, 0xca, 0x7c, 0x24, 0x61, + 0x47, 0x21, 0x9a, 0x73, 0x06, 0xb3, 0xe0, 0xbf, 0x82, 0x41, 0x39, 0x08, 0xa2, 0x20, 0x0a, 0x02, 0x32, 0x28, 0xe0, + 0x17, 0xe2, 0x8c, 0x11, 0x8c, 0x51, 0x02, 0x1d, 0x7e, 0xc7, 0x99, 0xcf, 0x88, 0xbc, 0x6c, 0x84, 0xb1, 0x74, 0x03, + 0x70, 0x2e, 0x65, 0xce, 0x63, 0xf4, 0xb1, 0x78, 0xc7, 0x59, 0x46, 0xe8, 0x3b, 0xef, 0x54, 0x7e, 0xc4, 0x1b, 0xc1, + 0xed, 0x76, 0x87, 0xed, 0x15, 0x0f, 0x33, 0xda, 0x1b, 0xd3, 0x77, 0x9c, 0x44, 0x59, 0xc3, 0x79, 0x98, 0x43, 0xcf, + 0x2a, 0xcb, 0x5a, 0x51, 0x43, 0x6e, 0x50, 0xac, 0x8b, 0x2c, 0x93, 0x93, 0xe1, 0xaa, 0x39, 0x15, 0xb8, 0xee, 0xec, + 0x7a, 0x01, 0x49, 0x99, 0xd0, 0x2c, 0x9d, 0x0d, 0x5f, 0x6d, 0x5b, 0xf6, 0xa2, 0x75, 0x0a, 0x79, 0x0d, 0x51, 0xd1, + 0x0f, 0x1d, 0x01, 0x35, 0xb4, 0xe2, 0xb2, 0x02, 0x97, 0x5d, 0xd3, 0x1e, 0x6e, 0xda, 0x63, 0x9a, 0xf1, 0x01, 0x62, + 0xc4, 0x71, 0x6c, 0x19, 0xd8, 0x4d, 0x38, 0x3c, 0x1b, 0xe7, 0xfb, 0xb2, 0x4b, 0x6f, 0x5d, 0x2d, 0x1e, 0x61, 0xed, + 0x79, 0x2b, 0x24, 0x04, 0x48, 0x4b, 0x53, 0xe9, 0x76, 0x1b, 0x04, 0x30, 0xc0, 0xfd, 0x7e, 0x0f, 0xb8, 0x56, 0xc3, + 0x4e, 0x9a, 0x5b, 0xb3, 0x25, 0xf6, 0x8a, 0xc2, 0x63, 0x20, 0x4a, 0xcd, 0x7f, 0x06, 0x01, 0xc5, 0x73, 0x37, 0x04, + 0xfb, 0x4a, 0x76, 0xb4, 0x29, 0xfa, 0xfd, 0x17, 0x05, 0x3e, 0xa0, 0x1c, 0x14, 0xc4, 0xba, 0x3a, 0x6e, 0x85, 0x61, + 0x9f, 0xd4, 0x87, 0x38, 0x16, 0x79, 0x16, 0x3a, 0xc2, 0x52, 0x19, 0xc2, 0xc2, 0x15, 0x23, 0x1d, 0xc4, 0x41, 0x4d, + 0x3a, 0x07, 0xab, 0x72, 0xc1, 0x86, 0x7b, 0xbd, 0x4f, 0x00, 0x0b, 0x9e, 0x79, 0xc3, 0xf2, 0xde, 0x03, 0x00, 0xeb, + 0xf5, 0x70, 0xa1, 0xb8, 0x97, 0xaf, 0x1a, 0xe8, 0x93, 0xf8, 0xd2, 0xb2, 0xeb, 0x33, 0x2d, 0x2b, 0x19, 0x8d, 0x46, + 0x55, 0xad, 0x24, 0x1f, 0x8e, 0xbc, 0xb4, 0x50, 0x2b, 0x65, 0x9c, 0xf2, 0x14, 0x2c, 0xbd, 0x0d, 0xa5, 0x9b, 0x2f, + 0xe8, 0x8a, 0x8b, 0x54, 0xfd, 0xf4, 0xd0, 0x26, 0x1b, 0xc4, 0x35, 0x6b, 0xea, 0x2c, 0xec, 0xf0, 0x43, 0xc0, 0x47, + 0xfb, 0x30, 0x73, 0xe9, 0x1a, 0x96, 0x16, 0xc4, 0x91, 0x71, 0xc1, 0x43, 0x97, 0x07, 0xb0, 0xfe, 0xcc, 0x21, 0x89, + 0x9f, 0xc2, 0xcf, 0x99, 0x49, 0xeb, 0xf8, 0x0c, 0x67, 0x33, 0x2a, 0xd5, 0x8d, 0xa0, 0xfd, 0x1a, 0x12, 0x89, 0x41, + 0x36, 0x6e, 0x30, 0x14, 0xad, 0xbb, 0x0d, 0x5c, 0xf9, 0x2d, 0xbd, 0xf3, 0x69, 0x10, 0x60, 0x5b, 0x63, 0x31, 0x00, + 0x18, 0x8a, 0x3f, 0x50, 0x55, 0x63, 0xae, 0x28, 0xa6, 0x61, 0x2a, 0xd1, 0xde, 0x71, 0x5c, 0x47, 0x8d, 0xab, 0xac, + 0x60, 0xa5, 0xb5, 0xe5, 0x75, 0x6f, 0x69, 0x61, 0x4b, 0x40, 0x35, 0x18, 0xee, 0x04, 0xf0, 0x19, 0x91, 0xea, 0x40, + 0x90, 0xdd, 0x07, 0x07, 0x00, 0x9a, 0xe1, 0x79, 0x18, 0xc2, 0x1f, 0x58, 0x38, 0xb0, 0x2c, 0x55, 0x3f, 0x97, 0xd3, + 0x18, 0xce, 0xdd, 0x5c, 0xed, 0xf0, 0xd9, 0x12, 0x14, 0x79, 0x6a, 0x4e, 0xcd, 0xe5, 0x2b, 0x6f, 0xec, 0xf7, 0x98, + 0x60, 0x1e, 0x33, 0xdb, 0xf0, 0x5b, 0x4f, 0xb7, 0xf5, 0x85, 0x75, 0x03, 0x27, 0xed, 0x85, 0xd3, 0x5e, 0x6c, 0x97, + 0x06, 0xe2, 0xae, 0x6e, 0x08, 0x11, 0x5e, 0x6b, 0x62, 0x91, 0x35, 0x64, 0x3a, 0x16, 0x1b, 0x43, 0xb5, 0xa9, 0x78, + 0xae, 0x15, 0xe2, 0xe5, 0x54, 0x5d, 0x98, 0x5a, 0xa9, 0x4c, 0x18, 0x84, 0x99, 0x12, 0x16, 0x55, 0x06, 0x3e, 0xfb, + 0x15, 0x52, 0x5c, 0x5b, 0xcf, 0x5b, 0x5c, 0xbe, 0x99, 0x69, 0xb3, 0xfd, 0xf4, 0x55, 0x1e, 0x5f, 0x6e, 0xb7, 0x61, + 0xf7, 0x0b, 0x30, 0xbf, 0x2c, 0x95, 0x46, 0x0d, 0x9c, 0x1e, 0x42, 0xf4, 0x73, 0xbe, 0x27, 0xe7, 0xc4, 0x71, 0x72, + 0xed, 0xe6, 0xcd, 0x76, 0x52, 0x8c, 0xc0, 0x02, 0x4e, 0x5c, 0xa4, 0x03, 0x2d, 0x15, 0x9c, 0xb6, 0x8c, 0xf7, 0x36, + 0xbd, 0xa3, 0x54, 0x78, 0xb5, 0xd0, 0x24, 0xa4, 0x72, 0xf7, 0x12, 0x3b, 0x6a, 0xc0, 0x39, 0xa9, 0x3b, 0x08, 0x38, + 0xa9, 0xe9, 0xc6, 0x5a, 0xc5, 0xa9, 0x49, 0xf0, 0x5e, 0xe9, 0xa1, 0x4b, 0xb4, 0x13, 0xb7, 0xdb, 0x56, 0x65, 0x0b, + 0xf5, 0x71, 0x2f, 0x67, 0x89, 0x3a, 0x1e, 0x50, 0xe8, 0xa2, 0x8e, 0x86, 0x7c, 0x41, 0x0a, 0xbd, 0x72, 0xb4, 0x6a, + 0x75, 0x57, 0x32, 0x50, 0xaa, 0x55, 0x90, 0xd7, 0xc4, 0xba, 0x6b, 0x65, 0x8d, 0xc5, 0x95, 0x13, 0x52, 0xd8, 0x84, + 0x2f, 0x2d, 0xc5, 0xc2, 0x0a, 0xf6, 0xc6, 0xd4, 0x17, 0x2e, 0x11, 0xda, 0xee, 0x36, 0xc4, 0x24, 0x83, 0x75, 0xb3, + 0xdd, 0xbe, 0x2e, 0xc2, 0x79, 0xb6, 0xa0, 0x72, 0x94, 0xa5, 0x08, 0x21, 0x66, 0x3c, 0x74, 0x6d, 0x17, 0xcc, 0xc4, + 0x50, 0xd7, 0x1e, 0x2f, 0xc9, 0x14, 0x6b, 0x93, 0xe4, 0x28, 0x3e, 0x97, 0x85, 0x5a, 0x6b, 0x84, 0xe0, 0xe1, 0xfe, + 0x67, 0x0a, 0x31, 0xdc, 0xcc, 0xba, 0xfb, 0x75, 0xe7, 0x86, 0xf8, 0x27, 0x04, 0x12, 0x28, 0xd9, 0xeb, 0x62, 0x74, + 0x9e, 0x89, 0x14, 0x77, 0xaa, 0x8a, 0x8a, 0xab, 0xd6, 0x41, 0xb3, 0xe5, 0xf6, 0x5e, 0x6c, 0x89, 0x02, 0xc4, 0x35, + 0x16, 0x9a, 0xf1, 0xac, 0x9c, 0xa5, 0x48, 0x46, 0xb1, 0x21, 0x51, 0xe9, 0x45, 0x45, 0xf7, 0x79, 0x1a, 0xd3, 0x43, + 0xb7, 0x06, 0xc1, 0x55, 0x73, 0x67, 0x23, 0xcd, 0x17, 0x84, 0xa8, 0x09, 0x90, 0xb0, 0x51, 0xcd, 0xa9, 0x75, 0x29, + 0x1e, 0x66, 0x95, 0xcf, 0xf4, 0x41, 0x7c, 0x29, 0x80, 0x87, 0xf5, 0xb6, 0xf7, 0x95, 0xf0, 0x58, 0x1b, 0x7c, 0xbb, + 0xdd, 0x5e, 0x8a, 0x79, 0x10, 0x78, 0x8c, 0xe6, 0x77, 0x4a, 0x62, 0xde, 0x1b, 0x53, 0x58, 0xf1, 0xbe, 0x4b, 0x5b, + 0x37, 0xa9, 0xb5, 0x16, 0xa8, 0x3b, 0x5c, 0x1f, 0xf0, 0x3c, 0x25, 0x8e, 0x76, 0x54, 0x4e, 0xa5, 0xd5, 0x95, 0x63, + 0x57, 0x04, 0x06, 0x86, 0xfe, 0x21, 0x65, 0x1b, 0x30, 0xc7, 0x03, 0x6b, 0x1b, 0xf4, 0x53, 0x52, 0x5a, 0x98, 0x31, + 0x1a, 0xb3, 0xc8, 0x75, 0x15, 0x1d, 0x70, 0x1d, 0xbd, 0x9d, 0x47, 0x7f, 0x7b, 0x36, 0xa6, 0x45, 0x2c, 0x52, 0x79, + 0x05, 0x2a, 0x08, 0x50, 0x86, 0xa0, 0xe1, 0xbf, 0xa6, 0x06, 0xa0, 0x41, 0x70, 0x03, 0xf0, 0xcf, 0x4e, 0xa7, 0x41, + 0x5b, 0x93, 0x8f, 0x49, 0xaa, 0x8a, 0x9c, 0xb5, 0xa1, 0xcc, 0x54, 0x72, 0x48, 0x1e, 0x97, 0x80, 0xe7, 0x88, 0xcd, + 0x52, 0x36, 0x17, 0x6a, 0xb3, 0xa9, 0xd7, 0x8a, 0x1d, 0xb9, 0x6d, 0x14, 0x6d, 0xd6, 0xa2, 0xb6, 0x93, 0x98, 0x2f, + 0xa6, 0xb7, 0x56, 0x18, 0x38, 0x35, 0xad, 0xb9, 0xd9, 0x81, 0x4e, 0xb3, 0xf5, 0x99, 0xdc, 0x04, 0x88, 0x03, 0x0c, + 0xd7, 0xed, 0xfc, 0x66, 0x41, 0xe8, 0x2d, 0xbb, 0xb5, 0x62, 0xd5, 0x1b, 0x2b, 0x17, 0x31, 0x69, 0x37, 0x83, 0x09, + 0x5c, 0xc6, 0x59, 0x61, 0x5f, 0x68, 0x75, 0x43, 0xd1, 0xd1, 0x36, 0x69, 0x3f, 0xef, 0x68, 0x37, 0x5c, 0xf0, 0xad, + 0x58, 0xc7, 0xb9, 0x21, 0x4d, 0x15, 0x7a, 0x74, 0xa0, 0xb7, 0x43, 0x40, 0x73, 0x36, 0xa6, 0x4b, 0x9a, 0xe2, 0x05, + 0x9a, 0xae, 0xc1, 0x2c, 0xe5, 0x02, 0xfa, 0xda, 0xed, 0x93, 0x7c, 0xa1, 0x7a, 0x22, 0xbc, 0x25, 0x0a, 0xbe, 0x1c, + 0x29, 0x78, 0x65, 0xe5, 0x3c, 0x36, 0x73, 0x08, 0x78, 0x2c, 0xaa, 0x44, 0xef, 0xa4, 0xb8, 0x04, 0x65, 0x2a, 0x1c, + 0x81, 0xa6, 0x6a, 0xc4, 0x12, 0x0e, 0x70, 0x7b, 0xf1, 0x34, 0x20, 0x14, 0xa4, 0xba, 0x6b, 0xbb, 0x22, 0x6f, 0xd9, + 0xd1, 0xe6, 0x16, 0xcc, 0x62, 0xab, 0x75, 0xd9, 0xfa, 0xca, 0x26, 0xbb, 0x8f, 0x6b, 0x82, 0x6d, 0xf7, 0x36, 0x48, + 0x78, 0x4b, 0x6f, 0xc8, 0xe6, 0xa6, 0xdf, 0x0f, 0xa1, 0x3f, 0x84, 0xea, 0x0e, 0xdd, 0x76, 0x76, 0xe8, 0xd6, 0x67, + 0x7e, 0xad, 0x9e, 0x4f, 0x79, 0x43, 0x7c, 0x40, 0x13, 0x2d, 0xba, 0x8a, 0xef, 0x60, 0x53, 0x47, 0x15, 0x55, 0x95, + 0x47, 0x09, 0x05, 0x15, 0x70, 0xc6, 0xcb, 0x53, 0x8e, 0xb1, 0x4d, 0xf5, 0xd3, 0x3b, 0xcd, 0xab, 0xad, 0xcd, 0xda, + 0x2c, 0xd7, 0xe7, 0x60, 0x11, 0x70, 0xce, 0xa3, 0x2b, 0x4d, 0x4b, 0x2e, 0x3d, 0xa6, 0xfe, 0x0c, 0x47, 0x25, 0xb8, + 0x88, 0xb3, 0x9c, 0xa7, 0x01, 0xbd, 0x68, 0xf6, 0x3f, 0xd4, 0xb6, 0x52, 0xcb, 0xc6, 0x99, 0x7b, 0x1d, 0x92, 0xcd, + 0xff, 0xd8, 0x40, 0xbd, 0x09, 0x31, 0x22, 0xaa, 0x59, 0xd0, 0x27, 0x0c, 0x62, 0x63, 0x06, 0xe5, 0x3a, 0x49, 0x78, + 0x59, 0x06, 0x46, 0xa9, 0xb5, 0x66, 0x6b, 0x73, 0x9e, 0x3d, 0x62, 0x47, 0x8f, 0x7a, 0x8c, 0xdd, 0x12, 0x9a, 0x68, + 0x9d, 0x90, 0xa9, 0x31, 0xf2, 0xb4, 0x40, 0xba, 0x43, 0x51, 0x76, 0x11, 0x9e, 0xa0, 0x90, 0xa5, 0xbd, 0xcf, 0xcd, + 0x89, 0xac, 0xbe, 0xd1, 0x46, 0x17, 0x91, 0x4a, 0x04, 0xd9, 0xf8, 0x0d, 0x02, 0xf6, 0x42, 0xb3, 0x03, 0xb2, 0x59, + 0xb2, 0x53, 0x7a, 0x66, 0x4d, 0x60, 0xe0, 0xf5, 0x89, 0x4a, 0x34, 0xa3, 0xac, 0x88, 0xae, 0x32, 0x72, 0xb9, 0x0b, + 0x49, 0x74, 0x16, 0x12, 0x3f, 0x37, 0x2c, 0xad, 0xeb, 0x10, 0xc5, 0xcc, 0x66, 0xc3, 0xab, 0xee, 0x3e, 0x6a, 0x6c, + 0x2b, 0xe3, 0x53, 0x7d, 0x6b, 0xd3, 0xc8, 0x14, 0xfa, 0x3a, 0x9c, 0xf4, 0xfb, 0xf0, 0x57, 0xd3, 0x0f, 0xbc, 0xa5, + 0xe0, 0x2f, 0xf6, 0x88, 0xd4, 0x09, 0x0b, 0x00, 0x8e, 0x30, 0xe7, 0x55, 0x73, 0x02, 0x1f, 0xb1, 0xa3, 0xcd, 0xa3, + 0xf0, 0xb4, 0x31, 0x73, 0x77, 0x21, 0x5e, 0xaa, 0x92, 0x9e, 0x37, 0x4f, 0x66, 0x20, 0x56, 0xa1, 0xd9, 0xaf, 0xb7, + 0xcc, 0xea, 0x13, 0x80, 0x48, 0xdd, 0x5a, 0x87, 0x52, 0xfc, 0xd8, 0x74, 0x99, 0x6c, 0x52, 0xd6, 0x66, 0xa2, 0x94, + 0x8a, 0xa4, 0xb9, 0x08, 0xa0, 0xdf, 0x30, 0x1c, 0x35, 0xc0, 0x7b, 0xeb, 0xb1, 0x37, 0x43, 0xe3, 0x8d, 0xa9, 0xa1, + 0x67, 0x1b, 0xbd, 0xbc, 0x1d, 0x85, 0x30, 0x63, 0x11, 0xdd, 0xba, 0x63, 0x31, 0x3c, 0xa5, 0x27, 0x50, 0xe1, 0x9b, + 0x10, 0xa3, 0xe9, 0x92, 0xba, 0x9e, 0xae, 0xd5, 0x56, 0xba, 0x21, 0x34, 0xc7, 0x28, 0x3e, 0x5e, 0xdb, 0xee, 0xa8, + 0x11, 0xda, 0x13, 0xca, 0xc3, 0x5b, 0x5a, 0xd1, 0x1b, 0xcb, 0x22, 0x38, 0xf9, 0xb1, 0x97, 0x9f, 0xd0, 0x73, 0x4f, + 0x60, 0x52, 0xb4, 0x35, 0x80, 0x3f, 0xa0, 0x7e, 0x38, 0xab, 0xa7, 0x56, 0xca, 0xe1, 0x29, 0x7c, 0xc9, 0x06, 0xe4, + 0x0a, 0x7a, 0xb1, 0xc6, 0xec, 0x28, 0x06, 0x1d, 0xd4, 0xce, 0xee, 0xf0, 0x26, 0xa5, 0x0c, 0xd1, 0xfa, 0xce, 0x41, + 0x3c, 0xfd, 0x13, 0x34, 0x7d, 0x90, 0x16, 0xa6, 0x74, 0x8d, 0x02, 0x1e, 0xd0, 0x37, 0xf5, 0xfb, 0x39, 0x3e, 0xd7, + 0x9e, 0x25, 0x16, 0xf6, 0x78, 0x49, 0xe8, 0xd2, 0x8b, 0x1b, 0x05, 0xd2, 0x66, 0xc7, 0x2a, 0x00, 0x2b, 0x92, 0x40, + 0x23, 0x12, 0xb0, 0xd4, 0xf1, 0xc4, 0x65, 0x1b, 0x34, 0x20, 0x89, 0x4a, 0x0a, 0x59, 0x22, 0x09, 0xfc, 0x30, 0x82, + 0x10, 0x45, 0x31, 0x88, 0x7b, 0xf5, 0xf2, 0x8a, 0x6b, 0x6a, 0xc0, 0x89, 0x22, 0x98, 0x60, 0x9d, 0x4e, 0x81, 0xd8, + 0x8a, 0xf5, 0x0a, 0x3c, 0x2f, 0x1d, 0x27, 0x8e, 0x2c, 0x01, 0x1a, 0xa4, 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, 0xca, + 0xae, 0x6d, 0xd7, 0x86, 0xd2, 0x50, 0xdd, 0x2b, 0xc7, 0x04, 0x54, 0x74, 0x15, 0x57, 0xcb, 0x28, 0x1b, 0xc1, 0x9f, + 0xed, 0x36, 0xd8, 0x0f, 0xc0, 0x02, 0xf2, 0x97, 0xf7, 0x3f, 0x45, 0x18, 0x9e, 0xe9, 0x97, 0xf7, 0x3f, 0x6d, 0xb7, + 0xcf, 0xc6, 0x63, 0xc3, 0x15, 0x38, 0xb5, 0x0e, 0xf0, 0x07, 0x86, 0x6d, 0xb0, 0x4b, 0x76, 0xbb, 0x7d, 0x06, 0x1c, + 0x84, 0x62, 0x1b, 0xcc, 0x2e, 0x56, 0x8e, 0x5c, 0x8a, 0xd5, 0xd0, 0x3b, 0x12, 0xb0, 0xea, 0x76, 0x58, 0x8a, 0x5d, + 0xea, 0xa3, 0x42, 0x30, 0xea, 0x45, 0xff, 0xb2, 0x53, 0x60, 0x49, 0xc1, 0x74, 0x35, 0x58, 0x56, 0xd5, 0xaa, 0x8c, + 0xf6, 0xf7, 0xe3, 0x55, 0x36, 0x2a, 0x33, 0xd8, 0xe6, 0xe5, 0xf5, 0x25, 0x00, 0x2a, 0x04, 0xb4, 0xf1, 0x6e, 0x2d, + 0x32, 0xf3, 0x62, 0x41, 0x97, 0x19, 0xae, 0x49, 0x30, 0x3b, 0xc8, 0xb9, 0xd5, 0x4d, 0x4e, 0x89, 0x7d, 0x00, 0x9b, + 0xc3, 0xed, 0xb6, 0xc1, 0x2f, 0x1c, 0x8d, 0x9e, 0xcd, 0x96, 0x99, 0x36, 0xe8, 0xe4, 0x66, 0xff, 0x93, 0xc8, 0x4b, + 0x43, 0xc5, 0x27, 0x99, 0xbe, 0xcc, 0x80, 0xcf, 0x63, 0x6f, 0x45, 0xe8, 0xb3, 0x5c, 0x8d, 0xd6, 0x00, 0x1b, 0x9b, + 0x5d, 0xdc, 0x8d, 0x52, 0x0e, 0x11, 0x29, 0x02, 0xab, 0xae, 0x59, 0x66, 0xc4, 0xb7, 0xa9, 0xb8, 0x6b, 0xa9, 0xc2, + 0xde, 0x0a, 0xcf, 0x59, 0x85, 0x1b, 0x47, 0x99, 0xde, 0x24, 0x0a, 0x5f, 0xa2, 0x10, 0x95, 0xa3, 0x31, 0x9d, 0x13, + 0x48, 0x65, 0x1e, 0x13, 0x8a, 0x39, 0xdc, 0xbb, 0x5f, 0x52, 0x67, 0x2e, 0xe3, 0x0b, 0xf7, 0x5e, 0xfa, 0x32, 0x93, + 0x5b, 0x09, 0xa0, 0x48, 0xaa, 0xf6, 0x9f, 0x3f, 0x23, 0x35, 0xfe, 0x57, 0xaa, 0x35, 0x00, 0xbd, 0x9f, 0xa1, 0x26, + 0x47, 0x10, 0xb0, 0x15, 0x53, 0x1f, 0x4d, 0xdf, 0x4a, 0xe6, 0x3f, 0xa0, 0x6e, 0x47, 0xb0, 0x8d, 0x8a, 0x9f, 0x13, + 0x55, 0xb4, 0xe0, 0xe9, 0x5a, 0xa4, 0xb1, 0x48, 0xee, 0x22, 0x5e, 0x4f, 0xb1, 0x24, 0x66, 0x23, 0x64, 0xfd, 0xdc, + 0xec, 0xc2, 0x4f, 0x45, 0xc3, 0x04, 0x9c, 0x96, 0xfe, 0xb6, 0xf2, 0x36, 0x93, 0x65, 0x9c, 0x91, 0x29, 0x57, 0x88, + 0xdd, 0x56, 0xdf, 0x63, 0x4e, 0xf0, 0xa7, 0x07, 0x4f, 0x09, 0xbd, 0x95, 0xd3, 0x12, 0x41, 0xe9, 0x44, 0x6a, 0x5d, + 0x35, 0xb1, 0x5f, 0x53, 0x88, 0xe2, 0x20, 0x18, 0x84, 0xee, 0x34, 0xed, 0x53, 0x7c, 0x9f, 0x2d, 0xfb, 0xad, 0x29, + 0x5b, 0x92, 0x8d, 0x80, 0x8e, 0x49, 0xe7, 0xed, 0xe9, 0xed, 0xd9, 0x99, 0xf7, 0x1b, 0x34, 0xe1, 0xa0, 0xba, 0x81, + 0x76, 0x15, 0x64, 0x1a, 0xa3, 0xd8, 0x2c, 0xc6, 0xda, 0xad, 0x89, 0x08, 0x82, 0x4e, 0x97, 0xb3, 0xb0, 0xdd, 0x4e, + 0x88, 0x2f, 0x81, 0x04, 0x0a, 0x5c, 0xb9, 0x28, 0x27, 0x21, 0x51, 0x17, 0x32, 0x3d, 0x59, 0xd7, 0x92, 0x05, 0x7a, + 0x8d, 0x1d, 0x04, 0xf4, 0x98, 0xdb, 0xa7, 0x80, 0xbe, 0x2f, 0xd8, 0x31, 0x1f, 0x04, 0x43, 0x8c, 0xaf, 0x1a, 0xd0, + 0x1b, 0xa9, 0x1e, 0xc1, 0x43, 0x18, 0x58, 0x2e, 0xfa, 0xaa, 0x60, 0x08, 0x2b, 0xf4, 0x57, 0xca, 0x26, 0xdf, 0xfc, + 0xdd, 0xcd, 0xef, 0xb9, 0x16, 0xb3, 0x83, 0x50, 0xdc, 0x5e, 0x4f, 0x80, 0xf8, 0x55, 0xfc, 0x0a, 0xac, 0xab, 0xb5, + 0xc4, 0xdb, 0x4d, 0xcf, 0x9f, 0xc2, 0x97, 0xa3, 0xdb, 0x4f, 0x4a, 0xf3, 0x09, 0x04, 0xa9, 0x71, 0x92, 0x72, 0xf7, + 0xdd, 0x47, 0xe9, 0x2a, 0x82, 0xd1, 0x02, 0xc4, 0xba, 0x7b, 0x2b, 0x39, 0x6b, 0x0a, 0xff, 0xb1, 0xce, 0xf7, 0x18, + 0x3b, 0x44, 0x9e, 0xe2, 0xf4, 0x37, 0xc0, 0xb0, 0xef, 0xfc, 0x5b, 0x99, 0x35, 0x24, 0x3a, 0x57, 0x1f, 0x01, 0xfd, + 0x1f, 0xeb, 0xf1, 0x3b, 0x46, 0x49, 0x5f, 0x12, 0xe7, 0x08, 0x57, 0xc4, 0x4b, 0x34, 0xd5, 0xeb, 0x8d, 0x6b, 0xfa, + 0xa9, 0x30, 0x2f, 0xb4, 0x82, 0xc3, 0xbe, 0x35, 0x0a, 0x0f, 0x3c, 0xf3, 0x7e, 0x15, 0x0d, 0x41, 0xf7, 0x6f, 0xb8, + 0x37, 0x7e, 0x15, 0x2c, 0xc3, 0x9b, 0x72, 0x96, 0x99, 0x3b, 0xdc, 0x4d, 0x26, 0x52, 0x79, 0xc3, 0x58, 0xb0, 0x16, + 0xca, 0x7c, 0x35, 0x0d, 0x66, 0x9b, 0x3a, 0x52, 0xc9, 0xee, 0xfb, 0xb7, 0x8d, 0x13, 0x36, 0x1b, 0x04, 0xa7, 0x95, + 0x2c, 0xe2, 0x4b, 0x1e, 0x4c, 0xb5, 0x8a, 0x22, 0xcb, 0xfa, 0xfd, 0x0c, 0x90, 0x61, 0x9c, 0xf6, 0x0e, 0x9e, 0x2c, + 0x35, 0x33, 0x21, 0xae, 0xad, 0xce, 0x02, 0xde, 0x9a, 0xd1, 0x3c, 0xae, 0x60, 0x97, 0xf9, 0x4a, 0x8a, 0x3f, 0x5b, + 0x92, 0x6c, 0xac, 0xbf, 0x21, 0xc3, 0xb6, 0xf2, 0x99, 0x73, 0xc0, 0x98, 0xb9, 0x91, 0x2a, 0xc8, 0x5d, 0x0f, 0x18, + 0x21, 0x24, 0x02, 0xc2, 0x59, 0x4c, 0xdc, 0x09, 0x13, 0xfe, 0xd1, 0x05, 0xc6, 0x89, 0x31, 0x30, 0xce, 0x47, 0x19, + 0x72, 0x7a, 0xcc, 0x07, 0x49, 0x63, 0xb6, 0xfe, 0x54, 0x25, 0xd2, 0x6b, 0x49, 0xe8, 0x19, 0xfc, 0x1e, 0xb7, 0x78, + 0xa0, 0x46, 0x70, 0x4a, 0x77, 0x73, 0xda, 0x7f, 0x55, 0x90, 0xe1, 0x5f, 0xe0, 0xdd, 0x15, 0xdb, 0xcb, 0x72, 0x02, + 0x8b, 0x3b, 0xf6, 0x8a, 0xa7, 0xb9, 0x6a, 0x71, 0x42, 0x3c, 0x62, 0x91, 0xfb, 0xc4, 0x02, 0x46, 0xd4, 0x30, 0x1a, + 0x3f, 0x9e, 0x9e, 0xbc, 0xd5, 0x98, 0x4d, 0xb9, 0xff, 0x01, 0x8c, 0xa8, 0x96, 0xb6, 0xdb, 0x01, 0x5f, 0x8e, 0xd0, + 0x60, 0x3b, 0x75, 0x83, 0xdd, 0xef, 0x9b, 0xb4, 0xa3, 0xd2, 0xcb, 0xe6, 0xc4, 0xa0, 0x3b, 0x4a, 0x9b, 0xa5, 0x32, + 0xa8, 0xed, 0x2a, 0x1c, 0xcd, 0x67, 0x8d, 0x58, 0xd5, 0xfb, 0x30, 0x5c, 0xd2, 0xd8, 0xca, 0xca, 0xed, 0x6e, 0xc2, + 0x91, 0x4d, 0x80, 0xeb, 0x53, 0x50, 0x56, 0xcd, 0x39, 0x68, 0x41, 0x67, 0x02, 0x47, 0xb4, 0xdd, 0x86, 0x10, 0x81, + 0xa3, 0x18, 0x4e, 0x66, 0x61, 0x31, 0x1c, 0xaa, 0x81, 0x2f, 0x08, 0x89, 0x3e, 0x15, 0xf3, 0x6c, 0xa1, 0x10, 0x7b, + 0xfc, 0x9d, 0xf4, 0x6b, 0xa1, 0x38, 0xe5, 0xde, 0xaf, 0x82, 0x6c, 0x7e, 0x4b, 0x31, 0xe6, 0xa0, 0xd3, 0x6c, 0x66, + 0x20, 0x61, 0x3d, 0xae, 0x88, 0x5a, 0x47, 0x76, 0x36, 0x40, 0x15, 0x8b, 0xa6, 0xb0, 0xa0, 0x6e, 0xf1, 0xc4, 0x7a, + 0x46, 0xef, 0x41, 0x25, 0x88, 0x6a, 0xc1, 0x6e, 0x0c, 0xd7, 0xda, 0x27, 0x11, 0x4a, 0xca, 0x49, 0x93, 0x99, 0xb1, + 0xa2, 0xc1, 0x02, 0x84, 0xa4, 0x71, 0x59, 0xbd, 0x91, 0x69, 0x76, 0x91, 0x01, 0x62, 0x82, 0xf3, 0x9f, 0x93, 0x8d, + 0x37, 0xcf, 0xd5, 0xbc, 0x74, 0x25, 0xce, 0x2c, 0xcc, 0x47, 0xd7, 0x5b, 0x5a, 0x90, 0xa8, 0x00, 0x1a, 0xe5, 0x6b, + 0x79, 0xfe, 0xb1, 0x63, 0x15, 0xb2, 0xfb, 0xe1, 0x54, 0xd9, 0x0e, 0xf1, 0x23, 0x56, 0x11, 0xef, 0xb4, 0xae, 0x94, + 0x48, 0xa3, 0xa3, 0x6d, 0x40, 0x0c, 0x5b, 0xf6, 0x2d, 0x6a, 0xf8, 0x20, 0xcc, 0xa0, 0x93, 0xfc, 0xa0, 0x67, 0x74, + 0x6c, 0x0d, 0x24, 0x7d, 0x2d, 0x82, 0xaf, 0xd1, 0x91, 0x4e, 0x94, 0x69, 0x24, 0xa6, 0x90, 0xe8, 0xd7, 0x0b, 0xad, + 0xb1, 0x8c, 0xb2, 0xaf, 0xc8, 0xff, 0x5e, 0x77, 0xef, 0x57, 0xb1, 0xdd, 0xc2, 0x24, 0x7b, 0x1e, 0x57, 0xb0, 0xa9, + 0x51, 0x2b, 0x84, 0xb3, 0x73, 0x5c, 0xa1, 0x76, 0xac, 0x17, 0x96, 0x40, 0x1e, 0xc0, 0x56, 0xa4, 0x41, 0x19, 0x24, + 0xfb, 0x54, 0xcc, 0xc5, 0xc2, 0x89, 0x72, 0xa4, 0xc2, 0xfb, 0x92, 0xa3, 0x94, 0xc3, 0x55, 0x2c, 0x2c, 0x18, 0xf2, + 0xab, 0xa3, 0x8b, 0x42, 0x5e, 0x81, 0xa4, 0xc4, 0x30, 0x54, 0x96, 0xd7, 0xc5, 0x55, 0x5b, 0x12, 0xda, 0x3b, 0x03, + 0x10, 0x96, 0x02, 0x04, 0x2f, 0x8d, 0x1a, 0x62, 0xb6, 0x51, 0xbb, 0x2b, 0xba, 0x97, 0x1c, 0x50, 0xa7, 0xbb, 0x76, + 0xeb, 0x4d, 0xd9, 0x66, 0x5b, 0x71, 0xe1, 0x9f, 0x50, 0xfa, 0x31, 0x1f, 0x14, 0x3e, 0x95, 0xc0, 0x8d, 0xaf, 0x36, + 0x59, 0x76, 0x71, 0x87, 0x4b, 0xbf, 0x6a, 0x8c, 0x5f, 0xbf, 0xdf, 0x53, 0x0b, 0xa1, 0x91, 0x0a, 0xcc, 0xb7, 0xcf, + 0x4c, 0x55, 0x46, 0x53, 0x6a, 0x2f, 0xc1, 0x95, 0xb3, 0x1f, 0x41, 0x45, 0x5c, 0x57, 0x64, 0x32, 0x35, 0x40, 0x7b, + 0x5e, 0x56, 0xb8, 0x95, 0x05, 0x78, 0xec, 0x04, 0x64, 0xbb, 0xe5, 0x61, 0xa0, 0x0f, 0x9d, 0xc0, 0xdf, 0x92, 0xa7, + 0xc8, 0xac, 0xd9, 0xc7, 0x9f, 0xb5, 0xe0, 0x1f, 0x5b, 0xf0, 0x13, 0x8a, 0x3b, 0xad, 0xcc, 0xbf, 0x95, 0xd6, 0x2d, + 0xee, 0xdf, 0xc9, 0x34, 0xa1, 0xa8, 0x4c, 0xa8, 0xfd, 0x4a, 0x7f, 0x37, 0xc1, 0x92, 0x54, 0xf6, 0x0f, 0x12, 0x3e, + 0x98, 0x35, 0x9e, 0x58, 0xe3, 0xc9, 0x70, 0xba, 0x95, 0x86, 0x21, 0x40, 0xa1, 0x9f, 0x97, 0xb9, 0xa2, 0xfa, 0xf9, + 0xe7, 0x35, 0x5f, 0xf3, 0x66, 0x8b, 0x6d, 0xd2, 0x03, 0x0d, 0xf6, 0xf2, 0x68, 0x4a, 0xe1, 0x24, 0xea, 0xdc, 0x48, + 0xd4, 0x45, 0xcd, 0x32, 0x54, 0x27, 0x78, 0x35, 0x4f, 0xf5, 0xb0, 0x37, 0x13, 0xd1, 0x5a, 0x49, 0x59, 0x62, 0xc0, + 0x5a, 0x47, 0x1e, 0x92, 0xbb, 0xb5, 0x8e, 0x3b, 0x0d, 0x75, 0x69, 0x0a, 0x25, 0xc0, 0x0a, 0x17, 0xe0, 0x08, 0xfa, + 0xa9, 0x08, 0x39, 0x5c, 0x53, 0x95, 0x7e, 0x41, 0x53, 0xf2, 0xc4, 0x53, 0xd4, 0x6a, 0x45, 0xba, 0xfd, 0x28, 0xc7, + 0x6e, 0xf8, 0xc6, 0x09, 0x39, 0x31, 0x42, 0x7f, 0x77, 0x2c, 0xe5, 0x0c, 0x2d, 0x1e, 0xd4, 0x09, 0xd6, 0xcb, 0x5b, + 0x0a, 0x14, 0x73, 0x74, 0x59, 0x75, 0xcd, 0x6b, 0xb4, 0x7d, 0x59, 0xf6, 0xfb, 0xb9, 0xad, 0x27, 0x65, 0x47, 0x9b, + 0xa5, 0xd9, 0x87, 0xa8, 0x98, 0xc2, 0x5d, 0x9f, 0x68, 0xfe, 0x2a, 0xd4, 0x57, 0x6d, 0x99, 0xf3, 0x11, 0x47, 0x5c, + 0x8c, 0x9c, 0xd4, 0x3f, 0xab, 0xa9, 0x57, 0xe2, 0x7e, 0x55, 0xc9, 0x77, 0xc2, 0x58, 0x31, 0x3a, 0xa0, 0xfe, 0x54, + 0xa9, 0xbc, 0x5f, 0x16, 0x00, 0xf7, 0x24, 0xd8, 0x27, 0xb0, 0xaf, 0xd0, 0x08, 0xbf, 0x2d, 0x01, 0xff, 0x46, 0x71, + 0x03, 0x56, 0x81, 0x01, 0x46, 0x93, 0xed, 0x39, 0x4d, 0xe0, 0x80, 0x13, 0x5a, 0x45, 0x41, 0x85, 0x19, 0x1a, 0x6a, + 0x0b, 0xa3, 0xa7, 0x28, 0xe3, 0x56, 0x99, 0xbd, 0x1b, 0x63, 0xa7, 0x05, 0x5e, 0xc3, 0x9f, 0xcf, 0x0b, 0x3d, 0x6c, + 0xd4, 0x41, 0x7a, 0x74, 0x12, 0xd3, 0x1f, 0xb7, 0x70, 0x72, 0xb3, 0x70, 0x96, 0x35, 0x4b, 0xa0, 0x3b, 0x70, 0x41, + 0x8c, 0xfb, 0xfd, 0x1c, 0x8e, 0x4c, 0x33, 0xf2, 0x05, 0xcb, 0x69, 0xcc, 0x96, 0x54, 0x7b, 0xda, 0x5d, 0x56, 0x61, + 0x4e, 0x97, 0x56, 0xc6, 0x9b, 0x32, 0x50, 0x19, 0x6d, 0xb7, 0x21, 0xfc, 0xe9, 0xb6, 0x76, 0x49, 0xe7, 0x4b, 0xc8, + 0x00, 0x7f, 0x40, 0x22, 0x8a, 0xd8, 0xd7, 0xff, 0x56, 0xe3, 0x94, 0x9e, 0x28, 0xad, 0x59, 0x42, 0xd7, 0x4c, 0xd7, + 0x4f, 0x2f, 0xd8, 0xba, 0xb1, 0x14, 0xb6, 0xdb, 0xb0, 0x99, 0xc0, 0x34, 0xe7, 0x4a, 0xa6, 0x17, 0xa8, 0x93, 0x02, + 0x2a, 0x16, 0x5e, 0xe0, 0xf2, 0x4b, 0x09, 0x85, 0xe6, 0xce, 0x97, 0x0b, 0xa3, 0xc4, 0x84, 0x56, 0xc9, 0x2f, 0x1f, + 0x2a, 0xf3, 0xb5, 0xf1, 0x88, 0xfb, 0x3d, 0x0d, 0x13, 0x53, 0x24, 0x2a, 0x44, 0x67, 0xbf, 0x82, 0x2c, 0x47, 0x00, + 0x8e, 0xe5, 0xa9, 0xac, 0xe9, 0x8f, 0x29, 0xc4, 0x41, 0x87, 0x06, 0xbd, 0x2b, 0xe4, 0x55, 0x56, 0xf2, 0x10, 0xef, + 0x09, 0x9e, 0x66, 0xf4, 0x7e, 0x83, 0x0f, 0x6d, 0xed, 0xd1, 0x13, 0x64, 0xe3, 0x29, 0xf7, 0xeb, 0xef, 0x44, 0x38, + 0x87, 0x68, 0x95, 0x0b, 0xaa, 0xd5, 0xd5, 0x0e, 0x80, 0xca, 0xb3, 0xbd, 0x7a, 0x04, 0xa7, 0x9b, 0xbe, 0xbe, 0x55, + 0xa1, 0x33, 0x07, 0x90, 0xf6, 0x90, 0xac, 0x6b, 0xae, 0x77, 0x80, 0x3b, 0x12, 0xab, 0x35, 0xd0, 0x58, 0xb7, 0x35, + 0x3b, 0xed, 0x51, 0x3c, 0x26, 0x32, 0x33, 0x16, 0x29, 0xc6, 0xdc, 0xad, 0xd3, 0xa2, 0x68, 0x83, 0x66, 0x08, 0xbb, + 0x77, 0x1d, 0xbe, 0x6e, 0x45, 0x58, 0xbf, 0xdf, 0xf6, 0x05, 0x46, 0xc3, 0x98, 0x6b, 0xf7, 0x3c, 0x43, 0x37, 0x6c, + 0xb0, 0x8d, 0x9c, 0x87, 0xc8, 0x87, 0x99, 0x3a, 0x10, 0x65, 0x6d, 0x0d, 0xd8, 0x1e, 0x71, 0xbd, 0x69, 0x15, 0x3f, + 0xaf, 0x62, 0xce, 0xf6, 0xac, 0x71, 0x4a, 0xeb, 0x6b, 0x5c, 0x73, 0x5c, 0x15, 0x22, 0x6a, 0xeb, 0x19, 0x0f, 0xc3, + 0xce, 0x17, 0xb8, 0x33, 0x2b, 0x0c, 0x5e, 0x84, 0xa5, 0x92, 0x9d, 0xca, 0xf5, 0xe7, 0xb0, 0xc5, 0x41, 0x2a, 0x5f, + 0x7a, 0xfd, 0xfd, 0xdd, 0x17, 0x5f, 0xa0, 0x9b, 0x9a, 0xf3, 0x23, 0x08, 0x32, 0x81, 0x0e, 0x59, 0x4a, 0xf5, 0xf8, + 0x5d, 0x01, 0xd4, 0x1e, 0xe6, 0xe1, 0xbb, 0x82, 0x89, 0xf8, 0x3a, 0xbb, 0x8c, 0x2b, 0x59, 0x8c, 0xae, 0xb9, 0x48, + 0x65, 0x61, 0xa5, 0xc6, 0xc1, 0xf1, 0x6a, 0x95, 0xf3, 0x00, 0x4c, 0xe5, 0x2d, 0xa3, 0x6c, 0x2b, 0xcb, 0xf4, 0xe0, + 0x6a, 0x79, 0x7a, 0xa5, 0x45, 0xe7, 0xe5, 0xf5, 0x65, 0x10, 0xe1, 0xaf, 0x73, 0xf3, 0xe3, 0x2a, 0x2e, 0x3f, 0x06, + 0x91, 0xb5, 0xa9, 0x33, 0x3f, 0x50, 0x2a, 0x0f, 0xfe, 0x53, 0x20, 0xd3, 0xfd, 0xae, 0x00, 0xcb, 0x6c, 0x5b, 0xf1, + 0x61, 0x8c, 0xb5, 0x0e, 0x27, 0x64, 0xa6, 0x4a, 0xf4, 0xde, 0x25, 0xeb, 0x02, 0xac, 0xfd, 0x14, 0x96, 0xb1, 0xca, + 0x35, 0xc3, 0xca, 0x54, 0x45, 0x66, 0x56, 0xd6, 0x6c, 0x3f, 0xb4, 0x4e, 0x34, 0x73, 0xf4, 0x16, 0xd0, 0x0f, 0x64, + 0xff, 0x92, 0x96, 0x6b, 0xe6, 0xf9, 0xd8, 0x34, 0x5e, 0x3f, 0xda, 0xbf, 0x74, 0x0b, 0xf6, 0xd6, 0xde, 0xc9, 0x51, + 0x98, 0x08, 0x9e, 0xb5, 0x66, 0x7c, 0x91, 0x67, 0x05, 0xac, 0x9c, 0xc9, 0x78, 0x4c, 0xbd, 0xa5, 0xd5, 0xba, 0x39, + 0x3a, 0x24, 0xd7, 0xec, 0x71, 0xf5, 0x98, 0x93, 0x7d, 0xde, 0x32, 0xb5, 0x6d, 0x5b, 0xc7, 0x79, 0x9a, 0x7c, 0x65, + 0xba, 0x2f, 0xd6, 0x36, 0x22, 0xba, 0x72, 0xee, 0x73, 0x5e, 0xc1, 0xad, 0x6f, 0x4a, 0x43, 0xaf, 0x25, 0x00, 0xd1, + 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, 0x8b, 0xdd, 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, 0x7e, 0x68, 0xdd, 0xbc, 0x1d, 0x25, 0xca, 0x20, + 0x1e, 0xb7, 0x76, 0xca, 0x41, 0x02, 0x01, 0xb8, 0xa7, 0x2a, 0x04, 0x87, 0x02, 0x59, 0x07, 0x57, 0x33, 0x8e, 0xe0, + 0xea, 0xca, 0x99, 0x8b, 0x1b, 0x80, 0x75, 0xe5, 0xcf, 0x65, 0x83, 0x0b, 0xeb, 0x11, 0x55, 0xe6, 0x8c, 0x53, 0x0c, + 0x62, 0x64, 0x09, 0xfa, 0xca, 0x52, 0xda, 0x4b, 0xd0, 0x34, 0x5e, 0xb1, 0x95, 0xf2, 0x01, 0xa0, 0xe7, 0x6c, 0xa5, + 0x8c, 0xfd, 0xf1, 0xeb, 0x33, 0xb6, 0xd2, 0xd2, 0xe0, 0xe9, 0xd5, 0xec, 0x7c, 0x76, 0x36, 0x60, 0x07, 0x51, 0xa8, + 0x0d, 0x18, 0x02, 0x87, 0xc4, 0x1f, 0x0c, 0x42, 0x8d, 0x77, 0x32, 0x50, 0x01, 0xb1, 0x88, 0xc7, 0x63, 0x23, 0x6e, + 0x56, 0x38, 0x1e, 0x62, 0xf0, 0xab, 0xe6, 0x0b, 0x12, 0x10, 0x6a, 0x4a, 0x43, 0x97, 0xc7, 0x70, 0x38, 0xd9, 0x9b, + 0x40, 0x2a, 0x66, 0x66, 0xaa, 0x30, 0x36, 0x26, 0x11, 0xc4, 0x3b, 0xed, 0xac, 0x17, 0xca, 0xed, 0xae, 0xd1, 0x40, + 0xae, 0x0c, 0xbe, 0xa8, 0xe2, 0xc9, 0xde, 0xb0, 0xab, 0x62, 0x1c, 0x85, 0x6b, 0xa3, 0x7c, 0x3b, 0x3b, 0x04, 0xf0, + 0xda, 0xb3, 0xa1, 0x2f, 0x97, 0x38, 0xdb, 0x7f, 0x4a, 0x1e, 0x3f, 0x25, 0xf4, 0x8c, 0x9d, 0x7d, 0xf5, 0x94, 0x9e, + 0x29, 0x72, 0xb2, 0x37, 0x89, 0xae, 0x99, 0xc5, 0x7c, 0x39, 0x50, 0x4d, 0xa0, 0x97, 0xa3, 0xb5, 0x50, 0x0b, 0x4c, + 0x3b, 0x34, 0x85, 0xdf, 0x8e, 0xf7, 0x82, 0xc1, 0x75, 0xbb, 0xe9, 0xd7, 0xed, 0xb6, 0x7a, 0x5e, 0x5d, 0x7b, 0x07, + 0xd1, 0x6e, 0x31, 0x93, 0xbf, 0x8f, 0xf7, 0xdc, 0x1c, 0x60, 0x7d, 0x0f, 0x8f, 0x89, 0x69, 0xd2, 0xce, 0xa8, 0xf8, + 0x35, 0x3d, 0xc1, 0x3e, 0x34, 0x8b, 0xec, 0xe8, 0xc3, 0xf0, 0xdf, 0xea, 0x44, 0x7d, 0xf6, 0xd5, 0x01, 0x90, 0x23, + 0x90, 0x81, 0x62, 0x89, 0x60, 0x86, 0x03, 0x4d, 0x01, 0x05, 0x99, 0x1e, 0x77, 0xaa, 0x87, 0x5f, 0x8d, 0x9a, 0x9a, + 0x91, 0x6b, 0x98, 0x1a, 0x6c, 0x0b, 0x7e, 0xa0, 0xba, 0xa1, 0xbf, 0xd1, 0xe8, 0x46, 0xda, 0xc9, 0xcc, 0xbc, 0xa4, + 0x36, 0xae, 0xdb, 0x35, 0x04, 0x30, 0x76, 0xf0, 0x82, 0x92, 0x7d, 0x7d, 0x78, 0xb9, 0x87, 0xab, 0x08, 0x50, 0xb2, + 0x58, 0xf0, 0xf5, 0xe0, 0x52, 0x6f, 0xee, 0xbd, 0x80, 0x0c, 0xbe, 0x0e, 0x8e, 0xbe, 0x1e, 0xc8, 0x41, 0x70, 0xb8, + 0x7f, 0x79, 0x14, 0x38, 0xe3, 0x7e, 0x08, 0xf1, 0xa8, 0x2a, 0x8a, 0x99, 0x30, 0x55, 0x24, 0xb6, 0xf6, 0xdc, 0xd6, + 0xab, 0x8c, 0xcf, 0x68, 0x3a, 0xb5, 0xc8, 0xdf, 0x61, 0xca, 0x62, 0xf3, 0x3b, 0x98, 0xf0, 0xab, 0x20, 0x72, 0x41, + 0x50, 0x67, 0x79, 0x14, 0xd3, 0x25, 0xbb, 0x15, 0x61, 0x4a, 0x93, 0xfd, 0x9c, 0x90, 0x28, 0x5c, 0x2a, 0xf0, 0x3c, + 0xf5, 0x3a, 0x81, 0x38, 0xae, 0xee, 0xf3, 0x5b, 0x11, 0x2e, 0x69, 0xbe, 0x9f, 0x90, 0x56, 0x11, 0x2e, 0x22, 0xcb, + 0xa6, 0xa6, 0x17, 0x2c, 0x5c, 0xd1, 0x4b, 0x60, 0xa6, 0xe4, 0x3a, 0xbc, 0x04, 0x2e, 0x6f, 0x3d, 0x5f, 0x2d, 0xd8, + 0x65, 0x43, 0xfa, 0x66, 0xf8, 0xe2, 0x0b, 0xeb, 0x93, 0x07, 0x3c, 0xa4, 0xf3, 0xc3, 0x4b, 0xc1, 0x06, 0xe0, 0x3a, + 0xe3, 0x37, 0xdf, 0xc9, 0x5b, 0x3d, 0x2f, 0xed, 0x29, 0xc6, 0x99, 0x69, 0x27, 0x26, 0xed, 0x84, 0xdc, 0xbf, 0x6f, + 0x6f, 0x62, 0x73, 0xb2, 0x97, 0xd1, 0x5a, 0xb9, 0xac, 0x5a, 0x86, 0xa4, 0x58, 0x33, 0xe4, 0xef, 0x51, 0x72, 0x6a, + 0x05, 0x9e, 0xec, 0x82, 0x57, 0xc9, 0xd2, 0x3f, 0xa8, 0xac, 0xd5, 0x80, 0x3d, 0x46, 0x2c, 0x0b, 0x85, 0x63, 0xff, + 0x26, 0x63, 0xc5, 0xda, 0x17, 0x68, 0xc4, 0xc8, 0xbd, 0xbd, 0xc9, 0x98, 0x17, 0x73, 0x35, 0x59, 0x7b, 0xa1, 0xea, + 0xbc, 0xf4, 0xbc, 0xc5, 0x7b, 0x39, 0xa5, 0x86, 0x91, 0x88, 0xee, 0x8d, 0x95, 0x19, 0xa5, 0x4a, 0xd4, 0x1a, 0x34, + 0x22, 0xd8, 0xd8, 0x05, 0xbf, 0x04, 0x27, 0x54, 0xee, 0xa9, 0xb3, 0x7d, 0x3b, 0xa5, 0xd2, 0x03, 0x96, 0xa5, 0x46, + 0x55, 0xee, 0x96, 0x99, 0x64, 0xd5, 0x20, 0x18, 0xfd, 0x59, 0x4a, 0x31, 0xc3, 0x3b, 0x23, 0x0b, 0xa6, 0x60, 0x25, + 0xa8, 0x6a, 0x19, 0x96, 0x43, 0x8e, 0x5a, 0x3c, 0xe3, 0x93, 0x2a, 0xf5, 0x8f, 0x8e, 0x20, 0xb9, 0xcb, 0x75, 0x2b, + 0x48, 0xee, 0xd3, 0xf1, 0x53, 0x3d, 0xd0, 0xe9, 0x5a, 0x3b, 0x1e, 0xfa, 0xfc, 0x36, 0xe2, 0x6b, 0xeb, 0xde, 0x53, + 0xad, 0x55, 0x28, 0x03, 0x2d, 0x56, 0x54, 0xae, 0xd4, 0x92, 0xde, 0xef, 0x22, 0x00, 0x16, 0xb1, 0x31, 0x1b, 0xef, + 0xda, 0x66, 0x85, 0xa0, 0xd1, 0x65, 0x47, 0x9b, 0x78, 0xc0, 0x12, 0xdd, 0xda, 0xc1, 0x84, 0xc6, 0x47, 0xac, 0xec, + 0xf7, 0xf3, 0x23, 0xa0, 0xa7, 0xda, 0x88, 0xa9, 0x80, 0x23, 0xff, 0x4b, 0x2b, 0x32, 0x45, 0x81, 0xcd, 0x9a, 0xba, + 0x5b, 0x63, 0x19, 0x89, 0xbe, 0x4c, 0xe9, 0xf2, 0x84, 0x67, 0xc0, 0xb4, 0x5e, 0xb7, 0x1c, 0x57, 0x76, 0x15, 0x47, + 0x9e, 0x0a, 0xcb, 0x8a, 0xf3, 0x2a, 0x1c, 0x6f, 0x3d, 0xbe, 0xc1, 0xbe, 0x61, 0xd3, 0x2e, 0xfc, 0x21, 0x84, 0x85, + 0xf0, 0x26, 0x83, 0xdb, 0x88, 0xb6, 0x93, 0x40, 0xe5, 0x8d, 0xb9, 0x4e, 0x28, 0x9b, 0xdb, 0xf5, 0xda, 0x33, 0x48, + 0x27, 0xe6, 0x40, 0xa9, 0x46, 0xd0, 0x1a, 0xcd, 0x82, 0xaa, 0x11, 0x8f, 0x1c, 0x0f, 0xef, 0x0c, 0x62, 0xb5, 0x7c, + 0x49, 0x53, 0x29, 0x1a, 0x80, 0x71, 0x01, 0x5c, 0x9e, 0x7e, 0x79, 0xff, 0xd3, 0x29, 0x8f, 0x8b, 0x64, 0xf9, 0x2e, + 0x2e, 0xe2, 0xab, 0x32, 0xdc, 0xa8, 0x31, 0x8a, 0x6b, 0x32, 0x15, 0x03, 0x26, 0xcd, 0x4a, 0x6a, 0xee, 0x4a, 0x4d, + 0x88, 0xb1, 0xce, 0x64, 0x5d, 0x56, 0xf2, 0xaa, 0x51, 0xe9, 0xba, 0xc8, 0xf0, 0xe3, 0x96, 0xcf, 0xe9, 0x3e, 0x00, + 0x79, 0x1a, 0x17, 0xd2, 0x48, 0xea, 0x42, 0x8c, 0xb9, 0x88, 0xd7, 0xf5, 0xf1, 0xb8, 0xd1, 0xf5, 0x92, 0x3d, 0x1b, + 0x3f, 0x99, 0xbe, 0xc9, 0xc2, 0x6c, 0x20, 0xc8, 0xa8, 0x5a, 0x72, 0xd1, 0x32, 0xe5, 0x54, 0x26, 0x01, 0xe8, 0xe3, + 0xd9, 0x63, 0xec, 0x60, 0x3c, 0x26, 0x9b, 0xb6, 0x78, 0x80, 0x87, 0xcb, 0x75, 0x58, 0x90, 0x99, 0xae, 0x23, 0x0a, + 0x04, 0xbf, 0xad, 0x02, 0x40, 0x72, 0xb4, 0x55, 0x19, 0x2e, 0x8d, 0x3d, 0x1b, 0x4f, 0xa8, 0xc4, 0x6e, 0x87, 0xa4, + 0xf6, 0x2a, 0x74, 0x33, 0x2f, 0x7d, 0x8f, 0x22, 0x69, 0x5c, 0x96, 0x76, 0x2a, 0x95, 0x6a, 0xcf, 0xcc, 0x5c, 0xd7, + 0x20, 0x06, 0x43, 0xa8, 0xeb, 0x2e, 0xbd, 0xba, 0x77, 0x9b, 0x6b, 0xcd, 0x76, 0xc0, 0x7b, 0x0d, 0x9a, 0xa1, 0xe4, + 0x2d, 0xe6, 0xad, 0x2b, 0xa2, 0xa6, 0xab, 0x35, 0x98, 0x15, 0xa3, 0x6c, 0x29, 0x4a, 0xd7, 0x14, 0x94, 0x82, 0xd1, + 0xc5, 0xda, 0x5b, 0xb8, 0x6f, 0x64, 0xe3, 0xc2, 0x92, 0xe9, 0xd5, 0xa2, 0xa4, 0x84, 0xea, 0xa6, 0x62, 0xa4, 0x84, + 0x91, 0xd2, 0xf0, 0x54, 0xbe, 0x17, 0x78, 0x9c, 0xe7, 0x41, 0xd4, 0xf2, 0x02, 0x3b, 0xae, 0xc8, 0x31, 0x38, 0x7a, + 0x99, 0x9c, 0x86, 0x02, 0xff, 0x98, 0x29, 0x10, 0xd3, 0xa1, 0xba, 0xdf, 0xe0, 0xe6, 0xff, 0x67, 0xc1, 0x02, 0x8f, + 0x6f, 0xbd, 0xc4, 0x6d, 0xf4, 0xcf, 0xc2, 0xa7, 0xa5, 0xcf, 0xa5, 0xef, 0xea, 0xe2, 0x49, 0x7b, 0xb3, 0x51, 0xb2, + 0xcc, 0xf2, 0xf4, 0xad, 0x4c, 0x39, 0x88, 0xcc, 0xd0, 0x1a, 0x94, 0x1d, 0x89, 0xc6, 0x0d, 0x0f, 0x8c, 0x18, 0x1b, + 0x37, 0xbe, 0x1f, 0x33, 0x90, 0x0d, 0x83, 0xd5, 0x37, 0x4b, 0x65, 0xb2, 0xbe, 0x02, 0x4c, 0x11, 0x25, 0x3f, 0x79, + 0x99, 0x73, 0x78, 0x0a, 0xf5, 0xf5, 0x0b, 0xdc, 0xe6, 0x4a, 0xdf, 0xe7, 0xfc, 0xc7, 0x8c, 0xfe, 0x88, 0x40, 0x27, + 0xf1, 0x0a, 0xe4, 0x1e, 0xcf, 0xa1, 0x6e, 0x84, 0xa9, 0xe5, 0x18, 0x1c, 0x08, 0xd1, 0x40, 0x44, 0xc5, 0x02, 0x05, + 0x75, 0x61, 0x80, 0x35, 0xd4, 0x05, 0x73, 0x78, 0x9e, 0xcb, 0xe4, 0xe3, 0xd4, 0xf8, 0xcc, 0x0f, 0x63, 0x8c, 0x99, + 0x1c, 0x0c, 0xc2, 0x6a, 0x16, 0x0c, 0xc7, 0xa3, 0xc9, 0xc1, 0x33, 0x38, 0xb7, 0x83, 0x71, 0x40, 0x06, 0x41, 0x5d, + 0xae, 0x62, 0x41, 0xcb, 0xeb, 0x4b, 0x5b, 0x06, 0x7e, 0x5c, 0x07, 0x83, 0x7f, 0x16, 0x9e, 0xe2, 0x1d, 0x34, 0x27, + 0x67, 0x32, 0x04, 0x1b, 0xfb, 0x35, 0x01, 0x49, 0x59, 0x4f, 0xf3, 0x93, 0xfa, 0x70, 0x63, 0x4a, 0xfb, 0x67, 0x0e, + 0x2f, 0x38, 0xec, 0x90, 0x40, 0x81, 0x34, 0x9e, 0x66, 0xa3, 0xd7, 0x4a, 0x91, 0xfb, 0xae, 0xe0, 0x70, 0x67, 0xee, + 0x39, 0xd3, 0x23, 0xa7, 0x90, 0x68, 0x66, 0x01, 0x37, 0xf2, 0xd7, 0xe2, 0x3a, 0xce, 0xb3, 0x74, 0xaf, 0xf9, 0x66, + 0xaf, 0xbc, 0x13, 0x55, 0x7c, 0x3b, 0x0a, 0x8c, 0x35, 0x21, 0xf7, 0x55, 0x4f, 0x80, 0x9e, 0x00, 0x5b, 0x00, 0x0c, + 0x88, 0x77, 0xcc, 0x4c, 0x66, 0x3c, 0x02, 0x8f, 0xc0, 0xa6, 0x0f, 0x64, 0x71, 0xe7, 0x5c, 0x92, 0xfc, 0xcd, 0x54, + 0xda, 0xab, 0x5e, 0xb9, 0x53, 0x90, 0xf5, 0x6a, 0x2b, 0x77, 0xdd, 0xfa, 0xec, 0x9b, 0x0e, 0xaf, 0xc0, 0x73, 0x09, + 0x6e, 0x91, 0xfd, 0x7e, 0x53, 0x50, 0x29, 0x8c, 0x8a, 0x78, 0x27, 0xb9, 0x46, 0xff, 0x76, 0x6f, 0x6c, 0x14, 0xc9, + 0x2d, 0x1f, 0x1e, 0x40, 0x9d, 0xc9, 0xbb, 0xe2, 0x76, 0x0e, 0x51, 0x5b, 0x77, 0xe3, 0x81, 0xd5, 0x06, 0xed, 0xb2, + 0xe6, 0x08, 0x2e, 0xbc, 0xd8, 0xcb, 0x60, 0x2c, 0x70, 0x56, 0x46, 0x4a, 0x8d, 0x6b, 0x48, 0x2d, 0xf8, 0x24, 0x4f, + 0xef, 0x21, 0x4b, 0x3d, 0x09, 0x8a, 0x1c, 0xcf, 0x62, 0xc8, 0x34, 0xde, 0x06, 0x1e, 0xbf, 0x93, 0x21, 0x48, 0xd3, + 0xb6, 0xdb, 0xe6, 0x08, 0x94, 0xdd, 0x03, 0x53, 0x92, 0xba, 0x36, 0xa6, 0x06, 0x1a, 0x6a, 0x0f, 0x35, 0x52, 0x11, + 0x67, 0x47, 0x6f, 0x40, 0x87, 0x08, 0xbe, 0xdf, 0x69, 0x56, 0x76, 0xbc, 0x98, 0x10, 0x3c, 0x79, 0x5f, 0xde, 0x66, + 0x65, 0x55, 0x46, 0xef, 0x53, 0x34, 0x84, 0x4a, 0xa4, 0x88, 0x5e, 0x41, 0x3c, 0xbd, 0x12, 0x7f, 0x97, 0xd1, 0x4f, + 0x29, 0x8d, 0xd3, 0x14, 0xd3, 0x5f, 0x14, 0xf0, 0xf3, 0x39, 0xa0, 0x3a, 0xe2, 0x4e, 0x88, 0xce, 0x25, 0xd8, 0xab, + 0x41, 0x34, 0xab, 0x8a, 0x03, 0x86, 0x66, 0x74, 0x2b, 0x28, 0x62, 0xb4, 0x61, 0xf6, 0x1f, 0x0a, 0x14, 0x0a, 0xa9, + 0x62, 0xbe, 0x13, 0xf6, 0x21, 0xfa, 0x11, 0x8b, 0x3c, 0x7e, 0xf7, 0xda, 0x0c, 0x69, 0x74, 0x27, 0xa9, 0xde, 0xda, + 0x78, 0x6c, 0x61, 0xe0, 0xb2, 0xe8, 0x72, 0x4d, 0xcf, 0xe2, 0x55, 0x16, 0x6d, 0x00, 0x7f, 0xe2, 0xdd, 0xeb, 0xe7, + 0xca, 0xc2, 0xe4, 0x45, 0x06, 0x8a, 0x83, 0xe3, 0x77, 0xaf, 0xdf, 0xc8, 0x74, 0x9d, 0xf3, 0xe8, 0x4c, 0x22, 0x69, + 0x3d, 0x7e, 0xf7, 0xfa, 0x67, 0x34, 0xf7, 0xfa, 0xa9, 0x80, 0xf7, 0xaf, 0x80, 0xb7, 0x8c, 0xe2, 0x35, 0xf4, 0x49, + 0xfd, 0x4e, 0xd6, 0xd8, 0x29, 0xaf, 0xd6, 0x32, 0xfa, 0x25, 0xad, 0x3d, 0x69, 0xd5, 0xbf, 0x0a, 0x9f, 0xda, 0x79, + 0x02, 0x9e, 0xdb, 0x3c, 0x13, 0x1f, 0x23, 0x2b, 0xda, 0x09, 0xa2, 0xaf, 0xf7, 0x6e, 0xaf, 0x72, 0x51, 0x46, 0xf8, + 0x82, 0xa1, 0x5d, 0x50, 0xb4, 0xbf, 0x7f, 0x73, 0x73, 0x33, 0xba, 0x79, 0x32, 0x92, 0xc5, 0xe5, 0xfe, 0xe4, 0xdb, + 0x6f, 0xbf, 0xdd, 0xc7, 0xb7, 0xc1, 0xd7, 0x6d, 0xb7, 0xf7, 0x8a, 0xf0, 0x01, 0x0b, 0x10, 0xa1, 0xfa, 0x6b, 0xb8, + 0xa2, 0x80, 0x16, 0x6e, 0xf0, 0x75, 0xf0, 0xb5, 0x3e, 0x74, 0xbe, 0x3e, 0x2c, 0xaf, 0x2f, 0x55, 0xf9, 0x5d, 0x25, + 0x1f, 0x8c, 0xc7, 0xe3, 0x7d, 0x90, 0x40, 0x7d, 0x3d, 0xe0, 0x83, 0xe0, 0x28, 0x18, 0x64, 0x70, 0xa1, 0x29, 0xaf, + 0x2f, 0x8f, 0x02, 0xcf, 0x34, 0xb7, 0xc1, 0x22, 0x3a, 0x10, 0x97, 0x60, 0xff, 0x92, 0x06, 0x5f, 0x07, 0xc4, 0xa5, + 0x7c, 0x05, 0x29, 0x5f, 0x1d, 0x3c, 0xf3, 0xd3, 0xfe, 0x97, 0x4a, 0x7b, 0xe2, 0xa7, 0x1d, 0x62, 0xda, 0x93, 0xe7, + 0x7e, 0xda, 0x91, 0x4a, 0x7b, 0xe9, 0xa7, 0xfd, 0xef, 0x72, 0x00, 0xa9, 0x7b, 0xbe, 0xf5, 0xdf, 0xb9, 0xd7, 0x1a, + 0x3c, 0x85, 0xa2, 0xec, 0x2a, 0xbe, 0xe4, 0xd0, 0xe8, 0xc1, 0xed, 0x55, 0x4e, 0x83, 0x01, 0xb6, 0xd7, 0x33, 0x09, + 0xf1, 0x3e, 0xf8, 0x7a, 0x5d, 0xe4, 0x61, 0xf0, 0xf5, 0x00, 0x0b, 0x19, 0x7c, 0x1d, 0x90, 0xaf, 0x8d, 0x81, 0x8c, + 0x60, 0x9b, 0xc0, 0x85, 0x22, 0x1d, 0xda, 0x00, 0x61, 0xbe, 0x34, 0xae, 0xa6, 0x7f, 0x15, 0xdd, 0xd9, 0xf0, 0x96, + 0xa8, 0xdc, 0x74, 0x83, 0x9a, 0x9e, 0x80, 0x77, 0x02, 0x34, 0x2a, 0x0a, 0xae, 0xe3, 0x22, 0x1c, 0x0e, 0xcb, 0xeb, + 0x4b, 0x02, 0x76, 0x99, 0x2b, 0x1e, 0x57, 0x51, 0x20, 0xe4, 0x50, 0xfd, 0x0c, 0x54, 0xe4, 0xab, 0x00, 0x01, 0x91, + 0xe0, 0xbf, 0xa0, 0xa6, 0xef, 0x24, 0xdb, 0x04, 0xc3, 0x1b, 0x7e, 0xfe, 0x31, 0xab, 0x86, 0x4a, 0xb4, 0x78, 0x2d, + 0x28, 0xfc, 0x80, 0xbf, 0xae, 0xea, 0xe8, 0x2f, 0x70, 0xe3, 0x6e, 0x6a, 0xd8, 0xdf, 0x49, 0xc7, 0xa2, 0xbe, 0x93, + 0xf3, 0x6c, 0x31, 0x6d, 0x1d, 0xe8, 0x27, 0x92, 0x54, 0xf3, 0x6c, 0x10, 0x0c, 0x83, 0x01, 0x5f, 0xb0, 0x13, 0x39, + 0xe7, 0x9e, 0xf9, 0xd4, 0x23, 0xe9, 0x4f, 0xf3, 0x2c, 0x1b, 0x80, 0x6f, 0x0a, 0xf2, 0x23, 0xfb, 0xff, 0x3d, 0x1f, + 0xa2, 0xf0, 0x70, 0xf0, 0x68, 0x9f, 0xcc, 0x82, 0xd5, 0x2d, 0x7a, 0x74, 0x46, 0x41, 0x26, 0x96, 0xbc, 0xc8, 0x2a, + 0x6f, 0xa9, 0xdc, 0xad, 0xdb, 0x5e, 0x1e, 0xf7, 0x9e, 0xcd, 0xab, 0x58, 0x04, 0xea, 0x9c, 0x03, 0xc5, 0x1b, 0xca, + 0x9e, 0xca, 0xa6, 0x84, 0x54, 0x1b, 0xf2, 0x86, 0xe5, 0x80, 0x05, 0x87, 0xbd, 0xe1, 0x70, 0x2f, 0x18, 0x38, 0x75, + 0xee, 0x20, 0xd8, 0x1b, 0x0e, 0x8f, 0x02, 0x77, 0x1f, 0xca, 0x46, 0xee, 0xce, 0x48, 0x0b, 0xf6, 0xaf, 0x22, 0x2c, + 0x29, 0x88, 0xc7, 0xa4, 0x16, 0x7f, 0x69, 0x70, 0x99, 0x01, 0x40, 0x1f, 0x29, 0x09, 0x98, 0x81, 0x95, 0x19, 0x40, + 0x68, 0x6e, 0x1a, 0xb3, 0x33, 0x60, 0x1e, 0x81, 0x63, 0x1e, 0x21, 0xe3, 0x00, 0x88, 0x25, 0x01, 0xce, 0x5d, 0x10, + 0xc5, 0xba, 0x90, 0x47, 0x00, 0x7a, 0x8f, 0x3f, 0x89, 0x29, 0x05, 0x93, 0x74, 0xac, 0x42, 0x10, 0xc4, 0xf1, 0xd9, + 0xb5, 0x68, 0x4d, 0xce, 0x12, 0x1d, 0xcc, 0x48, 0x02, 0x6c, 0x88, 0x81, 0x9d, 0x83, 0xfb, 0x39, 0x28, 0x3d, 0xac, + 0xde, 0x09, 0xb9, 0xe0, 0x3b, 0xee, 0xc9, 0x66, 0xe1, 0xea, 0x09, 0x07, 0xc1, 0x1d, 0xd7, 0x2c, 0xc0, 0xa8, 0x2a, + 0xd6, 0x65, 0xc5, 0xd3, 0x0f, 0x77, 0x2b, 0x88, 0x7d, 0x87, 0x03, 0xfa, 0x4e, 0xe6, 0x59, 0x72, 0x17, 0x3a, 0x7b, + 0xae, 0x8d, 0x4a, 0xff, 0xe1, 0xc3, 0x9b, 0x9f, 0x22, 0x10, 0x39, 0xd6, 0x86, 0xd2, 0xdf, 0x71, 0x3c, 0x9b, 0xfc, + 0x08, 0x4f, 0xfe, 0xc6, 0xbe, 0xe3, 0xf6, 0xf4, 0xe8, 0xf7, 0xa1, 0x6e, 0x7a, 0xc7, 0x67, 0x77, 0x7c, 0xe4, 0x8a, + 0x43, 0x75, 0x85, 0xfb, 0xfa, 0x66, 0xed, 0x1b, 0x21, 0x3d, 0x3c, 0xcf, 0x94, 0x37, 0xe6, 0x47, 0x3b, 0x18, 0x06, + 0xc1, 0x54, 0x0b, 0x25, 0x21, 0xea, 0x06, 0x53, 0x02, 0x86, 0x68, 0x4f, 0x2f, 0xab, 0x29, 0x72, 0x6e, 0x6a, 0x64, + 0xe1, 0xfd, 0x80, 0x69, 0xa1, 0x43, 0x23, 0x87, 0xf2, 0x83, 0xc3, 0x09, 0x63, 0x16, 0x7e, 0xab, 0x84, 0xe9, 0x57, + 0x8b, 0xca, 0x39, 0x88, 0xee, 0x81, 0x31, 0xae, 0xe0, 0x05, 0x74, 0x85, 0x5d, 0xaf, 0x55, 0x54, 0x0c, 0x04, 0x8f, + 0x43, 0x0e, 0xd0, 0xc3, 0x2e, 0x68, 0x59, 0x59, 0xaa, 0x5b, 0x95, 0xb3, 0x54, 0x51, 0x97, 0xa1, 0xac, 0x8c, 0x15, + 0xe6, 0x7b, 0xc9, 0x7e, 0x28, 0xd0, 0xb3, 0x7c, 0x2a, 0xba, 0xe0, 0x85, 0x50, 0x82, 0xe5, 0xba, 0xde, 0x89, 0x40, + 0xd4, 0xf9, 0xa1, 0x77, 0xd5, 0xd7, 0x38, 0x76, 0x3c, 0x7d, 0x23, 0x53, 0xae, 0x4d, 0x28, 0x34, 0x9f, 0x2f, 0x7d, + 0xc5, 0x44, 0xc1, 0x6e, 0xa0, 0x5f, 0x6d, 0x1b, 0x7d, 0x76, 0xb7, 0xd6, 0x9b, 0x41, 0x89, 0x8e, 0x79, 0x8d, 0x82, + 0x6b, 0xa5, 0x50, 0x30, 0xda, 0xdb, 0xf8, 0x33, 0x1c, 0xb9, 0xd5, 0xed, 0xa1, 0xf7, 0x5b, 0x15, 0x5f, 0xbe, 0x45, + 0xdf, 0x4e, 0xfb, 0x73, 0x54, 0xc9, 0x5f, 0x56, 0x2b, 0xf0, 0xa1, 0x82, 0xc8, 0x22, 0x16, 0x97, 0x16, 0xea, 0x39, + 0x7d, 0x77, 0xfc, 0x16, 0xfc, 0x28, 0xf1, 0xf7, 0xaf, 0xdf, 0x07, 0x35, 0x99, 0xc6, 0xb3, 0xc2, 0x7c, 0x68, 0x73, + 0x40, 0x68, 0x12, 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, 0x22, 0xde, 0xcb, 0x9d, + 0x10, 0x62, 0x5c, 0x61, 0xd1, 0xe8, 0x21, 0x9f, 0xf1, 0x48, 0x19, 0x16, 0xbd, 0xc7, 0x04, 0xe2, 0x0c, 0xa7, 0xd5, + 0x7b, 0xc4, 0x3c, 0xc6, 0xbb, 0x81, 0x96, 0x3d, 0x44, 0x19, 0x75, 0xd9, 0x1b, 0x16, 0xdf, 0x1f, 0xd7, 0x61, 0x66, + 0x2d, 0x2f, 0x87, 0xf0, 0x37, 0xd0, 0x06, 0xe0, 0x94, 0x23, 0xcb, 0x57, 0x99, 0x8d, 0xae, 0x96, 0x98, 0xde, 0x44, + 0x10, 0x8b, 0x47, 0xa7, 0xc3, 0xda, 0xd5, 0xa9, 0x7a, 0x57, 0x3b, 0x9f, 0x89, 0x5e, 0x05, 0x5a, 0xb9, 0xb6, 0x3d, + 0x1e, 0xc2, 0x5d, 0x6a, 0x69, 0x85, 0x8d, 0x28, 0xe7, 0xe2, 0xe9, 0xce, 0xb1, 0x39, 0x01, 0x0d, 0xae, 0x64, 0x0a, + 0xc0, 0x59, 0x5a, 0x8d, 0x46, 0x8d, 0xb0, 0xcf, 0xca, 0xf9, 0x1c, 0xb6, 0x16, 0xe2, 0x69, 0x01, 0x18, 0x6e, 0x13, + 0x83, 0x92, 0x77, 0x63, 0x50, 0x4e, 0x3f, 0x2a, 0x78, 0xeb, 0xe0, 0xac, 0x5c, 0xc6, 0xa9, 0xbc, 0x01, 0x2c, 0xc6, + 0xc0, 0x4f, 0xc5, 0x52, 0xbd, 0x84, 0x64, 0xc9, 0x93, 0x8f, 0x68, 0xb5, 0x91, 0x06, 0xc0, 0x55, 0x4e, 0x8d, 0xe5, + 0x9e, 0x02, 0x09, 0x75, 0xa5, 0xa8, 0x84, 0xb8, 0xaa, 0xe2, 0x64, 0x79, 0x8a, 0xa9, 0xe1, 0x06, 0x7a, 0x11, 0x05, + 0x72, 0xc5, 0x05, 0x90, 0xf4, 0x9c, 0xfd, 0x9e, 0x69, 0xac, 0xf1, 0xe7, 0x12, 0x05, 0x4c, 0x1a, 0x35, 0x18, 0x2b, + 0x65, 0x2f, 0xa5, 0x89, 0xf6, 0x16, 0x04, 0xb5, 0x7b, 0xf9, 0x17, 0xd4, 0xfd, 0x1c, 0x5a, 0x11, 0x36, 0xc0, 0x10, + 0xe5, 0x39, 0xee, 0xd0, 0xd4, 0x2e, 0x39, 0x0f, 0x18, 0xd1, 0x79, 0x9f, 0xd5, 0x76, 0xab, 0x3f, 0x5f, 0x02, 0xb6, + 0x69, 0x6a, 0x7c, 0x0a, 0xc3, 0x84, 0x98, 0xd8, 0xc0, 0x56, 0x59, 0x69, 0x37, 0x94, 0x69, 0x27, 0x5d, 0x32, 0xaf, + 0x85, 0xd3, 0xbc, 0xc7, 0xd8, 0x72, 0xa4, 0x72, 0xf7, 0xfb, 0xa1, 0xf9, 0xc9, 0x72, 0xfa, 0x5c, 0x87, 0x6c, 0xf6, + 0xc6, 0x83, 0xe6, 0x44, 0xab, 0xab, 0x3a, 0xfa, 0x01, 0x1d, 0x80, 0x99, 0xb6, 0x00, 0x99, 0x2e, 0xd8, 0xb4, 0xaf, + 0x44, 0xc5, 0x25, 0x09, 0x4b, 0x25, 0x81, 0x9d, 0xdd, 0x94, 0xec, 0x6c, 0x02, 0xe2, 0x19, 0xee, 0x7a, 0x5a, 0xec, + 0x84, 0x34, 0xe1, 0x2d, 0xf6, 0x12, 0x10, 0x75, 0xa8, 0xea, 0x12, 0xb2, 0x31, 0x86, 0x2e, 0xfe, 0x45, 0x29, 0x4c, + 0x58, 0xcb, 0xa4, 0x2a, 0x31, 0x41, 0x90, 0xca, 0xdd, 0x16, 0x81, 0x25, 0x0a, 0x76, 0x00, 0x7b, 0xef, 0x46, 0xdd, + 0x8c, 0x9a, 0xaa, 0x4e, 0xbd, 0x04, 0x1f, 0xa7, 0x59, 0x57, 0x41, 0x66, 0x61, 0x57, 0xc5, 0x9a, 0x07, 0x3a, 0x36, + 0x95, 0x32, 0x26, 0xee, 0xd2, 0x22, 0x43, 0x3c, 0x60, 0x8c, 0xa5, 0x0b, 0x81, 0x7c, 0xb3, 0xdd, 0x71, 0xd3, 0x13, + 0x84, 0x7e, 0xc2, 0x86, 0x12, 0xb8, 0xe9, 0x6c, 0x4f, 0x4d, 0x33, 0x1f, 0x10, 0x71, 0x18, 0x50, 0x20, 0xd9, 0x38, + 0xa4, 0x39, 0xd2, 0x17, 0x24, 0x4d, 0x18, 0x18, 0x5a, 0xf1, 0x9c, 0x20, 0x2b, 0x0a, 0x3d, 0x5b, 0x57, 0x6d, 0x9c, + 0x2b, 0xc3, 0x1c, 0x2d, 0x39, 0x15, 0x3e, 0x27, 0xc8, 0xc4, 0xee, 0x69, 0x9b, 0x99, 0x0c, 0x47, 0xc9, 0x02, 0xf3, + 0x2b, 0x88, 0x12, 0x77, 0xa6, 0x59, 0x95, 0x83, 0x71, 0x01, 0x0b, 0xb4, 0xf2, 0x3d, 0xa8, 0x1b, 0x6b, 0x68, 0xa3, + 0x61, 0x88, 0xdd, 0xfe, 0x04, 0xfb, 0xb5, 0x76, 0x5a, 0x97, 0x29, 0x96, 0x97, 0x29, 0x44, 0x7b, 0x21, 0xf3, 0x1b, + 0x45, 0xa2, 0x3b, 0x45, 0x18, 0x12, 0xd6, 0x51, 0xf6, 0xa4, 0x4d, 0x0d, 0xa0, 0xa7, 0x5e, 0xc0, 0xf3, 0xce, 0xb5, + 0x0c, 0xbb, 0x48, 0xf7, 0x57, 0x05, 0x9f, 0xd2, 0x0d, 0x82, 0x14, 0xbd, 0x49, 0xc1, 0x9c, 0xd7, 0xa3, 0xa4, 0xde, + 0x9c, 0xb6, 0xcc, 0xa8, 0x3a, 0x2a, 0x42, 0xca, 0x09, 0xfe, 0x93, 0x97, 0x52, 0x13, 0x9b, 0x30, 0xc1, 0x03, 0x1f, + 0xe6, 0x19, 0x36, 0xf0, 0x76, 0xfb, 0x2e, 0x0d, 0x93, 0x36, 0xdb, 0x90, 0x82, 0xb4, 0xc2, 0xc4, 0xc5, 0x80, 0xca, + 0x5e, 0xe3, 0x7e, 0xc1, 0x76, 0xd2, 0x14, 0x3c, 0x08, 0x1b, 0x0d, 0x4c, 0xdc, 0xea, 0xe2, 0xeb, 0x30, 0xa1, 0xe1, + 0x92, 0x6a, 0x67, 0x27, 0x2d, 0x69, 0x6e, 0xaf, 0xcb, 0x0b, 0xdb, 0x07, 0x1d, 0x3b, 0xac, 0x6b, 0x78, 0xa0, 0x79, + 0xcd, 0x2e, 0xae, 0x98, 0xa6, 0x89, 0xc6, 0x7a, 0x48, 0x59, 0x72, 0xac, 0xeb, 0xe9, 0x0a, 0x57, 0xcb, 0x4c, 0x03, + 0xbb, 0x4b, 0xbc, 0xd0, 0x03, 0x1e, 0x76, 0xb8, 0x22, 0xd1, 0x05, 0x36, 0x9b, 0xad, 0x6a, 0x32, 0xcd, 0xef, 0xcb, + 0x96, 0x9b, 0x80, 0x70, 0x96, 0xfa, 0xe6, 0x3e, 0x39, 0xd6, 0xb4, 0xcd, 0x4f, 0x02, 0x1c, 0x6f, 0xaf, 0x80, 0xa4, + 0x63, 0x09, 0xba, 0xf8, 0x96, 0xfe, 0x20, 0x52, 0x33, 0x15, 0xf4, 0xde, 0xf9, 0x22, 0x75, 0xf3, 0x0b, 0xb0, 0x8d, + 0xda, 0x18, 0xd3, 0xac, 0x6c, 0x1d, 0x26, 0xca, 0xc2, 0x1a, 0x59, 0xc8, 0x25, 0xf8, 0x60, 0xee, 0x36, 0x75, 0x7a, + 0xdc, 0x41, 0x84, 0xfd, 0x2e, 0x7a, 0x3c, 0xc2, 0x58, 0xb1, 0x06, 0x89, 0x61, 0x15, 0xd6, 0xb4, 0xb9, 0x1c, 0xa2, + 0x9c, 0x9a, 0x25, 0x13, 0x2d, 0xa9, 0x4f, 0x29, 0xa2, 0x14, 0xcc, 0x8d, 0xa7, 0x65, 0xc3, 0x94, 0x10, 0x21, 0x2b, + 0xa4, 0x03, 0xaa, 0xb5, 0xd0, 0x52, 0x4d, 0xd0, 0xeb, 0xd0, 0xcb, 0x42, 0x63, 0x0a, 0xa2, 0x8f, 0xc8, 0x70, 0x23, + 0x8e, 0x8c, 0xee, 0x8e, 0x51, 0x4c, 0x20, 0x54, 0xb5, 0x97, 0x17, 0x56, 0x9f, 0x96, 0x6d, 0x75, 0x10, 0x57, 0x88, + 0x7c, 0xdf, 0x4d, 0x50, 0x63, 0x14, 0xb4, 0x39, 0xdd, 0xe8, 0x2f, 0x45, 0xe8, 0xdb, 0x85, 0x63, 0x37, 0x0a, 0x22, + 0x21, 0x02, 0xab, 0xd7, 0x54, 0x0c, 0xc8, 0x3a, 0x8f, 0x5d, 0x84, 0x26, 0xdd, 0x2d, 0x44, 0x79, 0xa3, 0xb2, 0xfe, + 0xb8, 0x0e, 0xc9, 0x76, 0x8b, 0x65, 0x81, 0x2f, 0xfb, 0xe9, 0xfa, 0x1e, 0xc8, 0xef, 0x37, 0xeb, 0xcf, 0x42, 0x7e, + 0xbf, 0xce, 0xbe, 0x04, 0xf2, 0xfb, 0xcd, 0xfa, 0x7f, 0x1a, 0xf2, 0xfb, 0x74, 0xed, 0x41, 0x7e, 0xab, 0xc1, 0xf8, + 0xad, 0x60, 0xc1, 0xc9, 0xdb, 0x80, 0xbe, 0x90, 0x2c, 0x38, 0x79, 0xf5, 0xca, 0x37, 0x02, 0x11, 0x1a, 0xb9, 0xde, + 0xc8, 0x82, 0x11, 0xb7, 0x05, 0x5e, 0xa1, 0xd6, 0xc9, 0x07, 0x2a, 0xca, 0x00, 0x78, 0xbd, 0xfc, 0x67, 0x56, 0x2d, + 0xc3, 0x60, 0x3f, 0x20, 0x33, 0x07, 0x09, 0x3a, 0x9c, 0xc0, 0xed, 0x0d, 0x4a, 0xf9, 0xfe, 0x8b, 0xd0, 0xc3, 0x47, + 0xa3, 0x51, 0x5c, 0x5c, 0xe2, 0x9d, 0xce, 0xec, 0x23, 0xc4, 0x3b, 0xce, 0x78, 0x69, 0x23, 0x44, 0x2c, 0xe3, 0xf2, + 0x4c, 0x87, 0x66, 0x29, 0xed, 0x4e, 0x84, 0x80, 0xf3, 0x67, 0x00, 0x53, 0x6f, 0xb7, 0x66, 0x8c, 0xdd, 0x50, 0x0c, + 0xb1, 0x8e, 0x1f, 0xfb, 0x7c, 0xad, 0xdf, 0x9d, 0xc7, 0x25, 0x7f, 0x17, 0x57, 0x4b, 0x06, 0x9d, 0xd4, 0xdb, 0xb5, + 0x90, 0xeb, 0x15, 0x54, 0x02, 0x37, 0x13, 0xc1, 0x93, 0xca, 0x63, 0xa2, 0x14, 0x6c, 0x79, 0x46, 0x0d, 0x70, 0x79, + 0x47, 0x0e, 0x1a, 0xda, 0x61, 0xd2, 0x3e, 0xc1, 0x46, 0xda, 0x9c, 0x81, 0x11, 0xe3, 0xcb, 0x6b, 0x2e, 0xaa, 0x9f, + 0x00, 0x5f, 0x5d, 0xf0, 0x02, 0x6e, 0x0d, 0xc8, 0xd5, 0x02, 0xd8, 0x0a, 0x94, 0x5c, 0x58, 0xc6, 0x99, 0xd3, 0xd2, + 0xf7, 0xf7, 0x50, 0x67, 0xa5, 0x81, 0x2b, 0x6c, 0x0c, 0x07, 0xde, 0x8f, 0xd0, 0xe7, 0x13, 0xdd, 0x57, 0xc1, 0xb5, + 0xf0, 0xaf, 0x35, 0x3f, 0xcb, 0x52, 0x04, 0xb6, 0xc9, 0x52, 0x6d, 0x35, 0xa4, 0xa4, 0x19, 0x98, 0xb0, 0x51, 0x5e, + 0x17, 0xf0, 0xdb, 0xc3, 0x2f, 0xa5, 0x09, 0xda, 0xf3, 0x94, 0x34, 0x95, 0x80, 0xce, 0x1d, 0xc5, 0x00, 0x71, 0x6a, + 0x0b, 0x8b, 0x20, 0x37, 0xcd, 0xd2, 0x28, 0xb6, 0xea, 0x35, 0x87, 0x5a, 0x4a, 0x15, 0x14, 0xf5, 0x59, 0x12, 0x57, + 0xfc, 0x52, 0x82, 0x9b, 0xea, 0xa8, 0x95, 0x42, 0xc1, 0xba, 0x3a, 0x13, 0x97, 0x67, 0x38, 0xba, 0x11, 0x04, 0x17, + 0x1f, 0x35, 0x92, 0x48, 0x4f, 0x6d, 0xef, 0x22, 0xfa, 0x7e, 0xf4, 0xf2, 0xed, 0x87, 0xd7, 0x1f, 0xfe, 0x75, 0xf6, + 0xfc, 0xf8, 0xc3, 0xcb, 0xef, 0x4f, 0xde, 0xbf, 0x7e, 0x79, 0x3a, 0xb7, 0xce, 0x51, 0x3b, 0x05, 0x93, 0xc5, 0x76, + 0x6b, 0xbf, 0xf8, 0xe5, 0xed, 0x8b, 0x97, 0xaf, 0x5e, 0xbf, 0x7d, 0xf9, 0x82, 0xe2, 0x21, 0x72, 0x26, 0xd6, 0x57, + 0xbc, 0xc8, 0x92, 0xb3, 0x65, 0x56, 0x56, 0xd0, 0xac, 0xb9, 0x8e, 0xfb, 0xb5, 0xa8, 0xa7, 0x09, 0xae, 0x1e, 0xb5, + 0x34, 0x98, 0x59, 0x4d, 0xc7, 0x86, 0xdc, 0x50, 0xff, 0xb5, 0x01, 0xa0, 0x6f, 0x2e, 0xb7, 0x71, 0x6b, 0x55, 0x1a, + 0xd5, 0x6e, 0x2b, 0x55, 0xe1, 0x2c, 0x01, 0x79, 0xd7, 0x53, 0x7c, 0x41, 0x57, 0xd6, 0x06, 0x37, 0xbc, 0x60, 0xb9, + 0x1d, 0x86, 0x1b, 0x25, 0xc4, 0xd1, 0xe3, 0x70, 0x11, 0xe5, 0x0a, 0x5e, 0x68, 0xcd, 0xc2, 0x15, 0x5b, 0xde, 0x93, + 0x6b, 0x15, 0x2d, 0x1b, 0x40, 0x61, 0x79, 0x73, 0x50, 0x0f, 0x97, 0xcd, 0xe7, 0xd9, 0x70, 0x12, 0xb5, 0xb2, 0x30, + 0xc6, 0xda, 0x99, 0x60, 0xe1, 0xac, 0x67, 0xaa, 0xfa, 0x51, 0x25, 0x7f, 0x92, 0x37, 0xe6, 0x5a, 0x7d, 0xb8, 0xec, + 0x48, 0x84, 0x42, 0x27, 0x51, 0x7a, 0xb8, 0x56, 0x3f, 0x00, 0xf8, 0x45, 0x13, 0xe3, 0xbf, 0xd6, 0xdc, 0x40, 0xe3, + 0x87, 0xda, 0x3f, 0xd1, 0x29, 0x12, 0xf4, 0x54, 0x78, 0x26, 0x7d, 0x7a, 0x59, 0xce, 0xc1, 0x9c, 0xcc, 0x1f, 0xc3, + 0xb9, 0xd4, 0x31, 0xbc, 0xdb, 0xf3, 0xb9, 0x98, 0xc6, 0x1a, 0x91, 0x52, 0x73, 0x56, 0xf4, 0xcb, 0xbe, 0x03, 0xaf, + 0x46, 0x15, 0xec, 0x63, 0xf8, 0x6c, 0x4c, 0x6a, 0x6d, 0x69, 0x8f, 0x0b, 0xdc, 0xfe, 0x56, 0x9b, 0xc0, 0x3d, 0xdb, + 0x8d, 0x40, 0x9b, 0x3e, 0x12, 0xed, 0x1a, 0x69, 0x79, 0x4f, 0xf7, 0xc1, 0x2e, 0xbc, 0xba, 0x87, 0x32, 0x54, 0x5d, + 0x94, 0xc1, 0x9f, 0x13, 0x45, 0x21, 0x3e, 0x43, 0x1b, 0x4c, 0xbc, 0x2c, 0x45, 0xc0, 0x3c, 0xb2, 0x50, 0xb0, 0xa3, + 0xa2, 0x09, 0x8a, 0xa5, 0xcd, 0x3f, 0x37, 0xda, 0x6e, 0x02, 0xb6, 0x7d, 0x3d, 0xf5, 0x3f, 0x36, 0xb6, 0x09, 0x7e, + 0x9a, 0x5a, 0xca, 0x70, 0xda, 0x02, 0x99, 0x69, 0x2e, 0xc8, 0xc3, 0xa4, 0x95, 0x80, 0x8b, 0x01, 0x7b, 0xed, 0x13, + 0xd5, 0x8e, 0xbd, 0x8d, 0xb0, 0x7d, 0x1a, 0x1a, 0x31, 0xdc, 0x68, 0xfb, 0xdb, 0x66, 0x59, 0x91, 0xa8, 0x49, 0xb3, + 0x29, 0x0a, 0x9b, 0x08, 0x33, 0x77, 0x6c, 0xfe, 0xd6, 0xd7, 0xc3, 0x49, 0x4d, 0x6a, 0xb7, 0xbb, 0xad, 0xcc, 0xf1, + 0x0f, 0xc5, 0xe7, 0x9c, 0x3d, 0xda, 0x64, 0x7a, 0xba, 0xeb, 0x3f, 0x32, 0x1b, 0xa1, 0xb0, 0x71, 0x68, 0xd4, 0x7a, + 0xdf, 0xfb, 0x00, 0x81, 0x1d, 0xd9, 0x34, 0x71, 0x62, 0x59, 0xe7, 0xc9, 0x33, 0x52, 0x8f, 0x5c, 0x05, 0x6c, 0xeb, + 0xce, 0xc2, 0x6f, 0x79, 0x12, 0x76, 0x35, 0x4c, 0xdd, 0x09, 0x4d, 0x17, 0x10, 0x33, 0x15, 0x34, 0x41, 0xe1, 0x1f, + 0x8f, 0x36, 0xcd, 0x93, 0xac, 0xde, 0xf7, 0x3e, 0xc3, 0xdf, 0x59, 0x0a, 0x7f, 0xab, 0xfa, 0x0f, 0xba, 0xb9, 0xe2, + 0xd5, 0x52, 0xa6, 0x51, 0xf0, 0xee, 0xe4, 0xf4, 0x43, 0xa0, 0xb1, 0xf8, 0xf1, 0x46, 0x6a, 0x6c, 0x10, 0xcc, 0x32, + 0x03, 0x9d, 0x5c, 0x2e, 0x2f, 0x11, 0x8e, 0x52, 0xc7, 0x33, 0x38, 0x5d, 0xca, 0x9b, 0xe3, 0x3c, 0xf7, 0xaf, 0x4d, + 0xe6, 0xac, 0xd5, 0x37, 0x89, 0xc6, 0x89, 0x14, 0x82, 0xf4, 0x77, 0x94, 0x95, 0x67, 0x5a, 0x5f, 0x97, 0x9e, 0x9d, + 0xdf, 0x9d, 0x69, 0x99, 0xa0, 0xc5, 0x03, 0x7d, 0xfe, 0xc7, 0x61, 0x9a, 0x5d, 0xef, 0x21, 0x43, 0xc0, 0x02, 0x70, + 0xa6, 0xc8, 0xf9, 0xf9, 0xba, 0xaa, 0xa4, 0x18, 0x16, 0xf2, 0x26, 0x38, 0x3a, 0x54, 0x0f, 0x26, 0x43, 0xac, 0x1e, + 0x83, 0xbd, 0xff, 0x4a, 0xf2, 0x2c, 0xf9, 0xc8, 0x82, 0x47, 0x9b, 0x8c, 0x1d, 0xb5, 0x8e, 0xfd, 0x71, 0x1d, 0x1c, + 0x41, 0x5b, 0xf7, 0x8e, 0xf3, 0xfc, 0x70, 0x5f, 0x7d, 0x71, 0x74, 0xb8, 0x9f, 0x66, 0xd7, 0x47, 0x5e, 0x68, 0x06, + 0xad, 0xb7, 0x60, 0x1a, 0x02, 0xcf, 0x5a, 0x7a, 0x94, 0xe8, 0x53, 0x9d, 0xf0, 0xd0, 0x31, 0x9f, 0x80, 0xf5, 0xa0, + 0xda, 0x1b, 0x26, 0x28, 0xce, 0xca, 0x81, 0xd5, 0xda, 0x6e, 0x43, 0x6b, 0x07, 0xb6, 0xf4, 0x40, 0x92, 0x50, 0xcc, + 0x8e, 0x59, 0x28, 0x22, 0x3d, 0x90, 0xd0, 0x40, 0x39, 0xe5, 0x84, 0x26, 0x35, 0x05, 0xf6, 0xe3, 0x4d, 0xbc, 0x02, + 0x89, 0xbf, 0xfe, 0xe9, 0x71, 0xa4, 0x09, 0x44, 0x71, 0xf5, 0x16, 0x3a, 0xf1, 0x64, 0x9e, 0x8a, 0xce, 0x6b, 0x9c, + 0x2e, 0x10, 0x56, 0x62, 0x3d, 0x4a, 0x0e, 0x19, 0xe6, 0x04, 0xa2, 0x08, 0x59, 0x70, 0x8c, 0xb8, 0x36, 0x71, 0x7b, + 0xcc, 0xb8, 0xcc, 0x1a, 0x33, 0x14, 0xb5, 0xe7, 0xcb, 0xa0, 0xb6, 0xf5, 0xca, 0xfb, 0xa6, 0x0c, 0x64, 0xe8, 0x61, + 0x45, 0x5b, 0x74, 0x09, 0xbc, 0x6a, 0x3c, 0xc1, 0x2d, 0xa7, 0xe1, 0xbc, 0xa4, 0x72, 0xe1, 0xf6, 0x72, 0xa9, 0x8e, + 0xe2, 0x48, 0xd6, 0x0e, 0x40, 0x55, 0xcd, 0xfa, 0xd1, 0xa3, 0x8d, 0xc0, 0xcd, 0x5f, 0xb2, 0xa3, 0xe6, 0x3a, 0xa8, + 0xe2, 0xf3, 0xe1, 0x92, 0x83, 0xa7, 0x57, 0xb0, 0xf7, 0x5f, 0xe9, 0x79, 0x6e, 0x27, 0x5b, 0xad, 0xf4, 0x65, 0x2c, + 0xd2, 0x9c, 0x7f, 0x88, 0xcf, 0x7f, 0xf8, 0x3f, 0xcd, 0x3d, 0xed, 0x76, 0xdb, 0x36, 0xb2, 0xff, 0xfb, 0x14, 0x0c, + 0x93, 0x4d, 0xc9, 0x84, 0xa4, 0x49, 0xca, 0xb2, 0x15, 0xc9, 0xb2, 0xdb, 0xe6, 0x63, 0x37, 0xbd, 0x6e, 0xd3, 0x93, + 0xb8, 0xb9, 0xbb, 0xeb, 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, 0x65, 0x93, 0x76, 0xf7, 0xb4, 0x49, + 0x44, 0x10, 0x00, 0x31, 0x03, 0x60, 0x30, 0x33, 0x98, 0x0f, 0xac, 0xf3, 0x62, 0x1c, 0x3c, 0x87, 0x0a, 0x99, 0x7a, + 0xfa, 0x68, 0x43, 0xe4, 0xad, 0x89, 0x25, 0xc8, 0x68, 0x09, 0x54, 0xbf, 0x03, 0x1b, 0xdb, 0xf3, 0x43, 0x16, 0x53, + 0x6b, 0x1c, 0x2c, 0x91, 0x24, 0x8a, 0x23, 0x59, 0x1e, 0x19, 0x4f, 0xb9, 0x01, 0x6b, 0x53, 0xe1, 0x7b, 0x0c, 0xc6, + 0x15, 0x89, 0xfd, 0x26, 0xaf, 0x4c, 0x79, 0xb0, 0x2f, 0xb1, 0xdd, 0xdb, 0xe8, 0x56, 0x8c, 0x94, 0x23, 0x80, 0x42, + 0xc4, 0x9d, 0x3d, 0x1f, 0x9d, 0xc8, 0x6a, 0x59, 0xd4, 0x5d, 0x51, 0xbf, 0xf0, 0x2b, 0x53, 0x15, 0x6e, 0x22, 0xaa, + 0x42, 0x86, 0x13, 0xf5, 0xf4, 0xe4, 0x40, 0xae, 0x7d, 0x3a, 0xea, 0x9f, 0x4b, 0xc0, 0x61, 0xaf, 0x80, 0x84, 0x72, + 0x59, 0x8d, 0x83, 0x81, 0x1c, 0x5c, 0x05, 0x8f, 0x43, 0xcb, 0x43, 0x50, 0xb9, 0x48, 0xef, 0xe7, 0x73, 0x44, 0xa6, + 0x4f, 0xa2, 0xb7, 0x11, 0xff, 0xb7, 0x80, 0x19, 0x55, 0x49, 0x2c, 0x4c, 0xa2, 0x58, 0x05, 0x38, 0xaa, 0x89, 0x49, + 0x14, 0x29, 0x01, 0x10, 0x42, 0xd4, 0x78, 0x22, 0x03, 0x46, 0x0e, 0xaa, 0x4d, 0x25, 0xc0, 0x46, 0x7a, 0xf1, 0x43, + 0xe1, 0xc0, 0x54, 0xa8, 0x52, 0x3e, 0xc0, 0xf6, 0x04, 0x32, 0x97, 0x6f, 0x7c, 0xe3, 0x7f, 0x23, 0x63, 0xee, 0x19, + 0x4b, 0xcf, 0x78, 0x17, 0x5e, 0x65, 0x8d, 0xb3, 0x93, 0x27, 0x27, 0x32, 0xd8, 0x40, 0x83, 0x10, 0x27, 0x5c, 0xf0, + 0xe4, 0xe2, 0x98, 0x6f, 0xf1, 0x4b, 0xd9, 0x0b, 0x2f, 0x9e, 0x33, 0x91, 0x13, 0x48, 0xbc, 0x4d, 0x39, 0x56, 0x74, + 0x09, 0x2d, 0x10, 0xff, 0xe7, 0x01, 0xb7, 0x5d, 0xf1, 0xad, 0x49, 0x1a, 0x07, 0xff, 0xc3, 0xee, 0x41, 0x1c, 0x48, + 0xd2, 0x68, 0x05, 0x42, 0xa1, 0x37, 0xe7, 0x4a, 0x3e, 0x43, 0x63, 0xfb, 0x7d, 0xee, 0xe3, 0x47, 0x66, 0xe1, 0x92, + 0x04, 0x7e, 0xc1, 0x4a, 0xa3, 0xf9, 0x3c, 0x60, 0x9a, 0x2a, 0xb2, 0xd4, 0xa8, 0x46, 0xfe, 0x99, 0xb3, 0x07, 0xb6, + 0x08, 0x0d, 0xab, 0x67, 0x6d, 0x3b, 0x47, 0x40, 0xcc, 0xf2, 0xd8, 0x89, 0x24, 0x23, 0xe1, 0x25, 0xc0, 0x0d, 0xde, + 0xa3, 0xf1, 0x79, 0x29, 0x76, 0xa6, 0x39, 0x8d, 0xd6, 0xe3, 0x80, 0x99, 0xb8, 0xdc, 0xe1, 0x93, 0x9b, 0xf1, 0x7a, + 0x3c, 0x86, 0x74, 0x40, 0x0f, 0x6c, 0x03, 0x02, 0x1c, 0x45, 0x09, 0x2a, 0x1e, 0x32, 0x7d, 0x00, 0x40, 0x59, 0x69, + 0x75, 0xf8, 0x60, 0x94, 0x04, 0x3a, 0x45, 0xfa, 0x40, 0x0a, 0x4a, 0x86, 0xfa, 0xa6, 0x1d, 0xaa, 0xef, 0x60, 0xf1, + 0x25, 0xea, 0xa0, 0x81, 0x73, 0x18, 0x5e, 0xaa, 0xef, 0x10, 0xc3, 0x98, 0x24, 0xfb, 0x39, 0xad, 0x5d, 0xd5, 0x50, + 0xc9, 0xb6, 0x62, 0x8d, 0xe9, 0x32, 0xe0, 0x6e, 0xe1, 0x85, 0xef, 0xcd, 0xc3, 0x28, 0x49, 0xfd, 0x89, 0x7a, 0x35, + 0x78, 0xed, 0x6b, 0x97, 0xcb, 0x54, 0xd3, 0xaf, 0x8c, 0x3f, 0xcb, 0x89, 0x76, 0x04, 0x29, 0xc4, 0x3c, 0x3b, 0x2d, + 0x75, 0xe4, 0xdd, 0xb3, 0xad, 0x9e, 0x20, 0xb9, 0x58, 0xe7, 0xcf, 0x43, 0xa8, 0x55, 0x49, 0xd9, 0x83, 0xb9, 0xc7, + 0x20, 0x65, 0xcf, 0x9f, 0xf5, 0x01, 0x09, 0xc3, 0xcf, 0xd7, 0x1b, 0x3c, 0xfa, 0xd3, 0xe2, 0x74, 0xc5, 0x28, 0xd3, + 0xc2, 0x35, 0x8b, 0x9e, 0x1f, 0xc8, 0x66, 0xc5, 0xa5, 0x73, 0x7a, 0xf4, 0x6d, 0x99, 0x8f, 0x80, 0xf3, 0x1e, 0x6c, + 0x7a, 0xc2, 0x28, 0x55, 0x20, 0x74, 0x12, 0x7c, 0x70, 0x54, 0x35, 0x43, 0xe4, 0xbd, 0x6a, 0x7a, 0xc6, 0xa9, 0xc0, + 0x77, 0x78, 0x58, 0x6a, 0x3c, 0x80, 0x5e, 0x29, 0x46, 0x0a, 0xdd, 0x94, 0x87, 0x30, 0x73, 0x25, 0xee, 0x5f, 0xa2, + 0xe9, 0x3b, 0xcf, 0x6a, 0x4d, 0xc8, 0x40, 0x22, 0x6f, 0xa4, 0xc6, 0x45, 0x59, 0xc1, 0x18, 0x55, 0x36, 0x13, 0x9a, + 0x16, 0x09, 0x9e, 0x27, 0x05, 0x4b, 0x44, 0xa4, 0xf1, 0x40, 0x8b, 0xf8, 0xb1, 0x3e, 0xca, 0xae, 0x45, 0x72, 0x6e, + 0x91, 0x1a, 0xdb, 0x88, 0xe4, 0xbc, 0x4b, 0x7e, 0xb8, 0x5a, 0xa7, 0x18, 0xcc, 0x19, 0x06, 0xc0, 0x32, 0x55, 0x41, + 0x3e, 0x18, 0xc8, 0x73, 0xc1, 0xd2, 0x67, 0xaa, 0xe2, 0x4f, 0xeb, 0x65, 0x5c, 0x40, 0x01, 0xaa, 0x85, 0x84, 0x1d, + 0x55, 0xa1, 0xf0, 0x18, 0x73, 0x30, 0x26, 0x46, 0x91, 0x09, 0x41, 0x9b, 0xe0, 0x95, 0x61, 0x03, 0x49, 0x98, 0x50, + 0x3f, 0x03, 0x2d, 0x68, 0x04, 0x16, 0x02, 0xbc, 0x96, 0xc0, 0x1c, 0x3d, 0xda, 0x84, 0xd9, 0xd9, 0xa3, 0x4d, 0x92, + 0x0d, 0x1f, 0x6d, 0xbc, 0xdc, 0x1a, 0x45, 0xbd, 0x50, 0xc9, 0x14, 0x65, 0x84, 0x68, 0x18, 0x65, 0xd7, 0x85, 0x6f, + 0x58, 0x01, 0x2f, 0x2c, 0x32, 0x2a, 0x57, 0xd0, 0x38, 0x64, 0xc8, 0x4d, 0x40, 0x56, 0xb1, 0xbf, 0xf4, 0xe2, 0x7b, + 0xb2, 0x18, 0x31, 0x64, 0xb3, 0x12, 0x5d, 0x55, 0x88, 0xc2, 0x13, 0x02, 0x88, 0xd8, 0xab, 0xca, 0x37, 0x79, 0x99, + 0xd0, 0x4d, 0xe4, 0xd7, 0xe6, 0xf0, 0xad, 0x6b, 0xf5, 0x29, 0xb3, 0xa6, 0x2c, 0xf5, 0xfc, 0x80, 0x9a, 0x0c, 0xb4, + 0xa4, 0x05, 0xbc, 0xa4, 0x0c, 0x5e, 0x58, 0x5e, 0x3f, 0x08, 0x0c, 0xd1, 0x7e, 0x1a, 0x37, 0x42, 0x86, 0x79, 0xd2, + 0x9a, 0x67, 0x94, 0xde, 0xfd, 0xa1, 0xd3, 0xc1, 0x60, 0x3a, 0x42, 0x98, 0x0e, 0x16, 0x4e, 0xa2, 0x29, 0xfb, 0xf9, + 0xed, 0xeb, 0x3c, 0x31, 0x1b, 0xe8, 0x18, 0x47, 0x7c, 0x61, 0x26, 0xc8, 0x38, 0xc4, 0xc8, 0x34, 0x50, 0x0a, 0x35, + 0x25, 0x5f, 0x42, 0x71, 0xa6, 0x2a, 0x67, 0x34, 0x76, 0x36, 0xa5, 0x51, 0x0f, 0x23, 0x6c, 0x15, 0x67, 0x27, 0x07, + 0x54, 0x9b, 0x8e, 0x39, 0xaa, 0x04, 0x68, 0x88, 0x01, 0xc2, 0x02, 0x0b, 0x90, 0x43, 0x76, 0xe8, 0x14, 0x02, 0x88, + 0xb5, 0xc4, 0x9b, 0x1c, 0xe7, 0xac, 0xcc, 0xa3, 0x60, 0x2b, 0xf5, 0xf4, 0x04, 0xb3, 0xc2, 0xc1, 0x41, 0x0d, 0x71, + 0x64, 0x4e, 0x0e, 0xe8, 0x51, 0xa9, 0xec, 0x88, 0xa2, 0x13, 0x21, 0x86, 0xf7, 0x79, 0x07, 0x9f, 0xb4, 0x55, 0x92, + 0x94, 0xad, 0xa0, 0xd4, 0xcb, 0x54, 0x65, 0xc9, 0x79, 0x22, 0x1e, 0xb0, 0x0a, 0xa2, 0x59, 0xd8, 0xb0, 0x77, 0x55, + 0x65, 0xe9, 0xdd, 0x21, 0xe4, 0xe2, 0x8d, 0x77, 0xa7, 0x39, 0xfc, 0x55, 0xb1, 0xd7, 0x92, 0xf2, 0x5e, 0x9b, 0xf0, + 0xc9, 0x05, 0x57, 0x15, 0xc1, 0x0b, 0x6b, 0x0b, 0x34, 0x01, 0x68, 0x98, 0xdc, 0x85, 0x98, 0xdc, 0x69, 0xcb, 0xe4, + 0x4e, 0xb7, 0x4c, 0x6e, 0xc0, 0x27, 0x52, 0xc9, 0x51, 0x17, 0xa3, 0xfb, 0x61, 0x8e, 0x3c, 0xce, 0x61, 0xf4, 0xf9, + 0x3e, 0x43, 0x3c, 0x99, 0x49, 0x00, 0xe6, 0x77, 0x2d, 0xb8, 0x6a, 0xc2, 0x8b, 0x84, 0x88, 0x3a, 0xe0, 0xf9, 0xae, + 0x13, 0x70, 0x43, 0x96, 0x57, 0x2d, 0xa8, 0xc2, 0x0b, 0xab, 0x94, 0x32, 0xd8, 0x6b, 0x8b, 0x16, 0x48, 0x17, 0x5b, + 0x20, 0x9d, 0x94, 0xb6, 0x2e, 0x07, 0x9b, 0x36, 0xa1, 0x0c, 0x14, 0xac, 0x41, 0x30, 0x49, 0xc6, 0x25, 0x53, 0x5e, + 0x87, 0xed, 0x34, 0x56, 0x5a, 0x51, 0x2b, 0x2f, 0x49, 0x6e, 0xa3, 0x18, 0xee, 0xf4, 0xa0, 0x9b, 0x4f, 0x5b, 0x52, + 0x4b, 0x3f, 0xe4, 0xe1, 0x82, 0x5a, 0x17, 0x53, 0xf1, 0x5e, 0x5e, 0x52, 0x6e, 0xb7, 0x4b, 0x35, 0x56, 0x5e, 0x9a, + 0xb2, 0x18, 0x91, 0xee, 0x41, 0x5c, 0xf9, 0xff, 0x92, 0x65, 0xd6, 0x40, 0x43, 0x02, 0x89, 0xaa, 0x23, 0x85, 0x5e, + 0x01, 0x53, 0x15, 0x8b, 0x83, 0x58, 0x0a, 0x3d, 0x18, 0xe7, 0x88, 0xff, 0x11, 0xb7, 0xab, 0x16, 0x4b, 0xce, 0x71, + 0xce, 0x91, 0x6e, 0xad, 0xbc, 0xe9, 0x3b, 0xb8, 0x3a, 0xd6, 0x5c, 0x03, 0xcc, 0xc0, 0xe5, 0x40, 0x83, 0x31, 0x71, + 0x79, 0x93, 0x82, 0x48, 0x22, 0x95, 0xe4, 0x46, 0x76, 0xe0, 0x9f, 0xeb, 0x99, 0xb3, 0xab, 0x8d, 0x9b, 0x1d, 0xcc, + 0x7d, 0xbd, 0x46, 0x35, 0x81, 0xb4, 0x05, 0xc3, 0xd3, 0x5c, 0x13, 0x1b, 0x18, 0xce, 0x91, 0x0e, 0x77, 0x0b, 0x97, + 0x90, 0x31, 0xd7, 0x16, 0xf2, 0xef, 0x28, 0x86, 0x53, 0xeb, 0xd2, 0xbe, 0xca, 0x1e, 0xcf, 0xf1, 0x97, 0x73, 0x95, + 0x3d, 0x1e, 0xe3, 0x2f, 0xf7, 0x0a, 0x73, 0x23, 0x36, 0x08, 0xfe, 0x12, 0xcc, 0xea, 0x69, 0x69, 0x3d, 0x91, 0x85, + 0xe3, 0x27, 0x2c, 0x1b, 0x3e, 0xc1, 0x0f, 0x1f, 0x6d, 0x12, 0xf0, 0xe9, 0x95, 0x61, 0x08, 0xad, 0x58, 0xcf, 0x1a, + 0xcb, 0xe7, 0x2d, 0xe5, 0x63, 0xfd, 0x0f, 0x3e, 0xf8, 0x71, 0x95, 0x44, 0xc5, 0x99, 0x52, 0x56, 0x5b, 0x5c, 0x8f, + 0xfd, 0xd0, 0x8b, 0xef, 0xaf, 0x49, 0xae, 0xd0, 0x04, 0xd3, 0x9e, 0xab, 0x63, 0x88, 0xbb, 0x2c, 0x5f, 0xa8, 0xa6, + 0xd2, 0x65, 0xc1, 0x3d, 0x3f, 0xe8, 0x87, 0x7f, 0x8d, 0x25, 0xb6, 0xad, 0x24, 0x79, 0xf2, 0x09, 0x29, 0x7d, 0xe8, + 0xfa, 0xd1, 0x46, 0x63, 0xf5, 0x6e, 0x2a, 0xd0, 0x56, 0xf8, 0x42, 0x98, 0x1e, 0x94, 0x62, 0x97, 0x53, 0xbf, 0x8f, + 0x37, 0xa6, 0xe3, 0xe8, 0xce, 0x7c, 0xb4, 0x49, 0xcf, 0xd4, 0xa5, 0x17, 0x7f, 0x60, 0x53, 0x73, 0xe2, 0xc7, 0x93, + 0x80, 0xa9, 0x7d, 0x75, 0x1c, 0x78, 0xe1, 0x07, 0xfe, 0x68, 0x46, 0xeb, 0x14, 0x6d, 0x20, 0x76, 0x0a, 0xbd, 0x02, + 0x27, 0xa4, 0x5e, 0x45, 0x66, 0xb5, 0x01, 0x0b, 0xca, 0xf3, 0x5c, 0x41, 0x56, 0x30, 0x8a, 0x45, 0x2d, 0x03, 0x4c, + 0x78, 0xc1, 0x2c, 0x03, 0x7c, 0xa2, 0x0d, 0x15, 0xe7, 0x4b, 0x35, 0x64, 0x50, 0x49, 0xb1, 0x9c, 0x27, 0xf5, 0xbc, + 0xc6, 0x1e, 0xfe, 0xfd, 0xcf, 0x51, 0xba, 0xf5, 0xfd, 0x3f, 0x97, 0xf7, 0xf2, 0x79, 0x10, 0x42, 0xa9, 0x49, 0xbe, + 0x38, 0x9f, 0xf0, 0x71, 0xce, 0x60, 0xb6, 0x7f, 0x5a, 0x6e, 0xec, 0x25, 0xc9, 0x7a, 0xc9, 0xa6, 0x74, 0xf7, 0x7c, + 0x56, 0x0c, 0xaa, 0x2c, 0x59, 0xc8, 0x03, 0xfb, 0x65, 0xed, 0x1e, 0x1f, 0x3e, 0x07, 0x9b, 0x18, 0x60, 0x28, 0xa3, + 0xd9, 0x4c, 0x2d, 0x84, 0xfb, 0x1d, 0xcd, 0x9c, 0xc3, 0x5f, 0xd6, 0xaf, 0x5e, 0xda, 0xaf, 0xf2, 0xc6, 0x21, 0x30, + 0xc6, 0xe2, 0x82, 0x9f, 0xf3, 0xc5, 0xd2, 0x78, 0x05, 0x44, 0x33, 0x2f, 0x6c, 0x07, 0xe7, 0xb2, 0xb4, 0xc4, 0x57, + 0x8c, 0x4d, 0x81, 0xe1, 0x36, 0x6a, 0xa5, 0xd7, 0x01, 0xbb, 0x61, 0xb9, 0xf1, 0x40, 0xfd, 0x63, 0x0d, 0x2d, 0x30, + 0xba, 0x21, 0x37, 0x4a, 0xe0, 0x5c, 0x9d, 0x04, 0xd2, 0x08, 0x61, 0xe0, 0x90, 0xcb, 0x5b, 0xac, 0xb2, 0xa5, 0x46, + 0x86, 0x2a, 0x0d, 0xa0, 0x75, 0x64, 0x67, 0x2d, 0xe5, 0x7d, 0x4c, 0x6d, 0xde, 0x3c, 0x36, 0xc3, 0xd1, 0xfb, 0x10, + 0x0d, 0x9e, 0xe3, 0x29, 0x80, 0x9d, 0xa7, 0x15, 0x7a, 0x90, 0x36, 0x8c, 0x35, 0x69, 0xc7, 0x54, 0x52, 0xbb, 0x08, + 0x7b, 0x5a, 0x34, 0x2c, 0x17, 0x0a, 0xa8, 0xc6, 0xb9, 0x51, 0xca, 0x90, 0x8f, 0x31, 0x65, 0x70, 0xc8, 0x92, 0xa4, + 0x15, 0x61, 0xf9, 0xa4, 0x1b, 0x6a, 0x51, 0xbb, 0x8c, 0x8f, 0xa2, 0xdc, 0xb0, 0x0d, 0x60, 0x09, 0x10, 0xc0, 0xea, + 0xb7, 0xf0, 0x78, 0xb9, 0x5e, 0x72, 0x8b, 0xa8, 0x78, 0x3e, 0x56, 0xb9, 0xb5, 0x4a, 0xdb, 0xfb, 0x5b, 0x95, 0x0f, + 0xaa, 0x74, 0x4c, 0x37, 0x0e, 0x4d, 0x2b, 0x91, 0xde, 0x9a, 0x9e, 0x08, 0x3b, 0x10, 0x63, 0xaa, 0xd0, 0x57, 0x36, + 0x9b, 0xb1, 0x49, 0x9a, 0xe8, 0x42, 0x6b, 0x94, 0xc7, 0x27, 0x06, 0xbf, 0xb4, 0x07, 0x43, 0xf5, 0x47, 0x88, 0xd2, + 0x20, 0xc2, 0x78, 0xf1, 0x01, 0x09, 0x99, 0xa9, 0x19, 0x4d, 0xd4, 0x63, 0x19, 0x45, 0xfc, 0x2b, 0xa0, 0x38, 0x6e, + 0x28, 0xc7, 0xa1, 0xf1, 0xf3, 0xa7, 0x58, 0x17, 0x51, 0x6e, 0x37, 0xb5, 0x9d, 0x14, 0x6d, 0xdb, 0xbe, 0x1b, 0xe7, + 0x55, 0xd7, 0xb1, 0x33, 0xd5, 0x00, 0xef, 0xc0, 0x0f, 0xfb, 0x6e, 0x7a, 0x6c, 0xd5, 0x81, 0x56, 0xeb, 0xf0, 0x53, + 0xda, 0xb9, 0xce, 0x33, 0x47, 0x35, 0xc8, 0x28, 0x53, 0xa2, 0x6d, 0x93, 0xe8, 0x86, 0xc5, 0x9f, 0x0d, 0x4a, 0xb9, + 0xf3, 0xfd, 0xc6, 0x73, 0xe4, 0xd8, 0x40, 0x84, 0xd3, 0x68, 0xf5, 0x09, 0x20, 0x74, 0x54, 0x43, 0x9d, 0x04, 0x51, + 0xc2, 0x64, 0x18, 0x48, 0x09, 0xf2, 0x99, 0x40, 0xfc, 0xf4, 0xf6, 0xe5, 0xbb, 0x77, 0xaa, 0x81, 0xb9, 0x66, 0x13, + 0xb9, 0x77, 0xbe, 0xa0, 0x76, 0x50, 0xff, 0xc6, 0x75, 0x47, 0x27, 0x0c, 0x09, 0xb5, 0xe5, 0x35, 0x47, 0x65, 0xb5, + 0x25, 0xc7, 0x4f, 0x1e, 0xfe, 0x65, 0x92, 0x44, 0xf7, 0x82, 0xab, 0x81, 0x36, 0x6c, 0x3f, 0xde, 0x4a, 0x25, 0x4b, + 0x3f, 0xbc, 0x6e, 0x28, 0xf5, 0xee, 0x1a, 0x4a, 0x41, 0x94, 0xab, 0xd1, 0xaa, 0x75, 0xb4, 0x94, 0x58, 0x03, 0x48, + 0x15, 0xbe, 0x0b, 0x5d, 0x92, 0x3c, 0xf5, 0x19, 0x83, 0xe6, 0xb9, 0x02, 0xaa, 0xa3, 0x6e, 0x28, 0xe6, 0x42, 0x50, + 0x8e, 0xdb, 0x49, 0x00, 0x26, 0xa5, 0x4c, 0xbe, 0xc5, 0x2b, 0xb3, 0x8d, 0xdc, 0x20, 0x7c, 0x58, 0xe1, 0xd0, 0xa9, + 0x19, 0xdd, 0x7c, 0x70, 0xfa, 0xbe, 0xf2, 0xa6, 0x60, 0xa7, 0x69, 0x8e, 0xa3, 0x34, 0x8d, 0x96, 0x7d, 0xc7, 0x5e, + 0xdd, 0xa9, 0xca, 0x40, 0x28, 0x1e, 0xb8, 0x19, 0x69, 0xff, 0xb7, 0x7f, 0x55, 0x48, 0x2e, 0x95, 0x5f, 0xa7, 0x6c, + 0xb9, 0x62, 0xb1, 0x97, 0xae, 0x63, 0x96, 0x29, 0xbf, 0xfd, 0xdf, 0xf3, 0x8a, 0x90, 0x3d, 0x90, 0xdb, 0x10, 0x7b, + 0x2d, 0x37, 0xb9, 0x0e, 0xa2, 0xdb, 0x07, 0x85, 0xc3, 0xc8, 0x8e, 0xca, 0x0b, 0x7f, 0xbe, 0xc8, 0x6b, 0x9f, 0xa5, + 0x5b, 0x60, 0x13, 0xa3, 0x27, 0x6d, 0xbb, 0x72, 0x1e, 0xdd, 0xf6, 0x7f, 0xfb, 0x57, 0xae, 0x3c, 0xd9, 0xb9, 0xea, + 0x9a, 0x07, 0x5a, 0x9e, 0xd1, 0xe6, 0x3a, 0xb5, 0x29, 0x86, 0xf7, 0xb5, 0x09, 0xae, 0x15, 0xd2, 0xaa, 0xac, 0x5f, + 0x6d, 0x6d, 0x81, 0xe9, 0x2f, 0xfe, 0x7c, 0xf1, 0xb9, 0x40, 0x01, 0x42, 0x77, 0x42, 0x05, 0x95, 0xbe, 0x00, 0x58, + 0xa3, 0xfe, 0xfe, 0x13, 0xf6, 0x99, 0x70, 0xed, 0x02, 0xe9, 0x4b, 0x40, 0xc3, 0xb5, 0xa8, 0xcf, 0x47, 0xa3, 0x3c, + 0xd7, 0xa2, 0xdc, 0x1e, 0x5c, 0x5e, 0xce, 0x6a, 0x25, 0xfc, 0xa8, 0xef, 0xdb, 0x3a, 0xc5, 0xa2, 0xd8, 0x03, 0x21, + 0x68, 0xbc, 0xd9, 0x80, 0x8e, 0x76, 0x7a, 0x4d, 0x3e, 0x18, 0xb5, 0x6f, 0xd7, 0x88, 0x35, 0x94, 0x62, 0x9e, 0xbe, + 0xfc, 0x4e, 0xce, 0x68, 0x1e, 0xce, 0x6d, 0xec, 0xad, 0x48, 0x61, 0xaf, 0xe0, 0x7d, 0x04, 0x28, 0x40, 0x84, 0x44, + 0x8b, 0x69, 0x80, 0xde, 0xb4, 0x99, 0x4e, 0xfe, 0xb4, 0xdb, 0x74, 0xf2, 0x62, 0x2f, 0xd3, 0xc9, 0x9f, 0xbe, 0xb8, + 0xe9, 0xe4, 0x1b, 0xd9, 0x74, 0x12, 0xe6, 0xf2, 0x25, 0xdb, 0xcb, 0xa0, 0x51, 0x98, 0x05, 0x45, 0xb7, 0xc9, 0xd0, + 0xe1, 0x7c, 0x78, 0x32, 0x59, 0x30, 0x50, 0x6c, 0x70, 0xac, 0x07, 0xd1, 0x7c, 0xbb, 0xdd, 0xe1, 0x97, 0xb2, 0x3a, + 0x0c, 0xa2, 0xb9, 0x2a, 0x05, 0x42, 0x0e, 0x79, 0x20, 0xe4, 0x22, 0xb8, 0x19, 0x59, 0xf8, 0xd9, 0x86, 0x08, 0x85, + 0x66, 0x1e, 0xea, 0x52, 0xb2, 0xf7, 0xdc, 0xa8, 0xd3, 0x15, 0x36, 0x80, 0x7d, 0x15, 0xc2, 0xa2, 0xe4, 0x0d, 0xdd, + 0xa7, 0x22, 0xe4, 0x8b, 0xdc, 0x43, 0x6e, 0x3c, 0x4f, 0xe1, 0x53, 0x36, 0xea, 0x2f, 0x77, 0xce, 0x77, 0x97, 0xce, + 0xa0, 0xe3, 0x40, 0xcc, 0x02, 0x10, 0x8b, 0xb1, 0xc0, 0x1e, 0x74, 0x3a, 0x50, 0x70, 0x2b, 0x15, 0xb8, 0x50, 0xe0, + 0x4b, 0x05, 0x5d, 0x28, 0x98, 0x48, 0x05, 0x47, 0x50, 0x30, 0x95, 0x0a, 0x8e, 0xa1, 0xe0, 0x46, 0xcd, 0x2e, 0xc3, + 0x7c, 0xb8, 0xc7, 0xfa, 0x95, 0x41, 0x92, 0x90, 0x28, 0x3b, 0x36, 0x1c, 0xb0, 0xe3, 0xf3, 0xe6, 0xfd, 0xc8, 0x20, + 0x95, 0x68, 0x3f, 0x36, 0x6e, 0x17, 0x8c, 0xe2, 0xa7, 0xbf, 0xc0, 0x83, 0xd2, 0x4a, 0x23, 0x70, 0x27, 0x10, 0x71, + 0x49, 0x04, 0x1e, 0x14, 0x55, 0x07, 0x2d, 0xd7, 0x20, 0x9f, 0xb9, 0xb2, 0x01, 0x20, 0xce, 0x65, 0xf1, 0x8e, 0x3e, + 0x67, 0xe6, 0x4b, 0xa0, 0x30, 0xab, 0xd1, 0x64, 0x55, 0xea, 0x97, 0x30, 0x82, 0x78, 0xc1, 0xc6, 0xeb, 0xb9, 0x72, + 0x1e, 0xcd, 0x77, 0x1a, 0x3c, 0xc8, 0xaf, 0x60, 0x94, 0x2a, 0xdd, 0x19, 0x99, 0x62, 0x59, 0xf2, 0x6f, 0xd1, 0x63, + 0x56, 0xae, 0x9f, 0xc2, 0xd8, 0x94, 0x94, 0x28, 0x0e, 0x7c, 0x07, 0x70, 0x24, 0x59, 0x1c, 0x9c, 0x03, 0x9e, 0xa5, + 0xe7, 0x0b, 0x4f, 0x1a, 0xcf, 0xe9, 0x0f, 0x2c, 0x49, 0xbc, 0xb9, 0xa8, 0x5f, 0x1f, 0x27, 0x58, 0x26, 0xe5, 0x42, + 0x23, 0x22, 0x10, 0xd4, 0x8f, 0x7e, 0xcd, 0x50, 0x24, 0x8e, 0x6e, 0x15, 0x30, 0x71, 0x82, 0x05, 0x55, 0x58, 0x55, + 0xf8, 0x16, 0x4c, 0x61, 0xd9, 0xfe, 0x01, 0x36, 0xff, 0x0d, 0x0b, 0xaa, 0x85, 0xa9, 0x37, 0xaf, 0x16, 0xd1, 0x3a, + 0xc8, 0xe4, 0xb1, 0xe5, 0xe6, 0x07, 0xa5, 0xc2, 0xcf, 0xb9, 0x4f, 0x0f, 0xa2, 0xf9, 0xef, 0x7a, 0x99, 0xbe, 0xc5, + 0x08, 0xe2, 0x5d, 0x68, 0x84, 0xe9, 0xc8, 0x42, 0x1c, 0x2b, 0x16, 0xa0, 0xb0, 0x1f, 0xa6, 0x0b, 0x13, 0x3d, 0x2e, + 0x35, 0x37, 0xd4, 0x0d, 0x0b, 0xe7, 0x76, 0x53, 0xf5, 0x33, 0xef, 0xc7, 0xf3, 0xb1, 0xa7, 0x39, 0xee, 0xb1, 0x21, + 0xfe, 0x58, 0x76, 0x57, 0xcf, 0xb0, 0x07, 0x65, 0xea, 0xdf, 0x6c, 0x66, 0x51, 0x98, 0x9a, 0x33, 0x6f, 0xe9, 0x07, + 0xf7, 0xfd, 0x65, 0x14, 0x46, 0xc9, 0xca, 0x9b, 0xb0, 0x41, 0xa1, 0x05, 0x18, 0x60, 0x04, 0x13, 0xee, 0x44, 0xeb, + 0x58, 0x6e, 0xcc, 0x96, 0xd4, 0x3a, 0x0f, 0x50, 0x32, 0x0b, 0xd8, 0x5d, 0xc6, 0x3f, 0x5f, 0xaa, 0x4c, 0x55, 0x71, + 0xc9, 0x51, 0x0b, 0x60, 0xa3, 0x79, 0xf4, 0x13, 0x88, 0xf9, 0x35, 0xe0, 0xbc, 0x68, 0xdf, 0x72, 0xbb, 0x31, 0x5b, + 0x2a, 0x56, 0xb7, 0xb5, 0xf3, 0x38, 0xba, 0x3d, 0x85, 0xd1, 0x62, 0x63, 0x33, 0x61, 0xc1, 0x0c, 0xdf, 0x98, 0xe8, + 0x70, 0x25, 0xfa, 0x31, 0x51, 0x7b, 0x00, 0xbd, 0xb1, 0xe5, 0x00, 0x5e, 0xf7, 0x5d, 0xc5, 0x1e, 0x2c, 0xfd, 0xd0, + 0x24, 0x70, 0x8e, 0xed, 0x95, 0xd4, 0x97, 0x8c, 0x3f, 0x7d, 0x83, 0xd5, 0x1d, 0xc5, 0x1e, 0x80, 0x84, 0x39, 0x0b, + 0xa2, 0xdb, 0xfe, 0xc2, 0x9f, 0x4e, 0x59, 0x38, 0xc0, 0x31, 0xe7, 0x85, 0x2c, 0x08, 0xfc, 0x55, 0xe2, 0x27, 0x83, + 0xa5, 0x77, 0xc7, 0x7b, 0x3d, 0x6c, 0xeb, 0xb5, 0xc3, 0x7b, 0xed, 0xec, 0xdd, 0xab, 0xd4, 0x0d, 0x38, 0x77, 0x51, + 0x3f, 0x7c, 0x68, 0x5d, 0xc5, 0xae, 0xc0, 0xb9, 0x77, 0xaf, 0xab, 0x98, 0x6d, 0x96, 0x5e, 0x3c, 0xf7, 0xc3, 0xbe, + 0x9d, 0x59, 0x37, 0x1b, 0x5a, 0x18, 0x0f, 0x7b, 0xbd, 0x5e, 0x66, 0x4d, 0xc5, 0x93, 0x3d, 0x9d, 0x66, 0xd6, 0x44, + 0x3c, 0xcd, 0x66, 0xb6, 0x3d, 0x9b, 0x65, 0x96, 0x2f, 0x0a, 0x3a, 0xee, 0x64, 0xda, 0x71, 0x33, 0xeb, 0x56, 0xaa, + 0x91, 0x59, 0x8c, 0x3f, 0xc5, 0x6c, 0x3a, 0xc0, 0x85, 0xc4, 0x8d, 0x37, 0x8f, 0x6d, 0x3b, 0x43, 0x0a, 0x70, 0x59, + 0xa2, 0x4d, 0xa8, 0xa0, 0xba, 0xda, 0xec, 0x5d, 0x53, 0x29, 0x3e, 0x37, 0x99, 0x34, 0xd6, 0x9b, 0x7a, 0xf1, 0x87, + 0x2b, 0x45, 0x82, 0xc2, 0xf3, 0xa8, 0xda, 0x46, 0xa0, 0xc1, 0xbc, 0xeb, 0x43, 0x24, 0xbb, 0xc1, 0x38, 0x8a, 0x61, + 0xcf, 0xc6, 0xde, 0xd4, 0x5f, 0x27, 0x7d, 0xc7, 0x5d, 0xdd, 0x89, 0x22, 0xbe, 0xd6, 0x8b, 0x02, 0xdc, 0x7b, 0xfd, + 0x24, 0x0a, 0xfc, 0xa9, 0x28, 0x6a, 0xdb, 0x4b, 0x8e, 0xab, 0x0f, 0x30, 0x8e, 0x83, 0x8f, 0xd1, 0x48, 0xbc, 0x20, + 0x50, 0xac, 0x4e, 0xa2, 0x30, 0x2f, 0x41, 0xa5, 0xb8, 0x62, 0x27, 0x84, 0x17, 0x8c, 0xd9, 0xe0, 0x1c, 0xae, 0xee, + 0xf2, 0x35, 0xef, 0x1c, 0xad, 0xee, 0xb2, 0x6f, 0x96, 0x6c, 0xea, 0x7b, 0x8a, 0x56, 0xac, 0x26, 0xc7, 0x06, 0xc5, + 0xb9, 0xbe, 0x69, 0x59, 0xa6, 0x62, 0x5b, 0x40, 0xc4, 0xcf, 0x07, 0xfe, 0x72, 0x15, 0xc5, 0xa9, 0x17, 0xa6, 0x59, + 0x36, 0xba, 0xca, 0xb2, 0xc1, 0x85, 0xaf, 0x5d, 0xfe, 0x4d, 0xa3, 0x73, 0x9a, 0x2e, 0x9a, 0x32, 0xfd, 0xca, 0x78, + 0xc9, 0x64, 0x33, 0x17, 0x38, 0xc6, 0xd0, 0xc4, 0x45, 0xae, 0x4c, 0xa7, 0x64, 0xbd, 0x32, 0x21, 0x39, 0xaf, 0x4e, + 0x56, 0x33, 0xe5, 0x2a, 0x78, 0x02, 0x41, 0x85, 0x97, 0x6c, 0x78, 0x21, 0xd9, 0xcc, 0x00, 0xb3, 0x82, 0x95, 0xc9, + 0xdd, 0xe6, 0x51, 0x1b, 0xcf, 0xf8, 0xed, 0x6e, 0x9e, 0xf1, 0xef, 0xe9, 0x3e, 0x3c, 0xe3, 0xb7, 0x5f, 0x9c, 0x67, + 0x7c, 0x54, 0x77, 0xb7, 0x79, 0x1d, 0x0d, 0xd5, 0xfc, 0x5a, 0x04, 0x8e, 0xa6, 0x98, 0x02, 0x59, 0xbd, 0x4e, 0xff, + 0x5d, 0xf7, 0x18, 0xd1, 0x1b, 0xa5, 0x66, 0xa4, 0x93, 0x1b, 0x94, 0xc8, 0x6f, 0xc2, 0xe1, 0x5f, 0x63, 0xf9, 0x79, + 0x36, 0x1b, 0xbe, 0x88, 0xa4, 0x82, 0xfc, 0x89, 0x5b, 0x8c, 0x94, 0x82, 0x8e, 0xd0, 0x1b, 0x61, 0x10, 0x8a, 0x69, + 0x59, 0x20, 0x66, 0x01, 0x99, 0xb5, 0x4f, 0x73, 0x5b, 0xb9, 0x41, 0x79, 0x08, 0x5a, 0x6e, 0x7d, 0x2a, 0x3c, 0xd3, + 0x6a, 0xfa, 0xcf, 0x39, 0x4b, 0xb9, 0x2b, 0xf9, 0x77, 0xf7, 0xaf, 0xa7, 0xda, 0xeb, 0x48, 0xcf, 0xfc, 0xe4, 0x4d, + 0xd5, 0x2f, 0x8c, 0x5f, 0x58, 0x0d, 0x65, 0x70, 0x32, 0x6e, 0xef, 0x26, 0xe7, 0x5d, 0x87, 0xd7, 0xd4, 0xfc, 0xac, + 0x04, 0x69, 0x5f, 0x6e, 0xc8, 0xf3, 0xbf, 0xd5, 0x0e, 0x63, 0xee, 0x84, 0xb3, 0xe1, 0x1c, 0x20, 0xa6, 0xb4, 0x43, + 0x77, 0xfa, 0x29, 0x35, 0xf7, 0xa7, 0x59, 0xa6, 0x0f, 0x04, 0x22, 0xa4, 0x83, 0x96, 0xed, 0x62, 0xe2, 0x92, 0x42, + 0x1e, 0xe3, 0xd7, 0x9a, 0x74, 0x67, 0xf9, 0x1a, 0xac, 0x00, 0xf8, 0x0d, 0x27, 0xc7, 0x99, 0xaa, 0x10, 0xfa, 0xc8, + 0x3a, 0x44, 0x02, 0x08, 0xae, 0xad, 0x73, 0xfc, 0x8b, 0x57, 0xa2, 0xa0, 0x6e, 0x71, 0x4a, 0xc8, 0x41, 0x33, 0x06, + 0x08, 0x7e, 0x21, 0x94, 0x35, 0x44, 0x76, 0x78, 0x1d, 0x7c, 0xc8, 0xd4, 0x9c, 0xf7, 0xc3, 0xe5, 0x77, 0x7a, 0x72, + 0x00, 0x0d, 0x4e, 0x2b, 0x8a, 0x98, 0x1d, 0xf6, 0x94, 0xc0, 0x4a, 0x24, 0xb7, 0x86, 0x95, 0xdc, 0x2a, 0x4f, 0x36, + 0x22, 0x70, 0x4c, 0xea, 0xad, 0x4c, 0x90, 0xfe, 0x91, 0xf6, 0x72, 0x8a, 0x27, 0xc5, 0xa8, 0x19, 0xac, 0x13, 0xa0, + 0x8d, 0x28, 0x88, 0x22, 0xfd, 0x19, 0x4c, 0xd6, 0x71, 0x12, 0xc5, 0xfd, 0x55, 0xe4, 0x87, 0x29, 0x8b, 0x33, 0x44, + 0xd5, 0x25, 0xe2, 0x47, 0xa0, 0xe7, 0x6a, 0x13, 0xad, 0xbc, 0x89, 0x9f, 0xde, 0xf7, 0x6d, 0xce, 0x52, 0xd8, 0x03, + 0xce, 0x1d, 0xd8, 0x8d, 0xf5, 0xfb, 0x1c, 0x9b, 0x4f, 0x91, 0xf1, 0x8b, 0xeb, 0xec, 0x8c, 0xbc, 0xcc, 0x07, 0xd2, + 0x5b, 0x0a, 0x9d, 0x03, 0xec, 0x87, 0x17, 0x9b, 0x73, 0xa0, 0xf2, 0x30, 0xd5, 0xf6, 0x94, 0xcd, 0x0d, 0xa4, 0xda, + 0x70, 0x99, 0x20, 0xfe, 0x58, 0x5d, 0x5d, 0xb1, 0x9b, 0x8b, 0x81, 0xe3, 0xd1, 0xf7, 0x19, 0x59, 0xdf, 0x83, 0x44, + 0x73, 0xc6, 0x3e, 0x35, 0xc7, 0x6c, 0x16, 0xc5, 0x8c, 0xc2, 0x2c, 0x3b, 0xbd, 0xd5, 0xdd, 0xfe, 0xdd, 0x6f, 0x07, + 0xbf, 0xb9, 0x9f, 0x30, 0x4a, 0x35, 0xd1, 0x99, 0xbe, 0xa3, 0xb7, 0xfa, 0x79, 0x06, 0xac, 0x21, 0x61, 0x7e, 0x42, + 0x11, 0xed, 0xfa, 0xaa, 0x3a, 0x68, 0x8c, 0x66, 0xb7, 0x8a, 0xf8, 0x99, 0x17, 0xb3, 0xc0, 0x4b, 0xfd, 0x1b, 0xc1, + 0x33, 0x76, 0x8e, 0x56, 0x77, 0x62, 0x8e, 0xf1, 0xc0, 0xfb, 0x84, 0x49, 0xaa, 0x0c, 0x45, 0x4c, 0x52, 0xb5, 0x18, + 0x27, 0x69, 0x50, 0x83, 0x46, 0x04, 0x78, 0xa9, 0x9c, 0xf4, 0xdd, 0xd5, 0x9d, 0x7c, 0x44, 0x17, 0xcd, 0xf2, 0x93, + 0xba, 0x1a, 0x99, 0x6f, 0xe9, 0x4f, 0xa7, 0x01, 0xcb, 0x4a, 0x13, 0x5d, 0x9e, 0x4b, 0x09, 0x39, 0x39, 0x1e, 0xbc, + 0x71, 0x12, 0x05, 0xeb, 0x94, 0x35, 0xa3, 0x8b, 0x90, 0xe3, 0xda, 0x05, 0x72, 0xf0, 0x77, 0x79, 0xac, 0x5d, 0x60, + 0xb7, 0x61, 0x99, 0xd8, 0x03, 0x08, 0xc4, 0x6d, 0x76, 0xca, 0x43, 0x87, 0x57, 0xf9, 0xa0, 0x8d, 0x06, 0x40, 0x0c, + 0x38, 0x96, 0x88, 0x7a, 0x2b, 0x96, 0xc3, 0xcb, 0xf2, 0x60, 0xc4, 0x79, 0x51, 0x56, 0x06, 0xe6, 0xf7, 0xd9, 0x63, + 0xcf, 0x9a, 0xf7, 0xd8, 0x33, 0xb1, 0xc7, 0xb6, 0xaf, 0xcc, 0x87, 0x33, 0x07, 0xfe, 0x1b, 0x14, 0x00, 0xf5, 0x6d, + 0xa5, 0xb3, 0xba, 0x53, 0x9c, 0xd5, 0x9d, 0x62, 0xba, 0xab, 0x3b, 0x05, 0xbb, 0x46, 0x23, 0x16, 0xc3, 0x72, 0x75, + 0xc3, 0x56, 0xa0, 0x10, 0xfe, 0xd8, 0xa5, 0x57, 0xce, 0x21, 0xbc, 0x83, 0x56, 0xdd, 0xfa, 0x3b, 0x77, 0xfb, 0x56, + 0xa7, 0xbd, 0x24, 0x88, 0xb6, 0x6e, 0xa5, 0xde, 0x78, 0xcc, 0xa6, 0xfd, 0x59, 0x34, 0x59, 0x27, 0xff, 0xe4, 0xe3, + 0xe7, 0x48, 0xdc, 0x4a, 0x08, 0x2a, 0xfd, 0x88, 0xa6, 0x70, 0xbb, 0x73, 0xc3, 0x44, 0x0f, 0x9b, 0x7c, 0x9e, 0xfa, + 0x14, 0x35, 0xdc, 0xb5, 0x0e, 0x1b, 0x16, 0x79, 0x33, 0xa2, 0x7f, 0xb7, 0x59, 0x6a, 0x27, 0x31, 0x9f, 0x81, 0x96, + 0xad, 0xe8, 0xf8, 0x74, 0x6c, 0xf0, 0xd9, 0xb4, 0x7b, 0xcd, 0xc3, 0xbd, 0x14, 0x5f, 0xba, 0x12, 0x87, 0x0a, 0x3f, + 0xb7, 0xb8, 0x8f, 0xcc, 0xf6, 0x5e, 0xdb, 0xd6, 0x48, 0xad, 0xd7, 0x2d, 0x07, 0x42, 0x51, 0x77, 0x4f, 0x2a, 0xff, + 0xf0, 0xd9, 0x21, 0xfc, 0x47, 0x5c, 0xfd, 0xdf, 0xd3, 0x26, 0x46, 0xfd, 0x75, 0x5a, 0x62, 0xd4, 0x89, 0x55, 0x42, + 0x46, 0x7c, 0xff, 0xfa, 0xb3, 0xd9, 0xa7, 0x35, 0xd8, 0xbb, 0x36, 0xd9, 0x7f, 0x55, 0x6b, 0x7f, 0x17, 0x45, 0x90, + 0xd1, 0xb6, 0x5e, 0x5d, 0xa0, 0x87, 0xcc, 0xf3, 0xd3, 0x21, 0x34, 0x12, 0x72, 0x04, 0x99, 0x1e, 0xa8, 0xd8, 0x86, + 0x44, 0x89, 0x97, 0x6d, 0xa2, 0xc4, 0x8b, 0xdd, 0xa2, 0xc4, 0xf7, 0x7b, 0x89, 0x12, 0x2f, 0xbe, 0xb8, 0x28, 0xf1, + 0xb2, 0x2e, 0x4a, 0x5c, 0x44, 0xc2, 0xe8, 0xd7, 0x78, 0xbd, 0xe6, 0x3f, 0xdf, 0xd3, 0x4d, 0xe2, 0x79, 0x34, 0xec, + 0xda, 0x14, 0x09, 0xfc, 0xe2, 0xdf, 0x16, 0x2c, 0x70, 0x21, 0xbe, 0x45, 0x1b, 0xb8, 0x42, 0xb4, 0xe0, 0x94, 0x1d, + 0xbf, 0x23, 0x15, 0x07, 0x51, 0x38, 0xff, 0x09, 0x6e, 0x92, 0x41, 0x1d, 0x18, 0x4b, 0x2f, 0xfc, 0xe4, 0xa7, 0x68, + 0xb5, 0x5e, 0xbd, 0x86, 0xbe, 0xde, 0xfb, 0x89, 0x3f, 0x0e, 0x58, 0xee, 0xa1, 0x4f, 0x36, 0x7b, 0x5c, 0x27, 0x0e, + 0x66, 0xb2, 0xe2, 0xa7, 0x77, 0x27, 0x7e, 0xa2, 0x21, 0x2d, 0xff, 0x4d, 0xc6, 0x80, 0x6a, 0xb3, 0x20, 0x02, 0xa1, + 0xac, 0x2a, 0x83, 0xfe, 0x74, 0x61, 0xe4, 0x22, 0xd2, 0x1b, 0xa0, 0x14, 0x46, 0x1a, 0xad, 0xfd, 0xb0, 0x9a, 0x50, + 0xb3, 0xd6, 0x8d, 0x3c, 0x32, 0x5d, 0x5d, 0x0d, 0xbf, 0x8c, 0xd6, 0x09, 0x9b, 0x46, 0xb7, 0xa1, 0x6a, 0x84, 0xb9, + 0x67, 0x04, 0x5c, 0xcb, 0xe6, 0x6d, 0x30, 0xa7, 0xea, 0x3b, 0x64, 0x94, 0xa3, 0x58, 0x53, 0x21, 0xa5, 0xef, 0x7a, + 0x65, 0xd2, 0xfd, 0xb8, 0x89, 0x20, 0xaa, 0x79, 0xf2, 0xaf, 0x07, 0x9a, 0x16, 0x0d, 0x3f, 0xad, 0xa5, 0xb0, 0x2f, + 0x89, 0x2c, 0xae, 0x15, 0x4e, 0xb4, 0x50, 0x28, 0x17, 0x45, 0x78, 0x98, 0x86, 0x89, 0xe3, 0x6f, 0xc8, 0x37, 0xba, + 0x78, 0x0b, 0xc1, 0x75, 0xb2, 0x35, 0x9f, 0x0f, 0x1e, 0x2c, 0x85, 0x1e, 0x9f, 0x4b, 0x68, 0x7c, 0x73, 0xc3, 0xe2, + 0xc0, 0xbb, 0xd7, 0xf4, 0x2c, 0x0a, 0x7f, 0x00, 0x04, 0xbc, 0x88, 0x6e, 0x43, 0xb9, 0x02, 0xe6, 0x30, 0x6a, 0x58, + 0x4b, 0x8d, 0x61, 0x7d, 0xc0, 0xd7, 0x46, 0x1a, 0x01, 0x64, 0x8f, 0x9e, 0xb3, 0xbf, 0x1a, 0xf4, 0xef, 0xdf, 0xf4, + 0xcc, 0x38, 0x8f, 0xf2, 0x0f, 0xfd, 0xbc, 0xda, 0xe3, 0x33, 0x8f, 0x1f, 0x3f, 0x68, 0x07, 0x5b, 0x83, 0x44, 0xda, + 0x22, 0x27, 0xb4, 0xd6, 0xd0, 0x5a, 0x6f, 0xdd, 0x05, 0x30, 0x8a, 0x8b, 0x68, 0x3d, 0x59, 0xa0, 0x75, 0xee, 0x97, + 0x83, 0x37, 0x85, 0x3e, 0x31, 0x79, 0x6f, 0x0e, 0x7a, 0xa5, 0xa8, 0xc0, 0x02, 0x7e, 0xff, 0x25, 0xc4, 0xa5, 0xfd, + 0x0f, 0xa2, 0xa1, 0xbe, 0x6a, 0x72, 0x5f, 0xdc, 0x4f, 0x5a, 0xbc, 0x03, 0xc8, 0x31, 0xcb, 0x23, 0xbe, 0x88, 0xcb, + 0xb5, 0x66, 0x22, 0x93, 0x55, 0x91, 0x26, 0x47, 0x57, 0x6c, 0x0b, 0x1c, 0x29, 0xbe, 0xc2, 0x2c, 0x12, 0xd3, 0xb9, + 0x77, 0x84, 0xc1, 0x38, 0xb5, 0xaa, 0x10, 0x19, 0x6e, 0xa7, 0xc1, 0x90, 0x7c, 0x55, 0xdf, 0x2d, 0xfd, 0xd0, 0xc0, + 0xe4, 0x08, 0xf5, 0x37, 0xde, 0x1d, 0x84, 0x07, 0x07, 0xe2, 0x56, 0x7d, 0x05, 0x85, 0x86, 0xec, 0xe5, 0x07, 0x19, + 0xd0, 0xd4, 0x46, 0x4c, 0x88, 0x5b, 0xbc, 0xd1, 0x57, 0x8a, 0xa2, 0x28, 0xb9, 0x18, 0xa1, 0xe4, 0x72, 0x04, 0x96, + 0xa3, 0x38, 0x00, 0xb7, 0x25, 0xd9, 0xea, 0x8e, 0x4a, 0x40, 0x32, 0xc0, 0x9b, 0x59, 0x51, 0xc0, 0x23, 0x60, 0x76, + 0x6d, 0x51, 0x20, 0x04, 0x7a, 0x88, 0x5e, 0xe8, 0xc5, 0x10, 0x28, 0xbb, 0xaf, 0xa0, 0xc0, 0x8e, 0x6f, 0xb9, 0x26, + 0x58, 0xb1, 0xe9, 0x71, 0x34, 0x60, 0xcd, 0xa1, 0x12, 0x43, 0x89, 0x0a, 0xc2, 0xad, 0x43, 0x25, 0xf2, 0xb9, 0xc1, + 0x1a, 0x68, 0x23, 0xca, 0x45, 0x77, 0xe9, 0x92, 0x85, 0x6b, 0x15, 0x53, 0xa5, 0x61, 0xe8, 0x4a, 0xa8, 0xf3, 0x82, + 0x98, 0x2d, 0xa0, 0x36, 0xcd, 0x2d, 0x17, 0x74, 0x16, 0x26, 0x9c, 0xa4, 0x7a, 0xc6, 0x84, 0x5f, 0x6c, 0x26, 0x9c, + 0xb6, 0x55, 0x4f, 0x08, 0x3e, 0xa5, 0x51, 0xd5, 0xfb, 0x8c, 0xcc, 0xb7, 0x31, 0x2e, 0x0b, 0x2a, 0x8d, 0xb8, 0xba, + 0x48, 0xa0, 0x5d, 0xf3, 0xaa, 0x93, 0x96, 0xdf, 0xc8, 0x78, 0x15, 0x45, 0x51, 0xac, 0xd7, 0xbb, 0xe1, 0xe3, 0x84, + 0x78, 0x5d, 0xad, 0xfd, 0x4c, 0x6a, 0xfd, 0xb4, 0x00, 0xfd, 0x81, 0xdd, 0xd3, 0x41, 0x42, 0xa8, 0xfa, 0xc0, 0xee, + 0xc1, 0x60, 0xf1, 0x25, 0x68, 0x53, 0xd4, 0x2d, 0xe4, 0xda, 0x80, 0x0c, 0x18, 0x13, 0x88, 0xe1, 0xb6, 0x65, 0x03, + 0xd9, 0xd9, 0x16, 0x2a, 0x8e, 0x28, 0x86, 0x64, 0xe7, 0x62, 0x13, 0x73, 0xbf, 0x04, 0xad, 0x11, 0xc7, 0x66, 0xc3, + 0xd6, 0xd0, 0x9f, 0x38, 0xb6, 0x7d, 0x50, 0xab, 0x0f, 0x8a, 0xec, 0xa6, 0xda, 0xba, 0x91, 0x0e, 0x1d, 0xdb, 0xf4, + 0x9f, 0x58, 0xee, 0xa0, 0x76, 0x46, 0x4b, 0x21, 0x56, 0x47, 0xa8, 0xfe, 0x3a, 0x7d, 0xb4, 0xd1, 0x6a, 0x1b, 0x52, + 0xaf, 0xda, 0xf9, 0xe3, 0xd8, 0x32, 0xae, 0xff, 0x1a, 0xd5, 0x8f, 0x7e, 0x0a, 0xf0, 0x4a, 0xe9, 0x7e, 0x46, 0x10, + 0x24, 0x5c, 0x83, 0x6d, 0xf4, 0x27, 0xe5, 0xa9, 0xa2, 0xd1, 0xf6, 0xd1, 0xf5, 0x51, 0x9e, 0x45, 0x5e, 0x38, 0xc2, + 0xc9, 0x1d, 0x54, 0xbe, 0x98, 0x54, 0x29, 0x1c, 0x0f, 0x47, 0xcc, 0x8a, 0x1b, 0xbd, 0xad, 0xdc, 0x02, 0xf6, 0xdf, + 0x72, 0x7c, 0x5a, 0x63, 0x08, 0xbe, 0x00, 0x35, 0x20, 0xa5, 0xc0, 0xce, 0x0e, 0x21, 0xb6, 0x88, 0xdc, 0x5d, 0xf9, + 0x90, 0xdc, 0xbf, 0x33, 0x3c, 0x74, 0xf0, 0x0e, 0x2d, 0xef, 0xaf, 0xf9, 0xb8, 0xfb, 0xc4, 0x2e, 0x59, 0x38, 0x2d, + 0x77, 0x58, 0x39, 0xbf, 0xf6, 0xef, 0xae, 0x44, 0x51, 0x20, 0xd7, 0x46, 0xd4, 0x40, 0x51, 0xb2, 0x28, 0xc4, 0xc5, + 0x4f, 0xdb, 0xcd, 0xdf, 0x8b, 0x8b, 0xc1, 0x06, 0x14, 0x28, 0x2f, 0x6f, 0x26, 0x29, 0xc5, 0x21, 0xa0, 0x3b, 0x7a, + 0x31, 0x6b, 0x82, 0x11, 0x6d, 0x5d, 0x89, 0xa9, 0x30, 0x84, 0x2c, 0xda, 0xf8, 0x3c, 0x44, 0xeb, 0xbe, 0x5a, 0x6b, + 0x7f, 0xb7, 0xd6, 0x3a, 0xdd, 0xa5, 0xb5, 0x26, 0x1f, 0x30, 0xb2, 0xde, 0xc9, 0x7d, 0xe1, 0x04, 0x73, 0x2e, 0x7b, + 0x13, 0x96, 0x54, 0xdd, 0xe8, 0x32, 0x26, 0x5a, 0xd5, 0x7a, 0x23, 0xd3, 0x46, 0x54, 0x7f, 0x4b, 0x02, 0x8a, 0xb8, + 0x50, 0x97, 0x75, 0xe3, 0x17, 0x85, 0x6e, 0x9c, 0xa4, 0x9a, 0xc2, 0xfb, 0x47, 0x70, 0xff, 0x92, 0x67, 0x5d, 0x2e, + 0x1d, 0x14, 0x1e, 0x76, 0xc5, 0x48, 0x25, 0x9f, 0xb1, 0x42, 0xd0, 0x90, 0x3c, 0x11, 0x85, 0x94, 0x51, 0x76, 0x48, + 0x2c, 0x57, 0x2d, 0x5c, 0xc6, 0x8a, 0x72, 0xd0, 0xba, 0xe3, 0x90, 0xf3, 0x62, 0x79, 0xd9, 0x94, 0x7d, 0x86, 0xe4, + 0xd7, 0xd2, 0x22, 0xc9, 0x9d, 0x7b, 0x08, 0xc1, 0x42, 0x4d, 0x5f, 0xb9, 0xd7, 0xce, 0x6d, 0x20, 0x70, 0x90, 0x0d, + 0xbe, 0x88, 0xbb, 0xb5, 0xf3, 0x94, 0x46, 0xa4, 0xb8, 0xba, 0x76, 0xf0, 0x74, 0xa7, 0x9b, 0x60, 0x59, 0x1f, 0x81, + 0xb8, 0xbe, 0x92, 0x34, 0x08, 0x7d, 0x5b, 0xb1, 0x07, 0x0d, 0x0c, 0x00, 0x9e, 0xff, 0xd5, 0x67, 0xce, 0x0a, 0x80, + 0x26, 0x52, 0xb1, 0xe5, 0x3b, 0x7f, 0xdc, 0xc4, 0x26, 0x99, 0x1f, 0x63, 0xd5, 0xfa, 0x37, 0x49, 0xdf, 0xb3, 0xe1, + 0x9e, 0x3f, 0x65, 0x75, 0x3e, 0xaf, 0xd1, 0x17, 0xe3, 0xe0, 0xab, 0x2c, 0x5e, 0x87, 0x98, 0x1d, 0xc2, 0x4c, 0x63, + 0x6f, 0xf2, 0x61, 0x23, 0x7d, 0x8f, 0xab, 0x44, 0x41, 0x5d, 0x5c, 0xbe, 0x54, 0x18, 0x78, 0x18, 0x4c, 0x95, 0xf5, + 0x2d, 0x37, 0x91, 0x14, 0x35, 0xfd, 0x87, 0x76, 0xc7, 0x7b, 0x36, 0x3b, 0xac, 0xe8, 0x4f, 0xdd, 0x6e, 0x59, 0xbb, + 0x9e, 0x8f, 0x63, 0x19, 0xfd, 0xca, 0x7d, 0x24, 0xff, 0xf8, 0x4f, 0x27, 0xfc, 0x9b, 0x95, 0x39, 0xfa, 0x9c, 0x21, + 0x40, 0xfb, 0xd2, 0xc5, 0xb4, 0x7c, 0x4d, 0x53, 0x2b, 0x69, 0x1b, 0xd6, 0xcc, 0x0f, 0x02, 0x33, 0x00, 0x4f, 0x95, + 0xcd, 0x67, 0x81, 0x87, 0xfd, 0xac, 0x21, 0x8c, 0xf7, 0x67, 0xf4, 0x53, 0x5e, 0x29, 0xe9, 0x62, 0xbd, 0x1c, 0x6f, + 0x64, 0x45, 0xb9, 0xa4, 0x3f, 0xaf, 0xeb, 0xcc, 0xe5, 0xcf, 0xce, 0x66, 0xb3, 0xb2, 0xd6, 0xd8, 0x56, 0x0e, 0x51, + 0xf3, 0xfb, 0xd0, 0xb6, 0xed, 0x2a, 0x7e, 0xdb, 0x36, 0x0a, 0x6d, 0x0c, 0x13, 0x95, 0xf0, 0xbd, 0xdd, 0x6b, 0xea, + 0x0f, 0x1a, 0x2d, 0x75, 0xd5, 0xb6, 0x1f, 0x69, 0xa9, 0xfd, 0x57, 0x0c, 0x05, 0x49, 0xc3, 0xae, 0xed, 0x5f, 0x5f, + 0x2b, 0x5b, 0x7a, 0xaa, 0x6e, 0xe0, 0x4f, 0x6b, 0xbc, 0x63, 0xad, 0xef, 0xd1, 0xb4, 0x6d, 0x79, 0x67, 0x56, 0x71, + 0xec, 0x96, 0x6c, 0x96, 0x06, 0x64, 0xa9, 0xe4, 0xa7, 0x6c, 0x99, 0xf4, 0x27, 0x0c, 0x2f, 0x48, 0x2d, 0xe9, 0xb4, + 0x45, 0xab, 0x1e, 0x73, 0x0e, 0x76, 0x5c, 0x8e, 0xa0, 0xc3, 0xb6, 0x82, 0x97, 0x55, 0xb5, 0x9b, 0x35, 0xf1, 0x11, + 0x3c, 0xc5, 0x36, 0xf5, 0x0b, 0x27, 0x5c, 0xa6, 0x5d, 0xfb, 0x4f, 0xa5, 0x7a, 0x0a, 0x70, 0xa7, 0x1b, 0x61, 0x6d, + 0x42, 0x97, 0x27, 0xf8, 0x77, 0x7e, 0x39, 0xf7, 0x6c, 0x75, 0x57, 0x36, 0xee, 0xea, 0xc1, 0x75, 0x53, 0x71, 0x94, + 0xd1, 0xa8, 0x9b, 0x48, 0x5f, 0x6e, 0x02, 0x34, 0x93, 0xad, 0x5b, 0xc0, 0x82, 0xa6, 0x94, 0xb4, 0xaa, 0xe1, 0x6e, + 0x0c, 0xc5, 0x59, 0x58, 0x79, 0x85, 0x7e, 0xbf, 0x48, 0xb9, 0x0a, 0x30, 0x18, 0x4f, 0x7b, 0x78, 0xb9, 0x57, 0x5a, + 0xaa, 0x68, 0x2a, 0x83, 0x6b, 0x40, 0x48, 0xa4, 0xca, 0x3a, 0x0e, 0x4c, 0xca, 0xe8, 0xa4, 0xe9, 0x9b, 0x3a, 0xdc, + 0xed, 0xdd, 0x3b, 0x5d, 0xb8, 0xd7, 0xa8, 0xa3, 0x6a, 0xaf, 0xab, 0xbd, 0xea, 0x1d, 0xb6, 0x18, 0x27, 0xcc, 0x00, + 0x78, 0x52, 0x28, 0x68, 0x34, 0xa4, 0x54, 0x68, 0x1f, 0x01, 0x9d, 0xbf, 0x95, 0x89, 0xb5, 0x80, 0x13, 0xbb, 0x6b, + 0xae, 0x42, 0x7d, 0x8b, 0x9b, 0x41, 0xc0, 0x1d, 0xa7, 0x4e, 0xf8, 0x6c, 0xc2, 0x8a, 0x91, 0xc9, 0x95, 0x83, 0x2b, + 0x08, 0x77, 0xa9, 0x89, 0x7c, 0x72, 0x42, 0xbb, 0x14, 0xef, 0x12, 0xbe, 0x6f, 0x54, 0xde, 0x5f, 0x94, 0xb4, 0xf1, + 0xdc, 0x9d, 0xc5, 0xd5, 0xf7, 0xaa, 0xbd, 0xf4, 0xc3, 0xfd, 0xeb, 0x7a, 0x77, 0x7b, 0xd7, 0x05, 0xe6, 0x70, 0xef, + 0xca, 0xc0, 0x5d, 0x92, 0x95, 0x52, 0x3a, 0xfc, 0x5e, 0xba, 0x3c, 0x90, 0xc3, 0x22, 0xa8, 0xd8, 0x8a, 0x24, 0xfa, + 0x8b, 0xf5, 0x70, 0x74, 0x72, 0x76, 0xb7, 0x0c, 0x94, 0x1b, 0x16, 0x43, 0x76, 0xbb, 0xa1, 0xea, 0x58, 0xb6, 0xaa, + 0xa0, 0x93, 0xbf, 0x1f, 0xce, 0x87, 0xea, 0xcf, 0x17, 0xaf, 0xcc, 0x9e, 0x7a, 0x06, 0xe6, 0x18, 0x37, 0x73, 0x64, + 0x71, 0xcf, 0xbd, 0x7b, 0x16, 0x5f, 0xbb, 0xaa, 0x82, 0x49, 0xec, 0x88, 0xb9, 0xc5, 0x32, 0xc5, 0x55, 0xf7, 0xc8, + 0x95, 0xa4, 0x88, 0x74, 0xa7, 0x2a, 0x10, 0x56, 0xc7, 0xed, 0x29, 0x8e, 0x7b, 0x68, 0x1d, 0xf5, 0xd4, 0xd3, 0xaf, + 0x14, 0xe5, 0x64, 0xca, 0x66, 0xc9, 0x29, 0xaa, 0x63, 0x4e, 0x90, 0x1f, 0xa4, 0xdf, 0x8a, 0x62, 0x4d, 0x82, 0xc4, + 0x74, 0x94, 0x0d, 0x7f, 0x54, 0x14, 0x20, 0x46, 0x7d, 0xe5, 0xe1, 0xcc, 0x9d, 0x1d, 0xce, 0x9e, 0x0d, 0x78, 0x71, + 0xf6, 0x55, 0xa9, 0xba, 0x41, 0xff, 0xba, 0x52, 0xb3, 0x24, 0x8d, 0xa3, 0x0f, 0x8c, 0xf3, 0x92, 0x4a, 0xae, 0x28, + 0xaa, 0x36, 0x75, 0xeb, 0x5f, 0x72, 0x7a, 0xe3, 0xc9, 0xcc, 0x2d, 0xaa, 0xe3, 0x18, 0x0f, 0xf2, 0x41, 0x9e, 0x1c, + 0x88, 0xa1, 0x9f, 0xc8, 0x68, 0x72, 0xcc, 0x26, 0x44, 0x39, 0x2a, 0x87, 0x71, 0x2e, 0xe0, 0x3b, 0x81, 0x50, 0xc4, + 0x85, 0x03, 0x42, 0x82, 0xcd, 0x86, 0xea, 0x0f, 0x8e, 0xdb, 0x33, 0x1c, 0xe7, 0xc8, 0x3a, 0xea, 0x4d, 0x6c, 0xe3, + 0xd0, 0x3a, 0x34, 0x3b, 0xd6, 0x91, 0xd1, 0x33, 0x7b, 0x46, 0xef, 0x2f, 0xbd, 0x89, 0x79, 0x68, 0x1d, 0x1a, 0xb6, + 0xd9, 0x83, 0x42, 0xb3, 0x67, 0xf6, 0x6e, 0xcc, 0xc3, 0xde, 0xc4, 0xc6, 0x52, 0xd7, 0xea, 0x76, 0x4d, 0xc7, 0xb6, + 0xba, 0x5d, 0xa3, 0x6b, 0x1d, 0x1d, 0x99, 0x4e, 0xc7, 0x3a, 0x3a, 0x3a, 0xef, 0xf6, 0xac, 0x0e, 0xbc, 0xeb, 0x74, + 0x26, 0x1d, 0xcb, 0x71, 0x4c, 0xf8, 0xcb, 0xe8, 0x59, 0x2e, 0xfd, 0x70, 0x1c, 0xab, 0xe3, 0x18, 0x76, 0xd0, 0x75, + 0xad, 0xa3, 0x67, 0x06, 0xfe, 0x8d, 0xd5, 0x0c, 0xfc, 0x0b, 0xba, 0x31, 0x9e, 0x59, 0xee, 0x11, 0xfd, 0xc2, 0x0e, + 0x6f, 0x0e, 0x7b, 0x7f, 0x57, 0x0f, 0x5a, 0x61, 0x70, 0x08, 0x86, 0x5e, 0xd7, 0xea, 0x74, 0x8c, 0x43, 0xc7, 0xea, + 0x75, 0x16, 0xe6, 0xa1, 0x6b, 0x1d, 0x1d, 0x4f, 0x4c, 0xc7, 0x3a, 0x3e, 0x36, 0x6c, 0xb3, 0x63, 0xb9, 0x86, 0x63, + 0x1d, 0x76, 0xf0, 0x47, 0xc7, 0x72, 0x6f, 0x8e, 0x9f, 0x59, 0x47, 0xdd, 0xc5, 0x91, 0x75, 0xf8, 0xfe, 0xb0, 0x67, + 0xb9, 0x9d, 0x45, 0xe7, 0xc8, 0x72, 0x8f, 0x6f, 0x8e, 0xac, 0xc3, 0x85, 0xe9, 0x1e, 0x6d, 0x6d, 0xe9, 0xb8, 0x16, + 0xe0, 0x08, 0x5f, 0xc3, 0x0b, 0x83, 0xbf, 0x80, 0x3f, 0x0b, 0x6c, 0xfb, 0x07, 0x76, 0x93, 0xd4, 0x9b, 0x3e, 0xb3, + 0x7a, 0xc7, 0x13, 0xaa, 0x0e, 0x05, 0xa6, 0xa8, 0x01, 0x4d, 0x6e, 0x4c, 0xfa, 0x2c, 0x76, 0x67, 0x8a, 0x8e, 0xc4, + 0x1f, 0xfe, 0xb1, 0x1b, 0x13, 0x3e, 0x4c, 0xdf, 0xfd, 0x8f, 0xf6, 0x93, 0x4f, 0x39, 0x24, 0x6f, 0xfe, 0x8a, 0xff, + 0x43, 0x79, 0xcf, 0x46, 0xc6, 0x79, 0xdb, 0xa5, 0xe4, 0xdb, 0xdd, 0x97, 0x92, 0xaf, 0xd6, 0xfb, 0x5c, 0x4a, 0xbe, + 0xfd, 0xe2, 0x97, 0x92, 0xe7, 0x55, 0x9f, 0x98, 0xb7, 0xd5, 0xf4, 0x2c, 0xdf, 0x6f, 0xaa, 0x2a, 0x07, 0xdf, 0xd3, + 0x2e, 0x2f, 0xd6, 0x57, 0x10, 0xf7, 0xed, 0x6d, 0x34, 0x7c, 0xb5, 0x2e, 0x19, 0x7c, 0x46, 0x40, 0x63, 0xdf, 0x46, + 0x44, 0x63, 0x7f, 0x5d, 0x0f, 0xc1, 0xca, 0x8c, 0xb3, 0x39, 0xfe, 0xd4, 0x5c, 0x78, 0xc1, 0x2c, 0x67, 0x91, 0xa0, + 0x64, 0x80, 0xc5, 0xe0, 0x77, 0x05, 0xc7, 0x33, 0x48, 0x32, 0xeb, 0x65, 0x98, 0x80, 0x45, 0x30, 0x58, 0x72, 0xcc, + 0xe2, 0xac, 0xd2, 0xd8, 0x12, 0x91, 0xf2, 0xae, 0xb9, 0x07, 0x54, 0xeb, 0x7b, 0x34, 0x00, 0x6e, 0xee, 0xdd, 0xa9, + 0xf7, 0xab, 0x80, 0x65, 0x9d, 0x30, 0x90, 0x06, 0x6e, 0xbf, 0xe9, 0x7d, 0xd9, 0x0c, 0xb7, 0x62, 0x78, 0xdd, 0x3e, + 0x52, 0x18, 0x49, 0xb5, 0xbd, 0x53, 0x36, 0xe3, 0xdd, 0x05, 0x66, 0xc3, 0xe7, 0x4b, 0xcd, 0xb7, 0xd8, 0x10, 0xe7, + 0x1d, 0x57, 0x51, 0x55, 0x49, 0x2e, 0xda, 0x88, 0x90, 0x42, 0x40, 0x2d, 0x0c, 0x8d, 0x0b, 0x4e, 0xd5, 0x56, 0x90, + 0xdf, 0xb1, 0xa5, 0x77, 0xa5, 0x3e, 0x65, 0xe3, 0xe4, 0x27, 0x1b, 0x94, 0x2b, 0xfc, 0x5f, 0x81, 0x13, 0xe5, 0x1c, + 0xcf, 0x38, 0x92, 0xf1, 0xbc, 0x91, 0xfa, 0x25, 0x6d, 0x44, 0xb6, 0x70, 0x36, 0x75, 0x5e, 0xb4, 0xd5, 0x2d, 0xc1, + 0x61, 0x4b, 0xc1, 0x05, 0xe1, 0xe7, 0xc9, 0x09, 0x20, 0x23, 0x47, 0x0d, 0xf4, 0x73, 0xd8, 0xd6, 0x99, 0xa8, 0xf7, + 0x10, 0x16, 0xb1, 0xc1, 0x1f, 0xe4, 0xc0, 0x25, 0x9b, 0x59, 0x10, 0x79, 0x69, 0x1f, 0xd9, 0x34, 0x89, 0xe5, 0x75, + 0xd1, 0x63, 0x61, 0xb0, 0xc5, 0x98, 0x4e, 0xee, 0x98, 0x77, 0x82, 0x9e, 0x0f, 0xdb, 0xec, 0xef, 0x72, 0x47, 0xb1, + 0x4d, 0xc9, 0x1c, 0xc5, 0xe9, 0x1e, 0x1b, 0xce, 0x91, 0x61, 0x1d, 0x77, 0xf5, 0x4c, 0x6c, 0x38, 0xb9, 0xcb, 0x12, + 0x42, 0xc0, 0x01, 0x22, 0x1f, 0xa6, 0x1f, 0xfa, 0xa9, 0xef, 0x05, 0x19, 0xf0, 0xc3, 0x65, 0x21, 0xe5, 0x1f, 0xeb, + 0x24, 0x05, 0x18, 0x05, 0xd3, 0x8b, 0xce, 0x1f, 0xe6, 0x98, 0xa5, 0xb7, 0x8c, 0x85, 0x2d, 0x86, 0x31, 0x55, 0x5f, + 0x92, 0xdf, 0xcf, 0xb2, 0x3e, 0x23, 0xab, 0xb5, 0x71, 0x1a, 0xf2, 0xf5, 0x21, 0x1c, 0x1f, 0xb2, 0x91, 0xf1, 0x63, + 0x1b, 0xc1, 0xfd, 0xc7, 0x6e, 0x82, 0x9b, 0xb2, 0x7d, 0x08, 0xee, 0x3f, 0xbe, 0x38, 0xc1, 0xfd, 0x51, 0x26, 0xb8, + 0x25, 0xbf, 0xbf, 0xe2, 0x86, 0xe9, 0x1d, 0x3e, 0x6b, 0x90, 0xd8, 0xe0, 0xa9, 0x7a, 0x40, 0x0c, 0xbc, 0x2a, 0xe5, + 0x9b, 0x7f, 0x5f, 0x4a, 0xa0, 0x87, 0x0a, 0x50, 0x8c, 0x68, 0x4e, 0xc9, 0xba, 0x20, 0x17, 0xbb, 0x9d, 0x27, 0xec, + 0x62, 0xb7, 0xca, 0xeb, 0x30, 0x0d, 0xac, 0xb7, 0x5c, 0x8e, 0x84, 0x0b, 0xdd, 0x57, 0x51, 0xbc, 0xf4, 0x30, 0x34, + 0xa8, 0x8a, 0x89, 0x77, 0xe1, 0xc1, 0x06, 0x5f, 0xd2, 0x49, 0x14, 0x4e, 0xf3, 0x5b, 0x49, 0x36, 0xbc, 0x24, 0x8e, + 0x5b, 0xbd, 0x67, 0x5e, 0xac, 0x1a, 0xf4, 0x1a, 0x26, 0xf7, 0x49, 0xc7, 0x7e, 0xe2, 0x1e, 0x3e, 0x39, 0xb2, 0xe1, + 0x7f, 0x87, 0x75, 0x32, 0x83, 0x57, 0x5c, 0x46, 0x21, 0xe4, 0xfe, 0x12, 0x35, 0xdb, 0xaa, 0xdd, 0x32, 0xf6, 0xa1, + 0xa8, 0x75, 0xdc, 0x5c, 0x69, 0xea, 0xdd, 0x17, 0x75, 0x1a, 0x6b, 0x2c, 0xa2, 0xb5, 0x34, 0xac, 0x86, 0xd1, 0xf8, + 0xe1, 0x1a, 0xf4, 0xec, 0x52, 0x0d, 0xf9, 0x35, 0x07, 0xb7, 0x80, 0x8b, 0x75, 0xb2, 0xab, 0x22, 0xc1, 0xa0, 0x48, + 0x74, 0xb6, 0x13, 0x83, 0xfc, 0x8a, 0xd2, 0xc6, 0x60, 0xd0, 0x18, 0x96, 0x1d, 0x42, 0x41, 0xe7, 0x69, 0xe1, 0x3c, + 0x9a, 0xa0, 0x34, 0x5e, 0x87, 0x13, 0x0d, 0x7f, 0x7a, 0xe3, 0x44, 0xf3, 0x0f, 0x62, 0x8b, 0x7f, 0x58, 0xc7, 0x59, + 0xf3, 0x4e, 0xed, 0x22, 0x1b, 0x53, 0x22, 0x66, 0xc5, 0x7b, 0x92, 0x1a, 0x31, 0xe5, 0x70, 0xc7, 0xa9, 0x35, 0x87, + 0xde, 0x93, 0xbc, 0xe1, 0x93, 0xd4, 0x80, 0x3c, 0xea, 0x30, 0xdd, 0x8f, 0x1f, 0x53, 0x2d, 0xc8, 0x6c, 0x4c, 0x60, + 0x9d, 0x4d, 0x8a, 0xf8, 0x93, 0x8a, 0x37, 0x8f, 0x28, 0x04, 0x65, 0x7f, 0x62, 0x44, 0x4f, 0x9f, 0x9e, 0x0e, 0x1d, + 0x9d, 0xe7, 0xe5, 0x2e, 0x25, 0x91, 0x3c, 0xdf, 0xcf, 0xd0, 0x48, 0x6f, 0x74, 0x81, 0x5d, 0x81, 0xcc, 0x64, 0x0b, + 0x77, 0x04, 0x4e, 0xbd, 0x20, 0xa9, 0x13, 0x19, 0x14, 0x78, 0xc2, 0xe0, 0x47, 0xd4, 0xc9, 0xa5, 0xae, 0x8e, 0x65, + 0x5b, 0xb6, 0x9a, 0x37, 0x9c, 0xf9, 0xf3, 0xe1, 0x26, 0x4a, 0x3d, 0x48, 0x8f, 0x17, 0x44, 0x73, 0xf0, 0xa3, 0x4b, + 0xfd, 0x34, 0x80, 0x5c, 0x6b, 0xe0, 0x50, 0xb7, 0x24, 0xb9, 0x3c, 0xe3, 0xde, 0x0d, 0x5e, 0xfc, 0x01, 0xf3, 0xed, + 0x0a, 0x17, 0x5a, 0x0c, 0x89, 0xf6, 0x03, 0x1c, 0x86, 0x9a, 0xaa, 0x81, 0x6e, 0x80, 0xc5, 0x89, 0x29, 0x7b, 0x0b, + 0xf5, 0x15, 0x68, 0xa3, 0xab, 0x1c, 0x88, 0x59, 0xec, 0x2d, 0x21, 0x2f, 0xc9, 0x26, 0x33, 0x38, 0xa5, 0x55, 0x39, + 0xa9, 0x55, 0x9c, 0x67, 0x47, 0x86, 0xe2, 0x3a, 0x86, 0x62, 0x03, 0xb9, 0x55, 0x33, 0x63, 0x93, 0x5d, 0x0d, 0x76, + 0x19, 0x3c, 0x10, 0x7d, 0x79, 0x48, 0x70, 0x90, 0xa9, 0x03, 0xbf, 0x4a, 0x4a, 0x29, 0xb8, 0xad, 0x26, 0x85, 0xff, + 0xf7, 0xe9, 0xd2, 0xf3, 0x82, 0xdd, 0xa5, 0x3a, 0xe6, 0x22, 0xe3, 0x55, 0x7c, 0x7d, 0x83, 0x8e, 0xbe, 0x7e, 0xa8, + 0xf8, 0x1f, 0x3f, 0x6a, 0x3e, 0x38, 0x33, 0x0d, 0x25, 0xfc, 0xc0, 0xb3, 0x5e, 0x42, 0x98, 0x5f, 0x5c, 0xd3, 0x23, + 0xb2, 0xc0, 0xd3, 0x10, 0xfe, 0x2d, 0x8a, 0xc5, 0x0f, 0x6e, 0x26, 0x61, 0x05, 0x5e, 0x38, 0x07, 0x92, 0xe6, 0x85, + 0xf3, 0x9a, 0x39, 0x16, 0xf9, 0x2a, 0x57, 0x4a, 0x8b, 0xae, 0x0a, 0x53, 0xa9, 0xe4, 0xbb, 0xfb, 0x0b, 0xca, 0xb5, + 0xa8, 0xa9, 0x70, 0xca, 0xa1, 0x63, 0x6d, 0x71, 0x93, 0xfb, 0x74, 0xf8, 0xf5, 0xc9, 0x92, 0xa5, 0x1e, 0x5d, 0x03, + 0x81, 0xf0, 0x0b, 0xec, 0x80, 0x32, 0x11, 0x79, 0xd2, 0xf1, 0x68, 0x18, 0x4e, 0xd9, 0x8d, 0x3f, 0xe1, 0x72, 0xa9, + 0xa1, 0xf0, 0x73, 0xca, 0x44, 0x8b, 0xcf, 0xa1, 0x63, 0x90, 0xc3, 0xc1, 0xc4, 0xc3, 0x38, 0xb8, 0xc3, 0x30, 0x52, + 0x4f, 0xbf, 0xce, 0x7d, 0x33, 0xdb, 0x26, 0x01, 0x12, 0x1e, 0x5f, 0xc6, 0x2c, 0xf8, 0xe7, 0xf0, 0x6b, 0x38, 0xb8, + 0xbf, 0xbe, 0x52, 0xf5, 0x41, 0x6a, 0x2d, 0x62, 0x36, 0x1b, 0x7e, 0xdd, 0x90, 0xf8, 0x17, 0xc5, 0x7b, 0x1a, 0x8b, + 0xda, 0x71, 0x8b, 0xe8, 0x65, 0x9d, 0xbd, 0x84, 0xfa, 0x53, 0x2e, 0xad, 0x83, 0x04, 0xb8, 0x29, 0xc9, 0xd8, 0xce, + 0x00, 0xe5, 0xe7, 0x71, 0xe0, 0x4d, 0x3e, 0x0c, 0xe8, 0x4d, 0xe9, 0xc1, 0x84, 0xd3, 0x7a, 0xe2, 0xad, 0xfa, 0x78, + 0xbc, 0xca, 0x85, 0xe0, 0x9a, 0x4d, 0xa5, 0x39, 0x67, 0xd7, 0xb8, 0x96, 0x71, 0x29, 0x6f, 0xf0, 0xcb, 0xf8, 0xa9, + 0xdb, 0x85, 0x9f, 0x32, 0xf1, 0x29, 0x7c, 0xc8, 0x32, 0x21, 0xa8, 0x93, 0x88, 0x8a, 0x82, 0xb5, 0xd5, 0x51, 0x9c, + 0xde, 0x5f, 0xba, 0x37, 0x8e, 0xbd, 0x70, 0x1d, 0xab, 0xf7, 0xde, 0xe9, 0x2d, 0x3a, 0xd6, 0x71, 0x60, 0x76, 0xac, + 0x63, 0xf8, 0xf3, 0xfe, 0xd8, 0xea, 0x2d, 0x4c, 0xd7, 0x3a, 0x7c, 0xef, 0xb8, 0x81, 0xd9, 0xb3, 0x8e, 0xe1, 0xcf, + 0x39, 0xb5, 0x02, 0x01, 0x88, 0xe4, 0x9d, 0xaf, 0x4b, 0x54, 0x40, 0xfa, 0x9d, 0xdf, 0xc9, 0x1a, 0xa5, 0xe3, 0xad, + 0xe1, 0x5e, 0x17, 0x48, 0x46, 0x91, 0x41, 0x27, 0x1c, 0x68, 0xe1, 0x90, 0x51, 0x46, 0x0c, 0x61, 0xde, 0x26, 0x7c, + 0xd0, 0x45, 0x22, 0x97, 0xc6, 0x6d, 0xc4, 0xdb, 0x34, 0x67, 0xef, 0x10, 0xc9, 0x19, 0xe9, 0x22, 0xf8, 0xe7, 0x15, + 0x46, 0x59, 0x13, 0xf9, 0x46, 0x24, 0xaa, 0x54, 0x24, 0x08, 0xce, 0x76, 0x0f, 0x1c, 0xbd, 0xf0, 0x59, 0x9e, 0xa0, + 0xee, 0x8b, 0xf6, 0x2d, 0xe5, 0x15, 0xfa, 0xac, 0x7e, 0x30, 0x2f, 0x7b, 0x91, 0x7d, 0x04, 0xc2, 0x4d, 0x4f, 0xfd, + 0x38, 0x1f, 0x9e, 0x44, 0xa2, 0x9d, 0xe6, 0xb4, 0x27, 0x3a, 0x64, 0xf2, 0x7a, 0x0d, 0x5c, 0xf2, 0x8d, 0x17, 0x48, + 0x86, 0x6c, 0x52, 0xcb, 0x07, 0x39, 0xe5, 0x7f, 0xfc, 0xb8, 0x18, 0x9c, 0x59, 0x19, 0xf7, 0x89, 0xd3, 0x85, 0x63, + 0xb7, 0xcb, 0x3a, 0x5b, 0x6d, 0x2a, 0x77, 0x07, 0x2a, 0x2f, 0xe2, 0x19, 0x0b, 0xbb, 0x29, 0x61, 0xb1, 0xd1, 0x6a, + 0xd8, 0x59, 0xb3, 0xd7, 0x80, 0x10, 0xef, 0x15, 0x51, 0x47, 0xd5, 0x07, 0xa1, 0x30, 0x3f, 0x08, 0xb7, 0x04, 0x67, + 0xe7, 0xb2, 0x98, 0x0a, 0xa8, 0xd9, 0x02, 0xc7, 0x0e, 0x07, 0xf1, 0xff, 0x34, 0x10, 0xe8, 0xac, 0x09, 0xf6, 0x12, + 0x95, 0xdd, 0x5a, 0x72, 0xde, 0xcb, 0xcf, 0x55, 0x3a, 0x50, 0x59, 0x72, 0xa6, 0x42, 0x11, 0xa4, 0x78, 0xc4, 0xac, + 0xae, 0xb9, 0xb1, 0x68, 0x7e, 0x5a, 0x14, 0x05, 0x86, 0x8f, 0x99, 0x2e, 0x84, 0xe3, 0xa8, 0xfe, 0xf8, 0x71, 0xeb, + 0x21, 0x44, 0xc6, 0x39, 0x72, 0x72, 0x6b, 0x55, 0xa6, 0x6f, 0xaa, 0x4c, 0x62, 0xf2, 0x7e, 0x91, 0x6a, 0x08, 0x1b, + 0x57, 0x5a, 0x7b, 0xf8, 0x73, 0xcc, 0xbc, 0xd4, 0xe2, 0x97, 0xa5, 0x9a, 0x74, 0xb8, 0x1b, 0x0e, 0xeb, 0x80, 0x75, + 0x2b, 0x0f, 0xc6, 0xc8, 0x83, 0x9d, 0x3e, 0xda, 0xbc, 0x5f, 0xf3, 0xa8, 0x0e, 0xd0, 0xc7, 0x47, 0xbb, 0x88, 0x9f, + 0xf5, 0x26, 0xf5, 0x28, 0xc8, 0x92, 0x7c, 0xe4, 0x46, 0xa9, 0x27, 0x52, 0xa9, 0x01, 0x5f, 0x3e, 0x68, 0x34, 0xbf, + 0x90, 0x22, 0x3f, 0x9c, 0xbe, 0xb9, 0xf8, 0x56, 0xe1, 0xeb, 0x9f, 0xac, 0x05, 0x50, 0x90, 0xa1, 0x34, 0x2e, 0x43, + 0x4a, 0xe3, 0xa2, 0xf0, 0x24, 0x56, 0x90, 0x0c, 0x25, 0x3b, 0x20, 0x0c, 0xa2, 0x02, 0x9a, 0x6c, 0x28, 0x96, 0xeb, + 0x20, 0xf5, 0x57, 0x5e, 0x9c, 0x1e, 0x40, 0x53, 0x13, 0x88, 0x9c, 0xda, 0x16, 0x0f, 0x82, 0xcc, 0x30, 0x44, 0x0c, + 0xd0, 0x34, 0x14, 0x76, 0x18, 0x33, 0x3f, 0xc8, 0xcd, 0x30, 0xc4, 0x07, 0xbc, 0xc9, 0x84, 0xad, 0xd2, 0xa1, 0xea, + 0xad, 0x20, 0x95, 0x12, 0xc6, 0xda, 0x3f, 0x88, 0x26, 0x29, 0x4b, 0xcd, 0x24, 0x8d, 0x99, 0xb7, 0x54, 0xf3, 0x58, + 0xd3, 0xf5, 0xfe, 0x92, 0xf5, 0x78, 0xe9, 0xa7, 0x79, 0xb0, 0x56, 0x02, 0x10, 0x0c, 0x22, 0x60, 0x88, 0x10, 0x1c, + 0x86, 0x50, 0x78, 0x1e, 0xcd, 0x2b, 0x2b, 0xaa, 0xe0, 0x5c, 0xce, 0x30, 0x14, 0x38, 0x49, 0x32, 0xa0, 0x2d, 0x9e, + 0x44, 0xc1, 0x35, 0x8f, 0x61, 0x91, 0xc7, 0x94, 0x55, 0x4f, 0x4f, 0xb8, 0x78, 0xab, 0x60, 0xd8, 0x15, 0xb5, 0x6b, + 0x43, 0xb0, 0xf3, 0xb6, 0xe8, 0x16, 0x07, 0xbc, 0x32, 0x1c, 0x4d, 0xd4, 0x33, 0x66, 0xa0, 0xa0, 0xb1, 0x5c, 0x00, + 0x23, 0x54, 0x32, 0x98, 0x59, 0x38, 0xa7, 0xb9, 0x3b, 0x25, 0x8e, 0x0a, 0x79, 0xa5, 0x8f, 0x1f, 0x9f, 0x8f, 0x7e, + 0xfb, 0x17, 0xa4, 0xc9, 0x58, 0x38, 0x22, 0xa6, 0xc4, 0xa5, 0x5c, 0x8b, 0x73, 0x9f, 0xc6, 0x08, 0x8d, 0xa5, 0xd8, + 0x54, 0x04, 0xe6, 0x11, 0x4b, 0x2b, 0x1b, 0x5d, 0x89, 0x68, 0x7f, 0x90, 0x41, 0x47, 0x17, 0x91, 0x2f, 0x46, 0x30, + 0xbd, 0x23, 0x11, 0x70, 0x45, 0xf9, 0xe5, 0xee, 0xbb, 0x63, 0xa5, 0x08, 0xc1, 0xd3, 0x64, 0xd1, 0x43, 0x6b, 0xe8, + 0xf4, 0xc4, 0x53, 0x90, 0x69, 0x41, 0xf6, 0x23, 0xe9, 0x1f, 0x00, 0x98, 0x8b, 0x68, 0xc9, 0x2c, 0x3f, 0x3a, 0xb8, + 0x65, 0x63, 0xd3, 0x5b, 0xf9, 0x64, 0x97, 0x83, 0x7a, 0x37, 0x85, 0x38, 0xbf, 0xdc, 0xdc, 0x85, 0xf8, 0xeb, 0xac, + 0x40, 0x65, 0x54, 0x0e, 0xef, 0xd8, 0x75, 0x8b, 0x7b, 0x40, 0x88, 0x5f, 0x20, 0xe1, 0x31, 0x3a, 0x3d, 0x39, 0xf0, + 0x4e, 0xcb, 0xf1, 0x65, 0x2d, 0x91, 0xda, 0xa4, 0x7c, 0x08, 0x9c, 0x51, 0x98, 0x58, 0x11, 0x11, 0xb6, 0x78, 0x30, + 0xa3, 0xd9, 0x4c, 0x8e, 0x09, 0x6b, 0x95, 0x87, 0x97, 0x23, 0xad, 0x58, 0xd2, 0xd1, 0x8a, 0xbe, 0x54, 0xff, 0x44, + 0xfe, 0x53, 0xed, 0x63, 0x30, 0x68, 0x80, 0x19, 0xb6, 0x7b, 0x2d, 0xb6, 0x6c, 0x8e, 0xb1, 0x87, 0x54, 0x89, 0xd3, + 0x91, 0x6a, 0x26, 0x83, 0x16, 0xce, 0xe5, 0xc1, 0x70, 0x48, 0x64, 0xae, 0x4a, 0xed, 0x00, 0x89, 0x0d, 0x69, 0x5e, + 0x00, 0xd8, 0x14, 0x1a, 0x9a, 0xe4, 0x2e, 0x8b, 0x8d, 0xaa, 0xe0, 0xd4, 0xc7, 0x78, 0xe0, 0x89, 0xe5, 0x57, 0x5a, + 0xa0, 0xb0, 0xf0, 0xf8, 0xbc, 0x03, 0x7d, 0x17, 0xfd, 0x54, 0xc8, 0xbc, 0xf2, 0x0d, 0x51, 0x74, 0x33, 0xf0, 0xee, + 0x23, 0xc9, 0x8c, 0x89, 0x47, 0x34, 0x39, 0xc7, 0xd2, 0x0b, 0xe1, 0x49, 0x5c, 0xdb, 0x68, 0x79, 0xb6, 0x8c, 0xfa, + 0x66, 0x93, 0x33, 0x5c, 0xec, 0xda, 0x6b, 0x72, 0xdd, 0x32, 0x30, 0x48, 0x3c, 0xb3, 0x62, 0x1f, 0x96, 0x5e, 0x22, + 0x59, 0xc8, 0x4e, 0x0e, 0x00, 0x3e, 0x88, 0xc2, 0x52, 0x62, 0x9c, 0x7c, 0x1d, 0x42, 0xbd, 0x78, 0x09, 0x99, 0x62, + 0xbd, 0x9e, 0x0a, 0x9e, 0x0f, 0x05, 0xcb, 0x3c, 0x78, 0x75, 0xa9, 0x4a, 0x9d, 0x97, 0xb1, 0x9b, 0x99, 0xc0, 0xdd, + 0xa9, 0x65, 0x7e, 0x5d, 0x63, 0x5e, 0x99, 0x6c, 0x90, 0x32, 0x11, 0xe4, 0xe0, 0x3c, 0x6d, 0x89, 0x83, 0xd0, 0x56, + 0x85, 0xf8, 0xd9, 0x2d, 0x15, 0x8a, 0x75, 0xbc, 0xad, 0x56, 0xc1, 0x39, 0xe5, 0xd5, 0x56, 0x9e, 0xa6, 0x3e, 0xc4, + 0x15, 0x5f, 0xab, 0x8d, 0xa5, 0x50, 0xef, 0x3c, 0x1d, 0x42, 0x55, 0xa1, 0x8b, 0xf7, 0x56, 0x2b, 0xaa, 0xac, 0x0f, + 0x4e, 0x0e, 0x48, 0x2c, 0x3d, 0xa5, 0x15, 0x76, 0x7a, 0x02, 0xa6, 0xdc, 0x34, 0xe9, 0xde, 0x6a, 0xc5, 0xa7, 0x94, + 0x7e, 0xd1, 0x9b, 0x83, 0x45, 0xba, 0x0c, 0x4e, 0xff, 0x1f, 0xca, 0xa4, 0xb6, 0xee, 0x1c, 0x64, 0x03, 0x00}; + +} // namespace web_server +} // namespace esphome + +#endif +#endif diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 00b2e20015..0467023039 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,15 +1,16 @@ -#ifdef USE_ARDUINO - #include "web_server.h" - +#ifdef USE_WEBSERVER #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" +#ifdef USE_ARDUINO #include "StreamString.h" +#endif #include @@ -26,7 +27,11 @@ #endif #ifdef USE_WEBSERVER_LOCAL -#include "server_index.h" +#if USE_WEBSERVER_VERSION == 2 +#include "server_index_v2.h" +#elif USE_WEBSERVER_VERSION == 3 +#include "server_index_v3.h" +#endif #endif namespace esphome { @@ -34,27 +39,11 @@ namespace web_server { static const char *const TAG = "web_server"; -#if USE_WEBSERVER_VERSION == 1 -void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, - const std::function &action_func = nullptr) { - stream->print("print(klass.c_str()); - if (obj->is_internal()) - stream->print(" internal"); - stream->print("\" id=\""); - stream->print(klass.c_str()); - stream->print("-"); - stream->print(obj->get_object_id().c_str()); - stream->print("\">"); - stream->print(obj->get_name().c_str()); - stream->print(""); - stream->print(action.c_str()); - if (action_func) { - action_func(*stream, obj); - } - stream->print(""); - stream->print(""); -} +#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 UrlMatch match_url(const std::string &url, bool only_domain = false) { @@ -90,10 +79,22 @@ WebServer::WebServer(web_server_base::WebServerBase *base) #endif } -void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } +#ifdef USE_WEBSERVER_CSS_INCLUDE void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; } -void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } +#endif +#ifdef USE_WEBSERVER_JS_INCLUDE void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } +#endif + +std::string WebServer::get_config_json() { + return json::build_json([this](JsonObject root) { + root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); + root["comment"] = App.get_comment(); + root["ota"] = this->allow_ota_; + root["log"] = this->expose_log_; + root["lang"] = "en"; + }); +} void WebServer::setup() { ESP_LOGCONFIG(TAG, "Setting up web server..."); @@ -102,20 +103,21 @@ void WebServer::setup() { this->events_.onConnect([this](AsyncEventSourceClient *client) { // Configure reconnect timeout and send config + client->send(this->get_config_json().c_str(), "ping", millis(), 30000); - client->send(json::build_json([this](JsonObject root) { - root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); - root["comment"] = App.get_comment(); - root["ota"] = this->allow_ota_; - root["lang"] = "en"; - }).c_str(), - "ping", millis(), 30000); + for (auto &group : this->sorting_groups_) { + client->send(json::build_json([group](JsonObject root) { + root["name"] = group.second.name; + root["sorting_weight"] = group.second.weight; + }).c_str(), + "sorting_group"); + } this->entities_iterator_.begin(this->include_internal_); }); #ifdef USE_LOGGER - if (logger::global_logger != nullptr) { + if (logger::global_logger != nullptr && this->expose_log_) { logger::global_logger->add_on_log_callback( [this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); }); } @@ -159,235 +161,89 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { response->addHeader("Content-Encoding", "gzip"); request->send(response); } -#else +#elif USE_WEBSERVER_VERSION >= 2 void WebServer::handle_index_request(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("text/html"); - // All content is controlled and created by user - so allowing all origins is fine here. - stream->addHeader("Access-Control-Allow-Origin", "*"); -#if USE_WEBSERVER_VERSION == 1 - const std::string &title = App.get_name(); - stream->print(F("")); - stream->print(title.c_str()); - stream->print(F("")); -#else - stream->print(F("")); -#endif -#ifdef USE_WEBSERVER_CSS_INCLUDE - stream->print(F("")); -#endif - if (strlen(this->css_url_) > 0) { - stream->print(F("print(this->css_url_); - stream->print(F("\">")); - } - stream->print(F("")); -#if USE_WEBSERVER_VERSION == 1 - stream->print(F("

")); - stream->print(title.c_str()); - stream->print(F("

")); - stream->print(F("

States

")); - -#ifdef USE_SENSOR - for (auto *obj : App.get_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "sensor", ""); - } -#endif - -#ifdef USE_SWITCH - for (auto *obj : App.get_switches()) { - if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "switch", ""); - } -#endif - -#ifdef USE_BUTTON - for (auto *obj : App.get_buttons()) - write_row(stream, obj, "button", ""); -#endif - -#ifdef USE_BINARY_SENSOR - for (auto *obj : App.get_binary_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "binary_sensor", ""); - } -#endif - -#ifdef USE_FAN - for (auto *obj : App.get_fans()) { - if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "fan", ""); - } -#endif - -#ifdef USE_LIGHT - for (auto *obj : App.get_lights()) { - if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "light", ""); - } -#endif - -#ifdef USE_TEXT_SENSOR - for (auto *obj : App.get_text_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "text_sensor", ""); - } -#endif - -#ifdef USE_COVER - for (auto *obj : App.get_covers()) { - if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "cover", ""); - } -#endif - -#ifdef USE_NUMBER - for (auto *obj : App.get_numbers()) { - if (this->include_internal_ || !obj->is_internal()) { - write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) { - number::Number *number = (number::Number *) obj; - stream.print(R"(traits.get_min_value()); - stream.print(R"(" max=")"); - stream.print(number->traits.get_max_value()); - stream.print(R"(" step=")"); - stream.print(number->traits.get_step()); - stream.print(R"(" value=")"); - stream.print(number->state); - stream.print(R"("/>)"); - }); - } - } -#endif - -#ifdef USE_SELECT - for (auto *obj : App.get_selects()) { - if (this->include_internal_ || !obj->is_internal()) { - write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) { - select::Select *select = (select::Select *) obj; - stream.print(""); - }); - } - } -#endif - -#ifdef USE_LOCK - for (auto *obj : App.get_locks()) { - if (this->include_internal_ || !obj->is_internal()) { - write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) { - lock::Lock *lock = (lock::Lock *) obj; - stream.print(""); - if (lock->traits.get_supports_open()) { - stream.print(""); - } - }); - } - } -#endif - -#ifdef USE_CLIMATE - for (auto *obj : App.get_climates()) { - if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "climate", ""); - } -#endif - - stream->print(F("
NameStateActions

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

")); - if (this->allow_ota_) { - stream->print( - F("

OTA Update

")); - } - stream->print(F("

Debug Log

"));
-#endif
-#ifdef USE_WEBSERVER_JS_INCLUDE
-  if (this->js_include_ != nullptr) {
-    stream->print(F(""));
-  }
-#endif
-#if USE_WEBSERVER_VERSION == 2
-  stream->print(F(""));
-#endif
-  if (strlen(this->js_url_) > 0) {
-    stream->print(F(""));
-  }
-#if USE_WEBSERVER_VERSION == 1
-  stream->print(F("
")); -#else - stream->print(F("")); -#endif - - request->send(stream); + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); + // No gzip header here because the HTML file is so small + request->send(response); } #endif + +#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()); + request->send(response); +} +#endif + #ifdef USE_WEBSERVER_CSS_INCLUDE void WebServer::handle_css_request(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("text/css"); - if (this->css_include_ != nullptr) { - stream->print(this->css_include_); - } - - request->send(stream); + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); } #endif #ifdef USE_WEBSERVER_JS_INCLUDE void WebServer::handle_js_request(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("text/javascript"); - if (this->js_include_ != nullptr) { - stream->addHeader("Access-Control-Allow-Origin", "*"); - stream->print(this->js_include_); - } - - request->send(stream); + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); } #endif #define set_json_id(root, obj, sensor, start_config) \ (root)["id"] = sensor; \ - if (((start_config) == DETAIL_ALL)) \ - (root)["name"] = (obj)->get_name(); + if (((start_config) == DETAIL_ALL)) { \ + (root)["name"] = (obj)->get_name(); \ + (root)["icon"] = (obj)->get_icon(); \ + (root)["entity_category"] = (obj)->get_entity_category(); \ + if ((obj)->is_disabled_by_default()) \ + (root)["is_disabled_by_default"] = (obj)->is_disabled_by_default(); \ + } #define set_json_value(root, obj, sensor, value, start_config) \ - set_json_id((root), (obj), sensor, start_config)(root)["value"] = value; - -#define set_json_state_value(root, obj, sensor, state, value, start_config) \ - set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state; + set_json_id((root), (obj), sensor, start_config); \ + (root)["value"] = value; #define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \ - set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state; \ - if (((start_config) == DETAIL_ALL)) \ - (root)["icon"] = (obj)->get_icon(); + set_json_value(root, obj, sensor, value, start_config); \ + (root)["state"] = state; #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { + if (this->events_.count() == 0) + return; this->events_.send(this->sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { if (obj->get_object_id() != match.id) continue; - std::string data = this->sensor_json(obj, obj->state, DETAIL_STATE); - request->send(200, "application/json", data.c_str()); - return; + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->sensor_json(obj, obj->state, detail); + request->send(200, "application/json", data.c_str()); + return; + } } request->send(404); } std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { - return json::build_json([obj, value, start_config](JsonObject root) { + return json::build_json([this, obj, value, start_config](JsonObject root) { std::string state; - if (isnan(value)) { + if (std::isnan(value)) { state = "NA"; } else { state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); @@ -395,51 +251,77 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail state += " " + obj->get_unit_of_measurement(); } set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + if (!obj->get_unit_of_measurement().empty()) + root["uom"] = obj->get_unit_of_measurement(); + } }); } #endif #ifdef USE_TEXT_SENSOR void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { + if (this->events_.count() == 0) + return; this->events_.send(this->text_sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { if (obj->get_object_id() != match.id) continue; - std::string data = this->text_sensor_json(obj, obj->state, DETAIL_STATE); - request->send(200, "application/json", data.c_str()); - return; + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->text_sensor_json(obj, obj->state, detail); + request->send(200, "application/json", data.c_str()); + return; + } } request->send(404); } std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config) { - return json::build_json([obj, value, start_config](JsonObject root) { + return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } }); } #endif #ifdef USE_SWITCH void WebServer::on_switch_update(switch_::Switch *obj, bool state) { + if (this->events_.count() == 0) + return; this->events_.send(this->switch_json(obj, state, DETAIL_STATE).c_str(), "state"); } -std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { - return json::build_json([obj, value, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); - if (start_config == DETAIL_ALL) { - root["assumed_state"] = obj->assumed_state(); - } - }); -} void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_GET) { - std::string data = this->switch_json(obj, obj->state, DETAIL_STATE); + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->switch_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { this->schedule_([obj]() { obj->toggle(); }); @@ -457,19 +339,36 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } +std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { + return json::build_json([this, obj, value, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); + if (start_config == DETAIL_ALL) { + root["assumed_state"] = obj->assumed_state(); + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} #endif #ifdef USE_BUTTON -std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) { - return json::build_json( - [obj, start_config](JsonObject root) { set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); }); -} - void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_POST && match.method == "press") { + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->button_json(obj, detail); + request->send(200, "application/json", data.c_str()); + } else if (match.method == "press") { this->schedule_([obj]() { obj->press(); }); request->send(200); return; @@ -480,61 +379,86 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } +std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) { + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} #endif #ifdef USE_BINARY_SENSOR void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { + if (this->events_.count() == 0) + return; this->events_.send(this->binary_sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); } -std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { - return json::build_json([obj, value, start_config](JsonObject root) { - set_json_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); - }); -} void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { if (obj->get_object_id() != match.id) continue; - std::string data = this->binary_sensor_json(obj, obj->state, DETAIL_STATE); - request->send(200, "application/json", data.c_str()); - return; + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->binary_sensor_json(obj, obj->state, detail); + request->send(200, "application/json", data.c_str()); + return; + } } request->send(404); } +std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { + return json::build_json([this, obj, value, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, + start_config); + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} #endif #ifdef USE_FAN -void WebServer::on_fan_update(fan::Fan *obj) { this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); } -std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { - return json::build_json([obj, start_config](JsonObject root) { - set_json_state_value(root, obj, "fan-" + obj->get_object_id(), 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(); - } - if (obj->get_traits().supports_oscillation()) - root["oscillation"] = obj->oscillating; - }); +void WebServer::on_fan_update(fan::Fan *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_GET) { - std::string data = this->fan_json(obj, DETAIL_STATE); + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->fan_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); - if (request->hasParam("speed")) { - String speed = request->getParam("speed")->value(); - } if (request->hasParam("speed_level")) { - String speed_level = request->getParam("speed_level")->value(); + auto speed_level = request->getParam("speed_level")->value(); auto val = parse_number(speed_level.c_str()); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str()); @@ -543,7 +467,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc call.set_speed(*val); } if (request->hasParam("oscillation")) { - String speed = request->getParam("oscillation")->value(); + auto speed = request->getParam("oscillation")->value(); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -572,10 +496,33 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } request->send(404); } +std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), 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(); + } + if (obj->get_traits().supports_oscillation()) + root["oscillation"] = obj->oscillating; + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} #endif #ifdef USE_LIGHT void WebServer::on_light_update(light::LightState *obj) { + if (this->events_.count() == 0) + return; this->events_.send(this->light_json(obj, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -583,37 +530,67 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_GET) { - std::string data = this->light_json(obj, DETAIL_STATE); + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->light_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); - if (request->hasParam("brightness")) - call.set_brightness(request->getParam("brightness")->value().toFloat() / 255.0f); - if (request->hasParam("r")) - call.set_red(request->getParam("r")->value().toFloat() / 255.0f); - if (request->hasParam("g")) - call.set_green(request->getParam("g")->value().toFloat() / 255.0f); - if (request->hasParam("b")) - call.set_blue(request->getParam("b")->value().toFloat() / 255.0f); - if (request->hasParam("white_value")) - call.set_white(request->getParam("white_value")->value().toFloat() / 255.0f); - if (request->hasParam("color_temp")) - call.set_color_temperature(request->getParam("color_temp")->value().toFloat()); - + if (request->hasParam("brightness")) { + auto brightness = parse_number(request->getParam("brightness")->value().c_str()); + if (brightness.has_value()) { + call.set_brightness(*brightness / 255.0f); + } + } + if (request->hasParam("r")) { + auto r = parse_number(request->getParam("r")->value().c_str()); + if (r.has_value()) { + call.set_red(*r / 255.0f); + } + } + if (request->hasParam("g")) { + auto g = parse_number(request->getParam("g")->value().c_str()); + if (g.has_value()) { + call.set_green(*g / 255.0f); + } + } + if (request->hasParam("b")) { + auto b = parse_number(request->getParam("b")->value().c_str()); + if (b.has_value()) { + call.set_blue(*b / 255.0f); + } + } + if (request->hasParam("white_value")) { + auto white_value = parse_number(request->getParam("white_value")->value().c_str()); + if (white_value.has_value()) { + call.set_white(*white_value / 255.0f); + } + } + if (request->hasParam("color_temp")) { + auto color_temp = parse_number(request->getParam("color_temp")->value().c_str()); + if (color_temp.has_value()) { + call.set_color_temperature(*color_temp); + } + } if (request->hasParam("flash")) { - float length_s = request->getParam("flash")->value().toFloat(); - call.set_flash_length(static_cast(length_s * 1000)); + auto flash = parse_number(request->getParam("flash")->value().c_str()); + if (flash.has_value()) { + call.set_flash_length(*flash * 1000); + } } - if (request->hasParam("transition")) { - float length_s = request->getParam("transition")->value().toFloat(); - call.set_transition_length(static_cast(length_s * 1000)); + auto transition = parse_number(request->getParam("transition")->value().c_str()); + if (transition.has_value()) { + call.set_transition_length(*transition * 1000); + } } - if (request->hasParam("effect")) { const char *effect = request->getParam("effect")->value().c_str(); call.set_effect(effect); @@ -624,8 +601,10 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } else if (match.method == "turn_off") { auto call = obj->turn_off(); if (request->hasParam("transition")) { - auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000; - call.set_transition_length(length); + auto transition = parse_number(request->getParam("transition")->value().c_str()); + if (transition.has_value()) { + call.set_transition_length(*transition * 1000); + } } this->schedule_([call]() mutable { call.perform(); }); request->send(200); @@ -637,7 +616,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) { - return json::build_json([obj, start_config](JsonObject root) { + return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; @@ -648,6 +627,12 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi for (auto const &option : obj->get_effects()) { opt.add(option->get_name()); } + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } } }); } @@ -655,6 +640,8 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi #ifdef USE_COVER void WebServer::on_cover_update(cover::Cover *obj) { + if (this->events_.count() == 0) + return; this->events_.send(this->cover_json(obj, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -662,10 +649,15 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_GET) { - std::string data = this->cover_json(obj, DETAIL_STATE); + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->cover_json(obj, detail); request->send(200, "application/json", data.c_str()); - continue; + return; } auto call = obj->make_call(); @@ -675,6 +667,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa call.set_command_close(); } else if (match.method == "stop") { call.set_command_stop(); + } else if (match.method == "toggle") { + call.set_command_toggle(); } else if (match.method != "set") { request->send(404); return; @@ -687,10 +681,18 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) - call.set_position(request->getParam("position")->value().toFloat()); - if (request->hasParam("tilt")) - call.set_tilt(request->getParam("tilt")->value().toFloat()); + if (request->hasParam("position")) { + auto position = parse_number(request->getParam("position")->value().c_str()); + if (position.has_value()) { + call.set_position(*position); + } + } + if (request->hasParam("tilt")) { + auto tilt = parse_number(request->getParam("tilt")->value().c_str()); + if (tilt.has_value()) { + call.set_tilt(*tilt); + } + } this->schedule_([call]() mutable { call.perform(); }); request->send(200); @@ -699,19 +701,31 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { - return json::build_json([obj, start_config](JsonObject root) { - set_json_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", + obj->position, start_config); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); + if (obj->get_traits().get_supports_position()) + root["position"] = obj->position; if (obj->get_traits().get_supports_tilt()) root["tilt"] = obj->tilt; + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } }); } #endif #ifdef USE_NUMBER void WebServer::on_number_update(number::Number *obj, float state) { + if (this->events_.count() == 0) + return; this->events_.send(this->number_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -719,8 +733,272 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_GET) { - std::string data = this->number_json(obj, obj->state, DETAIL_STATE); + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->number_json(obj, obj->state, detail); + request->send(200, "application/json", data.c_str()); + return; + } + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + if (request->hasParam("value")) { + auto value = parse_number(request->getParam("value")->value().c_str()); + if (value.has_value()) + call.set_value(*value); + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} + +std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { + return json::build_json([this, obj, value, start_config](JsonObject root) { + set_json_id(root, obj, "number-" + obj->get_object_id(), 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(); + if (!obj->traits.get_unit_of_measurement().empty()) + root["uom"] = obj->traits.get_unit_of_measurement(); + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + if (std::isnan(value)) { + root["value"] = "\"NaN\""; + root["state"] = "NA"; + } else { + root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); + std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); + if (!obj->traits.get_unit_of_measurement().empty()) + state += " " + obj->traits.get_unit_of_measurement(); + root["state"] = state; + } + }); +} +#endif + +#ifdef USE_DATETIME_DATE +void WebServer::on_date_update(datetime::DateEntity *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->date_json(obj, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_dates()) { + if (obj->get_object_id() != match.id) + continue; + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->date_json(obj, detail); + request->send(200, "application/json", data.c_str()); + return; + } + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + + if (!request->hasParam("value")) { + request->send(409); + return; + } + + if (request->hasParam("value")) { + std::string value = request->getParam("value")->value().c_str(); + call.set_date(value); + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} + +std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) { + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); + std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); + root["value"] = value; + root["state"] = value; + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} +#endif // USE_DATETIME_DATE + +#ifdef USE_DATETIME_TIME +void WebServer::on_time_update(datetime::TimeEntity *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->time_json(obj, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_times()) { + if (obj->get_object_id() != match.id) + continue; + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->time_json(obj, detail); + request->send(200, "application/json", data.c_str()); + return; + } + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + + if (!request->hasParam("value")) { + request->send(409); + return; + } + + if (request->hasParam("value")) { + std::string value = request->getParam("value")->value().c_str(); + call.set_time(value); + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) { + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); + std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); + root["value"] = value; + root["state"] = value; + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} +#endif // USE_DATETIME_TIME + +#ifdef USE_DATETIME_DATETIME +void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->datetime_json(obj, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_datetimes()) { + if (obj->get_object_id() != match.id) + continue; + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->datetime_json(obj, detail); + request->send(200, "application/json", data.c_str()); + return; + } + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + + if (!request->hasParam("value")) { + request->send(409); + return; + } + + if (request->hasParam("value")) { + std::string value = request->getParam("value")->value().c_str(); + call.set_datetime(value); + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) { + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); + std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, + obj->minute, obj->second); + root["value"] = value; + root["state"] = value; + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} +#endif // USE_DATETIME_DATETIME + +#ifdef USE_TEXT +void WebServer::on_text_update(text::Text *obj, const std::string &state) { + if (this->events_.count() == 0) + return; + this->events_.send(this->text_json(obj, state, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_texts()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->text_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -732,36 +1010,36 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_value(*value_f); + call.set_value(value.c_str()); } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } request->send(404); } -std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { - return json::build_json([obj, value, start_config](JsonObject root) { - set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); - if (start_config == DETAIL_ALL) { - root["min_value"] = obj->traits.get_min_value(); - root["max_value"] = obj->traits.get_max_value(); - root["step"] = obj->traits.get_step(); - root["mode"] = (int) obj->traits.get_mode(); - } - if (isnan(value)) { - root["value"] = "\"NaN\""; - root["state"] = "NA"; +std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) { + return json::build_json([this, obj, value, start_config](JsonObject root) { + set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); + root["min_length"] = obj->traits.get_min_length(); + root["max_length"] = obj->traits.get_max_length(); + root["pattern"] = obj->traits.get_pattern(); + if (obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD) { + root["state"] = "********"; } else { - root["value"] = value; - std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - if (!obj->traits.get_unit_of_measurement().empty()) - state += " " + obj->traits.get_unit_of_measurement(); - root["state"] = state; + root["state"] = value; + } + root["value"] = value; + if (start_config == DETAIL_ALL) { + root["mode"] = (int) obj->traits.get_mode(); + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } } }); } @@ -769,6 +1047,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail #ifdef USE_SELECT void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { + if (this->events_.count() == 0) + return; this->events_.send(this->select_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -776,8 +1056,13 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_GET) { - std::string data = this->select_json(obj, obj->state, DETAIL_STATE); + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->select_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -790,7 +1075,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); if (request->hasParam("option")) { - String option = request->getParam("option")->value(); + auto option = request->getParam("option")->value(); call.set_option(option.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) } @@ -801,34 +1086,48 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { - return json::build_json([obj, value, start_config](JsonObject root) { - set_json_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); + return json::build_json([this, obj, value, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root.createNestedArray("option"); for (auto &option : obj->traits.get_options()) { opt.add(option); } + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } } }); } #endif +// Longest: HORIZONTAL +#define PSTR_LOCAL(mode_s) strncpy_P(buf, (PGM_P) ((mode_s)), 15) + #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { + if (this->events_.count() == 0) + return; this->events_.send(this->climate_json(obj, DETAIL_STATE).c_str(), "state"); } - void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_GET) { - std::string data = this->climate_json(obj, DETAIL_STATE); + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->climate_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { request->send(404); return; @@ -837,29 +1136,36 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); if (request->hasParam("mode")) { - String mode = request->getParam("mode")->value(); + auto mode = request->getParam("mode")->value(); call.set_mode(mode.c_str()); } + if (request->hasParam("fan_mode")) { + auto mode = request->getParam("fan_mode")->value(); + call.set_fan_mode(mode.c_str()); + } + + if (request->hasParam("swing_mode")) { + auto mode = request->getParam("swing_mode")->value(); + call.set_swing_mode(mode.c_str()); + } + if (request->hasParam("target_temperature_high")) { - String value = request->getParam("target_temperature_high")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature_high(*value_f); + auto target_temperature_high = parse_number(request->getParam("target_temperature_high")->value().c_str()); + if (target_temperature_high.has_value()) + call.set_target_temperature_high(*target_temperature_high); } if (request->hasParam("target_temperature_low")) { - String value = request->getParam("target_temperature_low")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature_low(*value_f); + auto target_temperature_low = parse_number(request->getParam("target_temperature_low")->value().c_str()); + if (target_temperature_low.has_value()) + call.set_target_temperature_low(*target_temperature_low); } if (request->hasParam("target_temperature")) { - String value = request->getParam("target_temperature")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature(*value_f); + auto target_temperature = parse_number(request->getParam("target_temperature")->value().c_str()); + if (target_temperature.has_value()) + call.set_target_temperature(*target_temperature); } this->schedule_([call]() mutable { call.perform(); }); @@ -868,17 +1174,13 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } request->send(404); } - -// Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) strncpy_P(__buf, (PGM_P) ((mode_s)), 15) - std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { - return json::build_json([obj, start_config](JsonObject root) { + return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); 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[16]; if (start_config == DETAIL_ALL) { JsonArray opt = root.createNestedArray("modes"); @@ -910,6 +1212,12 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } } bool has_state = false; @@ -962,21 +1270,22 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf #ifdef USE_LOCK void WebServer::on_lock_update(lock::Lock *obj) { + if (this->events_.count() == 0) + return; this->events_.send(this->lock_json(obj, obj->state, DETAIL_STATE).c_str(), "state"); } -std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { - return json::build_json([obj, value, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, - start_config); - }); -} void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_GET) { - std::string data = this->lock_json(obj, obj->state, DETAIL_STATE); + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->lock_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "lock") { this->schedule_([obj]() { obj->lock(); }); @@ -994,6 +1303,250 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } +std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { + return json::build_json([this, obj, value, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, + start_config); + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} +#endif + +#ifdef USE_VALVE +void WebServer::on_valve_update(valve::Valve *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->valve_json(obj, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (valve::Valve *obj : App.get_valves()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->valve_json(obj, detail); + request->send(200, "application/json", data.c_str()); + return; + } + + auto call = obj->make_call(); + if (match.method == "open") { + call.set_command_open(); + } else if (match.method == "close") { + call.set_command_close(); + } else if (match.method == "stop") { + call.set_command_stop(); + } else if (match.method == "toggle") { + call.set_command_toggle(); + } else if (match.method != "set") { + request->send(404); + return; + } + + auto traits = obj->get_traits(); + if (request->hasParam("position") && !traits.get_supports_position()) { + request->send(409); + return; + } + + if (request->hasParam("position")) { + auto position = parse_number(request->getParam("position")->value().c_str()); + if (position.has_value()) { + call.set_position(*position); + } + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", + obj->position, start_config); + root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); + + if (obj->get_traits().get_supports_position()) + root["position"] = obj->position; + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); + request->send(200, "application/json", data.c_str()); + return; + } + } + request->send(404); +} +std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config) { + return json::build_json([this, obj, value, start_config](JsonObject root) { + char buf[16]; + set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), + PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); + if (start_config == DETAIL_ALL) { + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} +#endif + +#ifdef USE_EVENT +void WebServer::on_event(event::Event *obj, const std::string &event_type) { + this->events_.send(this->event_json(obj, event_type, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (event::Event *obj : App.get_events()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->event_json(obj, "", detail); + request->send(200, "application/json", data.c_str()); + return; + } + } + request->send(404); +} +std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { + return json::build_json([this, obj, event_type, start_config](JsonObject root) { + set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); + if (!event_type.empty()) { + root["event_type"] = event_type; + } + if (start_config == DETAIL_ALL) { + JsonArray event_types = root.createNestedArray("event_types"); + for (auto const &event_type : obj->get_event_types()) { + event_types.add(event_type); + } + root["device_class"] = obj->get_device_class(); + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} +#endif + +#ifdef USE_UPDATE +void WebServer::on_update(update::UpdateEntity *obj) { + if (this->events_.count() == 0) + return; + this->events_.send(this->update_json(obj, DETAIL_STATE).c_str(), "state"); +} +void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (update::UpdateEntity *obj : App.get_updates()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET && match.method.empty()) { + auto detail = DETAIL_STATE; + auto *param = request->getParam("detail"); + if (param && param->value() == "all") { + detail = DETAIL_ALL; + } + std::string data = this->update_json(obj, detail); + request->send(200, "application/json", data.c_str()); + return; + } + + if (match.method != "install") { + request->send(404); + return; + } + + this->schedule_([obj]() mutable { obj->perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { + return json::build_json([this, obj, start_config](JsonObject root) { + set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); + root["value"] = obj->update_info.latest_version; + switch (obj->state) { + case update::UPDATE_STATE_NO_UPDATE: + root["state"] = "NO UPDATE"; + break; + case update::UPDATE_STATE_AVAILABLE: + root["state"] = "UPDATE AVAILABLE"; + break; + case update::UPDATE_STATE_INSTALLING: + root["state"] = "INSTALLING"; + break; + default: + root["state"] = "UNKNOWN"; + break; + } + 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; + if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[obj].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; + } + } + } + }); +} #endif bool WebServer::canHandle(AsyncWebServerRequest *request) { @@ -1010,6 +1563,18 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS + if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { +#ifdef USE_ARDUINO + // Header needs to be added to interesting header list for it to not be + // nuked by the time we handle the request later. + // Only required in Arduino framework. + request->addInterestingHeader(HEADER_CORS_REQ_PNA); +#endif + return true; + } +#endif + UrlMatch match = match_url(request->url().c_str(), true); if (!match.valid) return false; @@ -1024,7 +1589,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { #endif #ifdef USE_BUTTON - if (request->method() == HTTP_POST && match.domain == "button") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "button") return true; #endif @@ -1058,6 +1623,26 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_DATETIME_DATE + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "date") + return true; +#endif + +#ifdef USE_DATETIME_TIME + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "time") + return true; +#endif + +#ifdef USE_DATETIME_DATETIME + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "datetime") + return true; +#endif + +#ifdef USE_TEXT + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text") + return true; +#endif + #ifdef USE_SELECT if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "select") return true; @@ -1073,6 +1658,26 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_VALVE + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "valve") + return true; +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + if (request->method() == HTTP_GET && match.domain == "alarm_control_panel") + return true; +#endif + +#ifdef USE_EVENT + if (request->method() == HTTP_GET && match.domain == "event") + return true; +#endif + +#ifdef USE_UPDATE + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "update") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -1095,6 +1700,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif +#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS + if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { + this->handle_pna_cors_request(request); + return; + } +#endif + UrlMatch match = match_url(request->url().c_str()); #ifdef USE_SENSOR if (match.domain == "sensor") { @@ -1159,6 +1771,34 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif +#ifdef USE_DATETIME_DATE + if (match.domain == "date") { + this->handle_date_request(request, match); + return; + } +#endif + +#ifdef USE_DATETIME_TIME + if (match.domain == "time") { + this->handle_time_request(request, match); + return; + } +#endif + +#ifdef USE_DATETIME_DATETIME + if (match.domain == "datetime") { + this->handle_datetime_request(request, match); + return; + } +#endif + +#ifdef USE_TEXT + if (match.domain == "text") { + this->handle_text_request(request, match); + return; + } +#endif + #ifdef USE_SELECT if (match.domain == "select") { this->handle_select_request(request, match); @@ -1180,10 +1820,40 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_VALVE + if (match.domain == "valve") { + this->handle_valve_request(request, match); + return; + } +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + if (match.domain == "alarm_control_panel") { + this->handle_alarm_control_panel_request(request, match); + + return; + } +#endif + +#ifdef USE_UPDATE + if (match.domain == "update") { + this->handle_update_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } +void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { + this->sorting_entitys_[entity] = SortingComponents{weight, group}; +} + +void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_name, float weight) { + this->sorting_groups_[group_id] = SortingGroup{group_name, weight}; +} + void WebServer::schedule_(std::function &&f) { #ifdef USE_ESP32 xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY); @@ -1196,5 +1866,4 @@ void WebServer::schedule_(std::function &&f) { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index f4122ef754..8edb678169 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -1,19 +1,36 @@ #pragma once -#ifdef USE_ARDUINO - #include "list_entities.h" #include "esphome/components/web_server_base/web_server_base.h" +#ifdef USE_WEBSERVER #include "esphome/core/component.h" #include "esphome/core/controller.h" +#include "esphome/core/entity_base.h" +#include #include #ifdef USE_ESP32 -#include #include #include +#include #endif + +#if USE_WEBSERVER_VERSION >= 2 +extern const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM; +extern const size_t ESPHOME_WEBSERVER_INDEX_HTML_SIZE; +#endif + +#ifdef USE_WEBSERVER_CSS_INCLUDE +extern const uint8_t ESPHOME_WEBSERVER_CSS_INCLUDE[] PROGMEM; +extern const size_t ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE; +#endif + +#ifdef USE_WEBSERVER_JS_INCLUDE +extern const uint8_t ESPHOME_WEBSERVER_JS_INCLUDE[] PROGMEM; +extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE; +#endif + namespace esphome { namespace web_server { @@ -25,6 +42,16 @@ struct UrlMatch { bool valid; ///< Whether this match is valid }; +struct SortingComponents { + float weight; + uint64_t group_id; +}; + +struct SortingGroup { + std::string name; + float weight; +}; + enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; /** This class allows users to create a web server with their ESP nodes. @@ -40,6 +67,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { public: WebServer(web_server_base::WebServerBase *base); +#if USE_WEBSERVER_VERSION == 1 /** Set the URL to the CSS that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css * @@ -47,24 +75,29 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { */ void set_css_url(const char *css_url); - /** Set local path to the script that's embedded in the index page. Defaults to - * - * @param css_include Local path to web server script. - */ - void set_css_include(const char *css_include); - /** Set the URL to the script that's embedded in the index page. Defaults to * https://esphome.io/_static/webserver-v1.min.js * * @param js_url The url to the web server script. */ void set_js_url(const char *js_url); +#endif +#ifdef USE_WEBSERVER_CSS_INCLUDE + /** Set local path to the script that's embedded in the index page. Defaults to + * + * @param css_include Local path to web server script. + */ + void set_css_include(const char *css_include); +#endif + +#ifdef USE_WEBSERVER_JS_INCLUDE /** Set local path to the script that's embedded in the index page. Defaults to * * @param js_include Local path to web server script. */ void set_js_include(const char *js_include); +#endif /** Determine whether internal components should be displayed on the web server. * Defaults to false. @@ -77,6 +110,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { * @param allow_ota. */ void set_allow_ota(bool allow_ota) { this->allow_ota_ = allow_ota; } + /** Set whether or not the webserver should expose the Log. + * + * @param expose_log. + */ + void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -92,6 +130,9 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle an index request under '/'. void handle_index_request(AsyncWebServerRequest *request); + /// Return the webserver configuration as JSON. + std::string get_config_json(); + #ifdef USE_WEBSERVER_CSS_INCLUDE /// Handle included css request under '/0.css'. void handle_css_request(AsyncWebServerRequest *request); @@ -102,6 +143,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_js_request(AsyncWebServerRequest *request); #endif +#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS + // Handle Private Network Access CORS OPTIONS request + void handle_pna_cors_request(AsyncWebServerRequest *request); +#endif + #ifdef USE_SENSOR void on_sensor_update(sensor::Sensor *obj, float state) override; /// Handle a sensor request under '/sensor/'. @@ -188,6 +234,42 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string number_json(number::Number *obj, float value, JsonDetail start_config); #endif +#ifdef USE_DATETIME_DATE + void on_date_update(datetime::DateEntity *obj) override; + /// Handle a date request under '/date/'. + void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the date state with its value as a JSON string. + std::string date_json(datetime::DateEntity *obj, JsonDetail start_config); +#endif + +#ifdef USE_DATETIME_TIME + void on_time_update(datetime::TimeEntity *obj) override; + /// Handle a time request under '/time/'. + void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the time state with its value as a JSON string. + std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config); +#endif + +#ifdef USE_DATETIME_DATETIME + void on_datetime_update(datetime::DateTimeEntity *obj) override; + /// Handle a datetime request under '/datetime/'. + void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the datetime state with its value as a JSON string. + std::string datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config); +#endif + +#ifdef USE_TEXT + void on_text_update(text::Text *obj, const std::string &state) override; + /// Handle a text input request under '/text/'. + void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the text state with its value as a JSON string. + std::string text_json(text::Text *obj, const std::string &value, JsonDetail start_config); +#endif + #ifdef USE_SELECT void on_select_update(select::Select *obj, const std::string &state, size_t index) override; /// Handle a select request under '/select/'. @@ -216,12 +298,56 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif +#ifdef USE_VALVE + void on_valve_update(valve::Valve *obj) override; + + /// Handle a valve request under '/valve//'. + void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the valve state as a JSON string. + std::string valve_json(valve::Valve *obj, JsonDetail start_config); +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; + + /// Handle a alarm_control_panel request under '/alarm_control_panel/'. + void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the alarm_control_panel state with its value as a JSON string. + std::string alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); +#endif + +#ifdef USE_EVENT + void on_event(event::Event *obj, const std::string &event_type) override; + + /// Handle a event request under '/event'. + 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 + void on_update(update::UpdateEntity *obj) override; + + /// Handle a update request under '/update/'. + void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// 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. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. void handleRequest(AsyncWebServerRequest *request) override; /// This web handle is not trivial. - bool isRequestHandlerTrivial() override; + bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming) + + void add_entity_config(EntityBase *entity, float weight, uint64_t group); + void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); protected: void schedule_(std::function &&f); @@ -229,12 +355,22 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; ListEntitiesIterator entities_iterator_; + std::map sorting_entitys_; + std::map sorting_groups_; + +#if USE_WEBSERVER_VERSION == 1 const char *css_url_{nullptr}; - const char *css_include_{nullptr}; const char *js_url_{nullptr}; +#endif +#ifdef USE_WEBSERVER_CSS_INCLUDE + const char *css_include_{nullptr}; +#endif +#ifdef USE_WEBSERVER_JS_INCLUDE const char *js_include_{nullptr}; +#endif bool include_internal_{false}; bool allow_ota_{true}; + bool expose_log_{true}; #ifdef USE_ESP32 std::deque> to_schedule_; SemaphoreHandle_t to_schedule_lock_; @@ -243,5 +379,4 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp new file mode 100644 index 0000000000..c9b38a2dc4 --- /dev/null +++ b/esphome/components/web_server/web_server_v1.cpp @@ -0,0 +1,217 @@ +#include "web_server.h" +#include "esphome/core/application.h" + +#if USE_WEBSERVER_VERSION == 1 + +namespace esphome { +namespace web_server { + +void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, + const std::function &action_func = nullptr) { + stream->print("print(klass.c_str()); + if (obj->is_internal()) + stream->print(" internal"); + stream->print("\" id=\""); + stream->print(klass.c_str()); + stream->print("-"); + stream->print(obj->get_object_id().c_str()); + stream->print("\">"); + stream->print(obj->get_name().c_str()); + stream->print(""); + stream->print(action.c_str()); + if (action_func) { + action_func(*stream, obj); + } + stream->print(""); + stream->print(""); +} + +void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } + +void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } + +void WebServer::handle_index_request(AsyncWebServerRequest *request) { + AsyncResponseStream *stream = request->beginResponseStream("text/html"); + const std::string &title = App.get_name(); + stream->print(F("")); + stream->print(title.c_str()); + stream->print(F("")); +#ifdef USE_WEBSERVER_CSS_INCLUDE + stream->print(F("")); +#endif + if (strlen(this->css_url_) > 0) { + stream->print(F(R"(print(this->css_url_); + stream->print(F("\">")); + } + stream->print(F("")); + stream->print(F("

")); + stream->print(title.c_str()); + stream->print(F("

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

States

")); + +#ifdef USE_SENSOR + for (auto *obj : App.get_sensors()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "sensor", ""); + } +#endif + +#ifdef USE_SWITCH + for (auto *obj : App.get_switches()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "switch", ""); + } +#endif + +#ifdef USE_BUTTON + for (auto *obj : App.get_buttons()) + write_row(stream, obj, "button", ""); +#endif + +#ifdef USE_BINARY_SENSOR + for (auto *obj : App.get_binary_sensors()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "binary_sensor", ""); + } +#endif + +#ifdef USE_FAN + for (auto *obj : App.get_fans()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "fan", ""); + } +#endif + +#ifdef USE_LIGHT + for (auto *obj : App.get_lights()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "light", ""); + } +#endif + +#ifdef USE_TEXT_SENSOR + for (auto *obj : App.get_text_sensors()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "text_sensor", ""); + } +#endif + +#ifdef USE_COVER + for (auto *obj : App.get_covers()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "cover", ""); + } +#endif + +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) { + if (this->include_internal_ || !obj->is_internal()) { + write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) { + number::Number *number = (number::Number *) obj; + stream.print(R"(traits.get_min_value()); + stream.print(R"(" max=")"); + stream.print(number->traits.get_max_value()); + stream.print(R"(" step=")"); + stream.print(number->traits.get_step()); + stream.print(R"(" value=")"); + stream.print(number->state); + stream.print(R"("/>)"); + }); + } + } +#endif + +#ifdef USE_TEXT + for (auto *obj : App.get_texts()) { + if (this->include_internal_ || !obj->is_internal()) { + write_row(stream, obj, "text", "", [](AsyncResponseStream &stream, EntityBase *obj) { + text::Text *text = (text::Text *) obj; + auto mode = (int) text->traits.get_mode(); + stream.print(R"(traits.get_min_length()); + 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(R"(" value=")"); + stream.print(text->state.c_str()); + stream.print(R"("/>)"); + }); + } + } +#endif + +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) { + if (this->include_internal_ || !obj->is_internal()) { + write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) { + select::Select *select = (select::Select *) obj; + stream.print(""); + }); + } + } +#endif + +#ifdef USE_LOCK + for (auto *obj : App.get_locks()) { + if (this->include_internal_ || !obj->is_internal()) { + write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) { + lock::Lock *lock = (lock::Lock *) obj; + stream.print(""); + if (lock->traits.get_supports_open()) { + stream.print(""); + } + }); + } + } +#endif + +#ifdef USE_CLIMATE + for (auto *obj : App.get_climates()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "climate", ""); + } +#endif + + stream->print(F("
NameStateActions

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

")); + if (this->allow_ota_) { + stream->print( + F("

OTA Update

")); + } + stream->print(F("

Debug Log

"));
+#ifdef USE_WEBSERVER_JS_INCLUDE
+  if (this->js_include_ != nullptr) {
+    stream->print(F(""));
+  }
+#endif
+  if (strlen(this->js_url_) > 0) {
+    stream->print(F(""));
+  }
+  stream->print(F("
")); + request->send(stream); +} + +} // namespace web_server +} // namespace esphome +#endif diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 14fb033a56..4f894619b0 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -5,7 +5,15 @@ from esphome.core import coroutine_with_priority, CORE CODEOWNERS = ["@OttoWinter"] DEPENDENCIES = ["network"] -AUTO_LOAD = ["async_tcp"] + + +def AUTO_LOAD(): + if CORE.using_arduino: + return ["async_tcp"] + if CORE.using_esp_idf: + return ["web_server_idf"] + return [] + web_server_base_ns = cg.esphome_ns.namespace("web_server_base") WebServerBase = web_server_base_ns.class_("WebServerBase", cg.Component) @@ -23,9 +31,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CORE.is_esp32: - cg.add_library("WiFi", None) - cg.add_library("FS", None) - cg.add_library("Update", None) - # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json - cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.1.0") + if CORE.using_arduino: + if CORE.is_esp32: + cg.add_library("WiFi", None) + cg.add_library("FS", None) + cg.add_library("Update", None) + # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json + cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.2.2") diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 3c269b28b8..7c09022f27 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,16 +1,18 @@ -#ifdef USE_ARDUINO - #include "web_server_base.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" #include "esphome/core/application.h" -#include +#include "esphome/core/helpers.h" -#ifdef USE_ESP32 +#ifdef USE_ARDUINO +#include +#if defined(USE_ESP32) || defined(USE_LIBRETINY) #include #endif #ifdef USE_ESP8266 #include #endif +#endif namespace esphome { namespace web_server_base { @@ -24,18 +26,22 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); } this->handlers_.push_back(handler); - if (this->server_ != nullptr) + if (this->server_ != nullptr) { this->server_->addHandler(handler); + } } void report_ota_error() { +#ifdef USE_ARDUINO StreamString ss; Update.printError(ss); ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str()); +#endif } void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { +#ifdef USE_ARDUINO bool success; if (index == 0) { ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); @@ -45,9 +51,10 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // NOLINTNEXTLINE(readability-static-accessed-through-instance) success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); #endif -#ifdef USE_ESP32 - if (Update.isRunning()) +#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_LIBRETINY) + if (Update.isRunning()) { Update.abort(); + } success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH); #endif if (!success) { @@ -85,8 +92,10 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin report_ota_error(); } } +#endif } void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { +#ifdef USE_ARDUINO AsyncWebServerResponse *response; if (!Update.hasError()) { response = request->beginResponse(200, "text/plain", "Update Successful!"); @@ -98,10 +107,13 @@ void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { } response->addHeader("Connection", "close"); request->send(response); +#endif } void WebServerBase::add_ota_handler() { +#ifdef USE_ARDUINO this->add_handler(new OTARequestHandler(this)); // NOLINT +#endif } float WebServerBase::get_setup_priority() const { // Before WiFi (captive portal) @@ -110,5 +122,4 @@ float WebServerBase::get_setup_priority() const { } // namespace web_server_base } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index f6d3a84f89..f876d163bc 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,14 +1,18 @@ #pragma once - -#ifdef USE_ARDUINO - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include #include #include "esphome/core/component.h" +#ifdef USE_ARDUINO #include +#elif USE_ESP_IDF +#include "esphome/core/hal.h" +#include "esphome/components/web_server_idf/web_server_idf.h" +#endif namespace esphome { namespace web_server_base { @@ -83,6 +87,7 @@ class WebServerBase : public Component { return; } this->server_ = std::make_shared(this->port_); + // All content is controlled and created by user - so allowing all origins is fine here. DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); this->server_->begin(); @@ -130,6 +135,7 @@ class OTARequestHandler : public AsyncWebHandler { return request->url() == "/update" && request->method() == HTTP_POST; } + // NOLINTNEXTLINE(readability-identifier-naming) bool isRequestHandlerTrivial() override { return false; } protected: @@ -140,5 +146,4 @@ class OTARequestHandler : public AsyncWebHandler { } // namespace web_server_base } // namespace esphome - -#endif // USE_ARDUINO +#endif diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py new file mode 100644 index 0000000000..bd3db24bc6 --- /dev/null +++ b/esphome/components/web_server_idf/__init__.py @@ -0,0 +1,14 @@ +import esphome.config_validation as cv +from esphome.components.esp32 import add_idf_sdkconfig_option + +CODEOWNERS = ["@dentra"] + +CONFIG_SCHEMA = cv.All( + cv.Schema({}), + cv.only_with_esp_idf, +) + + +async def to_code(config): + # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server + add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp new file mode 100644 index 0000000000..6299937ce1 --- /dev/null +++ b/esphome/components/web_server_idf/utils.cpp @@ -0,0 +1,93 @@ +#ifdef USE_ESP_IDF +#include +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "http_parser.h" + +#include "utils.h" + +namespace esphome { +namespace web_server_idf { + +static const char *const TAG = "web_server_idf_utils"; + +void url_decode(char *str) { + char *ptr = str, buf; + for (; *str; str++, ptr++) { + if (*str == '%') { + str++; + if (parse_hex(str, 2, reinterpret_cast(&buf), 1) == 2) { + *ptr = buf; + str++; + } else { + str--; + *ptr = *str; + } + } else if (*str == '+') { + *ptr = ' '; + } else { + *ptr = *str; + } + } + *ptr = *str; +} + +bool request_has_header(httpd_req_t *req, const char *name) { return httpd_req_get_hdr_value_len(req, name); } + +optional request_get_header(httpd_req_t *req, const char *name) { + size_t len = httpd_req_get_hdr_value_len(req, name); + if (len == 0) { + return {}; + } + + std::string str; + str.resize(len); + + auto res = httpd_req_get_hdr_value_str(req, name, &str[0], len + 1); + if (res != ESP_OK) { + return {}; + } + + return {str}; +} + +optional request_get_url_query(httpd_req_t *req) { + auto len = httpd_req_get_url_query_len(req); + if (len == 0) { + return {}; + } + + std::string str; + str.resize(len); + + auto res = httpd_req_get_url_query_str(req, &str[0], len + 1); + if (res != ESP_OK) { + ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); + return {}; + } + + return {str}; +} + +optional query_key_value(const std::string &query_url, const std::string &key) { + if (query_url.empty()) { + return {}; + } + + auto val = std::unique_ptr(new char[query_url.size()]); + if (!val) { + ESP_LOGE(TAG, "Not enough memory to the query key value"); + return {}; + } + + if (httpd_query_key_value(query_url.c_str(), key.c_str(), val.get(), query_url.size()) != ESP_OK) { + return {}; + } + + url_decode(val.get()); + return {val.get()}; +} + +} // namespace web_server_idf +} // namespace esphome +#endif // USE_ESP_IDF diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h new file mode 100644 index 0000000000..9ed17c1d50 --- /dev/null +++ b/esphome/components/web_server_idf/utils.h @@ -0,0 +1,17 @@ +#pragma once +#ifdef USE_ESP_IDF + +#include +#include "esphome/core/helpers.h" + +namespace esphome { +namespace web_server_idf { + +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); +optional query_key_value(const std::string &query_url, const std::string &key); + +} // namespace web_server_idf +} // namespace esphome +#endif // USE_ESP_IDF diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp new file mode 100644 index 0000000000..cf187cd647 --- /dev/null +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -0,0 +1,371 @@ +#ifdef USE_ESP_IDF + +#include + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include "esp_tls_crypto.h" + +#include "utils.h" +#include "web_server_idf.h" + +namespace esphome { +namespace web_server_idf { + +#ifndef HTTPD_409 +#define HTTPD_409 "409 Conflict" +#endif + +#define CRLF_STR "\r\n" +#define CRLF_LEN (sizeof(CRLF_STR) - 1) + +static const char *const TAG = "web_server_idf"; + +void AsyncWebServer::end() { + if (this->server_) { + httpd_stop(this->server_); + this->server_ = nullptr; + } +} + +void AsyncWebServer::begin() { + if (this->server_) { + this->end(); + } + 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; }; + if (httpd_start(&this->server_, &config) == ESP_OK) { + const httpd_uri_t handler_get = { + .uri = "", + .method = HTTP_GET, + .handler = AsyncWebServer::request_handler, + .user_ctx = this, + }; + httpd_register_uri_handler(this->server_, &handler_get); + + const httpd_uri_t handler_post = { + .uri = "", + .method = HTTP_POST, + .handler = AsyncWebServer::request_post_handler, + .user_ctx = this, + }; + httpd_register_uri_handler(this->server_, &handler_post); + + const httpd_uri_t handler_options = { + .uri = "", + .method = HTTP_OPTIONS, + .handler = AsyncWebServer::request_handler, + .user_ctx = this, + }; + httpd_register_uri_handler(this->server_, &handler_options); + } +} + +esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { + ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri); + auto content_type = request_get_header(r, "Content-Type"); + if (content_type.has_value() && *content_type != "application/x-www-form-urlencoded") { + ESP_LOGW(TAG, "Only application/x-www-form-urlencoded supported for POST request"); + // fallback to get handler to support backward compatibility + return AsyncWebServer::request_handler(r); + } + + if (!request_has_header(r, "Content-Length")) { + ESP_LOGW(TAG, "Content length is requred for post: %s", r->uri); + httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr); + return ESP_OK; + } + + if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { + ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + + std::string post_query; + if (r->content_len > 0) { + post_query.resize(r->content_len); + const int ret = httpd_req_recv(r, &post_query[0], r->content_len + 1); + if (ret <= 0) { // 0 return value indicates connection closed + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr); + return ESP_ERR_TIMEOUT; + } + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + } + + AsyncWebServerRequest req(r, std::move(post_query)); + return static_cast(r->user_ctx)->request_handler_(&req); +} + +esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) { + ESP_LOGVV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri); + AsyncWebServerRequest req(r); + return static_cast(r->user_ctx)->request_handler_(&req); +} + +esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const { + for (auto *handler : this->handlers_) { + if (handler->canHandle(request)) { + // At now process only basic requests. + // OTA requires multipart request support and handleUpload for it + handler->handleRequest(request); + return ESP_OK; + } + } + if (this->on_not_found_) { + this->on_not_found_(request); + return ESP_OK; + } + return ESP_ERR_NOT_FOUND; +} + +AsyncWebServerRequest::~AsyncWebServerRequest() { + delete this->rsp_; + for (const auto &pair : this->params_) { + delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) + } +} + +bool AsyncWebServerRequest::hasHeader(const char *name) const { return request_has_header(*this, name); } + +optional AsyncWebServerRequest::get_header(const char *name) const { + return request_get_header(*this, name); +} + +std::string AsyncWebServerRequest::url() const { + auto *str = strchr(this->req_->uri, '?'); + if (str == nullptr) { + return this->req_->uri; + } + return std::string(this->req_->uri, str - this->req_->uri); +} + +std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } + +void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { + httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); +} + +void AsyncWebServerRequest::send(int code, const char *content_type, const char *content) { + this->init_response_(nullptr, code, content_type); + if (content) { + httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); + } else { + httpd_resp_send(*this, nullptr, 0); + } +} + +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_send(*this, nullptr, 0); +} + +void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { + httpd_resp_set_status(*this, code == 200 ? HTTPD_200 + : code == 404 ? HTTPD_404 + : code == 409 ? HTTPD_409 + : to_string(code).c_str()); + + if (content_type && *content_type) { + httpd_resp_set_type(*this, content_type); + } + httpd_resp_set_hdr(*this, "Accept-Ranges", "none"); + + for (const auto &pair : DefaultHeaders::Instance().headers_) { + httpd_resp_set_hdr(*this, pair.first.c_str(), pair.second.c_str()); + } + + delete this->rsp_; + this->rsp_ = rsp; +} + +bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const { + if (username == nullptr || password == nullptr || *username == 0) { + return true; + } + auto auth = this->get_header("Authorization"); + if (!auth.has_value()) { + return false; + } + + auto *auth_str = auth.value().c_str(); + + const auto auth_prefix_len = sizeof("Basic ") - 1; + if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) { + ESP_LOGW(TAG, "Only Basic authorization supported yet"); + return false; + } + + std::string user_info; + user_info += username; + user_info += ':'; + user_info += password; + + size_t n = 0, out; + esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast(user_info.c_str()), user_info.size()); + + auto digest = std::unique_ptr(new char[n + 1]); + esp_crypto_base64_encode(reinterpret_cast(digest.get()), n, &out, + reinterpret_cast(user_info.c_str()), user_info.size()); + + return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0; +} + +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()); + httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); +} + +AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { + auto find = this->params_.find(name); + if (find != this->params_.end()) { + return find->second; + } + + optional val = query_key_value(this->post_query_, name); + if (!val.has_value()) { + auto url_query = request_get_url_query(*this); + if (url_query.has_value()) { + val = query_key_value(url_query.value(), name); + } + } + + AsyncWebParameter *param = nullptr; + if (val.has_value()) { + param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory) + } + this->params_.insert({name, param}); + return param; +} + +void AsyncWebServerResponse::addHeader(const char *name, const char *value) { + httpd_resp_set_hdr(*this->req_, name, value); +} + +void AsyncResponseStream::print(float value) { this->print(to_string(value)); } + +void AsyncResponseStream::printf(const char *fmt, ...) { + va_list args; + + va_start(args, fmt); + const int length = vsnprintf(nullptr, 0, fmt, args); + va_end(args); + + std::string str; + str.resize(length); + + va_start(args, fmt); + vsnprintf(&str[0], length + 1, fmt, args); + va_end(args); + + this->print(str); +} + +AsyncEventSource::~AsyncEventSource() { + for (auto *ses : this->sessions_) { + delete ses; // NOLINT(cppcoreguidelines-owning-memory) + } +} + +void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { + auto *rsp = new AsyncEventSourceResponse(request, this); // NOLINT(cppcoreguidelines-owning-memory) + if (this->on_connect_) { + this->on_connect_(rsp); + } + this->sessions_.insert(rsp); +} + +void AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + for (auto *ses : this->sessions_) { + ses->send(message, event, id, reconnect); + } +} + +AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server) + : server_(server) { + httpd_req_t *req = *request; + + httpd_resp_set_status(req, HTTPD_200); + httpd_resp_set_type(req, "text/event-stream"); + httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); + httpd_resp_set_hdr(req, "Connection", "keep-alive"); + + for (const auto &pair : DefaultHeaders::Instance().headers_) { + httpd_resp_set_hdr(req, pair.first.c_str(), pair.second.c_str()); + } + + httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN); + + req->sess_ctx = this; + req->free_ctx = AsyncEventSourceResponse::destroy; + + this->hd_ = req->handle; + this->fd_ = httpd_req_to_sockfd(req); +} + +void AsyncEventSourceResponse::destroy(void *ptr) { + auto *rsp = static_cast(ptr); + rsp->server_->sessions_.erase(rsp); + delete rsp; // NOLINT(cppcoreguidelines-owning-memory) +} + +void AsyncEventSourceResponse::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + if (this->fd_ == 0) { + return; + } + + std::string ev; + + if (reconnect) { + ev.append("retry: ", sizeof("retry: ") - 1); + ev.append(to_string(reconnect)); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (id) { + ev.append("id: ", sizeof("id: ") - 1); + ev.append(to_string(id)); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (event && *event) { + ev.append("event: ", sizeof("event: ") - 1); + ev.append(event); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (message && *message) { + ev.append("data: ", sizeof("data: ") - 1); + ev.append(message); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (ev.empty()) { + return; + } + + ev.append(CRLF_STR, CRLF_LEN); + + // Sending chunked content prelude + auto cs = str_snprintf("%x" CRLF_STR, 4 * sizeof(ev.size()) + CRLF_LEN, ev.size()); + httpd_socket_send(this->hd_, this->fd_, cs.c_str(), cs.size(), 0); + + // Sendiing content chunk + httpd_socket_send(this->hd_, this->fd_, ev.c_str(), ev.size(), 0); + + // Indicate end of chunk + httpd_socket_send(this->hd_, this->fd_, CRLF_STR, CRLF_LEN, 0); +} + +} // namespace web_server_idf +} // namespace esphome + +#endif // !defined(USE_ESP_IDF) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h new file mode 100644 index 0000000000..2ead5e3f03 --- /dev/null +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -0,0 +1,285 @@ +#pragma once +#ifdef USE_ESP_IDF + +#include + +#include +#include +#include +#include +#include + +namespace esphome { +namespace web_server_idf { + +#define F(string_literal) (string_literal) +#define PGM_P const char * +#define strncpy_P strncpy + +using String = std::string; + +class AsyncWebParameter { + public: + AsyncWebParameter(std::string value) : value_(std::move(value)) {} + const std::string &value() const { return this->value_; } + + protected: + std::string value_; +}; + +class AsyncWebServerRequest; + +class AsyncWebServerResponse { + public: + AsyncWebServerResponse(const AsyncWebServerRequest *req) : req_(req) {} + virtual ~AsyncWebServerResponse() {} + + // NOLINTNEXTLINE(readability-identifier-naming) + void addHeader(const char *name, const char *value); + + virtual const char *get_content_data() const = 0; + virtual size_t get_content_size() const = 0; + + protected: + const AsyncWebServerRequest *req_; +}; + +class AsyncWebServerResponseEmpty : public AsyncWebServerResponse { + public: + AsyncWebServerResponseEmpty(const AsyncWebServerRequest *req) : AsyncWebServerResponse(req) {} + + const char *get_content_data() const override { return nullptr; }; + size_t get_content_size() const override { return 0; }; +}; + +class AsyncWebServerResponseContent : public AsyncWebServerResponse { + public: + AsyncWebServerResponseContent(const AsyncWebServerRequest *req, std::string content) + : AsyncWebServerResponse(req), content_(std::move(content)) {} + + const char *get_content_data() const override { return this->content_.c_str(); }; + size_t get_content_size() const override { return this->content_.size(); }; + + protected: + std::string content_; +}; + +class AsyncResponseStream : public AsyncWebServerResponse { + public: + AsyncResponseStream(const AsyncWebServerRequest *req) : AsyncWebServerResponse(req) {} + + const char *get_content_data() const override { return this->content_.c_str(); }; + size_t get_content_size() const override { return this->content_.size(); }; + + void print(const char *str) { this->content_.append(str); } + void print(const std::string &str) { this->content_.append(str); } + void print(float value); + void printf(const char *fmt, ...) __attribute__((format(printf, 2, 3))); + + protected: + std::string content_; +}; + +class AsyncWebServerResponseProgmem : public AsyncWebServerResponse { + public: + AsyncWebServerResponseProgmem(const AsyncWebServerRequest *req, const uint8_t *data, const size_t size) + : AsyncWebServerResponse(req), data_(data), size_(size) {} + + const char *get_content_data() const override { return reinterpret_cast(this->data_); }; + size_t get_content_size() const override { return this->size_; }; + + protected: + const uint8_t *data_; + size_t size_; +}; + +class AsyncWebServerRequest { + friend class AsyncWebServer; + + public: + ~AsyncWebServerRequest(); + + http_method method() const { return static_cast(this->req_->method); } + std::string url() const; + std::string host() const; + // NOLINTNEXTLINE(readability-identifier-naming) + size_t contentLength() const { return this->req_->content_len; } + + bool authenticate(const char *username, const char *password) const; + // NOLINTNEXTLINE(readability-identifier-naming) + void requestAuthentication(const char *realm = nullptr) const; + + void redirect(const std::string &url); + + void send(AsyncWebServerResponse *response); + void send(int code, const char *content_type = nullptr, const char *content = nullptr); + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse(int code, const char *content_type) { + auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, code, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse(int code, const char *content_type, const std::string &content) { + auto *res = new AsyncWebServerResponseContent(this, content); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, code, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data, + const size_t data_size) { + auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, code, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncResponseStream *beginResponseStream(const char *content_type) { + auto *res = new AsyncResponseStream(this); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, 200, content_type); + return res; + } + + // NOLINTNEXTLINE(readability-identifier-naming) + bool hasParam(const std::string &name) { return this->getParam(name) != nullptr; } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const std::string &name); + + // NOLINTNEXTLINE(readability-identifier-naming) + bool hasArg(const char *name) { return this->hasParam(name); } + std::string arg(const std::string &name) { + auto *param = this->getParam(name); + if (param) { + return param->value(); + } + return {}; + } + + operator httpd_req_t *() const { return this->req_; } + optional get_header(const char *name) const; + // NOLINTNEXTLINE(readability-identifier-naming) + bool hasHeader(const char *name) const; + + protected: + httpd_req_t *req_; + AsyncWebServerResponse *rsp_{}; + std::map params_; + std::string post_query_; + AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} + AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {} + void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type); +}; + +class AsyncWebHandler; + +class AsyncWebServer { + public: + AsyncWebServer(uint16_t port) : port_(port){}; + ~AsyncWebServer() { this->end(); } + + // NOLINTNEXTLINE(readability-identifier-naming) + void onNotFound(std::function fn) { on_not_found_ = std::move(fn); } + + void begin(); + void end(); + + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebHandler &addHandler(AsyncWebHandler *handler) { + this->handlers_.push_back(handler); + return *handler; + } + + 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; + std::vector handlers_; + std::function on_not_found_{}; +}; + +class AsyncWebHandler { + public: + virtual ~AsyncWebHandler() {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual bool canHandle(AsyncWebServerRequest *request) { return false; } + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleRequest(AsyncWebServerRequest *request) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleUpload(AsyncWebServerRequest *request, const std::string &filename, size_t index, uint8_t *data, + size_t len, bool final) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual bool isRequestHandlerTrivial() { return true; } +}; + +class AsyncEventSource; + +class AsyncEventSourceResponse { + friend class AsyncEventSource; + + public: + void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + + protected: + AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server); + static void destroy(void *p); + AsyncEventSource *server_; + httpd_handle_t hd_{}; + int fd_{}; +}; + +using AsyncEventSourceClient = AsyncEventSourceResponse; + +class AsyncEventSource : public AsyncWebHandler { + friend class AsyncEventSourceResponse; + using connect_handler_t = std::function; + + public: + AsyncEventSource(std::string url) : url_(std::move(url)) {} + ~AsyncEventSource() override; + + // NOLINTNEXTLINE(readability-identifier-naming) + bool canHandle(AsyncWebServerRequest *request) override { + return request->method() == HTTP_GET && request->url() == this->url_; + } + // NOLINTNEXTLINE(readability-identifier-naming) + void handleRequest(AsyncWebServerRequest *request) override; + // NOLINTNEXTLINE(readability-identifier-naming) + void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); } + + void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + + size_t count() const { return this->sessions_.size(); } + + protected: + std::string url_; + std::set sessions_; + connect_handler_t on_connect_{}; +}; + +class DefaultHeaders { + friend class AsyncWebServerRequest; + friend class AsyncEventSourceResponse; + + public: + // NOLINTNEXTLINE(readability-identifier-naming) + void addHeader(const char *name, const char *value) { this->headers_.emplace_back(name, value); } + + // NOLINTNEXTLINE(readability-identifier-naming) + static DefaultHeaders &Instance() { + static DefaultHeaders instance; + return instance; + } + + protected: + std::vector> headers_; +}; + +} // namespace web_server_idf +} // namespace esphome + +using namespace esphome::web_server_idf; // NOLINT(google-global-names-in-headers) + +#endif // !defined(USE_ESP_IDF) diff --git a/esphome/components/weikai/__init__.py b/esphome/components/weikai/__init__.py new file mode 100644 index 0000000000..4248c48e35 --- /dev/null +++ b/esphome/components/weikai/__init__.py @@ -0,0 +1,108 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import ( + CONF_BAUD_RATE, + CONF_CHANNEL, + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, +) + +CODEOWNERS = ["@DrCoolZic"] +AUTO_LOAD = ["uart"] + +MULTI_CONF = True +CONF_STOP_BITS = "stop_bits" +CONF_PARITY = "parity" +CONF_CRYSTAL = "crystal" +CONF_UART = "uart" +CONF_TEST_MODE = "test_mode" + +weikai_ns = cg.esphome_ns.namespace("weikai") +WeikaiComponent = weikai_ns.class_("WeikaiComponent", cg.Component) +WeikaiChannel = weikai_ns.class_("WeikaiChannel", uart.UARTComponent) + + +def check_channel_max(value, max): + channel_uniq = [] + channel_dup = [] + for x in value[CONF_UART]: + if x[CONF_CHANNEL] > max - 1: + raise cv.Invalid(f"Invalid channel number: {x[CONF_CHANNEL]}") + if x[CONF_CHANNEL] not in channel_uniq: + channel_uniq.append(x[CONF_CHANNEL]) + else: + channel_dup.append(x[CONF_CHANNEL]) + if len(channel_dup) > 0: + raise cv.Invalid(f"Duplicate channel list: {channel_dup}") + return value + + +def check_channel_max_4(value): + return check_channel_max(value, 4) + + +def check_channel_max_2(value): + return check_channel_max(value, 2) + + +WKBASE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(WeikaiComponent), + cv.Optional(CONF_CRYSTAL, default=14745600): cv.int_, + cv.Optional(CONF_TEST_MODE, default=0): cv.int_, + cv.Required(CONF_UART): cv.ensure_list( + { + cv.Required(CONF_ID): cv.declare_id(WeikaiChannel), + cv.Optional(CONF_CHANNEL, default=0): cv.int_range(min=0, max=3), + cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), + cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), + cv.Optional(CONF_PARITY, default="NONE"): cv.enum( + uart.UART_PARITY_OPTIONS, upper=True + ), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def register_weikai(var, config): + """Register an weikai device with the given config.""" + cg.add(var.set_crystal(config[CONF_CRYSTAL])) + cg.add(var.set_test_mode(config[CONF_TEST_MODE])) + await cg.register_component(var, config) + for uart_elem in config[CONF_UART]: + chan = cg.new_Pvariable(uart_elem[CONF_ID]) + cg.add(chan.set_channel_name(str(uart_elem[CONF_ID]))) + cg.add(chan.set_parent(var)) + cg.add(chan.set_channel(uart_elem[CONF_CHANNEL])) + cg.add(chan.set_baud_rate(uart_elem[CONF_BAUD_RATE])) + cg.add(chan.set_stop_bits(uart_elem[CONF_STOP_BITS])) + cg.add(chan.set_parity(uart_elem[CONF_PARITY])) + + +def validate_pin_mode(value): + """Checks input/output mode inconsistency""" + if not (value[CONF_MODE][CONF_INPUT] or value[CONF_MODE][CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_MODE][CONF_INPUT] and value[CONF_MODE][CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + return value + + +WEIKAI_PIN_SCHEMA = cv.Schema( + { + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp new file mode 100644 index 0000000000..00bce9bcff --- /dev/null +++ b/esphome/components/weikai/weikai.cpp @@ -0,0 +1,615 @@ +/// @file weikai.cpp +/// @brief WeiKai component family - classes implementation +/// @date Last Modified: 2024/04/06 15:13:11 +/// @details The classes declared in this file can be used by the Weikai family + +#include "weikai.h" + +namespace esphome { +namespace weikai { + +/*! @mainpage Weikai source code documentation + This documentation provides information about the implementation of the family of WeiKai Components in ESPHome. + Here is the class diagram related to Weikai family of components: + @image html weikai_class.png + + @section WKRingBuffer_ The WKRingBuffer template class +The WKRingBuffer template class has it names implies implement a simple ring buffer helper class. This straightforward +container implements FIFO functionality, enabling bytes to be pushed into one side and popped from the other in the +order of entry. Implementation is classic and therefore not described in any details. + + @section WeikaiRegister_ The WeikaiRegister class + The WeikaiRegister helper class creates objects that act as proxies to the device registers. + @details This is an abstract virtual class (interface) that provides all the necessary access to registers while hiding + the actual implementation. The access to the registers can be made through an I²C bus in for example for wk2168_i2c + component or through a SPI bus for example in the case of the wk2168_spi component. Derived classes will actually + performs the specific bus operations. + + @section WeikaiRegisterI2C_ WeikaiRegisterI2C + The weikai_i2c::WeikaiRegisterI2C class implements the virtual methods of the WeikaiRegister class for an I2C bus. + + @section WeikaiRegisterSPI_ WeikaiRegisterSPI + The weikai_spi::WeikaiRegisterSPI class implements the virtual methods of the WeikaiRegister class for an SPI bus. + + @section WeikaiComponent_ The WeikaiComponent class +The WeikaiComponent class stores the information global to a WeiKai family component and provides methods to set/access +this information. It also serves as a container for WeikaiChannel instances. This is done by maintaining an array of +references these WeikaiChannel instances. This class derives from the esphome::Component classes. This class override +esphome::Component::loop() method to facilitate the seamless transfer of accumulated bytes from the receive +FIFO into the ring buffer. This process ensures quick access to the stored bytes, enhancing the overall efficiency of +the component. + + @section WeikaiComponentI2C_ WeikaiComponentI2C + The weikai_i2c::WeikaiComponentI2C class implements the virtual methods of the WeikaiComponent class for an I2C bus. + + @section WeikaiComponentSPI_ WeikaiComponentSPI + The weikai_spi::WeikaiComponentSPI class implements the virtual methods of the WeikaiComponent class for an SPI bus. + + @section WeikaiGPIOPin_ WeikaiGPIOPin class + The WeikaiGPIOPin class is an helper class to expose the GPIO pins of WK family components as if they were internal + GPIO pins. It also provides the setup() and dump_summary() methods. + + @section WeikaiChannel_ The WeikaiChannel class + The WeikaiChannel class is used to implement all the virtual methods of the ESPHome uart::UARTComponent class. An + individual instance of this class is created for each UART channel. It has a link back to the WeikaiComponent object it + belongs to. This class derives from the uart::UARTComponent class. It collaborates through an aggregation with + WeikaiComponent. This implies that WeikaiComponent acts as a container, housing several WeikaiChannel instances. + Furthermore, the WeikaiChannel class derives from the ESPHome uart::UARTComponent class, it also has an association + relationship with the WKRingBuffer and WeikaiRegister helper classes. Consequently, when a WeikaiChannel instance is + destroyed, the associated WKRingBuffer instance is also destroyed. + +*/ + +static const char *const TAG = "weikai"; + +/// @brief convert an int to binary representation as C++ std::string +/// @param val integer to convert +/// @return a std::string +inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); } +/// Convert std::string to C string +#define I2S2CS(val) (i2s(val).c_str()) + +/// @brief measure the time elapsed between two calls +/// @param last_time time of the previous call +/// @return the elapsed time in milliseconds +uint32_t elapsed_ms(uint32_t &last_time) { + uint32_t e = millis() - last_time; + last_time = millis(); + return e; +}; + +/// @brief Converts the parity enum value to a C string +/// @param parity enum +/// @return the string +const char *p2s(uart::UARTParityOptions parity) { + using namespace uart; + switch (parity) { + case UART_CONFIG_PARITY_NONE: + return "NONE"; + case UART_CONFIG_PARITY_EVEN: + return "EVEN"; + case UART_CONFIG_PARITY_ODD: + return "ODD"; + default: + return "UNKNOWN"; + } +} + +/// @brief Display a buffer in hexadecimal format (32 hex values / line) for debug +void print_buffer(const uint8_t *data, size_t length) { + char hex_buffer[100]; + hex_buffer[(3 * 32) + 1] = 0; + for (size_t i = 0; i < length; i++) { + snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]); + if (i % 32 == 31) { + ESP_LOGVV(TAG, " %s", hex_buffer); + } + } + if (length % 32) { + // null terminate if incomplete line + hex_buffer[3 * (length % 32) + 2] = 0; + ESP_LOGVV(TAG, " %s", hex_buffer); + } +} + +static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER", + "SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"}; +static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL", + "TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"}; + +// method to print a register value as text: used in the log messages ... +const char *reg_to_str(int reg, bool page1) { + if (reg == WKREG_GPDAT) { + return "GPDAT"; + } else if (reg == WKREG_GPDIR) { + return "GPDIR"; + } else { + return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F]; + } +} + +enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO + +/////////////////////////////////////////////////////////////////////////////// +// The WeikaiRegister methods +/////////////////////////////////////////////////////////////////////////////// +WeikaiRegister &WeikaiRegister::operator=(uint8_t value) { + write_reg(value); + return *this; +} + +WeikaiRegister &WeikaiRegister::operator&=(uint8_t value) { + value &= read_reg(); + write_reg(value); + return *this; +} + +WeikaiRegister &WeikaiRegister::operator|=(uint8_t value) { + value |= read_reg(); + write_reg(value); + return *this; +} + +/////////////////////////////////////////////////////////////////////////////// +// The WeikaiComponent methods +/////////////////////////////////////////////////////////////////////////////// +void WeikaiComponent::loop() { + if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP) + return; + + // If there are some bytes in the receive FIFO we transfers them to the ring buffers + size_t transferred = 0; + for (auto *child : this->children_) { + // we look if some characters has been received in the fifo + transferred += child->xfer_fifo_to_buffer_(); + } + if (transferred > 0) { + ESP_LOGV(TAG, "we transferred %d bytes from fifo to buffer...", transferred); + } + +#ifdef TEST_COMPONENT + static uint32_t loop_time = 0; + static uint32_t loop_count = 0; + uint32_t time = 0; + + if (test_mode_ == 1) { // test component in loopback + ESP_LOGI(TAG, "Component loop %" PRIu32 " for %s : %" PRIu32 " ms since last call ...", loop_count++, + this->get_name(), millis() - loop_time); + loop_time = millis(); + char message[64]; + elapsed_ms(time); // set time to now + for (int i = 0; i < this->children_.size(); i++) { + if (i != ((loop_count - 1) % this->children_.size())) // we do only one per loop + continue; + snprintf(message, sizeof(message), "%s:%s", this->get_name(), children_[i]->get_channel_name()); + children_[i]->uart_send_test_(message); + uint32_t const start_time = millis(); + while (children_[i]->tx_fifo_is_not_empty_()) { // wait until buffer empty + if (millis() - start_time > 1500) { + ESP_LOGE(TAG, "timeout while flushing - %d bytes left in buffer...", children_[i]->tx_in_fifo_()); + break; + } + yield(); // reschedule our thread to avoid blocking + } + bool status = children_[i]->uart_receive_test_(message); + ESP_LOGI(TAG, "Test %s => send/received %u bytes %s - execution time %" PRIu32 " ms...", message, + RING_BUFFER_SIZE, status ? "correctly" : "with error", elapsed_ms(time)); + } + } + + if (this->test_mode_ == 2) { // test component in echo mode + for (auto *child : this->children_) { + uint8_t data = 0; + if (child->available()) { + child->read_byte(&data); + ESP_LOGI(TAG, "echo mode: read -> send %02X", data); + child->write_byte(data); + } + } + } + if (test_mode_ == 3) { + test_gpio_input_(); + } + + if (test_mode_ == 4) { + test_gpio_output_(); + } +#endif +} + +#if defined(TEST_COMPONENT) +void WeikaiComponent::test_gpio_input_() { + static bool init_input{false}; + static uint8_t state{0}; + uint8_t value; + if (!init_input) { + init_input = true; + // set all pins in input mode + this->reg(WKREG_GPDIR, 0) = 0x00; + ESP_LOGI(TAG, "initializing all pins to input mode"); + state = this->reg(WKREG_GPDAT, 0); + ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, I2S2CS(state)); + } + value = this->reg(WKREG_GPDAT, 0); + if (value != state) { + ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, I2S2CS(value)); + state = value; + } +} + +void WeikaiComponent::test_gpio_output_() { + static bool init_output{false}; + static uint8_t state{0}; + if (!init_output) { + init_output = true; + // set all pins in output mode + this->reg(WKREG_GPDIR, 0) = 0xFF; + ESP_LOGI(TAG, "initializing all pins to output mode"); + this->reg(WKREG_GPDAT, 0) = state; + ESP_LOGI(TAG, "setting all outputs to 0"); + } + state = ~state; + this->reg(WKREG_GPDAT, 0) = state; + ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, I2S2CS(state)); + delay(100); // NOLINT +} +#endif + +/////////////////////////////////////////////////////////////////////////////// +// The WeikaiGPIOPin methods +/////////////////////////////////////////////////////////////////////////////// +bool WeikaiComponent::read_pin_val_(uint8_t pin) { + this->input_state_ = this->reg(WKREG_GPDAT, 0); + ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin), I2S2CS(input_state_)); + return this->input_state_ & (1 << pin); +} + +void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) { + if (value) { + this->output_state_ |= (1 << pin); + } else { + this->output_state_ &= ~(1 << pin); + } + ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value), I2S2CS(this->output_state_)); + this->reg(WKREG_GPDAT, 0) = this->output_state_; +} + +void WeikaiComponent::set_pin_direction_(uint8_t pin, gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + this->pin_config_ &= ~(1 << pin); // clear bit (input mode) + } else { + if (flags == gpio::FLAG_OUTPUT) { + this->pin_config_ |= 1 << pin; // set bit (output mode) + } else { + ESP_LOGE(TAG, "pin %d direction invalid", pin); + } + } + ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, I2S2CS(this->pin_config_)); + this->reg(WKREG_GPDIR, 0) = this->pin_config_; // TODO check ~ +} + +void WeikaiGPIOPin::setup() { + ESP_LOGCONFIG(TAG, "Setting GPIO pin %d mode to %s", this->pin_, + flags_ == gpio::FLAG_INPUT ? "Input" + : this->flags_ == gpio::FLAG_OUTPUT ? "Output" + : "NOT SPECIFIED"); + // ESP_LOGCONFIG(TAG, "Setting GPIO pins mode to '%s' %02X", I2S2CS(this->flags_), this->flags_); + 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; +} + +/////////////////////////////////////////////////////////////////////////////// +// The WeikaiChannel methods +/////////////////////////////////////////////////////////////////////////////// +void WeikaiChannel::setup_channel() { + ESP_LOGCONFIG(TAG, " Setting up UART %s:%s ...", this->parent_->get_name(), this->get_channel_name()); + // we enable transmit and receive on this channel + if (this->check_channel_down()) { + ESP_LOGCONFIG(TAG, " Error channel %s not working...", this->get_channel_name()); + } + this->reset_fifo_(); + this->receive_buffer_.clear(); + this->set_line_param_(); + this->set_baudrate_(); +} + +void WeikaiChannel::dump_channel() { + ESP_LOGCONFIG(TAG, " UART %s ...", this->get_channel_name()); + ESP_LOGCONFIG(TAG, " Baud rate: %" PRIu32 " Bd", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Data bits: %u", this->data_bits_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); + ESP_LOGCONFIG(TAG, " Parity: %s", p2s(this->parity_)); +} + +void WeikaiChannel::reset_fifo_() { + // enable transmission and reception + this->reg(WKREG_SCR) = SCR_RXEN | SCR_TXEN; + // we reset and enable transmit and receive FIFO + this->reg(WKREG_FCR) = FCR_TFEN | FCR_RFEN | FCR_TFRST | FCR_RFRST; +} + +void WeikaiChannel::set_line_param_() { + this->data_bits_ = 8; // always equal to 8 for WeiKai (cant be changed) + uint8_t lcr = 0; + if (this->stop_bits_ == 2) + lcr |= LCR_STPL; + switch (this->parity_) { // parity selection settings + case uart::UART_CONFIG_PARITY_ODD: + lcr |= (LCR_PAEN | LCR_PAR_ODD); + break; + case uart::UART_CONFIG_PARITY_EVEN: + lcr |= (LCR_PAEN | LCR_PAR_EVEN); + break; + default: + break; // no parity 000x + } + this->reg(WKREG_LCR) = lcr; // write LCR + ESP_LOGV(TAG, " line config: %d data_bits, %d stop_bits, parity %s register [%s]", this->data_bits_, + this->stop_bits_, p2s(this->parity_), I2S2CS(lcr)); +} + +void WeikaiChannel::set_baudrate_() { + if (this->baud_rate_ > this->parent_->crystal_ / 16) { + baud_rate_ = this->parent_->crystal_ / 16; + ESP_LOGE(TAG, " Requested baudrate too high for crystal=%" PRIu32 " Hz. Has been reduced to %" PRIu32 " Bd", + this->parent_->crystal_, this->baud_rate_); + }; + uint16_t const val_int = this->parent_->crystal_ / (this->baud_rate_ * 16) - 1; + uint16_t val_dec = (this->parent_->crystal_ % (this->baud_rate_ * 16)) / (this->baud_rate_ * 16); + uint8_t const baud_high = (uint8_t) (val_int >> 8); + uint8_t const baud_low = (uint8_t) (val_int & 0xFF); + while (val_dec > 0x0A) + val_dec /= 0x0A; + uint8_t const baud_dec = (uint8_t) (val_dec); + + this->parent_->page1_ = true; // switch to page 1 + this->reg(WKREG_SPAGE) = 1; + this->reg(WKREG_BRH) = baud_high; + this->reg(WKREG_BRL) = baud_low; + this->reg(WKREG_BRD) = baud_dec; + this->parent_->page1_ = false; // switch back to page 0 + this->reg(WKREG_SPAGE) = 0; + + ESP_LOGV(TAG, " Crystal=%" PRId32 " baudrate=%" PRId32 " => registers [%d %d %d]", this->parent_->crystal_, + this->baud_rate_, baud_high, baud_low, baud_dec); +} + +inline bool WeikaiChannel::tx_fifo_is_not_empty_() { return this->reg(WKREG_FSR) & FSR_TFDAT; } + +size_t WeikaiChannel::tx_in_fifo_() { + size_t tfcnt = this->reg(WKREG_TFCNT); + if (tfcnt == 0) { + uint8_t const fsr = this->reg(WKREG_FSR); + if (fsr & FSR_TFFULL) { + ESP_LOGVV(TAG, "tx FIFO full FSR=%s", I2S2CS(fsr)); + tfcnt = FIFO_SIZE; + } + } + ESP_LOGVV(TAG, "tx FIFO contains %d bytes", tfcnt); + return tfcnt; +} + +size_t WeikaiChannel::rx_in_fifo_() { + size_t available = this->reg(WKREG_RFCNT); + uint8_t const fsr = this->reg(WKREG_FSR); + if (fsr & (FSR_RFOE | FSR_RFLB | FSR_RFFE | FSR_RFPE)) { + if (fsr & FSR_RFOE) + ESP_LOGE(TAG, "Receive data overflow FSR=%s", I2S2CS(fsr)); + if (fsr & FSR_RFLB) + ESP_LOGE(TAG, "Receive line break FSR=%s", I2S2CS(fsr)); + if (fsr & FSR_RFFE) + ESP_LOGE(TAG, "Receive frame error FSR=%s", I2S2CS(fsr)); + if (fsr & FSR_RFPE) + ESP_LOGE(TAG, "Receive parity error FSR=%s", I2S2CS(fsr)); + } + if ((available == 0) && (fsr & FSR_RFDAT)) { + // here we should be very careful because we can have something like this: + // - at time t0 we read RFCNT=0 because nothing yet received + // - at time t0+delta we might read FIFO not empty because one byte has just been received + // - so to be sure we need to do another read of RFCNT and if it is still zero -> buffer full + available = this->reg(WKREG_RFCNT); + if (available == 0) { // still zero ? + ESP_LOGV(TAG, "rx FIFO is full FSR=%s", I2S2CS(fsr)); + available = FIFO_SIZE; + } + } + ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, I2S2CS(fsr)); + return available; +} + +bool WeikaiChannel::check_channel_down() { + // to check if we channel is up we write to the LCR W/R register + // note that this will put a break on the tx line for few ms + WeikaiRegister &lcr = this->reg(WKREG_LCR); + lcr = 0x3F; + uint8_t val = lcr; + if (val != 0x3F) { + ESP_LOGE(TAG, "R/W of register failed expected 0x3F received 0x%02X", val); + return true; + } + lcr = 0; + val = lcr; + if (val != 0x00) { + ESP_LOGE(TAG, "R/W of register failed expected 0x00 received 0x%02X", val); + return true; + } + return false; +} + +bool WeikaiChannel::peek_byte(uint8_t *buffer) { + auto available = this->receive_buffer_.count(); + if (!available) + xfer_fifo_to_buffer_(); + return this->receive_buffer_.peek(*buffer); +} + +int WeikaiChannel::available() { + size_t available = this->receive_buffer_.count(); + if (!available) + available = xfer_fifo_to_buffer_(); + return available; +} + +bool WeikaiChannel::read_array(uint8_t *buffer, size_t length) { + bool status = true; + auto available = this->receive_buffer_.count(); + if (length > available) { + ESP_LOGW(TAG, "read_array: buffer underflow requested %d bytes only %d bytes available...", length, available); + length = available; + status = false; + } + // retrieve the bytes from ring buffer + for (size_t i = 0; i < length; i++) { + this->receive_buffer_.pop(buffer[i]); + } + ESP_LOGVV(TAG, "read_array(ch=%d buffer[0]=%02X, length=%d): status %s", this->channel_, *buffer, length, + status ? "OK" : "ERROR"); + return status; +} + +void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) { + if (length > XFER_MAX_SIZE) { + ESP_LOGE(TAG, "Write_array: invalid call - requested %d bytes but max size %d ...", length, XFER_MAX_SIZE); + length = XFER_MAX_SIZE; + } + this->reg(0).write_fifo(const_cast(buffer), length); +} + +void WeikaiChannel::flush() { + uint32_t const start_time = millis(); + while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty + if (millis() - start_time > 200) { + ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms...", this->tx_in_fifo_()); + return; + } + yield(); // reschedule our thread to avoid blocking + } +} + +size_t WeikaiChannel::xfer_fifo_to_buffer_() { + size_t to_transfer; + size_t free; + while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) { + // while bytes in fifo and some room in the buffer we transfer + if (to_transfer > XFER_MAX_SIZE) + to_transfer = XFER_MAX_SIZE; // we can only do so much + if (to_transfer > free) + to_transfer = free; // we'll do the rest next time + if (to_transfer) { + uint8_t data[to_transfer]; + this->reg(0).read_fifo(data, to_transfer); + for (size_t i = 0; i < to_transfer; i++) + this->receive_buffer_.push(data[i]); + } + } // while work to do + return to_transfer; +} + +/// +// TEST COMPONENT +// +#ifdef TEST_COMPONENT +/// @addtogroup test_ Test component information +/// @{ + +/// @brief An increment "Functor" (i.e. a class object that acts like a method with state!) +/// +/// Functors are objects that can be treated as though they are a function or function pointer. +class Increment { + public: + /// @brief constructor: initialize current value to 0 + Increment() : i_(0) {} + /// @brief overload of the parenthesis operator. + /// Returns the current value and auto increment it + /// @return the current value. + uint8_t operator()() { return i_++; } + + private: + uint8_t i_; +}; + +/// @brief Hex converter to print/display a buffer in hexadecimal format (32 hex values / line). +/// @param buffer contains the values to display +void print_buffer(std::vector buffer) { + char hex_buffer[100]; + hex_buffer[(3 * 32) + 1] = 0; + for (size_t i = 0; i < buffer.size(); i++) { + snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", buffer[i]); + if (i % 32 == 31) + ESP_LOGI(TAG, " %s", hex_buffer); + } + if (buffer.size() % 32) { + // null terminate if incomplete line + hex_buffer[3 * (buffer.size() % 32) + 1] = 0; + ESP_LOGI(TAG, " %s", hex_buffer); + } +} + +/// @brief test the write_array method +void WeikaiChannel::uart_send_test_(char *message) { + auto start_exec = micros(); + std::vector output_buffer(XFER_MAX_SIZE); + generate(output_buffer.begin(), output_buffer.end(), Increment()); // fill with incrementing number + size_t to_send = RING_BUFFER_SIZE; + while (to_send) { + this->write_array(&output_buffer[0], XFER_MAX_SIZE); // we send the buffer + this->flush(); + to_send -= XFER_MAX_SIZE; + } + ESP_LOGV(TAG, "%s => sent %d bytes - exec time %d µs ...", message, RING_BUFFER_SIZE, micros() - start_exec); +} + +/// @brief test read_array method +bool WeikaiChannel::uart_receive_test_(char *message) { + auto start_exec = micros(); + bool status = true; + size_t received = 0; + std::vector buffer(RING_BUFFER_SIZE); + + // we wait until we have received all the bytes + uint32_t const start_time = millis(); + status = true; + while (received < RING_BUFFER_SIZE) { + while (XFER_MAX_SIZE > this->available()) { + this->xfer_fifo_to_buffer_(); + if (millis() - start_time > 1500) { + ESP_LOGE(TAG, "uart_receive_test_() timeout: only %d bytes received...", this->available()); + break; + } + yield(); // reschedule our thread to avoid blocking + } + status = this->read_array(&buffer[received], XFER_MAX_SIZE) && status; + received += XFER_MAX_SIZE; + } + + uint8_t peek_value = 0; + this->peek_byte(&peek_value); + if (peek_value != 0) { + ESP_LOGE(TAG, "Peek first byte value error..."); + status = false; + } + + for (size_t i = 0; i < RING_BUFFER_SIZE; i++) { + if (buffer[i] != i % XFER_MAX_SIZE) { + ESP_LOGE(TAG, "Read buffer contains error...b=%x i=%x", buffer[i], i % XFER_MAX_SIZE); + print_buffer(buffer); + status = false; + break; + } + } + + ESP_LOGV(TAG, "%s => received %d bytes status %s - exec time %d µs ...", message, received, status ? "OK" : "ERROR", + micros() - start_exec); + return status; +} + +/// @} +#endif + +} // namespace weikai +} // namespace esphome diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h new file mode 100644 index 0000000000..175a067b27 --- /dev/null +++ b/esphome/components/weikai/weikai.h @@ -0,0 +1,443 @@ +/// @file weikai.h +/// @author DrCoolZic +/// @brief WeiKai component family - classes declaration +/// @date Last Modified: 2024/04/06 14:44:17 +/// @details The classes declared in this file can be used by the Weikai family +/// of UART and GPIO expander components. As of today it provides support for +/// wk2124_spi, wk2132_spi, wk2168_spi, wk2204_spi, wk2212_spi, +/// wk2132_i2c, wk2168_i2c, wk2204_i2c, wk2212_i2c + +#pragma once +#include +#include +#include +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "wk_reg_def.h" + +/// When the TEST_COMPONENT flag is defined we include some auto-test methods. Used to test the software during +/// development but can also be used in situ to test if the component is working correctly. For release we do +/// not set it by default but you can set it by using the following lines in you configuration file: +/// @code +/// esphome: +/// platformio_options: +/// build_flags: +/// - -DTEST_COMPONENT +/// @endcode +// #define TEST_COMPONENT + +namespace esphome { +namespace weikai { + +/// @brief XFER_MAX_SIZE defines the maximum number of bytes allowed during one transfer. +#if defined(I2C_BUFFER_LENGTH) +constexpr size_t XFER_MAX_SIZE = I2C_BUFFER_LENGTH; +#else +constexpr size_t XFER_MAX_SIZE = 128; +#endif + +/// @brief size of the internal WeiKai FIFO +constexpr size_t FIFO_SIZE = 256; + +/// @brief size of the ring buffer set to size of the FIFO +constexpr size_t RING_BUFFER_SIZE = FIFO_SIZE; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief This is an helper class that provides a simple ring buffers that works as a FIFO +/// @details This ring buffer is used to buffer the bytes received in the FIFO of the Weika device. The best way to read +/// characters from the device FIFO, is to first check how many bytes were received and then read them all at once. +/// Unfortunately in all the code I have reviewed the characters are read one by one in a while loop by checking if +/// bytes are available then reading the byte until no more byte available. This is pretty inefficient for two reasons: +/// - Fist you need to perform a test for each byte to read +/// - and second you call the read byte method for each character. +/// . +/// Assuming you need to read 100 bytes that results into 200 calls. This is to compare to 2 calls (one to find the +/// number of bytes available plus one to read all the bytes) in the best case! If the registers you read are located on +/// the micro-controller this is acceptable because the registers can be accessed fast. But when the registers are +/// located on a remote device accessing them requires several cycles on a slow bus. As it it not possible to fix this +/// problem by asking users to rewrite their code, I have implemented this ring buffer that store the bytes received +/// locally. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +template class WKRingBuffer { + public: + /// @brief pushes an item at the tail of the fifo + /// @param item item to push + /// @return true if item has been pushed, false il item could not pushed (buffer full) + bool push(const T item) { + if (is_full()) + return false; + this->rb_[this->head_] = item; + this->head_ = (this->head_ + 1) % SIZE; + this->count_++; + return true; + } + + /// @brief return and remove the item at head of the fifo + /// @param item item read + /// @return true if an item has been retrieved, false il no item available (buffer empty) + bool pop(T &item) { + if (is_empty()) + return false; + item = this->rb_[this->tail_]; + this->tail_ = (this->tail_ + 1) % SIZE; + this->count_--; + return true; + } + + /// @brief return the value of the item at fifo's head without removing it + /// @param item pointer to item to return + /// @return true if item has been retrieved, false il no item available (buffer empty) + bool peek(T &item) { + if (is_empty()) + return false; + item = this->rb_[this->tail_]; + return true; + } + + /// @brief test is the Ring Buffer is empty ? + /// @return true if empty + inline bool is_empty() { return (this->count_ == 0); } + + /// @brief test is the ring buffer is full ? + /// @return true if full + inline bool is_full() { return (this->count_ == SIZE); } + + /// @brief return the number of item in the ring buffer + /// @return the number of items + inline size_t count() { return this->count_; } + + /// @brief returns the number of free positions in the buffer + /// @return how many items can be added + inline size_t free() { return SIZE - this->count_; } + + /// @brief clear the buffer content + inline void clear() { this->head_ = this->tail_ = this->count_ = 0; } + + protected: + std::array rb_{0}; ///< the ring buffer + int tail_{0}; ///< position of the next element to read + int head_{0}; ///< position of the next element to write + size_t count_{0}; ///< count number of element in the buffer +}; + +class WeikaiComponent; +// class WeikaiComponentSPI; +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief WeikaiRegister objects acts as proxies to access remote register independently of the bus type. +/// @details This is an abstract interface class that provides many operations to access to registers while hiding +/// the actual implementation. This allow to accesses the registers in the Weikai component abstract class independently +/// of the actual bus (I2C, SPI). The derived classes will actually implements the specific bus operations dependant of +/// the bus used. +/// @n typical usage of WeikaiRegister: +/// @code +/// WeikaiRegister reg_X {&WeikaiComponent, ADDR_REGISTER_X, CHANNEL_NUM} // declaration +/// reg_X |= 0x01; // set bit 0 of the weikai register +/// reg_X &= ~0x01; // reset bit 0 of the weikai register +/// reg_X = 10; // Set the value of weikai register +/// uint val = reg_X; // get the value of weikai register +/// @endcode +class WeikaiRegister { + public: + /// @brief WeikaiRegister constructor. + /// @param comp our parent WeikaiComponent + /// @param reg address of the register + /// @param channel the channel of this register + WeikaiRegister(WeikaiComponent *const comp, uint8_t reg, uint8_t channel) + : comp_(comp), register_(reg), channel_(channel) {} + virtual ~WeikaiRegister() {} + + /// @brief overloads the = operator. This is used to set a value into the weikai register + /// @param value to be set + /// @return this object + WeikaiRegister &operator=(uint8_t value); + + /// @brief overloads the compound &= operator. This is often used to reset bits in the weikai register + /// @param value performs an & operation with value and store the result + /// @return this object + WeikaiRegister &operator&=(uint8_t value); + + /// @brief overloads the compound |= operator. This is often used to set bits in the weikai register + /// @param value performs an | operation with value and store the result + /// @return this object + WeikaiRegister &operator|=(uint8_t value); + + /// @brief cast operator that returns the content of the weikai register + operator uint8_t() const { return read_reg(); } + + /// @brief reads the register + /// @return the value read from the register + virtual uint8_t read_reg() const = 0; + + /// @brief writes the register + /// @param value to write in the register + virtual void write_reg(uint8_t value) = 0; + + /// @brief read an array of bytes from the receiver fifo + /// @param data pointer to data buffer + /// @param length number of bytes to read + virtual void read_fifo(uint8_t *data, size_t length) const = 0; + + /// @brief write an array of bytes to the transmitter fifo + /// @param data pointer to data buffer + /// @param length number of bytes to write + virtual void write_fifo(uint8_t *data, size_t length) = 0; + + WeikaiComponent *const comp_; ///< pointer to our parent (aggregation) + uint8_t register_; ///< address of the register + uint8_t channel_; ///< channel for this register +}; + +class WeikaiChannel; // forward declaration +//////////////////////////////////////////////////////////////////////////////////// +/// @brief The WeikaiComponent class stores the information global to the WeiKai component +/// and provides methods to set/access this information. It is also the container of +/// the WeikaiChannel children objects. This class is derived from esphome::Component +/// class. +//////////////////////////////////////////////////////////////////////////////////// +class WeikaiComponent : public Component { + public: + /// @brief virtual destructor + virtual ~WeikaiComponent() {} + + /// @brief store crystal frequency + /// @param crystal frequency + void set_crystal(uint32_t crystal) { this->crystal_ = crystal; } + + /// @brief store if the component is in test mode + /// @param test_mode 0=normal mode any other values mean component in test mode + void set_test_mode(int test_mode) { this->test_mode_ = test_mode; } + + /// @brief store the name for the component + /// @param name the name as defined by the python code generator + void set_name(std::string &&name) { this->name_ = std::move(name); } + + /// @brief Get the name of the component + /// @return the name + const char *get_name() { return this->name_.c_str(); } + + /// @brief override the Component loop() + void loop() override; + + bool page1() { return page1_; } + + /// @brief Factory method to create a Register object + /// @param reg address of the register + /// @param channel channel associated with this register + /// @return a reference to WeikaiRegister + virtual WeikaiRegister ®(uint8_t reg, uint8_t channel) = 0; + + protected: + friend class WeikaiChannel; + + /// @brief Get the priority of the component + /// @return the priority + /// @details The priority is set below setup_priority::BUS because we use + /// the spi/i2c busses (which has a priority of BUS) to communicate and the WeiKai + /// therefore it is seen by our client almost as if it was a bus. + float get_setup_priority() const override { return setup_priority::BUS - 0.1F; } + + friend class WeikaiGPIOPin; + /// Helper method to read the value of a pin. + bool read_pin_val_(uint8_t pin); + + /// Helper method to write the value of a pin. + void write_pin_val_(uint8_t pin, bool value); + + /// Helper method to set the pin mode of a pin. + void set_pin_direction_(uint8_t pin, gpio::Flags flags); + +#ifdef TEST_COMPONENT + /// @defgroup test_ Test component information + /// @brief Contains information about the auto-tests of the component + /// @{ + void test_gpio_input_(); + void test_gpio_output_(); + /// @} +#endif + + uint8_t pin_config_{0x00}; ///< pin config mask: 1 means OUTPUT, 0 means INPUT + uint8_t output_state_{0x00}; ///< output state: 1 means HIGH, 0 means LOW + uint8_t input_state_{0x00}; ///< input pin states: 1 means HIGH, 0 means LOW + uint32_t crystal_; ///< crystal value; + int test_mode_; ///< test mode value (0 -> no tests) + bool page1_{false}; ///< set to true when in "page1 mode" + std::vector children_{}; ///< the list of WeikaiChannel UART children + std::string name_; ///< name of entity +}; + +/////////////////////////////////////////////////////////////////////////////// +/// @brief Helper class to expose a WeiKai family IO pin as an internal GPIO pin. +/////////////////////////////////////////////////////////////////////////////// +class WeikaiGPIOPin : public GPIOPin { + public: + void set_parent(WeikaiComponent *parent) { this->parent_ = parent; } + 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; } + + void setup() override; + std::string dump_summary() 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_); } + + protected: + WeikaiComponent *parent_{nullptr}; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief The WeikaiChannel class is used to implement all the virtual methods of the ESPHome +/// uart::UARTComponent virtual class. This class is common to the different members of the Weikai +/// components family and therefore avoid code duplication. +/////////////////////////////////////////////////////////////////////////////////////////////////// +class WeikaiChannel : public uart::UARTComponent { + public: + /// @brief We belongs to this WeikaiComponent + /// @param parent pointer to the component we belongs to + void set_parent(WeikaiComponent *parent) { + this->parent_ = parent; + this->parent_->children_.push_back(this); // add ourself to the list (vector) + } + + /// @brief Sets the channel number + /// @param channel number + void set_channel(uint8_t channel) { this->channel_ = channel; } + + /// @brief The name as generated by the Python code generator + /// @param name of the channel + void set_channel_name(std::string &&name) { this->name_ = std::move(name); } + + /// @brief Get the channel name + /// @return the name + const char *get_channel_name() { return this->name_.c_str(); } + + /// @brief Setup the channel + void virtual setup_channel(); + + /// @brief dump channel information + void virtual dump_channel(); + + /// @brief Factory method to create a WeikaiRegister proxy object + /// @param reg address of the register + /// @return a reference to WeikaiRegister + WeikaiRegister ®(uint8_t reg) { return this->parent_->reg(reg, channel_); } + + // + // we implements/overrides the virtual class from UARTComponent + // + + /// @brief Writes a specified number of bytes to a serial port + /// @param buffer pointer to the buffer + /// @param length number of bytes to write + /// @details This method sends 'length' characters from the buffer to the serial line. Unfortunately (unlike the + /// Arduino equivalent) this method does not return any flag and therefore it is not possible to know if any/all bytes + /// have been transmitted correctly. Another problem is that it is not possible to know ahead of time how many bytes + /// we can safely send as there is no tx_available() method provided! To avoid overrun when using the write method you + /// can use the flush() method to wait until the transmit fifo is empty. + /// @n Typical usage could be: + /// @code + /// // ... + /// uint8_t buffer[128]; + /// // ... + /// write_array(&buffer, length); + /// flush(); + /// // ... + /// @endcode + void write_array(const uint8_t *buffer, size_t length) override; + + /// @brief Reads a specified number of bytes from a serial port + /// @param buffer buffer to store the bytes + /// @param length number of bytes to read + /// @return true if succeed, false otherwise + /// @details Typical usage: + /// @code + /// // ... + /// auto length = available(); + /// uint8_t buffer[128]; + /// if (length > 0) { + /// auto status = read_array(&buffer, length) + /// // test status ... + /// } + /// @endcode + bool read_array(uint8_t *buffer, size_t length) override; + + /// @brief Reads the first byte in FIFO without removing it + /// @param buffer pointer to the byte + /// @return true if succeed reading one byte, false if no character available + /// @details This method returns the next byte from receiving buffer without removing it from the internal fifo. It + /// returns true if a character is available and has been read, false otherwise.\n + bool peek_byte(uint8_t *buffer) override; + + /// @brief Returns the number of bytes in the receive buffer + /// @return the number of bytes available in the receiver fifo + int available() override; + + /// @brief Flush the output fifo. + /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data + /// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore + /// we wait until all bytes are gone with a timeout of 100 ms + void flush() override; + + protected: + friend class WeikaiComponent; + + /// @brief this cannot happen with external uart therefore we do nothing + void check_logger_conflict() override {} + + /// @brief reset the weikai internal FIFO + void reset_fifo_(); + + /// @brief set the line parameters + void set_line_param_(); + + /// @brief set the baud rate + void set_baudrate_(); + + /// @brief Returns the number of bytes in the receive fifo + /// @return the number of bytes in the fifo + size_t rx_in_fifo_(); + + /// @brief Returns the number of bytes in the transmit fifo + /// @return the number of bytes in the fifo + size_t tx_in_fifo_(); + + /// @brief test if transmit buffer is not empty in the status register (optimization) + /// @return true if not emptygroup test_ + bool tx_fifo_is_not_empty_(); + + /// @brief transfer bytes from the weikai internal FIFO to the buffer (if any) + /// @return number of bytes transferred + size_t xfer_fifo_to_buffer_(); + + /// @brief check if channel is alive + /// @return true if OK + bool virtual check_channel_down(); + +#ifdef TEST_COMPONENT + /// @ingroup test_ + /// @{ + + /// @brief Test the write_array() method + /// @param message to display + void uart_send_test_(char *message); + + /// @brief Test the read_array() method + /// @param message to display + /// @return true if success + bool uart_receive_test_(char *message); + /// @} +#endif + + /// @brief the buffer where we store temporarily the bytes received + WKRingBuffer receive_buffer_; + WeikaiComponent *parent_; ///< our WK2168component parent + uint8_t channel_; ///< our Channel number + uint8_t data_; ///< a one byte buffer for register read storage + std::string name_; ///< name of the entity +}; + +} // namespace weikai +} // namespace esphome diff --git a/esphome/components/weikai/wk_reg_def.h b/esphome/components/weikai/wk_reg_def.h new file mode 100644 index 0000000000..f3c90b196a --- /dev/null +++ b/esphome/components/weikai/wk_reg_def.h @@ -0,0 +1,304 @@ +/// @file wk_reg_def.h +/// @author DrCoolZic +/// @brief WeiKai component family - registers' definition +/// @date Last Modified: 2024/02/18 15:49:18 +#pragma once + +namespace esphome { +namespace weikai { + +//////////////////////////////////////////////////////////////////////////////////////// +/// Definition of the WeiKai registers +//////////////////////////////////////////////////////////////////////////////////////// + +/// @defgroup wk2168_gr_ WeiKai Global Registers +/// This section groups all **Global Registers**: these registers are global to the +/// the WeiKai chip (i.e. independent of the UART channel used) +/// @note only registers and parameters used have been fully documented +/// @{ + +/// @brief Global Control Register - 00 0000 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | M0 | M1 | RSV | C4EN | C3EN | C2EN | C1EN | name +/// ------------------------------------------------------------------------- +/// | R | R | R | R | W/R | W/R | W/R | W/R | type +/// ------------------------------------------------------------------------- +/// | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_GENA = 0x00; +/// @brief Channel 4 enable clock (0: disable, 1: enable) +constexpr uint8_t GENA_C4EN = 1 << 3; +/// @brief Channel 3 enable clock (0: disable, 1: enable) +constexpr uint8_t GENA_C3EN = 1 << 2; +/// @brief Channel 2 enable clock (0: disable, 1: enable) +constexpr uint8_t GENA_C2EN = 1 << 1; +/// @brief Channel 1 enable clock (0: disable, 1: enable) +constexpr uint8_t GENA_C1EN = 1 << 0; + +/// @brief Global Reset Register - 00 0001 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | C4SLEEP| C3SLEEP| C2SLEEP| C1SLEEP| C4RST | C3RST | C2RST | C1RST | name +/// ------------------------------------------------------------------------- +/// | R | R | R | R | W1/R0 | W1/R0 | W1/R0 | W1/R0 | type +/// ------------------------------------------------------------------------- +/// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_GRST = 0x01; +/// @brief Channel 4 soft reset (0: not reset, 1: reset) +constexpr uint8_t GRST_C4RST = 1 << 3; +/// @brief Channel 3 soft reset (0: not reset, 1: reset) +constexpr uint8_t GRST_C3RST = 1 << 2; +/// @brief Channel 2 soft reset (0: not reset, 1: reset) +constexpr uint8_t GRST_C2RST = 1 << 1; +/// @brief Channel 1 soft reset (0: not reset, 1: reset) +constexpr uint8_t GRST_C1RST = 1 << 0; + +/// @brief Global Master channel control register (not used) - 000010 +constexpr uint8_t WKREG_GMUT = 0x02; + +/// Global interrupt register (not used) - 01 0000 +constexpr uint8_t WKREG_GIER = 0x10; + +/// Global interrupt flag register (not used) 01 0001 +constexpr uint8_t WKREG_GIFR = 0x11; + +/// @brief Global GPIO direction register - 10 0001 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | PD7 | PD6 | PD5 | PD4 | PD3 | PD2 | PD1 | PD0 | name +/// ------------------------------------------------------------------------- +/// | W/R | W/R | W/R | W/R | W/R | W/R | W/R | W/R | type +/// ------------------------------------------------------------------------- +/// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_GPDIR = 0x21; + +/// @brief Global GPIO data register - 11 0001 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | PV7 | PV6 | PV5 | PV4 | PV3 | PV2 | PV1 | PV0 | name +/// ------------------------------------------------------------------------- +/// | W/R | W/R | W/R | W/R | W/R | W/R | W/R | W/R | type +/// ------------------------------------------------------------------------- +/// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_GPDAT = 0x31; + +/// @} +/// @defgroup WeiKai_cr_ WeiKai Channel Registers +/// @brief Definition of the register linked to a particular channel +/// @details This topic groups all the **Channel Registers**: these registers are specific +/// to the a specific channel i.e. each channel has its own set of registers +/// @note only registers and parameters used have been documented +/// @{ + +/// @defgroup cr_p0 Channel registers when SPAGE=0 +/// @brief Definition of the register linked to a particular channel when SPAGE=0 +/// @details The channel registers are further splitted into two groups. +/// This first group is defined when the Global register WKREG_SPAGE is 0 +/// @{ + +/// @brief Global Page register c0/c1 0011 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | RSV | PAGE | name +/// ------------------------------------------------------------------------- +/// | R | R | R | R | R | R | R | W/R | type +/// ------------------------------------------------------------------------- +/// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_SPAGE = 0x03; + +/// @brief Serial Control Register - c0/c1 0100 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | RSV | SLPEN | TXEN | RXEN | name +/// ------------------------------------------------------------------------- +/// | R | R | R | R | R | R/W | R/W | W/R | type +/// ------------------------------------------------------------------------- +/// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_SCR = 0x04; +/// @brief transmission control (0: enable, 1: disable) +constexpr uint8_t SCR_TXEN = 1 << 1; +/// @brief receiving control (0: enable, 1: disable) +constexpr uint8_t SCR_RXEN = 1 << 0; + +/// @brief Line Configuration Register - c0/c1 0101 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | RSV | BREAK | IREN | PAEN | PARITY | STPL | name +/// ------------------------------------------------------------------------- +/// | W/R | W/R | W/R | W/R | W/R | W/R | W/R | W/R | type +/// ------------------------------------------------------------------------- +/// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_LCR = 0x05; +/// @brief Parity enable (0: no check, 1: check) +constexpr uint8_t LCR_PAEN = 1 << 3; +/// @brief Parity force 0 +constexpr uint8_t LCR_PAR_F0 = 0 << 1; +/// @brief Parity odd +constexpr uint8_t LCR_PAR_ODD = 1 << 1; +/// @brief Parity even +constexpr uint8_t LCR_PAR_EVEN = 2 << 1; +/// @brief Parity force 1 +constexpr uint8_t LCR_PAR_F1 = 3 << 1; +/// @brief Stop length (0: 1 bit, 1: 2 bits) +constexpr uint8_t LCR_STPL = 1 << 0; + +/// @brief FIFO Control Register - c0/c1 0110 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | TFTRIG | RFTRIG | TFEN | RFEN | TFRST | RFRST | name +/// ------------------------------------------------------------------------- +/// | W/R | W/R | W/R | W/R | W/R | W/R | W/R | W/R | type +/// ------------------------------------------------------------------------- +/// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_FCR = 0x06; +/// @brief Transmitter FIFO enable +constexpr uint8_t FCR_TFEN = 1 << 3; +/// @brief Receiver FIFO enable +constexpr uint8_t FCR_RFEN = 1 << 2; +/// @brief Transmitter FIFO reset +constexpr uint8_t FCR_TFRST = 1 << 1; +/// @brief Receiver FIFO reset +constexpr uint8_t FCR_RFRST = 1 << 0; + +/// @brief Serial Interrupt Enable Register (not used) - c0/c1 0111 +constexpr uint8_t WKREG_SIER = 0x07; + +/// @brief Serial Interrupt Flag Register (not used) - c0/c1 1000 +constexpr uint8_t WKREG_SIFR = 0x08; + +/// @brief Transmitter FIFO Count - c0/c1 1001 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | +/// ------------------------------------------------------------------------- +/// | NUMBER OF DATA IN TRANSMITTER FIFO | +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_TFCNT = 0x09; + +/// @brief Receiver FIFO count - c0/c1 1010 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | +/// ------------------------------------------------------------------------- +/// | NUMBER OF DATA IN RECEIVER FIFO | +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_RFCNT = 0x0A; + +/// @brief FIFO Status Register - c0/c1 1011 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | bit +/// ------------------------------------------------------------------------- +/// | RFOE | RFLB | RFFE | RFPE | RFDAT | TFDAT | TFFULL | TBUSY | name +/// ------------------------------------------------------------------------- +/// | R | W/R | W/R | W/R | W/R | W/R | W/R | W/R | type +/// ------------------------------------------------------------------------- +/// | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reset +/// ------------------------------------------------------------------------- +/// @endcode +/// @warning The received buffer can hold 256 bytes. However, as the RFCNT reg +/// is 8 bits, if we have 256 byte in the register this is reported as 0 ! Therefore +/// RFCNT=0 can indicate that there are 0 **or** 256 bytes in the buffer. If we +/// have RFDAT = 1 and RFCNT = 0 it should be interpreted as 256 bytes in the FIFO. +/// @note Note that in case of overflow the RFOE goes to one **but** as soon as you read +/// the FSR this bit is cleared. Therefore Overflow can be read only once. +/// @n The same problem applies to the transmit buffer but here we have to check the +/// TFFULL flag. So if TFFULL is set and TFCNT is 0 this should be interpreted as 256 +constexpr uint8_t WKREG_FSR = 0x0B; +/// @brief Receiver FIFO Overflow Error (0: no OE, 1: OE) +constexpr uint8_t FSR_RFOE = 1 << 7; +/// @brief Receiver FIFO Line Break (0: no LB, 1: LB) +constexpr uint8_t FSR_RFLB = 1 << 6; +/// @brief Receiver FIFO Frame Error (0: no FE, 1: FE) +constexpr uint8_t FSR_RFFE = 1 << 5; +/// @brief Receiver Parity Error (0: no PE, 1: PE) +constexpr uint8_t FSR_RFPE = 1 << 4; +/// @brief Receiver FIFO count (0: empty, 1: not empty) +constexpr uint8_t FSR_RFDAT = 1 << 3; +/// @brief Transmitter FIFO count (0: empty, 1: not empty) +constexpr uint8_t FSR_TFDAT = 1 << 2; +/// @brief Transmitter FIFO full (0: not full, 1: full) +constexpr uint8_t FSR_TFFULL = 1 << 1; +/// @brief Transmitter busy (0 nothing to transmit, 1: transmitter busy sending) +constexpr uint8_t FSR_TBUSY = 1 << 0; + +/// @brief Line Status Register (not used - using FIFO) +constexpr uint8_t WKREG_LSR = 0x0C; + +/// @brief FIFO Data Register (not used - does not seems to work) +constexpr uint8_t WKREG_FDAT = 0x0D; + +/// @} +/// @defgroup cr_p1 Channel registers for SPAGE=1 +/// @brief Definition of the register linked to a particular channel when SPAGE=1 +/// @details The channel registers are further splitted into two groups. +/// This second group is defined when the Global register WKREG_SPAGE is 1 +/// @{ + +/// @brief Baud rate configuration register: high byte - c0/c1 0100 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | +/// ------------------------------------------------------------------------- +/// | High byte of the baud rate | +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_BRH = 0x04; + +/// @brief Baud rate configuration register: low byte - c0/c1 0101 +/// @details @code +/// ------------------------------------------------------------------------- +/// | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | +/// ------------------------------------------------------------------------- +/// | Low byte of the baud rate | +/// ------------------------------------------------------------------------- +/// @endcode +constexpr uint8_t WKREG_BRL = 0x05; + +/// @brief Baud rate configuration register decimal part - c0/c1 0110 +constexpr uint8_t WKREG_BRD = 0x06; + +/// @brief Receive FIFO Interrupt trigger configuration (not used) - c0/c1 0111 +constexpr uint8_t WKREG_RFI = 0x07; + +/// @brief Transmit FIFO interrupt trigger configuration (not used) - c0/c1 1000 +constexpr uint8_t WKREG_TFI = 0x08; + +/// @} +/// @} +} // namespace weikai +} // namespace esphome diff --git a/esphome/components/weikai_i2c/__init__.py b/esphome/components/weikai_i2c/__init__.py new file mode 100644 index 0000000000..2c6a421a0a --- /dev/null +++ b/esphome/components/weikai_i2c/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@DrCoolZic"] diff --git a/esphome/components/weikai_i2c/weikai_i2c.cpp b/esphome/components/weikai_i2c/weikai_i2c.cpp new file mode 100644 index 0000000000..9d0c9446ec --- /dev/null +++ b/esphome/components/weikai_i2c/weikai_i2c.cpp @@ -0,0 +1,177 @@ +/// @file weikai_i2c.cpp +/// @brief WeiKai component family - classes implementation +/// @date Last Modified: 2024/04/06 14:43:31 +/// @details The classes declared in this file can be used by the Weikai family + +#include "weikai_i2c.h" + +namespace esphome { +namespace weikai_i2c { +static const char *const TAG = "weikai_i2c"; + +/// @brief Display a buffer in hexadecimal format (32 hex values / line). +void print_buffer(const uint8_t *data, size_t length) { + char hex_buffer[100]; + hex_buffer[(3 * 32) + 1] = 0; + for (size_t i = 0; i < length; i++) { + snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]); + if (i % 32 == 31) { + ESP_LOGVV(TAG, " %s", hex_buffer); + } + } + if (length % 32) { + // null terminate if incomplete line + hex_buffer[3 * (length % 32) + 2] = 0; + ESP_LOGVV(TAG, " %s", hex_buffer); + } +} + +static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER", + "SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"}; +static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL", + "TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"}; +using namespace weikai; +// method to print a register value as text: used in the log messages ... +const char *reg_to_str(int reg, bool page1) { + if (reg == WKREG_GPDAT) { + return "GPDAT"; + } else if (reg == WKREG_GPDIR) { + return "GPDIR"; + } else { + return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F]; + } +} +enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO + +/// @brief Computes the I²C bus's address used to access the component +/// @param base_address the base address of the component - set by the A1 A0 pins +/// @param channel (0-3) the UART channel +/// @param fifo (0-1) 0 = access to internal register, 1 = direct access to fifo +/// @return the i2c address to use +inline uint8_t i2c_address(uint8_t base_address, uint8_t channel, RegType fifo) { + // the address of the device is: + // +----+----+----+----+----+----+----+----+ + // | 0 | A1 | A0 | 1 | 0 | C1 | C0 | F | + // +----+----+----+----+----+----+----+----+ + // where: + // - A1,A0 is the address read from A1,A0 switch + // - C1,C0 is the channel number (in practice only 00 or 01) + // - F is: 0 when accessing register, one when accessing FIFO + uint8_t const addr = base_address | channel << 1 | fifo << 0; + return addr; +} + +/////////////////////////////////////////////////////////////////////////////// +// The WeikaiRegisterI2C methods +/////////////////////////////////////////////////////////////////////////////// +uint8_t WeikaiRegisterI2C::read_reg() const { + uint8_t value = 0x00; + WeikaiComponentI2C *comp_i2c = static_cast(this->comp_); + uint8_t address = i2c_address(comp_i2c->base_address_, this->channel_, REG); + comp_i2c->set_i2c_address(address); + auto error = comp_i2c->read_register(this->register_, &value, 1); + if (error == i2c::NO_ERROR) { + this->comp_->status_clear_warning(); + ESP_LOGVV(TAG, "WeikaiRegisterI2C::read_reg() @%02X reg=%s ch=%u I2C_code:%d, buf=%02X", address, + reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value); + } else { // error + this->comp_->status_set_warning(); + ESP_LOGE(TAG, "WeikaiRegisterI2C::read_reg() @%02X reg=%s ch=%u I2C_code:%d, buf=%02X", address, + reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value); + } + return value; +} + +void WeikaiRegisterI2C::read_fifo(uint8_t *data, size_t length) const { + WeikaiComponentI2C *comp_i2c = static_cast(this->comp_); + uint8_t address = i2c_address(comp_i2c->base_address_, this->channel_, FIFO); + comp_i2c->set_i2c_address(address); + auto error = comp_i2c->read(data, length); + if (error == i2c::NO_ERROR) { + this->comp_->status_clear_warning(); +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + ESP_LOGVV(TAG, "WeikaiRegisterI2C::read_fifo() @%02X ch=%d I2C_code:%d len=%d buffer", address, this->channel_, + (int) error, length); + print_buffer(data, length); +#endif + } else { // error + this->comp_->status_set_warning(); + ESP_LOGE(TAG, "WeikaiRegisterI2C::read_fifo() @%02X reg=N/A ch=%d I2C_code:%d len=%d buf=%02X...", address, + this->channel_, (int) error, length, data[0]); + } +} + +void WeikaiRegisterI2C::write_reg(uint8_t value) { + WeikaiComponentI2C *comp_i2c = static_cast(this->comp_); + uint8_t address = i2c_address(comp_i2c->base_address_, this->channel_, REG); // update the i2c bus + comp_i2c->set_i2c_address(address); + auto error = comp_i2c->write_register(this->register_, &value, 1); + if (error == i2c::NO_ERROR) { + this->comp_->status_clear_warning(); + ESP_LOGVV(TAG, "WK2168Reg::write_reg() @%02X reg=%s ch=%d I2C_code:%d buf=%02X", address, + reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value); + } else { // error + this->comp_->status_set_warning(); + ESP_LOGE(TAG, "WK2168Reg::write_reg() @%02X reg=%s ch=%d I2C_code:%d buf=%d", address, + reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value); + } +} + +void WeikaiRegisterI2C::write_fifo(uint8_t *data, size_t length) { + WeikaiComponentI2C *comp_i2c = static_cast(this->comp_); + uint8_t address = i2c_address(comp_i2c->base_address_, this->channel_, FIFO); // set fifo flag + comp_i2c->set_i2c_address(address); + auto error = comp_i2c->write(data, length); + if (error == i2c::NO_ERROR) { + this->comp_->status_clear_warning(); +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + ESP_LOGVV(TAG, "WK2168Reg::write_fifo() @%02X ch=%d I2C_code:%d len=%d buffer", address, this->channel_, + (int) error, length); + print_buffer(data, length); +#endif + } else { // error + this->comp_->status_set_warning(); + ESP_LOGE(TAG, "WK2168Reg::write_fifo() @%02X reg=N/A, ch=%d I2C_code:%d len=%d, buf=%02X...", address, + this->channel_, (int) error, length, data[0]); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// The WeikaiComponentI2C methods +/////////////////////////////////////////////////////////////////////////////// +void WeikaiComponentI2C::setup() { + // before any manipulation we store the address to base_address_ for future use + this->base_address_ = this->address_; + ESP_LOGCONFIG(TAG, "Setting up wk2168_i2c: %s with %d UARTs at @%02X ...", this->get_name(), this->children_.size(), + this->base_address_); + + // enable all channels + this->reg(WKREG_GENA, 0) = GENA_C1EN | GENA_C2EN | GENA_C3EN | GENA_C4EN; + // reset all channels + this->reg(WKREG_GRST, 0) = GRST_C1RST | GRST_C2RST | GRST_C3RST | GRST_C4RST; + // initialize the spage register to page 0 + this->reg(WKREG_SPAGE, 0) = 0; + this->page1_ = false; + + // we setup our children channels + for (auto *child : this->children_) { + child->setup_channel(); + } +} + +void WeikaiComponentI2C::dump_config() { + ESP_LOGCONFIG(TAG, "Initialization of %s with %d UARTs completed", this->get_name(), this->children_.size()); + ESP_LOGCONFIG(TAG, " Crystal: %" PRIu32, this->crystal_); + if (test_mode_) + ESP_LOGCONFIG(TAG, " Test mode: %d", test_mode_); + ESP_LOGCONFIG(TAG, " Transfer buffer size: %d", XFER_MAX_SIZE); + this->address_ = this->base_address_; // we restore the base_address before display (less confusing) + LOG_I2C_DEVICE(this); + + for (auto *child : this->children_) { + child->dump_channel(); + } +} + +} // namespace weikai_i2c +} // namespace esphome diff --git a/esphome/components/weikai_i2c/weikai_i2c.h b/esphome/components/weikai_i2c/weikai_i2c.h new file mode 100644 index 0000000000..0da9ed9cde --- /dev/null +++ b/esphome/components/weikai_i2c/weikai_i2c.h @@ -0,0 +1,61 @@ +/// @file weikai_i2c.h +/// @author DrCoolZic +/// @brief WeiKai component family - classes declaration +/// @date Last Modified: 2024/03/01 13:31:57 +/// @details The classes declared in this file can be used by the Weikai family +/// wk2132_i2c, wk2168_i2c, wk2204_i2c, wk2212_i2c + +#pragma once +#include +#include +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/weikai/weikai.h" + +namespace esphome { +namespace weikai_i2c { + +class WeikaiComponentI2C; + +// using namespace weikai; +//////////////////////////////////////////////////////////////////////////////////// +/// @brief WeikaiRegisterI2C objects acts as proxies to access remote register through an I2C Bus +//////////////////////////////////////////////////////////////////////////////////// +class WeikaiRegisterI2C : public weikai::WeikaiRegister { + public: + uint8_t read_reg() const override; + void write_reg(uint8_t value) override; + void read_fifo(uint8_t *data, size_t length) const override; + void write_fifo(uint8_t *data, size_t length) override; + + protected: + friend WeikaiComponentI2C; + WeikaiRegisterI2C(weikai::WeikaiComponent *const comp, uint8_t reg, uint8_t channel) + : weikai::WeikaiRegister(comp, reg, channel) {} +}; + +//////////////////////////////////////////////////////////////////////////////////// +/// @brief The WeikaiComponentI2C class stores the information to the WeiKai component +/// connected through an I2C bus. +//////////////////////////////////////////////////////////////////////////////////// +class WeikaiComponentI2C : public weikai::WeikaiComponent, public i2c::I2CDevice { + public: + weikai::WeikaiRegister ®(uint8_t reg, uint8_t channel) override { + reg_i2c_.register_ = reg; + reg_i2c_.channel_ = channel; + return reg_i2c_; + } + + // + // override Component methods + // + void setup() override; + void dump_config() override; + + uint8_t base_address_; ///< base address of I2C device + WeikaiRegisterI2C reg_i2c_{this, 0, 0}; ///< init to this component +}; + +} // namespace weikai_i2c +} // namespace esphome diff --git a/esphome/components/weikai_spi/__init__.py b/esphome/components/weikai_spi/__init__.py new file mode 100644 index 0000000000..2c6a421a0a --- /dev/null +++ b/esphome/components/weikai_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@DrCoolZic"] diff --git a/esphome/components/weikai_spi/weikai_spi.cpp b/esphome/components/weikai_spi/weikai_spi.cpp new file mode 100644 index 0000000000..22c63bbd2d --- /dev/null +++ b/esphome/components/weikai_spi/weikai_spi.cpp @@ -0,0 +1,189 @@ +/// @file weikai_spi.cpp +/// @brief WeiKai component family - classes implementation +/// @date Last Modified: 2024/04/06 14:46:09 +/// @details The classes declared in this file can be used by the Weikai family + +#include "weikai_spi.h" + +namespace esphome { +namespace weikai_spi { +using namespace weikai; +static const char *const TAG = "weikai_spi"; + +/// @brief convert an int to binary representation as C++ std::string +/// @param val integer to convert +/// @return a std::string +inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); } +/// Convert std::string to C string +#define I2S2CS(val) (i2s(val).c_str()) + +/// @brief measure the time elapsed between two calls +/// @param last_time time of the previous call +/// @return the elapsed time in microseconds +uint32_t elapsed_ms(uint32_t &last_time) { + uint32_t e = millis() - last_time; + last_time = millis(); + return e; +}; + +/// @brief Converts the parity enum value to a C string +/// @param parity enum +/// @return the string +const char *p2s(uart::UARTParityOptions parity) { + using namespace uart; + switch (parity) { + case UART_CONFIG_PARITY_NONE: + return "NONE"; + case UART_CONFIG_PARITY_EVEN: + return "EVEN"; + case UART_CONFIG_PARITY_ODD: + return "ODD"; + default: + return "UNKNOWN"; + } +} + +/// @brief Display a buffer in hexadecimal format (32 hex values / line). +void print_buffer(const uint8_t *data, size_t length) { + char hex_buffer[100]; + hex_buffer[(3 * 32) + 1] = 0; + for (size_t i = 0; i < length; i++) { + snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]); + if (i % 32 == 31) { + ESP_LOGVV(TAG, " %s", hex_buffer); + } + } + if (length % 32) { + // null terminate if incomplete line + hex_buffer[3 * (length % 32) + 2] = 0; + ESP_LOGVV(TAG, " %s", hex_buffer); + } +} + +static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER", + "SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"}; +static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL", + "TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"}; + +// method to print a register value as text: used in the log messages ... +const char *reg_to_str(int reg, bool page1) { + if (reg == WKREG_GPDAT) { + return "GPDAT"; + } else if (reg == WKREG_GPDIR) { + return "GPDIR"; + } else { + return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F]; + } +} + +enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO +enum CmdType { WRITE_CMD = 0, READ_CMD = 1 }; ///< Read or Write transfer + +/// @brief Computes the SPI command byte +/// @param transfer_type read or write command +/// @param reg (0-15) the address of the register +/// @param channel (0-3) the UART channel +/// @param fifo (0-1) 0 = access to internal register, 1 = direct access to fifo +/// @return the spi command byte +/// @details +/// +------+------+------+------+------+------+------+------+ +/// | FIFO | R/W | C1-C0 | A3-A0 | +/// +------+------+-------------+---------------------------+ +/// FIFO: 0 = register, 1 = FIFO +/// R/W: 0 = write, 1 = read +/// C1-C0: Channel (0-1) +/// A3-A0: Address (0-F) +inline static uint8_t cmd_byte(RegType fifo, CmdType transfer_type, uint8_t channel, uint8_t reg) { + return (fifo << 7 | transfer_type << 6 | channel << 4 | reg << 0); +} + +/////////////////////////////////////////////////////////////////////////////// +// The WeikaiRegisterSPI methods +/////////////////////////////////////////////////////////////////////////////// +uint8_t WeikaiRegisterSPI::read_reg() const { + auto *spi_comp = static_cast(this->comp_); + uint8_t cmd = cmd_byte(REG, READ_CMD, this->channel_, this->register_); + spi_comp->enable(); + spi_comp->write_byte(cmd); + uint8_t val = spi_comp->read_byte(); + spi_comp->disable(); + ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", I2S2CS(cmd), cmd, + reg_to_str(this->register_, this->comp_->page1()), this->channel_, val); + return val; +} + +void WeikaiRegisterSPI::read_fifo(uint8_t *data, size_t length) const { + auto *spi_comp = static_cast(this->comp_); + uint8_t cmd = cmd_byte(FIFO, READ_CMD, this->channel_, this->register_); + spi_comp->enable(); + spi_comp->write_byte(cmd); + spi_comp->read_array(data, length); + spi_comp->disable(); +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", I2S2CS(cmd), cmd, this->channel_, + length); + print_buffer(data, length); +#endif +} + +void WeikaiRegisterSPI::write_reg(uint8_t value) { + auto *spi_comp = static_cast(this->comp_); + uint8_t buf[2]{cmd_byte(REG, WRITE_CMD, this->channel_, this->register_), value}; + spi_comp->enable(); + spi_comp->write_array(buf, 2); + spi_comp->disable(); + ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", I2S2CS(buf[0]), buf[0], + reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]); +} + +void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) { + auto *spi_comp = static_cast(this->comp_); + uint8_t cmd = cmd_byte(FIFO, WRITE_CMD, this->channel_, this->register_); + spi_comp->enable(); + spi_comp->write_byte(cmd); + spi_comp->write_array(data, length); + spi_comp->disable(); + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", I2S2CS(cmd), cmd, this->channel_, + length); + print_buffer(data, length); +#endif +} + +/////////////////////////////////////////////////////////////////////////////// +// The WeikaiComponentSPI methods +/////////////////////////////////////////////////////////////////////////////// +void WeikaiComponentSPI::setup() { + using namespace weikai; + ESP_LOGCONFIG(TAG, "Setting up wk2168_spi: %s with %d UARTs...", this->get_name(), this->children_.size()); + this->spi_setup(); + // enable all channels + this->reg(WKREG_GENA, 0) = GENA_C1EN | GENA_C2EN | GENA_C3EN | GENA_C4EN; + // reset all channels + this->reg(WKREG_GRST, 0) = GRST_C1RST | GRST_C2RST | GRST_C3RST | GRST_C4RST; + // initialize the spage register to page 0 + this->reg(WKREG_SPAGE, 0) = 0; + this->page1_ = false; + + // we setup our children channels + for (auto *child : this->children_) { + child->setup_channel(); + } +} + +void WeikaiComponentSPI::dump_config() { + ESP_LOGCONFIG(TAG, "Initialization of %s with %d UARTs completed", this->get_name(), this->children_.size()); + ESP_LOGCONFIG(TAG, " Crystal: %" PRIu32 "", this->crystal_); + if (test_mode_) + ESP_LOGCONFIG(TAG, " Test mode: %d", test_mode_); + ESP_LOGCONFIG(TAG, " Transfer buffer size: %d", XFER_MAX_SIZE); + LOG_PIN(" CS Pin: ", this->cs_); + + for (auto *child : this->children_) { + child->dump_channel(); + } +} + +} // namespace weikai_spi +} // namespace esphome diff --git a/esphome/components/weikai_spi/weikai_spi.h b/esphome/components/weikai_spi/weikai_spi.h new file mode 100644 index 0000000000..dd0dc8d495 --- /dev/null +++ b/esphome/components/weikai_spi/weikai_spi.h @@ -0,0 +1,54 @@ +/// @file weikai.h +/// @author DrCoolZic +/// @brief WeiKai component family - classes declaration +/// @date Last Modified: 2024/02/29 17:20:32 +/// @details The classes declared in this file can be used by the Weikai family +/// wk2124_spi, wk2132_spi, wk2168_spi, wk2204_spi, wk2212_spi, + +#pragma once +#include +#include +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/weikai/weikai.h" + +namespace esphome { +namespace weikai_spi { +//////////////////////////////////////////////////////////////////////////////////// +/// @brief WeikaiRegisterSPI objects acts as proxies to access remote register through an SPI Bus +//////////////////////////////////////////////////////////////////////////////////// +class WeikaiRegisterSPI : public weikai::WeikaiRegister { + public: + WeikaiRegisterSPI(weikai::WeikaiComponent *const comp, uint8_t reg, uint8_t channel) + : weikai::WeikaiRegister(comp, reg, channel) {} + + uint8_t read_reg() const override; + void write_reg(uint8_t value) override; + void read_fifo(uint8_t *data, size_t length) const override; + void write_fifo(uint8_t *data, size_t length) override; +}; + +//////////////////////////////////////////////////////////////////////////////////// +/// @brief The WeikaiComponentSPI class stores the information to the WeiKai component +/// connected through an SPI bus. +//////////////////////////////////////////////////////////////////////////////////// +class WeikaiComponentSPI : public weikai::WeikaiComponent, + public spi::SPIDevice { + public: + weikai::WeikaiRegister ®(uint8_t reg, uint8_t channel) override { + reg_spi_.register_ = reg; + reg_spi_.channel_ = channel; + return reg_spi_; + } + + void setup() override; + void dump_config() override; + + protected: + WeikaiRegisterSPI reg_spi_{this, 0, 0}; ///< init to this component +}; + +} // namespace weikai_spi +} // namespace esphome diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 225423b4db..1ac32f30da 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -33,6 +33,7 @@ const uint8_t WHIRLPOOL_SWING_MASK = 128; const uint8_t WHIRLPOOL_POWER = 0x04; void WhirlpoolClimate::transmit_state() { + this->last_transmit_time_ = millis(); // setting the time of the last transmission. uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; remote_state[0] = 0x83; remote_state[1] = 0x06; @@ -149,6 +150,12 @@ void WhirlpoolClimate::transmit_state() { } bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { + // Check if the esp isn't currently transmitting. + if (millis() - this->last_transmit_time_ < 500) { + ESP_LOGV(TAG, "Blocked receive because of current trasmittion"); + return false; + } + // Validate header if (!data.expect_item(WHIRLPOOL_HEADER_MARK, WHIRLPOOL_HEADER_SPACE)) { ESP_LOGV(TAG, "Header fail"); diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 7f31894df9..907a21225c 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -47,6 +47,8 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { void transmit_state() override; /// Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; + /// Set the time of the last transmission. + int32_t last_transmit_time_{}; bool send_swing_cmd_{false}; Model model_; diff --git a/esphome/components/whynter/climate.py b/esphome/components/whynter/climate.py index b9dc5868bc..1d576344e6 100644 --- a/esphome/components/whynter/climate.py +++ b/esphome/components/whynter/climate.py @@ -1,14 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate_ir -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT AUTO_LOAD = ["climate_ir"] whynter_ns = cg.esphome_ns.namespace("whynter") Whynter = whynter_ns.class_("Whynter", climate_ir.ClimateIR) -CONF_USE_FAHRENHEIT = "use_fahrenheit" CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( { diff --git a/esphome/components/whynter/whynter.cpp b/esphome/components/whynter/whynter.cpp index 190bf70acc..9f57fdb843 100644 --- a/esphome/components/whynter/whynter.cpp +++ b/esphome/components/whynter/whynter.cpp @@ -118,7 +118,7 @@ bool Whynter::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, "Decoded 0x%02X", remote_state); + ESP_LOGD(TAG, "Decoded 0x%02" PRIX32, remote_state); if ((remote_state & COMMAND_MASK) != COMMAND_CODE) return false; if ((remote_state & POWER_MASK) != POWER_MASK) { @@ -156,7 +156,7 @@ bool Whynter::on_receive(remote_base::RemoteReceiveData data) { } void Whynter::transmit_(uint32_t value) { - ESP_LOGD(TAG, "Sending whynter code: 0x%02X", value); + ESP_LOGD(TAG, "Sending Whynter code: 0x%02" PRIX32, value); auto transmit = this->transmitter_->transmit(); auto *data = transmit.get_data(); diff --git a/esphome/components/whynter/whynter.h b/esphome/components/whynter/whynter.h index 939583ebfb..8273c21e4b 100644 --- a/esphome/components/whynter/whynter.h +++ b/esphome/components/whynter/whynter.h @@ -2,6 +2,8 @@ #include "esphome/components/climate_ir/climate_ir.h" +#include + namespace esphome { namespace whynter { diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c9da07795c..ea03cc16d1 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,45 +1,52 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -import esphome.final_validate as fv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.network import IPAddress +import esphome.config_validation as cv from esphome.const import ( CONF_AP, CONF_BSSID, + CONF_CERTIFICATE, + CONF_CERTIFICATE_AUTHORITY, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, + CONF_EAP, CONF_ENABLE_BTM, + CONF_ENABLE_ON_BOOT, CONF_ENABLE_RRM, CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, + CONF_IDENTITY, + CONF_KEY, CONF_MANUAL_IP, CONF_NETWORKS, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, CONF_PASSWORD, CONF_POWER_SAVE_MODE, + CONF_PRIORITY, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET, + CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, - CONF_PRIORITY, - CONF_IDENTITY, - CONF_CERTIFICATE_AUTHORITY, - CONF_CERTIFICATE, - CONF_KEY, CONF_USERNAME, - CONF_EAP, ) from esphome.core import CORE, HexInt, coroutine_with_priority -from esphome.components.esp32 import add_idf_sdkconfig_option -from esphome.components.network import IPAddress +import esphome.final_validate as fv + from . import wpa2_eap AUTO_LOAD = ["network"] +NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2] + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -53,6 +60,9 @@ WIFI_POWER_SAVE_MODES = { "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) +WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) +WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) +WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) def validate_password(value): @@ -90,6 +100,14 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend( } ) +TTLS_PHASE_2 = { + "pap": cg.global_ns.ESP_EAP_TTLS_PHASE2_PAP, + "chap": cg.global_ns.ESP_EAP_TTLS_PHASE2_CHAP, + "mschap": cg.global_ns.ESP_EAP_TTLS_PHASE2_MSCHAP, + "mschapv2": cg.global_ns.ESP_EAP_TTLS_PHASE2_MSCHAPV2, + "eap": cg.global_ns.ESP_EAP_TTLS_PHASE2_EAP, +} + EAP_AUTH_SCHEMA = cv.All( cv.Schema( { @@ -97,6 +115,9 @@ EAP_AUTH_SCHEMA = cv.All( cv.Optional(CONF_USERNAME): cv.string_strict, cv.Optional(CONF_PASSWORD): cv.string_strict, cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate, + cv.SplitDefault(CONF_TTLS_PHASE_2, esp32_idf="mschapv2"): cv.All( + cv.enum(TTLS_PHASE_2), cv.only_with_esp_idf + ), cv.Inclusive( CONF_CERTIFICATE, "certificate_and_key" ): wpa2_eap.validate_certificate, @@ -145,6 +166,13 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( ) +def validate_variant(_): + if CORE.is_esp32: + variant = get_esp32_variant() + if variant in NO_WIFI_VARIANTS: + raise cv.Invalid(f"{variant} does not support WiFi") + + def final_validate(config): has_sta = bool(config.get(CONF_NETWORKS, True)) has_ap = CONF_AP in config @@ -196,6 +224,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( extra=cv.ALLOW_EXTRA, ), final_validate, + validate_variant, ) @@ -268,7 +297,12 @@ CONFIG_SCHEMA = cv.All( CONF_REBOOT_TIMEOUT, default="15min" ): cv.positive_time_period_milliseconds, cv.SplitDefault( - CONF_POWER_SAVE_MODE, esp8266="none", esp32="light", rp2040="light" + CONF_POWER_SAVE_MODE, + esp8266="none", + esp32="light", + rp2040="light", + bk72xx="none", + rtl87xx="none", ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, @@ -286,6 +320,11 @@ CONFIG_SCHEMA = cv.All( "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_ON_CONNECT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation( + single=True + ), } ), _validate, @@ -312,6 +351,7 @@ def eap_auth(config): ("ca_cert", ca_cert), ("client_cert", client_cert), ("client_key", key), + ("ttls_phase_2", config.get(CONF_TTLS_PHASE_2)), ) @@ -377,6 +417,10 @@ async def to_code(config): lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_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: + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) + add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) @@ -385,6 +429,8 @@ async def to_code(config): if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) elif CORE.is_esp32 and CORE.using_arduino: @@ -403,10 +449,37 @@ async def to_code(config): cg.add_define("USE_WIFI") - # Register at end for OTA safe mode + # must register before OTA safe mode check await cg.register_component(var, config) + await cg.past_safe_mode() + + if on_connect_config := config.get(CONF_ON_CONNECT): + await automation.build_automation( + var.get_connect_trigger(), [], on_connect_config + ) + + if on_disconnect_config := config.get(CONF_ON_DISCONNECT): + await automation.build_automation( + var.get_disconnect_trigger(), [], on_disconnect_config + ) + @automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({})) async def wifi_connected_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) + + +@automation.register_condition("wifi.enabled", WiFiEnabledCondition, cv.Schema({})) +async def wifi_enabled_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) + + +@automation.register_action("wifi.disable", WiFiDisableAction, cv.Schema({})) +async def wifi_disable_to_code(config, action_id, template_arg, args): + return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index db19f9bcc0..8788711d5a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,5 +1,15 @@ #include "wifi_component.h" +#ifdef USE_WIFI #include +#include + +#ifdef USE_ESP_IDF +#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) +#include +#else +#include +#endif +#endif #if defined(USE_ESP32) || defined(USE_ESP_IDF) #include @@ -8,16 +18,16 @@ #include #endif -#include #include -#include "lwip/err.h" +#include #include "lwip/dns.h" +#include "lwip/err.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/hal.h" #include "esphome/core/util.h" -#include "esphome/core/application.h" #ifdef USE_CAPTIVE_PORTAL #include "esphome/components/captive_portal/captive_portal.h" @@ -36,13 +46,28 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up WiFi..."); - ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); - this->last_connected_ = millis(); this->wifi_pre_setup_(); + if (this->enable_on_boot_) { + this->start(); + } else { +#ifdef USE_ESP32 + esp_netif_init(); +#endif + this->state_ = WIFI_COMPONENT_STATE_DISABLED; + } +} + +void WiFiComponent::start() { + ESP_LOGCONFIG(TAG, "Starting WiFi..."); + ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + this->last_connected_ = millis(); uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; this->pref_ = global_preferences->make_preference(hash, true); + if (this->fast_connect_) { + this->fast_connect_pref_ = global_preferences->make_preference(hash, false); + } SavedWifiSettings save{}; if (this->pref_.load(&save)) { @@ -66,10 +91,12 @@ void WiFiComponent::setup() { if (this->fast_connect_) { this->selected_ap_ = this->sta_[0]; + this->load_fast_connect_settings_(); this->start_connecting(this->selected_ap_, false); } else { this->start_scanning(); } +#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_)) { @@ -82,9 +109,10 @@ void WiFiComponent::setup() { captive_portal::global_captive_portal->start(); } #endif +#endif // USE_WIFI_AP } #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr) { + if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); } @@ -97,11 +125,20 @@ void WiFiComponent::loop() { const uint32_t now = millis(); if (this->has_sta()) { + if (this->is_connected() != this->handled_connected_state_) { + if (this->handled_connected_state_) { + this->disconnect_trigger_->trigger(); + } else { + this->connect_trigger_->trigger(); + } + this->handled_connected_state_ = this->is_connected(); + } + switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { - this->status_set_warning(); + this->status_set_warning("waiting to reconnect"); if (millis() - this->action_started_ > 5000) { - if (this->fast_connect_) { + if (this->fast_connect_ || this->retry_hidden_) { this->start_connecting(this->sta_[0], false); } else { this->start_scanning(); @@ -110,13 +147,13 @@ void WiFiComponent::loop() { break; } case WIFI_COMPONENT_STATE_STA_SCANNING: { - this->status_set_warning(); + this->status_set_warning("scanning for networks"); this->check_scanning_finished(); break; } case WIFI_COMPONENT_STATE_STA_CONNECTING: case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { - this->status_set_warning(); + this->status_set_warning("associating to network"); this->check_connecting_finished(); break; } @@ -135,10 +172,13 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_OFF: case WIFI_COMPONENT_STATE_AP: break; + case WIFI_COMPONENT_STATE_DISABLED: + return; } +#ifdef USE_WIFI_AP if (this->has_ap() && !this->ap_setup_) { - if (now - this->last_connected_ > this->ap_timeout_) { + if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) { ESP_LOGI(TAG, "Starting fallback AP!"); this->setup_ap_config_(); #ifdef USE_CAPTIVE_PORTAL @@ -147,10 +187,11 @@ void WiFiComponent::loop() { #endif } } +#endif // USE_WIFI_AP #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr) { - if (!this->is_connected()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); } @@ -176,11 +217,15 @@ void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } #endif -network::IPAddress WiFiComponent::get_ip_address() { +network::IPAddresses WiFiComponent::get_ip_addresses() { if (this->has_sta()) - return this->wifi_sta_ip(); + return this->wifi_sta_ip_addresses(); + +#ifdef USE_WIFI_AP if (this->has_ap()) - return this->wifi_soft_ap_ip(); + return {this->wifi_soft_ap_ip()}; +#endif // USE_WIFI_AP + return {}; } network::IPAddress WiFiComponent::get_dns_address(int num) { @@ -195,6 +240,8 @@ std::string WiFiComponent::get_use_address() const { return this->use_address_; } void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } + +#ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { this->wifi_mode_({}, true); @@ -232,13 +279,16 @@ void WiFiComponent::setup_ap_config_() { } } -float WiFiComponent::get_loop_priority() const { - return 10.0f; // before other loop components -} void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; this->has_ap_ = true; } +#endif // USE_WIFI_AP + +float WiFiComponent::get_loop_priority() const { + return 10.0f; // before other loop components +} + void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); @@ -247,8 +297,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; - strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid)); - strncpy(save.password, password.c_str(), sizeof(save.password)); + snprintf(save.ssid, sizeof(save.ssid), "%s", ssid.c_str()); + snprintf(save.password, sizeof(save.password), "%s", password.c_str()); this->pref_.save(&save); // ensure it's written immediately global_preferences->sync(); @@ -278,6 +328,16 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { 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()); +#ifdef USE_ESP_IDF +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + std::map phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"}, + {ESP_EAP_TTLS_PHASE2_CHAP, "chap"}, + {ESP_EAP_TTLS_PHASE2_MSCHAP, "mschap"}, + {ESP_EAP_TTLS_PHASE2_MSCHAPV2, "mschapv2"}, + {ESP_EAP_TTLS_PHASE2_EAP, "eap"}}; + ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), phase2types[eap_config.ttls_phase_2].c_str()); +#endif +#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); @@ -366,8 +426,16 @@ void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + if (this->is_disabled()) { + ESP_LOGCONFIG(TAG, " WiFi is disabled!"); + return; + } ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); - ESP_LOGCONFIG(TAG, " IP Address: %s", wifi_sta_ip().str().c_str()); + for (auto &ip : wifi_sta_ip_addresses()) { + if (ip.is_set()) { + ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); + } + } ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); ESP_LOGCONFIG(TAG, " Hostname: '%s'", App.get_name().c_str()); @@ -387,6 +455,28 @@ void WiFiComponent::print_connect_params_() { #endif } +void WiFiComponent::enable() { + if (this->state_ != WIFI_COMPONENT_STATE_DISABLED) + return; + + ESP_LOGD(TAG, "Enabling WIFI..."); + this->error_from_callback_ = false; + this->state_ = WIFI_COMPONENT_STATE_OFF; + this->start(); +} + +void WiFiComponent::disable() { + if (this->state_ == WIFI_COMPONENT_STATE_DISABLED) + return; + + ESP_LOGD(TAG, "Disabling WIFI..."); + this->state_ = WIFI_COMPONENT_STATE_DISABLED; + this->wifi_disconnect_(); + this->wifi_mode_(false, false); +} + +bool WiFiComponent::is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; } + void WiFiComponent::start_scanning() { this->action_started_ = millis(); ESP_LOGD(TAG, "Starting scan..."); @@ -521,6 +611,9 @@ void WiFiComponent::check_connecting_finished() { return; } + // We won't retry hidden networks unless a reconnect fails more than three times again + this->retry_hidden_ = false; + ESP_LOGI(TAG, "WiFi Connected!"); this->print_connect_params_(); @@ -541,6 +634,11 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; + + if (this->fast_connect_) { + this->save_fast_connect_settings_(); + } + return; } @@ -586,12 +684,20 @@ void WiFiComponent::retry_connect() { delay(10); if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && - (this->num_retried_ > 5 || this->error_from_callback_)) { - // If retry failed for more than 5 times, let's restart STA - ESP_LOGW(TAG, "Restarting WiFi adapter..."); - this->wifi_mode_(false, {}); - delay(100); // NOLINT - this->num_retried_ = 0; + (this->num_retried_ > 3 || this->error_from_callback_)) { + if (this->num_retried_ > 5) { + // If retry failed for more than 5 times, let's restart STA + ESP_LOGW(TAG, "Restarting WiFi adapter..."); + this->wifi_mode_(false, {}); + delay(100); // NOLINT + this->num_retried_ = 0; + this->retry_hidden_ = false; + } else { + // Try hidden networks after 3 failed retries + ESP_LOGD(TAG, "Retrying with hidden networks..."); + this->retry_hidden_ = true; + this->num_retried_++; + } } else { this->num_retried_++; } @@ -608,7 +714,7 @@ void WiFiComponent::retry_connect() { } bool WiFiComponent::can_proceed() { - if (!this->has_sta()) { + if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { return true; } return this->is_connected(); @@ -642,6 +748,35 @@ bool WiFiComponent::is_esp32_improv_active_() { #endif } +void WiFiComponent::load_fast_connect_settings_() { + SavedWifiFastConnectSettings fast_connect_save{}; + + if (this->fast_connect_pref_.load(&fast_connect_save)) { + bssid_t bssid{}; + std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); + this->selected_ap_.set_bssid(bssid); + this->selected_ap_.set_channel(fast_connect_save.channel); + + ESP_LOGD(TAG, "Loaded saved fast_connect wifi settings"); + } +} + +void WiFiComponent::save_fast_connect_settings_() { + bssid_t bssid = wifi_bssid(); + uint8_t channel = wifi_channel_(); + + if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) { + SavedWifiFastConnectSettings fast_connect_save{}; + + memcpy(fast_connect_save.bssid, bssid.data(), 6); + fast_connect_save.channel = channel; + + this->fast_connect_pref_.save(&fast_connect_save); + + ESP_LOGD(TAG, "Saved fast_connect wifi settings"); + } +} + 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; } @@ -722,3 +857,4 @@ WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-con } // namespace wifi } // namespace esphome +#endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d42be43b2d..dde0d1d5a5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -1,18 +1,31 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/automation.h" -#include "esphome/core/helpers.h" +#ifdef USE_WIFI #include "esphome/components/network/ip_address.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include #include #ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include -#include #include +#include +#include +#endif + +#ifdef USE_LIBRETINY +#include +#endif + +#if defined(USE_ESP_IDF) && defined(USE_WIFI_WPA2_EAP) +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#include +#else +#include +#endif #endif #ifdef USE_ESP8266 @@ -44,9 +57,16 @@ struct SavedWifiSettings { char password[65]; } PACKED; // NOLINT +struct SavedWifiFastConnectSettings { + uint8_t bssid[6]; + uint8_t channel; +} PACKED; // NOLINT + enum WiFiComponentState { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, + /** WiFi is disabled. */ + WIFI_COMPONENT_STATE_DISABLED, /** WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of time. */ WIFI_COMPONENT_STATE_COOLDOWN, /** WiFi is in STA-only mode and currently scanning for APs. */ @@ -91,6 +111,10 @@ struct EAPAuth { // used for EAP-TLS const char *client_cert; const char *client_key; +// used for EAP-TTLS +#ifdef USE_ESP_IDF + esp_eap_ttls_phase2_types ttls_phase_2; +#endif }; #endif // USE_WIFI_WPA2_EAP @@ -188,6 +212,7 @@ class WiFiComponent : public Component { void add_sta(const WiFiAP &ap); void clear_sta(); +#ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. * * This can also be used without set_sta(). Then the AP will always be active. @@ -197,7 +222,11 @@ class WiFiComponent : public Component { */ void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } +#endif // USE_WIFI_AP + void enable(); + void disable(); + bool is_disabled(); void start_scanning(); void check_scanning_finished(); void start_connecting(const WiFiAP &ap, bool two); @@ -224,6 +253,7 @@ class WiFiComponent : public Component { // (In most use cases you won't need these) /// Setup WiFi interface. void setup() override; + void start(); void dump_config() override; /// WIFI setup_priority. float get_setup_priority() const override; @@ -241,7 +271,7 @@ class WiFiComponent : public Component { #endif network::IPAddress get_dns_address(int num); - network::IPAddress get_ip_address(); + network::IPAddresses get_ip_addresses(); std::string get_use_address() const; void set_use_address(const std::string &use_address); @@ -276,15 +306,24 @@ class WiFiComponent : public Component { }); } - network::IPAddress wifi_sta_ip(); + network::IPAddresses wifi_sta_ip_addresses(); std::string wifi_ssid(); bssid_t wifi_bssid(); int8_t wifi_rssi(); + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + + Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }; + Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; }; + protected: static std::string format_mac_addr(const uint8_t mac[6]); + +#ifdef USE_WIFI_AP void setup_ap_config_(); +#endif // USE_WIFI_AP + void print_connect_params_(); void wifi_loop_(); @@ -298,8 +337,12 @@ class WiFiComponent : public Component { void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_(); bool wifi_scan_start_(bool passive); + +#ifdef USE_WIFI_AP bool wifi_ap_ip_config_(optional manual_ip); bool wifi_start_ap_(const WiFiAP &ap); +#endif // USE_WIFI_AP + bool wifi_disconnect_(); int32_t wifi_channel_(); network::IPAddress wifi_subnet_mask_(); @@ -309,6 +352,9 @@ class WiFiComponent : public Component { bool is_captive_portal_active_(); bool is_esp32_improv_active_(); + void load_fast_connect_settings_(); + void save_fast_connect_settings_(); + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -316,11 +362,7 @@ class WiFiComponent : public Component { #endif #ifdef USE_ESP32_FRAMEWORK_ARDUINO -#if ESP_IDF_VERSION_MAJOR >= 4 void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); -#else - void wifi_event_callback_(system_event_id_t event, system_event_info_t info); -#endif void wifi_scan_done_callback_(); #endif #ifdef USE_ESP_IDF @@ -332,15 +374,22 @@ class WiFiComponent : public Component { void wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); #endif +#ifdef USE_LIBRETINY + void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); + void wifi_scan_done_callback_(); +#endif + std::string use_address_; std::vector sta_; std::vector sta_priorities_; WiFiAP selected_ap_; bool fast_connect_{false}; + bool retry_hidden_{false}; bool has_ap_{false}; WiFiAP ap_; WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; + bool handled_connected_state_{false}; uint32_t action_started_; uint8_t num_retried_{0}; uint32_t last_connected_{0}; @@ -354,23 +403,44 @@ class WiFiComponent : public Component { optional output_power_; bool passive_scan_{false}; ESPPreferenceObject pref_; + ESPPreferenceObject fast_connect_pref_; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT bool btm_{false}; bool rrm_{false}; #endif + bool enable_on_boot_; + bool got_ipv4_address_{false}; +#if USE_NETWORK_IPV6 + uint8_t num_ipv6_addresses_{0}; +#endif /* USE_NETWORK_IPV6 */ + + Trigger<> *connect_trigger_{new Trigger<>()}; + Trigger<> *disconnect_trigger_{new Trigger<>()}; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class WiFiConnectedCondition : public Condition { public: - bool check(Ts... x) override; + bool check(Ts... x) override { return global_wifi_component->is_connected(); } }; -template bool WiFiConnectedCondition::check(Ts... x) { - return global_wifi_component->is_connected(); -} +template class WiFiEnabledCondition : public Condition { + public: + bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } +}; + +template class WiFiEnableAction : public Action { + public: + void play(Ts... x) override { global_wifi_component->enable(); } +}; + +template class WiFiDisableAction : public Action { + public: + void play(Ts... x) override { global_wifi_component->disable(); } +}; } // namespace wifi } // namespace esphome +#endif diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index f35f5dfc43..88648093c6 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -1,7 +1,9 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_ESP32_FRAMEWORK_ARDUINO +#include #include #include @@ -24,45 +26,78 @@ namespace wifi { static const char *const TAG = "wifi_esp32"; +static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#ifdef USE_WIFI_AP +static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif // USE_WIFI_AP + static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +void WiFiComponent::wifi_pre_setup_() { + uint8_t mac[6]; + if (has_custom_mac_address()) { + get_mac_address_raw(mac); + set_mac_address(mac); + } + auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); + WiFi.onEvent(f); + WiFi.persistent(false); + // Make sure WiFi is in clean state before anything starts + this->wifi_mode_(false, false); +} + bool WiFiComponent::wifi_mode_(optional sta, optional ap) { - uint8_t current_mode = WiFiClass::getMode(); - bool current_sta = current_mode & 0b01; - bool current_ap = current_mode & 0b10; - bool enable_sta = sta.value_or(current_sta); - bool enable_ap = ap.value_or(current_ap); - if (current_sta == enable_sta && current_ap == enable_ap) + wifi_mode_t current_mode = WiFiClass::getMode(); + bool current_sta = current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA; + bool current_ap = current_mode == WIFI_MODE_AP || current_mode == WIFI_MODE_APSTA; + + bool set_sta = sta.value_or(current_sta); + bool set_ap = ap.value_or(current_ap); + + wifi_mode_t set_mode; + if (set_sta && set_ap) { + set_mode = WIFI_MODE_APSTA; + } else if (set_sta && !set_ap) { + set_mode = WIFI_MODE_STA; + } else if (!set_sta && set_ap) { + set_mode = WIFI_MODE_AP; + } else { + set_mode = WIFI_MODE_NULL; + } + + if (current_mode == set_mode) return true; - if (enable_sta && !current_sta) { + if (set_sta && !current_sta) { ESP_LOGV(TAG, "Enabling STA."); - } else if (!enable_sta && current_sta) { + } else if (!set_sta && current_sta) { ESP_LOGV(TAG, "Disabling STA."); } - if (enable_ap && !current_ap) { + if (set_ap && !current_ap) { ESP_LOGV(TAG, "Enabling AP."); - } else if (!enable_ap && current_ap) { + } else if (!set_ap && current_ap) { ESP_LOGV(TAG, "Disabling AP."); } - uint8_t mode = 0; - if (enable_sta) - mode |= 0b01; - if (enable_ap) - mode |= 0b10; - bool ret = WiFiClass::mode(static_cast(mode)); + bool ret = WiFiClass::mode(set_mode); if (!ret) { ESP_LOGW(TAG, "Setting WiFi mode failed!"); + return false; } + // WiFiClass::mode above calls esp_netif_create_default_wifi_sta() and + // esp_netif_create_default_wifi_ap(), which creates the interfaces. + // s_sta_netif handle is set during ESPHOME_EVENT_ID_WIFI_STA_START event + +#ifdef USE_WIFI_AP + if (set_ap) + s_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); +#endif + return ret; } -bool WiFiComponent::wifi_apply_output_power_(float output_power) { - int8_t val = static_cast(output_power * 4); - return esp_wifi_set_max_tx_power(val) == ESP_OK; -} + bool WiFiComponent::wifi_sta_pre_setup_() { if (!this->wifi_mode_(true, {})) return false; @@ -71,6 +106,12 @@ bool WiFiComponent::wifi_sta_pre_setup_() { delay(10); return true; } + +bool WiFiComponent::wifi_apply_output_power_(float output_power) { + int8_t val = static_cast(output_power * 4); + return esp_wifi_set_max_tx_power(val) == ESP_OK; +} + bool WiFiComponent::wifi_apply_power_save_() { wifi_ps_type_t power_save; switch (this->power_save_) { @@ -87,82 +128,7 @@ bool WiFiComponent::wifi_apply_power_save_() { } return esp_wifi_set_ps(power_save) == ESP_OK; } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { - // enable STA - if (!this->wifi_mode_(true, {})) - return false; - tcpip_adapter_dhcp_status_t dhcp_status; - tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status); - if (!manual_ip.has_value()) { - // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, - // the built-in SNTP client has a memory leak in certain situations. Disable this feature. - // https://github.com/esphome/issues/issues/2299 - sntp_servermode_dhcp(false); - - // Use DHCP client - if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) { - esp_err_t err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA); - if (err != ESP_OK) { - ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); - } - return err == ESP_OK; - } - return true; - } - - tcpip_adapter_ip_info_t info; - memset(&info, 0, sizeof(info)); - info.ip.addr = static_cast(manual_ip->static_ip); - info.gw.addr = static_cast(manual_ip->gateway); - info.netmask.addr = static_cast(manual_ip->subnet); - - esp_err_t dhcp_stop_ret = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA); - if (dhcp_stop_ret != ESP_OK && dhcp_stop_ret != ESP_ERR_TCPIP_ADAPTER_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(dhcp_stop_ret)); - } - - esp_err_t wifi_set_info_ret = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &info); - if (wifi_set_info_ret != ESP_OK) { - ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(wifi_set_info_ret)); - } - - ip_addr_t dns; -#if LWIP_IPV6 - dns.type = IPADDR_TYPE_V4; -#endif - if (uint32_t(manual_ip->dns1) != 0) { -#if LWIP_IPV6 - dns.u_addr.ip4.addr = static_cast(manual_ip->dns1); -#else - dns.addr = static_cast(manual_ip->dns1); -#endif - dns_setserver(0, &dns); - } - if (uint32_t(manual_ip->dns2) != 0) { -#if LWIP_IPV6 - dns.u_addr.ip4.addr = static_cast(manual_ip->dns2); -#else - dns.addr = static_cast(manual_ip->dns2); -#endif - dns_setserver(1, &dns); - } - - return true; -} - -network::IPAddress WiFiComponent::wifi_sta_ip() { - if (!this->has_sta()) - return {}; - tcpip_adapter_ip_info_t ip; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); - return {ip.ip.addr}; -} - -bool WiFiComponent::wifi_apply_hostname_() { - // setting is done in SYSTEM_EVENT_STA_START callback - return true; -} bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // enable STA if (!this->wifi_mode_(true, {})) @@ -171,8 +137,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); - strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); + snprintf(reinterpret_cast(conf.sta.ssid), sizeof(conf.sta.ssid), "%s", ap.get_ssid().c_str()); + snprintf(reinterpret_cast(conf.sta.password), sizeof(conf.sta.password), "%s", ap.get_password().c_str()); // The weakest authmode to accept in the fast scan mode if (ap.get_password().empty()) { @@ -203,12 +169,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // Units: AP beacon intervals. Defaults to 3 if set to 0. conf.sta.listen_interval = 0; -#if ESP_IDF_VERSION_MAJOR >= 4 // Protected Management Frame // Device will prefer to connect in PMF mode if other device also advertises PMF capability. conf.sta.pmf_cfg.capable = true; conf.sta.pmf_cfg.required = false; -#endif // note, we do our own filtering // The minimum rssi to accept in the fast scan mode @@ -218,19 +182,24 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { wifi_config_t current_conf; esp_err_t err; - esp_wifi_get_config(WIFI_IF_STA, ¤t_conf); + err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err)); + // can continue + } - if (memcmp(¤t_conf, &conf, sizeof(wifi_config_t)) != 0) { + if (memcmp(¤t_conf, &conf, sizeof(wifi_config_t)) != 0) { // NOLINT err = esp_wifi_disconnect(); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_disconnect failed! %d", err); + ESP_LOGV(TAG, "esp_wifi_disconnect failed: %s", esp_err_to_name(err)); return false; } } err = esp_wifi_set_config(WIFI_IF_STA, &conf); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); + ESP_LOGV(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err)); + return false; } if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { @@ -289,12 +258,98 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { err = esp_wifi_connect(); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_connect failed! %d", err); + ESP_LOGW(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(err)); return false; } return true; } + +bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + esp_netif_dhcp_status_t dhcp_status; + esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err)); + return false; + } + + if (!manual_ip.has_value()) { + // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, + // the built-in SNTP client has a memory leak in certain situations. Disable this feature. + // https://github.com/esphome/issues/issues/2299 + sntp_servermode_dhcp(false); + + // No manual IP is set; use DHCP client + if (dhcp_status != ESP_NETIF_DHCP_STARTED) { + err = esp_netif_dhcpc_start(s_sta_netif); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); + } + return err == ESP_OK; + } + return true; + } + + esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw + info.ip = manual_ip->static_ip; + info.gw = manual_ip->gateway; + info.netmask = manual_ip->subnet; + err = esp_netif_dhcpc_stop(s_sta_netif); + if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { + ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(err)); + } + + err = esp_netif_set_ip_info(s_sta_netif, &info); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(err)); + } + + esp_netif_dns_info_t dns; + if (manual_ip->dns1.is_set()) { + dns.ip = manual_ip->dns1; + esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns); + } + if (manual_ip->dns2.is_set()) { + dns.ip = manual_ip->dns2; + esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns); + } + + return true; +} + +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { + if (!this->has_sta()) + return {}; + network::IPAddresses addresses; + esp_netif_ip_info_t ip; + esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); + // TODO: do something smarter + // return false; + } else { + addresses[0] = network::IPAddress(&ip.ip); + } +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + addresses[i + 1] = network::IPAddress(&if_ip6s[i]); + } +#endif /* USE_NETWORK_IPV6 */ + return addresses; +} + +bool WiFiComponent::wifi_apply_hostname_() { + // setting is done in SYSTEM_EVENT_STA_START callback + return true; +} const char *get_auth_mode_str(uint8_t mode) { switch (mode) { case WIFI_AUTH_OPEN: @@ -309,16 +364,18 @@ const char *get_auth_mode_str(uint8_t mode) { return "WPA/WPA2 PSK"; case WIFI_AUTH_WPA2_ENTERPRISE: return "WPA2 Enterprise"; + case WIFI_AUTH_WPA3_PSK: + return "WPA3 PSK"; + case WIFI_AUTH_WPA2_WPA3_PSK: + return "WPA2/WPA3 PSK"; + case WIFI_AUTH_WAPI_PSK: + return "WAPI PSK"; default: return "UNKNOWN"; } } -#if ESP_IDF_VERSION_MAJOR >= 4 using esphome_ip4_addr_t = esp_ip4_addr_t; -#else -using esphome_ip4_addr_t = ip4_addr_t; -#endif std::string format_ip4_addr(const esphome_ip4_addr_t &ip) { char buf[20]; @@ -398,13 +455,15 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Handshake Failed"; case WIFI_REASON_CONNECTION_FAIL: return "Connection Failed"; + case WIFI_REASON_ROAMING: + return "Station Roaming"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; } } -#if ESP_IDF_VERSION_MAJOR >= 4 +void WiFiComponent::wifi_loop_() {} #define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY #define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE @@ -426,28 +485,6 @@ 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; -#else // ESP_IDF_VERSION_MAJOR >= 4 - -#define ESPHOME_EVENT_ID_WIFI_READY SYSTEM_EVENT_WIFI_READY -#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE SYSTEM_EVENT_SCAN_DONE -#define ESPHOME_EVENT_ID_WIFI_STA_START SYSTEM_EVENT_STA_START -#define ESPHOME_EVENT_ID_WIFI_STA_STOP SYSTEM_EVENT_STA_STOP -#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED SYSTEM_EVENT_STA_CONNECTED -#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED SYSTEM_EVENT_STA_DISCONNECTED -#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE SYSTEM_EVENT_STA_AUTHMODE_CHANGE -#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP SYSTEM_EVENT_STA_GOT_IP -#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP SYSTEM_EVENT_STA_LOST_IP -#define ESPHOME_EVENT_ID_WIFI_AP_START SYSTEM_EVENT_AP_START -#define ESPHOME_EVENT_ID_WIFI_AP_STOP SYSTEM_EVENT_AP_STOP -#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED SYSTEM_EVENT_AP_STACONNECTED -#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED SYSTEM_EVENT_AP_STADISCONNECTED -#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED SYSTEM_EVENT_AP_STAIPASSIGNED -#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED SYSTEM_EVENT_AP_PROBEREQRECVED -using esphome_wifi_event_id_t = system_event_id_t; -using esphome_wifi_event_info_t = system_event_info_t; - -#endif // !(ESP_IDF_VERSION_MAJOR >= 4) - void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { switch (event) { case ESPHOME_EVENT_ID_WIFI_READY: { @@ -455,11 +492,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { -#if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_scan_done; -#else - auto it = info.scan_done; -#endif ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); this->wifi_scan_done_callback_(); @@ -467,7 +500,12 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_START: { ESP_LOGV(TAG, "Event: WiFi STA start"); - tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); + // apply hostname + s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); + } break; } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { @@ -475,25 +513,20 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { -#if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_connected; -#else - auto it = info.connected; -#endif char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); +#if USE_NETWORK_IPV6 + this->set_timeout(100, [] { WiFi.enableIpV6(); }); +#endif /* USE_NETWORK_IPV6 */ break; } case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { -#if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_disconnected; -#else - auto it = info.disconnected; -#endif char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; @@ -519,11 +552,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { -#if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_authmode_change; -#else - auto it = info.auth_change; -#endif ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); // Mitigate CVE-2020-12638 @@ -544,11 +573,26 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str()); + this->got_ipv4_address_ = true; +#if USE_NETWORK_IPV6 + s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT; +#else s_sta_connecting = false; +#endif /* USE_NETWORK_IPV6 */ break; } +#if USE_NETWORK_IPV6 + case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { + auto it = info.got_ip6.ip6_info; + ESP_LOGV(TAG, "Got IPv6 address=" IPV6STR, IPV62STR(it.ip)); + this->num_ipv6_addresses_++; + s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT)); + break; + } +#endif /* USE_NETWORK_IPV6 */ case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { ESP_LOGV(TAG, "Event: Lost IP"); + this->got_ipv4_address_ = false; break; } case ESPHOME_EVENT_ID_WIFI_AP_START: { @@ -560,24 +604,14 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { -#if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_connected; auto &mac = it.bssid; -#else - auto it = info.sta_connected; - auto &mac = it.mac; -#endif ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { -#if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; -#else - auto it = info.sta_disconnected; - auto &mac = it.mac; -#endif ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); break; } @@ -586,11 +620,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { -#if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_ap_probereqrecved; -#else - auto it = info.ap_probereqrecved; -#endif ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } @@ -598,23 +628,20 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } } -void WiFiComponent::wifi_pre_setup_() { - auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); - WiFi.onEvent(f); - WiFi.persistent(false); - // Make sure WiFi is in clean state before anything starts - this->wifi_mode_(false, false); -} + WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { auto status = WiFiClass::status(); + if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) { + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; + } + if (status == WL_NO_SSID_AVAIL) { + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + } + if (s_sta_connecting) { + return WiFiSTAConnectStatus::CONNECTING; + } 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; } return WiFiSTAConnectStatus::IDLE; } @@ -654,6 +681,8 @@ void WiFiComponent::wifi_scan_done_callback_() { WiFi.scanDelete(); this->scan_done_ = true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { esp_err_t err; @@ -661,56 +690,55 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { if (!this->wifi_mode_({}, true)) return false; - tcpip_adapter_ip_info_t info; - memset(&info, 0, sizeof(info)); + esp_netif_ip_info_t info; if (manual_ip.has_value()) { - info.ip.addr = static_cast(manual_ip->static_ip); - info.gw.addr = static_cast(manual_ip->gateway); - info.netmask.addr = static_cast(manual_ip->subnet); + info.ip = manual_ip->static_ip; + info.gw = manual_ip->gateway; + info.netmask = manual_ip->subnet; } else { - info.ip.addr = static_cast(network::IPAddress(192, 168, 4, 1)); - info.gw.addr = static_cast(network::IPAddress(192, 168, 4, 1)); - info.netmask.addr = static_cast(network::IPAddress(255, 255, 255, 0)); + info.ip = network::IPAddress(192, 168, 4, 1); + info.gw = network::IPAddress(192, 168, 4, 1); + info.netmask = network::IPAddress(255, 255, 255, 0); } - tcpip_adapter_dhcp_status_t dhcp_status; - tcpip_adapter_dhcps_get_status(TCPIP_ADAPTER_IF_AP, &dhcp_status); - err = tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP); - if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_stop failed! %d", err); + + err = esp_netif_dhcps_stop(s_ap_netif); + if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { + ESP_LOGE(TAG, "esp_netif_dhcps_stop failed: %s", esp_err_to_name(err)); return false; } - err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info); + err = esp_netif_set_ip_info(s_ap_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed! %d", err); + ESP_LOGE(TAG, "esp_netif_set_ip_info failed! %d", err); return false; } dhcps_lease_t lease; lease.enable = true; - network::IPAddress start_address = info.ip.addr; - start_address[3] += 99; - lease.start_ip.addr = static_cast(start_address); + 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()); - start_address[3] += 100; - lease.end_ip.addr = static_cast(start_address); + start_address += 10; + lease.end_ip = start_address; ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); - err = tcpip_adapter_dhcps_option(TCPIP_ADAPTER_OP_SET, TCPIP_ADAPTER_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); + err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_option failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_option failed! %d", err); return false; } - err = tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP); + err = esp_netif_dhcps_start(s_ap_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_start failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_start failed! %d", err); return false; } return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -718,7 +746,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); + snprintf(reinterpret_cast(conf.ap.ssid), sizeof(conf.ap.ssid), "%s", ap.get_ssid().c_str()); conf.ap.channel = ap.get_channel().value_or(1); conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; @@ -729,13 +757,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid)); + snprintf(reinterpret_cast(conf.ap.password), sizeof(conf.ap.password), "%s", ap.get_password().c_str()); } -#if ESP_IDF_VERSION_MAJOR >= 4 // pairwise cipher of SoftAP, group cipher will be derived using this. conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP; -#endif esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); if (err != ESP_OK) { @@ -752,11 +778,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { - tcpip_adapter_ip_info_t ip; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); - return {ip.ip.addr}; + esp_netif_ip_info_t ip; + esp_netif_get_ip_info(s_ap_netif, &ip); + return network::IPAddress(&ip.ip); } +#endif // USE_WIFI_AP + bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } bssid_t WiFiComponent::wifi_bssid() { @@ -771,12 +800,12 @@ bssid_t WiFiComponent::wifi_bssid() { std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } int32_t WiFiComponent::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_() {} +network::IPAddress WiFiComponent::wifi_subnet_mask_() { return network::IPAddress(WiFi.subnetMask()); } +network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(WiFi.gatewayIP()); } +network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(WiFi.dnsIP(num)); } } // namespace wifi } // namespace esphome #endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a28aa8b858..4568895950 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -1,6 +1,7 @@ #include "wifi_component.h" #include "esphome/core/defines.h" +#ifdef USE_WIFI #ifdef USE_ESP8266 #include @@ -17,15 +18,18 @@ extern "C" { #include "lwip/dhcp.h" #include "lwip/init.h" // LWIP_VERSION_ #include "lwip/apps/sntp.h" -#if LWIP_IPV6 #include "lwip/netif.h" // struct netif -#endif +#include #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) #include "LwipDhcpServer.h" +#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) +#include +#include "ESP8266WiFiAP.h" #define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease) #define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time) #define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode) #endif +#endif } #include "esphome/core/helpers.h" @@ -97,6 +101,7 @@ bool WiFiComponent::wifi_apply_power_save_() { power_save = NONE_SLEEP_T; break; } + wifi_fpm_auto_sleep_set_in_null_mode(1); return wifi_set_sleep_type(power_save); } @@ -145,9 +150,9 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { #endif struct ip_info info {}; - info.ip.addr = static_cast(manual_ip->static_ip); - info.gw.addr = static_cast(manual_ip->gateway); - info.netmask.addr = static_cast(manual_ip->subnet); + info.ip = manual_ip->static_ip; + info.gw = manual_ip->gateway; + info.netmask = manual_ip->subnet; if (dhcp_status == DHCP_STARTED) { bool dhcp_stop_ret = wifi_station_dhcpc_stop(); @@ -163,12 +168,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { } ip_addr_t dns; - if (uint32_t(manual_ip->dns1) != 0) { - dns.addr = static_cast(manual_ip->dns1); + if (manual_ip->dns1.is_set()) { + dns = manual_ip->dns1; dns_setserver(0, &dns); } - if (uint32_t(manual_ip->dns2) != 0) { - dns.addr = static_cast(manual_ip->dns2); + if (manual_ip->dns2.is_set()) { + dns = manual_ip->dns2; dns_setserver(1, &dns); } @@ -183,12 +188,15 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return ret; } -network::IPAddress WiFiComponent::wifi_sta_ip() { +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (!this->has_sta()) return {}; - struct ip_info ip {}; - wifi_get_ip_info(STATION_IF, &ip); - return {ip.ip.addr}; + network::IPAddresses addresses; + uint8_t index = 0; + for (auto &addr : addrList) { + addresses[index++] = addr.ipFromNetifNum(); + } + return addresses; } bool WiFiComponent::wifi_apply_hostname_() { const std::string &hostname = App.get_name(); @@ -228,8 +236,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { struct station_config conf {}; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid)); - strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password)); + snprintf(reinterpret_cast(conf.ssid), sizeof(conf.ssid), "%s", ap.get_ssid().c_str()); + snprintf(reinterpret_cast(conf.password), sizeof(conf.password), "%s", ap.get_password().c_str()); if (ap.get_bssid().has_value()) { conf.bssid_set = 1; @@ -325,6 +333,21 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#if USE_NETWORK_IPV6 + bool connected = false; + while (!connected) { + uint8_t ipv6_addr_count = 0; + for (auto addr : addrList) { + ESP_LOGV(TAG, "Address %s", addr.toString().c_str()); + if (addr.isV6()) { + ipv6_addr_count++; + } + } + delay(500); // NOLINT + connected = (ipv6_addr_count >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); + } +#endif /* USE_NETWORK_IPV6 */ + if (ap.get_channel().has_value()) { ret = wifi_set_channel(*ap.get_channel()); if (!ret) { @@ -674,6 +697,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } this->scan_done_ = true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) @@ -681,71 +706,76 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { struct ip_info info {}; if (manual_ip.has_value()) { - info.ip.addr = static_cast(manual_ip->static_ip); - info.gw.addr = static_cast(manual_ip->gateway); - info.netmask.addr = static_cast(manual_ip->subnet); + info.ip = manual_ip->static_ip; + info.gw = manual_ip->gateway; + info.netmask = manual_ip->subnet; } else { - info.ip.addr = static_cast(network::IPAddress(192, 168, 4, 1)); - info.gw.addr = static_cast(network::IPAddress(192, 168, 4, 1)); - info.netmask.addr = static_cast(network::IPAddress(255, 255, 255, 0)); + info.ip = network::IPAddress(192, 168, 4, 1); + info.gw = network::IPAddress(192, 168, 4, 1); + info.netmask = network::IPAddress(255, 255, 255, 0); } if (wifi_softap_dhcps_status() == DHCP_STARTED) { if (!wifi_softap_dhcps_stop()) { - ESP_LOGV(TAG, "Stopping DHCP server failed!"); + ESP_LOGW(TAG, "Stopping DHCP server failed!"); } } if (!wifi_set_ip_info(SOFTAP_IF, &info)) { - ESP_LOGV(TAG, "Setting SoftAP info failed!"); + ESP_LOGE(TAG, "Setting SoftAP info failed!"); return false; } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) +#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) dhcpSoftAP.begin(&info); #endif struct dhcps_lease lease {}; lease.enable = true; - network::IPAddress start_address = info.ip.addr; - start_address[3] += 99; - lease.start_ip.addr = static_cast(start_address); + 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()); - start_address[3] += 100; - lease.end_ip.addr = static_cast(start_address); + start_address += 10; + lease.end_ip = start_address; ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); if (!wifi_softap_set_dhcps_lease(&lease)) { - ESP_LOGV(TAG, "Setting SoftAP DHCP lease failed!"); + ESP_LOGE(TAG, "Setting SoftAP DHCP lease failed!"); return false; } // lease time 1440 minutes (=24 hours) if (!wifi_softap_set_dhcps_lease_time(1440)) { - ESP_LOGV(TAG, "Setting SoftAP DHCP lease time failed!"); + ESP_LOGE(TAG, "Setting SoftAP DHCP lease time failed!"); return false; } +#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0) + ESP8266WiFiClass::softAPDhcpServer().setRouter(true); // send ROUTER option with netif's gateway IP +#else uint8_t mode = 1; // bit0, 1 enables router information from ESP8266 SoftAP DHCP server. if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) { - ESP_LOGV(TAG, "wifi_softap_set_dhcps_offer_option failed!"); + ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed!"); return false; } +#endif if (!wifi_softap_dhcps_start()) { - ESP_LOGV(TAG, "Starting SoftAP DHCPS failed!"); + ESP_LOGE(TAG, "Starting SoftAP DHCPS failed!"); return false; } return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) return false; struct softap_config conf {}; - strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid)); + snprintf(reinterpret_cast(conf.ssid), sizeof(conf.ssid), "%s", ap.get_ssid().c_str()); conf.ssid_len = static_cast(ap.get_ssid().size()); conf.channel = ap.get_channel().value_or(1); conf.ssid_hidden = ap.get_hidden(); @@ -757,7 +787,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.password = 0; } else { conf.authmode = AUTH_WPA2_PSK; - strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password)); + snprintf(reinterpret_cast(conf.password), sizeof(conf.password), "%s", ap.get_password().c_str()); } ETS_UART_INTR_DISABLE(); @@ -776,11 +806,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { struct ip_info ip {}; wifi_get_ip_info(SOFTAP_IF, &ip); - return {ip.ip.addr}; + return network::IPAddress(&ip.ip); } +#endif // USE_WIFI_AP + bssid_t WiFiComponent::wifi_bssid() { bssid_t bssid{}; uint8_t *raw_bssid = WiFi.BSSID(); @@ -793,12 +826,13 @@ bssid_t WiFiComponent::wifi_bssid() { std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } int32_t WiFiComponent::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)}; } +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 #endif +#endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e18d3cc043..13870136d4 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1,30 +1,40 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_ESP_IDF -#include -#include -#include +#include +#include #include #include #include -#include -#include +#include +#include +#include +#include #include #include -#include #ifdef USE_WIFI_WPA2_EAP +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#include +#else #include #endif -#include "dhcpserver/dhcpserver.h" -#include "lwip/err.h" -#include "lwip/dns.h" +#endif +#ifdef USE_WIFI_AP +#include "dhcpserver/dhcpserver.h" +#endif // USE_WIFI_AP + +#include "lwip/apps/sntp.h" +#include "lwip/dns.h" +#include "lwip/err.h" + +#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/hal.h" -#include "esphome/core/application.h" #include "esphome/core/util.h" namespace esphome { @@ -35,15 +45,16 @@ static const char *const TAG = "wifi_esp32"; static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -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_sta_got_ip = 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) -static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#ifdef USE_WIFI_AP +static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#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) +static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) struct IDFWiFiEvent { esp_event_base_t event_base; @@ -58,7 +69,9 @@ struct IDFWiFiEvent { wifi_event_ap_probe_req_rx_t ap_probe_req_rx; wifi_event_bss_rssi_low_t bss_rssi_low; ip_event_got_ip_t ip_got_ip; +#if USE_NETWORK_IPV6 ip_event_got_ip6_t ip_got_ip6; +#endif /* USE_NETWORK_IPV6 */ ip_event_ap_staipassigned_t ip_ap_staipassigned; } data; }; @@ -82,8 +95,10 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi memcpy(&event.data.sta_disconnected, event_data, sizeof(wifi_event_sta_disconnected_t)); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { memcpy(&event.data.ip_got_ip, event_data, sizeof(ip_event_got_ip_t)); +#if USE_NETWORK_IPV6 } else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) { memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t)); +#endif } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone) // no data } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { @@ -115,12 +130,11 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi } void WiFiComponent::wifi_pre_setup_() { -#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC uint8_t mac[6]; - get_mac_address_raw(mac); - set_mac_address(mac); - ESP_LOGV(TAG, "Use EFuse MAC without checking CRC: %s", get_mac_address_pretty().c_str()); -#endif + if (has_custom_mac_address()) { + get_mac_address_raw(mac); + set_mac_address(mac); + } esp_err_t err = esp_netif_init(); if (err != ERR_OK) { ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err)); @@ -155,7 +169,11 @@ void WiFiComponent::wifi_pre_setup_() { } s_sta_netif = esp_netif_create_default_wifi_sta(); + +#ifdef USE_WIFI_AP s_ap_netif = esp_netif_create_default_wifi_ap(); +#endif // USE_WIFI_AP + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); // cfg.nvs_enable = false; err = esp_wifi_init(&cfg); @@ -183,8 +201,8 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { bool current_sta = current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA; bool current_ap = current_mode == WIFI_MODE_AP || current_mode == WIFI_MODE_APSTA; - bool set_sta = sta.has_value() ? *sta : current_sta; - bool set_ap = ap.has_value() ? *ap : current_ap; + bool set_sta = sta.value_or(current_sta); + bool set_ap = ap.value_or(current_ap); wifi_mode_t set_mode; if (set_sta && set_ap) { @@ -271,8 +289,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); - strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); + snprintf(reinterpret_cast(conf.sta.ssid), sizeof(conf.sta.ssid), "%s", ap.get_ssid().c_str()); + snprintf(reinterpret_cast(conf.sta.password), sizeof(conf.sta.password), "%s", ap.get_password().c_str()); // The weakest authmode to accept in the fast scan mode if (ap.get_password().empty()) { @@ -308,12 +326,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // Units: AP beacon intervals. Defaults to 3 if set to 0. conf.sta.listen_interval = 0; -#if ESP_IDF_VERSION_MAJOR >= 4 // Protected Management Frame // Device will prefer to connect in PMF mode if other device also advertises PMF capability. conf.sta.pmf_cfg.capable = true; conf.sta.pmf_cfg.required = false; -#endif // note, we do our own filtering // The minimum rssi to accept in the fast scan mode @@ -329,7 +345,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // can continue } - if (memcmp(¤t_conf, &conf, sizeof(wifi_config_t)) != 0) { + if (memcmp(¤t_conf, &conf, sizeof(wifi_config_t)) != 0) { // NOLINT err = esp_wifi_disconnect(); if (err != ESP_OK) { ESP_LOGV(TAG, "esp_wifi_disconnect failed: %s", esp_err_to_name(err)); @@ -352,43 +368,78 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_eap().has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. EAPAuth eap = ap.get_eap().value(); +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) + err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); +#else err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); +#endif if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err); + ESP_LOGV(TAG, "set_identity failed %d", err); } int ca_cert_len = strlen(eap.ca_cert); int client_cert_len = strlen(eap.client_cert); int client_key_len = strlen(eap.client_key); if (ca_cert_len) { +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) + err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); +#else err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); +#endif if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err); + ESP_LOGV(TAG, "set_ca_cert failed %d", err); } } // workout what type of EAP this is // validation is not required as the config tool has already validated it if (client_cert_len && client_key_len) { // if we have certs, this must be EAP-TLS +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) + err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1, + (uint8_t *) eap.client_key, client_key_len + 1, + (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); +#else err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, (uint8_t *) eap.client_key, client_key_len + 1, (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); +#endif if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err); + ESP_LOGV(TAG, "set_cert_key failed %d", err); } } else { // in the absence of certs, assume this is username/password based +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) + err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); +#else err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); +#endif if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err); + ESP_LOGV(TAG, "set_username failed %d", err); } +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) + err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); +#else err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); +#endif if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); + ESP_LOGV(TAG, "set_password failed %d", err); + } + // set TTLS Phase 2, defaults to MSCHAPV2 +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) + err = esp_eap_client_set_ttls_phase2_method(eap.ttls_phase_2); +#else + err = esp_wifi_sta_wpa2_ent_set_ttls_phase2_method(eap.ttls_phase_2); +#endif + if (err != ESP_OK) { + ESP_LOGV(TAG, "set_ttls_phase2_method failed %d", err); } } +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) + err = esp_wifi_sta_enterprise_enable(); +#else err = esp_wifi_sta_wpa2_ent_enable(); +#endif if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); + ESP_LOGV(TAG, "enterprise_enable failed %d", err); } } #endif // USE_WIFI_WPA2_EAP @@ -397,7 +448,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // may be called from wifi_station_connect s_sta_connecting = true; s_sta_connected = false; - s_sta_got_ip = false; s_sta_connect_error = false; s_sta_connect_not_found = false; @@ -423,6 +473,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { } if (!manual_ip.has_value()) { + // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, + // the built-in SNTP client has a memory leak in certain situations. Disable this feature. + // https://github.com/esphome/issues/issues/2299 + sntp_servermode_dhcp(false); + // No manual IP is set; use DHCP client if (dhcp_status != ESP_NETIF_DHCP_STARTED) { err = esp_netif_dhcpc_start(s_sta_netif); @@ -435,43 +490,55 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { } esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw - info.ip.addr = static_cast(manual_ip->static_ip); - info.gw.addr = static_cast(manual_ip->gateway); - info.netmask.addr = static_cast(manual_ip->subnet); + info.ip = manual_ip->static_ip; + info.gw = manual_ip->gateway; + info.netmask = manual_ip->subnet; err = esp_netif_dhcpc_stop(s_sta_netif); if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "esp_netif_dhcpc_stop failed: %s", esp_err_to_name(err)); - return false; + ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(err)); } + err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_netif_set_ip_info failed: %s", esp_err_to_name(err)); - return false; + ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(err)); } esp_netif_dns_info_t dns; - if (uint32_t(manual_ip->dns1) != 0) { - dns.ip.u_addr.ip4.addr = static_cast(manual_ip->dns1); + if (manual_ip->dns1.is_set()) { + dns.ip = manual_ip->dns1; esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns); } - if (uint32_t(manual_ip->dns2) != 0) { - dns.ip.u_addr.ip4.addr = static_cast(manual_ip->dns2); + if (manual_ip->dns2.is_set()) { + dns.ip = manual_ip->dns2; esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns); } return true; } -network::IPAddress WiFiComponent::wifi_sta_ip() { +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (!this->has_sta()) return {}; + network::IPAddresses addresses; esp_netif_ip_info_t ip; esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip); if (err != ESP_OK) { ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); - return false; + // TODO: do something smarter + // return false; + } else { + addresses[0] = network::IPAddress(&ip.ip); } - return {ip.ip.addr}; +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + addresses[i + 1] = network::IPAddress(&if_ip6s[i]); + } +#endif /* USE_NETWORK_IPV6 */ + return addresses; } bool WiFiComponent::wifi_apply_hostname_() { @@ -504,7 +571,9 @@ 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: @@ -563,6 +632,8 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Handshake Failed"; case WIFI_REASON_CONNECTION_FAIL: return "Connection Failed"; + case WIFI_REASON_ROAMING: + return "Station Roaming"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; @@ -625,7 +696,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { if (it.reason == WIFI_REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; - + } else if (it.reason == WIFI_REASON_ROAMING) { + ESP_LOGI(TAG, "Event: Disconnected ssid='%s' reason='Station Roaming'", buf); + return; } else { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); @@ -637,20 +710,23 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; -#if LWIP_IPV6_AUTOCONFIG +#if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); -#endif +#endif /* USE_NETWORK_IPV6 */ ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), format_ip4_addr(it.ip_info.gw).c_str()); - s_sta_got_ip = true; + this->got_ipv4_address_ = true; +#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, "Event: Got IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); + this->num_ipv6_addresses_++; +#endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { ESP_LOGV(TAG, "Event: Lost IP"); - s_sta_got_ip = false; + this->got_ipv4_address_ = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_SCAN_DONE) { const auto &it = data->data.sta_scan_done; @@ -663,6 +739,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } + if (it.number == 0) { + // no results + return; + } + uint16_t number = it.number; std::vector records(number); err = esp_wifi_scan_get_ap_records(&number, records.data()); @@ -709,8 +790,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - if (s_sta_connected && s_sta_got_ip) { + if (s_sta_connected && this->got_ipv4_address_) { +#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) + if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) { + return WiFiSTAConnectStatus::CONNECTED; + } +#else return WiFiSTAConnectStatus::CONNECTED; +#endif /* USE_NETWORK_IPV6 */ } if (s_sta_connect_error) { return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; @@ -747,9 +834,11 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { return false; } - scan_done_ = false; + this->scan_done_ = false; return true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { esp_err_t err; @@ -759,52 +848,53 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { esp_netif_ip_info_t info; if (manual_ip.has_value()) { - info.ip.addr = static_cast(manual_ip->static_ip); - info.gw.addr = static_cast(manual_ip->gateway); - info.netmask.addr = static_cast(manual_ip->subnet); + info.ip = manual_ip->static_ip; + info.gw = manual_ip->gateway; + info.netmask = manual_ip->subnet; } else { - info.ip.addr = static_cast(network::IPAddress(192, 168, 4, 1)); - info.gw.addr = static_cast(network::IPAddress(192, 168, 4, 1)); - info.netmask.addr = static_cast(network::IPAddress(255, 255, 255, 0)); + info.ip = network::IPAddress(192, 168, 4, 1); + info.gw = network::IPAddress(192, 168, 4, 1); + info.netmask = network::IPAddress(255, 255, 255, 0); } - err = esp_netif_dhcpc_stop(s_sta_netif); + err = esp_netif_dhcps_stop(s_ap_netif); if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "esp_netif_dhcpc_stop failed: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "esp_netif_dhcps_stop failed: %s", esp_err_to_name(err)); return false; } - err = esp_netif_set_ip_info(s_sta_netif, &info); + err = esp_netif_set_ip_info(s_ap_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_netif_set_ip_info failed! %d", err); + ESP_LOGE(TAG, "esp_netif_set_ip_info failed! %d", err); return false; } dhcps_lease_t lease; lease.enable = true; - network::IPAddress start_address = info.ip.addr; - start_address[3] += 99; - lease.start_ip.addr = static_cast(start_address); + 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()); - start_address[3] += 100; - lease.end_ip.addr = static_cast(start_address); + start_address += 10; + lease.end_ip = start_address; ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); - err = esp_netif_dhcps_option(s_sta_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); + err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_netif_dhcps_option failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_option failed! %d", err); return false; } - err = esp_netif_dhcps_start(s_sta_netif); + err = esp_netif_dhcps_start(s_ap_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_netif_dhcps_start failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_start failed! %d", err); return false; } return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -826,41 +916,42 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password)); } -#if ESP_IDF_VERSION_MAJOR >= 4 // pairwise cipher of SoftAP, group cipher will be derived using this. conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP; -#endif esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); + ESP_LOGE(TAG, "esp_wifi_set_config failed! %d", err); return false; } if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGE(TAG, "wifi_ap_ip_config_ failed!"); return false; } return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { esp_netif_ip_info_t ip; - esp_netif_get_ip_info(s_sta_netif, &ip); - return {ip.ip.addr}; + esp_netif_get_ip_info(s_ap_netif, &ip); + return network::IPAddress(&ip.ip); } +#endif // USE_WIFI_AP + bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } bssid_t WiFiComponent::wifi_bssid() { + bssid_t bssid{}; wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); - bssid_t res{}; if (err != ESP_OK) { ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); - return res; + return bssid; } - std::copy(info.bssid, info.bssid + 6, res.begin()); - return res; + std::copy(info.bssid, info.bssid + 6, bssid.begin()); + return bssid; } std::string WiFiComponent::wifi_ssid() { wifi_ap_record_t info{}; @@ -899,7 +990,7 @@ network::IPAddress WiFiComponent::wifi_subnet_mask_() { ESP_LOGW(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); return {}; } - return {ip.netmask.addr}; + return network::IPAddress(&ip.netmask); } network::IPAddress WiFiComponent::wifi_gateway_ip_() { esp_netif_ip_info_t ip; @@ -908,18 +999,15 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { ESP_LOGW(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); return {}; } - return {ip.gw.addr}; + return network::IPAddress(&ip.gw); } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { const ip_addr_t *dns_ip = dns_getserver(num); -#if LWIP_IPV6 - return {dns_ip->u_addr.ip4.addr}; -#else - return {dns_ip->addr}; -#endif + return network::IPAddress(dns_ip); } } // namespace wifi } // namespace esphome +#endif // USE_ESP_IDF #endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp new file mode 100644 index 0000000000..afb30c3bcf --- /dev/null +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -0,0 +1,486 @@ +#include "wifi_component.h" + +#ifdef USE_WIFI +#ifdef USE_LIBRETINY + +#include +#include +#include "lwip/ip_addr.h" +#include "lwip/err.h" +#include "lwip/dns.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esphome/core/application.h" +#include "esphome/core/util.h" + +namespace esphome { +namespace wifi { + +static const char *const TAG = "wifi_lt"; + +static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +bool WiFiComponent::wifi_mode_(optional sta, optional ap) { + uint8_t current_mode = WiFi.getMode(); + bool current_sta = current_mode & 0b01; + bool current_ap = current_mode & 0b10; + bool enable_sta = sta.value_or(current_sta); + bool enable_ap = ap.value_or(current_ap); + if (current_sta == enable_sta && current_ap == enable_ap) + return true; + + if (enable_sta && !current_sta) { + ESP_LOGV(TAG, "Enabling STA."); + } else if (!enable_sta && current_sta) { + ESP_LOGV(TAG, "Disabling STA."); + } + if (enable_ap && !current_ap) { + ESP_LOGV(TAG, "Enabling AP."); + } else if (!enable_ap && current_ap) { + ESP_LOGV(TAG, "Disabling AP."); + } + + uint8_t mode = 0; + if (enable_sta) + mode |= 0b01; + if (enable_ap) + mode |= 0b10; + bool ret = WiFi.mode(static_cast(mode)); + + if (!ret) { + ESP_LOGW(TAG, "Setting WiFi mode failed!"); + } + + return ret; +} +bool WiFiComponent::wifi_apply_output_power_(float output_power) { + int8_t val = static_cast(output_power * 4); + return WiFi.setTxPower(val); +} +bool WiFiComponent::wifi_sta_pre_setup_() { + if (!this->wifi_mode_(true, {})) + return false; + + WiFi.setAutoReconnect(false); + delay(10); + return true; +} +bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); } +bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + if (!manual_ip.has_value()) { + return true; + } + + WiFi.config(manual_ip->static_ip, manual_ip->gateway, manual_ip->subnet, manual_ip->dns1, manual_ip->dns2); + + return true; +} + +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { + if (!this->has_sta()) + return {}; + network::IPAddresses addresses; + addresses[0] = WiFi.localIP(); +#if USE_NETWORK_IPV6 + int i = 1; + auto v6_addresses = WiFi.allLocalIPv6(); + for (auto address : v6_addresses) { + addresses[i++] = network::IPAddress(address.toString().c_str()); + } +#endif /* USE_NETWORK_IPV6 */ + return addresses; +} + +bool WiFiComponent::wifi_apply_hostname_() { + // setting is done in SYSTEM_EVENT_STA_START callback too + WiFi.setHostname(App.get_name().c_str()); + return true; +} +bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + String ssid = WiFi.SSID(); + if (ssid && strcmp(ssid.c_str(), ap.get_ssid().c_str()) != 0) { + WiFi.disconnect(); + } + + if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { + return false; + } + + this->wifi_apply_hostname_(); + + s_sta_connecting = true; + + 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); + if (status != WL_CONNECTED) { + ESP_LOGW(TAG, "esp_wifi_connect failed! %d", status); + return false; + } + + return true; +} +const char *get_auth_mode_str(uint8_t mode) { + switch (mode) { + case WIFI_AUTH_OPEN: + return "OPEN"; + case WIFI_AUTH_WEP: + return "WEP"; + case WIFI_AUTH_WPA_PSK: + return "WPA PSK"; + case WIFI_AUTH_WPA2_PSK: + return "WPA2 PSK"; + case WIFI_AUTH_WPA_WPA2_PSK: + return "WPA/WPA2 PSK"; + default: + return "UNKNOWN"; + } +} + +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: + return "OFF"; + case WIFI_STA: + return "STA"; + case WIFI_AP: + return "AP"; + case WIFI_AP_STA: + return "AP+STA"; + default: + return "UNKNOWN"; + } +} +const char *get_disconnect_reason_str(uint8_t reason) { + switch (reason) { + case WIFI_REASON_AUTH_EXPIRE: + return "Auth Expired"; + case WIFI_REASON_AUTH_LEAVE: + return "Auth Leave"; + case WIFI_REASON_ASSOC_EXPIRE: + return "Association Expired"; + case WIFI_REASON_ASSOC_TOOMANY: + return "Too Many Associations"; + case WIFI_REASON_NOT_AUTHED: + return "Not Authenticated"; + case WIFI_REASON_NOT_ASSOCED: + return "Not Associated"; + case WIFI_REASON_ASSOC_LEAVE: + return "Association Leave"; + case WIFI_REASON_ASSOC_NOT_AUTHED: + return "Association not Authenticated"; + case WIFI_REASON_DISASSOC_PWRCAP_BAD: + return "Disassociate Power Cap Bad"; + case WIFI_REASON_DISASSOC_SUPCHAN_BAD: + return "Disassociate Supported Channel Bad"; + case WIFI_REASON_IE_INVALID: + return "IE Invalid"; + case WIFI_REASON_MIC_FAILURE: + return "Mic Failure"; + case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: + return "4-Way Handshake Timeout"; + case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT: + return "Group Key Update Timeout"; + case WIFI_REASON_IE_IN_4WAY_DIFFERS: + return "IE In 4-Way Handshake Differs"; + case WIFI_REASON_GROUP_CIPHER_INVALID: + return "Group Cipher Invalid"; + case WIFI_REASON_PAIRWISE_CIPHER_INVALID: + return "Pairwise Cipher Invalid"; + case WIFI_REASON_AKMP_INVALID: + return "AKMP Invalid"; + case WIFI_REASON_UNSUPP_RSN_IE_VERSION: + return "Unsupported RSN IE version"; + case WIFI_REASON_INVALID_RSN_IE_CAP: + return "Invalid RSN IE Cap"; + case WIFI_REASON_802_1X_AUTH_FAILED: + return "802.1x Authentication Failed"; + case WIFI_REASON_CIPHER_SUITE_REJECTED: + return "Cipher Suite Rejected"; + case WIFI_REASON_BEACON_TIMEOUT: + return "Beacon Timeout"; + case WIFI_REASON_NO_AP_FOUND: + return "AP Not Found"; + case WIFI_REASON_AUTH_FAIL: + return "Authentication Failed"; + case WIFI_REASON_ASSOC_FAIL: + return "Association Failed"; + case WIFI_REASON_HANDSHAKE_TIMEOUT: + return "Handshake Failed"; + case WIFI_REASON_CONNECTION_FAIL: + return "Connection Failed"; + case WIFI_REASON_UNSPECIFIED: + default: + return "Unspecified"; + } +} + +#define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY +#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE +#define ESPHOME_EVENT_ID_WIFI_STA_START ARDUINO_EVENT_WIFI_STA_START +#define ESPHOME_EVENT_ID_WIFI_STA_STOP ARDUINO_EVENT_WIFI_STA_STOP +#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED ARDUINO_EVENT_WIFI_STA_CONNECTED +#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED ARDUINO_EVENT_WIFI_STA_DISCONNECTED +#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE +#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP ARDUINO_EVENT_WIFI_STA_GOT_IP +#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6 ARDUINO_EVENT_WIFI_STA_GOT_IP6 +#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP ARDUINO_EVENT_WIFI_STA_LOST_IP +#define ESPHOME_EVENT_ID_WIFI_AP_START ARDUINO_EVENT_WIFI_AP_START +#define ESPHOME_EVENT_ID_WIFI_AP_STOP ARDUINO_EVENT_WIFI_AP_STOP +#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED ARDUINO_EVENT_WIFI_AP_STACONNECTED +#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED ARDUINO_EVENT_WIFI_AP_STADISCONNECTED +#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED +#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED +#define ESPHOME_EVENT_ID_WIFI_AP_GOT_IP6 ARDUINO_EVENT_WIFI_AP_GOT_IP6 +using esphome_wifi_event_id_t = arduino_event_id_t; +using esphome_wifi_event_info_t = arduino_event_info_t; + +void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { + switch (event) { + case ESPHOME_EVENT_ID_WIFI_READY: { + ESP_LOGV(TAG, "Event: WiFi ready"); + break; + } + case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { + auto it = info.wifi_scan_done; + ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + + this->wifi_scan_done_callback_(); + break; + } + case ESPHOME_EVENT_ID_WIFI_STA_START: { + ESP_LOGV(TAG, "Event: WiFi STA start"); + WiFi.setHostname(App.get_name().c_str()); + break; + } + case ESPHOME_EVENT_ID_WIFI_STA_STOP: { + ESP_LOGV(TAG, "Event: WiFi STA stop"); + 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, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + + 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, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + } else { + ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + } + + uint8_t reason = it.reason; + if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT || + reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL || + reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { + WiFi.disconnect(); + this->error_from_callback_ = true; + } + + s_sta_connecting = false; + break; + } + case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { + auto it = info.wifi_sta_authmode_change; + ESP_LOGV(TAG, "Event: 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; + } + break; + } + case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: { + // auto it = info.got_ip.ip_info; + ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), + format_ip4_addr(WiFi.gatewayIP()).c_str()); + s_sta_connecting = false; + break; + } + case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { + // auto it = info.got_ip.ip_info; + ESP_LOGV(TAG, "Event: Got IPv6"); + break; + } + case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { + ESP_LOGV(TAG, "Event: Lost IP"); + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_START: { + ESP_LOGV(TAG, "Event: WiFi AP start"); + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_STOP: { + ESP_LOGV(TAG, "Event: WiFi AP stop"); + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { + auto it = info.wifi_sta_connected; + auto &mac = it.bssid; + ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str()); + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { + auto it = info.wifi_sta_disconnected; + auto &mac = it.bssid; + ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { + ESP_LOGV(TAG, "Event: AP client assigned IP"); + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { + auto it = info.wifi_ap_probereqrecved; + ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + break; + } + default: + break; + } +} +void WiFiComponent::wifi_pre_setup_() { + 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; + } + return WiFiSTAConnectStatus::IDLE; +} +bool WiFiComponent::wifi_scan_start_(bool passive) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + // need to use WiFi because of WiFiScanClass allocations :( + int16_t err = WiFi.scanNetworks(true, true, passive, 200); + if (err != WIFI_SCAN_RUNNING) { + ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); + return false; + } + + return true; +} +void WiFiComponent::wifi_scan_done_callback_() { + this->scan_result_.clear(); + + int16_t num = WiFi.scanComplete(); + if (num < 0) + return; + + this->scan_result_.reserve(static_cast(num)); + for (int i = 0; i < num; i++) { + String ssid = WiFi.SSID(i); + wifi_auth_mode_t authmode = WiFi.encryptionType(i); + int32_t rssi = WiFi.RSSI(i); + uint8_t *bssid = WiFi.BSSID(i); + int32_t channel = WiFi.channel(i); + + WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()), + channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0); + this->scan_result_.push_back(scan); + } + WiFi.scanDelete(); + this->scan_done_ = true; +} + +#ifdef USE_WIFI_AP +bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + if (manual_ip.has_value()) { + return WiFi.softAPConfig(manual_ip->static_ip, manual_ip->gateway, manual_ip->subnet); + } else { + return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); + } +} + +bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + return false; + } + + 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()); +} + +network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; } +#endif // USE_WIFI_AP + +bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); } + +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]; + } + return bssid; +} +std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int32_t WiFiComponent::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_() {} + +} // namespace wifi +} // namespace esphome + +#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 489ebc3699..bac986d899 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -1,11 +1,13 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_RP2040 #include "lwip/dns.h" #include "lwip/err.h" #include "lwip/netif.h" +#include #include "esphome/core/application.h" #include "esphome/core/hal.h" @@ -70,11 +72,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return true; } - IPAddress ip_address = IPAddress(manual_ip->static_ip); - IPAddress gateway = IPAddress(manual_ip->gateway); - IPAddress subnet = IPAddress(manual_ip->subnet); + IPAddress ip_address = manual_ip->static_ip; + IPAddress gateway = manual_ip->gateway; + IPAddress subnet = manual_ip->subnet; - IPAddress dns = IPAddress(manual_ip->dns1); + IPAddress dns = manual_ip->dns1; WiFi.config(ip_address, dns, gateway, subnet); return true; @@ -138,20 +140,39 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { return true; } +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { - // TODO: - return false; + esphome::network::IPAddress ip_address, gateway, subnet, dns; + if (manual_ip.has_value()) { + ip_address = manual_ip->static_ip; + gateway = manual_ip->gateway; + subnet = manual_ip->subnet; + dns = manual_ip->static_ip; + } else { + ip_address = network::IPAddress(192, 168, 4, 1); + gateway = network::IPAddress(192, 168, 4, 1); + subnet = network::IPAddress(255, 255, 255, 0); + dns = network::IPAddress(192, 168, 4, 1); + } + WiFi.config(ip_address, dns, gateway, subnet); + return true; } bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; + if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + return false; + } WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1)); return true; } -network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.localIP()}; } + +network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; } +#endif // USE_WIFI_AP bool WiFiComponent::wifi_disconnect_() { int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); @@ -170,12 +191,19 @@ std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } int32_t WiFiComponent::wifi_channel_() { return WiFi.channel(); } -network::IPAddress WiFiComponent::wifi_sta_ip() { return {WiFi.localIP()}; } -network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } -network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } +network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { + network::IPAddresses addresses; + uint8_t index = 0; + for (auto addr : addrList) { + addresses[index++] = addr.ipFromNetifNum(); + } + return addresses; +} +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) { const ip_addr_t *dns_ip = dns_getserver(num); - return {dns_ip->addr}; + return network::IPAddress(dns_ip); } void WiFiComponent::wifi_loop_() { @@ -191,3 +219,4 @@ void WiFiComponent::wifi_pre_setup_() {} } // namespace esphome #endif +#endif diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 3cb60e6175..5d5bd8dca3 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -3,19 +3,19 @@ The cryptography package is loaded lazily in the functions so that it doesn't crash if it's not installed. """ + import logging from pathlib import Path -from esphome.core import CORE import esphome.config_validation as cv from esphome.const import ( - CONF_USERNAME, - CONF_IDENTITY, - CONF_PASSWORD, CONF_CERTIFICATE, + CONF_IDENTITY, CONF_KEY, + CONF_PASSWORD, + CONF_USERNAME, ) - +from esphome.core import CORE _LOGGER = logging.getLogger(__name__) @@ -48,8 +48,8 @@ def wrapped_load_pem_x509_certificate(value): def wrapped_load_pem_private_key(value, password): validate_cryptography_installed() - from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import load_pem_private_key if password: password = password.encode("UTF-8") @@ -90,7 +90,7 @@ def _validate_load_private_key(key, cert_pw): def _check_private_key_cert_match(key, cert): - from cryptography.hazmat.primitives.asymmetric import rsa, ec + from cryptography.hazmat.primitives.asymmetric import ec, rsa def check_match_a(): return key.public_key().public_numbers() == cert.public_key().public_numbers() diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 659fd08db1..4ceb73a695 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -1,13 +1,13 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import text_sensor +import esphome.config_validation as cv from esphome.const import ( CONF_BSSID, + CONF_DNS_ADDRESS, CONF_IP_ADDRESS, + CONF_MAC_ADDRESS, CONF_SCAN_RESULTS, CONF_SSID, - CONF_MAC_ADDRESS, - CONF_DNS_ADDRESS, ENTITY_CATEGORY_DIAGNOSTIC, ) @@ -37,7 +37,16 @@ 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(cv.polling_component_schema("1s")) + .extend( + { + cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + for x in range(5) + } + ), cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("60s")), @@ -65,9 +74,15 @@ async def setup_conf(config, key): async def to_code(config): - await setup_conf(config, CONF_IP_ADDRESS) await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) await setup_conf(config, CONF_MAC_ADDRESS) await setup_conf(config, CONF_SCAN_RESULTS) await setup_conf(config, CONF_DNS_ADDRESS) + 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]) + for x in range(5): + if sensor_conf := conf.get(f"address_{x}"): + sens = await text_sensor.new_text_sensor(sensor_conf) + cg.add(wifi_info.add_ip_sensors(x, sens)) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index eeb4985398..150c7229f8 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -1,4 +1,5 @@ #include "wifi_info_text_sensor.h" +#ifdef USE_WIFI #include "esphome/core/log.h" namespace esphome { @@ -15,3 +16,4 @@ void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo DNS Addre } // namespace wifi_info } // namespace esphome +#endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 35ce108c86..0aa44a0894 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/wifi/wifi_component.h" +#ifdef USE_WIFI +#include namespace esphome { namespace wifi_info { @@ -10,32 +12,38 @@ namespace wifi_info { class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { - auto ip = wifi::global_wifi_component->wifi_sta_ip(); - if (ip != this->last_ip_) { - this->last_ip_ = ip; - this->publish_state(ip.str()); + 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; } std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; } void dump_config() override; + void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } protected: - network::IPAddress last_ip_; + network::IPAddresses last_ips_; + std::array ip_sensors_; }; class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { - std::string dns_results; - auto dns_one = wifi::global_wifi_component->get_dns_address(0); auto dns_two = wifi::global_wifi_component->get_dns_address(1); - dns_results += "DNS1: "; - dns_results += dns_one.str(); - dns_results += " DNS2: "; - dns_results += dns_two.str(); + std::string dns_results = dns_one.str() + " " + dns_two.str(); if (dns_results != this->last_results_) { this->last_results_ = dns_results; @@ -124,3 +132,4 @@ class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { } // namespace wifi_info } // namespace esphome +#endif diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index 77fabf272e..99b51adea0 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor +import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, ENTITY_CATEGORY_DIAGNOSTIC, diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.cpp b/esphome/components/wifi_signal/wifi_signal_sensor.cpp index ba22138e2a..4347295421 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.cpp +++ b/esphome/components/wifi_signal/wifi_signal_sensor.cpp @@ -1,4 +1,5 @@ #include "wifi_signal_sensor.h" +#ifdef USE_WIFI #include "esphome/core/log.h" namespace esphome { @@ -10,3 +11,4 @@ void WiFiSignalSensor::dump_config() { LOG_SENSOR("", "WiFi Signal", this); } } // namespace wifi_signal } // namespace esphome +#endif diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index f797aaa590..fbe03a6404 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -4,7 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/wifi/wifi_component.h" - +#ifdef USE_WIFI namespace esphome { namespace wifi_signal { @@ -19,3 +19,4 @@ class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { } // namespace wifi_signal } // namespace esphome +#endif diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py new file mode 100644 index 0000000000..5e34a8a19b --- /dev/null +++ b/esphome/components/wireguard/__init__.py @@ -0,0 +1,184 @@ +import ipaddress +import re + +from esphome import automation +import esphome.codegen as cg +from esphome.components import time +from esphome.components.esp32 import CORE, add_idf_sdkconfig_option +import esphome.config_validation as cv +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_REBOOT_TIMEOUT, + CONF_TIME_ID, + KEY_CORE, + KEY_FRAMEWORK_VERSION, +) +from esphome.core import TimePeriod + +CONF_NETMASK = "netmask" +CONF_PRIVATE_KEY = "private_key" +CONF_PEER_ENDPOINT = "peer_endpoint" +CONF_PEER_PUBLIC_KEY = "peer_public_key" +CONF_PEER_PORT = "peer_port" +CONF_PEER_PRESHARED_KEY = "peer_preshared_key" +CONF_PEER_ALLOWED_IPS = "peer_allowed_ips" +CONF_PEER_PERSISTENT_KEEPALIVE = "peer_persistent_keepalive" +CONF_REQUIRE_CONNECTION_TO_PROCEED = "require_connection_to_proceed" + +CONF_WIREGUARD_ID = "wireguard_id" + +DEPENDENCIES = ["time"] +CODEOWNERS = ["@lhoracek", "@droscy", "@thomas0bernard"] + +# The key validation regex has been described by Jason Donenfeld himself +# url: https://lists.zx2c4.com/pipermail/wireguard/2020-December/006222.html +_WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$") + +wireguard_ns = cg.esphome_ns.namespace("wireguard") +Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent) +WireguardPeerOnlineCondition = wireguard_ns.class_( + "WireguardPeerOnlineCondition", automation.Condition +) +WireguardEnabledCondition = wireguard_ns.class_( + "WireguardEnabledCondition", automation.Condition +) +WireguardEnableAction = wireguard_ns.class_("WireguardEnableAction", automation.Action) +WireguardDisableAction = wireguard_ns.class_( + "WireguardDisableAction", automation.Action +) + + +def _wireguard_key(value): + if _WG_KEY_REGEX.match(cv.string(value)) is not None: + return value + raise cv.Invalid(f"Invalid WireGuard key: {value}") + + +def _cidr_network(value): + try: + ipaddress.ip_network(value, strict=False) + except ValueError as err: + raise cv.Invalid(f"Invalid network in CIDR notation: {err}") + return value + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Wireguard), + cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Required(CONF_ADDRESS): cv.ipv4, + cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4, + cv.Required(CONF_PRIVATE_KEY): _wireguard_key, + cv.Required(CONF_PEER_ENDPOINT): cv.string, + cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key, + cv.Optional(CONF_PEER_PORT, default=51820): cv.port, + cv.Optional(CONF_PEER_PRESHARED_KEY): _wireguard_key, + cv.Optional(CONF_PEER_ALLOWED_IPS, default=["0.0.0.0/0"]): cv.ensure_list( + _cidr_network + ), + cv.Optional(CONF_PEER_PERSISTENT_KEEPALIVE, default="0s"): cv.All( + cv.positive_time_period_seconds, + cv.Range(max=TimePeriod(seconds=65535)), + ), + cv.Optional( + CONF_REBOOT_TIMEOUT, default="15min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REQUIRE_CONNECTION_TO_PROCEED, default=False): cv.boolean, + } +).extend(cv.polling_component_schema("10s")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + cg.add_define("USE_WIREGUARD") + + cg.add(var.set_address(str(config[CONF_ADDRESS]))) + cg.add(var.set_netmask(str(config[CONF_NETMASK]))) + cg.add(var.set_private_key(config[CONF_PRIVATE_KEY])) + cg.add(var.set_peer_endpoint(config[CONF_PEER_ENDPOINT])) + cg.add(var.set_peer_public_key(config[CONF_PEER_PUBLIC_KEY])) + cg.add(var.set_peer_port(config[CONF_PEER_PORT])) + cg.add(var.set_keepalive(config[CONF_PEER_PERSISTENT_KEEPALIVE])) + cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) + + if CONF_PEER_PRESHARED_KEY in config: + cg.add(var.set_preshared_key(config[CONF_PEER_PRESHARED_KEY])) + + allowed_ips = list( + ipaddress.collapse_addresses( + [ + ipaddress.ip_network(ip, strict=False) + for ip in config[CONF_PEER_ALLOWED_IPS] + ] + ) + ) + + for ip in allowed_ips: + cg.add(var.add_allowed_ip(str(ip.network_address), str(ip.netmask))) + + cg.add(var.set_srctime(await cg.get_variable(config[CONF_TIME_ID]))) + + if config[CONF_REQUIRE_CONNECTION_TO_PROCEED]: + cg.add(var.disable_auto_proceed()) + + # Workaround for crash on IDF 5+ + # See https://github.com/trombik/esp_wireguard/issues/33#issuecomment-1568503651 + if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( + 5, 0, 0 + ): + add_idf_sdkconfig_option("CONFIG_LWIP_PPP_SUPPORT", True) + + # This flag is added here because the esp_wireguard library statically + # set the size of its allowed_ips list at compile time using this value; + # the '+1' modifier is relative to the device's own address that will + # be automatically added to the provided list. + cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}") + cg.add_library("droscy/esp_wireguard", "0.4.2") + + await cg.register_component(var, config) + + +@automation.register_condition( + "wireguard.peer_online", + WireguardPeerOnlineCondition, + cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +) +async def wireguard_peer_up_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_condition( + "wireguard.enabled", + WireguardEnabledCondition, + cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +) +async def wireguard_enabled_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "wireguard.enable", + WireguardEnableAction, + cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +) +async def wireguard_enable_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( + "wireguard.disable", + WireguardDisableAction, + cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +) +async def wireguard_disable_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/wireguard/binary_sensor.py b/esphome/components/wireguard/binary_sensor.py new file mode 100644 index 0000000000..7344558659 --- /dev/null +++ b/esphome/components/wireguard/binary_sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_STATUS, + DEVICE_CLASS_CONNECTIVITY, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +from . import CONF_WIREGUARD_ID, Wireguard + +CONF_ENABLED = "enabled" + +DEPENDENCIES = ["wireguard"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_WIREGUARD_ID): cv.use_id(Wireguard), + cv.Optional(CONF_STATUS): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_CONNECTIVITY, + ), + cv.Optional(CONF_ENABLED): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_WIREGUARD_ID]) + + if status_config := config.get(CONF_STATUS): + sens = await binary_sensor.new_binary_sensor(status_config) + cg.add(parent.set_status_sensor(sens)) + + if enabled_config := config.get(CONF_ENABLED): + sens = await binary_sensor.new_binary_sensor(enabled_config) + cg.add(parent.set_enabled_sensor(sens)) diff --git a/esphome/components/wireguard/sensor.py b/esphome/components/wireguard/sensor.py new file mode 100644 index 0000000000..85703d24b3 --- /dev/null +++ b/esphome/components/wireguard/sensor.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +from . import CONF_WIREGUARD_ID, Wireguard + +CONF_LATEST_HANDSHAKE = "latest_handshake" + +DEPENDENCIES = ["wireguard"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_WIREGUARD_ID): cv.use_id(Wireguard), + cv.Optional(CONF_LATEST_HANDSHAKE): sensor.sensor_schema( + device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_WIREGUARD_ID]) + + if latest_handshake_config := config.get(CONF_LATEST_HANDSHAKE): + sens = await sensor.new_sensor(latest_handshake_config) + cg.add(parent.set_handshake_sensor(sens)) diff --git a/esphome/components/wireguard/text_sensor.py b/esphome/components/wireguard/text_sensor.py new file mode 100644 index 0000000000..51614a1a28 --- /dev/null +++ b/esphome/components/wireguard/text_sensor.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + CONF_ADDRESS, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +from . import CONF_WIREGUARD_ID, Wireguard + +DEPENDENCIES = ["wireguard"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_WIREGUARD_ID): cv.use_id(Wireguard), + cv.Optional(CONF_ADDRESS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_WIREGUARD_ID]) + + if address_config := config.get(CONF_ADDRESS): + sens = await text_sensor.new_text_sensor(address_config) + cg.add(parent.set_address_sensor(sens)) diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp new file mode 100644 index 0000000000..7b4011cb79 --- /dev/null +++ b/esphome/components/wireguard/wireguard.cpp @@ -0,0 +1,292 @@ +#include "wireguard.h" +#ifdef USE_WIREGUARD +#include +#include +#include + +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/time.h" +#include "esphome/components/network/util.h" + +#include +#include + +namespace esphome { +namespace wireguard { + +static const char *const TAG = "wireguard"; + +/* + * Cannot use `static const char*` for LOGMSG_PEER_STATUS on esp8266 platform + * because log messages in `Wireguard::update()` method fail. + */ +#define LOGMSG_PEER_STATUS "WireGuard remote peer is %s (latest handshake %s)" + +static const char *const LOGMSG_ONLINE = "online"; +static const char *const LOGMSG_OFFLINE = "offline"; + +void Wireguard::setup() { + ESP_LOGD(TAG, "initializing WireGuard..."); + + this->wg_config_.address = this->address_.c_str(); + this->wg_config_.private_key = this->private_key_.c_str(); + this->wg_config_.endpoint = this->peer_endpoint_.c_str(); + this->wg_config_.public_key = this->peer_public_key_.c_str(); + this->wg_config_.port = this->peer_port_; + this->wg_config_.netmask = this->netmask_.c_str(); + this->wg_config_.persistent_keepalive = this->keepalive_; + + if (this->preshared_key_.length() > 0) + this->wg_config_.preshared_key = this->preshared_key_.c_str(); + + this->publish_enabled_state(); + + this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); + + if (this->wg_initialized_ == ESP_OK) { + ESP_LOGI(TAG, "WireGuard initialized"); + this->wg_peer_offline_time_ = millis(); + this->srctime_->add_on_time_sync_callback(std::bind(&Wireguard::start_connection_, this)); + this->defer(std::bind(&Wireguard::start_connection_, this)); // defer to avoid blocking setup + +#ifdef USE_TEXT_SENSOR + if (this->address_sensor_ != nullptr) { + this->address_sensor_->publish_state(this->address_); + } +#endif + } else { + ESP_LOGE(TAG, "cannot initialize WireGuard, error code %d", this->wg_initialized_); + this->mark_failed(); + } +} + +void Wireguard::loop() { + if (!this->enabled_) { + return; + } + + if ((this->wg_initialized_ == ESP_OK) && (this->wg_connected_ == ESP_OK) && (!network::is_connected())) { + ESP_LOGV(TAG, "local network connection has been lost, stopping WireGuard..."); + this->stop_connection_(); + } +} + +void Wireguard::update() { + bool peer_up = this->is_peer_up(); + time_t lhs = this->get_latest_handshake(); + bool lhs_updated = (lhs > this->latest_saved_handshake_); + + ESP_LOGV(TAG, "enabled=%d, connected=%d, peer_up=%d, handshake: current=%.0f latest=%.0f updated=%d", + (int) this->enabled_, (int) (this->wg_connected_ == ESP_OK), (int) peer_up, (double) lhs, + (double) this->latest_saved_handshake_, (int) lhs_updated); + + if (lhs_updated) { + this->latest_saved_handshake_ = lhs; + } + + std::string latest_handshake = + (this->latest_saved_handshake_ > 0) + ? ESPTime::from_epoch_local(this->latest_saved_handshake_).strftime("%Y-%m-%d %H:%M:%S %Z") + : "timestamp not available"; + + if (peer_up) { + if (this->wg_peer_offline_time_ != 0) { + ESP_LOGI(TAG, LOGMSG_PEER_STATUS, LOGMSG_ONLINE, latest_handshake.c_str()); + this->wg_peer_offline_time_ = 0; + } else { + ESP_LOGD(TAG, LOGMSG_PEER_STATUS, LOGMSG_ONLINE, latest_handshake.c_str()); + } + } else { + if (this->wg_peer_offline_time_ == 0) { + ESP_LOGW(TAG, LOGMSG_PEER_STATUS, LOGMSG_OFFLINE, latest_handshake.c_str()); + this->wg_peer_offline_time_ = millis(); + } else if (this->enabled_) { + ESP_LOGD(TAG, LOGMSG_PEER_STATUS, LOGMSG_OFFLINE, latest_handshake.c_str()); + this->start_connection_(); + } + + // check reboot timeout every time the peer is down + if (this->enabled_ && this->reboot_timeout_ > 0) { + if (millis() - this->wg_peer_offline_time_ > this->reboot_timeout_) { + ESP_LOGE(TAG, "WireGuard remote peer is unreachable, rebooting..."); + App.reboot(); + } + } + } + +#ifdef USE_BINARY_SENSOR + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(peer_up); + } +#endif + +#ifdef USE_SENSOR + if (this->handshake_sensor_ != nullptr && lhs_updated) { + this->handshake_sensor_->publish_state((double) this->latest_saved_handshake_); + } +#endif +} + +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_.length() > 0 ? mask_key(this->preshared_key_).c_str() : "NOT IN USE")); + 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()); + } + ESP_LOGCONFIG(TAG, " Peer Persistent Keepalive: %d%s", this->keepalive_, + (this->keepalive_ > 0 ? "s" : " (DISABLED)")); + ESP_LOGCONFIG(TAG, " Reboot Timeout: %" PRIu32 "%s", (this->reboot_timeout_ / 1000), + (this->reboot_timeout_ != 0 ? "s" : " (DISABLED)")); + // be careful: if proceed_allowed_ is true, require connection is false + ESP_LOGCONFIG(TAG, " Require Connection to Proceed: %s", (this->proceed_allowed_ ? "NO" : "YES")); + LOG_UPDATE_INTERVAL(this); +} + +void Wireguard::on_shutdown() { this->stop_connection_(); } + +bool Wireguard::can_proceed() { return (this->proceed_allowed_ || this->is_peer_up() || !this->enabled_); } + +bool Wireguard::is_peer_up() const { + return (this->wg_initialized_ == ESP_OK) && (this->wg_connected_ == ESP_OK) && + (esp_wireguardif_peer_is_up(&(this->wg_ctx_)) == ESP_OK); +} + +time_t Wireguard::get_latest_handshake() const { + time_t result; + if (esp_wireguard_latest_handshake(&(this->wg_ctx_), &result) != ESP_OK) { + result = 0; + } + return result; +} + +void Wireguard::set_address(const std::string &address) { this->address_ = address; } +void Wireguard::set_netmask(const std::string &netmask) { this->netmask_ = netmask; } +void Wireguard::set_private_key(const std::string &key) { this->private_key_ = key; } +void Wireguard::set_peer_endpoint(const std::string &endpoint) { this->peer_endpoint_ = endpoint; } +void Wireguard::set_peer_public_key(const std::string &key) { this->peer_public_key_ = key; } +void Wireguard::set_peer_port(const uint16_t port) { this->peer_port_ = port; } +void Wireguard::set_preshared_key(const std::string &key) { this->preshared_key_ = key; } + +void Wireguard::add_allowed_ip(const std::string &ip, const std::string &netmask) { + this->allowed_ips_.emplace_back(ip, netmask); +} + +void Wireguard::set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; } +void Wireguard::set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; } +void Wireguard::set_srctime(time::RealTimeClock *srctime) { this->srctime_ = srctime; } + +#ifdef USE_BINARY_SENSOR +void Wireguard::set_status_sensor(binary_sensor::BinarySensor *sensor) { this->status_sensor_ = sensor; } +void Wireguard::set_enabled_sensor(binary_sensor::BinarySensor *sensor) { this->enabled_sensor_ = sensor; } +#endif + +#ifdef USE_SENSOR +void Wireguard::set_handshake_sensor(sensor::Sensor *sensor) { this->handshake_sensor_ = sensor; } +#endif + +#ifdef USE_TEXT_SENSOR +void Wireguard::set_address_sensor(text_sensor::TextSensor *sensor) { this->address_sensor_ = sensor; } +#endif + +void Wireguard::disable_auto_proceed() { this->proceed_allowed_ = false; } + +void Wireguard::enable() { + this->enabled_ = true; + ESP_LOGI(TAG, "WireGuard enabled"); + this->publish_enabled_state(); +} + +void Wireguard::disable() { + this->enabled_ = false; + this->defer(std::bind(&Wireguard::stop_connection_, this)); // defer to avoid blocking running loop + ESP_LOGI(TAG, "WireGuard disabled"); + this->publish_enabled_state(); +} + +void Wireguard::publish_enabled_state() { +#ifdef USE_BINARY_SENSOR + if (this->enabled_sensor_ != nullptr) { + this->enabled_sensor_->publish_state(this->enabled_); + } +#endif +} + +bool Wireguard::is_enabled() { return this->enabled_; } + +void Wireguard::start_connection_() { + if (!this->enabled_) { + ESP_LOGV(TAG, "WireGuard is disabled, cannot start connection"); + return; + } + + if (this->wg_initialized_ != ESP_OK) { + ESP_LOGE(TAG, "cannot start WireGuard, initialization in error with code %d", this->wg_initialized_); + return; + } + + if (!network::is_connected()) { + ESP_LOGD(TAG, "WireGuard is waiting for local network connection to be available"); + return; + } + + if (!this->srctime_->now().is_valid()) { + ESP_LOGD(TAG, "WireGuard is waiting for system time to be synchronized"); + return; + } + + if (this->wg_connected_ == ESP_OK) { + ESP_LOGV(TAG, "WireGuard connection already started"); + return; + } + + ESP_LOGD(TAG, "starting WireGuard connection..."); + this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_)); + + if (this->wg_connected_ == ESP_OK) { + ESP_LOGI(TAG, "WireGuard connection started"); + } else if (this->wg_connected_ == ESP_ERR_RETRY) { + ESP_LOGD(TAG, "WireGuard is waiting for endpoint IP address to be available"); + return; + } else { + ESP_LOGW(TAG, "cannot start WireGuard connection, error code %d", this->wg_connected_); + return; + } + + ESP_LOGD(TAG, "configuring WireGuard allowed IPs list..."); + bool allowed_ips_ok = true; + for (std::tuple ip : this->allowed_ips_) { + allowed_ips_ok &= + (esp_wireguard_add_allowed_ip(&(this->wg_ctx_), std::get<0>(ip).c_str(), std::get<1>(ip).c_str()) == ESP_OK); + } + + if (allowed_ips_ok) { + ESP_LOGD(TAG, "allowed IPs list configured correctly"); + } else { + ESP_LOGE(TAG, "cannot configure WireGuard allowed IPs list, aborting..."); + this->stop_connection_(); + this->mark_failed(); + } +} + +void Wireguard::stop_connection_() { + if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) { + ESP_LOGD(TAG, "stopping WireGuard connection..."); + esp_wireguard_disconnect(&(this->wg_ctx_)); + this->wg_connected_ = ESP_FAIL; + } +} + +std::string mask_key(const std::string &key) { return (key.substr(0, 5) + "[...]="); } + +} // namespace wireguard +} // namespace esphome +#endif diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h new file mode 100644 index 0000000000..5db9a48c90 --- /dev/null +++ b/esphome/components/wireguard/wireguard.h @@ -0,0 +1,174 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_WIREGUARD +#include +#include +#include + +#include "esphome/core/component.h" +#include "esphome/components/time/real_time_clock.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif + +#include + +namespace esphome { +namespace wireguard { + +/// Main Wireguard component class. +class Wireguard : public PollingComponent { + public: + void setup() override; + void loop() override; + void update() override; + void dump_config() override; + void on_shutdown() override; + bool can_proceed() override; + + float get_setup_priority() const override { return esphome::setup_priority::BEFORE_CONNECTION; } + + void set_address(const std::string &address); + void set_netmask(const std::string &netmask); + void set_private_key(const std::string &key); + void set_peer_endpoint(const std::string &endpoint); + void set_peer_public_key(const std::string &key); + void set_peer_port(uint16_t port); + void set_preshared_key(const std::string &key); + + void add_allowed_ip(const std::string &ip, const std::string &netmask); + + void set_keepalive(uint16_t seconds); + void set_reboot_timeout(uint32_t seconds); + void set_srctime(time::RealTimeClock *srctime); + +#ifdef USE_BINARY_SENSOR + void set_status_sensor(binary_sensor::BinarySensor *sensor); + void set_enabled_sensor(binary_sensor::BinarySensor *sensor); +#endif + +#ifdef USE_SENSOR + void set_handshake_sensor(sensor::Sensor *sensor); +#endif + +#ifdef USE_TEXT_SENSOR + void set_address_sensor(text_sensor::TextSensor *sensor); +#endif + + /// Block the setup step until peer is connected. + void disable_auto_proceed(); + + /// Enable the WireGuard component. + void enable(); + + /// Stop any running connection and disable the WireGuard component. + void disable(); + + /// Publish the enabled state if the enabled binary sensor is configured. + void publish_enabled_state(); + + /// Return if the WireGuard component is or is not enabled. + bool is_enabled(); + + bool is_peer_up() const; + time_t get_latest_handshake() const; + + protected: + std::string address_; + std::string netmask_; + std::string private_key_; + std::string peer_endpoint_; + std::string peer_public_key_; + std::string preshared_key_; + + std::vector> allowed_ips_; + + uint16_t peer_port_; + uint16_t keepalive_; + uint32_t reboot_timeout_; + + time::RealTimeClock *srctime_; + +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *status_sensor_ = nullptr; + binary_sensor::BinarySensor *enabled_sensor_ = nullptr; +#endif + +#ifdef USE_SENSOR + sensor::Sensor *handshake_sensor_ = nullptr; +#endif + +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *address_sensor_ = nullptr; +#endif + + /// Set to false to block the setup step until peer is connected. + bool proceed_allowed_ = true; + + /// When false the wireguard link will not be established + bool enabled_ = true; + + wireguard_config_t wg_config_ = ESP_WIREGUARD_CONFIG_DEFAULT(); + wireguard_ctx_t wg_ctx_ = ESP_WIREGUARD_CONTEXT_DEFAULT(); + + esp_err_t wg_initialized_ = ESP_FAIL; + esp_err_t wg_connected_ = ESP_FAIL; + + /// The last time the remote peer become offline. + uint32_t wg_peer_offline_time_ = 0; + + /** \brief The latest saved handshake. + * + * This is used to save (and log) the latest completed handshake even + * after a full refresh of the wireguard keys (for example after a + * stop/start connection cycle). + */ + time_t latest_saved_handshake_ = 0; + + void start_connection_(); + void stop_connection_(); +}; + +// These are used for possibly long DNS resolution to temporarily suspend the watchdog +void suspend_wdt(); +void resume_wdt(); + +/// Strip most part of the key only for secure printing +std::string mask_key(const std::string &key); + +/// Condition to check if remote peer is online. +template class WireguardPeerOnlineCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_peer_up(); } +}; + +/// Condition to check if Wireguard component is enabled. +template class WireguardEnabledCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_enabled(); } +}; + +/// Action to enable Wireguard component. +template class WireguardEnableAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->enable(); } +}; + +/// Action to disable Wireguard component. +template class WireguardDisableAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->disable(); } +}; + +} // namespace wireguard +} // namespace esphome +#endif diff --git a/esphome/components/wk2132_i2c/__init__.py b/esphome/components/wk2132_i2c/__init__.py new file mode 100644 index 0000000000..912ab04236 --- /dev/null +++ b/esphome/components/wk2132_i2c/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, weikai +from esphome.const import CONF_ID + +CODEOWNERS = ["@DrCoolZic"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["weikai", "weikai_i2c"] +MULTI_CONF = True + +weikai_i2c_ns = cg.esphome_ns.namespace("weikai_i2c") +WeikaiComponentI2C = weikai_i2c_ns.class_( + "WeikaiComponentI2C", weikai.WeikaiComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.All( + weikai.WKBASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiComponentI2C), + } + ).extend(i2c.i2c_device_schema(0x2C)), + weikai.check_channel_max_2, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_name(str(config[CONF_ID]))) + await weikai.register_weikai(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/wk2132_i2c/wk2132_i2c.cpp b/esphome/components/wk2132_i2c/wk2132_i2c.cpp new file mode 100644 index 0000000000..aaefae6f97 --- /dev/null +++ b/esphome/components/wk2132_i2c/wk2132_i2c.cpp @@ -0,0 +1,4 @@ +/* compiling with esp-idf framework requires a .cpp file for some reason ? */ +namespace esphome { +namespace wk2132_i2c {} +} // namespace esphome diff --git a/esphome/components/wk2132_spi/__init__.py b/esphome/components/wk2132_spi/__init__.py new file mode 100644 index 0000000000..02c5fd9604 --- /dev/null +++ b/esphome/components/wk2132_spi/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, weikai + +from esphome.const import CONF_ID + +CODEOWNERS = ["@DrCoolZic"] +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["weikai", "weikai_spi"] +MULTI_CONF = True + +weikai_spi_ns = cg.esphome_ns.namespace("weikai_spi") +WeikaiComponentSPI = weikai_spi_ns.class_( + "WeikaiComponentSPI", weikai.WeikaiComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = cv.All( + weikai.WKBASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiComponentSPI), + } + ).extend(spi.spi_device_schema()), + weikai.check_channel_max_2, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_name(str(config[CONF_ID]))) + await weikai.register_weikai(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/wk2168_i2c/__init__.py b/esphome/components/wk2168_i2c/__init__.py new file mode 100644 index 0000000000..93a8161e8e --- /dev/null +++ b/esphome/components/wk2168_i2c/__init__.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c, weikai +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, +) + +CODEOWNERS = ["@DrCoolZic"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["weikai", "weikai_i2c"] +MULTI_CONF = True +CONF_WK2168_I2C = "wk2168_i2c" + +weikai_ns = cg.esphome_ns.namespace("weikai") +weikai_i2c_ns = cg.esphome_ns.namespace("weikai_i2c") +WeikaiComponentI2C = weikai_i2c_ns.class_( + "WeikaiComponentI2C", weikai.WeikaiComponent, i2c.I2CDevice +) +WeikaiGPIOPin = weikai_ns.class_( + "WeikaiGPIOPin", cg.GPIOPin, cg.Parented.template(WeikaiComponentI2C) +) + +CONFIG_SCHEMA = cv.All( + weikai.WKBASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiComponentI2C), + } + ).extend(i2c.i2c_device_schema(0x2C)), + weikai.check_channel_max_4, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_name(str(config[CONF_ID]))) + await weikai.register_weikai(var, config) + await i2c.register_i2c_device(var, config) + + +WK2168_PIN_SCHEMA = cv.All( + weikai.WEIKAI_PIN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiGPIOPin), + cv.Required(CONF_WK2168_I2C): cv.use_id(WeikaiComponentI2C), + } + ), + weikai.validate_pin_mode, +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_WK2168_I2C, WK2168_PIN_SCHEMA) +async def sc16is75x_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_WK2168_I2C]) + cg.add(var.set_parent(parent)) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/wk2168_spi/__init__.py b/esphome/components/wk2168_spi/__init__.py new file mode 100644 index 0000000000..8861a6738c --- /dev/null +++ b/esphome/components/wk2168_spi/__init__.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, weikai +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, +) + +CODEOWNERS = ["@DrCoolZic"] +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["weikai", "weikai_spi"] +MULTI_CONF = True +CONF_WK2168_SPI = "wk2168_spi" + +weikai_spi_ns = cg.esphome_ns.namespace("weikai_spi") +weikai_ns = cg.esphome_ns.namespace("weikai") +WeikaiComponentSPI = weikai_spi_ns.class_( + "WeikaiComponentSPI", weikai.WeikaiComponent, spi.SPIDevice +) +WeikaiGPIOPin = weikai_ns.class_( + "WeikaiGPIOPin", cg.GPIOPin, cg.Parented.template(WeikaiComponentSPI) +) + +CONFIG_SCHEMA = cv.All( + weikai.WKBASE_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(WeikaiComponentSPI)} + ).extend(spi.spi_device_schema()), + weikai.check_channel_max_4, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_name(str(config[CONF_ID]))) + await weikai.register_weikai(var, config) + await spi.register_spi_device(var, config) + + +WK2168_PIN_SCHEMA = cv.All( + weikai.WEIKAI_PIN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiGPIOPin), + cv.Required(CONF_WK2168_SPI): cv.use_id(WeikaiComponentSPI), + }, + ), + weikai.validate_pin_mode, +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_WK2168_SPI, WK2168_PIN_SCHEMA) +async def sc16is75x_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_WK2168_SPI]) + cg.add(var.set_parent(parent)) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/wk2204_i2c/__init__.py b/esphome/components/wk2204_i2c/__init__.py new file mode 100644 index 0000000000..98eca56c4d --- /dev/null +++ b/esphome/components/wk2204_i2c/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, weikai +from esphome.const import CONF_ID + +CODEOWNERS = ["@DrCoolZic"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["weikai", "weikai_i2c"] +MULTI_CONF = True + +weikai_i2c_ns = cg.esphome_ns.namespace("weikai_i2c") +WeikaiComponentI2C = weikai_i2c_ns.class_( + "WeikaiComponentI2C", weikai.WeikaiComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.All( + weikai.WKBASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiComponentI2C), + } + ).extend(i2c.i2c_device_schema(0x2C)), + weikai.check_channel_max_4, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_name(str(config[CONF_ID]))) + await weikai.register_weikai(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/wk2204_spi/__init__.py b/esphome/components/wk2204_spi/__init__.py new file mode 100644 index 0000000000..447805375d --- /dev/null +++ b/esphome/components/wk2204_spi/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, weikai +from esphome.const import CONF_ID + +CODEOWNERS = ["@DrCoolZic"] +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["weikai", "weikai_spi"] +MULTI_CONF = True + +weikai_spi_ns = cg.esphome_ns.namespace("weikai_spi") +WeikaiComponentSPI = weikai_spi_ns.class_( + "WeikaiComponentSPI", weikai.WeikaiComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = cv.All( + weikai.WKBASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiComponentSPI), + } + ).extend(spi.spi_device_schema()), + weikai.check_channel_max_4, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_name(str(config[CONF_ID]))) + await weikai.register_weikai(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/wk2212_i2c/__init__.py b/esphome/components/wk2212_i2c/__init__.py new file mode 100644 index 0000000000..fd4d717b31 --- /dev/null +++ b/esphome/components/wk2212_i2c/__init__.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c, weikai +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, +) + +CODEOWNERS = ["@DrCoolZic"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["weikai", "weikai_i2c"] +MULTI_CONF = True +CONF_WK2212_I2C = "wk2212_i2c" + +weikai_ns = cg.esphome_ns.namespace("weikai") +weikai_i2c_ns = cg.esphome_ns.namespace("weikai_i2c") +WeikaiComponentI2C = weikai_i2c_ns.class_( + "WeikaiComponentI2C", weikai.WeikaiComponent, i2c.I2CDevice +) +WeikaiGPIOPin = weikai_ns.class_( + "WeikaiGPIOPin", cg.GPIOPin, cg.Parented.template(WeikaiComponentI2C) +) + +CONFIG_SCHEMA = cv.All( + weikai.WKBASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiComponentI2C), + } + ).extend(i2c.i2c_device_schema(0x2C)), + weikai.check_channel_max_2, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_name(str(config[CONF_ID]))) + await weikai.register_weikai(var, config) + await i2c.register_i2c_device(var, config) + + +WK2212_PIN_SCHEMA = cv.All( + weikai.WEIKAI_PIN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiGPIOPin), + cv.Required(CONF_WK2212_I2C): cv.use_id(WeikaiComponentI2C), + } + ), + weikai.validate_pin_mode, +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_WK2212_I2C, WK2212_PIN_SCHEMA) +async def sc16is75x_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_WK2212_I2C]) + cg.add(var.set_parent(parent)) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/wk2212_spi/__init__.py b/esphome/components/wk2212_spi/__init__.py new file mode 100644 index 0000000000..bfeca87c22 --- /dev/null +++ b/esphome/components/wk2212_spi/__init__.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, weikai +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, +) + +CODEOWNERS = ["@DrCoolZic"] +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["weikai", "weikai_spi"] +MULTI_CONF = True +CONF_WK2212_SPI = "wk2212_spi" + +weikai_ns = cg.esphome_ns.namespace("weikai") +weikai_spi_ns = cg.esphome_ns.namespace("weikai_spi") +WeikaiComponentSPI = weikai_spi_ns.class_( + "WeikaiComponentSPI", weikai.WeikaiComponent, spi.SPIDevice +) +WeikaiGPIOPin = weikai_ns.class_( + "WeikaiGPIOPin", cg.GPIOPin, cg.Parented.template(WeikaiComponentSPI) +) + +CONFIG_SCHEMA = cv.All( + weikai.WKBASE_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(WeikaiComponentSPI)} + ).extend(spi.spi_device_schema()), + weikai.check_channel_max_2, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_name(str(config[CONF_ID]))) + await weikai.register_weikai(var, config) + await spi.register_spi_device(var, config) + + +WK2212_PIN_SCHEMA = cv.All( + weikai.WEIKAI_PIN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(WeikaiGPIOPin), + cv.Required(CONF_WK2212_SPI): cv.use_id(WeikaiComponentSPI), + }, + ), + weikai.validate_pin_mode, +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_WK2212_SPI, WK2212_PIN_SCHEMA) +async def sc16is75x_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_WK2212_SPI]) + cg.add(var.set_parent(parent)) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index 3ffa0c63ce..403f8bd1b3 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -1,6 +1,8 @@ #include "wl_134.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace wl_134 { @@ -71,7 +73,7 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() { 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: %d", reading.reserved1); + ESP_LOGV(TAG, "Reserved1: %" PRId32, reading.reserved1); char buf[20]; sprintf(buf, "%03d%012lld", reading.country, reading.id); diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py index 2795529203..396d5891d8 100644 --- a/esphome/components/wled/__init__.py +++ b/esphome/components/wled/__init__.py @@ -8,6 +8,8 @@ wled_ns = cg.esphome_ns.namespace("wled") WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect) CONFIG_SCHEMA = cv.All(cv.Schema({}), cv.only_with_arduino) +CONF_SYNC_GROUP_MASK = "sync_group_mask" +CONF_BLANK_ON_START = "blank_on_start" @register_addressable_effect( @@ -16,10 +18,13 @@ CONFIG_SCHEMA = cv.All(cv.Schema({}), cv.only_with_arduino) "WLED", { cv.Optional(CONF_PORT, default=21324): cv.port, + cv.Optional(CONF_SYNC_GROUP_MASK, default=0): cv.int_range(min=0, max=255), + cv.Optional(CONF_BLANK_ON_START, default=True): cv.boolean, }, ) async def wled_light_effect_to_code(config, effect_id): effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(effect.set_port(config[CONF_PORT])) - + cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK])) + cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START])) return effect diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 8c68bca6e3..84842dff39 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -13,6 +13,10 @@ #include #endif +#ifdef USE_BK72XX +#include +#endif + namespace esphome { namespace wled { @@ -29,7 +33,11 @@ WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffe void WLEDLightEffect::start() { AddressableLightEffect::start(); - blank_at_ = 0; + if (this->blank_on_start_) { + this->blank_at_ = 0; + } else { + this->blank_at_ = UINT32_MAX; + } } void WLEDLightEffect::stop() { @@ -101,8 +109,11 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p if (!parse_drgb_frame_(it, payload, size)) return false; } else { - if (!parse_notifier_frame_(it, payload, size)) + if (!parse_notifier_frame_(it, payload, size)) { return false; + } else { + timeout = UINT8_MAX; + } } break; @@ -143,8 +154,32 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p } bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { - // Packet needs to be empty - return size == 0; + // Receive at least RGBW and Brightness for all LEDs from WLED Sync Notification + // https://kno.wled.ge/interfaces/udp-notifier/ + // https://github.com/Aircoookie/WLED/blob/main/wled00/udp.cpp + + if (size < 34) { + return false; + } + + uint8_t payload_sync_group_mask = payload[34]; + + if (this->sync_group_mask_ && !(payload_sync_group_mask & this->sync_group_mask_)) { + ESP_LOGD(TAG, "sync group mask does not match"); + return false; + } + + uint8_t bri = payload[0]; + uint8_t r = esp_scale8(payload[1], bri); + uint8_t g = esp_scale8(payload[2], bri); + uint8_t b = esp_scale8(payload[3], bri); + uint8_t w = esp_scale8(payload[8], bri); + + for (auto &&led : it) { + led.set(Color(r, g, b, w)); + } + + return true; } bool WLEDLightEffect::parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index 8f239276d7..a591e1fd1a 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -21,6 +21,8 @@ class WLEDLightEffect : public light::AddressableLightEffect { void stop() override; void apply(light::AddressableLight &it, const Color ¤t_color) override; void set_port(uint16_t port) { this->port_ = port; } + void set_sync_group_mask(uint8_t mask) { this->sync_group_mask_ = mask; } + void set_blank_on_start(bool blank) { this->blank_on_start_ = blank; } protected: void blank_all_leds_(light::AddressableLight &it); @@ -35,6 +37,8 @@ class WLEDLightEffect : public light::AddressableLightEffect { std::unique_ptr udp_; uint32_t blank_at_{0}; uint32_t dropped_{0}; + uint8_t sync_group_mask_{0}; + bool blank_on_start_{true}; }; } // namespace wled diff --git a/esphome/components/x9c/output.py b/esphome/components/x9c/output.py index 44e9d729b3..4497994982 100644 --- a/esphome/components/x9c/output.py +++ b/esphome/components/x9c/output.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_INC_PIN, CONF_UD_PIN, CONF_INITIAL_VALUE, + CONF_STEP_DELAY, ) CODEOWNERS = ["@EtienneMD"] @@ -26,6 +27,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INITIAL_VALUE, default=1.0): cv.float_range( min=0.01, max=1.0 ), + cv.Optional(CONF_STEP_DELAY, default="1us"): cv.All( + cv.positive_time_period_microseconds, + cv.Range( + min=cv.TimePeriod(microseconds=1), + max=cv.TimePeriod(microseconds=100), + ), + ), } ) ) @@ -44,3 +52,4 @@ async def to_code(config): cg.add(var.set_ud_pin(ud_pin)) cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) + cg.add(var.set_step_delay(config[CONF_STEP_DELAY])) diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp index ff7777e71f..4e7a94266e 100644 --- a/esphome/components/x9c/x9c.cpp +++ b/esphome/components/x9c/x9c.cpp @@ -7,6 +7,10 @@ namespace x9c { static const char *const TAG = "x9c.output"; void X9cOutput::trim_value(int change_amount) { + if (change_amount == 0) { + return; + } + if (change_amount > 0) { // Set change direction this->ud_pin_->digital_write(true); } else { @@ -18,9 +22,9 @@ void X9cOutput::trim_value(int change_amount) { for (int i = 0; i < abs(change_amount); i++) { // Move wiper this->inc_pin_->digital_write(true); - delayMicroseconds(1); + delayMicroseconds(this->step_delay_); this->inc_pin_->digital_write(false); - delayMicroseconds(1); + delayMicroseconds(this->step_delay_); } delayMicroseconds(100); // Let value settle @@ -45,17 +49,17 @@ void X9cOutput::setup() { if (this->initial_value_ <= 0.50) { this->trim_value(-101); // Set min value (beyond 0) - this->trim_value((int) (this->initial_value_ * 100)); + this->trim_value(static_cast(roundf(this->initial_value_ * 100))); } else { this->trim_value(101); // Set max value (beyond 100) - this->trim_value((int) (this->initial_value_ * 100) - 100); + this->trim_value(static_cast(roundf(this->initial_value_ * 100) - 100)); } this->pot_value_ = this->initial_value_; this->write_state(this->initial_value_); } void X9cOutput::write_state(float state) { - this->trim_value((int) ((state - this->pot_value_) * 100)); + this->trim_value(static_cast(roundf((state - this->pot_value_) * 100))); this->pot_value_ = state; } @@ -65,6 +69,7 @@ void X9cOutput::dump_config() { LOG_PIN(" Increment Pin: ", this->inc_pin_); LOG_PIN(" Up/Down Pin: ", this->ud_pin_); ESP_LOGCONFIG(TAG, " Initial Value: %f", this->initial_value_); + ESP_LOGCONFIG(TAG, " Step Delay: %d", this->step_delay_); LOG_FLOAT_OUTPUT(this); } diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h index 924460c841..e7cc29a6cc 100644 --- a/esphome/components/x9c/x9c.h +++ b/esphome/components/x9c/x9c.h @@ -13,6 +13,7 @@ class X9cOutput : public output::FloatOutput, public Component { void set_inc_pin(InternalGPIOPin *pin) { inc_pin_ = pin; } void set_ud_pin(InternalGPIOPin *pin) { ud_pin_ = pin; } void set_initial_value(float initial_value) { initial_value_ = initial_value; } + void set_step_delay(int step_delay) { step_delay_ = step_delay; } void setup() override; void dump_config() override; @@ -26,6 +27,7 @@ class X9cOutput : public output::FloatOutput, public Component { InternalGPIOPin *ud_pin_; float initial_value_; float pot_value_; + int step_delay_; }; } // namespace x9c diff --git a/esphome/components/xgzp68xx/__init__.py b/esphome/components/xgzp68xx/__init__.py new file mode 100644 index 0000000000..122ffaf6ed --- /dev/null +++ b/esphome/components/xgzp68xx/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gcormier"] diff --git a/esphome/components/xgzp68xx/sensor.py b/esphome/components/xgzp68xx/sensor.py new file mode 100644 index 0000000000..3e381aaa61 --- /dev/null +++ b/esphome/components/xgzp68xx/sensor.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_PRESSURE, + CONF_ID, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_PASCAL, + UNIT_CELSIUS, + CONF_TEMPERATURE, + CONF_PRESSURE, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@gcormier"] + +CONF_K_VALUE = "k_value" + +xgzp68xx_ns = cg.esphome_ns.namespace("xgzp68xx") +XGZP68XXComponent = xgzp68xx_ns.class_( + "XGZP68XXComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XGZP68XXComponent), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + 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_K_VALUE, default=4096): cv.uint16_t, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x6D)) +) + + +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) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + + cg.add(var.set_k_value(config[CONF_K_VALUE])) diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp new file mode 100644 index 0000000000..ad6217845d --- /dev/null +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -0,0 +1,93 @@ +#include "xgzp68xx.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace xgzp68xx { + +static const char *const TAG = "xgzp68xx.sensor"; + +static const uint8_t CMD_ADDRESS = 0x30; +static const uint8_t SYSCONFIG_ADDRESS = 0xA5; +static const uint8_t PCONFIG_ADDRESS = 0xA6; +static const uint8_t READ_COMMAND = 0x0A; + +void XGZP68XXComponent::update() { + // Request temp + pressure acquisition + this->write_register(0x30, &READ_COMMAND, 1); + + // Wait 20mS per datasheet + this->set_timeout("measurement", 20, [this]() { + uint8_t data[5]; + uint32_t pressure_raw; + uint16_t temperature_raw; + float pressure_in_pa, temperature; + int success; + + // Read the sensor data + success = this->read_register(0x06, data, 5); + if (success != 0) { + ESP_LOGE(TAG, "Failed to read sensor data! Error code: %i", success); + return; + } + + pressure_raw = encode_uint24(data[0], data[1], data[2]); + 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_); + + // The most significant bit of both pressure and temperature will be 1 to indicate a negative value. + // This is directly from the datasheet, and the calculations below will handle this. + if (pressure_raw > pow(2, 23)) { + // Negative pressure + pressure_in_pa = (pressure_raw - pow(2, 24)) / (float) (this->k_value_); + } else { + // Positive pressure + pressure_in_pa = pressure_raw / (float) (this->k_value_); + } + + if (temperature_raw > pow(2, 15)) { + // Negative temperature + temperature = (float) (temperature_raw - pow(2, 16)) / 256.0f; + } else { + // Positive temperature + temperature = (float) temperature_raw / 256.0f; + } + + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure_in_pa); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + }); // end of set_timeout +} + +void XGZP68XXComponent::setup() { + ESP_LOGD(TAG, "Setting up XGZP68xx..."); + uint8_t config; + + // Display some sample bits to confirm we are talking to the sensor + this->read_register(SYSCONFIG_ADDRESS, &config, 1); + ESP_LOGCONFIG(TAG, "Gain value is %d", (config >> 3) & 0b111); + ESP_LOGCONFIG(TAG, "XGZP68xx started!"); +} + +void XGZP68XXComponent::dump_config() { + ESP_LOGCONFIG(TAG, "XGZP68xx"); + LOG_SENSOR(" ", "Temperature: ", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure: ", this->pressure_sensor_); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, " Connection with XGZP68xx failed!"); + } + LOG_UPDATE_INTERVAL(this); +} + +} // namespace xgzp68xx +} // namespace esphome diff --git a/esphome/components/xgzp68xx/xgzp68xx.h b/esphome/components/xgzp68xx/xgzp68xx.h new file mode 100644 index 0000000000..1bb7304b15 --- /dev/null +++ b/esphome/components/xgzp68xx/xgzp68xx.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace xgzp68xx { + +class XGZP68XXComponent : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { + public: + SUB_SENSOR(temperature) + SUB_SENSOR(pressure) + void set_k_value(uint16_t k_value) { this->k_value_ = k_value; } + + void update() override; + void setup() override; + void dump_config() override; + + protected: + /// Internal method to read the pressure from the component after it has been scheduled. + void read_pressure_(); + uint16_t k_value_; +}; + +} // namespace xgzp68xx +} // namespace esphome diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 95faea0446..85434341cc 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -49,8 +49,8 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ const uint16_t conductivity = encode_uint16(data[1], data[0]); result.conductivity = conductivity; } - // battery, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x100A) && (value_length == 1)) { + // battery / MiaoMiaoce battery, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x100A || value_type == 0x4803) && (value_length == 1)) { result.battery_level = data[0]; } // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % @@ -80,6 +80,17 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ result.has_motion = !idle_time; } else if ((value_type == 0x1018) && (value_length == 1)) { result.is_light = data[0]; + } + // MiaoMiaoce temperature, 4 bytes, float, 0.1 °C + else if ((value_type == 0x4C01) && (value_length == 4)) { + const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]); + float temperature; + std::memcpy(&temperature, &int_number, sizeof(temperature)); + result.temperature = temperature; + } + // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x4C02) && (value_length == 1)) { + result.humidity = data[0]; } else { return false; } @@ -111,7 +122,8 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult } while (payload_length > 3) { - if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00) { + if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00 && + payload[payload_offset + 1] != 0x4C && payload[payload_offset + 1] != 0x48) { ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); break; } @@ -190,6 +202,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } else if (device_uuid == 0x045b) { // rectangular body, e-ink display result.type = XiaomiParseResult::TYPE_LYWSD02; result.name = "LYWSD02"; + } else if (device_uuid == 0x2542) { // rectangular body, e-ink display — with bindkeys + result.type = XiaomiParseResult::TYPE_LYWSD02MMC; + result.name = "LYWSD02MMC"; + if (raw.size() == 19) + result.raw_offset -= 6; } else if (device_uuid == 0x040a) { // Mosquito Repellent Smart Version result.type = XiaomiParseResult::TYPE_WX08ZM; result.name = "WX08ZM"; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index c1086605d1..6978be97f4 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -17,6 +17,7 @@ struct XiaomiParseResult { TYPE_HHCCPOT002, TYPE_LYWSDCGQ, TYPE_LYWSD02, + TYPE_LYWSD02MMC, TYPE_CGG1, TYPE_LYWSD03MMC, TYPE_CGD1, diff --git a/esphome/components/xiaomi_hhccjcy10/__init__.py b/esphome/components/xiaomi_hhccjcy10/__init__.py new file mode 100644 index 0000000000..d47cef13c6 --- /dev/null +++ b/esphome/components/xiaomi_hhccjcy10/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@fariouche"] diff --git a/esphome/components/xiaomi_hhccjcy10/sensor.py b/esphome/components/xiaomi_hhccjcy10/sensor.py new file mode 100644 index 0000000000..4f77fa8103 --- /dev/null +++ b/esphome/components/xiaomi_hhccjcy10/sensor.py @@ -0,0 +1,96 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + CONF_ID, + CONF_MOISTURE, + CONF_ILLUMINANCE, + UNIT_LUX, + CONF_CONDUCTIVITY, + UNIT_MICROSIEMENS_PER_CENTIMETER, + ICON_FLOWER, + DEVICE_CLASS_BATTERY, + CONF_BATTERY_LEVEL, +) + +DEPENDENCIES = ["esp32_ble_tracker"] + +xiaomi_hhccjcy10_ns = cg.esphome_ns.namespace("xiaomi_hhccjcy10") +XiaomiHHCCJCY10 = xiaomi_hhccjcy10_ns.class_( + "XiaomiHHCCJCY10", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY10), + 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_MOISTURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + 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, + ), + } + ) + .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 moisture_config := config.get(CONF_MOISTURE): + sens = await sensor.new_sensor(moisture_config) + cg.add(var.set_moisture(sens)) + if illuminance_config := config.get(CONF_ILLUMINANCE): + sens = await sensor.new_sensor(illuminance_config) + cg.add(var.set_illuminance(sens)) + if conductivity_config := config.get(CONF_CONDUCTIVITY): + sens = await sensor.new_sensor(conductivity_config) + cg.add(var.set_conductivity(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)) diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp new file mode 100644 index 0000000000..45d4cceb52 --- /dev/null +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp @@ -0,0 +1,68 @@ +#include "xiaomi_hhccjcy10.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_hhccjcy10 { + +static const char *const TAG = "xiaomi_hhccjcy10"; + +void XiaomiHHCCJCY10::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi HHCCJCY10"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiHHCCJCY10::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + if (!service_data.uuid.contains(0x50, 0xFD)) { + ESP_LOGVV(TAG, "no tuya service data UUID."); + continue; + } + if (service_data.data.size() != 9) { // tuya alternate between two service data + continue; + } + const uint8_t *data = service_data.data.data(); + + if (this->temperature_ != nullptr) { + const int16_t temperature = encode_uint16(data[1], data[2]); + this->temperature_->publish_state((float) temperature / 10.0f); + } + + if (this->moisture_ != nullptr) + this->moisture_->publish_state(data[0]); + + if (this->conductivity_ != nullptr) { + const uint16_t conductivity = encode_uint16(data[7], data[8]); + this->conductivity_->publish_state((float) conductivity); + } + + if (this->illuminance_ != nullptr) { + const uint32_t illuminance = encode_uint24(data[3], data[4], data[5]); + this->illuminance_->publish_state((float) illuminance); + } + + if (this->battery_level_ != nullptr) + this->battery_level_->publish_state(data[6]); + success = true; + } + + return success; +} + +} // namespace xiaomi_hhccjcy10 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h new file mode 100644 index 0000000000..bc1e580ce4 --- /dev/null +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h @@ -0,0 +1,38 @@ +#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 { +namespace xiaomi_hhccjcy10 { + +class XiaomiHHCCJCY10 : 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; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_moisture(sensor::Sensor *moisture) { this->moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { this->conductivity_ = conductivity; } + void set_illuminance(sensor::Sensor *illuminance) { this->illuminance_ = illuminance; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_hhccjcy10 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02mmc/__init__.py b/esphome/components/xiaomi_lywsd02mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsd02mmc/sensor.py b/esphome/components/xiaomi_lywsd02mmc/sensor.py new file mode 100644 index 0000000000..43784ef698 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/sensor.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_HUMIDITY, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_BATTERY, + CONF_ID, + CONF_BINDKEY, +) + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@juanluss31"] +DEPENDENCIES = ["esp32_ble_tracker"] + +xiaomi_lywsd02mmc_ns = cg.esphome_ns.namespace("xiaomi_lywsd02mmc") +XiaomiLYWSD02MMC = xiaomi_lywsd02mmc_ns.class_( + "XiaomiLYWSD02MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiLYWSD02MMC), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_BINDKEY): cv.bind_key, + 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_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, + ), + } + ) + .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)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_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)) diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp new file mode 100644 index 0000000000..cc122f2264 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp @@ -0,0 +1,73 @@ +#include "xiaomi_lywsd02mmc.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02mmc { + +static const char *const TAG = "xiaomi_lywsd02mmc"; + +void XiaomiLYWSD02MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiLYWSD02MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + 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); + } +} + +} // namespace xiaomi_lywsd02mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h new file mode 100644 index 0000000000..19092aa2a9 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef USE_ESP32 + +namespace esphome { +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); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + 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; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_lywsd02mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py index 8eee10685e..ef8a472d66 100644 --- a/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py +++ b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( DEVICE_CLASS_LIGHT, DEVICE_CLASS_MOTION, CONF_ID, + CONF_BUTTON, ) from esphome.core import TimePeriod @@ -15,8 +16,6 @@ from . import XiaomiRTCGQ02LM DEPENDENCIES = ["xiaomi_rtcgq02lm"] -CONF_BUTTON = "button" - CONFIG_SCHEMA = cv.Schema( { diff --git a/esphome/components/xl9535/__init__.py b/esphome/components/xl9535/__init__.py new file mode 100644 index 0000000000..e6f8b28b46 --- /dev/null +++ b/esphome/components/xl9535/__init__.py @@ -0,0 +1,79 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, +) +from esphome import pins + +CONF_XL9535 = "xl9535" + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@mreditor97"] + +xl9535_ns = cg.esphome_ns.namespace(CONF_XL9535) + +XL9535Component = xl9535_ns.class_("XL9535Component", cg.Component, i2c.I2CDevice) +XL9535GPIOPin = xl9535_ns.class_("XL9535GPIOPin", cg.GPIOPin) + +MULTI_CONF = True +CONFIG_SCHEMA = ( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(XL9535Component)}) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x20)) +) + + +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) + + +def validate_mode(mode): + if not (mode[CONF_INPUT] or mode[CONF_OUTPUT]) or ( + mode[CONF_INPUT] and mode[CONF_OUTPUT] + ): + raise cv.Invalid("Mode must be either a input or a output") + return mode + + +def validate_pin(pin): + if pin in (8, 9): + raise cv.Invalid(f"pin {pin} doesn't exist") + return pin + + +XL9535_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(XL9535GPIOPin), + cv.Required(CONF_XL9535): cv.use_id(XL9535Component), + cv.Required(CONF_NUMBER): cv.All(cv.int_range(min=0, max=17), validate_pin), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_XL9535, XL9535_PIN_SCHEMA) +async def xl9535_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_XL9535]) + + cg.add(var.set_parent(parent)) + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + + return var diff --git a/esphome/components/xl9535/xl9535.cpp b/esphome/components/xl9535/xl9535.cpp new file mode 100644 index 0000000000..93c65a4cb7 --- /dev/null +++ b/esphome/components/xl9535/xl9535.cpp @@ -0,0 +1,122 @@ +#include "xl9535.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace xl9535 { + +static const char *const TAG = "xl9535"; + +void XL9535Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up XL9535..."); + + // Check to see if the device can read from the register + uint8_t port = 0; + if (this->read_register(XL9535_INPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } +} + +void XL9535Component::dump_config() { + ESP_LOGCONFIG(TAG, "XL9535:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with XL9535 failed!"); + } +} + +bool XL9535Component::digital_read(uint8_t pin) { + bool state = false; + uint8_t port = 0; + + if (pin > 7) { + if (this->read_register(XL9535_INPUT_PORT_1_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return state; + } + + state = (port & (1 << (pin - 10))) != 0; + } else { + if (this->read_register(XL9535_INPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return state; + } + + state = (port & (1 << pin)) != 0; + } + + this->status_clear_warning(); + return state; +} + +void XL9535Component::digital_write(uint8_t pin, bool value) { + uint8_t port = 0; + uint8_t register_data = 0; + + if (pin > 7) { + if (this->read_register(XL9535_OUTPUT_PORT_1_REGISTER, ®ister_data, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + register_data = register_data & (~(1 << (pin - 10))); + port = register_data | value << (pin - 10); + + if (this->write_register(XL9535_OUTPUT_PORT_1_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + } else { + if (this->read_register(XL9535_OUTPUT_PORT_0_REGISTER, ®ister_data, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + register_data = register_data & (~(1 << pin)); + port = register_data | value << pin; + + if (this->write_register(XL9535_OUTPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + } + + this->status_clear_warning(); +} + +void XL9535Component::pin_mode(uint8_t pin, gpio::Flags mode) { + uint8_t port = 0; + + if (pin > 7) { + this->read_register(XL9535_CONFIG_PORT_1_REGISTER, &port, 1); + + if (mode == gpio::FLAG_INPUT) { + port = port | (1 << (pin - 10)); + } else if (mode == gpio::FLAG_OUTPUT) { + port = port & (~(1 << (pin - 10))); + } + + this->write_register(XL9535_CONFIG_PORT_1_REGISTER, &port, 1); + } else { + this->read_register(XL9535_CONFIG_PORT_0_REGISTER, &port, 1); + + if (mode == gpio::FLAG_INPUT) { + port = port | (1 << pin); + } else if (mode == gpio::FLAG_OUTPUT) { + port = port & (~(1 << pin)); + } + + this->write_register(XL9535_CONFIG_PORT_0_REGISTER, &port, 1); + } +} + +void XL9535GPIOPin::setup() { this->pin_mode(this->flags_); } + +std::string XL9535GPIOPin::dump_summary() const { return str_snprintf("%u via XL9535", 15, 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_; } +void XL9535GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace xl9535 +} // namespace esphome diff --git a/esphome/components/xl9535/xl9535.h b/esphome/components/xl9535/xl9535.h new file mode 100644 index 0000000000..dd67990fa8 --- /dev/null +++ b/esphome/components/xl9535/xl9535.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace xl9535 { + +enum { + XL9535_INPUT_PORT_0_REGISTER = 0x00, + XL9535_INPUT_PORT_1_REGISTER = 0x01, + XL9535_OUTPUT_PORT_0_REGISTER = 0x02, + XL9535_OUTPUT_PORT_1_REGISTER = 0x03, + XL9535_INVERSION_PORT_0_REGISTER = 0x04, + XL9535_INVERSION_PORT_1_REGISTER = 0x05, + XL9535_CONFIG_PORT_0_REGISTER = 0x06, + XL9535_CONFIG_PORT_1_REGISTER = 0x07, +}; + +class XL9535Component : public Component, public i2c::I2CDevice { + public: + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, gpio::Flags mode); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::IO; } +}; + +class XL9535GPIOPin : public GPIOPin { + public: + void set_parent(XL9535Component *parent) { this->parent_ = parent; } + 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; } + + void setup() override; + std::string dump_summary() const override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + XL9535Component *parent_; + + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace xl9535 +} // namespace esphome diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py deleted file mode 100644 index 5a6cfe4919..0000000000 --- a/esphome/components/xpt2046/binary_sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("Rename this platform component to Touchscreen.") diff --git a/esphome/components/xpt2046/touchscreen.py b/esphome/components/xpt2046/touchscreen.py deleted file mode 100644 index e45b723179..0000000000 --- a/esphome/components/xpt2046/touchscreen.py +++ /dev/null @@ -1,117 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv - -from esphome import pins -from esphome.components import spi, touchscreen -from esphome.const import CONF_ID, CONF_THRESHOLD, CONF_INTERRUPT_PIN - -CODEOWNERS = ["@numo68", "@nielsnl68"] -DEPENDENCIES = ["spi"] - -XPT2046_ns = cg.esphome_ns.namespace("xpt2046") -XPT2046Component = XPT2046_ns.class_( - "XPT2046Component", - touchscreen.Touchscreen, - cg.PollingComponent, - spi.SPIDevice, -) - -CONF_REPORT_INTERVAL = "report_interval" -CONF_CALIBRATION_X_MIN = "calibration_x_min" -CONF_CALIBRATION_X_MAX = "calibration_x_max" -CONF_CALIBRATION_Y_MIN = "calibration_y_min" -CONF_CALIBRATION_Y_MAX = "calibration_y_max" -CONF_SWAP_X_Y = "swap_x_y" - -# obsolete Keys -CONF_DIMENSION_X = "dimension_x" -CONF_DIMENSION_Y = "dimension_y" -CONF_IRQ_PIN = "irq_pin" - - -def validate_xpt2046(config): - if ( - abs( - cv.int_(config[CONF_CALIBRATION_X_MAX]) - - cv.int_(config[CONF_CALIBRATION_X_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration X values difference < 1000") - - if ( - abs( - cv.int_(config[CONF_CALIBRATION_Y_MAX]) - - cv.int_(config[CONF_CALIBRATION_Y_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration Y values difference < 1000") - - return config - - -def report_interval(value): - if value == "never": - return 4294967295 # uint32_t max - return cv.positive_time_period_milliseconds(value) - - -CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(XPT2046Component), - cv.Optional(CONF_INTERRUPT_PIN): cv.All( - pins.internal_gpio_input_pin_schema - ), - cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), - cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval, - cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean, - # obsolete Keys - cv.Optional(CONF_IRQ_PIN): cv.invalid("Rename IRQ_PIN to INTERUPT_PIN"), - cv.Optional(CONF_DIMENSION_X): cv.invalid( - "This key is now obsolete, please remove it" - ), - cv.Optional(CONF_DIMENSION_Y): cv.invalid( - "This key is now obsolete, please remove it" - ), - }, - ) - .extend(cv.polling_component_schema("50ms")) - .extend(spi.spi_device_schema()), -).add_extra(validate_xpt2046) - - -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) - await touchscreen.register_touchscreen(var, config) - - cg.add(var.set_threshold(config[CONF_THRESHOLD])) - cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL])) - cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y])) - cg.add( - var.set_calibration( - config[CONF_CALIBRATION_X_MIN], - config[CONF_CALIBRATION_X_MAX], - config[CONF_CALIBRATION_Y_MIN], - config[CONF_CALIBRATION_Y_MAX], - ) - ) - - if CONF_INTERRUPT_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/__init__.py b/esphome/components/xpt2046/touchscreen/__init__.py new file mode 100644 index 0000000000..d91ae44789 --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/__init__.py @@ -0,0 +1,43 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import spi, touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_THRESHOLD + +CODEOWNERS = ["@numo68", "@nielsnl68"] +DEPENDENCIES = ["spi"] + +XPT2046_ns = cg.esphome_ns.namespace("xpt2046") +XPT2046Component = XPT2046_ns.class_( + "XPT2046Component", + touchscreen.Touchscreen, + spi.SPIDevice, +) + +CONFIG_SCHEMA = cv.All( + touchscreen.touchscreen_schema(calibration_required=True) + .extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XPT2046Component), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), + }, + ) + ) + .extend(spi.spi_device_schema()), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await spi.register_spi_device(var, config) + await touchscreen.register_touchscreen(var, config) + + cg.add(var.set_threshold(config[CONF_THRESHOLD])) + + if CONF_INTERRUPT_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp new file mode 100644 index 0000000000..a4e2b84656 --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -0,0 +1,112 @@ +#include "xpt2046.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace xpt2046 { + +static const char *const TAG = "xpt2046"; + +void XPT2046Component::setup() { + if (this->irq_pin_ != nullptr) { + // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state + // while the channels are read and wiring it as an interrupt is not straightforward and would + // need careful masking. A GPIO poll is cheap so we'll just use that. + + this->irq_pin_->setup(); // INPUT + this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->irq_pin_->setup(); + this->attach_interrupt_(this->irq_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + this->spi_setup(); + this->read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin +} + +void XPT2046Component::update_touches() { + int16_t data[6], x_raw, y_raw, z_raw; + bool touch = false; + + enable(); + + int16_t touch_pressure_1 = this->read_adc_(0xB1 /* touch_pressure_1 */); + int16_t touch_pressure_2 = this->read_adc_(0xC1 /* touch_pressure_2 */); + z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; + ESP_LOGVV(TAG, "Touchscreen Update z = %d", z_raw); + touch = (z_raw >= this->threshold_); + if (touch) { + read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy + data[0] = this->read_adc_(0x91 /* Y */); + data[1] = this->read_adc_(0xD1 /* X */); // make 3 x-y measurements + data[2] = this->read_adc_(0x91 /* Y */); + data[3] = this->read_adc_(0xD1 /* X */); + data[4] = this->read_adc_(0x91 /* Y */); + } + + data[5] = this->read_adc_(0xD0 /* X */); // Last X touch power down + + disable(); + + if (touch) { + x_raw = best_two_avg(data[1], data[3], data[5]); + y_raw = best_two_avg(data[0], data[2], data[4]); + + ESP_LOGD(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); + + this->add_raw_touch_position_(0, x_raw, y_raw, z_raw); + } +} + +void XPT2046Component::dump_config() { + ESP_LOGCONFIG(TAG, "XPT2046:"); + + LOG_PIN(" IRQ Pin: ", this->irq_pin_); + ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_); + ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); + ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); + ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); + + ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); + ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); + ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); + + ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); + + LOG_UPDATE_INTERVAL(this); +} + +// float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } + +int16_t XPT2046Component::best_two_avg(int16_t value1, int16_t value2, int16_t value3) { + int16_t delta_a, delta_b, delta_c; + int16_t reta = 0; + + delta_a = (value1 > value2) ? value1 - value2 : value2 - value1; + delta_b = (value1 > value3) ? value1 - value3 : value3 - value1; + delta_c = (value3 > value2) ? value3 - value2 : value2 - value3; + + if (delta_a <= delta_b && delta_a <= delta_c) { + reta = (value1 + value2) >> 1; + } else if (delta_b <= delta_a && delta_b <= delta_c) { + reta = (value1 + value3) >> 1; + } else { + reta = (value2 + value3) >> 1; + } + + return reta; +} + +int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT + uint8_t data[2]; + + this->write_byte(ctrl); + delay(1); + data[0] = this->read_byte(); + data[1] = this->read_byte(); + + return ((data[0] << 8) | data[1]) >> 3; +} + +} // namespace xpt2046 +} // namespace esphome diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.h b/esphome/components/xpt2046/touchscreen/xpt2046.h new file mode 100644 index 0000000000..a635c08f82 --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/xpt2046.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace xpt2046 { + +using namespace touchscreen; + +class XPT2046Component : public Touchscreen, + public spi::SPIDevice { + public: + /// Set the threshold for the touch detection. + void set_threshold(int16_t threshold) { this->threshold_ = threshold; } + /// Set the pin used to detect the touch. + void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } + + void setup() override; + void dump_config() override; + // float get_setup_priority() const override; + + protected: + static int16_t best_two_avg(int16_t value1, int16_t value2, int16_t value3); + + int16_t read_adc_(uint8_t ctrl); + + void update_touches() override; + + int16_t threshold_; + + InternalGPIOPin *irq_pin_{nullptr}; +}; + +} // namespace xpt2046 +} // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp deleted file mode 100644 index 6c7c55a995..0000000000 --- a/esphome/components/xpt2046/xpt2046.cpp +++ /dev/null @@ -1,206 +0,0 @@ -#include "xpt2046.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -#include - -namespace esphome { -namespace xpt2046 { - -static const char *const TAG = "xpt2046"; - -void XPT2046TouchscreenStore::gpio_intr(XPT2046TouchscreenStore *store) { store->touch = true; } - -void XPT2046Component::setup() { - if (this->irq_pin_ != nullptr) { - // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state - // while the channels are read and wiring it as an interrupt is not straightforward and would - // need careful masking. A GPIO poll is cheap so we'll just use that. - - this->irq_pin_->setup(); // INPUT - this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - this->irq_pin_->setup(); - this->irq_pin_->attach_interrupt(XPT2046TouchscreenStore::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); - } - spi_setup(); - read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin -} - -void XPT2046Component::loop() { - if ((this->irq_pin_ != nullptr) && (this->store_.touch || this->touched)) { - this->store_.touch = false; - check_touch_(); - } -} - -void XPT2046Component::update() { - if (this->irq_pin_ == nullptr) - check_touch_(); -} - -void XPT2046Component::check_touch_() { - int16_t data[6]; - bool touch = false; - uint32_t now = millis(); - - enable(); - - int16_t touch_pressure_1 = read_adc_(0xB1 /* touch_pressure_1 */); - int16_t touch_pressure_2 = read_adc_(0xC1 /* touch_pressure_2 */); - - this->z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; - - touch = (this->z_raw >= this->threshold_); - if (touch) { - read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy - data[0] = read_adc_(0x91 /* Y */); - data[1] = read_adc_(0xD1 /* X */); // make 3 x-y measurements - data[2] = read_adc_(0x91 /* Y */); - data[3] = read_adc_(0xD1 /* X */); - data[4] = read_adc_(0x91 /* Y */); - } - - data[5] = read_adc_(0xD0 /* X */); // Last X touch power down - - disable(); - - if (touch) { - this->x_raw = best_two_avg(data[1], data[3], data[5]); - this->y_raw = best_two_avg(data[0], data[2], data[4]); - - ESP_LOGVV(TAG, "Update [x, y] = [%d, %d], z = %d", this->x_raw, this->y_raw, this->z_raw); - - TouchPoint touchpoint; - - touchpoint.x = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); - touchpoint.y = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); - - if (this->swap_x_y_) { - std::swap(touchpoint.x, touchpoint.y); - } - - if (this->invert_x_) { - touchpoint.x = 0xfff - touchpoint.x; - } - - if (this->invert_y_) { - touchpoint.y = 0xfff - touchpoint.y; - } - - switch (static_cast(this->display_->get_rotation())) { - case ROTATE_0_DEGREES: - break; - case ROTATE_90_DEGREES: - std::swap(touchpoint.x, touchpoint.y); - touchpoint.y = 0xfff - touchpoint.y; - break; - case ROTATE_180_DEGREES: - touchpoint.x = 0xfff - touchpoint.x; - touchpoint.y = 0xfff - touchpoint.y; - break; - case ROTATE_270_DEGREES: - std::swap(touchpoint.x, touchpoint.y); - touchpoint.x = 0xfff - touchpoint.x; - break; - } - - touchpoint.x = (int16_t) ((int) touchpoint.x * this->display_->get_width() / 0xfff); - touchpoint.y = (int16_t) ((int) touchpoint.y * this->display_->get_height() / 0xfff); - - if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) { - ESP_LOGV(TAG, "Touching at [%03X, %03X] => [%3d, %3d]", this->x_raw, this->y_raw, touchpoint.x, touchpoint.y); - - this->defer([this, touchpoint]() { this->send_touch_(touchpoint); }); - - this->x = touchpoint.x; - this->y = touchpoint.y; - this->touched = true; - this->last_pos_ms_ = now; - } - } - - if (!touch && this->touched) { - this->x_raw = this->y_raw = this->z_raw = 0; - ESP_LOGV(TAG, "Released [%d, %d]", this->x, this->y); - this->touched = false; - for (auto *listener : this->touch_listeners_) - listener->release(); - } -} - -void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { // NOLINT - this->x_raw_min_ = std::min(x_min, x_max); - this->x_raw_max_ = std::max(x_min, x_max); - this->y_raw_min_ = std::min(y_min, y_max); - this->y_raw_max_ = std::max(y_min, y_max); - this->invert_x_ = (x_min > x_max); - this->invert_y_ = (y_min > y_max); -} - -void XPT2046Component::dump_config() { - ESP_LOGCONFIG(TAG, "XPT2046:"); - - LOG_PIN(" IRQ Pin: ", this->irq_pin_); - ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_); - ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); - ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); - ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); - - ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); - ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); - ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); - - ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); - ESP_LOGCONFIG(TAG, " Report interval: %u", this->report_millis_); - - LOG_UPDATE_INTERVAL(this); -} - -float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } - -int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) { // NOLINT - int16_t da, db, dc; // NOLINT - int16_t reta = 0; - - da = (x > y) ? x - y : y - x; - db = (x > z) ? x - z : z - x; - dc = (z > y) ? z - y : y - z; - - if (da <= db && da <= dc) { - reta = (x + y) >> 1; - } else if (db <= da && db <= dc) { - reta = (x + z) >> 1; - } else { - reta = (y + z) >> 1; - } - - return reta; -} - -int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_val) { - int16_t ret; - - if (val <= min_val) { - ret = 0; - } else if (val >= max_val) { - ret = 0xfff; - } else { - ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); - } - - return ret; -} - -int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT - uint8_t data[2]; - - write_byte(ctrl); - delay(1); - data[0] = read_byte(); - data[1] = read_byte(); - - return ((data[0] << 8) | data[1]) >> 3; -} - -} // namespace xpt2046 -} // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.h b/esphome/components/xpt2046/xpt2046.h deleted file mode 100644 index e7d9caba21..0000000000 --- a/esphome/components/xpt2046/xpt2046.h +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/spi/spi.h" -#include "esphome/components/touchscreen/touchscreen.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace xpt2046 { - -using namespace touchscreen; - -struct XPT2046TouchscreenStore { - volatile bool touch; - static void gpio_intr(XPT2046TouchscreenStore *store); -}; - -class XPT2046Component : public Touchscreen, - public PollingComponent, - public spi::SPIDevice { - public: - /// Set the logical touch screen dimensions. - void set_dimensions(int16_t x, int16_t y) { - this->display_width_ = x; - this->display_height_ = y; - } - /// Set the coordinates for the touch screen edges. - void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max); - /// If true the x and y axes will be swapped - void set_swap_x_y(bool val) { this->swap_x_y_ = val; } - - /// Set the interval to report the touch point perodically. - void set_report_interval(uint32_t interval) { this->report_millis_ = interval; } - uint32_t get_report_interval() { return this->report_millis_; } - - /// Set the threshold for the touch detection. - void set_threshold(int16_t threshold) { this->threshold_ = threshold; } - /// Set the pin used to detect the touch. - void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } - - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - - /** Detect the touch if the irq pin is specified. - * - * If the touch is detected and the component does not already know about it - * the update() is called immediately. If the irq pin is not specified - * the loop() is a no-op. - */ - void loop() override; - - /** Read and process the values from the hardware. - * - * Read the raw x, y and touch pressure values from the chip, detect the touch, - * and if touched, transform to the user x and y coordinates. If the state has - * changed or if the value should be reported again due to the - * report interval, run the action and inform the virtual buttons. - */ - void update() override; - - /**@{*/ - /** Coordinates of the touch position. - * - * The values are set immediately before the on_state action with touched == true - * is triggered. The action with touched == false sends the coordinates of the last - * reported touch. - */ - int16_t x{0}, y{0}; - /**@}*/ - - /// True if the component currently detects the touch - bool touched{false}; - - /**@{*/ - /** Raw sensor values of the coordinates and the pressure. - * - * The values are set each time the update() method is called. - */ - int16_t x_raw{0}, y_raw{0}, z_raw{0}; - /**@}*/ - - protected: - static int16_t best_two_avg(int16_t x, int16_t y, int16_t z); - static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val); - - int16_t read_adc_(uint8_t ctrl); - void check_touch_(); - - int16_t threshold_; - int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_; - - bool invert_x_, invert_y_; - bool swap_x_y_; - - uint32_t report_millis_; - uint32_t last_pos_ms_{0}; - - InternalGPIOPin *irq_pin_{nullptr}; - XPT2046TouchscreenStore store_; -}; - -} // namespace xpt2046 -} // namespace esphome diff --git a/esphome/components/zhlt01/__init__.py b/esphome/components/zhlt01/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/zhlt01/climate.py b/esphome/components/zhlt01/climate.py new file mode 100644 index 0000000000..1451f8ec69 --- /dev/null +++ b/esphome/components/zhlt01/climate.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] +CODEOWNERS = ["@cfeenstra1024"] + +zhlt01_ns = cg.esphome_ns.namespace("zhlt01") +ZHLT01Climate = zhlt01_ns.class_("ZHLT01Climate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(ZHLT01Climate)} +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/zhlt01/zhlt01.cpp b/esphome/components/zhlt01/zhlt01.cpp new file mode 100644 index 0000000000..36d1737c14 --- /dev/null +++ b/esphome/components/zhlt01/zhlt01.cpp @@ -0,0 +1,238 @@ +#include "zhlt01.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace zhlt01 { + +static const char *const TAG = "zhlt01.climate"; + +void ZHLT01Climate::transmit_state() { + uint8_t ir_message[12] = {0}; + + // Byte 1 : Timer + ir_message[1] = 0x00; // Timer off + + // Byte 3 : Turbo mode + if (this->preset.value() == climate::CLIMATE_PRESET_BOOST) { + ir_message[3] = AC1_FAN_TURBO; + } + + // Byte 5 : Last pressed button + ir_message[5] = 0x00; // fixed as power button + + // Byte 7 : Power | Swing | Fan + // -- Power + if (this->mode == climate::CLIMATE_MODE_OFF) { + ir_message[7] = AC1_POWER_OFF; + } else { + ir_message[7] = AC1_POWER_ON; + } + + // -- Swing + switch (this->swing_mode) { + case climate::CLIMATE_SWING_OFF: + ir_message[7] |= AC1_HDIR_FIXED | AC1_VDIR_FIXED; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + ir_message[7] |= AC1_HDIR_SWING | AC1_VDIR_FIXED; + break; + case climate::CLIMATE_SWING_VERTICAL: + ir_message[7] |= AC1_HDIR_FIXED | AC1_VDIR_SWING; + break; + case climate::CLIMATE_SWING_BOTH: + ir_message[7] |= AC1_HDIR_SWING | AC1_VDIR_SWING; + break; + default: + break; + } + + // -- Fan + switch (this->preset.value()) { + case climate::CLIMATE_PRESET_BOOST: + ir_message[7] |= AC1_FAN3; + break; + case climate::CLIMATE_PRESET_SLEEP: + ir_message[7] |= AC1_FAN_SILENT; + break; + default: + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + ir_message[7] |= AC1_FAN1; + break; + case climate::CLIMATE_FAN_MEDIUM: + ir_message[7] |= AC1_FAN2; + break; + case climate::CLIMATE_FAN_HIGH: + ir_message[7] |= AC1_FAN3; + break; + case climate::CLIMATE_FAN_AUTO: + ir_message[7] |= AC1_FAN_AUTO; + break; + default: + break; + } + } + + // Byte 9 : AC Mode | Temperature + // -- AC Mode + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: + ir_message[9] = AC1_MODE_AUTO; + break; + case climate::CLIMATE_MODE_COOL: + ir_message[9] = AC1_MODE_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + ir_message[9] = AC1_MODE_HEAT; + break; + case climate::CLIMATE_MODE_DRY: + ir_message[9] = AC1_MODE_DRY; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + ir_message[9] = AC1_MODE_FAN; + break; + default: + break; + } + + // -- Temperature + ir_message[9] |= (uint8_t) (this->target_temperature - 16.0f); + + // Byte 11 : Remote control ID + ir_message[11] = 0xD5; + + // Set checksum bytes + for (int i = 0; i < 12; i += 2) { + ir_message[i] = ~ir_message[i + 1]; + } + + // Send the code + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + + data->set_carrier_frequency(38000); // 38 kHz PWM + + // Header + data->mark(AC1_HDR_MARK); + data->space(AC1_HDR_SPACE); + + // Data + for (uint8_t i : ir_message) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(AC1_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? AC1_ONE_SPACE : AC1_ZERO_SPACE); + } + } + + // Footer + data->mark(AC1_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +bool ZHLT01Climate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(AC1_HDR_MARK, AC1_HDR_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + // Decode IR message + uint8_t ir_message[12] = {0}; + // Read all bytes + for (int i = 0; i < 12; i++) { + // Read bit + for (int j = 0; j < 8; j++) { + if (data.expect_item(AC1_BIT_MARK, AC1_ONE_SPACE)) { + ir_message[i] |= 1 << j; + } else if (!data.expect_item(AC1_BIT_MARK, AC1_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + ESP_LOGVV(TAG, "Byte %d %02X", i, ir_message[i]); + } + + // Validate footer + if (!data.expect_mark(AC1_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + // Validate checksum + for (int i = 0; i < 12; i += 2) { + if (ir_message[i] != (uint8_t) (~ir_message[i + 1])) { + ESP_LOGV(TAG, "Byte %d checksum incorrect (%02X != %02X)", i, ir_message[i], (uint8_t) (~ir_message[i + 1])); + return false; + } + } + + // Validate remote control ID + if (ir_message[11] != 0xD5) { + ESP_LOGV(TAG, "Invalid remote control ID"); + return false; + } + + // All is good to go + + if ((ir_message[7] & AC1_POWER_ON) == 0) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + // Vertical swing + if ((ir_message[7] & 0x0C) == AC1_VDIR_FIXED) { + if ((ir_message[7] & 0x10) == AC1_HDIR_FIXED) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } else { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } + } else { + if ((ir_message[7] & 0x10) == AC1_HDIR_FIXED) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_BOTH; + } + } + + // Preset + Fan speed + if ((ir_message[3] & AC1_FAN_TURBO) == AC1_FAN_TURBO) { + this->preset = climate::CLIMATE_PRESET_BOOST; + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } else if ((ir_message[7] & 0xE1) == AC1_FAN_SILENT) { + this->preset = climate::CLIMATE_PRESET_SLEEP; + this->fan_mode = climate::CLIMATE_FAN_LOW; + } else if ((ir_message[7] & 0xE1) == AC1_FAN_AUTO) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + } else if ((ir_message[7] & 0xE1) == AC1_FAN1) { + this->fan_mode = climate::CLIMATE_FAN_LOW; + } else if ((ir_message[7] & 0xE1) == AC1_FAN2) { + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + } else if ((ir_message[7] & 0xE1) == AC1_FAN3) { + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } + + // AC Mode + if ((ir_message[9] & 0xE0) == AC1_MODE_COOL) { + this->mode = climate::CLIMATE_MODE_COOL; + } else if ((ir_message[9] & 0xE0) == AC1_MODE_HEAT) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else if ((ir_message[9] & 0xE0) == AC1_MODE_DRY) { + this->mode = climate::CLIMATE_MODE_DRY; + } else if ((ir_message[9] & 0xE0) == AC1_MODE_FAN) { + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + } else { + this->mode = climate::CLIMATE_MODE_AUTO; + } + + // Taregt Temperature + this->target_temperature = (ir_message[9] & 0x1F) + 16.0f; + } + + this->publish_state(); + return true; +} + +} // namespace zhlt01 +} // namespace esphome diff --git a/esphome/components/zhlt01/zhlt01.h b/esphome/components/zhlt01/zhlt01.h new file mode 100644 index 0000000000..4413be2835 --- /dev/null +++ b/esphome/components/zhlt01/zhlt01.h @@ -0,0 +1,167 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +/*********************************************************************************** + * SOURCE + *********************************************************************************** + * The IR codes and the functional description below were taken from + * 'arduino-heatpumpir/ZHLT01HeatpumpIR.h' as can be found on GitHub + * https://github.com/ToniA/arduino-heatpumpir/blob/master/ZHLT01HeatpumpIR.h + * + ************************************************************************************ + * Airconditional remote control encoder for: + * + * ZH/LT-01 Remote control https://www.google.com/search?q=zh/lt-01 + * + * The ZH/LT-01 remote control is used for many locally branded Split + * airconditioners, so it is better to name this protocol by the name of the + * REMOTE rather then the name of the Airconditioner. For this project I used + * a 2014 model Eurom-airconditioner, which is Dutch-branded and sold in + * the Netherlands at Hornbach. + * + * For airco-brands: + * Eurom + * Chigo + * Tristar + * Tecnomaster + * Elgin + * Geant + * Tekno + * Topair + * Proma + * Sumikura + * JBS + * Turbo Air + * Nakatomy + * Celestial Air + * Ager + * Blueway + * Airlux + * Etc. + * + *********************************************************************************** + * SUMMARY FUNCTIONAL DESCRIPTION + *********************************************************************************** + * The remote sends a 12 Byte message which contains all possible settings every + * time. + * + * Byte 11 (and 10) contain the remote control identifier and are always 0xD5 and + * 0x2A respectively for the ZH/LT-01 remote control. + * Every UNeven Byte (01,03,05,07 and 09) holds command data + * Every EVEN Byte (00,02,04,06,08 and 10) holds a checksum of the corresponding + * command-, or identifier-byte by _inverting_ the bits, for example: + * + * The identifier byte[11] = 0xD5 = B1101 0101 + * The checksum byte[10] = 0x2A = B0010 1010 + * + * So, you can check the message by: + * - inverting the bits of the checksum byte with the corresponding command-, or + * identifier byte, they should be the same, or + * - Summing up the checksum byte and the corresponding command-, or identifier byte, + * they should always add up to 0xFF = B11111111 = 255 + * + * Control bytes: + * [01] - Timer (1-24 hours, Off) + * Time is hardcoded to OFF + * + * [03] - LAMP ON/OFF, TURBO ON/OFF, HOLD ON/OFF + * Lamp and Hold are hardcoded to OFF + * Turbo is used for the BOOST preset + * + * [05] - Indicates which button the user _pressed_ on the remote control + * Hardcoded to POWER-button + * + * [07] - POWER ON/OFF, FAN AUTO/3/2/1, SLEEP ON/OFF, AIRFLOW ON/OFF, + * VERTICAL SWING/WIND/FIXED + * SLEEP is used for preset SLEEP + * Vertical Swing supports Fixed, Swing and "Wind". The Wind option + * is ignored in this implementation + * + * [09] - MODE AUTO/COOL/VENT/DRY/HEAT, TEMPERATURE (16 - 32°C) + * + ***********************************************************************************/ + +namespace esphome { +namespace zhlt01 { + +/******************************************************************************** + * TIMINGS + * Space: Not used + * Header Mark: 6100 us + * Header Space: 7400 us + * Bit Mark: 500 us + * Zero Space: 600 us + * One Space: 1800 us + * + * Note : These timings are slightly different than those of ZHLT01HeatpumpIR + * The values below were measured by taking the average of 2 different + * remote controls each sending 10 commands + *******************************************************************************/ +static const uint32_t AC1_HDR_MARK = 6100; +static const uint32_t AC1_HDR_SPACE = 7400; +static const uint32_t AC1_BIT_MARK = 500; +static const uint32_t AC1_ZERO_SPACE = 600; +static const uint32_t AC1_ONE_SPACE = 1800; + +/******************************************************************************** + * + * ZHLT01 codes + * + *******************************************************************************/ + +// Power +static const uint8_t AC1_POWER_OFF = 0x00; +static const uint8_t AC1_POWER_ON = 0x02; + +// Operating Modes +static const uint8_t AC1_MODE_AUTO = 0x00; +static const uint8_t AC1_MODE_COOL = 0x20; +static const uint8_t AC1_MODE_DRY = 0x40; +static const uint8_t AC1_MODE_FAN = 0x60; +static const uint8_t AC1_MODE_HEAT = 0x80; + +// Fan control +static const uint8_t AC1_FAN_AUTO = 0x00; +static const uint8_t AC1_FAN_SILENT = 0x01; +static const uint8_t AC1_FAN1 = 0x60; +static const uint8_t AC1_FAN2 = 0x40; +static const uint8_t AC1_FAN3 = 0x20; +static const uint8_t AC1_FAN_TURBO = 0x08; + +// Vertical Swing +static const uint8_t AC1_VDIR_WIND = 0x00; // "Natural Wind", ignore +static const uint8_t AC1_VDIR_SWING = 0x04; // Swing +static const uint8_t AC1_VDIR_FIXED = 0x08; // Fixed + +// Horizontal Swing +static const uint8_t AC1_HDIR_SWING = 0x00; // Swing +static const uint8_t AC1_HDIR_FIXED = 0x10; // Fixed + +// Temperature range +static const float AC1_TEMP_MIN = 16.0f; +static const float AC1_TEMP_MAX = 32.0f; +static const float AC1_TEMP_INC = 1.0f; + +class ZHLT01Climate : public climate_ir::ClimateIR { + public: + ZHLT01Climate() + : climate_ir::ClimateIR( + AC1_TEMP_MIN, AC1_TEMP_MAX, AC1_TEMP_INC, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, + climate::CLIMATE_SWING_BOTH}, + {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_BOOST}) {} + + void setup() override { climate_ir::ClimateIR::setup(); } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; +}; + +} // namespace zhlt01 +} // namespace esphome diff --git a/esphome/components/zio_ultrasonic/__init__.py b/esphome/components/zio_ultrasonic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/zio_ultrasonic/sensor.py b/esphome/components/zio_ultrasonic/sensor.py new file mode 100644 index 0000000000..c5eed14e64 --- /dev/null +++ b/esphome/components/zio_ultrasonic/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_DISTANCE, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@kahrendt"] + +zio_ultrasonic_ns = cg.esphome_ns.namespace("zio_ultrasonic") + +ZioUltrasonicComponent = zio_ultrasonic_ns.class_( + "ZioUltrasonicComponent", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + ZioUltrasonicComponent, + unit_of_measurement="mm", + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x00)) +) + + +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/zio_ultrasonic/zio_ultrasonic.cpp b/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp new file mode 100644 index 0000000000..565bbe9b4f --- /dev/null +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp @@ -0,0 +1,31 @@ + +#include "zio_ultrasonic.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace zio_ultrasonic { + +static const char *const TAG = "zio_ultrasonic"; + +void ZioUltrasonicComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Zio Ultrasonic Sensor:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Sensor:", this); +} + +void ZioUltrasonicComponent::update() { + uint16_t distance; + + // Read an unsigned two byte integerfrom register 0x01 which gives distance in mm + if (!this->read_byte_16(0x01, &distance)) { + ESP_LOGE(TAG, "Error reading data from Zio Ultrasonic"); + this->publish_state(NAN); + } else { + this->publish_state(distance); + } +} + +} // namespace zio_ultrasonic +} // namespace esphome diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.h b/esphome/components/zio_ultrasonic/zio_ultrasonic.h new file mode 100644 index 0000000000..84c8d44c65 --- /dev/null +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +static const char *const TAG = "Zio Ultrasonic"; + +namespace esphome { +namespace zio_ultrasonic { + +class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, public sensor::Sensor { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + + void dump_config() override; + + void update() override; +}; + +} // namespace zio_ultrasonic +} // namespace esphome diff --git a/esphome/config.py b/esphome/config.py index b04de020e0..7d48569d2d 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,43 +1,54 @@ +from __future__ import annotations + import abc +from contextlib import contextmanager +import contextvars import functools import heapq import logging import re - -from typing import Optional, Union - -from contextlib import contextmanager +from typing import Any, Union import voluptuous as vol -from esphome import core, yaml_util, loader -import esphome.core.config as core_config +from esphome import core, loader, pins, yaml_util +from esphome.config_helpers import Extend, Remove +import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, - CONF_ID, - CONF_PLATFORM, - CONF_PACKAGES, - CONF_SUBSTITUTIONS, CONF_EXTERNAL_COMPONENTS, + CONF_ID, + CONF_PACKAGES, + CONF_PLATFORM, + CONF_SUBSTITUTIONS, TARGET_PLATFORMS, ) -from esphome.core import CORE, EsphomeError -from esphome.helpers import indent -from esphome.util import safe_print, OrderedDict - -from esphome.config_helpers import Extend -from esphome.loader import get_component, get_platform, ComponentManifest -from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue -from esphome.voluptuous_schema import ExtraKeysInvalid -from esphome.log import color, Fore +from esphome.core import CORE, DocumentRange, EsphomeError +import esphome.core.config as core_config import esphome.final_validate as fv -import esphome.config_validation as cv -from esphome.types import ConfigType, ConfigPathType, ConfigFragmentType +from esphome.helpers import indent +from esphome.loader import ComponentManifest, get_component, get_platform +from esphome.log import Fore, color +from esphome.types import ConfigFragmentType, ConfigType +from esphome.util import OrderedDict, safe_print +from esphome.voluptuous_schema import ExtraKeysInvalid +from esphome.yaml_util import ESPForceValue, ESPHomeDataBase, is_secret _LOGGER = logging.getLogger(__name__) def iter_components(config): + for domain, conf in config.items(): + component = get_component(domain) + yield domain, component + if component.is_platform_component: + for p_config in conf: + p_name = f"{domain}.{p_config[CONF_PLATFORM]}" + platform = get_platform(domain, p_config[CONF_PLATFORM]) + yield p_name, platform + + +def iter_component_configs(config): for domain, conf in config.items(): component = get_component(domain) if component.multi_conf: @@ -53,6 +64,7 @@ def iter_components(config): ConfigPath = list[Union[str, int]] +path_context = contextvars.ContextVar("Config path") def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool: @@ -63,7 +75,7 @@ def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool: @functools.total_ordering class _ValidationStepTask: - def __init__(self, priority: float, id_number: int, step: "ConfigValidationStep"): + def __init__(self, priority: float, id_number: int, step: ConfigValidationStep): self.priority = priority self.id_number = id_number self.step = step @@ -109,10 +121,15 @@ class Config(OrderedDict, fv.FinalValidateConfig): last_root = max( i for i, v in enumerate(error.path) if v is cv.ROOT_CONFIG_PATH ) - error.path = error.path[last_root + 1 :] + # can't change the path so re-create the error + error = vol.Invalid( + message=error.error_message, + path=error.path[last_root + 1 :], + error_type=error.error_type, + ) self.errors.append(error) - def add_validation_step(self, step: "ConfigValidationStep"): + def add_validation_step(self, step: ConfigValidationStep): id_num = self._validation_tasks_id self._validation_tasks_id += 1 heapq.heappush( @@ -120,7 +137,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): ) def run_validation_steps(self): - while self._validation_tasks: + while self._validation_tasks and not self.errors: task = heapq.heappop(self._validation_tasks) task.step.run(self) @@ -129,6 +146,8 @@ class Config(OrderedDict, fv.FinalValidateConfig): path = path or [] try: yield + except cv.FinalExternalInvalid as e: + self.add_error(e) except vol.Invalid as e: e.prepend(path) self.add_error(e) @@ -154,7 +173,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): conf = conf[key] conf[path[-1]] = value - def get_error_for_path(self, path: ConfigPath) -> Optional[vol.Invalid]: + def get_error_for_path(self, path: ConfigPath) -> vol.Invalid | None: for err in self.errors: if self.get_deepest_path(err.path) == path: self.errors.remove(err) @@ -163,7 +182,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): def get_deepest_document_range_for_path( self, path: ConfigPath, get_key: bool = False - ) -> Optional[ESPHomeDataBase]: + ) -> DocumentRange | None: data = self doc_range = None for index, path_item in enumerate(path): @@ -192,7 +211,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): return doc_range def get_nested_item( - self, path: ConfigPathType, raise_error: bool = False + self, path: ConfigPath, raise_error: bool = False ) -> ConfigFragmentType: data = self for item_index in path: @@ -223,7 +242,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): return path raise KeyError(f"ID {id} not found in configuration") - def get_config_for_path(self, path: ConfigPathType) -> ConfigFragmentType: + def get_config_for_path(self, path: ConfigPath) -> ConfigFragmentType: return self.get_nested_item(path, raise_error=True) @property @@ -274,8 +293,7 @@ class ConfigValidationStep(abc.ABC): priority: float = 0.0 @abc.abstractmethod - def run(self, result: Config) -> None: - ... + def run(self, result: Config) -> None: ... # noqa: E704 class LoadValidationStep(ConfigValidationStep): @@ -296,8 +314,14 @@ class LoadValidationStep(ConfigValidationStep): # Ignore top-level keys starting with a dot return result.add_output_path([self.domain], self.domain) - result[self.domain] = self.conf component = get_component(self.domain) + if ( + component is not None + and component.multi_conf_no_default + and isinstance(self.conf, core.AutoLoad) + ): + self.conf = [] + result[self.domain] = self.conf path = [self.domain] if component is None: result.add_str_error(f"Component not found: {self.domain}", path) @@ -309,10 +333,11 @@ class LoadValidationStep(ConfigValidationStep): if load not in result: result.add_validation_step(AutoLoadValidationStep(load)) + result.add_validation_step( + MetadataValidationStep([self.domain], self.domain, self.conf, component) + ) + if not component.is_platform_component: - result.add_validation_step( - MetadataValidationStep([self.domain], self.domain, self.conf, component) - ) return # This is a platform component, proceed to reading platform entries @@ -343,7 +368,16 @@ class LoadValidationStep(ConfigValidationStep): path + [CONF_ID], ) continue - result.add_str_error("No platform specified! See 'platform' key.", path) + if isinstance(p_id, Remove): + result.add_str_error( + f"Source for removal of ID '{p_id.value}' was not found.", + path + [CONF_ID], + ) + continue + result.add_str_error( + f"'{self.domain}' requires a 'platform' key but it was not specified.", + path, + ) continue # Remove temp output path and construct new one result.remove_output_path(path, p_domain) @@ -411,13 +445,35 @@ class MetadataValidationStep(ConfigValidationStep): def run(self, result: Config) -> None: if self.conf is None: - result[self.domain] = self.conf = {} + if self.comp.multi_conf and self.comp.multi_conf_no_default: + result[self.domain] = self.conf = [] + else: + result[self.domain] = self.conf = {} success = True for dependency in self.comp.dependencies: - if dependency not in result: + dependency_parts = dependency.split(".") + if len(dependency_parts) > 2: result.add_str_error( - f"Component {self.domain} requires component {dependency}", + "Dependencies must be specified as a single component or in component.platform format only", + self.path, + ) + return + component_dep = dependency_parts[0] + platform_dep = dependency_parts[-1] + if component_dep not in result: + result.add_str_error( + f"Component {self.domain} requires component {component_dep}", + self.path, + ) + success = False + elif component_dep != platform_dep and ( + not isinstance(platform_list := result.get(component_dep), list) + or not any(CONF_PLATFORM in p for p in platform_list) + or not any(p[CONF_PLATFORM] == platform_dep for p in platform_list) + ): + result.add_str_error( + f"Component {self.domain} requires 'platform: {platform_dep}' in component '{component_dep}'", self.path, ) success = False @@ -487,8 +543,7 @@ class SchemaValidationStep(ConfigValidationStep): self.comp = comp def run(self, result: Config) -> None: - if self.comp.config_schema is None: - return + token = path_context.set(self.path) with result.catch_error(self.path): if self.comp.is_platform: # Remove 'platform' key for validation @@ -502,11 +557,12 @@ class SchemaValidationStep(ConfigValidationStep): validated["platform"] = platform_val validated.move_to_end("platform", last=False) result.set_by_path(self.path, validated) - else: + elif self.comp.config_schema is not None: schema = cv.Schema(self.comp.config_schema) validated = schema(self.conf) result.set_by_path(self.path, validated) + path_context.reset(token) result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) @@ -630,6 +686,35 @@ class IDPassValidationStep(ConfigValidationStep): ) +class RemoveReferenceValidationStep(ConfigValidationStep): + """ + Make sure all !remove references have been removed from the config. + Any left overs mean the merge step couldn't find corresponding previously existing id/key + """ + + def run(self, result: Config) -> None: + if result.errors: + # If result already has errors, skip this step + return + + def recursive_check_remove_tag(config: Config, path: ConfigPath = None): + path = path or [] + + if isinstance(config, Remove): + result.add_str_error( + f"Source for removal at '{'->'.join([str(p) for p in path])}' was not found.", + path, + ) + elif isinstance(config, list): + for i, item in enumerate(config): + recursive_check_remove_tag(item, path + [i]) + elif isinstance(config, dict): + for key, value in config.items(): + recursive_check_remove_tag(value, path + [key]) + + recursive_check_remove_tag(result) + + class FinalValidateValidationStep(ConfigValidationStep): """Run final_validate_schema for all components.""" @@ -645,19 +730,34 @@ class FinalValidateValidationStep(ConfigValidationStep): # If result already has errors, skip this step return - if self.comp.final_validate_schema is None: - return - token = fv.full_config.set(result) conf = result.get_nested_item(self.path) with result.catch_error(self.path): - self.comp.final_validate_schema(conf) + if self.comp.final_validate_schema is not None: + self.comp.final_validate_schema(conf) fv.full_config.reset(token) -def validate_config(config, command_line_substitutions) -> Config: +class PinUseValidationCheck(ConfigValidationStep): + """Check for pin reuse""" + + priority = -30 # Should happen after component final validations + + def __init__(self) -> None: + pass + + def run(self, result: Config) -> None: + if result.errors: + # If result already has errors, skip this step + return + pins.PIN_SCHEMA_REGISTRY.final_validate(result) + + +def validate_config( + config: dict[str, Any], command_line_substitutions: dict[str, Any] +) -> Config: result = Config() loader.clear_component_meta_finders() @@ -678,11 +778,11 @@ def validate_config(config, command_line_substitutions) -> Config: CORE.raw_config = config # 1. Load substitutions - if CONF_SUBSTITUTIONS in config: + if CONF_SUBSTITUTIONS in config or command_line_substitutions: from esphome.components import substitutions result[CONF_SUBSTITUTIONS] = { - **config[CONF_SUBSTITUTIONS], + **(config.get(CONF_SUBSTITUTIONS) or {}), **command_line_substitutions, } result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) @@ -752,6 +852,9 @@ def validate_config(config, command_line_substitutions) -> Config: for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) + result.add_validation_step(PinUseValidationCheck()) + + result.add_validation_step(RemoveReferenceValidationStep()) result.run_validation_steps() @@ -780,6 +883,9 @@ def _get_parent_name(path, config): # Sub-item break return domain + # When processing a list, skip back over the index + while len(path) > 1 and isinstance(path[-1], int): + path = path[:-1] return path[-1] @@ -818,24 +924,23 @@ class InvalidYAMLError(EsphomeError): self.base_exc = base_exc -def _load_config(command_line_substitutions): +def _load_config(command_line_substitutions: dict[str, Any]) -> Config: + """Load the configuration file.""" try: config = yaml_util.load_yaml(CORE.config_path) except EsphomeError as e: raise InvalidYAMLError(e) from e try: - result = validate_config(config, command_line_substitutions) + return validate_config(config, command_line_substitutions) except EsphomeError: raise except Exception: _LOGGER.error("Unexpected exception while reading configuration:") raise - return result - -def load_config(command_line_substitutions): +def load_config(command_line_substitutions: dict[str, Any]) -> Config: try: return _load_config(command_line_substitutions) except vol.Invalid as err: @@ -1002,11 +1107,18 @@ def read_config(command_line_substitutions): if errline: errstr += f" {errline}" safe_print(errstr) - safe_print(indent(dump_dict(res, path)[0])) + split_dump = dump_dict(res, path)[0].splitlines() + # find the last error message + i = len(split_dump) - 1 + while i > 10 and "\033[" not in split_dump[i]: + i = i - 1 + # discard lines more than 4 beyond the last error + i = min(i + 4, len(split_dump)) + safe_print(indent("\n".join(split_dump[:i]))) for err in res.errors: safe_print(color(Fore.BOLD_RED, err.msg)) safe_print("") return None - return OrderedDict(res) + return res diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index e1d63775bb..54242bc259 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,9 +1,4 @@ -import json -import os - from esphome.const import CONF_ID -from esphome.core import CORE -from esphome.helpers import read_file class Extend: @@ -13,6 +8,9 @@ class Extend: def __str__(self): return f"!extend {self.value}" + def __repr__(self): + return f"Extend({self.value})" + def __eq__(self, b): """ Check if two Extend objects contain the same ID. @@ -22,23 +20,23 @@ class Extend: return isinstance(b, Extend) and self.value == b.value -def read_config_file(path: str) -> str: - if CORE.vscode and ( - not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) - ): - print( - json.dumps( - { - "type": "read_file", - "path": path, - } - ) - ) - data = json.loads(input()) - assert data["type"] == "file_response" - return data["content"] +class Remove: + def __init__(self, value=None): + self.value = value - return read_file(path) + def __str__(self): + return f"!remove {self.value}" + + def __repr__(self): + return f"Remove({self.value})" + + def __eq__(self, b): + """ + Check if two Remove objects contain the same ID. + + Only used in unit tests. + """ + return isinstance(b, Remove) and self.value == b.value def merge_config(full_old, full_new): @@ -48,29 +46,56 @@ def merge_config(full_old, full_new): return new res = old.copy() for k, v in new.items(): - res[k] = merge(old[k], v) if k in old else v + if isinstance(v, Remove) and k in old: + del res[k] + else: + res[k] = merge(old[k], v) if k in old else v return res if isinstance(new, list): if not isinstance(old, list): return new res = old.copy() ids = { - v[CONF_ID]: i + v_id: i for i, v in enumerate(res) - if CONF_ID in v and isinstance(v[CONF_ID], str) + if isinstance(v, dict) + and (v_id := v.get(CONF_ID)) + and isinstance(v_id, str) } + extend_ids = { + v_id.value: i + for i, v in enumerate(res) + if isinstance(v, dict) + and (v_id := v.get(CONF_ID)) + and isinstance(v_id, Extend) + } + + ids_to_delete = [] for v in new: - if CONF_ID in v: - new_id = v[CONF_ID] + if isinstance(v, dict) and (new_id := v.get(CONF_ID)): if isinstance(new_id, Extend): new_id = new_id.value if new_id in ids: v[CONF_ID] = new_id res[ids[new_id]] = merge(res[ids[new_id]], v) continue + elif isinstance(new_id, Remove): + new_id = new_id.value + if new_id in ids: + ids_to_delete.append(ids[new_id]) + continue + elif ( + new_id in extend_ids + ): # When a package is extending a non-packaged item + extend_res = res[extend_ids[new_id]] + extend_res[CONF_ID] = new_id + new_v = merge(v, extend_res) + res[extend_ids[new_id]] = new_v + continue else: ids[new_id] = len(res) res.append(v) + res = [v for i, v in enumerate(res) if i not in ids_to_delete] return res if new is None: return old diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 0a6b2dfbb0..98b81ec328 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1,49 +1,54 @@ """Helpers for config validation using voluptuous.""" +from contextlib import contextmanager from dataclasses import dataclass +from datetime import datetime import logging import os import re -from contextlib import contextmanager -import uuid as uuid_ -from datetime import datetime from string import ascii_letters, digits +import uuid as uuid_ import voluptuous as vol from esphome import core import esphome.codegen as cg -from esphome.config_helpers import Extend +from esphome.config_helpers import Extend, Remove from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, - CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, + CONF_COMMAND_TOPIC, + CONF_DAY, CONF_DISABLED_BY_DEFAULT, CONF_DISCOVERY, CONF_ENTITY_CATEGORY, + CONF_HOUR, CONF_ICON, CONF_ID, CONF_INTERNAL, + CONF_MINUTE, + CONF_MONTH, CONF_NAME, + CONF_PASSWORD, + CONF_PATH, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, + CONF_REF, CONF_RETAIN, + CONF_SECOND, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, + CONF_SUBSCRIBE_QOS, CONF_TOPIC, - CONF_HOUR, - CONF_MINUTE, - CONF_SECOND, - CONF_VALUE, - CONF_UPDATE_INTERVAL, - CONF_TYPE_ID, CONF_TYPE, - CONF_REF, + CONF_TYPE_ID, + CONF_UPDATE_INTERVAL, CONF_URL, - CONF_PATH, CONF_USERNAME, - CONF_PASSWORD, + CONF_VALUE, + CONF_YEAR, ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_NONE, @@ -51,9 +56,13 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, TYPE_GIT, TYPE_LOCAL, VALID_SUBSTITUTIONS_CHARACTERS, + __version__ as ESPHOME_VERSION, ) from esphome.core import ( CORE, @@ -63,14 +72,15 @@ from esphome.core import ( TimePeriod, TimePeriodMicroseconds, TimePeriodMilliseconds, - TimePeriodSeconds, TimePeriodMinutes, + TimePeriodNanoseconds, + TimePeriodSeconds, ) -from esphome.helpers import list_starts_with, add_class_to_obj +from esphome.helpers import add_class_to_obj, list_starts_with from esphome.schema_extractors import ( SCHEMA_EXTRACT, - schema_extractor_list, schema_extractor, + schema_extractor_list, schema_extractor_registry, schema_extractor_typed, ) @@ -82,7 +92,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=consider-using-f-string VARIABLE_PROG = re.compile( - "\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS) + f"\\$([{VALID_SUBSTITUTIONS_CHARACTERS}]+|\\{{[{VALID_SUBSTITUTIONS_CHARACTERS}]*\\}})" ) # pylint: disable=invalid-name @@ -108,6 +118,7 @@ ROOT_CONFIG_PATH = object() RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword + "alarm", "alignas", "alignof", "and", @@ -124,6 +135,7 @@ RESERVED_IDS = [ "char16_t", "char32_t", "class", + "clock", "compl", "concept", "const", @@ -177,6 +189,7 @@ RESERVED_IDS = [ "struct", "switch", "template", + "text", "this", "thread_local", "throw", @@ -255,6 +268,10 @@ class Required(vol.Required): super().__init__(key, msg=msg) +class FinalExternalInvalid(Invalid): + """Represents an invalid value in the final validation phase where the path should not be prepended.""" + + def check_not_templatable(value): if isinstance(value, Lambda): raise Invalid("This option is not templatable!") @@ -292,7 +309,7 @@ def string(value): """Validate that a configuration value is a string. If not, automatically converts to a string. Note that this can be lossy, for example the input value 60.00 (float) will be turned into - "60.0" (string). For values where this could be a problem `string_string` has to be used. + "60.0" (string). For values where this could be a problem `string_strict` has to be used. """ check_not_templatable(value) if isinstance(value, (dict, list)): @@ -354,6 +371,20 @@ def boolean(value): ) +def boolean_false(value): + """Validate the given config option to be a boolean, set to False. + + This option allows a bunch of different ways of expressing boolean values: + - instance of boolean + - 'true'/'false' + - 'yes'/'no' + - 'enable'/disable + """ + if boolean(value): + raise Invalid("Expected boolean value to be false") + return False + + @schema_extractor_list def ensure_list(*validators): """Validate this configuration option to be a list. @@ -448,6 +479,7 @@ zero_to_one_float = float_range(min=0, max=1) negative_one_to_one_float = float_range(min=-1, max=1) positive_int = int_range(min=0) positive_not_null_int = int_range(min=0, min_included=False) +positive_not_null_float = float_range(min=0, min_included=False) def validate_id_name(value): @@ -525,6 +557,10 @@ def declare_id(type): if isinstance(value, Extend): raise Invalid(f"Source for extension of ID '{value.value}' was not found.") + + if isinstance(value, Remove): + raise Invalid(f"Source for Removal of ID '{value.value}' was not found.") + return core.ID(validate_id_name(value), is_declaration=True, type=type) return validator @@ -581,9 +617,9 @@ def only_with_framework(frameworks): return validator_ -only_on_esp32 = only_on("esp32") -only_on_esp8266 = only_on("esp8266") -only_on_rp2040 = only_on("rp2040") +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("arduino") only_with_esp_idf = only_with_framework("esp-idf") @@ -712,7 +748,10 @@ def time_period_str_unit(value): raise Invalid("Expected string for time period with unit.") unit_to_kwarg = { + "ns": "nanoseconds", + "nanoseconds": "nanoseconds", "us": "microseconds", + "µs": "microseconds", "microseconds": "microseconds", "ms": "milliseconds", "milliseconds": "milliseconds", @@ -733,7 +772,10 @@ def time_period_str_unit(value): raise Invalid(f"Expected time period with unit, got {value}") kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))] - return TimePeriod(**{kwarg: float(match.group(1))}) + try: + return TimePeriod(**{kwarg: float(match.group(1))}) + except ValueError as e: + raise Invalid(e) from e def time_period_in_milliseconds_(value): @@ -743,10 +785,18 @@ def time_period_in_milliseconds_(value): def time_period_in_microseconds_(value): + if value.nanoseconds is not None and value.nanoseconds != 0: + raise Invalid("Maximum precision is microseconds") return TimePeriodMicroseconds(**value.as_dict()) +def time_period_in_nanoseconds_(value): + return TimePeriodNanoseconds(**value.as_dict()) + + def time_period_in_seconds_(value): + if value.nanoseconds is not None and value.nanoseconds != 0: + raise Invalid("Maximum precision is seconds") if value.microseconds is not None and value.microseconds != 0: raise Invalid("Maximum precision is seconds") if value.milliseconds is not None and value.milliseconds != 0: @@ -755,6 +805,8 @@ def time_period_in_seconds_(value): def time_period_in_minutes_(value): + if value.nanoseconds is not None and value.nanoseconds != 0: + raise Invalid("Maximum precision is minutes") if value.microseconds is not None and value.microseconds != 0: raise Invalid("Maximum precision is minutes") if value.milliseconds is not None and value.milliseconds != 0: @@ -781,27 +833,113 @@ time_period_microseconds = All(time_period, time_period_in_microseconds_) positive_time_period_microseconds = All( positive_time_period, time_period_in_microseconds_ ) +positive_time_period_nanoseconds = All( + positive_time_period, time_period_in_nanoseconds_ +) positive_not_null_time_period = All( time_period, Range(min=TimePeriod(), min_included=False) ) def time_of_day(value): - value = string(value) - try: - date = datetime.strptime(value, "%H:%M:%S") - except ValueError as err: - try: - date = datetime.strptime(value, "%H:%M:%S %p") - except ValueError: - # pylint: disable=raise-missing-from - raise Invalid(f"Invalid time of day: {err}") + return date_time(date=False, time=True)(value) - return { - CONF_HOUR: date.hour, - CONF_MINUTE: date.minute, - CONF_SECOND: date.second, - } + +def date_time(date: bool, time: bool): + pattern_str = r"^" # Start of string + if date: + pattern_str += r"\d{4}-\d{1,2}-\d{1,2}" + if time: + pattern_str += r" " + if time: + pattern_str += ( + r"\d{1,2}:\d{2}" # Hour/Minute + r"(:\d{2})?" # 1. Seconds + r"(" # 2. Optional AM/PM group + r"(\s)?" # 3. Optional Space + r"(?:AM|PM|am|pm)" # AM/PM string matching + r")?" # End optional AM/PM group + ) + pattern_str += r"$" # End of string + + pattern = re.compile(pattern_str) + + exc_message = "" + if date: + exc_message += "date" + if time: + exc_message += "time" + + schema = Schema({}) + if date: + schema = schema.extend( + { + Required(CONF_YEAR): int_range(min=1970, max=3000), + Required(CONF_MONTH): int_range(min=1, max=12), + Required(CONF_DAY): int_range(min=1, max=31), + } + ) + if time: + schema = schema.extend( + { + Required(CONF_HOUR): int_range(min=0, max=23), + Required(CONF_MINUTE): int_range(min=0, max=59), + Required(CONF_SECOND): int_range(min=0, max=59), + } + ) + + def validator(value): + if isinstance(value, dict): + return schema(value) + value = string(value) + + match = pattern.match(value) + if match is None: + # pylint: disable=raise-missing-from + raise Invalid(f"Invalid {exc_message}: {value}") + + if time: + has_seconds = match[1] is not None + has_ampm = match[2] is not None + has_ampm_space = match[3] is not None + + format = "" + if date: + format += "%Y-%m-%d" + if time: + format += " " + if time: + if has_ampm: + format += "%I:%M" + else: + format += "%H:%M" + if has_seconds: + format += ":%S" + if has_ampm_space: + format += " " + if has_ampm: + format += "%p" + + try: + date_obj = datetime.strptime(value, format) + except ValueError as err: + # pylint: disable=raise-missing-from + raise Invalid(f"Invalid {exc_message}: {err}") + + return_value = {} + if date: + return_value[CONF_YEAR] = date_obj.year + return_value[CONF_MONTH] = date_obj.month + return_value[CONF_DAY] = date_obj.day + + if time: + return_value[CONF_HOUR] = date_obj.hour + return_value[CONF_MINUTE] = date_obj.minute + return_value[CONF_SECOND] = date_obj.second if has_seconds else 0 + + return schema(return_value) + + return validator def mac_address(value): @@ -927,6 +1065,27 @@ def temperature(value): raise err +def temperature_delta(value): + err = None + try: + return _temperature_c(value) + except Invalid as orig_err: + err = orig_err + + try: + return _temperature_k(value) + except Invalid: + pass + + try: + fahrenheit = _temperature_f(value) + return fahrenheit * (5 / 9) + except Invalid: + pass + + raise err + + _color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)") _color_temperature_kelvin = float_with_unit("Color Temperature", r"(K|Kelvin)") @@ -1020,6 +1179,8 @@ def ipv4(value): def _valid_topic(value): """Validate that this is a valid topic name/filter.""" + if value is None: # Used to disable publishing and subscribing + return "" if isinstance(value, dict): raise Invalid("Can't use dictionary with topic") value = string(value) @@ -1442,6 +1603,10 @@ def typed_schema(schemas, **kwargs): """Create a schema that has a key to distinguish between schemas""" key = kwargs.pop("key", CONF_TYPE) default_schema_option = kwargs.pop("default_type", None) + enum_mapping = kwargs.pop("enum", None) + if enum_mapping is not None: + assert isinstance(enum_mapping, dict) + assert set(enum_mapping.keys()) == set(schemas.keys()) key_validator = one_of(*schemas, **kwargs) def validator(value): @@ -1452,6 +1617,9 @@ def typed_schema(schemas, **kwargs): if schema_option is None: raise Invalid(f"{key} not specified!") key_v = key_validator(schema_option) + if enum_mapping is not None: + key_v = add_class_to_obj(key_v, core.EnumValue) + key_v.enum_value = enum_mapping[key_v] value = Schema(schemas[key_v])(value) value[key] = key_v return value @@ -1466,6 +1634,13 @@ class GenerateID(Optional): super().__init__(key, default=lambda: None) +def _get_priority_default(*args): + for arg in args: + if arg is not vol.UNDEFINED: + return arg + return vol.UNDEFINED + + class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" @@ -1476,30 +1651,90 @@ class SplitDefault(Optional): esp32=vol.UNDEFINED, esp32_arduino=vol.UNDEFINED, esp32_idf=vol.UNDEFINED, + esp32_s2=vol.UNDEFINED, + esp32_s2_arduino=vol.UNDEFINED, + esp32_s2_idf=vol.UNDEFINED, + esp32_s3=vol.UNDEFINED, + esp32_s3_arduino=vol.UNDEFINED, + esp32_s3_idf=vol.UNDEFINED, + esp32_c3=vol.UNDEFINED, + esp32_c3_arduino=vol.UNDEFINED, + esp32_c3_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, + bk72xx=vol.UNDEFINED, + rtl87xx=vol.UNDEFINED, host=vol.UNDEFINED, ): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_arduino_default = vol.default_factory( - esp32_arduino if esp32 is vol.UNDEFINED else esp32 + _get_priority_default(esp32_arduino, esp32) ) self._esp32_idf_default = vol.default_factory( - esp32_idf if esp32 is vol.UNDEFINED else esp32 + _get_priority_default(esp32_idf, esp32) + ) + self._esp32_s2_arduino_default = vol.default_factory( + _get_priority_default(esp32_s2_arduino, esp32_s2, esp32_arduino, esp32) + ) + self._esp32_s2_idf_default = vol.default_factory( + _get_priority_default(esp32_s2_idf, esp32_s2, esp32_idf, esp32) + ) + self._esp32_s3_arduino_default = vol.default_factory( + _get_priority_default(esp32_s3_arduino, esp32_s3, esp32_arduino, esp32) + ) + self._esp32_s3_idf_default = vol.default_factory( + _get_priority_default(esp32_s3_idf, esp32_s3, esp32_idf, esp32) + ) + self._esp32_c3_arduino_default = vol.default_factory( + _get_priority_default(esp32_c3_arduino, esp32_c3, esp32_arduino, esp32) + ) + self._esp32_c3_idf_default = vol.default_factory( + _get_priority_default(esp32_c3_idf, esp32_c3, esp32_idf, esp32) ) self._rp2040_default = vol.default_factory(rp2040) + self._bk72xx_default = vol.default_factory(bk72xx) + self._rtl87xx_default = vol.default_factory(rtl87xx) self._host_default = vol.default_factory(host) @property def default(self): if CORE.is_esp8266: return self._esp8266_default - if CORE.is_esp32 and CORE.using_arduino: - return self._esp32_arduino_default - if CORE.is_esp32 and CORE.using_esp_idf: - return self._esp32_idf_default + if CORE.is_esp32: + from esphome.components.esp32 import get_esp32_variant + from esphome.components.esp32.const import ( + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + ) + + variant = get_esp32_variant() + if variant == VARIANT_ESP32S2: + if CORE.using_arduino: + return self._esp32_s2_arduino_default + if CORE.using_esp_idf: + return self._esp32_s2_idf_default + elif variant == VARIANT_ESP32S3: + if CORE.using_arduino: + return self._esp32_s3_arduino_default + if CORE.using_esp_idf: + return self._esp32_s3_idf_default + elif variant == VARIANT_ESP32C3: + if CORE.using_arduino: + return self._esp32_c3_arduino_default + if CORE.using_esp_idf: + return self._esp32_c3_idf_default + else: + if CORE.using_arduino: + return self._esp32_arduino_default + if CORE.using_esp_idf: + return self._esp32_idf_default if CORE.is_rp2040: return self._rp2040_default + if CORE.is_bk72xx: + return self._bk72xx_default + if CORE.is_rtl87xx: + return self._rtl87xx_default if CORE.is_host: return self._host_default raise NotImplementedError @@ -1638,7 +1873,7 @@ def maybe_simple_value(*validators, **kwargs): return validate -_ENTITY_CATEGORIES = { +ENTITY_CATEGORIES = { ENTITY_CATEGORY_NONE: cg.EntityCategory.ENTITY_CATEGORY_NONE, ENTITY_CATEGORY_CONFIG: cg.EntityCategory.ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC: cg.EntityCategory.ENTITY_CATEGORY_DIAGNOSTIC, @@ -1646,7 +1881,7 @@ _ENTITY_CATEGORIES = { def entity_category(value): - return enum(_ENTITY_CATEGORIES, lower=True)(value) + return enum(ENTITY_CATEGORIES, lower=True)(value) MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema( @@ -1659,8 +1894,10 @@ MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema( MQTT_COMPONENT_SCHEMA = Schema( { + Optional(CONF_QOS): All(requires_component("mqtt"), mqtt_qos), Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean), Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean), + Optional(CONF_SUBSCRIBE_QOS): All(requires_component("mqtt"), mqtt_qos), Optional(CONF_STATE_TOPIC): All(requires_component("mqtt"), publish_topic), Optional(CONF_AVAILABILITY): All( requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA) @@ -1729,13 +1966,13 @@ def url(value): except ValueError as e: raise Invalid("Not a valid URL") from e - if not parsed.scheme or not parsed.netloc: - raise Invalid("Expected a URL scheme and host") - return parsed.geturl() + if parsed.scheme and parsed.netloc or parsed.scheme == "file": + return parsed.geturl() + raise Invalid("Expected a file scheme or a URL scheme with host") def git_ref(value): - if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: + if re.match(r"[a-zA-Z0-9_./-]+", value) is None: raise Invalid("Not a valid git ref") return value @@ -1776,6 +2013,16 @@ def version_number(value): raise Invalid("Not a valid version number") from e +def validate_esphome_version(value: str): + min_version = Version.parse(value) + current_version = Version.parse(ESPHOME_VERSION) + if current_version < min_version: + raise Invalid( + f"Your ESPHome version is too old. Please update to at least {min_version}" + ) + return value + + def platformio_version_constraint(value): # for documentation on valid version constraints: # https://docs.platformio.org/en/latest/core/userguide/platforms/cmd_install.html#cmd-platform-install @@ -1801,6 +2048,8 @@ def require_framework_version( esp32_arduino=None, esp8266_arduino=None, rp2040_arduino=None, + bk72xx_libretiny=None, + host=None, max_version=False, extra_message=None, ): @@ -1814,6 +2063,13 @@ def require_framework_version( msg += f". {extra_message}" raise Invalid(msg) required = esp_idf + elif CORE.is_bk72xx and framework == "arduino": + if bk72xx_libretiny is None: + msg = "This feature is incompatible with BK72XX" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) + required = bk72xx_libretiny elif CORE.is_esp32 and framework == "arduino": if esp32_arduino is None: msg = "This feature is incompatible with ESP32 using arduino framework" @@ -1835,6 +2091,13 @@ def require_framework_version( msg += f". {extra_message}" raise Invalid(msg) required = rp2040_arduino + elif CORE.is_host and framework == "host": + if host is None: + msg = "This feature is incompatible with host platform" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) + required = host else: raise Invalid( f""" @@ -1885,15 +2148,20 @@ def suppress_invalid(): pass -GIT_SCHEMA = { - Required(CONF_URL): url, - Optional(CONF_REF): git_ref, - Optional(CONF_USERNAME): string, - Optional(CONF_PASSWORD): string, -} -LOCAL_SCHEMA = { - Required(CONF_PATH): directory, -} +GIT_SCHEMA = Schema( + { + Required(CONF_URL): url, + Optional(CONF_REF): git_ref, + Optional(CONF_USERNAME): string, + Optional(CONF_PASSWORD): string, + Optional(CONF_PATH): string, + } +) +LOCAL_SCHEMA = Schema( + { + Required(CONF_PATH): directory, + } +) def validate_source_shorthand(value): @@ -1934,8 +2202,18 @@ SOURCE_SCHEMA = Any( validate_source_shorthand, typed_schema( { - TYPE_GIT: Schema(GIT_SCHEMA), - TYPE_LOCAL: Schema(LOCAL_SCHEMA), + TYPE_GIT: GIT_SCHEMA, + TYPE_LOCAL: LOCAL_SCHEMA, } ), ) + + +def rename_key(old_key, new_key): + def validator(config: dict) -> dict: + config = config.copy() + if old_key in config: + config[new_key] = config.pop(old_key) + return config + + return validator diff --git a/esphome/const.py b/esphome/const.py index cbc8f428f5..5645c9eaab 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,18 +1,29 @@ """Constants used by esphome.""" -__version__ = "2023.6.0-dev" +__version__ = "2024.11.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" ) +PLATFORM_BK72XX = "bk72xx" PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" -PLATFORM_RP2040 = "rp2040" PLATFORM_HOST = "host" +PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" +PLATFORM_RP2040 = "rp2040" +PLATFORM_RTL87XX = "rtl87xx" -TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_HOST] +TARGET_PLATFORMS = [ + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_HOST, + PLATFORM_LIBRETINY_OLDSTYLE, + PLATFORM_RP2040, + PLATFORM_RTL87XX, +] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} @@ -26,21 +37,32 @@ CONF_ACCELERATION_Y = "acceleration_y" CONF_ACCELERATION_Z = "acceleration_z" CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" +CONF_ACTION = "action" CONF_ACTION_ID = "action_id" CONF_ACTION_STATE_TOPIC = "action_state_topic" +CONF_ACTIONS = "actions" CONF_ACTIVE = "active" CONF_ACTIVE_POWER = "active_power" CONF_ACTUAL_GAIN = "actual_gain" +CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ADVANCED = "advanced" CONF_AFTER = "after" +CONF_ALL = "all" +CONF_ALLOW_OTHER_USES = "allow_other_uses" CONF_ALPHA = "alpha" CONF_ALTITUDE = "altitude" +CONF_AMBIENT_LIGHT = "ambient_light" +CONF_AMMONIA = "ammonia" +CONF_ANALOG = "analog" CONF_AND = "and" +CONF_ANGLE = "angle" +CONF_ANY = "any" CONF_AP = "ap" CONF_APPARENT_POWER = "apparent_power" CONF_ARDUINO_VERSION = "arduino_version" +CONF_AREA = "area" CONF_ARGS = "args" CONF_ASSUMED_STATE = "assumed_state" CONF_AT = "at" @@ -56,6 +78,7 @@ CONF_AWAY = "away" CONF_AWAY_COMMAND_TOPIC = "away_command_topic" CONF_AWAY_CONFIG = "away_config" CONF_AWAY_STATE_TOPIC = "away_state_topic" +CONF_BACKGROUND_COLOR = "background_color" CONF_BACKLIGHT_PIN = "backlight_pin" CONF_BASELINE = "baseline" CONF_BATTERY_LEVEL = "battery_level" @@ -69,6 +92,7 @@ CONF_BINARY_SENSORS = "binary_sensors" CONF_BINDKEY = "bindkey" CONF_BIRTH_MESSAGE = "birth_message" CONF_BIT_DEPTH = "bit_depth" +CONF_BITS_PER_SAMPLE = "bits_per_sample" CONF_BLOCK = "block" CONF_BLUE = "blue" CONF_BOARD = "board" @@ -76,18 +100,21 @@ CONF_BOARD_FLASH_MODE = "board_flash_mode" CONF_BORDER = "border" CONF_BRANCH = "branch" CONF_BRIGHTNESS = "brightness" +CONF_BRIGHTNESS_LIMITS = "brightness_limits" CONF_BROKER = "broker" CONF_BSSID = "bssid" CONF_BUFFER_SIZE = "buffer_size" CONF_BUILD_PATH = "build_path" CONF_BUS_VOLTAGE = "bus_voltage" CONF_BUSY_PIN = "busy_pin" +CONF_BUTTON = "button" CONF_BYTES = "bytes" CONF_CALCULATED_LUX = "calculated_lux" CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" CONF_CAPACITY = "capacity" +CONF_CARBON_MONOXIDE = "carbon_monoxide" CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent" CONF_CARRIER_FREQUENCY = "carrier_frequency" CONF_CERTIFICATE = "certificate" @@ -96,8 +123,12 @@ CONF_CHANGE_MODE_EVERY = "change_mode_every" CONF_CHANNEL = "channel" CONF_CHANNELS = "channels" CONF_CHARACTERISTIC_UUID = "characteristic_uuid" +CONF_CHECK = "check" CONF_CHIPSET = "chipset" +CONF_CLEAN_SESSION = "clean_session" CONF_CLEAR_IMPEDANCE = "clear_impedance" +CONF_CLIENT_CERTIFICATE = "client_certificate" +CONF_CLIENT_CERTIFICATE_KEY = "client_certificate_key" CONF_CLIENT_ID = "client_id" CONF_CLK_PIN = "clk_pin" CONF_CLOCK_PIN = "clock_pin" @@ -106,6 +137,7 @@ CONF_CLOSE_DURATION = "close_duration" CONF_CLOSE_ENDSTOP = "close_endstop" CONF_CO2 = "co2" CONF_CODE = "code" +CONF_COL = "col" CONF_COLD_WHITE = "cold_white" CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" CONF_COLOR = "color" @@ -113,14 +145,17 @@ CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" CONF_COLOR_INTERLOCK = "color_interlock" CONF_COLOR_MODE = "color_mode" +CONF_COLOR_ORDER = "color_order" CONF_COLOR_PALETTE = "color_palette" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" +CONF_COMMAND_REPEATS = "command_repeats" CONF_COMMAND_RETAIN = "command_retain" CONF_COMMAND_TOPIC = "command_topic" CONF_COMMENT = "comment" CONF_COMMIT = "commit" +CONF_COMPENSATION = "compensation" CONF_COMPILE_PROCESS_LIMIT = "compile_process_limit" CONF_COMPONENT_ID = "component_id" CONF_COMPONENTS = "components" @@ -142,6 +177,7 @@ CONF_CS_PIN = "cs_pin" CONF_CSS_INCLUDE = "css_include" CONF_CSS_URL = "css_url" CONF_CURRENT = "current" +CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" @@ -157,6 +193,9 @@ CONF_DATA_PIN = "data_pin" CONF_DATA_PINS = "data_pins" CONF_DATA_RATE = "data_rate" CONF_DATA_TEMPLATE = "data_template" +CONF_DATE = "date" +CONF_DATETIME = "datetime" +CONF_DAY = "day" CONF_DAYS_OF_MONTH = "days_of_month" CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" @@ -169,6 +208,7 @@ CONF_DEFAULT_MODE = "default_mode" CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high" CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low" CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length" +CONF_DEFAULTS = "defaults" CONF_DELAY = "delay" CONF_DELIMITER = "delimiter" CONF_DELTA = "delta" @@ -191,6 +231,8 @@ CONF_DISCOVERY_OBJECT_ID_GENERATOR = "discovery_object_id_generator" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_DISCOVERY_RETAIN = "discovery_retain" CONF_DISCOVERY_UNIQUE_ID_GENERATOR = "discovery_unique_id_generator" +CONF_DISPLAY = "display" +CONF_DISPLAY_ID = "display_id" CONF_DISTANCE = "distance" CONF_DITHER = "dither" CONF_DIV_RATIO = "div_ratio" @@ -198,6 +240,7 @@ CONF_DNS_ADDRESS = "dns_address" CONF_DNS1 = "dns1" CONF_DNS2 = "dns2" CONF_DOMAIN = "domain" +CONF_DOOYA = "dooya" CONF_DRY_ACTION = "dry_action" CONF_DRY_MODE = "dry_mode" CONF_DUMMY_RECEIVER = "dummy_receiver" @@ -213,7 +256,9 @@ CONF_EFFECTS = "effects" CONF_ELSE = "else" CONF_ENABLE_BTM = "enable_btm" CONF_ENABLE_IPV6 = "enable_ipv6" +CONF_ENABLE_ON_BOOT = "enable_on_boot" CONF_ENABLE_PIN = "enable_pin" +CONF_ENABLE_PRIVATE_NETWORK_ACCESS = "enable_private_network_access" CONF_ENABLE_RRM = "enable_rrm" CONF_ENABLE_TIME = "enable_time" CONF_ENERGY = "energy" @@ -223,14 +268,19 @@ CONF_ENUM_DATAPOINT = "enum_datapoint" CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" +CONF_ETHANOL = "ethanol" CONF_ETHERNET = "ethernet" CONF_EVENT = "event" +CONF_EVENT_TYPE = "event_type" +CONF_EVENT_TYPES = "event_types" CONF_EXPIRE_AFTER = "expire_after" CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy" CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy" CONF_EXTERNAL_CLOCK_INPUT = "external_clock_input" CONF_EXTERNAL_COMPONENTS = "external_components" +CONF_EXTERNAL_TEMPERATURE = "external_temperature" CONF_EXTERNAL_VCC = "external_vcc" +CONF_FACTORY_RESET = "factory_reset" CONF_FALLING_EDGE = "falling_edge" CONF_FAMILY = "family" CONF_FAN_MODE = "fan_mode" @@ -253,6 +303,9 @@ CONF_FAN_ONLY_MODE = "fan_only_mode" CONF_FAN_WITH_COOLING = "fan_with_cooling" CONF_FAN_WITH_HEATING = "fan_with_heating" CONF_FAST_CONNECT = "fast_connect" +CONF_FIELD_STRENGTH_X = "field_strength_x" +CONF_FIELD_STRENGTH_Y = "field_strength_y" +CONF_FIELD_STRENGTH_Z = "field_strength_z" CONF_FILE = "file" CONF_FILES = "files" CONF_FILTER = "filter" @@ -262,9 +315,12 @@ CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" +CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" +CONF_FONT = "font" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" +CONF_FOREGROUND_COLOR = "foreground_color" CONF_FORMALDEHYDE = "formaldehyde" CONF_FORMAT = "format" CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" @@ -275,6 +331,7 @@ CONF_FREQUENCY = "frequency" CONF_FRIENDLY_NAME = "friendly_name" CONF_FROM = "from" CONF_FULL_SPECTRUM = "full_spectrum" +CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts" CONF_FULL_UPDATE_EVERY = "full_update_every" CONF_GAIN = "gain" CONF_GAMMA_CORRECT = "gamma_correct" @@ -285,7 +342,15 @@ CONF_GLYPHS = "glyphs" CONF_GPIO = "gpio" CONF_GREEN = "green" CONF_GROUP = "group" +CONF_GYROSCOPE_X = "gyroscope_x" +CONF_GYROSCOPE_Y = "gyroscope_y" +CONF_GYROSCOPE_Z = "gyroscope_z" CONF_HARDWARE_UART = "hardware_uart" +CONF_HAS_MOVING_TARGET = "has_moving_target" +CONF_HAS_STILL_TARGET = "has_still_target" +CONF_HAS_TARGET = "has_target" +CONF_HEAD = "head" +CONF_HEADING = "heading" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" CONF_HEAT_DEADBAND = "heat_deadband" @@ -299,10 +364,14 @@ CONF_HIGH = "high" CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference" CONF_HOUR = "hour" CONF_HOURS = "hours" +CONF_HSYNC_PIN = "hsync_pin" CONF_HUMIDITY = "humidity" +CONF_HUMIDITY_SENSOR = "humidity_sensor" +CONF_HYDROGEN = "hydrogen" CONF_HYSTERESIS = "hysteresis" CONF_I2C = "i2c" CONF_I2C_ID = "i2c_id" +CONF_IAQ_ACCURACY = "iaq_accuracy" CONF_IBEACON_MAJOR = "ibeacon_major" CONF_IBEACON_MINOR = "ibeacon_minor" CONF_IBEACON_UUID = "ibeacon_uuid" @@ -314,7 +383,11 @@ CONF_IDLE_ACTION = "idle_action" CONF_IDLE_LEVEL = "idle_level" CONF_IDLE_TIME = "idle_time" CONF_IF = "if" +CONF_IGNORE_EFUSE_CUSTOM_MAC = "ignore_efuse_custom_mac" CONF_IGNORE_EFUSE_MAC_CRC = "ignore_efuse_mac_crc" +CONF_IGNORE_OUT_OF_RANGE = "ignore_out_of_range" +CONF_IGNORE_PIN_VALIDATION_ERROR = "ignore_pin_validation_error" +CONF_IGNORE_STRAPPING_WARNING = "ignore_strapping_warning" CONF_IIR_FILTER = "iir_filter" CONF_ILLUMINANCE = "illuminance" CONF_IMPEDANCE = "impedance" @@ -326,8 +399,10 @@ CONF_INCLUDES = "includes" CONF_INDEX = "index" CONF_INDOOR = "indoor" CONF_INFRARED = "infrared" +CONF_INIT_SEQUENCE = "init_sequence" CONF_INITIAL_MODE = "initial_mode" CONF_INITIAL_OPTION = "initial_option" +CONF_INITIAL_STATE = "initial_state" CONF_INITIAL_VALUE = "initial_value" CONF_INPUT = "input" CONF_INTEGRATION_TIME = "integration_time" @@ -336,13 +411,18 @@ CONF_INTERLOCK = "interlock" CONF_INTERNAL = "internal" CONF_INTERNAL_FILTER = "internal_filter" CONF_INTERNAL_FILTER_MODE = "internal_filter_mode" +CONF_INTERNAL_TEMPERATURE = "internal_temperature" CONF_INTERRUPT = "interrupt" CONF_INTERRUPT_PIN = "interrupt_pin" CONF_INTERVAL = "interval" CONF_INVALID_COOLDOWN = "invalid_cooldown" CONF_INVERT = "invert" +CONF_INVERT_COLORS = "invert_colors" CONF_INVERTED = "inverted" CONF_IP_ADDRESS = "ip_address" +CONF_IRQ_PIN = "irq_pin" +CONF_IS_RGBW = "is_rgbw" +CONF_ITEMS = "items" CONF_JS_INCLUDE = "js_include" CONF_JS_URL = "js_url" CONF_JVC = "jvc" @@ -363,11 +443,14 @@ CONF_LIGHT = "light" CONF_LIGHT_ID = "light_id" CONF_LIGHTNING_ENERGY = "lightning_energy" CONF_LIGHTNING_THRESHOLD = "lightning_threshold" +CONF_LIMIT_MODE = "limit_mode" +CONF_LINE_FREQUENCY = "line_frequency" CONF_LINE_THICKNESS = "line_thickness" CONF_LINE_TYPE = "line_type" CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOCAL = "local" CONF_LOCK_ACTION = "lock_action" +CONF_LOG = "log" CONF_LOG_TOPIC = "log_topic" CONF_LOGGER = "logger" CONF_LOGS = "logs" @@ -399,8 +482,11 @@ CONF_MAX_VOLTAGE = "max_voltage" CONF_MDNS = "mdns" CONF_MEASUREMENT_DURATION = "measurement_duration" CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" +CONF_MEDIA_PLAYER = "media_player" CONF_MEDIUM = "medium" CONF_MEMORY_BLOCKS = "memory_blocks" +CONF_MESSAGE = "message" +CONF_METHANE = "methane" CONF_METHOD = "method" CONF_MICROPHONE = "microphone" CONF_MIN_BRIGHTNESS = "min_brightness" @@ -412,6 +498,7 @@ CONF_MIN_FANNING_RUN_TIME = "min_fanning_run_time" CONF_MIN_HEATING_OFF_TIME = "min_heating_off_time" CONF_MIN_HEATING_RUN_TIME = "min_heating_run_time" CONF_MIN_IDLE_TIME = "min_idle_time" +CONF_MIN_IPV6_ADDR_COUNT = "min_ipv6_addr_count" CONF_MIN_LENGTH = "min_length" CONF_MIN_LEVEL = "min_level" CONF_MIN_POWER = "min_power" @@ -422,18 +509,22 @@ CONF_MIN_VALUE = "min_value" CONF_MIN_VERSION = "min_version" CONF_MINUTE = "minute" CONF_MINUTES = "minutes" +CONF_MIRROR_X = "mirror_x" +CONF_MIRROR_Y = "mirror_y" CONF_MISO_PIN = "miso_pin" CONF_MODE = "mode" CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_STATE_TOPIC = "mode_state_topic" CONF_MODEL = "model" CONF_MOISTURE = "moisture" +CONF_MONTH = "month" CONF_MONTHS = "months" CONF_MOSI_PIN = "mosi_pin" CONF_MOTION = "motion" CONF_MOVEMENT_COUNTER = "movement_counter" CONF_MQTT = "mqtt" CONF_MQTT_ID = "mqtt_id" +CONF_MULTIPLE = "multiple" CONF_MULTIPLEXER = "multiplexer" CONF_MULTIPLY = "multiply" CONF_NAME = "name" @@ -442,6 +533,7 @@ CONF_NBITS = "nbits" CONF_NEC = "nec" CONF_NETWORKS = "networks" CONF_NEW_PASSWORD = "new_password" +CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" CONF_NOISE_LEVEL = "noise_level" CONF_NUM_ATTEMPTS = "num_attempts" CONF_NUM_CHANNELS = "num_channels" @@ -450,29 +542,45 @@ CONF_NUM_LEDS = "num_leds" CONF_NUM_SCANS = "num_scans" CONF_NUMBER = "number" CONF_NUMBER_DATAPOINT = "number_datapoint" +CONF_OE_PIN = "oe_pin" CONF_OFF_MODE = "off_mode" +CONF_OFF_SPEED_CYCLE = "off_speed_cycle" CONF_OFFSET = "offset" +CONF_OFFSET_HEIGHT = "offset_height" +CONF_OFFSET_WIDTH = "offset_width" CONF_ON = "on" CONF_ON_BLE_ADVERTISE = "on_ble_advertise" CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise" CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" +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_DIRECTION_SET = "on_direction_set" CONF_ON_DISCONNECT = "on_disconnect" CONF_ON_DOUBLE_CLICK = "on_double_click" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan" +CONF_ON_ERROR = "on_error" +CONF_ON_EVENT = "on_event" +CONF_ON_FINGER_SCAN_INVALID = "on_finger_scan_invalid" CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched" +CONF_ON_FINGER_SCAN_MISPLACED = "on_finger_scan_misplaced" +CONF_ON_FINGER_SCAN_START = "on_finger_scan_start" CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched" +CONF_ON_FINISHED_WRITE = "on_finished_write" +CONF_ON_IDLE = "on_idle" CONF_ON_JSON_MESSAGE = "on_json_message" CONF_ON_LOCK = "on_lock" CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" CONF_ON_MULTI_CLICK = "on_multi_click" CONF_ON_OPEN = "on_open" +CONF_ON_OSCILLATING_SET = "on_oscillating_set" +CONF_ON_PRESET_SET = "on_preset_set" CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" @@ -488,6 +596,7 @@ CONF_ON_TOUCH = "on_touch" CONF_ON_TURN_OFF = "on_turn_off" CONF_ON_TURN_ON = "on_turn_on" CONF_ON_UNLOCK = "on_unlock" +CONF_ON_UPDATE = "on_update" CONF_ON_VALUE = "on_value" CONF_ON_VALUE_RANGE = "on_value_range" CONF_ONE = "one" @@ -506,6 +615,7 @@ CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" CONF_OSCILLATION_OUTPUT = "oscillation_output" CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" CONF_OTA = "ota" +CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_OUTPUT = "output" CONF_OUTPUT_ID = "output_id" CONF_OUTPUTS = "outputs" @@ -517,13 +627,17 @@ CONF_PANASONIC = "panasonic" CONF_PARAMETERS = "parameters" CONF_PASSWORD = "password" CONF_PATH = "path" +CONF_PATTERN = "pattern" CONF_PAYLOAD = "payload" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_PERIOD = "period" CONF_PH = "ph" +CONF_PHASE_A = "phase_a" CONF_PHASE_ANGLE = "phase_angle" +CONF_PHASE_B = "phase_b" CONF_PHASE_BALANCER = "phase_balancer" +CONF_PHASE_C = "phase_c" CONF_PIN = "pin" CONF_PIN_A = "pin_a" CONF_PIN_B = "pin_b" @@ -532,6 +646,7 @@ CONF_PIN_D = "pin_d" CONF_PINS = "pins" CONF_PIXEL_MAPPER = "pixel_mapper" CONF_PLATFORM = "platform" +CONF_PLATFORM_VERSION = "platform_version" CONF_PLATFORMIO_OPTIONS = "platformio_options" CONF_PM_0_3UM = "pm_0_3um" CONF_PM_0_5UM = "pm_0_5um" @@ -552,6 +667,7 @@ CONF_PMC_1_0 = "pmc_1_0" CONF_PMC_10_0 = "pmc_10_0" CONF_PMC_2_5 = "pmc_2_5" CONF_PMC_4_0 = "pmc_4_0" +CONF_POLLING_INTERVAL = "polling_interval" CONF_PORT = "port" CONF_POSITION = "position" CONF_POSITION_ACTION = "position_action" @@ -566,6 +682,7 @@ CONF_PRESET = "preset" CONF_PRESET_BOOST = "preset_boost" CONF_PRESET_COMMAND_TOPIC = "preset_command_topic" CONF_PRESET_ECO = "preset_eco" +CONF_PRESET_MODES = "preset_modes" CONF_PRESET_SLEEP = "preset_sleep" CONF_PRESET_STATE_TOPIC = "preset_state_topic" CONF_PRESSURE = "pressure" @@ -598,6 +715,7 @@ CONF_RED = "red" CONF_REF = "ref" CONF_REFERENCE_RESISTANCE = "reference_resistance" CONF_REFERENCE_TEMPERATURE = "reference_temperature" +CONF_REFERENCE_VOLTAGE = "reference_voltage" CONF_REFRESH = "refresh" CONF_RELABEL = "relabel" CONF_REPEAT = "repeat" @@ -606,6 +724,7 @@ CONF_RESET_DURATION = "reset_duration" CONF_RESET_PIN = "reset_pin" CONF_RESIZE = "resize" CONF_RESOLUTION = "resolution" +CONF_RESTART = "restart" CONF_RESTORE = "restore" CONF_RESTORE_MODE = "restore_mode" CONF_RESTORE_STATE = "restore_state" @@ -616,7 +735,9 @@ CONF_REVERSED = "reversed" CONF_RGB_ORDER = "rgb_order" CONF_RGBW = "rgbw" CONF_RISING_EDGE = "rising_edge" +CONF_RMT_CHANNEL = "rmt_channel" CONF_ROTATION = "rotation" +CONF_ROW = "row" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" @@ -625,7 +746,9 @@ CONF_RW_PIN = "rw_pin" CONF_RX_BUFFER_SIZE = "rx_buffer_size" CONF_RX_ONLY = "rx_only" CONF_RX_PIN = "rx_pin" +CONF_RX_QUEUE_LEN = "rx_queue_len" CONF_SAFE_MODE = "safe_mode" +CONF_SAMPLE_RATE = "sample_rate" CONF_SAMSUNG = "samsung" CONF_SATELLITES = "satellites" CONF_SCAN = "scan" @@ -642,6 +765,7 @@ CONF_SEL_PIN = "sel_pin" CONF_SEND_EVERY = "send_every" CONF_SEND_FIRST_AT = "send_first_at" CONF_SENSING_PIN = "sensing_pin" +CONF_SENSITIVITY = "sensitivity" CONF_SENSOR = "sensor" CONF_SENSOR_DATAPOINT = "sensor_datapoint" CONF_SENSOR_ID = "sensor_id" @@ -651,6 +775,7 @@ CONF_SERVERS = "servers" CONF_SERVICE = "service" CONF_SERVICE_UUID = "service_uuid" CONF_SERVICES = "services" +CONF_SET_ACTION = "set_action" CONF_SET_POINT_MINIMUM_DIFFERENTIAL = "set_point_minimum_differential" CONF_SETUP_MODE = "setup_mode" CONF_SETUP_PRIORITY = "setup_priority" @@ -676,6 +801,7 @@ CONF_SPEED_COUNT = "speed_count" CONF_SPEED_LEVEL_COMMAND_TOPIC = "speed_level_command_topic" CONF_SPEED_LEVEL_STATE_TOPIC = "speed_level_state_topic" CONF_SPEED_STATE_TOPIC = "speed_state_topic" +CONF_SPI = "spi" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" @@ -688,12 +814,14 @@ CONF_STATIC_IP = "static_ip" CONF_STATUS = "status" CONF_STB_PIN = "stb_pin" CONF_STEP = "step" +CONF_STEP_DELAY = "step_delay" CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" CONF_STORE_BASELINE = "store_baseline" CONF_SUBNET = "subnet" +CONF_SUBSCRIBE_QOS = "subscribe_qos" CONF_SUBSTITUTIONS = "substitutions" CONF_SUM = "sum" CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" @@ -706,6 +834,7 @@ CONF_SUPPORTED_PRESETS = "supported_presets" CONF_SUPPORTED_SWING_MODES = "supported_swing_modes" CONF_SUPPORTS_COOL = "supports_cool" CONF_SUPPORTS_HEAT = "supports_heat" +CONF_SWAP_XY = "swap_xy" CONF_SWING_BOTH_ACTION = "swing_both_action" CONF_SWING_HORIZONTAL_ACTION = "swing_horizontal_action" CONF_SWING_MODE = "swing_mode" @@ -719,6 +848,8 @@ CONF_SYNC = "sync" CONF_TABLET = "tablet" CONF_TAG = "tag" CONF_TARGET = "target" +CONF_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" CONF_TARGET_TEMPERATURE = "target_temperature" CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action" CONF_TARGET_TEMPERATURE_COMMAND_TOPIC = "target_temperature_command_topic" @@ -730,10 +861,13 @@ CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC = "target_temperature_low_command_topi CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC = "target_temperature_low_state_topic" CONF_TARGET_TEMPERATURE_STATE_TOPIC = "target_temperature_state_topic" CONF_TEMPERATURE = "temperature" +CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_TEMPERATURE_SOURCE = "temperature_source" CONF_TEMPERATURE_STEP = "temperature_step" +CONF_TEXT = "text" CONF_TEXT_SENSORS = "text_sensors" CONF_THEN = "then" +CONF_THERMOCOUPLE_TYPE = "thermocouple_type" CONF_THRESHOLD = "threshold" CONF_THROTTLE = "throttle" CONF_TILT = "tilt" @@ -754,15 +888,19 @@ CONF_TOPIC_PREFIX = "topic_prefix" CONF_TOTAL = "total" CONF_TOTAL_POWER = "total_power" CONF_TRACES = "traces" +CONF_TRANSFORM = "transform" CONF_TRANSITION_LENGTH = "transition_length" CONF_TRIGGER_ID = "trigger_id" CONF_TRIGGER_PIN = "trigger_pin" +CONF_TTLS_PHASE_2 = "ttls_phase_2" +CONF_TUNE_ANTENNA = "tune_antenna" CONF_TURN_OFF_ACTION = "turn_off_action" CONF_TURN_ON_ACTION = "turn_on_action" CONF_TVOC = "tvoc" CONF_TX_BUFFER_SIZE = "tx_buffer_size" CONF_TX_PIN = "tx_pin" CONF_TX_POWER = "tx_power" +CONF_TX_QUEUE_LEN = "tx_queue_len" CONF_TYPE = "type" CONF_TYPE_ID = "type_id" CONF_UART_ID = "uart_id" @@ -776,6 +914,7 @@ CONF_UPDATE_ON_BOOT = "update_on_boot" CONF_URL = "url" CONF_USE_ABBREVIATIONS = "use_abbreviations" CONF_USE_ADDRESS = "use_address" +CONF_USE_FAHRENHEIT = "use_fahrenheit" CONF_USERNAME = "username" CONF_UUID = "uuid" CONF_VALIDITY_PERIOD = "validity_period" @@ -784,11 +923,15 @@ CONF_VALUE_FONT = "value_font" CONF_VARIABLES = "variables" CONF_VARIANT = "variant" CONF_VERSION = "version" +CONF_VIBRATIONS = "vibrations" CONF_VISIBLE = "visible" CONF_VISUAL = "visual" CONF_VOLTAGE = "voltage" CONF_VOLTAGE_ATTENUATION = "voltage_attenuation" CONF_VOLTAGE_DIVIDER = "voltage_divider" +CONF_VOLTAGE_GAIN = "voltage_gain" +CONF_VOLUME = "volume" +CONF_VSYNC_PIN = "vsync_pin" CONF_WAIT_TIME = "wait_time" CONF_WAIT_UNTIL = "wait_until" CONF_WAKEUP_PIN = "wakeup_pin" @@ -797,6 +940,7 @@ CONF_WARM_WHITE = "warm_white" CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature" CONF_WATCHDOG_THRESHOLD = "watchdog_threshold" CONF_WEB_SERVER = "web_server" +CONF_WEB_SERVER_ID = "web_server_id" CONF_WEIGHT = "weight" CONF_WHILE = "while" CONF_WHITE = "white" @@ -809,6 +953,7 @@ CONF_WINDOW_SIZE = "window_size" CONF_WRITE_PIN = "write_pin" CONF_X_GRID = "x_grid" CONF_Y_GRID = "y_grid" +CONF_YEAR = "year" CONF_ZERO = "zero" TYPE_GIT = "git" @@ -823,6 +968,7 @@ ICON_ACCELERATION_Y = "mdi:axis-y-arrow" ICON_ACCELERATION_Z = "mdi:axis-z-arrow" ICON_ACCOUNT = "mdi:account" ICON_ACCOUNT_CHECK = "mdi:account-check" +ICON_AIR_FILTER = "mdi:air-filter" ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical" ICON_BATTERY = "mdi:battery" ICON_BLUETOOTH = "mdi:bluetooth" @@ -831,6 +977,7 @@ ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" ICON_BRIGHTNESS_5 = "mdi:brightness-5" ICON_BRIGHTNESS_6 = "mdi:brightness-6" ICON_BUG = "mdi:bug" +ICON_CELLPHONE_ARROW_DOWN = "mdi:cellphone-arrow-down" ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" ICON_CHIP = "mdi:chip" @@ -838,18 +985,25 @@ ICON_COUNTER = "mdi:counter" ICON_CURRENT_AC = "mdi:current-ac" ICON_DATABASE = "mdi:database" ICON_EMPTY = "" +ICON_FAN = "mdi:fan" ICON_FINGERPRINT = "mdi:fingerprint" ICON_FLASH = "mdi:flash" ICON_FLASK = "mdi:flask" ICON_FLASK_OUTLINE = "mdi:flask-outline" +ICON_FLASK_ROUND_BOTTOM = "mdi:flask-round-bottom" ICON_FLOWER = "mdi:flower" ICON_GAS_CYLINDER = "mdi:gas-cylinder" ICON_GAUGE = "mdi:gauge" ICON_GRAIN = "mdi:grain" +ICON_GYROSCOPE_X = "mdi:axis-x-rotate-clockwise" +ICON_GYROSCOPE_Y = "mdi:axis-y-rotate-clockwise" +ICON_GYROSCOPE_Z = "mdi:axis-z-rotate-clockwise" +ICON_HEATING_COIL = "mdi:heating-coil" ICON_KEY_PLUS = "mdi:key-plus" ICON_LIGHTBULB = "mdi:lightbulb" ICON_MAGNET = "mdi:magnet" ICON_MEMORY = "mdi:memory" +ICON_MOLECULE_CO = "mdi:molecule-co" ICON_MOLECULE_CO2 = "mdi:molecule-co2" ICON_MOTION_SENSOR = "mdi:motion-sensor" ICON_NEW_BOX = "mdi:new-box" @@ -873,6 +1027,7 @@ ICON_SIGNAL_DISTANCE_VARIANT = "mdi:signal" ICON_THERMOMETER = "mdi:thermometer" ICON_TIMELAPSE = "mdi:timelapse" ICON_TIMER = "mdi:timer-outline" +ICON_VIBRATE = "mdi:vibrate" ICON_WATER = "mdi:water" ICON_WATER_PERCENT = "mdi:water-percent" ICON_WEATHER_SUNSET = "mdi:weather-sunset" @@ -887,8 +1042,9 @@ UNIT_BYTES = "B" UNIT_CELSIUS = "°C" UNIT_CENTIMETER = "cm" UNIT_COUNT_DECILITRE = "/dL" -UNIT_COUNTS_PER_CUBIC_METER = "#/m³" +UNIT_COUNTS_PER_CUBIC_CENTIMETER = "#/cm³" UNIT_CUBIC_METER = "m³" +UNIT_CUBIC_METER_PER_HOUR = "m³/h" UNIT_DECIBEL = "dB" UNIT_DECIBEL_MILLIWATT = "dBm" UNIT_DEGREE_PER_SECOND = "°/s" @@ -903,18 +1059,25 @@ UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" -UNIT_KILOVOLT_AMPS_REACTIVE = "kVAr" -UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh" +UNIT_KILOVOLT_AMPS = "kVA" +UNIT_KILOVOLT_AMPS_HOURS = "kVAh" +UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" +UNIT_LITRE = "L" UNIT_LUX = "lx" UNIT_METER = "m" UNIT_METER_PER_SECOND_SQUARED = "m/s²" +UNIT_MICROAMP = "µA" UNIT_MICROGRAMS_PER_CUBIC_METER = "µg/m³" UNIT_MICROMETER = "µm" UNIT_MICROSIEMENS_PER_CENTIMETER = "µS/cm" +UNIT_MICROSILVERTS_PER_HOUR = "µSv/h" UNIT_MICROTESLA = "µT" +UNIT_MILLIAMP = "mA" UNIT_MILLIGRAMS_PER_CUBIC_METER = "mg/m³" +UNIT_MILLIMETER = "mm" UNIT_MILLISECOND = "ms" UNIT_MILLISIEMENS_PER_CENTIMETER = "mS/cm" UNIT_MINUTE = "min" @@ -926,10 +1089,12 @@ UNIT_PERCENT = "%" UNIT_PH = "pH" UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = "pulses/min" +UNIT_REVOLUTIONS_PER_MINUTE = "RPM" UNIT_SECOND = "s" UNIT_STEPS = "steps" UNIT_VOLT = "V" UNIT_VOLT_AMPS = "VA" +UNIT_VOLT_AMPS_HOURS = "VAh" UNIT_VOLT_AMPS_REACTIVE = "VAR" UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" UNIT_WATT = "W" @@ -943,9 +1108,11 @@ DEVICE_CLASS_AWNING = "awning" DEVICE_CLASS_BATTERY = "battery" DEVICE_CLASS_BATTERY_CHARGING = "battery_charging" DEVICE_CLASS_BLIND = "blind" +DEVICE_CLASS_BUTTON = "button" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_COLD = "cold" +DEVICE_CLASS_CONDUCTIVITY = "conductivity" DEVICE_CLASS_CONNECTIVITY = "connectivity" DEVICE_CLASS_CURRENT = "current" DEVICE_CLASS_CURTAIN = "curtain" @@ -955,10 +1122,12 @@ DEVICE_CLASS_DATA_SIZE = "data_size" DEVICE_CLASS_DATE = "date" DEVICE_CLASS_DISTANCE = "distance" DEVICE_CLASS_DOOR = "door" +DEVICE_CLASS_DOORBELL = "doorbell" DEVICE_CLASS_DURATION = "duration" DEVICE_CLASS_EMPTY = "" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_ENERGY_STORAGE = "energy_storage" +DEVICE_CLASS_FIRMWARE = "firmware" DEVICE_CLASS_FREQUENCY = "frequency" DEVICE_CLASS_GARAGE = "garage" DEVICE_CLASS_GARAGE_DOOR = "garage_door" @@ -966,6 +1135,7 @@ DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_GATE = "gate" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_HUMIDITY = "humidity" +DEVICE_CLASS_IDENTIFY = "identify" DEVICE_CLASS_ILLUMINANCE = "illuminance" DEVICE_CLASS_IRRADIANCE = "irradiance" DEVICE_CLASS_LIGHT = "light" @@ -981,6 +1151,7 @@ DEVICE_CLASS_OCCUPANCY = "occupancy" DEVICE_CLASS_OPENING = "opening" DEVICE_CLASS_OUTLET = "outlet" DEVICE_CLASS_OZONE = "ozone" +DEVICE_CLASS_PH = "ph" DEVICE_CLASS_PLUG = "plug" DEVICE_CLASS_PM1 = "pm1" DEVICE_CLASS_PM10 = "pm10" @@ -1014,6 +1185,7 @@ DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" DEVICE_CLASS_VOLTAGE = "voltage" DEVICE_CLASS_VOLUME = "volume" +DEVICE_CLASS_VOLUME_FLOW_RATE = "volume_flow_rate" DEVICE_CLASS_VOLUME_STORAGE = "volume_storage" DEVICE_CLASS_WATER = "water" DEVICE_CLASS_WEIGHT = "weight" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 891936adc3..a97c3b18c9 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -7,21 +7,30 @@ from typing import TYPE_CHECKING, Optional, Union from esphome.const import ( CONF_COMMENT, CONF_ESPHOME, - CONF_USE_ADDRESS, CONF_ETHERNET, + CONF_PORT, + CONF_USE_ADDRESS, CONF_WEB_SERVER, CONF_WIFI, - CONF_PORT, KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_HOST, + PLATFORM_RP2040, + PLATFORM_RTL87XX, ) -from esphome.coroutine import FakeAwaitable as _FakeAwaitable -from esphome.coroutine import FakeEventLoop as _FakeEventLoop # pylint: disable=unused-import -from esphome.coroutine import coroutine, coroutine_with_priority # noqa -from esphome.helpers import ensure_unique_string, is_ha_addon +from esphome.coroutine import ( # noqa: F401 + FakeAwaitable as _FakeAwaitable, + FakeEventLoop as _FakeEventLoop, + coroutine, + coroutine_with_priority, +) +from esphome.helpers import ensure_unique_string, get_str_env, is_ha_addon from esphome.util import OrderedDict if TYPE_CHECKING: @@ -81,6 +90,7 @@ def is_approximately_integer(value): class TimePeriod: def __init__( self, + nanoseconds=None, microseconds=None, milliseconds=None, seconds=None, @@ -130,13 +140,23 @@ class TimePeriod: if microseconds is not None: if not is_approximately_integer(microseconds): - raise ValueError("Maximum precision is microseconds") + frac_microseconds, microseconds = math.modf(microseconds) + nanoseconds = (nanoseconds or 0) + frac_microseconds * 1000 self.microseconds = int(round(microseconds)) else: self.microseconds = None + if nanoseconds is not None: + if not is_approximately_integer(nanoseconds): + raise ValueError("Maximum precision is nanoseconds") + self.nanoseconds = int(round(nanoseconds)) + else: + self.nanoseconds = None + def as_dict(self): out = OrderedDict() + if self.nanoseconds is not None: + out["nanoseconds"] = self.nanoseconds if self.microseconds is not None: out["microseconds"] = self.microseconds if self.milliseconds is not None: @@ -152,6 +172,8 @@ class TimePeriod: return out def __str__(self): + if self.nanoseconds is not None: + return f"{self.total_nanoseconds}ns" if self.microseconds is not None: return f"{self.total_microseconds}us" if self.milliseconds is not None: @@ -167,7 +189,11 @@ class TimePeriod: return "0s" def __repr__(self): - return f"TimePeriod<{self.total_microseconds}>" + return f"TimePeriod<{self.total_nanoseconds}ns>" + + @property + def total_nanoseconds(self): + return self.total_microseconds * 1000 + (self.nanoseconds or 0) @property def total_microseconds(self): @@ -195,35 +221,39 @@ class TimePeriod: def __eq__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds == other.total_microseconds + return self.total_nanoseconds == other.total_nanoseconds return NotImplemented def __ne__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds != other.total_microseconds + return self.total_nanoseconds != other.total_nanoseconds return NotImplemented def __lt__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds < other.total_microseconds + return self.total_nanoseconds < other.total_nanoseconds return NotImplemented def __gt__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds > other.total_microseconds + return self.total_nanoseconds > other.total_nanoseconds return NotImplemented def __le__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds <= other.total_microseconds + return self.total_nanoseconds <= other.total_nanoseconds return NotImplemented def __ge__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds >= other.total_microseconds + return self.total_nanoseconds >= other.total_nanoseconds return NotImplemented +class TimePeriodNanoseconds(TimePeriod): + pass + + class TimePeriodMicroseconds(TimePeriod): pass @@ -306,13 +336,15 @@ class ID: else: self.is_manual = is_manual self.is_declaration = is_declaration - self.type: Optional["MockObjClass"] = type + self.type: Optional[MockObjClass] = type def resolve(self, registered_ids): from esphome.config_validation import RESERVED_IDS if self.id is None: base = str(self.type).replace("::", "_").lower() + if base == self.type: + base = base + "_id" name = "".join(c for c in base if c.isalnum() or c == "_") used = set(registered_ids) | set(RESERVED_IDS) | CORE.loaded_integrations self.id = ensure_unique_string(name, used) @@ -458,6 +490,8 @@ class EsphomeCore: self.name: Optional[str] = None # The friendly name of the node self.friendly_name: Optional[str] = None + # The area / zone of the node + self.area: Optional[str] = None # Additional data components can store temporary data in # The first key to this dict should always be the integration name self.data = {} @@ -466,7 +500,7 @@ class EsphomeCore: # The relative path to where all build files are stored self.build_path: Optional[str] = None # The validated configuration, this is None until the config has been validated - self.config: Optional["ConfigType"] = None + self.config: Optional[ConfigType] = None # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -474,17 +508,17 @@ class EsphomeCore: # Task counter for pending tasks self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj - self.variables: dict[str, "MockObj"] = {} + self.variables: dict[str, MockObj] = {} # A list of statements that go in the main setup() block - self.main_statements: list["Statement"] = [] + self.main_statements: list[Statement] = [] # A list of statements to insert in the global block (includes and global variables) - self.global_statements: list["Statement"] = [] + self.global_statements: list[Statement] = [] # A set of platformio libraries to add to the project self.libraries: list[Library] = [] # A set of build flags to set in the platformio project self.build_flags: set[str] = set() # A set of defines to set for the compile process in esphome/core/defines.h - self.defines: set["Define"] = set() + self.defines: set[Define] = set() # A map of all platformio options to apply self.platformio_options: dict[str, Union[str, list[str]]] = {} # A set of strings of names of loaded integrations, used to find namespace ID conflicts @@ -493,11 +527,16 @@ class EsphomeCore: self.component_ids = set() # Whether ESPHome was started in verbose mode self.verbose = False + # Whether ESPHome was started in quiet mode + self.quiet = False def reset(self): + from esphome.pins import PIN_SCHEMA_REGISTRY + self.dashboard = False self.name = None self.friendly_name = None + self.area = None self.data = {} self.config_path = None self.build_path = None @@ -513,6 +552,7 @@ class EsphomeCore: self.platformio_options = {} self.loaded_integrations = set() self.component_ids = set() + PIN_SCHEMA_REGISTRY.reset() @property def address(self) -> Optional[str]: @@ -554,6 +594,14 @@ class EsphomeCore: def config_dir(self): return os.path.dirname(self.config_path) + @property + def data_dir(self): + if is_ha_addon(): + return os.path.join("/data") + if "ESPHOME_DATA_DIR" in os.environ: + return get_str_env("ESPHOME_DATA_DIR", None) + return self.relative_config_path(".esphome") + @property def config_filename(self): return os.path.basename(self.config_path) @@ -563,7 +611,7 @@ class EsphomeCore: return os.path.join(self.config_dir, path_) def relative_internal_path(self, *path: str) -> str: - return self.relative_config_path(".esphome", *path) + return os.path.join(self.data_dir, *path) def relative_build_path(self, *path): path_ = os.path.expanduser(os.path.join(*path)) @@ -573,17 +621,15 @@ class EsphomeCore: return self.relative_build_path("src", *path) def relative_pioenvs_path(self, *path): - if is_ha_addon(): - return os.path.join("/data", self.name, ".pioenvs", *path) return self.relative_build_path(".pioenvs", *path) def relative_piolibdeps_path(self, *path): - if is_ha_addon(): - return os.path.join("/data", self.name, ".piolibdeps", *path) return self.relative_build_path(".piolibdeps", *path) @property def firmware_bin(self): + if self.is_libretiny: + return self.relative_pioenvs_path(self.name, "firmware.uf2") return self.relative_pioenvs_path(self.name, "firmware.bin") @property @@ -592,19 +638,31 @@ class EsphomeCore: @property def is_esp8266(self): - return self.target_platform == "esp8266" + return self.target_platform == PLATFORM_ESP8266 @property def is_esp32(self): - return self.target_platform == "esp32" + return self.target_platform == PLATFORM_ESP32 @property def is_rp2040(self): - return self.target_platform == "rp2040" + return self.target_platform == PLATFORM_RP2040 + + @property + def is_bk72xx(self): + return self.target_platform == PLATFORM_BK72XX + + @property + def is_rtl87xx(self): + return self.target_platform == PLATFORM_RTL87XX + + @property + def is_libretiny(self): + return self.is_bk72xx or self.is_rtl87xx @property def is_host(self): - return self.target_platform == "host" + return self.target_platform == PLATFORM_HOST @property def target_framework(self): diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c012195f34..a4550bcd9e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -81,13 +81,11 @@ void Application::loop() { const uint32_t now = millis(); - if (HighFrequencyLoopRequester::is_high_frequency()) { + auto elapsed = now - this->last_loop_; + if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { yield(); } else { - uint32_t delay_time = this->loop_interval_; - if (now - this->last_loop_ < this->loop_interval_) - delay_time = this->loop_interval_ - (now - this->last_loop_); - + uint32_t delay_time = this->loop_interval_ - elapsed; uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time); // next_schedule is max 0.5*delay_time // otherwise interval=0 schedules result in constant looping with almost no sleep @@ -99,7 +97,7 @@ 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_.c_str()); + ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_); #ifdef ESPHOME_PROJECT_NAME ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); #endif diff --git a/esphome/core/application.h b/esphome/core/application.h index 0992a4df39..462beb1f25 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -39,22 +39,46 @@ #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_DATETIME_DATE +#include "esphome/components/datetime/date_entity.h" +#endif +#ifdef USE_DATETIME_TIME +#include "esphome/components/datetime/time_entity.h" +#endif +#ifdef USE_DATETIME_DATETIME +#include "esphome/components/datetime/datetime_entity.h" +#endif +#ifdef USE_TEXT +#include "esphome/components/text/text.h" +#endif #ifdef USE_SELECT #include "esphome/components/select/select.h" #endif #ifdef USE_LOCK #include "esphome/components/lock/lock.h" #endif +#ifdef USE_VALVE +#include "esphome/components/valve/valve.h" +#endif #ifdef USE_MEDIA_PLAYER #include "esphome/components/media_player/media_player.h" #endif +#ifdef USE_ALARM_CONTROL_PANEL +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#endif +#ifdef USE_EVENT +#include "esphome/components/event/event.h" +#endif +#ifdef USE_UPDATE +#include "esphome/components/update/update_entity.h" +#endif namespace esphome { class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &comment, - const char *compilation_time, bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &area, + const char *comment, const char *compilation_time, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { @@ -68,6 +92,7 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } + this->area_ = area; this->comment_ = comment; this->compilation_time_ = compilation_time; } @@ -114,6 +139,22 @@ class Application { void register_number(number::Number *number) { this->numbers_.push_back(number); } #endif +#ifdef USE_DATETIME_DATE + void register_date(datetime::DateEntity *date) { this->dates_.push_back(date); } +#endif + +#ifdef USE_DATETIME_TIME + void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); } +#endif + +#ifdef USE_DATETIME_DATETIME + void register_datetime(datetime::DateTimeEntity *datetime) { this->datetimes_.push_back(datetime); } +#endif + +#ifdef USE_TEXT + void register_text(text::Text *text) { this->texts_.push_back(text); } +#endif + #ifdef USE_SELECT void register_select(select::Select *select) { this->selects_.push_back(select); } #endif @@ -122,10 +163,28 @@ class Application { void register_lock(lock::Lock *a_lock) { this->locks_.push_back(a_lock); } #endif +#ifdef USE_VALVE + void register_valve(valve::Valve *valve) { this->valves_.push_back(valve); } +#endif + #ifdef USE_MEDIA_PLAYER void register_media_player(media_player::MediaPlayer *media_player) { this->media_players_.push_back(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL + void register_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + this->alarm_control_panels_.push_back(a_alarm_control_panel); + } +#endif + +#ifdef USE_EVENT + void register_event(event::Event *event) { this->events_.push_back(event); } +#endif + +#ifdef USE_UPDATE + void register_update(update::UpdateEntity *update) { this->updates_.push_back(update); } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -144,12 +203,16 @@ class Application { /// Get the friendly name of this Application set by pre_setup(). const std::string &get_friendly_name() const { return this->friendly_name_; } + + /// Get the area of this Application set by pre_setup(). + const std::string &get_area() const { return this->area_; } + /// Get the comment of this Application set by pre_setup(). - const std::string &get_comment() const { return this->comment_; } + std::string get_comment() const { return this->comment_; } bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } - const std::string &get_compilation_time() const { return this->compilation_time_; } + std::string get_compilation_time() const { return this->compilation_time_; } /** Set the target interval with which to run the loop() calls. * If the loop() method takes longer than the target interval, ESPHome won't @@ -166,6 +229,8 @@ class Application { */ void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; } + uint32_t get_loop_interval() const { return this->loop_interval_; } + void schedule_dump_config() { this->dump_config_at_ = 0; } void feed_wdt(); @@ -181,117 +246,215 @@ class Application { #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->binary_sensors_) + for (auto *obj : this->binary_sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_SWITCH const std::vector &get_switches() { return this->switches_; } switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->switches_) + for (auto *obj : this->switches_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_BUTTON const std::vector &get_buttons() { return this->buttons_; } button::Button *get_button_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->buttons_) + for (auto *obj : this->buttons_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_SENSOR const std::vector &get_sensors() { return this->sensors_; } sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->sensors_) + for (auto *obj : this->sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_TEXT_SENSOR const std::vector &get_text_sensors() { return this->text_sensors_; } text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->text_sensors_) + for (auto *obj : this->text_sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_FAN const std::vector &get_fans() { return this->fans_; } fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->fans_) + for (auto *obj : this->fans_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_COVER const std::vector &get_covers() { return this->covers_; } cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->covers_) + for (auto *obj : this->covers_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_LIGHT const std::vector &get_lights() { return this->lights_; } light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->lights_) + for (auto *obj : this->lights_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_CLIMATE const std::vector &get_climates() { return this->climates_; } climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->climates_) + for (auto *obj : this->climates_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_NUMBER const std::vector &get_numbers() { return this->numbers_; } number::Number *get_number_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->numbers_) + for (auto *obj : this->numbers_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } + return nullptr; + } +#endif +#ifdef USE_DATETIME_DATE + const std::vector &get_dates() { return this->dates_; } + datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->dates_) { + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + } + return nullptr; + } +#endif +#ifdef USE_DATETIME_TIME + const std::vector &get_times() { return this->times_; } + datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->times_) { + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + } + return nullptr; + } +#endif +#ifdef USE_DATETIME_DATETIME + const std::vector &get_datetimes() { return this->datetimes_; } + datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->datetimes_) { + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + } + return nullptr; + } +#endif +#ifdef USE_TEXT + const std::vector &get_texts() { return this->texts_; } + text::Text *get_text_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->texts_) { + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + } return nullptr; } #endif #ifdef USE_SELECT const std::vector &get_selects() { return this->selects_; } select::Select *get_select_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->selects_) + for (auto *obj : this->selects_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_LOCK const std::vector &get_locks() { return this->locks_; } lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->locks_) + for (auto *obj : this->locks_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } + return nullptr; + } +#endif +#ifdef USE_VALVE + const std::vector &get_valves() { return this->valves_; } + valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->valves_) { + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + } return nullptr; } #endif #ifdef USE_MEDIA_PLAYER const std::vector &get_media_players() { return this->media_players_; } media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->media_players_) + for (auto *obj : this->media_players_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } + return nullptr; + } +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + const std::vector &get_alarm_control_panels() { + return this->alarm_control_panels_; + } + alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->alarm_control_panels_) { + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + } + return nullptr; + } +#endif + +#ifdef USE_EVENT + const std::vector &get_events() { return this->events_; } + event::Event *get_event_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->events_) { + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + } + return nullptr; + } +#endif + +#ifdef USE_UPDATE + const std::vector &get_updates() { return this->updates_; } + update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->updates_) { + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + } return nullptr; } #endif @@ -319,6 +482,9 @@ class Application { #ifdef USE_BUTTON std::vector buttons_{}; #endif +#ifdef USE_EVENT + std::vector events_{}; +#endif #ifdef USE_SENSOR std::vector sensors_{}; #endif @@ -340,20 +506,42 @@ class Application { #ifdef USE_NUMBER std::vector numbers_{}; #endif +#ifdef USE_DATETIME_DATE + std::vector dates_{}; +#endif +#ifdef USE_DATETIME_TIME + std::vector times_{}; +#endif +#ifdef USE_DATETIME_DATETIME + std::vector datetimes_{}; +#endif #ifdef USE_SELECT std::vector selects_{}; #endif +#ifdef USE_TEXT + std::vector texts_{}; +#endif #ifdef USE_LOCK std::vector locks_{}; #endif +#ifdef USE_VALVE + std::vector valves_{}; +#endif #ifdef USE_MEDIA_PLAYER std::vector media_players_{}; #endif +#ifdef USE_ALARM_CONTROL_PANEL + std::vector alarm_control_panels_{}; +#endif +#ifdef USE_UPDATE + std::vector updates_{}; +#endif std::string name_; std::string friendly_name_; - std::string comment_; - std::string compilation_time_; + std::string area_; + const char *comment_{nullptr}; + const char *compilation_time_{nullptr}; bool name_add_mac_suffix_; uint32_t last_loop_{0}; uint32_t loop_interval_{16}; diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 84c754e081..e77e453431 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -24,7 +24,7 @@ template struct gens<0, S...> { using type = seq; }; // NOLINT template class TemplatableValue { public: - TemplatableValue() : type_(EMPTY) {} + TemplatableValue() : type_(NONE) {} template::value, int> = 0> TemplatableValue(F value) : type_(VALUE), value_(value) {} @@ -32,13 +32,13 @@ template class TemplatableValue { template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA), f_(f) {} - bool has_value() { return this->type_ != EMPTY; } + bool has_value() { return this->type_ != NONE; } T value(X... x) { if (this->type_ == LAMBDA) { return this->f_(x...); } - // return value also when empty + // return value also when none return this->value_; } @@ -58,7 +58,7 @@ template class TemplatableValue { protected: enum { - EMPTY, + NONE, VALUE, LAMBDA, } type_; @@ -82,7 +82,7 @@ template class Condition { } protected: - template bool check_tuple_(const std::tuple &tuple, seq) { + template bool check_tuple_(const std::tuple &tuple, seq /*unused*/) { return this->check(std::get(tuple)...); } }; @@ -156,7 +156,7 @@ template class Action { } } } - template void play_next_tuple_(const std::tuple &tuple, seq) { + template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { this->play_next_(std::get(tuple)...); } void play_next_tuple_(const std::tuple &tuple) { @@ -218,12 +218,14 @@ template class ActionList { /// Return the number of actions in this action list that are currently running. int num_running() { if (this->actions_begin_ == nullptr) - return false; + return 0; return this->actions_begin_->num_running_total(); } protected: - template void play_tuple_(const std::tuple &tuple, seq) { this->play(std::get(tuple)...); } + template void play_tuple_(const std::tuple &tuple, seq /*unused*/) { + this->play(std::get(tuple)...); + } Action *actions_begin_{nullptr}; Action *actions_end_{nullptr}; @@ -233,7 +235,7 @@ template class Automation { public: explicit Automation(Trigger *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); } - Action *add_action(Action *action) { this->actions_.add_action(action); } + void add_action(Action *action) { this->actions_.add_action(action); } void add_actions(const std::vector *> &actions) { this->actions_.add_actions(actions); } void stop() { this->actions_.stop(); } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index daa09b912e..dcf7da2f21 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -2,6 +2,8 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" #include @@ -48,6 +50,22 @@ template class NotCondition : public Condition { Condition *condition_; }; +template class XorCondition : public Condition { + public: + explicit XorCondition(const std::vector *> &conditions) : conditions_(conditions) {} + bool check(Ts... x) override { + size_t result = 0; + for (auto *condition : this->conditions_) { + result += condition->check(x...); + } + + return result == 1; + } + + protected: + std::vector *> conditions_; +}; + template class LambdaCondition : public Condition { public: explicit LambdaCondition(std::function &&f) : f_(std::move(f)) {} @@ -109,6 +127,27 @@ class LoopTrigger : public Trigger<>, public Component { float get_setup_priority() const override { return setup_priority::DATA; } }; +#ifdef ESPHOME_PROJECT_NAME +class ProjectUpdateTrigger : public Trigger, public Component { + public: + void setup() override { + uint32_t hash = fnv1_hash(ESPHOME_PROJECT_NAME); + ESPPreferenceObject pref = global_preferences->make_preference(hash, true); + char previous_version[30]; + char current_version[30] = ESPHOME_PROJECT_VERSION_30; + if (pref.load(&previous_version)) { + int cmp = strcmp(previous_version, current_version); + if (cmp < 0) { + this->trigger(previous_version); + } + } + pref.save(¤t_version); + global_preferences->sync(); + } + float get_setup_priority() const override { return setup_priority::PROCESSOR; } +}; +#endif + template class DelayAction : public Action, public Component { public: explicit DelayAction() = default; @@ -239,17 +278,22 @@ template class RepeatAction : public Action { this->then_.add_actions(actions); this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { iteration++; - if (iteration >= this->count_.value(x...)) + if (iteration >= this->count_.value(x...)) { this->play_next_tuple_(this->var_); - else + } else { this->then_.play(iteration, x...); + } })); } void play_complex(Ts... x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - this->then_.play(0, x...); + if (this->count_.value(x...) > 0) { + this->then_.play(0, x...); + } else { + this->play_next_tuple_(this->var_); + } } void play(Ts... x) override { /* ignore - see play_complex */ @@ -326,4 +370,38 @@ template class UpdateComponentAction : public Action { PollingComponent *component_; }; +template class SuspendComponentAction : public Action { + public: + SuspendComponentAction(PollingComponent *component) : component_(component) {} + + void play(Ts... x) override { + if (!this->component_->is_ready()) + return; + this->component_->stop_poller(); + } + + protected: + PollingComponent *component_; +}; + +template class ResumeComponentAction : public Action { + public: + ResumeComponentAction(PollingComponent *component) : component_(component) {} + TEMPLATABLE_VALUE(uint32_t, update_interval) + + void play(Ts... x) override { + if (!this->component_->is_ready()) { + return; + } + optional update_interval = this->update_interval_.optional_value(x...); + if (update_interval.has_value()) { + this->component_->set_update_interval(update_interval.value()); + } + this->component_->start_poller(); + } + + protected: + PollingComponent *component_; +}; + } // namespace esphome diff --git a/esphome/core/color.h b/esphome/core/color.h index 7062a2a8c8..1c43fd9d3e 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -31,19 +31,19 @@ struct Color { uint32_t raw_32; }; - inline Color() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {} // NOLINT - inline Color(uint8_t red, uint8_t green, uint8_t blue) ALWAYS_INLINE : r(red), g(green), b(blue), w(0) {} + inline Color() ESPHOME_ALWAYS_INLINE : r(0), g(0), b(0), w(0) {} // NOLINT + inline Color(uint8_t red, uint8_t green, uint8_t blue) ESPHOME_ALWAYS_INLINE : r(red), g(green), b(blue), w(0) {} - inline Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ALWAYS_INLINE : r(red), - g(green), - b(blue), - w(white) {} - inline explicit Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), - g((colorcode >> 8) & 0xFF), - b((colorcode >> 0) & 0xFF), - w((colorcode >> 24) & 0xFF) {} + inline Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ESPHOME_ALWAYS_INLINE : r(red), + g(green), + b(blue), + w(white) {} + inline explicit Color(uint32_t colorcode) ESPHOME_ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), + g((colorcode >> 8) & 0xFF), + b((colorcode >> 0) & 0xFF), + w((colorcode >> 24) & 0xFF) {} - inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; } + inline bool is_on() ESPHOME_ALWAYS_INLINE { return this->raw_32 != 0; } inline bool operator==(const Color &rhs) { // NOLINT return this->raw_32 == rhs.raw_32; @@ -57,92 +57,88 @@ struct Color { inline bool operator!=(uint32_t colorcode) { // NOLINT return this->raw_32 != colorcode; } - - inline Color &operator=(const Color &rhs) ALWAYS_INLINE { // NOLINT - this->r = rhs.r; - this->g = rhs.g; - this->b = rhs.b; - this->w = rhs.w; - return *this; - } - inline Color &operator=(uint32_t colorcode) ALWAYS_INLINE { - this->w = (colorcode >> 24) & 0xFF; - this->r = (colorcode >> 16) & 0xFF; - this->g = (colorcode >> 8) & 0xFF; - this->b = (colorcode >> 0) & 0xFF; - return *this; - } - inline uint8_t &operator[](uint8_t x) ALWAYS_INLINE { return this->raw[x]; } - inline Color operator*(uint8_t scale) const ALWAYS_INLINE { + inline uint8_t &operator[](uint8_t x) ESPHOME_ALWAYS_INLINE { return this->raw[x]; } + inline Color operator*(uint8_t scale) const ESPHOME_ALWAYS_INLINE { return Color(esp_scale8(this->red, scale), esp_scale8(this->green, scale), esp_scale8(this->blue, scale), esp_scale8(this->white, scale)); } - inline Color &operator*=(uint8_t scale) ALWAYS_INLINE { + inline Color operator~() const ESPHOME_ALWAYS_INLINE { + return Color(255 - this->red, 255 - this->green, 255 - this->blue); + } + inline Color &operator*=(uint8_t scale) ESPHOME_ALWAYS_INLINE { this->red = esp_scale8(this->red, scale); this->green = esp_scale8(this->green, scale); this->blue = esp_scale8(this->blue, scale); this->white = esp_scale8(this->white, scale); return *this; } - inline Color operator*(const Color &scale) const ALWAYS_INLINE { + inline Color operator*(const Color &scale) const ESPHOME_ALWAYS_INLINE { return Color(esp_scale8(this->red, scale.red), esp_scale8(this->green, scale.green), esp_scale8(this->blue, scale.blue), esp_scale8(this->white, scale.white)); } - inline Color &operator*=(const Color &scale) ALWAYS_INLINE { + inline Color &operator*=(const Color &scale) ESPHOME_ALWAYS_INLINE { this->red = esp_scale8(this->red, scale.red); this->green = esp_scale8(this->green, scale.green); this->blue = esp_scale8(this->blue, scale.blue); this->white = esp_scale8(this->white, scale.white); return *this; } - inline Color operator+(const Color &add) const ALWAYS_INLINE { + inline Color operator+(const Color &add) const ESPHOME_ALWAYS_INLINE { Color ret; - if (uint8_t(add.r + this->r) < this->r) + if (uint8_t(add.r + this->r) < this->r) { ret.r = 255; - else + } else { ret.r = this->r + add.r; - if (uint8_t(add.g + this->g) < this->g) + } + if (uint8_t(add.g + this->g) < this->g) { ret.g = 255; - else + } else { ret.g = this->g + add.g; - if (uint8_t(add.b + this->b) < this->b) + } + if (uint8_t(add.b + this->b) < this->b) { ret.b = 255; - else + } else { ret.b = this->b + add.b; - if (uint8_t(add.w + this->w) < this->w) + } + if (uint8_t(add.w + this->w) < this->w) { ret.w = 255; - else + } else { ret.w = this->w + add.w; + } return ret; } - inline Color &operator+=(const Color &add) ALWAYS_INLINE { return *this = (*this) + add; } - inline Color operator+(uint8_t add) const ALWAYS_INLINE { return (*this) + Color(add, add, add, add); } - inline Color &operator+=(uint8_t add) ALWAYS_INLINE { return *this = (*this) + add; } - inline Color operator-(const Color &subtract) const ALWAYS_INLINE { + inline Color &operator+=(const Color &add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; } + inline Color operator+(uint8_t add) const ESPHOME_ALWAYS_INLINE { return (*this) + Color(add, add, add, add); } + inline Color &operator+=(uint8_t add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; } + inline Color operator-(const Color &subtract) const ESPHOME_ALWAYS_INLINE { Color ret; - if (subtract.r > this->r) + if (subtract.r > this->r) { ret.r = 0; - else + } else { ret.r = this->r - subtract.r; - if (subtract.g > this->g) + } + if (subtract.g > this->g) { ret.g = 0; - else + } else { ret.g = this->g - subtract.g; - if (subtract.b > this->b) + } + if (subtract.b > this->b) { ret.b = 0; - else + } else { ret.b = this->b - subtract.b; - if (subtract.w > this->w) + } + if (subtract.w > this->w) { ret.w = 0; - else + } else { ret.w = this->w - subtract.w; + } return ret; } - inline Color &operator-=(const Color &subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } - inline Color operator-(uint8_t subtract) const ALWAYS_INLINE { + inline Color &operator-=(const Color &subtract) ESPHOME_ALWAYS_INLINE { return *this = (*this) - subtract; } + inline Color operator-(uint8_t subtract) const ESPHOME_ALWAYS_INLINE { return (*this) - Color(subtract, subtract, subtract, subtract); } - inline Color &operator-=(uint8_t subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } + inline Color &operator-=(uint8_t subtract) ESPHOME_ALWAYS_INLINE { return *this = (*this) - subtract; } static Color random_color() { uint32_t rand = random_uint32(); uint8_t w = rand >> 24; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 49ef8ecde7..ae73a451d9 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -1,10 +1,11 @@ #include "esphome/core/component.h" +#include +#include #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include namespace esphome { @@ -75,7 +76,12 @@ bool Component::cancel_timeout(const std::string &name) { // NOLINT void Component::call_loop() { this->loop(); } void Component::call_setup() { this->setup(); } -void Component::call_dump_config() { this->dump_config(); } +void Component::call_dump_config() { + this->dump_config(); + if (this->is_failed()) { + ESP_LOGE(TAG, " Component %s is marked FAILED", this->get_component_source()); + } +} uint32_t Component::get_component_state() const { return this->component_state_; } void Component::call() { @@ -134,24 +140,41 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: float backoff_increase_factor) { // NOLINT App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); } -bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } -bool Component::is_ready() { +bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } +bool Component::is_ready() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; } bool Component::can_proceed() { return true; } -bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; } -bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; } -void Component::status_set_warning() { +bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } +bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } +void Component::status_set_warning(const char *message) { + // Don't spam the log. This risks missing different warning messages though. + if ((this->component_state_ & STATUS_LED_WARNING) != 0) + return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; + ESP_LOGW(TAG, "Component %s set Warning flag: %s", this->get_component_source(), message); } -void Component::status_set_error() { +void Component::status_set_error(const char *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, "Component %s set Error flag: %s", this->get_component_source(), message); +} +void Component::status_clear_warning() { + if ((this->component_state_ & STATUS_LED_WARNING) == 0) + return; + this->component_state_ &= ~STATUS_LED_WARNING; + ESP_LOGW(TAG, "Component %s cleared Warning flag", this->get_component_source()); +} +void Component::status_clear_error() { + if ((this->component_state_ & STATUS_LED_ERROR) == 0) + return; + this->component_state_ &= ~STATUS_LED_ERROR; + ESP_LOGE(TAG, "Component %s cleared Error flag", this->get_component_source()); } -void Component::status_clear_warning() { this->component_state_ &= ~STATUS_LED_WARNING; } -void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; } void Component::status_momentary_warning(const std::string &name, uint32_t length) { this->status_set_warning(); this->set_timeout(name, length, [this]() { this->status_clear_warning(); }); @@ -169,7 +192,7 @@ float Component::get_actual_setup_priority() const { void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } bool Component::has_overridden_loop() const { -#ifdef CLANG_TIDY +#if defined(USE_HOST) || defined(CLANG_TIDY) bool loop_overridden = true; bool call_loop_overridden = true; #else @@ -188,10 +211,20 @@ void PollingComponent::call_setup() { // Let the polling component subclass setup their HW. this->setup(); + // init the poller + this->start_poller(); +} + +void PollingComponent::start_poller() { // Register interval. this->set_interval("update", this->get_update_interval(), [this]() { this->update(); }); } +void PollingComponent::stop_poller() { + // Clear the interval to suspend component + this->cancel_interval("update"); +} + uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } @@ -201,8 +234,8 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() { uint32_t now = millis(); if (now - started_ > 50) { const char *src = component_ == nullptr ? "" : component_->get_component_source(); - ESP_LOGV(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f); - ESP_LOGV(TAG, "Components should block for at most 20-30ms."); + ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, (now - started_)); + ESP_LOGW(TAG, "Components should block for at most 30 ms."); ; } } diff --git a/esphome/core/component.h b/esphome/core/component.h index 7382f1c617..a6bd8f81ac 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -1,8 +1,9 @@ #pragma once -#include -#include #include +#include +#include +#include #include "esphome/core/optional.h" @@ -84,7 +85,7 @@ class Component { /** priority of setup(). higher -> executed earlier * - * Defaults to 0. + * Defaults to setup_priority::DATA, i.e. 600. * * @return The setup priority of this component */ @@ -117,19 +118,19 @@ class Component { */ virtual void mark_failed(); - bool is_failed(); + bool is_failed() const; - bool is_ready(); + bool is_ready() const; virtual bool can_proceed(); - bool status_has_warning(); + bool status_has_warning() const; - bool status_has_error(); + bool status_has_error() const; - void status_set_warning(); + void status_set_warning(const char *message = "unspecified"); - void status_set_error(); + void status_set_error(const char *message = "unspecified"); void status_clear_warning(); @@ -192,7 +193,7 @@ class Component { * again in the future. * * The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is - * increased by multipling by `backoff_increase_factor` each time. If no backoff_increase_factor is + * increased by multiplying by `backoff_increase_factor` each time. If no backoff_increase_factor is * supplied (default = 1.0), the wait time will stay constant. * * The retry function f needs to accept a single argument: the number of attempts remaining. On the @@ -308,6 +309,12 @@ class PollingComponent : public Component { /// Get the update interval in ms of this sensor virtual uint32_t get_update_interval() const; + // Start the poller, used for component.suspend + void start_poller(); + + // Stop the poller, used for component.suspend + void stop_poller(); + protected: uint32_t update_interval_; }; diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 16c56a90c5..da593340c1 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -202,6 +202,66 @@ void ComponentIterator::advance() { } break; #endif +#ifdef USE_DATETIME_DATE + case IteratorState::DATETIME_DATE: + if (this->at_ >= App.get_dates().size()) { + advance_platform = true; + } else { + auto *date = App.get_dates()[this->at_]; + if (date->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_date(date); + } + } + break; +#endif +#ifdef USE_DATETIME_TIME + case IteratorState::DATETIME_TIME: + if (this->at_ >= App.get_times().size()) { + advance_platform = true; + } else { + auto *time = App.get_times()[this->at_]; + if (time->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_time(time); + } + } + break; +#endif +#ifdef USE_DATETIME_DATETIME + case IteratorState::DATETIME_DATETIME: + if (this->at_ >= App.get_datetimes().size()) { + advance_platform = true; + } else { + auto *datetime = App.get_datetimes()[this->at_]; + if (datetime->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_datetime(datetime); + } + } + break; +#endif +#ifdef USE_TEXT + case IteratorState::TEXT: + if (this->at_ >= App.get_texts().size()) { + advance_platform = true; + } else { + auto *text = App.get_texts()[this->at_]; + if (text->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_text(text); + } + } + break; +#endif #ifdef USE_SELECT case IteratorState::SELECT: if (this->at_ >= App.get_selects().size()) { @@ -232,6 +292,21 @@ void ComponentIterator::advance() { } break; #endif +#ifdef USE_VALVE + case IteratorState::VALVE: + if (this->at_ >= App.get_valves().size()) { + advance_platform = true; + } else { + auto *valve = App.get_valves()[this->at_]; + if (valve->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_valve(valve); + } + } + break; +#endif #ifdef USE_MEDIA_PLAYER case IteratorState::MEDIA_PLAYER: if (this->at_ >= App.get_media_players().size()) { @@ -246,6 +321,51 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + case IteratorState::ALARM_CONTROL_PANEL: + if (this->at_ >= App.get_alarm_control_panels().size()) { + advance_platform = true; + } else { + auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_]; + if (a_alarm_control_panel->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_alarm_control_panel(a_alarm_control_panel); + } + } + break; +#endif +#ifdef USE_EVENT + case IteratorState::EVENT: + if (this->at_ >= App.get_events().size()) { + advance_platform = true; + } else { + auto *event = App.get_events()[this->at_]; + if (event->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_event(event); + } + } + break; +#endif +#ifdef USE_UPDATE + case IteratorState::UPDATE: + if (this->at_ >= App.get_updates().size()) { + advance_platform = true; + } else { + auto *update = App.get_updates()[this->at_]; + if (update->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_update(update); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index e8e048bd73..9e187f6c57 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -57,14 +57,38 @@ class ComponentIterator { #ifdef USE_NUMBER virtual bool on_number(number::Number *number) = 0; #endif +#ifdef USE_DATETIME_DATE + virtual bool on_date(datetime::DateEntity *date) = 0; +#endif +#ifdef USE_DATETIME_TIME + virtual bool on_time(datetime::TimeEntity *time) = 0; +#endif +#ifdef USE_DATETIME_DATETIME + virtual bool on_datetime(datetime::DateTimeEntity *datetime) = 0; +#endif +#ifdef USE_TEXT + virtual bool on_text(text::Text *text) = 0; +#endif #ifdef USE_SELECT virtual bool on_select(select::Select *select) = 0; #endif #ifdef USE_LOCK virtual bool on_lock(lock::Lock *a_lock) = 0; #endif +#ifdef USE_VALVE + virtual bool on_valve(valve::Valve *valve) = 0; +#endif #ifdef USE_MEDIA_PLAYER virtual bool on_media_player(media_player::MediaPlayer *media_player); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) = 0; +#endif +#ifdef USE_EVENT + virtual bool on_event(event::Event *event) = 0; +#endif +#ifdef USE_UPDATE + virtual bool on_update(update::UpdateEntity *update) = 0; #endif virtual bool on_end(); @@ -108,14 +132,38 @@ class ComponentIterator { #ifdef USE_NUMBER NUMBER, #endif +#ifdef USE_DATETIME_DATE + DATETIME_DATE, +#endif +#ifdef USE_DATETIME_TIME + DATETIME_TIME, +#endif +#ifdef USE_DATETIME_DATETIME + DATETIME_DATETIME, +#endif +#ifdef USE_TEXT + TEXT, +#endif #ifdef USE_SELECT SELECT, #endif #ifdef USE_LOCK LOCK, #endif +#ifdef USE_VALVE + VALVE, +#endif #ifdef USE_MEDIA_PLAYER MEDIA_PLAYER, +#endif +#ifdef USE_ALARM_CONTROL_PANEL + ALARM_CONTROL_PANEL, +#endif +#ifdef USE_EVENT + EVENT, +#endif +#ifdef USE_UPDATE + UPDATE, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/core/config.py b/esphome/core/config.py index ef6553026e..8c130eb6db 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -3,11 +3,12 @@ import multiprocessing import os import re +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation from esphome.const import ( CONF_ARDUINO_VERSION, + CONF_AREA, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, @@ -15,14 +16,15 @@ from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, CONF_FRAMEWORK, + CONF_FRIENDLY_NAME, CONF_INCLUDES, CONF_LIBRARIES, CONF_MIN_VERSION, CONF_NAME, - CONF_FRIENDLY_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, + CONF_ON_UPDATE, CONF_PLATFORM, CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, @@ -32,12 +34,12 @@ from esphome.const import ( CONF_TYPE, CONF_VERSION, KEY_CORE, - TARGET_PLATFORMS, PLATFORM_ESP8266, + TARGET_PLATFORMS, __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority -from esphome.helpers import copy_file_if_changed, walk_files +from esphome.helpers import copy_file_if_changed, get_str_env, walk_files _LOGGER = logging.getLogger(__name__) @@ -51,6 +53,9 @@ ShutdownTrigger = cg.esphome_ns.class_( LoopTrigger = cg.esphome_ns.class_( "LoopTrigger", cg.Component, automation.Trigger.template() ) +ProjectUpdateTrigger = cg.esphome_ns.class_( + "ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string) +) VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$") @@ -95,19 +100,6 @@ def valid_include(value): def valid_project_name(value: str): if value.count(".") != 1: raise cv.Invalid("project name needs to have a namespace") - - value = value.replace(" ", "_") - - return value - - -def validate_version(value: str): - min_version = cv.Version.parse(value) - current_version = cv.Version.parse(ESPHOME_VERSION) - if current_version < min_version: - raise cv.Invalid( - f"Your ESPHome version is too old. Please update to at least {min_version}" - ) return value @@ -126,6 +118,7 @@ CONFIG_SCHEMA = cv.All( { cv.Required(CONF_NAME): cv.valid_name, cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, + cv.Optional(CONF_AREA, ""): cv.string, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( @@ -159,10 +152,17 @@ CONFIG_SCHEMA = cv.All( cv.string_strict, valid_project_name ), cv.Required(CONF_VERSION): cv.string_strict, + cv.Optional(CONF_ON_UPDATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ProjectUpdateTrigger + ), + } + ), } ), cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( - cv.version_number, validate_version + cv.version_number, cv.validate_esphome_version ), cv.Optional( CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default @@ -198,8 +198,9 @@ def preload_core_config(config, result): CORE.data[KEY_CORE] = {} if CONF_BUILD_PATH not in conf: - conf[CONF_BUILD_PATH] = f".esphome/build/{CORE.name}" - CORE.build_path = CORE.relative_config_path(conf[CONF_BUILD_PATH]) + build_path = get_str_env("ESPHOME_BUILD_PATH", "build") + conf[CONF_BUILD_PATH] = os.path.join(build_path, CORE.name) + CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH]) has_oldstyle = CONF_PLATFORM in conf newstyle_found = [key for key in TARGET_PLATFORMS if key in config] @@ -317,6 +318,8 @@ async def add_includes(includes): async def _add_platformio_options(pio_options): # Add includes at the very end, so that they override everything for key, val in pio_options.items(): + if key == "build_flags" and not isinstance(val, list): + val = [val] cg.add_platformio_option(key, val) @@ -350,6 +353,7 @@ async def to_code(config): cg.App.pre_setup( config[CONF_NAME], config[CONF_FRIENDLY_NAME], + config[CONF_AREA], config.get(CONF_COMMENT, ""), cg.RawExpression('__DATE__ ", " __TIME__'), config[CONF_NAME_ADD_MAC_SUFFIX], @@ -380,15 +384,22 @@ async def to_code(config): cg.add_build_flag("-Wno-unused-but-set-variable") cg.add_build_flag("-Wno-sign-compare") - if CORE.using_arduino: + if CORE.using_arduino and not CORE.is_bk72xx: CORE.add_job(add_arduino_global_workaround) if config[CONF_INCLUDES]: CORE.add_job(add_includes, config[CONF_INCLUDES]) - if CONF_PROJECT in config: - cg.add_define("ESPHOME_PROJECT_NAME", config[CONF_PROJECT][CONF_NAME]) - cg.add_define("ESPHOME_PROJECT_VERSION", config[CONF_PROJECT][CONF_VERSION]) + if project_conf := config.get(CONF_PROJECT): + cg.add_define("ESPHOME_PROJECT_NAME", project_conf[CONF_NAME]) + cg.add_define("ESPHOME_PROJECT_VERSION", project_conf[CONF_VERSION]) + cg.add_define("ESPHOME_PROJECT_VERSION_30", project_conf[CONF_VERSION][:29]) + for conf in project_conf.get(CONF_ON_UPDATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + await cg.register_component(trigger, conf) + await automation.build_automation( + trigger, [(cg.std_string, "version")], conf + ) if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 857ee6398f..d6d98a4316 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -1,6 +1,6 @@ #include "controller.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" namespace esphome { @@ -50,7 +50,7 @@ void Controller::setup_controller(bool include_internal) { #ifdef USE_CLIMATE for (auto *obj : App.get_climates()) { if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_climate_update(obj); }); + obj->add_on_state_callback([this, obj](climate::Climate & /*unused*/) { this->on_climate_update(obj); }); } #endif #ifdef USE_NUMBER @@ -59,6 +59,30 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); } #endif +#ifdef USE_DATETIME_DATE + for (auto *obj : App.get_dates()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); }); + } +#endif +#ifdef USE_DATETIME_TIME + for (auto *obj : App.get_times()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); }); + } +#endif +#ifdef USE_DATETIME_DATETIME + for (auto *obj : App.get_datetimes()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); }); + } +#endif +#ifdef USE_TEXT + for (auto *obj : App.get_texts()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_update(obj, state); }); + } +#endif #ifdef USE_SELECT for (auto *obj : App.get_selects()) { if (include_internal || !obj->is_internal()) { @@ -73,12 +97,36 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); }); } #endif +#ifdef USE_VALVE + for (auto *obj : App.get_valves()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_valve_update(obj); }); + } +#endif #ifdef USE_MEDIA_PLAYER for (auto *obj : App.get_media_players()) { if (include_internal || !obj->is_internal()) obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); }); } #endif +#ifdef USE_ALARM_CONTROL_PANEL + for (auto *obj : App.get_alarm_control_panels()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); }); + } +#endif +#ifdef USE_EVENT + for (auto *obj : App.get_events()) { + if (include_internal || !obj->is_internal()) + obj->add_on_event_callback([this, obj](const std::string &event_type) { this->on_event(obj, event_type); }); + } +#endif +#ifdef USE_UPDATE + for (auto *obj : App.get_updates()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_update(obj); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 0966504c43..39e0b2ba26 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -31,15 +31,39 @@ #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_DATETIME_DATE +#include "esphome/components/datetime/date_entity.h" +#endif +#ifdef USE_DATETIME_TIME +#include "esphome/components/datetime/time_entity.h" +#endif +#ifdef USE_DATETIME_DATETIME +#include "esphome/components/datetime/datetime_entity.h" +#endif +#ifdef USE_TEXT +#include "esphome/components/text/text.h" +#endif #ifdef USE_SELECT #include "esphome/components/select/select.h" #endif #ifdef USE_LOCK #include "esphome/components/lock/lock.h" #endif +#ifdef USE_VALVE +#include "esphome/components/valve/valve.h" +#endif #ifdef USE_MEDIA_PLAYER #include "esphome/components/media_player/media_player.h" #endif +#ifdef USE_ALARM_CONTROL_PANEL +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#endif +#ifdef USE_EVENT +#include "esphome/components/event/event.h" +#endif +#ifdef USE_UPDATE +#include "esphome/components/update/update_entity.h" +#endif namespace esphome { @@ -73,15 +97,39 @@ class Controller { #ifdef USE_NUMBER virtual void on_number_update(number::Number *obj, float state){}; #endif +#ifdef USE_DATETIME_DATE + virtual void on_date_update(datetime::DateEntity *obj){}; +#endif +#ifdef USE_DATETIME_TIME + virtual void on_time_update(datetime::TimeEntity *obj){}; +#endif +#ifdef USE_DATETIME_DATETIME + virtual void on_datetime_update(datetime::DateTimeEntity *obj){}; +#endif +#ifdef USE_TEXT + virtual void on_text_update(text::Text *obj, const std::string &state){}; +#endif #ifdef USE_SELECT virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){}; #endif #ifdef USE_LOCK virtual void on_lock_update(lock::Lock *obj){}; #endif +#ifdef USE_VALVE + virtual void on_valve_update(valve::Valve *obj){}; +#endif #ifdef USE_MEDIA_PLAYER virtual void on_media_player_update(media_player::MediaPlayer *obj){}; #endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; +#endif +#ifdef USE_EVENT + virtual void on_event(event::Event *obj, const std::string &event_type){}; +#endif +#ifdef USE_UPDATE + virtual void on_update(update::UpdateEntity *obj){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 64edabc878..3798ddba6a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -11,9 +11,11 @@ #define ESPHOME_BOARD "dummy_board" #define ESPHOME_PROJECT_NAME "dummy project" #define ESPHOME_PROJECT_VERSION "v2" +#define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_VARIANT "ESP32" // Feature flags +#define USE_ALARM_CONTROL_PANEL #define USE_API #define USE_API_NOISE #define USE_API_PLAINTEXT @@ -21,40 +23,69 @@ #define USE_BUTTON #define USE_CLIMATE #define USE_COVER +#define USE_DATETIME +#define USE_DATETIME_DATE +#define USE_DATETIME_DATETIME +#define USE_DATETIME_TIME #define USE_DEEP_SLEEP +#define USE_DISPLAY +#define USE_ESP32_IMPROV_STATE_CALLBACK +#define USE_EVENT #define USE_FAN #define USE_GRAPH +#define USE_GRAPHICAL_DISPLAY_MENU #define USE_HOMEASSISTANT_TIME +#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_JSON #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LVGL +#define USE_LVGL_ANIMIMG +#define USE_LVGL_BINARY_SENSOR +#define USE_LVGL_BUTTONMATRIX +#define USE_LVGL_DROPDOWN +#define USE_LVGL_FONT +#define USE_LVGL_IMAGE +#define USE_LVGL_KEY_LISTENER +#define USE_LVGL_KEYBOARD +#define USE_LVGL_ROLLER +#define USE_LVGL_ROTARY_ENCODER +#define USE_LVGL_TOUCHSCREEN +#define USE_MD5 #define USE_MDNS #define USE_MEDIA_PLAYER #define USE_MQTT +#define USE_NETWORK +#define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER +#define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK +#define USE_OTA_VERSION 1 +#define USE_OUTPUT #define USE_POWER_SUPPLY #define USE_QR_CODE #define USE_SELECT #define USE_SENSOR #define USE_STATUS_LED #define USE_SWITCH +#define USE_TEXT #define USE_TEXT_SENSOR #define USE_TIME #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER +#define USE_UPDATE +#define USE_VALVE #define USE_WIFI +#define USE_WIFI_AP +#define USE_WIREGUARD // Arduino-specific feature flags #ifdef USE_ARDUINO #define USE_CAPTIVE_PORTAL -#define USE_NEXTION_TFT_UPLOAD #define USE_PROMETHEUS -#define USE_WEBSERVER -#define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WIFI_WPA2_EAP #endif @@ -65,16 +96,22 @@ // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_BLUETOOTH_PROXY +#define USE_ESP32_BLE #define USE_ESP32_BLE_CLIENT #define USE_ESP32_BLE_SERVER #define USE_ESP32_CAMERA #define USE_IMPROV -#define USE_SOCKET_IMPL_BSD_SOCKETS -#define USE_WIFI_11KV_SUPPORT -#define USE_BLUETOOTH_PROXY -#define USE_VOICE_ASSISTANT +#define USE_MICRO_WAKE_WORD_VAD #define USE_MICROPHONE +#define USE_PSRAM +#define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SPEAKER +#define USE_SPI +#define USE_VOICE_ASSISTANT +#define USE_WEBSERVER +#define USE_WEBSERVER_PORT 80 // NOLINT +#define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO #define USE_ARDUINO_VERSION_CODE VERSION_CODE(2, 0, 5) @@ -84,22 +121,47 @@ #ifdef USE_ESP_IDF #define USE_ESP_IDF_VERSION_CODE VERSION_CODE(4, 4, 2) #endif + +#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_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) +#define USE_LOGGER_USB_CDC +#define USE_LOGGER_USB_SERIAL_JTAG +#endif #endif // ESP8266-specific feature flags #ifdef USE_ESP8266 #define USE_ADC_SENSOR_VCC -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 0, 2) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2) #define USE_ESP8266_PREFERENCES_FLASH #define USE_HTTP_REQUEST_ESP8266_HTTPS #define USE_SOCKET_IMPL_LWIP_TCP +#define USE_SPI + // Dummy firmware payload for shelly_dimmer #define USE_SHD_FIRMWARE_MAJOR_VERSION 56 #define USE_SHD_FIRMWARE_MINOR_VERSION 5 #define USE_SHD_FIRMWARE_DATA \ {} +#define USE_WEBSERVER +#define USE_WEBSERVER_PORT 80 // NOLINT +#endif + +#ifdef USE_RP2040 +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) +#define USE_LOGGER_USB_CDC +#define USE_SOCKET_IMPL_LWIP_TCP +#define USE_SPI +#endif + +#ifdef USE_LIBRETINY +#define USE_SOCKET_IMPL_LWIP_SOCKETS +#define USE_WEBSERVER +#define USE_WEBSERVER_PORT 80 // NOLINT #endif #ifdef USE_HOST @@ -107,6 +169,7 @@ #endif // Disabled feature flags -//#define USE_BSEC // Requires a library with proprietary license. +// #define USE_BSEC // Requires a library with proprietary license +// #define USE_BSEC2 // Requires a library with proprietary license #define USE_DASHBOARD_IMPORT diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 434111de79..4ca21f9ee5 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -63,7 +63,7 @@ class EntityBase { EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; }; -class EntityBase_DeviceClass { +class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. std::string get_device_class(); @@ -74,7 +74,7 @@ class EntityBase_DeviceClass { const char *device_class_{nullptr}; ///< Device class override }; -class EntityBase_UnitOfMeasurement { +class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. std::string get_unit_of_measurement(); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index f921711ec2..7f6a9b48ab 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -1,6 +1,5 @@ -import esphome.final_validate as fv - from esphome.const import CONF_ID +import esphome.final_validate as fv def inherit_property_from(property_to_inherit, parent_id_property, transform=None): diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 4c6ee84dde..dae60a4e1d 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -10,7 +10,16 @@ #include #include #include +#include +#ifdef USE_HOST +#ifndef _WIN32 +#include +#include +#include +#endif +#include +#endif #if defined(USE_ESP8266) #include #include @@ -34,19 +43,41 @@ #include #include #endif +#ifdef USE_ESP32 +#include "esp32/rom/crc.h" -#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC #include "esp_efuse.h" #include "esp_efuse_table.h" #endif +#ifdef USE_LIBRETINY +#include // for macAddress() +#endif + namespace esphome { static const char *const TAG = "helpers"; +static const uint16_t CRC16_A001_LE_LUT_L[] = {0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, + 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440}; +static const uint16_t CRC16_A001_LE_LUT_H[] = {0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401, + 0xa001, 0x6c00, 0x7800, 0xb401, 0x5000, 0x9c01, 0x8801, 0x4400}; + +#ifndef USE_ESP32 +static const uint16_t CRC16_8408_LE_LUT_L[] = {0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, + 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7}; +static const uint16_t CRC16_8408_LE_LUT_H[] = {0x0000, 0x1081, 0x2102, 0x3183, 0x4204, 0x5285, 0x6306, 0x7387, + 0x8408, 0x9489, 0xa50a, 0xb58b, 0xc60c, 0xd68d, 0xe70e, 0xf78f}; + +static const uint16_t CRC16_1021_BE_LUT_L[] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef}; +static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0x48c4, 0x5af5, 0x6ca6, 0x7e97, + 0x9188, 0x83b9, 0xb5ea, 0xa7db, 0xd94c, 0xcb7d, 0xfd2e, 0xef1f}; +#endif + // STL backports -#if _GLIBCXX_RELEASE < 7 +#if _GLIBCXX_RELEASE < 8 std::string to_string(int value) { return str_snprintf("%d", 32, value); } // NOLINT std::string to_string(long value) { return str_snprintf("%ld", 32, value); } // NOLINT std::string to_string(long long value) { return str_snprintf("%lld", 32, value); } // NOLINT @@ -61,7 +92,7 @@ std::string to_string(long double value) { return str_snprintf("%Lf", 32, value) // Mathematics float lerp(float completion, float start, float end) { return start + (end - start) * completion; } -uint8_t crc8(uint8_t *data, uint8_t len) { +uint8_t crc8(const uint8_t *data, uint8_t len) { uint8_t crc = 0; while ((len--) != 0u) { @@ -76,21 +107,79 @@ uint8_t crc8(uint8_t *data, uint8_t len) { } return crc; } -uint16_t crc16(const uint8_t *data, uint8_t len) { - uint16_t crc = 0xFFFF; - while (len--) { - crc ^= *data++; - for (uint8_t i = 0; i < 8; i++) { - if ((crc & 0x01) != 0) { - crc >>= 1; - crc ^= 0xA001; - } else { - crc >>= 1; + +uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout) { +#ifdef USE_ESP32 + if (reverse_poly == 0x8408) { + crc = crc16_le(refin ? crc : (crc ^ 0xffff), data, len); + return refout ? crc : (crc ^ 0xffff); + } +#endif + if (refin) { + crc ^= 0xffff; + } +#ifndef USE_ESP32 + if (reverse_poly == 0x8408) { + while (len--) { + uint8_t combo = crc ^ (uint8_t) *data++; + crc = (crc >> 8) ^ CRC16_8408_LE_LUT_L[combo & 0x0F] ^ CRC16_8408_LE_LUT_H[combo >> 4]; + } + } else +#endif + if (reverse_poly == 0xa001) { + while (len--) { + uint8_t combo = crc ^ (uint8_t) *data++; + crc = (crc >> 8) ^ CRC16_A001_LE_LUT_L[combo & 0x0F] ^ CRC16_A001_LE_LUT_H[combo >> 4]; + } + } else { + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if (crc & 0x0001) { + crc = (crc >> 1) ^ reverse_poly; + } else { + crc >>= 1; + } } } } - return crc; + return refout ? (crc ^ 0xffff) : crc; } + +uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly, bool refin, bool refout) { +#ifdef USE_ESP32 + if (poly == 0x1021) { + crc = crc16_be(refin ? crc : (crc ^ 0xffff), data, len); + return refout ? crc : (crc ^ 0xffff); + } +#endif + if (refin) { + crc ^= 0xffff; + } +#ifndef USE_ESP32 + if (poly == 0x1021) { + while (len--) { + uint8_t combo = (crc >> 8) ^ *data++; + crc = (crc << 8) ^ CRC16_1021_BE_LUT_L[combo & 0x0F] ^ CRC16_1021_BE_LUT_H[combo >> 4]; + } + } else { +#endif + while (len--) { + crc ^= (((uint16_t) *data++) << 8); + for (uint8_t i = 0; i < 8; i++) { + if (crc & 0x8000) { + crc = (crc << 1) ^ poly; + } else { + crc <<= 1; + } + } + } +#ifndef USE_ESP32 + } +#endif + return refout ? (crc ^ 0xffff) : crc; +} + uint32_t fnv1_hash(const std::string &str) { uint32_t hash = 2166136261UL; for (char c : str) { @@ -100,35 +189,39 @@ uint32_t fnv1_hash(const std::string &str) { return hash; } -uint32_t random_uint32() { #ifdef USE_ESP32 - return esp_random(); +uint32_t random_uint32() { return esp_random(); } #elif defined(USE_ESP8266) - return os_random(); +uint32_t random_uint32() { return os_random(); } #elif defined(USE_RP2040) +uint32_t random_uint32() { uint32_t result = 0; for (uint8_t i = 0; i < 32; i++) { result <<= 1; result |= rosc_hw->randombit; } return result; +} +#elif defined(USE_LIBRETINY) +uint32_t random_uint32() { return rand(); } #elif defined(USE_HOST) +uint32_t random_uint32() { std::random_device dev; std::mt19937 rng(dev()); std::uniform_int_distribution dist(0, std::numeric_limits::max()); return dist(rng); -#else -#error "No random source available for this configuration." -#endif } +#endif float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } -bool random_bytes(uint8_t *data, size_t len) { #ifdef USE_ESP32 +bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; +} #elif defined(USE_ESP8266) - return os_get_random(data, len) == 0; +bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; } #elif defined(USE_RP2040) +bool random_bytes(uint8_t *data, size_t len) { while (len-- != 0) { uint8_t result = 0; for (uint8_t i = 0; i < 8; i++) { @@ -138,7 +231,14 @@ bool random_bytes(uint8_t *data, size_t len) { *data++ = result; } return true; +} +#elif defined(USE_LIBRETINY) +bool random_bytes(uint8_t *data, size_t len) { + lt_rand_bytes(data, len); + return true; +} #elif defined(USE_HOST) +bool random_bytes(uint8_t *data, size_t len) { FILE *fp = fopen("/dev/urandom", "r"); if (fp == nullptr) { ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno); @@ -151,10 +251,8 @@ bool random_bytes(uint8_t *data, size_t len) { } fclose(fp); return true; -#else -#error "No random source available for this configuration." -#endif } +#endif // Strings @@ -191,10 +289,13 @@ std::string str_snake_case(const std::string &str) { return result; } std::string str_sanitize(const std::string &str) { - std::string out; - std::copy_if(str.begin(), str.end(), std::back_inserter(out), [](const char &c) { - return c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); - }); + 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 str_snprintf(const char *fmt, size_t len, ...) { @@ -325,7 +426,7 @@ std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { int8_t step_to_accuracy_decimals(float step) { // use printf %g to find number of digits based on temperature step char buf[32]; - sprintf(buf, "%.5g", step); + snprintf(buf, sizeof buf, "%.5g", step); std::string str{buf}; size_t dot_pos = str.find('.'); @@ -335,6 +436,107 @@ int8_t step_to_accuracy_decimals(float step) { return str.length() - dot_pos - 1; } +static const std::string BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); } + +std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } + +std::string base64_encode(const uint8_t *buf, size_t buf_len) { + std::string ret; + int i = 0; + int j = 0; + char char_array_3[3]; + char char_array_4[4]; + + while (buf_len--) { + char_array_3[i++] = *(buf++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (i = 0; (i < 4); i++) + ret += BASE64_CHARS[char_array_4[i]]; + i = 0; + } + } + + if (i) { + for (j = i; j < 3; j++) + char_array_3[j] = '\0'; + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (j = 0; (j < i + 1); j++) + ret += BASE64_CHARS[char_array_4[j]]; + + while ((i++ < 3)) + ret += '='; + } + + return ret; +} + +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(); +} + +std::vector base64_decode(const std::string &encoded_string) { + int in_len = encoded_string.size(); + int i = 0; + int j = 0; + int in = 0; + uint8_t char_array_4[4], char_array_3[3]; + std::vector ret; + + while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) { + char_array_4[i++] = encoded_string[in]; + in++; + if (i == 4) { + for (i = 0; i < 4; i++) + char_array_4[i] = BASE64_CHARS.find(char_array_4[i]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + 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]); + i = 0; + } + } + + if (i) { + for (j = i; j < 4; j++) + char_array_4[j] = 0; + + for (j = 0; j < 4; j++) + char_array_4[j] = BASE64_CHARS.find(char_array_4[j]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + 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]); + } + + return ret; +} + // Colors float gamma_correct(float value, float gamma) { @@ -422,11 +624,13 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green #if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_HOST) // ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. Mutex::Mutex() {} +Mutex::~Mutex() {} void Mutex::lock() {} bool Mutex::try_lock() { return true; } void Mutex::unlock() {} -#elif defined(USE_ESP32) +#elif defined(USE_ESP32) || defined(USE_LIBRETINY) Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } +Mutex::~Mutex() {} void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } void Mutex::unlock() { xSemaphoreGive(this->handle_); } @@ -435,7 +639,7 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); } #if defined(USE_ESP8266) IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } -#elif defined(USE_ESP32) +#elif defined(USE_ESP32) || defined(USE_LIBRETINY) // only affects the executing core // so should not be used as a mutex lock, only to get accurate timing IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } @@ -460,37 +664,92 @@ void HighFrequencyLoopRequester::stop() { } bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; } +#if defined(USE_HOST) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) -#if defined(USE_ESP32) -#if defined(USE_ESP32_IGNORE_EFUSE_MAC_CRC) - // On some devices, the MAC address that is burnt into EFuse does not - // match the CRC that goes along with it. For those devices, this - // work-around reads and uses the MAC address as-is from EFuse, - // without doing the CRC check. - esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS; + memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address)); +} +#elif defined(USE_ESP32) +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) +#if defined(CONFIG_SOC_IEEE802154_SUPPORTED) + // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default + // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. + if (has_custom_mac_address()) { + esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); + } else { + esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + } #else - esp_efuse_mac_get_default(mac); + if (has_custom_mac_address()) { + esp_efuse_mac_get_custom(mac); + } else { + esp_efuse_mac_get_default(mac); + } #endif +} #elif defined(USE_ESP8266) +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) wifi_get_macaddr(STATION_IF, mac); -#elif defined(USE_RP2040) && defined(USE_WIFI) +} +#elif defined(USE_RP2040) +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) +#ifdef USE_WIFI WiFi.macAddress(mac); #endif } +#elif defined(USE_LIBRETINY) +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) + WiFi.macAddress(mac); +} +#endif + std::string get_mac_address() { uint8_t mac[6]; get_mac_address_raw(mac); return str_snprintf("%02x%02x%02x%02x%02x%02x", 12, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } + std::string get_mac_address_pretty() { uint8_t mac[6]; get_mac_address_raw(mac); return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } + #ifdef USE_ESP32 void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } #endif +bool has_custom_mac_address() { +#if defined(USE_ESP32) && !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC) + uint8_t mac[6]; + // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails +#ifndef USE_ESP32_VARIANT_ESP32 + return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); +#else + return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); +#endif +#else + return false; +#endif +} + +bool mac_address_is_valid(const uint8_t *mac) { + bool is_all_zeros = true; + bool is_all_ones = true; + + for (uint8_t i = 0; i < 6; i++) { + if (mac[i] != 0) { + is_all_zeros = false; + } + } + for (uint8_t i = 0; i < 6; i++) { + if (mac[i] != 0xFF) { + is_all_ones = false; + } + } + return !(is_all_zeros || is_all_ones); +} + void delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 05a7eaa4cc..43001bafdd 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "esphome/core/optional.h" @@ -17,11 +18,14 @@ #if defined(USE_ESP32) #include #include +#elif defined(USE_LIBRETINY) +#include +#include #endif #define HOT __attribute__((hot)) #define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg))) -#define ALWAYS_INLINE __attribute__((always_inline)) +#define ESPHOME_ALWAYS_INLINE __attribute__((always_inline)) #define PACKED __attribute__((packed)) // Various functions can be constexpr in C++14, but not in C++11 (because their body isn't just a return statement). @@ -152,10 +156,13 @@ template T remap(U value, U min, U max, T min_out, T max } /// Calculate a CRC-8 checksum of \p data with size \p len. -uint8_t crc8(uint8_t *data, uint8_t len); +uint8_t crc8(const uint8_t *data, uint8_t len); /// Calculate a CRC-16 checksum of \p data with size \p len. -uint16_t crc16(const uint8_t *data, uint8_t len); +uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc = 0xffff, uint16_t reverse_poly = 0xa001, + bool refin = false, bool refout = false); +uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc = 0, uint16_t poly = 0x1021, bool refin = false, + bool refout = false); /// Calculate a FNV-1 hash of \p str. uint32_t fnv1_hash(const std::string &str); @@ -429,6 +436,12 @@ std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); +std::string base64_encode(const uint8_t *buf, size_t buf_len); +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); + ///@} /// @name Colors @@ -475,6 +488,7 @@ template class CallbackManager { for (auto &cb : this->callbacks_) cb(args...); } + size_t size() const { return this->callbacks_.size(); } /// Call all callbacks in this manager. void operator()(Ts... args) { call(args...); } @@ -532,6 +546,7 @@ class Mutex { public: Mutex(); Mutex(const Mutex &) = delete; + ~Mutex(); void lock(); bool try_lock(); void unlock(); @@ -539,8 +554,10 @@ class Mutex { Mutex &operator=(const Mutex &) = delete; private: -#if defined(USE_ESP32) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) SemaphoreHandle_t handle_; +#else + void *handle_; // d-pointer to store private data on new platforms #endif }; @@ -622,6 +639,14 @@ std::string get_mac_address_pretty(); void set_mac_address(uint8_t *mac); #endif +/// Check if a custom MAC address is set (ESP32 & variants) +/// @return True if a custom MAC address is set (ESP32 & variants), else false +bool has_custom_mac_address(); + +/// Check if the MAC address is not all zeros or all ones +/// @return True if MAC is valid, else false +bool mac_address_is_valid(const uint8_t *mac); + /// Delay for the given amount of microseconds, possibly yielding to other processes during the wait. void delay_microseconds_safe(uint32_t us); @@ -630,35 +655,45 @@ void delay_microseconds_safe(uint32_t us); /// @name Memory management ///@{ -/** An STL allocator that uses SPI RAM. +/** An STL allocator that uses SPI or internal RAM. + * Returns `nullptr` in case no memory is available. * - * By setting flags, it can be configured to don't try main memory if SPI RAM is full or unavailable, and to return - * `nulllptr` instead of aborting when no memory is available. + * By setting flags, it can be configured to: + * - perform external allocation falling back to main memory if SPI RAM is full or unavailable + * - perform external allocation only + * - perform internal allocation only */ -template class ExternalRAMAllocator { +template class RAMAllocator { public: using value_type = T; enum Flags { - NONE = 0, - REFUSE_INTERNAL = 1 << 0, ///< Refuse falling back to internal memory when external RAM is full or unavailable. - ALLOW_FAILURE = 1 << 1, ///< Don't abort when memory allocation fails. + NONE = 0, // Perform external allocation and fall back to internal memory + ALLOC_EXTERNAL = 1 << 0, // Perform external allocation only. + ALLOC_INTERNAL = 1 << 1, // Perform internal allocation only. + ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility. }; - ExternalRAMAllocator() = default; - ExternalRAMAllocator(Flags flags) : flags_{flags} {} - template constexpr ExternalRAMAllocator(const ExternalRAMAllocator &other) : flags_{other.flags_} {} + RAMAllocator() = default; + RAMAllocator(uint8_t flags) : flags_{flags} {} + template constexpr RAMAllocator(const RAMAllocator &other) : flags_{other.flags_} {} T *allocate(size_t n) { size_t size = n * sizeof(T); T *ptr = nullptr; #ifdef USE_ESP32 - ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); -#endif - if (ptr == nullptr && (this->flags_ & Flags::REFUSE_INTERNAL) == 0) + // External allocation by default or if explicitely requested + if ((this->flags_ & Flags::ALLOC_EXTERNAL) || ((this->flags_ & Flags::ALLOC_INTERNAL) == 0)) { + ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); + } + // Fallback to internal allocation if explicitely requested or no flag is specified + if (ptr == nullptr && ((this->flags_ & Flags::ALLOC_INTERNAL) || (this->flags_ & Flags::ALLOC_EXTERNAL) == 0)) { ptr = static_cast(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) - if (ptr == nullptr && (this->flags_ & Flags::ALLOW_FAILURE) == 0) - abort(); + } +#else + // Ignore ALLOC_EXTERNAL/ALLOC_INTERNAL flags if external allocation is not supported + ptr = static_cast(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) +#endif return ptr; } @@ -667,9 +702,11 @@ template class ExternalRAMAllocator { } private: - Flags flags_{Flags::NONE}; + uint8_t flags_{Flags::ALLOW_FAILURE}; }; +template using ExternalRAMAllocator = RAMAllocator; + /// @} /// @name Internal functions diff --git a/esphome/core/log.h b/esphome/core/log.h index 6775aa5ac5..86af534f98 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -17,6 +17,9 @@ #ifdef USE_ESP32_FRAMEWORK_ARDUINO #include #endif +#ifdef USE_LIBRETINY +#include +#endif namespace esphome { diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 5b96781e63..1e28ef1354 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -24,7 +24,7 @@ namespace esphome { struct nullopt_t { // NOLINT struct init {}; // NOLINT - nullopt_t(init) {} + nullopt_t(init /*unused*/) {} }; // extra parenthesis to prevent the most vexing parse: @@ -42,13 +42,13 @@ template class optional { // NOLINT optional() {} - optional(nullopt_t) {} + optional(nullopt_t /*unused*/) {} optional(T const &arg) : has_value_(true), value_(arg) {} // NOLINT template optional(optional const &other) : has_value_(other.has_value()), value_(other.value()) {} - optional &operator=(nullopt_t) { + optional &operator=(nullopt_t /*unused*/) { reset(); return *this; } @@ -104,7 +104,6 @@ template class optional { // NOLINT has_value_ = true; } - private: bool has_value_{false}; // NOLINT value_type value_; // NOLINT }; @@ -131,29 +130,29 @@ template inline bool operator>=(optional const &x, op // Comparison with nullopt -template inline bool operator==(optional const &x, nullopt_t) { return (!x); } +template inline bool operator==(optional const &x, nullopt_t /*unused*/) { return (!x); } -template inline bool operator==(nullopt_t, optional const &x) { return (!x); } +template inline bool operator==(nullopt_t /*unused*/, optional const &x) { return (!x); } -template inline bool operator!=(optional const &x, nullopt_t) { return bool(x); } +template inline bool operator!=(optional const &x, nullopt_t /*unused*/) { return bool(x); } -template inline bool operator!=(nullopt_t, optional const &x) { return bool(x); } +template inline bool operator!=(nullopt_t /*unused*/, optional const &x) { return bool(x); } -template inline bool operator<(optional const &, nullopt_t) { return false; } +template inline bool operator<(optional const & /*unused*/, nullopt_t /*unused*/) { return false; } -template inline bool operator<(nullopt_t, optional const &x) { return bool(x); } +template inline bool operator<(nullopt_t /*unused*/, optional const &x) { return bool(x); } -template inline bool operator<=(optional const &x, nullopt_t) { return (!x); } +template inline bool operator<=(optional const &x, nullopt_t /*unused*/) { return (!x); } -template inline bool operator<=(nullopt_t, optional const &) { return true; } +template inline bool operator<=(nullopt_t /*unused*/, optional const & /*unused*/) { return true; } -template inline bool operator>(optional const &x, nullopt_t) { return bool(x); } +template inline bool operator>(optional const &x, nullopt_t /*unused*/) { return bool(x); } -template inline bool operator>(nullopt_t, optional const &) { return false; } +template inline bool operator>(nullopt_t /*unused*/, optional const & /*unused*/) { return false; } -template inline bool operator>=(optional const &, nullopt_t) { return true; } +template inline bool operator>=(optional const & /*unused*/, nullopt_t /*unused*/) { return true; } -template inline bool operator>=(nullopt_t, optional const &x) { return (!x); } +template inline bool operator>=(nullopt_t /*unused*/, optional const &x) { return (!x); } // Comparison with T diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp new file mode 100644 index 0000000000..f97c686684 --- /dev/null +++ b/esphome/core/ring_buffer.cpp @@ -0,0 +1,71 @@ +#include "ring_buffer.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +#include "helpers.h" + +namespace esphome { + +static const char *const TAG = "ring_buffer"; + +RingBuffer::~RingBuffer() { + if (this->handle_ != nullptr) { + vStreamBufferDelete(this->handle_); + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(this->storage_, this->size_); + } +} + +std::unique_ptr RingBuffer::create(size_t len) { + std::unique_ptr rb = make_unique(); + + rb->size_ = len + 1; + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + rb->storage_ = allocator.allocate(rb->size_); + if (rb->storage_ == nullptr) { + return nullptr; + } + + rb->handle_ = xStreamBufferCreateStatic(rb->size_, 1, rb->storage_, &rb->structure_); + ESP_LOGD(TAG, "Created ring buffer with size %u", len); + return rb; +} + +size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { + if (ticks_to_wait > 0) + xStreamBufferSetTriggerLevel(this->handle_, len); + + size_t bytes_read = xStreamBufferReceive(this->handle_, data, len, ticks_to_wait); + + xStreamBufferSetTriggerLevel(this->handle_, 1); + + return bytes_read; +} + +size_t RingBuffer::write(void *data, size_t len) { + size_t free = this->free(); + if (free < len) { + size_t needed = len - free; + uint8_t discard[needed]; + xStreamBufferReceive(this->handle_, discard, needed, 0); + } + return xStreamBufferSend(this->handle_, data, len, 0); +} + +size_t RingBuffer::write_without_replacement(void *data, size_t len, TickType_t ticks_to_wait) { + return xStreamBufferSend(this->handle_, data, len, ticks_to_wait); +} + +size_t RingBuffer::available() const { return xStreamBufferBytesAvailable(this->handle_); } + +size_t RingBuffer::free() const { return xStreamBufferSpacesAvailable(this->handle_); } + +BaseType_t RingBuffer::reset() { return xStreamBufferReset(this->handle_); } + +} // namespace esphome + +#endif diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h new file mode 100644 index 0000000000..c0511fb52e --- /dev/null +++ b/esphome/core/ring_buffer.h @@ -0,0 +1,93 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +#include +#include + +namespace esphome { + +class RingBuffer { + public: + ~RingBuffer(); + + /** + * @brief Reads from the ring buffer, waiting up to a specified number of ticks if necessary. + * + * Available bytes are read into the provided data pointer. If not enough bytes are available, + * the function will wait up to `ticks_to_wait` FreeRTOS ticks before reading what is available. + * + * @param data Pointer to copy read data into + * @param len Number of bytes to read + * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) + * @return Number of bytes read + */ + size_t read(void *data, size_t len, TickType_t ticks_to_wait = 0); + + /** + * @brief Writes to the ring buffer, overwriting oldest data if necessary. + * + * The provided data is written to the ring buffer. If not enough space is available, + * the function will overwrite the oldest data in the ring buffer. + * + * @param data Pointer to data for writing + * @param len Number of bytes to write + * @return Number of bytes written + */ + size_t write(void *data, size_t len); + + /** + * @brief Writes to the ring buffer without overwriting oldest data. + * + * The provided data is written to the ring buffer. If not enough space is available, + * the function will wait up to `ticks_to_wait` FreeRTOS ticks before writing as much as possible. + * + * @param data Pointer to data for writing + * @param len Number of bytes to write + * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) + * @return Number of bytes written + */ + size_t write_without_replacement(void *data, size_t len, TickType_t ticks_to_wait = 0); + + /** + * @brief Returns the number of available bytes in the ring buffer. + * + * This function provides the number of bytes that can be read from the ring buffer + * without blocking the calling FreeRTOS task. + * + * @return Number of available bytes + */ + size_t available() const; + + /** + * @brief Returns the number of free bytes in the ring buffer. + * + * This function provides the number of bytes that can be written to the ring buffer + * without overwriting data or blocking the calling FreeRTOS task. + * + * @return Number of free bytes + */ + size_t free() const; + + /** + * @brief Resets the ring buffer, discarding all stored data. + * + * @return pdPASS if successful, pdFAIL otherwise + */ + BaseType_t reset(); + + static std::unique_ptr create(size_t len); + + protected: + StreamBufferHandle_t handle_; + StaticStreamBuffer_t structure_; + uint8_t *storage_; + size_t size_{0}; +}; + +} // namespace esphome + +#endif diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 7c76c8490b..012c9af3c6 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -29,7 +29,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u if (timeout == SCHEDULER_DONT_RUN) return; - ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout); + ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%" PRIu32 ")", name.c_str(), timeout); auto item = make_unique(); item->component = component; @@ -60,7 +60,7 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name, if (interval != 0) offset = (random_uint32() % interval) / 2; - ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset); + ESP_LOGVV(TAG, "set_interval(name='%s', interval=%" PRIu32 ", offset=%" PRIu32 ")", name.c_str(), interval, offset); auto item = make_unique(); item->component = component; @@ -108,8 +108,8 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin if (initial_wait_time == SCHEDULER_DONT_RUN) return; - ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u, max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), - initial_wait_time, max_attempts, backoff_increase_factor); + ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)", + name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); if (backoff_increase_factor < 0.0001) { ESP_LOGE(TAG, @@ -154,16 +154,16 @@ void HOT Scheduler::call() { if (now - last_print > 2000) { last_print = now; std::vector> old_items; - ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now); + ESP_LOGVV(TAG, "Items: count=%u, now=%" PRIu32, this->items_.size(), now); while (!this->empty_()) { this->lock_.lock(); auto item = std::move(this->items_[0]); this->pop_raw_(); this->lock_.unlock(); - ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(), - item->name.c_str(), item->interval, item->last_execution, item->last_execution_major, - item->next_execution(), item->next_execution_major()); + ESP_LOGVV(TAG, " %s '%s' interval=%" PRIu32 " last_execution=%" PRIu32 " (%u) next=%" PRIu32 " (%u)", + item->get_type_str(), item->name.c_str(), item->interval, item->last_execution, + item->last_execution_major, item->next_execution(), item->next_execution_major()); old_items.push_back(std::move(item)); } @@ -222,8 +222,8 @@ void HOT Scheduler::call() { } #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", item->get_type_str(), - item->name.c_str(), item->interval, item->last_execution, now); + ESP_LOGVV(TAG, "Running %s '%s' with interval=%" PRIu32 " last_execution=%" PRIu32 " (now=%" PRIu32 ")", + item->get_type_str(), item->name.c_str(), item->interval, item->last_execution, now); #endif // Warning: During callback(), a lot of stuff can happen, including: diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp new file mode 100644 index 0000000000..f7aa4fdddb --- /dev/null +++ b/esphome/core/time.cpp @@ -0,0 +1,239 @@ +#include "time.h" // NOLINT +#include "helpers.h" + +#include + +namespace esphome { + +bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } + +uint8_t days_in_month(uint8_t month, uint16_t year) { + static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + uint8_t days = DAYS_IN_MONTH[month]; + if (month == 2 && is_leap_year(year)) + return 29; + return days; +} + +size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { + struct tm c_tm = this->to_c_tm(); + return ::strftime(buffer, buffer_len, format, &c_tm); +} +ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { + ESPTime res{}; + res.second = uint8_t(c_tm->tm_sec); + res.minute = uint8_t(c_tm->tm_min); + res.hour = uint8_t(c_tm->tm_hour); + res.day_of_week = uint8_t(c_tm->tm_wday + 1); + res.day_of_month = uint8_t(c_tm->tm_mday); + res.day_of_year = uint16_t(c_tm->tm_yday + 1); + res.month = uint8_t(c_tm->tm_mon + 1); + res.year = uint16_t(c_tm->tm_year + 1900); + res.is_dst = bool(c_tm->tm_isdst); + res.timestamp = c_time; + return res; +} +struct tm ESPTime::to_c_tm() { + struct tm c_tm {}; + c_tm.tm_sec = this->second; + c_tm.tm_min = this->minute; + c_tm.tm_hour = this->hour; + c_tm.tm_mday = this->day_of_month; + c_tm.tm_mon = this->month - 1; + c_tm.tm_year = this->year - 1900; + c_tm.tm_wday = this->day_of_week - 1; + c_tm.tm_yday = this->day_of_year - 1; + c_tm.tm_isdst = this->is_dst; + return c_tm; +} +std::string ESPTime::strftime(const std::string &format) { + std::string timestr; + timestr.resize(format.size() * 4); + struct tm c_tm = this->to_c_tm(); + size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + while (len == 0) { + if (timestr.size() >= 128) { + // strftime has failed for reasons unrelated to the size of the buffer + // so return a formatting error + return "ERROR"; + } + timestr.resize(timestr.size() * 2); + len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + } + timestr.resize(len); + return timestr; +} + +bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + int num; + + if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT + &hour, // NOLINT + &minute, // NOLINT + &second, &num) == 6 && // NOLINT + num == time_to_parse.size()) { + esp_time.year = year; + esp_time.month = month; + esp_time.day_of_month = day; + esp_time.hour = hour; + esp_time.minute = minute; + esp_time.second = second; + } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT + &hour, // NOLINT + &minute, &num) == 5 && // NOLINT + num == time_to_parse.size()) { + esp_time.year = year; + esp_time.month = month; + esp_time.day_of_month = day; + esp_time.hour = hour; + esp_time.minute = minute; + esp_time.second = 0; + } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT + num == time_to_parse.size()) { + esp_time.hour = hour; + esp_time.minute = minute; + esp_time.second = second; + } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT + num == time_to_parse.size()) { + esp_time.hour = hour; + esp_time.minute = minute; + esp_time.second = 0; + } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT + num == time_to_parse.size()) { + esp_time.year = year; + esp_time.month = month; + esp_time.day_of_month = day; + } else { + return false; + } + return true; +} + +void ESPTime::increment_second() { + this->timestamp++; + if (!increment_time_value(this->second, 0, 60)) + return; + + // second roll-over, increment minute + if (!increment_time_value(this->minute, 0, 60)) + return; + + // minute roll-over, increment hour + if (!increment_time_value(this->hour, 0, 24)) + return; + + // hour roll-over, increment day + increment_time_value(this->day_of_week, 1, 8); + + if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { + // day of month roll-over, increment month + increment_time_value(this->month, 1, 13); + } + + uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; + if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { + // day of year roll-over, increment year + this->year++; + } +} +void ESPTime::increment_day() { + this->timestamp += 86400; + + // increment day + increment_time_value(this->day_of_week, 1, 8); + + if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { + // day of month roll-over, increment month + increment_time_value(this->month, 1, 13); + } + + uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; + if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { + // day of year roll-over, increment year + this->year++; + } +} +void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { + time_t res = 0; + + if (!this->fields_in_range()) { + this->timestamp = -1; + return; + } + + for (int i = 1970; i < this->year; i++) + res += is_leap_year(i) ? 366 : 365; + + if (use_day_of_year) { + res += this->day_of_year - 1; + } else { + for (int i = 1; i < this->month; i++) + res += days_in_month(i, this->year); + + res += this->day_of_month - 1; + } + + res *= 24; + res += this->hour; + res *= 60; + res += this->minute; + res *= 60; + res += this->second; + this->timestamp = res; +} + +void ESPTime::recalc_timestamp_local(bool use_day_of_year) { + this->recalc_timestamp_utc(use_day_of_year); + this->timestamp -= ESPTime::timezone_offset(); + ESPTime temp = ESPTime::from_epoch_local(this->timestamp); + if (temp.is_dst) { + this->timestamp -= 3600; + } +} + +int32_t ESPTime::timezone_offset() { + int32_t offset = 0; + time_t now = ::time(nullptr); + auto local = ESPTime::from_epoch_local(now); + auto utc = ESPTime::from_epoch_utc(now); + bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; + + if (utc.minute > local.minute) { + local.minute += 60; + local.hour -= 1; + } + offset += (local.minute - utc.minute) * 60; + + if (negative) { + offset -= (utc.hour - local.hour) * 3600; + } else { + if (utc.hour > local.hour) { + local.hour += 24; + } + offset += (local.hour - utc.hour) * 3600; + } + return offset; +} + +bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } +bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } +bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } +bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; } +bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; } + +template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) { + current++; + if (current >= end) { + current = begin; + return true; + } + return false; +} + +} // namespace esphome diff --git a/esphome/core/time.h b/esphome/core/time.h new file mode 100644 index 0000000000..bce1108d93 --- /dev/null +++ b/esphome/core/time.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include + +namespace esphome { + +template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end); + +bool is_leap_year(uint32_t year); + +uint8_t days_in_month(uint8_t month, uint16_t year); + +/// A more user-friendly version of struct tm from time.h +struct ESPTime { + /** seconds after the minute [0-60] + * @note second is generally 0-59; the extra range is to accommodate leap seconds. + */ + uint8_t second; + /// minutes after the hour [0-59] + uint8_t minute; + /// hours since midnight [0-23] + uint8_t hour; + /// day of the week; sunday=1 [1-7] + uint8_t day_of_week; + /// day of the month [1-31] + uint8_t day_of_month; + /// day of the year [1-366] + uint16_t day_of_year; + /// month; january=1 [1-12] + uint8_t month; + /// year + uint16_t year; + /// daylight saving time flag + bool is_dst; + /// unix epoch time (seconds since UTC Midnight January 1, 1970) + time_t timestamp; + + /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. + * Up to buffer_len bytes are written. + * + * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + */ + size_t strftime(char *buffer, size_t buffer_len, const char *format); + + /** Convert this ESPTime struct to a string as specified by the format argument. + * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + * + * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some + * microcontrollers. + * + * @warning This method can return "ERROR" when the underlying strftime() call fails, e.g. when the + * format string contains unsupported specifiers or when the format string doesn't produce any + * output. + */ + std::string strftime(const std::string &format); + + /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) + bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } + + /// Check if all time fields of this ESPTime are in range. + bool fields_in_range() const { + return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && + this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && + this->day_of_year < 367 && this->month > 0 && this->month < 13; + } + + /** Convert a string to ESPTime struct as specified by the format argument. + * @param time_to_parse null-terminated c string formatet like this: 2020-08-25 05:30:00. + * @param esp_time an instance of a ESPTime struct + * @return the success sate of the parsing + */ + static bool strptime(const std::string &time_to_parse, ESPTime &esp_time); + + /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance. + static ESPTime from_c_tm(struct tm *c_tm, time_t c_time); + + /** Convert an UTC epoch timestamp to a local time ESPTime instance. + * + * @param epoch Seconds since 1st January 1970. In UTC. + * @return The generated ESPTime + */ + static ESPTime from_epoch_local(time_t epoch) { + struct tm *c_tm = ::localtime(&epoch); + return ESPTime::from_c_tm(c_tm, epoch); + } + /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. + * + * @param epoch Seconds since 1st January 1970. In UTC. + * @return The generated ESPTime + */ + static ESPTime from_epoch_utc(time_t epoch) { + struct tm *c_tm = ::gmtime(&epoch); + return ESPTime::from_c_tm(c_tm, epoch); + } + + /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). + void recalc_timestamp_utc(bool use_day_of_year = true); + + /// Recalculate the timestamp field from the other fields of this ESPTime instance assuming local fields. + void recalc_timestamp_local(bool use_day_of_year = true); + + /// Convert this ESPTime instance back to a tm struct. + struct tm to_c_tm(); + + static int32_t timezone_offset(); + + /// Increment this clock instance by one second. + void increment_second(); + /// Increment this clock instance by one day. + void increment_day(); + bool operator<(ESPTime other); + bool operator<=(ESPTime other); + bool operator==(ESPTime other); + bool operator>=(ESPTime other); + bool operator>(ESPTime other); +}; +} // namespace esphome diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 5f391dc7ad..30ebb8147e 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -43,13 +43,13 @@ the last `yield` expression defines what is returned. """ import collections +from collections.abc import Awaitable, Generator, Iterator import functools import heapq import inspect import logging import types from typing import Any, Callable -from collections.abc import Awaitable, Generator, Iterator _LOGGER = logging.getLogger(__name__) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 789bd58e5c..7a82d5cba1 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,8 +1,8 @@ import abc +from collections.abc import Sequence import inspect import math import re -from collections.abc import Generator, Sequence from typing import Any, Callable, Optional, Union from esphome.core import ( @@ -17,6 +17,7 @@ from esphome.core import ( TimePeriodMicroseconds, TimePeriodMilliseconds, TimePeriodMinutes, + TimePeriodNanoseconds, TimePeriodSeconds, ) from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last @@ -351,6 +352,8 @@ def safe_exp(obj: SafeExpType) -> Expression: return IntLiteral(obj) if isinstance(obj, float): return FloatLiteral(obj) + if isinstance(obj, TimePeriodNanoseconds): + return IntLiteral(int(obj.total_nanoseconds)) if isinstance(obj, TimePeriodMicroseconds): return IntLiteral(int(obj.total_microseconds)) if isinstance(obj, TimePeriodMilliseconds): @@ -474,8 +477,9 @@ def 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 register: If true register the variable with the core - :returns The new variable as a MockObj. + :return: The new variable as a MockObj. """ assert isinstance(id_, ID) rhs = safe_exp(rhs) @@ -489,9 +493,7 @@ def variable( return obj -def with_local_variable( - id_: ID, rhs: SafeExpType, callback: Callable[["MockObj"], None], *args -) -> None: +def with_local_variable(id_: ID, rhs: SafeExpType, callback: Callable, *args) -> None: """Declare a new variable, not pointer type, in the code generation, within a scoped block The variable is only usable within the callback The callback cannot be async. @@ -523,7 +525,7 @@ def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj :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). - :returns The new variable as a MockObj. + :return: The new variable as a MockObj. """ assert isinstance(id_, ID) rhs = safe_exp(rhs) @@ -546,7 +548,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": :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). - :returns The new variable as a MockObj. + :return: The new variable as a MockObj. """ rhs = safe_exp(rhs) obj = MockObj(id_, "->") @@ -567,7 +569,7 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable: :param id_: The ID used to declare the variable (also specifies the type). :param args: The values to pass to the constructor. - :returns The new variable as a MockObj. + :return: The new variable as a MockObj. """ if args and isinstance(args[0], TemplateArguments): id_ = id_.copy() @@ -596,6 +598,7 @@ def add_library(name: str, version: Optional[str], repository: Optional[str] = N :param name: The name of the library (for example 'AsyncTCP') :param version: The version of the library, may be None. + :param repository: The repository for the library """ CORE.add_library(Library(name, version, repository)) @@ -651,7 +654,7 @@ async def process_lambda( parameters: list[tuple[SafeExpType, str]], capture: str = "=", return_type: SafeExpType = None, -) -> Generator[LambdaExpression, None, None]: +) -> Union[LambdaExpression, None]: """Process the given lambda value into a LambdaExpression. This is a coroutine because lambdas can depend on other IDs, @@ -663,10 +666,14 @@ async def process_lambda( :param return_type: The return type of the lambda. :return: The generated lambda expression. """ - from esphome.components.globals import GlobalsComponent, RestoringGlobalsComponent + from esphome.components.globals import ( + GlobalsComponent, + RestoringGlobalsComponent, + RestoringGlobalStringComponent, + ) if value is None: - return + return None parts = value.parts[:] for i, id in enumerate(value.requires_ids): full_id, var = await get_variable_with_full_id(id) @@ -676,6 +683,7 @@ async def process_lambda( and ( full_id.type.inherits_from(GlobalsComponent) or full_id.type.inherits_from(RestoringGlobalsComponent) + or full_id.type.inherits_from(RestoringGlobalStringComponent) ) ): parts[i * 3 + 1] = var.value() @@ -704,7 +712,7 @@ async def templatable( value: Any, args: list[tuple[SafeExpType, str]], output_type: Optional[SafeExpType], - to_exp: Any = None, + to_exp: Union[Callable, dict] = None, ): """Generate code for a templatable config option. diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index cc53f491f5..9a775bad33 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -6,22 +6,19 @@ from esphome.const import ( CONF_ICON, CONF_INTERNAL, CONF_NAME, - CONF_SETUP_PRIORITY, - CONF_UPDATE_INTERVAL, - CONF_TYPE_ID, - CONF_OTA, CONF_SAFE_MODE, + CONF_SETUP_PRIORITY, + CONF_TYPE_ID, + CONF_UPDATE_INTERVAL, KEY_PAST_SAFE_MODE, ) - -from esphome.core import coroutine, ID, CORE +from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.types import ConfigType, ConfigFragmentType from esphome.cpp_generator import add, get_variable from esphome.cpp_types import App +from esphome.helpers import sanitize, snake_case +from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry -from esphome.helpers import snake_case, sanitize - _LOGGER = logging.getLogger(__name__) @@ -35,7 +32,7 @@ async def gpio_pin_expression(conf): return None from esphome import pins - for key, (func, _) in pins.PIN_SCHEMA_REGISTRY.items(): + for key, (func, _, _) in pins.PIN_SCHEMA_REGISTRY.items(): if key in conf: return await coroutine(func)(conf) return await coroutine(pins.PIN_SCHEMA_REGISTRY[CORE.target_platform][0])(conf) @@ -139,15 +136,12 @@ async def build_registry_list(registry, config): async def past_safe_mode(): - safe_mode_enabled = ( - CONF_OTA in CORE.config and CORE.config[CONF_OTA][CONF_SAFE_MODE] - ) - if not safe_mode_enabled: + if CONF_SAFE_MODE not in CORE.config: return def _safe_mode_generator(): while True: - if CORE.data.get(CONF_OTA, {}).get(KEY_PAST_SAFE_MODE, False): + if CORE.data.get(CONF_SAFE_MODE, {}).get(KEY_PAST_SAFE_MODE, False): return yield diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 7d0e386b66..dab993f87f 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -8,7 +8,9 @@ double = global_ns.namespace("double") bool_ = global_ns.namespace("bool") int_ = global_ns.namespace("int") std_ns = global_ns.namespace("std") +std_shared_ptr = std_ns.class_("shared_ptr") std_string = std_ns.class_("string") +std_string_ref = std_ns.namespace("string &") std_vector = std_ns.class_("vector") uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") @@ -38,3 +40,4 @@ gpio_ns = esphome_ns.namespace("gpio") gpio_Flags = gpio_ns.enum("Flags", is_class=True) EntityCategory = esphome_ns.enum("EntityCategory") Parented = esphome_ns.class_("Parented") +ESPTime = esphome_ns.struct("ESPTime") diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py new file mode 100644 index 0000000000..db66cb5ead --- /dev/null +++ b/esphome/dashboard/const.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +EVENT_ENTRY_ADDED = "entry_added" +EVENT_ENTRY_REMOVED = "entry_removed" +EVENT_ENTRY_UPDATED = "entry_updated" +EVENT_ENTRY_STATE_CHANGED = "entry_state_changed" +MAX_EXECUTOR_WORKERS = 48 + + +SENTINEL = object() + +DASHBOARD_COMMAND = ["esphome", "--dashboard"] diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py new file mode 100644 index 0000000000..563ca1506d --- /dev/null +++ b/esphome/dashboard/core.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Coroutine +import contextlib +from dataclasses import dataclass +from functools import partial +import json +import logging +from pathlib import Path +import threading +from typing import TYPE_CHECKING, Any, Callable + +from esphome.storage_json import ignored_devices_storage_path + +from ..zeroconf import DiscoveredImport +from .dns import DNSCache +from .entries import DashboardEntries +from .settings import DashboardSettings + +if TYPE_CHECKING: + from .status.mdns import MDNSStatus + + +_LOGGER = logging.getLogger(__name__) + +IGNORED_DEVICES_STORAGE_PATH = "ignored-devices.json" + + +@dataclass +class Event: + """Dashboard Event.""" + + event_type: str + data: dict[str, Any] + + +class EventBus: + """Dashboard event bus.""" + + def __init__(self) -> None: + """Initialize the Dashboard event bus.""" + self._listeners: dict[str, set[Callable[[Event], None]]] = {} + + def async_add_listener( + self, event_type: str, listener: Callable[[Event], None] + ) -> Callable[[], None]: + """Add a listener to the event bus.""" + self._listeners.setdefault(event_type, set()).add(listener) + return partial(self._async_remove_listener, event_type, listener) + + def _async_remove_listener( + self, event_type: str, listener: Callable[[Event], None] + ) -> None: + """Remove a listener from the event bus.""" + self._listeners[event_type].discard(listener) + + def async_fire(self, event_type: str, event_data: dict[str, Any]) -> None: + """Fire an event.""" + event = Event(event_type, event_data) + + _LOGGER.debug("Firing event: %s", event) + + for listener in self._listeners.get(event_type, set()): + listener(event) + + +class ESPHomeDashboard: + """Class that represents the dashboard.""" + + __slots__ = ( + "bus", + "entries", + "loop", + "import_result", + "stop_event", + "ping_request", + "mqtt_ping_request", + "mdns_status", + "settings", + "dns_cache", + "_background_tasks", + "ignored_devices", + ) + + def __init__(self) -> None: + """Initialize the ESPHomeDashboard.""" + self.bus = EventBus() + self.entries: DashboardEntries | None = None + self.loop: asyncio.AbstractEventLoop | None = None + self.import_result: dict[str, DiscoveredImport] = {} + self.stop_event = threading.Event() + self.ping_request: asyncio.Event | None = None + self.mqtt_ping_request = threading.Event() + self.mdns_status: MDNSStatus | None = None + self.settings = DashboardSettings() + self.dns_cache = DNSCache() + self._background_tasks: set[asyncio.Task] = set() + self.ignored_devices: set[str] = set() + + async def async_setup(self) -> None: + """Setup the dashboard.""" + self.loop = asyncio.get_running_loop() + self.ping_request = asyncio.Event() + self.entries = DashboardEntries(self) + self.load_ignored_devices() + + def load_ignored_devices(self) -> None: + storage_path = Path(ignored_devices_storage_path()) + try: + with storage_path.open("r", encoding="utf-8") as f_handle: + data = json.load(f_handle) + self.ignored_devices = set(data.get("ignored_devices", set())) + except FileNotFoundError: + pass + + def save_ignored_devices(self) -> None: + storage_path = Path(ignored_devices_storage_path()) + with storage_path.open("w", encoding="utf-8") as f_handle: + json.dump( + {"ignored_devices": sorted(self.ignored_devices)}, indent=2, fp=f_handle + ) + + async def async_run(self) -> None: + """Run the dashboard.""" + settings = self.settings + mdns_task: asyncio.Task | None = None + ping_status_task: asyncio.Task | None = None + await self.entries.async_update_entries() + + if settings.status_use_ping: + from .status.ping import PingStatus + + ping_status = PingStatus() + ping_status_task = asyncio.create_task(ping_status.async_run()) + else: + from .status.mdns import MDNSStatus + + mdns_status = MDNSStatus() + await mdns_status.async_refresh_hosts() + self.mdns_status = mdns_status + mdns_task = asyncio.create_task(mdns_status.async_run()) + + if settings.status_use_mqtt: + from .status.mqtt import MqttStatusThread + + status_thread_mqtt = MqttStatusThread() + status_thread_mqtt.start() + + shutdown_event = asyncio.Event() + try: + await shutdown_event.wait() + finally: + _LOGGER.info("Shutting down...") + self.stop_event.set() + self.ping_request.set() + if ping_status_task: + ping_status_task.cancel() + if mdns_task: + mdns_task.cancel() + if settings.status_use_mqtt: + status_thread_mqtt.join() + self.mqtt_ping_request.set() + for task in self._background_tasks: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + await asyncio.sleep(0) + + def async_create_background_task( + self, coro: Coroutine[Any, Any, Any] + ) -> asyncio.Task: + """Create a background task.""" + task = self.loop.create_task(coro) + task.add_done_callback(self._background_tasks.discard) + return task + + +DASHBOARD = ESPHomeDashboard() diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 8d8eb74b4b..9de2d39ce2 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,1291 +1,153 @@ -import base64 -import binascii -import codecs -import collections -import functools -import hashlib -import hmac -import json +from __future__ import annotations + +import asyncio +from asyncio import events +from concurrent.futures import ThreadPoolExecutor import logging -import multiprocessing import os -import secrets -import shutil -import subprocess +import socket import threading -from pathlib import Path -from typing import Optional +from time import monotonic +import traceback +from typing import Any -import tornado -import tornado.concurrent -import tornado.gen -import tornado.httpserver -import tornado.ioloop -import tornado.iostream -import tornado.netutil -import tornado.process -import tornado.web -import tornado.websocket -import yaml -from tornado.log import access_log +from esphome.storage_json import EsphomeStorageJSON, esphome_storage_path -from esphome import const, platformio_api, util, yaml_util -from esphome.helpers import get_bool_env, mkdir_p, run_system_command -from esphome.storage_json import ( - EsphomeStorageJSON, - StorageJSON, - esphome_storage_path, - ext_storage_path, - trash_storage_path, -) -from esphome.util import get_serial_ports, shlex_quote -from esphome.zeroconf import DashboardImportDiscovery, DashboardStatus, EsphomeZeroconf - -from .util import friendly_name_slugify, password_hash - -_LOGGER = logging.getLogger(__name__) +from .const import MAX_EXECUTOR_WORKERS +from .core import DASHBOARD +from .web_server import make_app, start_web_server ENV_DEV = "ESPHOME_DASHBOARD_DEV" - -class DashboardSettings: - def __init__(self): - self.config_dir = "" - self.password_hash = "" - self.username = "" - self.using_password = False - self.on_ha_addon = False - self.cookie_secret = None - self.absolute_config_dir = None - - def parse_args(self, args): - self.on_ha_addon = args.ha_addon - password = args.password or os.getenv("PASSWORD", "") - if not self.on_ha_addon: - self.username = args.username or os.getenv("USERNAME", "") - self.using_password = bool(password) - if self.using_password: - self.password_hash = password_hash(password) - self.config_dir = args.configuration - self.absolute_config_dir = Path(self.config_dir).resolve() - - @property - def relative_url(self): - return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL", "/") - - @property - def status_use_ping(self): - return get_bool_env("ESPHOME_DASHBOARD_USE_PING") - - @property - def status_use_mqtt(self): - return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT") - - @property - def using_ha_addon_auth(self): - if not self.on_ha_addon: - return False - return not get_bool_env("DISABLE_HA_AUTHENTICATION") - - @property - def using_auth(self): - return self.using_password or self.using_ha_addon_auth - - def check_password(self, username, password): - if not self.using_auth: - return True - if username != self.username: - return False - - # Compare password in constant running time (to prevent timing attacks) - return hmac.compare_digest(self.password_hash, password_hash(password)) - - def rel_path(self, *args): - joined_path = os.path.join(self.config_dir, *args) - # Raises ValueError if not relative to ESPHome config folder - Path(joined_path).resolve().relative_to(self.absolute_config_dir) - return joined_path - - def list_yaml_files(self): - return util.list_yaml_files([self.config_dir]) +settings = DASHBOARD.settings -settings = DashboardSettings() +def can_use_pidfd() -> bool: + """Check if pidfd_open is available. -cookie_authenticated_yes = b"yes" - - -def template_args(): - version = const.__version__ - if "b" in version: - docs_link = "https://beta.esphome.io/" - elif "dev" in version: - docs_link = "https://next.esphome.io/" - else: - docs_link = "https://www.esphome.io/" - - return { - "version": version, - "docs_link": docs_link, - "get_static_file_url": get_static_file_url, - "relative_url": settings.relative_url, - "streamer_mode": get_bool_env("ESPHOME_STREAMER_MODE"), - "config_dir": settings.config_dir, - } - - -def authenticated(func): - @functools.wraps(func) - def decorator(self, *args, **kwargs): - if not is_authenticated(self): - self.redirect("./login") - return None - return func(self, *args, **kwargs) - - return decorator - - -def is_authenticated(request_handler): - if settings.on_ha_addon: - # Handle ingress - disable auth on ingress port - # X-HA-Ingress is automatically stripped on the non-ingress server in nginx - header = request_handler.request.headers.get("X-HA-Ingress", "NO") - if str(header) == "YES": - return True - if settings.using_auth: - return ( - request_handler.get_secure_cookie("authenticated") - == cookie_authenticated_yes - ) + Back ported from cpython 3.12 + """ + if not hasattr(os, "pidfd_open"): + return False + try: + pid = os.getpid() + os.close(os.pidfd_open(pid, 0)) + except OSError: + # blocked by security policy like SECCOMP + return False return True -def bind_config(func): - def decorator(self, *args, **kwargs): - configuration = self.get_argument("configuration") - kwargs = kwargs.copy() - kwargs["configuration"] = configuration - return func(self, *args, **kwargs) - - return decorator - - -# pylint: disable=abstract-method -class BaseHandler(tornado.web.RequestHandler): - pass - - -def websocket_class(cls): - # pylint: disable=protected-access - if not hasattr(cls, "_message_handlers"): - cls._message_handlers = {} - - for _, method in cls.__dict__.items(): - if hasattr(method, "_message_handler"): - cls._message_handlers[method._message_handler] = method - - return cls - - -def websocket_method(name): - def wrap(fn): - # pylint: disable=protected-access - fn._message_handler = name - return fn - - return wrap - - -@websocket_class -class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): - def __init__(self, application, request, **kwargs): - super().__init__(application, request, **kwargs) - self._proc = None - self._is_closed = False - - @authenticated - def on_message(self, message): - # Messages are always JSON, 500 when not - json_message = json.loads(message) - type_ = json_message["type"] - # pylint: disable=no-member - handlers = type(self)._message_handlers - if type_ not in handlers: - _LOGGER.warning("Requested unknown message type %s", type_) - return - - handlers[type_](self, json_message) - - @websocket_method("spawn") - def handle_spawn(self, json_message): - if self._proc is not None: - # spawn can only be called once - return - command = self.build_command(json_message) - _LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command)) - self._proc = tornado.process.Subprocess( - command, - stdout=tornado.process.Subprocess.STREAM, - stderr=subprocess.STDOUT, - stdin=tornado.process.Subprocess.STREAM, - ) - self._proc.set_exit_callback(self._proc_on_exit) - tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout) - - @property - def is_process_active(self): - return self._proc is not None and self._proc.returncode is None - - @websocket_method("stdin") - def handle_stdin(self, json_message): - if not self.is_process_active: - return - data = json_message["data"] - data = codecs.encode(data, "utf8", "replace") - _LOGGER.debug("< stdin: %s", data) - self._proc.stdin.write(data) - - @tornado.gen.coroutine - def _redirect_stdout(self): - reg = b"[\n\r]" - - while True: - try: - data = yield self._proc.stdout.read_until_regex(reg) - except tornado.iostream.StreamClosedError: - break - data = codecs.decode(data, "utf8", "replace") - - _LOGGER.debug("> stdout: %s", data) - self.write_message({"event": "line", "data": data}) - - def _proc_on_exit(self, returncode): - if not self._is_closed: - # Check if the proc was not forcibly closed - _LOGGER.info("Process exited with return code %s", returncode) - self.write_message({"event": "exit", "code": returncode}) - - def on_close(self): - # Check if proc exists (if 'start' has been run) - if self.is_process_active: - _LOGGER.debug("Terminating process") - self._proc.proc.terminate() - # Shutdown proc on WS close - self._is_closed = True - - def build_command(self, json_message): - raise NotImplementedError - - -class EsphomeLogsHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - return [ - "esphome", - "--dashboard", - "logs", - config_file, - "--device", - json_message["port"], - ] - - -class EsphomeRenameHandler(EsphomeCommandWebSocket): - old_name: str - - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - self.old_name = json_message["configuration"] - return [ - "esphome", - "--dashboard", - "rename", - config_file, - json_message["newName"], - ] - - def _proc_on_exit(self, returncode): - super()._proc_on_exit(returncode) - - if returncode != 0: - return - - # Remove the old ping result from the cache - PING_RESULT.pop(self.old_name, None) - - -class EsphomeUploadHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - return [ - "esphome", - "--dashboard", - "upload", - config_file, - "--device", - json_message["port"], - ] - - -class EsphomeRunHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - return [ - "esphome", - "--dashboard", - "run", - config_file, - "--device", - json_message["port"], - ] - - -class EsphomeCompileHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - command = ["esphome", "--dashboard", "compile"] - if json_message.get("only_generate", False): - command.append("--only-generate") - command.append(config_file) - return command - - -class EsphomeValidateHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", "config", config_file] - - -class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", "clean-mqtt", config_file] - - -class EsphomeCleanHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", "clean", config_file] - - -class EsphomeVscodeHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - return ["esphome", "--dashboard", "-q", "vscode", "dummy"] - - -class EsphomeAceEditorHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - return ["esphome", "--dashboard", "-q", "vscode", "--ace", settings.config_dir] - - -class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - return ["esphome", "--dashboard", "update-all", settings.config_dir] - - -class SerialPortRequestHandler(BaseHandler): - @authenticated - def get(self): - ports = get_serial_ports() - data = [] - for port in ports: - desc = port.description - if port.path == "/dev/ttyAMA0": - desc = "UART pins on GPIO header" - split_desc = desc.split(" - ") - if len(split_desc) == 2 and split_desc[0] == split_desc[1]: - # Some serial ports repeat their values - desc = split_desc[0] - data.append({"port": port.path, "desc": desc}) - data.append({"port": "OTA", "desc": "Over-The-Air"}) - data.sort(key=lambda x: x["port"], reverse=True) - self.set_header("content-type", "application/json") - self.write(json.dumps(data)) - - -class WizardRequestHandler(BaseHandler): - @authenticated - def post(self): - from esphome import wizard - - kwargs = { - k: v - for k, v in json.loads(self.request.body.decode()).items() - if k in ("name", "platform", "board", "ssid", "psk", "password") - } - if not kwargs["name"]: - self.set_status(422) - self.set_header("content-type", "application/json") - self.write(json.dumps({"error": "Name is required"})) - return - - kwargs["friendly_name"] = kwargs["name"] - kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) - - kwargs["ota_password"] = secrets.token_hex(16) - noise_psk = secrets.token_bytes(32) - kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() - filename = f"{kwargs['name']}.yaml" - destination = settings.rel_path(filename) - wizard.wizard_write(path=destination, **kwargs) - self.set_status(200) - self.set_header("content-type", "application/json") - self.write(json.dumps({"configuration": filename})) - self.finish() - - -class ImportRequestHandler(BaseHandler): - @authenticated - def post(self): - from esphome.components.dashboard_import import import_config - - args = json.loads(self.request.body.decode()) - try: - name = args["name"] - friendly_name = args.get("friendly_name") - encryption = args.get("encryption", False) - - imported_device = next( - (res for res in IMPORT_RESULT.values() if res.device_name == name), None - ) - - if imported_device is not None: - network = imported_device.network - if friendly_name is None: - friendly_name = imported_device.friendly_name - else: - network = const.CONF_WIFI - - import_config( - settings.rel_path(f"{name}.yaml"), - name, - friendly_name, - args["project_name"], - args["package_import_url"], - network, - encryption, - ) - except FileExistsError: - self.set_status(500) - self.write("File already exists") - return - except ValueError: - self.set_status(422) - self.write("Invalid package url") - return - - self.set_status(200) - self.set_header("content-type", "application/json") - self.write(json.dumps({"configuration": f"{name}.yaml"})) - self.finish() - - -class DownloadBinaryRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - type = self.get_argument("type", "firmware.bin") - - storage_path = ext_storage_path(settings.config_dir, configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.send_error(404) - return - - if storage_json.target_platform.lower() == const.PLATFORM_RP2040: - filename = f"{storage_json.name}.uf2" - path = storage_json.firmware_bin_path.replace( - "firmware.bin", "firmware.uf2" - ) - - elif storage_json.target_platform.lower() == const.PLATFORM_ESP8266: - filename = f"{storage_json.name}.bin" - path = storage_json.firmware_bin_path - - elif type == "firmware.bin": - filename = f"{storage_json.name}.bin" - path = storage_json.firmware_bin_path - - elif type == "firmware-factory.bin": - filename = f"{storage_json.name}-factory.bin" - path = storage_json.firmware_bin_path.replace( - "firmware.bin", "firmware-factory.bin" - ) - - else: - args = ["esphome", "idedata", settings.rel_path(configuration)] - rc, stdout, _ = run_system_command(*args) - - if rc != 0: - self.send_error(404 if rc == 2 else 500) - return - - idedata = platformio_api.IDEData(json.loads(stdout)) - - found = False - for image in idedata.extra_flash_images: - if image.path.endswith(type): - path = image.path - filename = type - found = True - break - - if not found: - self.send_error(404) - return - - self.set_header("Content-Type", "application/octet-stream") - self.set_header("Content-Disposition", f'attachment; filename="{filename}"') - self.set_header("Cache-Control", "no-cache") - if not Path(path).is_file(): - self.send_error(404) - return - - with open(path, "rb") as f: - while True: - data = f.read(16384) - if not data: - break - self.write(data) - self.finish() - - -class EsphomeVersionHandler(BaseHandler): - @authenticated - def get(self): - self.set_header("Content-Type", "application/json") - self.write(json.dumps({"version": const.__version__})) - self.finish() - - -def _list_dashboard_entries(): - files = settings.list_yaml_files() - return [DashboardEntry(file) for file in files] - - -class DashboardEntry: - def __init__(self, path): - self.path = path - self._storage = None - self._loaded_storage = False - - @property - def filename(self): - return os.path.basename(self.path) - - @property - def storage(self) -> Optional[StorageJSON]: - if not self._loaded_storage: - self._storage = StorageJSON.load( - ext_storage_path(settings.config_dir, self.filename) - ) - self._loaded_storage = True - return self._storage - - @property - def address(self): - if self.storage is None: - return None - return self.storage.address - - @property - def no_mdns(self): - if self.storage is None: - return None - return self.storage.no_mdns - - @property - def web_port(self): - if self.storage is None: - return None - return self.storage.web_port - - @property - def name(self): - if self.storage is None: - return self.filename.replace(".yml", "").replace(".yaml", "") - return self.storage.name - - @property - def friendly_name(self): - if self.storage is None: - return self.name - return self.storage.friendly_name - - @property - def comment(self): - if self.storage is None: - return None - return self.storage.comment - - @property - def target_platform(self): - if self.storage is None: - return None - return self.storage.target_platform - - @property - def update_available(self): - if self.storage is None: - return True - return self.update_old != self.update_new - - @property - def update_old(self): - if self.storage is None: - return "" - return self.storage.esphome_version or "" - - @property - def update_new(self): - return const.__version__ - - @property - def loaded_integrations(self): - if self.storage is None: - return [] - return self.storage.loaded_integrations - - -class ListDevicesHandler(BaseHandler): - @authenticated - def get(self): - entries = _list_dashboard_entries() - self.set_header("content-type", "application/json") - configured = {entry.name for entry in entries} - self.write( - json.dumps( - { - "configured": [ - { - "name": entry.name, - "friendly_name": entry.friendly_name, - "configuration": entry.filename, - "loaded_integrations": entry.loaded_integrations, - "deployed_version": entry.update_old, - "current_version": entry.update_new, - "path": entry.path, - "comment": entry.comment, - "address": entry.address, - "web_port": entry.web_port, - "target_platform": entry.target_platform, - } - for entry in entries - ], - "importable": [ - { - "name": res.device_name, - "friendly_name": res.friendly_name, - "package_import_url": res.package_import_url, - "project_name": res.project_name, - "project_version": res.project_version, - "network": res.network, - } - for res in IMPORT_RESULT.values() - if res.device_name not in configured - ], - } - ) - ) - - -class MainRequestHandler(BaseHandler): - @authenticated - def get(self): - begin = bool(self.get_argument("begin", False)) - - self.render( - "index.template.html", - begin=begin, - **template_args(), - login_enabled=settings.using_password, - ) - - -def _ping_func(filename, address): - if os.name == "nt": - command = ["ping", "-n", "1", address] - else: - command = ["ping", "-c", "1", address] - rc, _, _ = run_system_command(*command) - return filename, rc == 0 - - -class PrometheusServiceDiscoveryHandler(BaseHandler): - @authenticated - def get(self): - entries = _list_dashboard_entries() - self.set_header("content-type", "application/json") - sd = [] - for entry in entries: - if entry.web_port is None: - continue - labels = { - "__meta_name": entry.name, - "__meta_esp_platform": entry.target_platform, - "__meta_esphome_version": entry.storage.esphome_version, - } - for integration in entry.storage.loaded_integrations: - labels[f"__meta_integration_{integration}"] = "true" - sd.append( - { - "targets": [ - f"{entry.address}:{entry.web_port}", - ], - "labels": labels, - } - ) - self.write(json.dumps(sd)) - - -class BoardsRequestHandler(BaseHandler): - @authenticated - def get(self, platform: str): - from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS - from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS - from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS - - platform_to_boards = { - "esp32": ESP32_BOARDS, - "esp8266": ESP8266_BOARDS, - "rp2040": RP2040_BOARDS, - } - # filter all ESP32 variants by requested platform - if platform.startswith("esp32"): - boards = { - k: v - for k, v in platform_to_boards["esp32"].items() - if v[const.KEY_VARIANT] == platform.upper() - } - else: - boards = platform_to_boards[platform] - - # map to a {board_name: board_title} dict - platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()} - # sort by board title - boards_items = sorted(platform_boards.items(), key=lambda item: item[1]) - output = [{"items": dict(boards_items)}] - - self.set_header("content-type", "application/json") - self.write(json.dumps(output)) - - -class MDNSStatusThread(threading.Thread): - def run(self): - global IMPORT_RESULT - - zc = EsphomeZeroconf() - - def on_update(dat): - for key, b in dat.items(): - PING_RESULT[key] = b - - stat = DashboardStatus(zc, on_update) - imports = DashboardImportDiscovery(zc) - - stat.start() - while not STOP_EVENT.is_set(): - entries = _list_dashboard_entries() - hosts = {} - for entry in entries: - if entry.no_mdns is not True: - hosts[entry.filename] = f"{entry.name}.local." - - stat.request_query(hosts) - IMPORT_RESULT = imports.import_state - - PING_REQUEST.wait() - PING_REQUEST.clear() - - stat.stop() - stat.join() - imports.cancel() - zc.close() - - -class PingStatusThread(threading.Thread): - def run(self): - with multiprocessing.Pool(processes=8) as pool: - while not STOP_EVENT.wait(2): - # Only do pings if somebody has the dashboard open - - def callback(ret): - PING_RESULT[ret[0]] = ret[1] - - entries = _list_dashboard_entries() - queue = collections.deque() - for entry in entries: - if entry.no_mdns is True: - continue - - if entry.address is None: - PING_RESULT[entry.filename] = None - continue - - result = pool.apply_async( - _ping_func, (entry.filename, entry.address), callback=callback +class DashboardEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + """Event loop policy for Home Assistant.""" + + def __init__(self, debug: bool) -> None: + """Init the event loop policy.""" + super().__init__() + self.debug = debug + self._watcher: asyncio.AbstractChildWatcher | None = None + + def _init_watcher(self) -> None: + """Initialize the watcher for child processes. + + Back ported from cpython 3.12 + """ + with events._lock: # type: ignore[attr-defined] # pylint: disable=protected-access + if self._watcher is None: # pragma: no branch + if can_use_pidfd(): + self._watcher = asyncio.PidfdChildWatcher() + else: + self._watcher = asyncio.ThreadedChildWatcher() + if threading.current_thread() is threading.main_thread(): + self._watcher.attach_loop( + self._local._loop # type: ignore[attr-defined] # pylint: disable=protected-access ) - queue.append(result) - while queue: - item = queue[0] - if item.ready(): - queue.popleft() - continue + @property + def loop_name(self) -> str: + """Return name of the loop.""" + return self._loop_factory.__name__ # type: ignore[no-any-return,attr-defined] - try: - item.get(0.1) - except OSError: - # ping not installed - pass - except multiprocessing.TimeoutError: - pass + def new_event_loop(self) -> asyncio.AbstractEventLoop: + """Get the event loop.""" + loop: asyncio.AbstractEventLoop = super().new_event_loop() + loop.set_exception_handler(_async_loop_exception_handler) - if STOP_EVENT.is_set(): - pool.terminate() - return + if self.debug: + loop.set_debug(True) - PING_REQUEST.wait() - PING_REQUEST.clear() - - -class MqttStatusThread(threading.Thread): - def run(self): - from esphome import mqtt - - entries = _list_dashboard_entries() - - config = mqtt.config_from_env() - topic = "esphome/discover/#" - - def on_message(client, userdata, msg): - nonlocal entries - - payload = msg.payload.decode(errors="backslashreplace") - if len(payload) > 0: - data = json.loads(payload) - if "name" not in data: - return - for entry in entries: - if entry.name == data["name"]: - PING_RESULT[entry.filename] = True - return - - def on_connect(client, userdata, flags, return_code): - client.publish("esphome/discover", None, retain=False) - - mqttid = str(binascii.hexlify(os.urandom(6)).decode()) - - client = mqtt.prepare( - config, - [topic], - on_message, - on_connect, - None, - None, - f"esphome-dashboard-{mqttid}", + executor = ThreadPoolExecutor( + thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS ) - client.loop_start() - - while not STOP_EVENT.wait(2): - # update entries - entries = _list_dashboard_entries() - - # will be set to true on on_message - for entry in entries: - if entry.no_mdns: - PING_RESULT[entry.filename] = False - - client.publish("esphome/discover", None, retain=False) - MQTT_PING_REQUEST.wait() - MQTT_PING_REQUEST.clear() - - client.disconnect() - client.loop_stop() + loop.set_default_executor(executor) + # bind the built-in time.monotonic directly as loop.time to avoid the + # overhead of the additional method call since its the most called loop + # method and its roughly 10%+ of all the call time in base_events.py + loop.time = monotonic # type: ignore[method-assign] + return loop -class PingRequestHandler(BaseHandler): - @authenticated - def get(self): - PING_REQUEST.set() - if settings.status_use_mqtt: - MQTT_PING_REQUEST.set() - self.set_header("content-type", "application/json") - self.write(json.dumps(PING_RESULT)) +def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: + """Handle all exception inside the core loop.""" + kwargs = {} + if exception := context.get("exception"): + kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) - -class InfoRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - yaml_path = settings.rel_path(configuration) - all_yaml_files = settings.list_yaml_files() - - if yaml_path not in all_yaml_files: - self.set_status(404) - return - - self.set_header("content-type", "application/json") - self.write(DashboardEntry(yaml_path).storage.to_json()) - - -class EditRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - filename = settings.rel_path(configuration) - content = "" - if os.path.isfile(filename): - with open(file=filename, encoding="utf-8") as f: - content = f.read() - self.write(content) - - @authenticated - @bind_config - def post(self, configuration=None): - with open(file=settings.rel_path(configuration), mode="wb") as f: - f.write(self.request.body) - self.set_status(200) - - -class DeleteRequestHandler(BaseHandler): - @authenticated - @bind_config - def post(self, configuration=None): - config_file = settings.rel_path(configuration) - storage_path = ext_storage_path(settings.config_dir, configuration) - - trash_path = trash_storage_path(settings.config_dir) - mkdir_p(trash_path) - shutil.move(config_file, os.path.join(trash_path, configuration)) - - storage_json = StorageJSON.load(storage_path) - if storage_json is not None: - # Delete build folder (if exists) - name = storage_json.name - build_folder = os.path.join(settings.config_dir, name) - if build_folder is not None: - shutil.rmtree(build_folder, os.path.join(trash_path, name)) - - # Remove the old ping result from the cache - PING_RESULT.pop(configuration, None) - - -class UndoDeleteRequestHandler(BaseHandler): - @authenticated - @bind_config - def post(self, configuration=None): - config_file = settings.rel_path(configuration) - trash_path = trash_storage_path(settings.config_dir) - shutil.move(os.path.join(trash_path, configuration), config_file) - - -PING_RESULT: dict = {} -IMPORT_RESULT = {} -STOP_EVENT = threading.Event() -PING_REQUEST = threading.Event() -MQTT_PING_REQUEST = threading.Event() - - -class LoginHandler(BaseHandler): - def get(self): - if is_authenticated(self): - self.redirect("./") - else: - self.render_login_page() - - def render_login_page(self, error=None): - self.render( - "login.template.html", - error=error, - ha_addon=settings.using_ha_addon_auth, - has_username=bool(settings.username), - **template_args(), + logger = logging.getLogger(__package__) + if source_traceback := context.get("source_traceback"): + stack_summary = "".join(traceback.format_list(source_traceback)) + logger.error( + "Error doing job: %s: %s", + context["message"], + stack_summary, + **kwargs, # type: ignore[arg-type] ) + return - def post_ha_addon_login(self): - import requests - - headers = { - "X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN"), - } - - data = { - "username": self.get_argument("username", ""), - "password": self.get_argument("password", ""), - } - try: - req = requests.post( - "http://supervisor/auth", headers=headers, json=data, timeout=30 - ) - if req.status_code == 200: - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("/") - return - except Exception as err: # pylint: disable=broad-except - _LOGGER.warning("Error during Hass.io auth request: %s", err) - self.set_status(500) - self.render_login_page(error="Internal server error") - return - self.set_status(401) - self.render_login_page(error="Invalid username or password") - - def post_native_login(self): - username = self.get_argument("username", "") - password = self.get_argument("password", "") - if settings.check_password(username, password): - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("./") - return - error_str = ( - "Invalid username or password" if settings.username else "Invalid password" - ) - self.set_status(401) - self.render_login_page(error=error_str) - - def post(self): - if settings.using_ha_addon_auth: - self.post_ha_addon_login() - else: - self.post_native_login() - - -class LogoutHandler(BaseHandler): - @authenticated - def get(self): - self.clear_cookie("authenticated") - self.redirect("./login") - - -class SecretKeysRequestHandler(BaseHandler): - @authenticated - def get(self): - filename = None - - for secret_filename in const.SECRETS_FILES: - relative_filename = settings.rel_path(secret_filename) - if os.path.isfile(relative_filename): - filename = relative_filename - break - - if filename is None: - self.send_error(404) - return - - secret_keys = list(yaml_util.load_yaml(filename, clear_secrets=False)) - - self.set_header("content-type", "application/json") - self.write(json.dumps(secret_keys)) - - -class SafeLoaderIgnoreUnknown(yaml.SafeLoader): - def ignore_unknown(self, node): - return f"{node.tag} {node.value}" - - def construct_yaml_binary(self, node) -> str: - return super().construct_yaml_binary(node).decode("ascii") - - -SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown) -SafeLoaderIgnoreUnknown.add_constructor( - "tag:yaml.org,2002:binary", SafeLoaderIgnoreUnknown.construct_yaml_binary -) - - -class JsonConfigRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - filename = settings.rel_path(configuration) - if not os.path.isfile(filename): - self.send_error(404) - return - - args = ["esphome", "config", settings.rel_path(configuration), "--show-secrets"] - - rc, stdout, _ = run_system_command(*args) - - if rc != 0: - self.send_error(422) - return - - data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown) - self.set_header("content-type", "application/json") - self.write(json.dumps(data)) - self.finish() - - -def get_base_frontend_path(): - if ENV_DEV not in os.environ: - import esphome_dashboard - - return esphome_dashboard.where() - - static_path = os.environ[ENV_DEV] - if not static_path.endswith("/"): - static_path += "/" - - # This path can be relative, so resolve against the root or else templates don't work - return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) - - -def get_static_path(*args): - return os.path.join(get_base_frontend_path(), "static", *args) - - -@functools.cache -def get_static_file_url(name): - base = f"./static/{name}" - - if ENV_DEV in os.environ: - return base - - # Module imports can't deduplicate if stuff added to url - if name == "js/esphome/index.js": - import esphome_dashboard - - return base.replace("index.js", esphome_dashboard.entrypoint()) - - path = get_static_path(name) - with open(path, "rb") as f_handle: - hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] - return f"{base}?hash={hash_}" - - -def make_app(debug=get_bool_env(ENV_DEV)): - def log_function(handler): - if handler.get_status() < 400: - log_method = access_log.info - - if isinstance(handler, SerialPortRequestHandler) and not debug: - return - if isinstance(handler, PingRequestHandler) and not debug: - return - elif handler.get_status() < 500: - log_method = access_log.warning - else: - log_method = access_log.error - - request_time = 1000.0 * handler.request.request_time() - # pylint: disable=protected-access - log_method( - "%d %s %.2fms", - handler.get_status(), - handler._request_summary(), - request_time, - ) - - class StaticFileHandler(tornado.web.StaticFileHandler): - def set_extra_headers(self, path): - if "favicon.ico" in path: - self.set_header("Cache-Control", "max-age=84600, public") - else: - self.set_header( - "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0" - ) - - app_settings = { - "debug": debug, - "cookie_secret": settings.cookie_secret, - "log_function": log_function, - "websocket_ping_interval": 30.0, - "template_path": get_base_frontend_path(), - } - rel = settings.relative_url - app = tornado.web.Application( - [ - (f"{rel}", MainRequestHandler), - (f"{rel}login", LoginHandler), - (f"{rel}logout", LogoutHandler), - (f"{rel}logs", EsphomeLogsHandler), - (f"{rel}upload", EsphomeUploadHandler), - (f"{rel}run", EsphomeRunHandler), - (f"{rel}compile", EsphomeCompileHandler), - (f"{rel}validate", EsphomeValidateHandler), - (f"{rel}clean-mqtt", EsphomeCleanMqttHandler), - (f"{rel}clean", EsphomeCleanHandler), - (f"{rel}vscode", EsphomeVscodeHandler), - (f"{rel}ace", EsphomeAceEditorHandler), - (f"{rel}update-all", EsphomeUpdateAllHandler), - (f"{rel}info", InfoRequestHandler), - (f"{rel}edit", EditRequestHandler), - (f"{rel}download.bin", DownloadBinaryRequestHandler), - (f"{rel}serial-ports", SerialPortRequestHandler), - (f"{rel}ping", PingRequestHandler), - (f"{rel}delete", DeleteRequestHandler), - (f"{rel}undo-delete", UndoDeleteRequestHandler), - (f"{rel}wizard", WizardRequestHandler), - (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}), - (f"{rel}devices", ListDevicesHandler), - (f"{rel}import", ImportRequestHandler), - (f"{rel}secret_keys", SecretKeysRequestHandler), - (f"{rel}json-config", JsonConfigRequestHandler), - (f"{rel}rename", EsphomeRenameHandler), - (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), - (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler), - (f"{rel}version", EsphomeVersionHandler), - ], - **app_settings, + logger.error( + "Error doing job: %s", + context["message"], + **kwargs, # type: ignore[arg-type] ) - return app - -def start_web_server(args): +def start_dashboard(args) -> None: + """Start the dashboard.""" settings.parse_args(args) - mkdir_p(settings.rel_path(".esphome")) if settings.using_auth: - path = esphome_storage_path(settings.config_dir) + path = esphome_storage_path() storage = EsphomeStorageJSON.load(path) if storage is None: storage = EsphomeStorageJSON.get_default() storage.save(path) settings.cookie_secret = storage.cookie_secret - app = make_app(args.verbose) - if args.socket is not None: - _LOGGER.info( - "Starting dashboard web server on unix socket %s and configuration dir %s...", - args.socket, - settings.config_dir, - ) - server = tornado.httpserver.HTTPServer(app) - socket = tornado.netutil.bind_unix_socket(args.socket, mode=0o666) - server.add_socket(socket) - else: - _LOGGER.info( - "Starting dashboard web server on http://%s:%s and configuration dir %s...", - args.address, - args.port, - settings.config_dir, - ) - app.listen(args.port, args.address) - - if args.open_ui: - import webbrowser - - webbrowser.open(f"http://{args.address}:{args.port}") - - if settings.status_use_ping: - status_thread = PingStatusThread() - else: - status_thread = MDNSStatusThread() - status_thread.start() - - if settings.status_use_mqtt: - status_thread_mqtt = MqttStatusThread() - status_thread_mqtt.start() + asyncio.set_event_loop_policy(DashboardEventLoopPolicy(settings.verbose)) try: - tornado.ioloop.IOLoop.current().start() + asyncio.run(async_start(args)) except KeyboardInterrupt: - _LOGGER.info("Shutting down...") - STOP_EVENT.set() - PING_REQUEST.set() - status_thread.join() - if settings.status_use_mqtt: - status_thread_mqtt.join() - MQTT_PING_REQUEST.set() - if args.socket is not None: - os.remove(args.socket) + pass + + +async def async_start(args) -> None: + """Start the dashboard.""" + dashboard = DASHBOARD + await dashboard.async_setup() + sock: socket.socket | None = args.socket + address: str | None = args.address + port: int | None = args.port + + start_web_server(make_app(args.verbose), sock, address, port, settings.config_dir) + + if args.open_ui: + import webbrowser + + webbrowser.open(f"http://{args.address}:{args.port}") + + try: + await dashboard.async_run() + finally: + if sock: + os.remove(sock) diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py new file mode 100644 index 0000000000..b78a909220 --- /dev/null +++ b/esphome/dashboard/dns.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import asyncio +import sys + +from icmplib import NameLookupError, async_resolve + +if sys.version_info >= (3, 11): + from asyncio import timeout as async_timeout +else: + from async_timeout import timeout as async_timeout + + +async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: + """Wrap the icmplib async_resolve function.""" + try: + async with async_timeout(2): + return await async_resolve(hostname) + except (asyncio.TimeoutError, NameLookupError, UnicodeError) as ex: + return ex + + +class DNSCache: + """DNS cache for the dashboard.""" + + def __init__(self, ttl: int | None = 120) -> None: + """Initialize the DNSCache.""" + self._cache: dict[str, tuple[float, list[str] | Exception]] = {} + self._ttl = ttl + + async def async_resolve( + self, hostname: str, now_monotonic: float + ) -> list[str] | Exception: + """Resolve a hostname to a list of IP address.""" + if expire_time_addresses := self._cache.get(hostname): + expire_time, addresses = expire_time_addresses + if expire_time > now_monotonic: + return addresses + + expires = now_monotonic + self._ttl + addresses = await _async_resolve_wrapper(hostname) + self._cache[hostname] = (expires, addresses) + return addresses diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py new file mode 100644 index 0000000000..cb0d4a3772 --- /dev/null +++ b/esphome/dashboard/entries.py @@ -0,0 +1,397 @@ +from __future__ import annotations + +import asyncio +from collections import defaultdict +import logging +import os +from typing import TYPE_CHECKING, Any + +from esphome import const, util +from esphome.storage_json import StorageJSON, ext_storage_path + +from .const import ( + DASHBOARD_COMMAND, + EVENT_ENTRY_ADDED, + EVENT_ENTRY_REMOVED, + EVENT_ENTRY_STATE_CHANGED, + EVENT_ENTRY_UPDATED, +) +from .enum import StrEnum +from .util.subprocess import async_run_system_command + +if TYPE_CHECKING: + from .core import ESPHomeDashboard + +_LOGGER = logging.getLogger(__name__) + + +DashboardCacheKeyType = tuple[int, int, float, int] + +# Currently EntryState is a simple +# online/offline/unknown enum, but in the future +# it may be expanded to include more states + + +class EntryState(StrEnum): + ONLINE = "online" + OFFLINE = "offline" + UNKNOWN = "unknown" + + +_BOOL_TO_ENTRY_STATE = { + True: EntryState.ONLINE, + False: EntryState.OFFLINE, + None: EntryState.UNKNOWN, +} +_ENTRY_STATE_TO_BOOL = { + EntryState.ONLINE: True, + EntryState.OFFLINE: False, + EntryState.UNKNOWN: None, +} + + +def bool_to_entry_state(value: bool) -> EntryState: + """Convert a bool to an entry state.""" + return _BOOL_TO_ENTRY_STATE[value] + + +def entry_state_to_bool(value: EntryState) -> bool | None: + """Convert an entry state to a bool.""" + return _ENTRY_STATE_TO_BOOL[value] + + +class DashboardEntries: + """Represents all dashboard entries.""" + + __slots__ = ( + "_dashboard", + "_loop", + "_config_dir", + "_entries", + "_entry_states", + "_loaded_entries", + "_update_lock", + "_name_to_entry", + ) + + def __init__(self, dashboard: ESPHomeDashboard) -> None: + """Initialize the DashboardEntries.""" + self._dashboard = dashboard + self._loop = asyncio.get_running_loop() + self._config_dir = dashboard.settings.config_dir + # Entries are stored as + # { + # "path/to/file.yaml": DashboardEntry, + # ... + # } + self._entries: dict[str, DashboardEntry] = {} + self._loaded_entries = False + self._update_lock = asyncio.Lock() + self._name_to_entry: dict[str, set[DashboardEntry]] = defaultdict(set) + + def get(self, path: str) -> DashboardEntry | None: + """Get an entry by path.""" + return self._entries.get(path) + + def get_by_name(self, name: str) -> set[DashboardEntry] | None: + """Get an entry by name.""" + return self._name_to_entry.get(name) + + async def _async_all(self) -> list[DashboardEntry]: + """Return all entries.""" + return list(self._entries.values()) + + def all(self) -> list[DashboardEntry]: + """Return all entries.""" + return asyncio.run_coroutine_threadsafe(self._async_all(), self._loop).result() + + def async_all(self) -> list[DashboardEntry]: + """Return all entries.""" + return list(self._entries.values()) + + def set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + asyncio.run_coroutine_threadsafe( + self._async_set_state(entry, state), self._loop + ).result() + + async def _async_set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + self.async_set_state(entry, state) + + def async_set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + if entry.state == state: + return + entry.state = state + self._dashboard.bus.async_fire( + EVENT_ENTRY_STATE_CHANGED, {"entry": entry, "state": state} + ) + + async def async_request_update_entries(self) -> None: + """Request an update of the dashboard entries from disk. + + If an update is already in progress, this will do nothing. + """ + if self._update_lock.locked(): + _LOGGER.debug("Dashboard entries are already being updated") + return + await self.async_update_entries() + + async def async_update_entries(self) -> None: + """Update the dashboard entries from disk.""" + async with self._update_lock: + await self._async_update_entries() + + def _load_entries( + self, entries: dict[DashboardEntry, DashboardCacheKeyType] + ) -> None: + """Load all entries from disk.""" + for entry, cache_key in entries.items(): + _LOGGER.debug( + "Loading dashboard entry %s because cache key changed: %s", + entry.path, + cache_key, + ) + entry.load_from_disk(cache_key) + + async def _async_update_entries(self) -> list[DashboardEntry]: + """Sync the dashboard entries from disk.""" + _LOGGER.debug("Updating dashboard entries") + # At some point it would be nice to use watchdog to avoid polling + + path_to_cache_key = await self._loop.run_in_executor( + None, self._get_path_to_cache_key + ) + entries = self._entries + name_to_entry = self._name_to_entry + added: dict[DashboardEntry, DashboardCacheKeyType] = {} + updated: dict[DashboardEntry, DashboardCacheKeyType] = {} + removed: set[DashboardEntry] = { + entry + for filename, entry in entries.items() + if filename not in path_to_cache_key + } + original_names: dict[DashboardEntry, str] = {} + + for path, cache_key in path_to_cache_key.items(): + if not (entry := entries.get(path)): + entry = DashboardEntry(path, cache_key) + added[entry] = cache_key + continue + + if entry.cache_key != cache_key: + updated[entry] = cache_key + original_names[entry] = entry.name + + if added or updated: + await self._loop.run_in_executor( + None, self._load_entries, {**added, **updated} + ) + + bus = self._dashboard.bus + for entry in added: + entries[entry.path] = entry + name_to_entry[entry.name].add(entry) + bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry}) + + for entry in removed: + del entries[entry.path] + name_to_entry[entry.name].discard(entry) + bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry}) + + for entry in updated: + if (original_name := original_names[entry]) != (current_name := entry.name): + name_to_entry[original_name].discard(entry) + name_to_entry[current_name].add(entry) + bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry}) + + def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: + """Return a dict of path to cache key.""" + path_to_cache_key: dict[str, DashboardCacheKeyType] = {} + # + # The cache key is (inode, device, mtime, size) + # which allows us to avoid locking since it ensures + # every iteration of this call will always return the newest + # items from disk at the cost of a stat() call on each + # file which is much faster than reading the file + # for the cache hit case which is the common case. + # + for file in util.list_yaml_files([self._config_dir]): + try: + # Prefer the json storage path if it exists + stat = os.stat(ext_storage_path(os.path.basename(file))) + except OSError: + try: + # Fallback to the yaml file if the storage + # file does not exist or could not be generated + stat = os.stat(file) + except OSError: + # File was deleted, ignore + continue + path_to_cache_key[file] = ( + stat.st_ino, + stat.st_dev, + stat.st_mtime, + stat.st_size, + ) + return path_to_cache_key + + def async_schedule_storage_json_update(self, filename: str) -> None: + """Schedule a task to update the storage JSON file.""" + self._dashboard.async_create_background_task( + async_run_system_command( + [*DASHBOARD_COMMAND, "compile", "--only-generate", filename] + ) + ) + + +class DashboardEntry: + """Represents a single dashboard entry. + + This class is thread-safe and read-only. + """ + + __slots__ = ( + "path", + "filename", + "_storage_path", + "cache_key", + "storage", + "state", + "_to_dict", + ) + + def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None: + """Initialize the DashboardEntry.""" + self.path = path + self.filename: str = os.path.basename(path) + self._storage_path = ext_storage_path(self.filename) + self.cache_key = cache_key + self.storage: StorageJSON | None = None + self.state = EntryState.UNKNOWN + self._to_dict: dict[str, Any] | None = None + + def __repr__(self) -> str: + """Return the representation of this entry.""" + return ( + f"DashboardEntry(path={self.path} " + f"address={self.address} " + f"web_port={self.web_port} " + f"name={self.name} " + f"no_mdns={self.no_mdns} " + f"state={self.state} " + ")" + ) + + def to_dict(self) -> dict[str, Any]: + """Return a dict representation of this entry. + + The dict includes the loaded configuration but not + the current state of the entry. + """ + if self._to_dict is None: + self._to_dict = { + "name": self.name, + "friendly_name": self.friendly_name, + "configuration": self.filename, + "loaded_integrations": sorted(self.loaded_integrations), + "deployed_version": self.update_old, + "current_version": self.update_new, + "path": self.path, + "comment": self.comment, + "address": self.address, + "web_port": self.web_port, + "target_platform": self.target_platform, + } + return self._to_dict + + def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None: + """Load this entry from disk.""" + self.storage = StorageJSON.load(self._storage_path) + self._to_dict = None + # + # Currently StorageJSON.load() will return None if the file does not exist + # + # StorageJSON currently does not provide an updated cache key so we use the + # one that is passed in. + # + # The cache key was read from the disk moments ago and may be stale but + # it does not matter since we are polling anyways, and the next call to + # async_update_entries() will load it again in the extremely rare case that + # it changed between the two calls. + # + if cache_key: + self.cache_key = cache_key + + @property + def address(self) -> str | None: + """Return the address of this entry.""" + if self.storage is None: + return None + return self.storage.address + + @property + def no_mdns(self) -> bool | None: + """Return the no_mdns of this entry.""" + if self.storage is None: + return None + return self.storage.no_mdns + + @property + def web_port(self) -> int | None: + """Return the web port of this entry.""" + if self.storage is None: + return None + return self.storage.web_port + + @property + def name(self) -> str: + """Return the name of this entry.""" + if self.storage is None: + return self.filename.replace(".yml", "").replace(".yaml", "") + return self.storage.name + + @property + def friendly_name(self) -> str: + """Return the friendly name of this entry.""" + if self.storage is None: + return self.name + return self.storage.friendly_name + + @property + def comment(self) -> str | None: + """Return the comment of this entry.""" + if self.storage is None: + return None + return self.storage.comment + + @property + def target_platform(self) -> str | None: + """Return the target platform of this entry.""" + if self.storage is None: + return None + return self.storage.target_platform + + @property + def update_available(self) -> bool: + """Return if an update is available for this entry.""" + if self.storage is None: + return True + return self.update_old != self.update_new + + @property + def update_old(self) -> str: + if self.storage is None: + return "" + return self.storage.esphome_version or "" + + @property + def update_new(self) -> str: + return const.__version__ + + @property + def loaded_integrations(self) -> set[str]: + if self.storage is None: + return [] + return self.storage.loaded_integrations diff --git a/esphome/dashboard/enum.py b/esphome/dashboard/enum.py new file mode 100644 index 0000000000..0fe30cf92a --- /dev/null +++ b/esphome/dashboard/enum.py @@ -0,0 +1,20 @@ +"""Enum backports from standard lib.""" + +from __future__ import annotations + +from enum import Enum +from typing import Any + + +class StrEnum(str, Enum): + """Partial backport of Python 3.11's StrEnum for our basic use cases.""" + + def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum: + """Create a new StrEnum instance.""" + if not isinstance(value, str): + raise TypeError(f"{value!r} is not a string") + return super().__new__(cls, value, *args, **kwargs) + + def __str__(self) -> str: + """Return self.value.""" + return str(self.value) diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py new file mode 100644 index 0000000000..1f05abab4c --- /dev/null +++ b/esphome/dashboard/settings.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import hmac +import os +from pathlib import Path +from typing import Any + +from esphome.core import CORE +from esphome.helpers import get_bool_env + +from .util.password import password_hash + + +class DashboardSettings: + """Settings for the dashboard.""" + + __slots__ = ( + "config_dir", + "password_hash", + "username", + "using_password", + "on_ha_addon", + "cookie_secret", + "absolute_config_dir", + "verbose", + ) + + def __init__(self) -> None: + """Initialize the dashboard settings.""" + self.config_dir: str = "" + self.password_hash: str = "" + self.username: str = "" + self.using_password: bool = False + self.on_ha_addon: bool = False + self.cookie_secret: str | None = None + self.absolute_config_dir: Path | None = None + self.verbose: bool = False + + def parse_args(self, args: Any) -> None: + """Parse the arguments.""" + self.on_ha_addon: bool = args.ha_addon + password = args.password or os.getenv("PASSWORD") or "" + if not self.on_ha_addon: + self.username = args.username or os.getenv("USERNAME") or "" + self.using_password = bool(password) + if self.using_password: + self.password_hash = password_hash(password) + self.config_dir = args.configuration + self.absolute_config_dir = Path(self.config_dir).resolve() + self.verbose = args.verbose + CORE.config_path = os.path.join(self.config_dir, ".") + + @property + def relative_url(self) -> str: + return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL") or "/" + + @property + def status_use_ping(self): + return get_bool_env("ESPHOME_DASHBOARD_USE_PING") + + @property + def status_use_mqtt(self) -> bool: + return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT") + + @property + def using_ha_addon_auth(self) -> bool: + if not self.on_ha_addon: + return False + return not get_bool_env("DISABLE_HA_AUTHENTICATION") + + @property + def using_auth(self) -> bool: + return self.using_password or self.using_ha_addon_auth + + @property + def streamer_mode(self) -> bool: + return get_bool_env("ESPHOME_STREAMER_MODE") + + def check_password(self, username: str, password: str) -> bool: + if not self.using_auth: + return True + if username != self.username: + return False + + # Compare password in constant running time (to prevent timing attacks) + return hmac.compare_digest(self.password_hash, password_hash(password)) + + def rel_path(self, *args: Any) -> str: + """Return a path relative to the ESPHome config folder.""" + joined_path = os.path.join(self.config_dir, *args) + # Raises ValueError if not relative to ESPHome config folder + Path(joined_path).resolve().relative_to(self.absolute_config_dir) + return joined_path diff --git a/esphome/dashboard/status/__init__.py b/esphome/dashboard/status/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py new file mode 100644 index 0000000000..bd212bc563 --- /dev/null +++ b/esphome/dashboard/status/mdns.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import asyncio + +from esphome.zeroconf import ( + ESPHOME_SERVICE_TYPE, + AsyncEsphomeZeroconf, + DashboardBrowser, + DashboardImportDiscovery, + DashboardStatus, +) + +from ..const import SENTINEL +from ..core import DASHBOARD +from ..entries import DashboardEntry, bool_to_entry_state + + +class MDNSStatus: + """Class that updates the mdns status.""" + + def __init__(self) -> None: + """Initialize the MDNSStatus class.""" + super().__init__() + self.aiozc: AsyncEsphomeZeroconf | None = None + # This is the current mdns state for each host (True, False, None) + self.host_mdns_state: dict[str, bool | None] = {} + self._loop = asyncio.get_running_loop() + + async def async_resolve_host(self, host_name: str) -> str | None: + """Resolve a host name to an address in a thread-safe manner.""" + if aiozc := self.aiozc: + return await aiozc.async_resolve_host(host_name) + return None + + async def async_refresh_hosts(self): + """Refresh the hosts to track.""" + dashboard = DASHBOARD + host_mdns_state = self.host_mdns_state + entries = dashboard.entries + poll_names: dict[str, set[DashboardEntry]] = {} + for entry in entries.async_all(): + if entry.no_mdns: + continue + # If we just adopted/imported this host, we likely + # already have a state for it, so we should make sure + # to set it so the dashboard shows it as online + if entry.loaded_integrations and "api" not in entry.loaded_integrations: + # No api available so we have to poll since + # the device won't respond to a request to ._esphomelib._tcp.local. + poll_names.setdefault(entry.name, set()).add(entry) + elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: + entries.async_set_state(entry, bool_to_entry_state(online)) + + if poll_names and self.aiozc: + results = await asyncio.gather( + *(self.aiozc.async_resolve_host(name) for name in poll_names) + ) + for name, address in zip(poll_names, results): + result = bool(address) + host_mdns_state[name] = result + for entry in poll_names[name]: + entries.async_set_state(entry, bool_to_entry_state(result)) + + async def async_run(self) -> None: + dashboard = DASHBOARD + entries = dashboard.entries + aiozc = AsyncEsphomeZeroconf() + self.aiozc = aiozc + host_mdns_state = self.host_mdns_state + + def on_update(dat: dict[str, bool | None]) -> None: + """Update the entry state.""" + for name, result in dat.items(): + host_mdns_state[name] = result + if matching_entries := entries.get_by_name(name): + for entry in matching_entries: + if not entry.no_mdns: + entries.async_set_state(entry, bool_to_entry_state(result)) + + stat = DashboardStatus(on_update) + imports = DashboardImportDiscovery() + dashboard.import_result = imports.import_state + + browser = DashboardBrowser( + aiozc.zeroconf, + ESPHOME_SERVICE_TYPE, + [stat.browser_callback, imports.browser_callback], + ) + + ping_request = dashboard.ping_request + while not dashboard.stop_event.is_set(): + await self.async_refresh_hosts() + await ping_request.wait() + ping_request.clear() + + await browser.async_cancel() + await aiozc.async_close() + self.aiozc = None diff --git a/esphome/dashboard/status/mqtt.py b/esphome/dashboard/status/mqtt.py new file mode 100644 index 0000000000..8c35dd2535 --- /dev/null +++ b/esphome/dashboard/status/mqtt.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import binascii +import json +import os +import threading + +from esphome import mqtt + +from ..core import DASHBOARD +from ..entries import EntryState + + +class MqttStatusThread(threading.Thread): + """Status thread to get the status of the devices via MQTT.""" + + def run(self) -> None: + """Run the status thread.""" + dashboard = DASHBOARD + entries = dashboard.entries + current_entries = entries.all() + + config = mqtt.config_from_env() + topic = "esphome/discover/#" + + def on_message(client, userdata, msg): + nonlocal current_entries + + payload = msg.payload.decode(errors="backslashreplace") + if len(payload) > 0: + data = json.loads(payload) + if "name" not in data: + return + for entry in current_entries: + if entry.name == data["name"]: + entries.set_state(entry, EntryState.ONLINE) + return + + def on_connect(client, userdata, flags, return_code): + client.publish("esphome/discover", None, retain=False) + + mqttid = str(binascii.hexlify(os.urandom(6)).decode()) + + client = mqtt.prepare( + config, + [topic], + on_message, + on_connect, + None, + None, + f"esphome-dashboard-{mqttid}", + ) + client.loop_start() + + while not dashboard.stop_event.wait(2): + current_entries = entries.all() + # will be set to true on on_message + for entry in current_entries: + if entry.no_mdns: + entries.set_state(entry, EntryState.OFFLINE) + + client.publish("esphome/discover", None, retain=False) + dashboard.mqtt_ping_request.wait() + dashboard.mqtt_ping_request.clear() + + client.disconnect() + client.loop_stop() diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py new file mode 100644 index 0000000000..6630f03c9d --- /dev/null +++ b/esphome/dashboard/status/ping.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import asyncio +import logging +import time +from typing import cast + +from icmplib import Host, SocketPermissionError, async_ping + +from ..const import MAX_EXECUTOR_WORKERS +from ..core import DASHBOARD +from ..entries import DashboardEntry, EntryState, bool_to_entry_state +from ..util.itertools import chunked + +_LOGGER = logging.getLogger(__name__) + +GROUP_SIZE = int(MAX_EXECUTOR_WORKERS / 2) + + +class PingStatus: + def __init__(self) -> None: + """Initialize the PingStatus class.""" + super().__init__() + self._loop = asyncio.get_running_loop() + + async def async_run(self) -> None: + """Run the ping status.""" + dashboard = DASHBOARD + entries = dashboard.entries + privileged = await _can_use_icmp_lib_with_privilege() + if privileged is None: + _LOGGER.warning("Cannot use icmplib because privileges are insufficient") + return + + while not dashboard.stop_event.is_set(): + # Only ping if the dashboard is open + await dashboard.ping_request.wait() + dashboard.ping_request.clear() + current_entries = dashboard.entries.async_all() + to_ping: list[DashboardEntry] = [ + entry for entry in current_entries if entry.address is not None + ] + + # Resolve DNS for all entries + entries_with_addresses: dict[DashboardEntry, list[str]] = {} + for ping_group in chunked(to_ping, GROUP_SIZE): + ping_group = cast(list[DashboardEntry], ping_group) + now_monotonic = time.monotonic() + dns_results = await asyncio.gather( + *( + dashboard.dns_cache.async_resolve(entry.address, now_monotonic) + for entry in ping_group + ), + return_exceptions=True, + ) + + for entry, result in zip(ping_group, dns_results): + if isinstance(result, Exception): + entries.async_set_state(entry, EntryState.UNKNOWN) + continue + if isinstance(result, BaseException): + raise result + entries_with_addresses[entry] = result + + # Ping all entries with valid addresses + for ping_group in chunked(entries_with_addresses.items(), GROUP_SIZE): + entry_addresses = cast(tuple[DashboardEntry, list[str]], ping_group) + + results = await asyncio.gather( + *( + async_ping(addresses[0], privileged=privileged) + for _, addresses in entry_addresses + ), + return_exceptions=True, + ) + + for entry_addresses, result in zip(entry_addresses, results): + if isinstance(result, Exception): + ping_result = False + elif isinstance(result, BaseException): + raise result + else: + host: Host = result + ping_result = host.is_alive + entry, _ = entry_addresses + entries.async_set_state(entry, bool_to_entry_state(ping_result)) + + +async def _can_use_icmp_lib_with_privilege() -> None | bool: + """Verify we can create a raw socket.""" + try: + await async_ping("127.0.0.1", count=0, timeout=0, privileged=True) + except SocketPermissionError: + try: + await async_ping("127.0.0.1", count=0, timeout=0, privileged=False) + except SocketPermissionError: + _LOGGER.debug( + "Cannot use icmplib because privileges are insufficient to create the" + " socket" + ) + return None + + _LOGGER.debug("Using icmplib in privileged=False mode") + return False + + _LOGGER.debug("Using icmplib in privileged=True mode") + return True diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py deleted file mode 100644 index a2ad530b74..0000000000 --- a/esphome/dashboard/util.py +++ /dev/null @@ -1,32 +0,0 @@ -import hashlib -import unicodedata - -from esphome.const import ALLOWED_NAME_CHARS - - -def password_hash(password: str) -> bytes: - """Create a hash of a password to transform it to a fixed-length digest. - - Note this is not meant for secure storage, but for securely comparing passwords. - """ - return hashlib.sha256(password.encode()).digest() - - -def strip_accents(value): - return "".join( - c - for c in unicodedata.normalize("NFD", str(value)) - if unicodedata.category(c) != "Mn" - ) - - -def friendly_name_slugify(value): - value = ( - strip_accents(value) - .lower() - .replace(" ", "-") - .replace("_", "-") - .replace("--", "-") - .strip("-") - ) - return "".join(c for c in value if c in ALLOWED_NAME_CHARS) diff --git a/esphome/dashboard/util/__init__.py b/esphome/dashboard/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/dashboard/util/file.py b/esphome/dashboard/util/file.py new file mode 100644 index 0000000000..bb263f9ad7 --- /dev/null +++ b/esphome/dashboard/util/file.py @@ -0,0 +1,63 @@ +import logging +import os +from pathlib import Path +import tempfile + +_LOGGER = logging.getLogger(__name__) + + +def write_utf8_file( + filename: Path, + utf8_str: str, + private: bool = False, +) -> None: + """Write a file and rename it into place. + + Writes all or nothing. + """ + write_file(filename, utf8_str.encode("utf-8"), private) + + +# from https://github.com/home-assistant/core/blob/dev/homeassistant/util/file.py +def write_file( + filename: Path, + utf8_data: bytes, + private: bool = False, +) -> None: + """Write a file and rename it into place. + + Writes all or nothing. + """ + + tmp_filename = "" + missing_fchmod = False + try: + # Modern versions of Python tempfile create this file with mode 0o600 + with tempfile.NamedTemporaryFile( + mode="wb", dir=os.path.dirname(filename), delete=False + ) as fdesc: + fdesc.write(utf8_data) + tmp_filename = fdesc.name + if not private: + try: + os.fchmod(fdesc.fileno(), 0o644) + except AttributeError: + # os.fchmod is not available on Windows + missing_fchmod = True + + os.replace(tmp_filename, filename) + if missing_fchmod: + os.chmod(filename, 0o644) + finally: + if os.path.exists(tmp_filename): + try: + os.remove(tmp_filename) + except OSError as err: + # If we are cleaning up then something else went wrong, so + # we should suppress likely follow-on errors in the cleanup + _LOGGER.error( + "File replacement cleanup failed for %s while saving %s: %s", + tmp_filename, + filename, + err, + ) diff --git a/esphome/dashboard/util/itertools.py b/esphome/dashboard/util/itertools.py new file mode 100644 index 0000000000..54e95ef802 --- /dev/null +++ b/esphome/dashboard/util/itertools.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from collections.abc import Iterable +from functools import partial +from itertools import islice +from typing import Any + + +def take(take_num: int, iterable: Iterable) -> list[Any]: + """Return first n items of the iterable as a list. + + From itertools recipes + """ + return list(islice(iterable, take_num)) + + +def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]: + """Break *iterable* into lists of length *n*. + + From more-itertools + """ + return iter(partial(take, chunked_num, iter(iterable)), []) diff --git a/esphome/dashboard/util/password.py b/esphome/dashboard/util/password.py new file mode 100644 index 0000000000..e7ea28c25d --- /dev/null +++ b/esphome/dashboard/util/password.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import hashlib + + +def password_hash(password: str) -> bytes: + """Create a hash of a password to transform it to a fixed-length digest. + + Note this is not meant for secure storage, but for securely comparing passwords. + """ + return hashlib.sha256(password.encode()).digest() diff --git a/esphome/dashboard/util/subprocess.py b/esphome/dashboard/util/subprocess.py new file mode 100644 index 0000000000..583dd116e3 --- /dev/null +++ b/esphome/dashboard/util/subprocess.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Iterable + + +async def async_system_command_status(command: Iterable[str]) -> bool: + """Run a system command checking only the status.""" + process = await asyncio.create_subprocess_exec( + *command, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + close_fds=False, + ) + await process.wait() + return process.returncode == 0 + + +async def async_run_system_command(command: Iterable[str]) -> tuple[bool, bytes, bytes]: + """Run a system command and return a tuple of returncode, stdout, stderr.""" + process = await asyncio.create_subprocess_exec( + *command, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + close_fds=False, + ) + stdout, stderr = await process.communicate() + await process.wait() + return process.returncode, stdout, stderr diff --git a/esphome/dashboard/util/text.py b/esphome/dashboard/util/text.py new file mode 100644 index 0000000000..08d2df6abf --- /dev/null +++ b/esphome/dashboard/util/text.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import unicodedata + +from esphome.const import ALLOWED_NAME_CHARS + + +def strip_accents(value): + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) + + +def friendly_name_slugify(value): + value = ( + strip_accents(value) + .lower() + .replace(" ", "-") + .replace("_", "-") + .replace("--", "-") + .strip("-") + ) + return "".join(c for c in value if c in ALLOWED_NAME_CHARS) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py new file mode 100644 index 0000000000..9aeece9aab --- /dev/null +++ b/esphome/dashboard/web_server.py @@ -0,0 +1,1224 @@ +from __future__ import annotations + +import asyncio +import base64 +from collections.abc import Iterable +import datetime +import functools +import gzip +import hashlib +import importlib +import json +import logging +import os +from pathlib import Path +import secrets +import shutil +import subprocess +import threading +import time +from typing import TYPE_CHECKING, Any, Callable, TypeVar +from urllib.parse import urlparse + +import tornado +import tornado.concurrent +import tornado.gen +import tornado.httpserver +import tornado.httputil +import tornado.ioloop +import tornado.iostream +from tornado.log import access_log +import tornado.netutil +import tornado.process +import tornado.queues +import tornado.web +import tornado.websocket +import yaml +from yaml.nodes import Node + +from esphome import const, platformio_api, yaml_util +from esphome.helpers import get_bool_env, mkdir_p +from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_path +from esphome.util import get_serial_ports, shlex_quote +from esphome.yaml_util import FastestAvailableSafeLoader + +from .const import DASHBOARD_COMMAND +from .core import DASHBOARD +from .entries import EntryState, entry_state_to_bool +from .util.file import write_file +from .util.subprocess import async_run_system_command +from .util.text import friendly_name_slugify + +if TYPE_CHECKING: + from requests import Response + + +_LOGGER = logging.getLogger(__name__) + +ENV_DEV = "ESPHOME_DASHBOARD_DEV" + +COOKIE_AUTHENTICATED_YES = b"yes" + +AUTH_COOKIE_NAME = "authenticated" + + +settings = DASHBOARD.settings + + +def template_args() -> dict[str, Any]: + version = const.__version__ + if "b" in version: + docs_link = "https://beta.esphome.io/" + elif "dev" in version: + docs_link = "https://next.esphome.io/" + else: + docs_link = "https://www.esphome.io/" + + return { + "version": version, + "docs_link": docs_link, + "get_static_file_url": get_static_file_url, + "relative_url": settings.relative_url, + "streamer_mode": settings.streamer_mode, + "config_dir": settings.config_dir, + } + + +T = TypeVar("T", bound=Callable[..., Any]) + + +def authenticated(func: T) -> T: + @functools.wraps(func) + def decorator(self, *args: Any, **kwargs: Any): + if not is_authenticated(self): + self.redirect("./login") + return None + return func(self, *args, **kwargs) + + return decorator + + +def is_authenticated(handler: BaseHandler) -> bool: + """Check if the request is authenticated.""" + if settings.on_ha_addon: + # Handle ingress - disable auth on ingress port + # X-HA-Ingress is automatically stripped on the non-ingress server in nginx + header = handler.request.headers.get("X-HA-Ingress", "NO") + if str(header) == "YES": + return True + + if settings.using_auth: + return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES + + return True + + +def bind_config(func): + def decorator(self, *args, **kwargs): + configuration = self.get_argument("configuration") + kwargs = kwargs.copy() + kwargs["configuration"] = configuration + return func(self, *args, **kwargs) + + return decorator + + +# pylint: disable=abstract-method +class BaseHandler(tornado.web.RequestHandler): + pass + + +def websocket_class(cls): + # pylint: disable=protected-access + if not hasattr(cls, "_message_handlers"): + cls._message_handlers = {} + + for _, method in cls.__dict__.items(): + if hasattr(method, "_message_handler"): + cls._message_handlers[method._message_handler] = method + + return cls + + +def websocket_method(name): + def wrap(fn): + # pylint: disable=protected-access + fn._message_handler = name + return fn + + return wrap + + +@websocket_class +class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): + """Base class for ESPHome websocket commands.""" + + def __init__( + self, + application: tornado.web.Application, + request: tornado.httputil.HTTPServerRequest, + **kwargs: Any, + ) -> None: + """Initialize the websocket.""" + super().__init__(application, request, **kwargs) + self._proc = None + self._queue = None + self._is_closed = False + # Windows doesn't support non-blocking pipes, + # 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 + # to avoid a 200-500ms delay when nodelay is not set. + self.set_nodelay(True) + + @authenticated + async def on_message( # pylint: disable=invalid-overridden-method + self, message: str + ) -> None: + # Since tornado 4.5, on_message is allowed to be a coroutine + # Messages are always JSON, 500 when not + json_message = json.loads(message) + type_ = json_message["type"] + # pylint: disable=no-member + handlers = type(self)._message_handlers + if type_ not in handlers: + _LOGGER.warning("Requested unknown message type %s", type_) + return + + await handlers[type_](self, json_message) + + @websocket_method("spawn") + async def handle_spawn(self, json_message: dict[str, Any]) -> None: + if self._proc is not None: + # spawn can only be called once + return + command = await self.build_command(json_message) + _LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command)) + + if self._use_popen: + self._queue = tornado.queues.Queue() + # pylint: disable=consider-using-with + self._proc = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout_thread = threading.Thread(target=self._stdout_thread) + stdout_thread.daemon = True + stdout_thread.start() + else: + self._proc = tornado.process.Subprocess( + command, + stdout=tornado.process.Subprocess.STREAM, + stderr=subprocess.STDOUT, + stdin=tornado.process.Subprocess.STREAM, + close_fds=False, + ) + self._proc.set_exit_callback(self._proc_on_exit) + + tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout) + + @property + def is_process_active(self) -> bool: + return self._proc is not None and self._proc.returncode is None + + @websocket_method("stdin") + async def handle_stdin(self, json_message: dict[str, Any]) -> None: + if not self.is_process_active: + return + text: str = json_message["data"] + data = text.encode("utf-8", "replace") + _LOGGER.debug("< stdin: %s", data) + self._proc.stdin.write(data) + + @tornado.gen.coroutine + def _redirect_stdout(self) -> None: + reg = b"[\n\r]" + + while True: + try: + if self._use_popen: + data: bytes = yield self._queue.get() + if data is None: + self._proc_on_exit(self._proc.poll()) + break + else: + data: bytes = yield self._proc.stdout.read_until_regex(reg) + except tornado.iostream.StreamClosedError: + break + + text = data.decode("utf-8", "replace") + _LOGGER.debug("> stdout: %s", text) + self.write_message({"event": "line", "data": text}) + + def _stdout_thread(self) -> None: + if not self._use_popen: + return + while True: + data = self._proc.stdout.readline() + if data: + data = data.replace(b"\r", b"") + self._queue.put_nowait(data) + if self._proc.poll() is not None: + break + self._proc.wait(1.0) + self._queue.put_nowait(None) + + def _proc_on_exit(self, returncode: int) -> None: + if not self._is_closed: + # Check if the proc was not forcibly closed + _LOGGER.info("Process exited with return code %s", returncode) + self.write_message({"event": "exit", "code": returncode}) + + def on_close(self) -> None: + # Check if proc exists (if 'start' has been run) + if self.is_process_active: + _LOGGER.debug("Terminating process") + if self._use_popen: + self._proc.terminate() + else: + self._proc.proc.terminate() + # Shutdown proc on WS close + self._is_closed = True + + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + raise NotImplementedError + + +class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): + """Base class for commands that require a port.""" + + async def build_device_command( + self, args: list[str], json_message: dict[str, Any] + ) -> list[str]: + """Build the command to run.""" + dashboard = DASHBOARD + entries = dashboard.entries + configuration = json_message["configuration"] + config_file = settings.rel_path(configuration) + port = json_message["port"] + if ( + port == "OTA" # pylint: disable=too-many-boolean-expressions + and (entry := entries.get(config_file)) + and entry.loaded_integrations + and "api" in entry.loaded_integrations + ): + if (mdns := dashboard.mdns_status) and ( + address := await mdns.async_resolve_host(entry.name) + ): + # Use the IP address if available but only + # if the API is loaded and the device is online + # since MQTT logging will not work otherwise + port = address + elif ( + entry.address + and ( + address_list := await dashboard.dns_cache.async_resolve( + entry.address, time.monotonic() + ) + ) + and not isinstance(address_list, Exception) + ): + # If mdns is not available, try to use the DNS cache + port = address_list[0] + + return [ + *DASHBOARD_COMMAND, + *args, + config_file, + "--device", + port, + ] + + +class EsphomeLogsHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["logs"], json_message) + + +class EsphomeRenameHandler(EsphomeCommandWebSocket): + old_name: str + + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + self.old_name = json_message["configuration"] + return [ + *DASHBOARD_COMMAND, + "rename", + config_file, + json_message["newName"], + ] + + def _proc_on_exit(self, returncode): + super()._proc_on_exit(returncode) + + if returncode != 0: + return + + # Remove the old ping result from the cache + entries = DASHBOARD.entries + if entry := entries.get(self.old_name): + entries.async_set_state(entry, EntryState.UNKNOWN) + + +class EsphomeUploadHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["upload"], json_message) + + +class EsphomeRunHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["run"], json_message) + + +class EsphomeCompileHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + command = [*DASHBOARD_COMMAND, "compile"] + if json_message.get("only_generate", False): + command.append("--only-generate") + command.append(config_file) + return command + + +class EsphomeValidateHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + command = [*DASHBOARD_COMMAND, "config", config_file] + if not settings.streamer_mode: + command.append("--show-secrets") + return command + + +class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] + + +class EsphomeCleanHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + return [*DASHBOARD_COMMAND, "clean", config_file] + + +class EsphomeVscodeHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "-q", "vscode", "dummy"] + + +class EsphomeAceEditorHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "-q", "vscode", "--ace", settings.config_dir] + + +class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "update-all", settings.config_dir] + + +class SerialPortRequestHandler(BaseHandler): + @authenticated + async def get(self) -> None: + ports = await asyncio.get_running_loop().run_in_executor(None, get_serial_ports) + data = [] + for port in ports: + desc = port.description + if port.path == "/dev/ttyAMA0": + desc = "UART pins on GPIO header" + split_desc = desc.split(" - ") + if len(split_desc) == 2 and split_desc[0] == split_desc[1]: + # Some serial ports repeat their values + desc = split_desc[0] + data.append({"port": port.path, "desc": desc}) + data.append({"port": "OTA", "desc": "Over-The-Air"}) + data.sort(key=lambda x: x["port"], reverse=True) + self.set_header("content-type", "application/json") + self.write(json.dumps(data)) + + +class WizardRequestHandler(BaseHandler): + @authenticated + def post(self) -> None: + from esphome import wizard + + kwargs = { + k: v + for k, v in json.loads(self.request.body.decode()).items() + if k in ("name", "platform", "board", "ssid", "psk", "password") + } + if not kwargs["name"]: + self.set_status(422) + self.set_header("content-type", "application/json") + self.write(json.dumps({"error": "Name is required"})) + return + + kwargs["friendly_name"] = kwargs["name"] + kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) + + kwargs["ota_password"] = secrets.token_hex(16) + noise_psk = secrets.token_bytes(32) + kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() + filename = f"{kwargs['name']}.yaml" + destination = settings.rel_path(filename) + wizard.wizard_write(path=destination, **kwargs) + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": filename})) + self.finish() + + +class ImportRequestHandler(BaseHandler): + @authenticated + def post(self) -> None: + from esphome.components.dashboard_import import import_config + + dashboard = DASHBOARD + args = json.loads(self.request.body.decode()) + try: + name = args["name"] + friendly_name = args.get("friendly_name") + encryption = args.get("encryption", False) + + imported_device = next( + ( + res + for res in dashboard.import_result.values() + if res.device_name == name + ), + None, + ) + + if imported_device is not None: + network = imported_device.network + if friendly_name is None: + friendly_name = imported_device.friendly_name + else: + network = const.CONF_WIFI + + import_config( + settings.rel_path(f"{name}.yaml"), + name, + friendly_name, + args["project_name"], + args["package_import_url"], + network, + encryption, + ) + # Make sure the device gets marked online right away + dashboard.ping_request.set() + except FileExistsError: + self.set_status(500) + self.write("File already exists") + return + except ValueError as e: + _LOGGER.error(e) + self.set_status(422) + self.write("Invalid package url") + return + + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": f"{name}.yaml"})) + self.finish() + + +class IgnoreDeviceRequestHandler(BaseHandler): + @authenticated + def post(self) -> None: + dashboard = DASHBOARD + try: + args = json.loads(self.request.body.decode()) + device_name = args["name"] + ignore = args["ignore"] + except (json.JSONDecodeError, KeyError): + self.set_status(400) + self.set_header("content-type", "application/json") + self.write(json.dumps({"error": "Invalid payload"})) + return + + ignored_device = next( + ( + res + for res in dashboard.import_result.values() + if res.device_name == device_name + ), + None, + ) + + if ignored_device is None: + self.set_status(404) + self.set_header("content-type", "application/json") + self.write(json.dumps({"error": "Device not found"})) + return + + if ignore: + dashboard.ignored_devices.add(ignored_device.device_name) + else: + dashboard.ignored_devices.discard(ignored_device.device_name) + + dashboard.save_ignored_devices() + + self.set_status(204) + self.finish() + + +class DownloadListRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration: str | None = None) -> None: + storage_path = ext_storage_path(configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + + from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS + + downloads = [] + platform: str = storage_json.target_platform.lower() + + if platform.upper() in ESP32_VARIANTS: + platform = "esp32" + elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX): + platform = "libretiny" + + try: + module = importlib.import_module(f"esphome.components.{platform}") + get_download_types = getattr(module, "get_download_types") + except AttributeError as exc: + raise ValueError(f"Unknown platform {platform}") from exc + downloads = get_download_types(storage_json) + + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps(downloads)) + self.finish() + return + + +class DownloadBinaryRequestHandler(BaseHandler): + def _load_file(self, path: str, compressed: bool) -> bytes: + """Load a file from disk and compress it if requested.""" + with open(path, "rb") as f: + data = f.read() + if compressed: + return gzip.compress(data, 9) + return data + + @authenticated + @bind_config + async def get(self, configuration: str | None = None) -> None: + """Download a binary file.""" + loop = asyncio.get_running_loop() + compressed = self.get_argument("compressed", "0") == "1" + + storage_path = ext_storage_path(configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + + # fallback to type=, but prioritize file= + file_name = self.get_argument("type", None) + file_name = self.get_argument("file", file_name) + if file_name is None: + self.send_error(400) + return + file_name = file_name.replace("..", "").lstrip("/") + # get requested download name, or build it based on filename + download_name = self.get_argument( + "download", + f"{storage_json.name}-{file_name}", + ) + path = os.path.dirname(storage_json.firmware_bin_path) + path = os.path.join(path, file_name) + + if not Path(path).is_file(): + args = ["esphome", "idedata", settings.rel_path(configuration)] + rc, stdout, _ = await async_run_system_command(args) + + if rc != 0: + self.send_error(404 if rc == 2 else 500) + return + + idedata = platformio_api.IDEData(json.loads(stdout)) + + found = False + for image in idedata.extra_flash_images: + if image.path.endswith(file_name): + path = image.path + download_name = file_name + found = True + break + + if not found: + self.send_error(404) + return + + download_name = download_name + ".gz" if compressed else download_name + + self.set_header("Content-Type", "application/octet-stream") + self.set_header( + "Content-Disposition", f'attachment; filename="{download_name}"' + ) + self.set_header("Cache-Control", "no-cache") + if not Path(path).is_file(): + self.send_error(404) + return + + data = await loop.run_in_executor(None, self._load_file, path, compressed) + self.write(data) + + self.finish() + + +class EsphomeVersionHandler(BaseHandler): + @authenticated + def get(self) -> None: + self.set_header("Content-Type", "application/json") + self.write(json.dumps({"version": const.__version__})) + self.finish() + + +class ListDevicesHandler(BaseHandler): + @authenticated + async def get(self) -> None: + dashboard = DASHBOARD + await dashboard.entries.async_request_update_entries() + entries = dashboard.entries.async_all() + self.set_header("content-type", "application/json") + configured = {entry.name for entry in entries} + + self.write( + json.dumps( + { + "configured": [entry.to_dict() for entry in entries], + "importable": [ + { + "name": res.device_name, + "friendly_name": res.friendly_name, + "package_import_url": res.package_import_url, + "project_name": res.project_name, + "project_version": res.project_version, + "network": res.network, + "ignored": res.device_name in dashboard.ignored_devices, + } + for res in dashboard.import_result.values() + if res.device_name not in configured + ], + } + ) + ) + + +class MainRequestHandler(BaseHandler): + @authenticated + def get(self) -> None: + begin = bool(self.get_argument("begin", False)) + if settings.using_password: + # Simply accessing the xsrf_token sets the cookie for us + self.xsrf_token # pylint: disable=pointless-statement + else: + self.clear_cookie("_xsrf") + + self.render( + "index.template.html", + begin=begin, + **template_args(), + login_enabled=settings.using_password, + ) + + +class PrometheusServiceDiscoveryHandler(BaseHandler): + @authenticated + async def get(self) -> None: + dashboard = DASHBOARD + await dashboard.entries.async_request_update_entries() + entries = dashboard.entries.async_all() + self.set_header("content-type", "application/json") + sd = [] + for entry in entries: + if entry.web_port is None: + continue + labels = { + "__meta_name": entry.name, + "__meta_esp_platform": entry.target_platform, + "__meta_esphome_version": entry.storage.esphome_version, + } + for integration in entry.storage.loaded_integrations: + labels[f"__meta_integration_{integration}"] = "true" + sd.append( + { + "targets": [ + f"{entry.address}:{entry.web_port}", + ], + "labels": labels, + } + ) + self.write(json.dumps(sd)) + + +class BoardsRequestHandler(BaseHandler): + @authenticated + def get(self, platform: str) -> None: + # filter all ESP32 variants by requested platform + if platform.startswith("esp32"): + from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS + + boards = { + k: v + for k, v in ESP32_BOARDS.items() + if v[const.KEY_VARIANT] == platform.upper() + } + elif platform == const.PLATFORM_ESP8266: + from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS + + boards = ESP8266_BOARDS + elif platform == const.PLATFORM_RP2040: + from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS + + boards = RP2040_BOARDS + elif platform == const.PLATFORM_BK72XX: + from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS + + boards = BK72XX_BOARDS + elif platform == const.PLATFORM_RTL87XX: + from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS + + boards = RTL87XX_BOARDS + else: + raise ValueError(f"Unknown platform {platform}") + + # map to a {board_name: board_title} dict + platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()} + # sort by board title + boards_items = sorted(platform_boards.items(), key=lambda item: item[1]) + output = [{"items": dict(boards_items)}] + + self.set_header("content-type", "application/json") + self.write(json.dumps(output)) + + +class PingRequestHandler(BaseHandler): + @authenticated + def get(self) -> None: + dashboard = DASHBOARD + dashboard.ping_request.set() + if settings.status_use_mqtt: + dashboard.mqtt_ping_request.set() + self.set_header("content-type", "application/json") + + self.write( + json.dumps( + { + entry.filename: entry_state_to_bool(entry.state) + for entry in dashboard.entries.async_all() + } + ) + ) + + +class InfoRequestHandler(BaseHandler): + @authenticated + @bind_config + async def get(self, configuration: str | None = None) -> None: + yaml_path = settings.rel_path(configuration) + dashboard = DASHBOARD + entry = dashboard.entries.get(yaml_path) + + if not entry: + self.set_status(404) + return + + self.set_header("content-type", "application/json") + self.write(entry.storage.to_json()) + + +class EditRequestHandler(BaseHandler): + @authenticated + @bind_config + async def get(self, configuration: str | None = None) -> None: + """Get the content of a file.""" + if not configuration.endswith((".yaml", ".yml")): + self.send_error(404) + return + + filename = settings.rel_path(configuration) + if Path(filename).resolve().parent != settings.absolute_config_dir: + self.send_error(404) + return + + loop = asyncio.get_running_loop() + content = await loop.run_in_executor( + None, self._read_file, filename, configuration + ) + if content is not None: + self.set_header("Content-Type", "application/yaml") + self.write(content) + + def _read_file(self, filename: str, configuration: str) -> bytes | None: + """Read a file and return the content as bytes.""" + try: + with open(file=filename, encoding="utf-8") as f: + return f.read() + except FileNotFoundError: + if configuration in const.SECRETS_FILES: + return "" + self.set_status(404) + return None + + def _write_file(self, filename: str, content: bytes) -> None: + """Write a file with the given content.""" + write_file(filename, content) + + @authenticated + @bind_config + async def post(self, configuration: str | None = None) -> None: + """Write the content of a file.""" + if not configuration.endswith((".yaml", ".yml")): + self.send_error(404) + return + + filename = settings.rel_path(configuration) + if Path(filename).resolve().parent != settings.absolute_config_dir: + self.send_error(404) + return + + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, self._write_file, filename, self.request.body) + # Ensure the StorageJSON is updated as well + DASHBOARD.entries.async_schedule_storage_json_update(filename) + self.set_status(200) + + +class DeleteRequestHandler(BaseHandler): + @authenticated + @bind_config + def post(self, configuration: str | None = None) -> None: + config_file = settings.rel_path(configuration) + storage_path = ext_storage_path(configuration) + + trash_path = trash_storage_path() + mkdir_p(trash_path) + shutil.move(config_file, os.path.join(trash_path, configuration)) + + storage_json = StorageJSON.load(storage_path) + if storage_json is not None: + # Delete build folder (if exists) + name = storage_json.name + build_folder = os.path.join(settings.config_dir, name) + if build_folder is not None: + shutil.rmtree(build_folder, os.path.join(trash_path, name)) + + +class UndoDeleteRequestHandler(BaseHandler): + @authenticated + @bind_config + def post(self, configuration: str | None = None) -> None: + config_file = settings.rel_path(configuration) + trash_path = trash_storage_path() + shutil.move(os.path.join(trash_path, configuration), config_file) + + +class LoginHandler(BaseHandler): + def get(self) -> None: + if is_authenticated(self): + self.redirect("./") + else: + self.render_login_page() + + def render_login_page(self, error: str | None = None) -> None: + self.render( + "login.template.html", + error=error, + ha_addon=settings.using_ha_addon_auth, + has_username=bool(settings.username), + **template_args(), + ) + + def _make_supervisor_auth_request(self) -> Response: + """Make a request to the supervisor auth endpoint.""" + import requests + + headers = {"X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN")} + data = { + "username": self.get_argument("username", ""), + "password": self.get_argument("password", ""), + } + return requests.post( + "http://supervisor/auth", headers=headers, json=data, timeout=30 + ) + + async def post_ha_addon_login(self) -> None: + loop = asyncio.get_running_loop() + try: + req = await loop.run_in_executor(None, self._make_supervisor_auth_request) + except Exception as err: # pylint: disable=broad-except + _LOGGER.warning("Error during Hass.io auth request: %s", err) + self.set_status(500) + self.render_login_page(error="Internal server error") + return + + if req.status_code == 200: + self._set_authenticated() + self.redirect("/") + return + self.set_status(401) + self.render_login_page(error="Invalid username or password") + + def _set_authenticated(self) -> None: + """Set the authenticated cookie.""" + self.set_secure_cookie(AUTH_COOKIE_NAME, COOKIE_AUTHENTICATED_YES) + + def post_native_login(self) -> None: + username = self.get_argument("username", "") + password = self.get_argument("password", "") + if settings.check_password(username, password): + self._set_authenticated() + self.redirect("./") + return + error_str = ( + "Invalid username or password" if settings.username else "Invalid password" + ) + self.set_status(401) + self.render_login_page(error=error_str) + + async def post(self): + if settings.using_ha_addon_auth: + await self.post_ha_addon_login() + else: + self.post_native_login() + + +class LogoutHandler(BaseHandler): + @authenticated + def get(self) -> None: + self.clear_cookie(AUTH_COOKIE_NAME) + self.redirect("./login") + + +class SecretKeysRequestHandler(BaseHandler): + @authenticated + def get(self) -> None: + filename = None + + for secret_filename in const.SECRETS_FILES: + relative_filename = settings.rel_path(secret_filename) + if os.path.isfile(relative_filename): + filename = relative_filename + break + + if filename is None: + self.send_error(404) + return + + secret_keys = list(yaml_util.load_yaml(filename, clear_secrets=False)) + + self.set_header("content-type", "application/json") + self.write(json.dumps(secret_keys)) + + +class SafeLoaderIgnoreUnknown(FastestAvailableSafeLoader): + def ignore_unknown(self, node: Node) -> str: + return f"{node.tag} {node.value}" + + def construct_yaml_binary(self, node: Node) -> str: + return super().construct_yaml_binary(node).decode("ascii") + + +SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown) +SafeLoaderIgnoreUnknown.add_constructor( + "tag:yaml.org,2002:binary", SafeLoaderIgnoreUnknown.construct_yaml_binary +) + + +class JsonConfigRequestHandler(BaseHandler): + @authenticated + @bind_config + async def get(self, configuration: str | None = None) -> None: + filename = settings.rel_path(configuration) + if not os.path.isfile(filename): + self.send_error(404) + return + + args = ["esphome", "config", filename, "--show-secrets"] + + rc, stdout, _ = await async_run_system_command(args) + + if rc != 0: + self.send_error(422) + return + + data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown) + self.set_header("content-type", "application/json") + self.write(json.dumps(data)) + self.finish() + + +def get_base_frontend_path() -> str: + if ENV_DEV not in os.environ: + import esphome_dashboard + + return esphome_dashboard.where() + + static_path = os.environ[ENV_DEV] + if not static_path.endswith("/"): + static_path += "/" + + # This path can be relative, so resolve against the root or else templates don't work + return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) + + +def get_static_path(*args: Iterable[str]) -> str: + return os.path.join(get_base_frontend_path(), "static", *args) + + +@functools.cache +def get_static_file_url(name: str) -> str: + base = f"./static/{name}" + + if ENV_DEV in os.environ: + return base + + # Module imports can't deduplicate if stuff added to url + if name == "js/esphome/index.js": + import esphome_dashboard + + return base.replace("index.js", esphome_dashboard.entrypoint()) + + path = get_static_path(name) + with open(path, "rb") as f_handle: + hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] + return f"{base}?hash={hash_}" + + +def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application: + def log_function(handler: tornado.web.RequestHandler) -> None: + if handler.get_status() < 400: + log_method = access_log.info + + if isinstance(handler, SerialPortRequestHandler) and not debug: + return + if isinstance(handler, PingRequestHandler) and not debug: + return + elif handler.get_status() < 500: + log_method = access_log.warning + else: + log_method = access_log.error + + request_time = 1000.0 * handler.request.request_time() + # pylint: disable=protected-access + log_method( + "%d %s %.2fms", + handler.get_status(), + handler._request_summary(), + request_time, + ) + + class StaticFileHandler(tornado.web.StaticFileHandler): + def get_cache_time( + self, path: str, modified: datetime.datetime | None, mime_type: str + ) -> int: + """Override to customize cache control behavior.""" + if debug: + return 0 + # Assets that are hashed have ?hash= in the URL, all javascript + # filenames hashed so we can cache them for a long time + if "hash" in self.request.arguments or "/javascript" in mime_type: + return self.CACHE_MAX_AGE + return super().get_cache_time(path, modified, mime_type) + + app_settings = { + "debug": debug, + "cookie_secret": settings.cookie_secret, + "log_function": log_function, + "websocket_ping_interval": 30.0, + "template_path": get_base_frontend_path(), + "xsrf_cookies": settings.using_password, + } + rel = settings.relative_url + return tornado.web.Application( + [ + (f"{rel}", MainRequestHandler), + (f"{rel}login", LoginHandler), + (f"{rel}logout", LogoutHandler), + (f"{rel}logs", EsphomeLogsHandler), + (f"{rel}upload", EsphomeUploadHandler), + (f"{rel}run", EsphomeRunHandler), + (f"{rel}compile", EsphomeCompileHandler), + (f"{rel}validate", EsphomeValidateHandler), + (f"{rel}clean-mqtt", EsphomeCleanMqttHandler), + (f"{rel}clean", EsphomeCleanHandler), + (f"{rel}vscode", EsphomeVscodeHandler), + (f"{rel}ace", EsphomeAceEditorHandler), + (f"{rel}update-all", EsphomeUpdateAllHandler), + (f"{rel}info", InfoRequestHandler), + (f"{rel}edit", EditRequestHandler), + (f"{rel}downloads", DownloadListRequestHandler), + (f"{rel}download.bin", DownloadBinaryRequestHandler), + (f"{rel}serial-ports", SerialPortRequestHandler), + (f"{rel}ping", PingRequestHandler), + (f"{rel}delete", DeleteRequestHandler), + (f"{rel}undo-delete", UndoDeleteRequestHandler), + (f"{rel}wizard", WizardRequestHandler), + (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}), + (f"{rel}devices", ListDevicesHandler), + (f"{rel}import", ImportRequestHandler), + (f"{rel}secret_keys", SecretKeysRequestHandler), + (f"{rel}json-config", JsonConfigRequestHandler), + (f"{rel}rename", EsphomeRenameHandler), + (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), + (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler), + (f"{rel}version", EsphomeVersionHandler), + (f"{rel}ignore-device", IgnoreDeviceRequestHandler), + ], + **app_settings, + ) + + +def start_web_server( + app: tornado.web.Application, + socket: str | None, + address: str | None, + port: int | None, + config_dir: str, +) -> None: + """Start the web server listener.""" + if socket is None: + _LOGGER.info( + "Starting dashboard web server on http://%s:%s and configuration dir %s...", + address, + port, + config_dir, + ) + app.listen(port, address) + return + + _LOGGER.info( + "Starting dashboard web server on unix socket %s and configuration dir %s...", + socket, + config_dir, + ) + server = tornado.httpserver.HTTPServer(app) + socket = tornado.netutil.bind_unix_socket(socket, mode=0o666) + server.add_socket(socket) diff --git a/esphome/espota2.py b/esphome/espota2.py index 98d6d3a0d9..580536153a 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -1,45 +1,54 @@ +from __future__ import annotations + +import gzip import hashlib +import io import logging import random import socket import sys import time -import gzip from esphome.core import EsphomeError from esphome.helpers import is_ip_address, resolve_ip_address -RESPONSE_OK = 0 -RESPONSE_REQUEST_AUTH = 1 +RESPONSE_OK = 0x00 +RESPONSE_REQUEST_AUTH = 0x01 -RESPONSE_HEADER_OK = 64 -RESPONSE_AUTH_OK = 65 -RESPONSE_UPDATE_PREPARE_OK = 66 -RESPONSE_BIN_MD5_OK = 67 -RESPONSE_RECEIVE_OK = 68 -RESPONSE_UPDATE_END_OK = 69 -RESPONSE_SUPPORTS_COMPRESSION = 70 +RESPONSE_HEADER_OK = 0x40 +RESPONSE_AUTH_OK = 0x41 +RESPONSE_UPDATE_PREPARE_OK = 0x42 +RESPONSE_BIN_MD5_OK = 0x43 +RESPONSE_RECEIVE_OK = 0x44 +RESPONSE_UPDATE_END_OK = 0x45 +RESPONSE_SUPPORTS_COMPRESSION = 0x46 +RESPONSE_CHUNK_OK = 0x47 -RESPONSE_ERROR_MAGIC = 128 -RESPONSE_ERROR_UPDATE_PREPARE = 129 -RESPONSE_ERROR_AUTH_INVALID = 130 -RESPONSE_ERROR_WRITING_FLASH = 131 -RESPONSE_ERROR_UPDATE_END = 132 -RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 133 -RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 134 -RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 135 -RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 136 -RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137 -RESPONSE_ERROR_NO_UPDATE_PARTITION = 138 -RESPONSE_ERROR_MD5_MISMATCH = 139 -RESPONSE_ERROR_UNKNOWN = 255 +RESPONSE_ERROR_MAGIC = 0x80 +RESPONSE_ERROR_UPDATE_PREPARE = 0x81 +RESPONSE_ERROR_AUTH_INVALID = 0x82 +RESPONSE_ERROR_WRITING_FLASH = 0x83 +RESPONSE_ERROR_UPDATE_END = 0x84 +RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 0x85 +RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 0x86 +RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 0x87 +RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88 +RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89 +RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A +RESPONSE_ERROR_MD5_MISMATCH = 0x8B +RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 +OTA_VERSION_2_0 = 2 MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] FEATURE_SUPPORTS_COMPRESSION = 0x01 + +UPLOAD_BLOCK_SIZE = 8192 +UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8 + _LOGGER = logging.getLogger(__name__) @@ -184,7 +193,9 @@ def send_check(sock, data, msg): raise OTAError(f"Error sending {msg}: {err}") from err -def perform_ota(sock, password, file_handle, filename): +def perform_ota( + sock: socket.socket, password: str, file_handle: io.IOBase, filename: str +) -> None: file_contents = file_handle.read() file_size = len(file_contents) _LOGGER.info("Uploading %s (%s bytes)", filename, file_size) @@ -194,8 +205,12 @@ def perform_ota(sock, password, file_handle, filename): send_check(sock, MAGIC_BYTES, "magic bytes") _, version = receive_exactly(sock, 2, "version", RESPONSE_OK) - if version != OTA_VERSION_1_0: - raise OTAError(f"Unsupported OTA version {version}") + _LOGGER.debug("Device support OTA version: %s", version) + supported_versions = (OTA_VERSION_1_0, OTA_VERSION_2_0) + if version not in supported_versions: + raise OTAError( + f"Device uses unsupported OTA version {version}, this ESPHome supports {supported_versions}" + ) # Features send_check(sock, FEATURE_SUPPORTS_COMPRESSION, "features") @@ -254,20 +269,24 @@ def perform_ota(sock, password, file_handle, filename): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0) # Limit send buffer (usually around 100kB) in order to have progress bar # show the actual progress - sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192) + + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, UPLOAD_BUFFER_SIZE) # Set higher timeout during upload - sock.settimeout(20.0) + sock.settimeout(30.0) + start_time = time.perf_counter() offset = 0 progress = ProgressBar() while True: - chunk = upload_contents[offset : offset + 1024] + chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE] if not chunk: break offset += len(chunk) try: sock.sendall(chunk) + if version >= OTA_VERSION_2_0: + receive_exactly(sock, 1, "chunk OK", RESPONSE_CHUNK_OK) except OSError as err: sys.stderr.write("\n") raise OTAError(f"Error sending data: {err}") from err @@ -277,8 +296,9 @@ def perform_ota(sock, password, file_handle, filename): # Enable nodelay for last checks sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + duration = time.perf_counter() - start_time - _LOGGER.info("Waiting for result...") + _LOGGER.info("Upload took %.2f seconds, waiting for result...", duration) receive_exactly(sock, 1, "receive OK", RESPONSE_RECEIVE_OK) receive_exactly(sock, 1, "Update end", RESPONSE_UPDATE_END_OK) diff --git a/esphome/external_files.py b/esphome/external_files.py new file mode 100644 index 0000000000..057ff52f3f --- /dev/null +++ b/esphome/external_files.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from datetime import datetime +import logging +import os +from pathlib import Path + +import requests + +import esphome.config_validation as cv +from esphome.const import __version__ +from esphome.core import CORE, TimePeriodSeconds + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@landonr"] + +NETWORK_TIMEOUT = 30 + +IF_MODIFIED_SINCE = "If-Modified-Since" +CACHE_CONTROL = "Cache-Control" +CACHE_CONTROL_MAX_AGE = "max-age=" +CONTENT_DISPOSITION = "content-disposition" +TEMP_DIR = "temp" + + +def has_remote_file_changed(url, local_file_path): + if os.path.exists(local_file_path): + _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) + try: + local_modification_time = os.path.getmtime(local_file_path) + local_modification_time_str = datetime.utcfromtimestamp( + local_modification_time + ).strftime("%a, %d %b %Y %H:%M:%S GMT") + + headers = { + IF_MODIFIED_SINCE: local_modification_time_str, + CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600", + } + response = requests.head( + url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True + ) + + _LOGGER.debug( + "has_remote_file_changed: File %s, Local modified %s, response code %d", + local_file_path, + local_modification_time_str, + response.status_code, + ) + + if response.status_code == 304: + _LOGGER.debug( + "has_remote_file_changed: File not modified since %s", + local_modification_time_str, + ) + return False + _LOGGER.debug("has_remote_file_changed: File modified") + return True + except requests.exceptions.RequestException as e: + raise cv.Invalid( + f"Could not check if {url} has changed, please check if file exists " + f"({e})" + ) + + _LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path) + return True + + +def is_file_recent(file_path: str, refresh: TimePeriodSeconds) -> bool: + if os.path.exists(file_path): + creation_time = os.path.getctime(file_path) + current_time = datetime.now().timestamp() + return current_time - creation_time <= refresh.total_seconds + return False + + +def compute_local_file_dir(domain: str) -> Path: + base_directory = Path(CORE.data_dir) / domain + base_directory.mkdir(parents=True, exist_ok=True) + + return base_directory + + +def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes: + if not has_remote_file_changed(url, path): + _LOGGER.debug("Remote file has not changed %s", url) + return path.read_bytes() + + _LOGGER.debug( + "Remote file has changed, downloading from %s to %s", + url, + path, + ) + + try: + req = requests.get( + url, + timeout=timeout, + headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"}, + ) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download from {url}: {e}") + + path.parent.mkdir(parents=True, exist_ok=True) + data = req.content + path.write_bytes(data) + return data diff --git a/esphome/final_validate.py b/esphome/final_validate.py index 5e9d2207b0..cebd2f1cda 100644 --- a/esphome/final_validate.py +++ b/esphome/final_validate.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod -from typing import Any import contextvars +from typing import Any -from esphome.types import ConfigFragmentType, ID, ConfigPathType import esphome.config_validation as cv +from esphome.types import ID, ConfigFragmentType, ConfigPathType class FinalValidateConfig(ABC): diff --git a/esphome/git.py b/esphome/git.py index a607325b73..144c160b20 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -1,12 +1,12 @@ -import hashlib -import logging -import re -import subprocess -import urllib.parse from dataclasses import dataclass from datetime import datetime +import hashlib +import logging from pathlib import Path +import re +import subprocess from typing import Callable, Optional +import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds @@ -15,6 +15,7 @@ _LOGGER = logging.getLogger(__name__) def run_git_command(cmd, cwd=None) -> str: + _LOGGER.debug("Running git command: %s", " ".join(cmd)) try: ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) except FileNotFoundError as err: @@ -34,7 +35,7 @@ def run_git_command(cmd, cwd=None) -> str: def _compute_destination_path(key: str, domain: str) -> Path: - base_dir = Path(CORE.config_dir) / ".esphome" / domain + base_dir = Path(CORE.data_dir) / domain h = hashlib.new("sha256") h.update(key.encode()) return base_dir / h.hexdigest()[:8] @@ -48,6 +49,7 @@ def clone_or_update( domain: str, username: str = None, password: str = None, + submodules: Optional[list[str]] = None, ) -> tuple[Path, Optional[Callable[[], None]]]: key = f"{url}@{ref}" @@ -57,23 +59,28 @@ def clone_or_update( ) repo_dir = _compute_destination_path(key, domain) - fetch_pr_branch = ref is not None and ref.startswith("pull/") if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) cmd = ["git", "clone", "--depth=1"] - if ref is not None and not fetch_pr_branch: - cmd += ["--branch", ref] cmd += ["--", url, str(repo_dir)] run_git_command(cmd) - if fetch_pr_branch: + if ref is not None: # We need to fetch the PR branch first, otherwise git will complain # about missing objects _LOGGER.info("Fetching %s", ref) run_git_command(["git", "fetch", "--", "origin", ref], str(repo_dir)) run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + if submodules is not None: + _LOGGER.info( + "Initialising submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, str(repo_dir) + ) + else: # Check refresh needed file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") @@ -97,6 +104,14 @@ def clone_or_update( # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + if submodules is not None: + _LOGGER.info( + "Updating submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, str(repo_dir) + ) + def revert(): _LOGGER.info("Reverting changes to %s -> %s", key, old_sha) run_git_command(["git", "reset", "--hard", old_sha], str(repo_dir)) diff --git a/esphome/helpers.py b/esphome/helpers.py index fd8893ad99..2a7e5cd9b6 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,16 +1,20 @@ import codecs from contextlib import suppress - import logging import os from pathlib import Path -from typing import Union -import tempfile -from urllib.parse import urlparse +import platform import re +import tempfile +from typing import Union +from urllib.parse import urlparse _LOGGER = logging.getLogger(__name__) +IS_MACOS = platform.system() == "Darwin" +IS_WINDOWS = platform.system() == "Windows" +IS_LINUX = platform.system() == "Linux" + def ensure_unique_string(preferred_string, current_strings): test_string = preferred_string @@ -124,9 +128,10 @@ def _resolve_with_zeroconf(host): def resolve_ip_address(host): - from esphome.core import EsphomeError import socket + from esphome.core import EsphomeError + errs = [] if host.endswith(".local"): @@ -144,7 +149,14 @@ def resolve_ip_address(host): def get_bool_env(var, default=False): - return bool(os.getenv(var, default)) + value = os.getenv(var, default) + if isinstance(value, str): + value = value.lower() + if value in ["1", "true"]: + return True + if value in ["0", "false"]: + return False + return bool(value) def get_str_env(var, default=None): @@ -350,6 +362,9 @@ def snake_case(value): return value.replace(" ", "_").lower() +_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9-_]") + + def sanitize(value): """Same behaviour as `helpers.cpp` method `str_sanitize`.""" - return re.sub("[^-_0-9a-zA-Z]", r"", value) + return _DISALLOWED_CHARS.sub("_", value) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml new file mode 100644 index 0000000000..c79ba1b0ed --- /dev/null +++ b/esphome/idf_component.yml @@ -0,0 +1,13 @@ +dependencies: + esp-tflite-micro: + git: https://github.com/espressif/esp-tflite-micro.git + version: v1.3.1 + esp32_camera: + git: https://github.com/espressif/esp32-camera.git + version: v2.0.9 + mdns: + git: https://github.com/espressif/esp-protocols.git + version: mdns-v1.3.2 + path: components/mdns + rules: + - if: "idf_version >=5.0" diff --git a/esphome/loader.py b/esphome/loader.py index cd21e5a509..d808805119 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -1,17 +1,18 @@ -import logging -from typing import Callable, Optional, Any, ContextManager -from types import ModuleType -import importlib -import importlib.util -import importlib.resources -import importlib.abc -import sys -from pathlib import Path +from contextlib import AbstractContextManager from dataclasses import dataclass +import importlib +import importlib.abc +import importlib.resources +import importlib.util +import logging +from pathlib import Path +import sys +from types import ModuleType +from typing import Any, Callable, Optional from esphome.const import SOURCE_FILE_EXTENSIONS -import esphome.core.config from esphome.core import CORE +import esphome.core.config from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -22,8 +23,10 @@ class FileResource: package: str resource: str - def path(self) -> ContextManager[Path]: - return importlib.resources.path(self.package, self.resource) + def path(self) -> AbstractContextManager[Path]: + return importlib.resources.as_file( + importlib.resources.files(self.package) / self.resource + ) class ComponentManifest: @@ -57,6 +60,10 @@ class ComponentManifest: def multi_conf(self) -> bool: return getattr(self.module, "MULTI_CONF", False) + @property + def multi_conf_no_default(self) -> bool: + return getattr(self.module, "MULTI_CONF_NO_DEFAULT", False) + @property def to_code(self) -> Optional[Callable[[Any], None]]: return getattr(self.module, "to_code", None) @@ -97,10 +104,15 @@ class ComponentManifest: loaded .py file (does not look through subdirectories) """ ret = [] - for resource in importlib.resources.contents(self.package): + + for resource in ( + r.name + for r in importlib.resources.files(self.package).iterdir() + if r.is_file() + ): if Path(resource).suffix not in SOURCE_FILE_EXTENSIONS: continue - if not importlib.resources.is_resource(self.package, resource): + if not importlib.resources.files(self.package).joinpath(resource).is_file(): # Not a resource = this is a directory (yeah this is confusing) continue ret.append(FileResource(self.package, resource)) @@ -164,7 +176,11 @@ def _lookup_module(domain): try: module = importlib.import_module(f"esphome.components.{domain}") except ImportError as e: - if "No module named" not in str(e): + if "No module named" in str(e): + _LOGGER.info( + "Unable to import component %s: %s", domain, str(e), exc_info=False + ) + else: _LOGGER.error("Unable to import component %s:", domain, exc_info=True) return None except Exception: # pylint: disable=broad-except diff --git a/esphome/log.py b/esphome/log.py index e7ba0fdd82..23dc453d32 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -71,18 +71,20 @@ def setup_log( ) -> None: import colorama + colorama.init() + if debug: log_level = logging.DEBUG CORE.verbose = True elif quiet: log_level = logging.CRITICAL + CORE.quiet = True else: log_level = logging.INFO logging.basicConfig(level=log_level) logging.getLogger("urllib3").setLevel(logging.WARNING) - colorama.init() logging.getLogger().handlers[0].setFormatter( ESPHomeLogFormatter(include_timestamp=include_timestamp) ) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 166301005d..d55fb0202d 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,15 +1,15 @@ from datetime import datetime import hashlib +import json import logging import ssl -import sys import time -import json import paho.mqtt.client as mqtt from esphome.const import ( CONF_BROKER, + CONF_CERTIFICATE_AUTHORITY, CONF_DISCOVERY_PREFIX, CONF_ESPHOME, CONF_LOG_TOPIC, @@ -23,9 +23,9 @@ from esphome.const import ( CONF_USERNAME, ) from esphome.core import CORE, EsphomeError -from esphome.log import color, Fore +from esphome.helpers import get_int_env, get_str_env +from esphome.log import Fore, color from esphome.util import safe_print -from esphome.helpers import get_str_env, get_int_env _LOGGER = logging.getLogger(__name__) @@ -99,11 +99,10 @@ def prepare( elif username: client.username_pw_set(username, password) - if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS): - if sys.version_info >= (2, 7, 13): - tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member - else: - tls_version = ssl.PROTOCOL_SSLv23 + if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( + CONF_CERTIFICATE_AUTHORITY + ): + tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member client.tls_set( ca_certs=None, certfile=None, @@ -210,6 +209,12 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): elif CONF_MQTT in config: conf = config[CONF_MQTT] if CONF_LOG_TOPIC in conf: + if config[CONF_MQTT][CONF_LOG_TOPIC] is None: + _LOGGER.error("MQTT log topic set to null, can't start MQTT logs") + return 1 + if CONF_TOPIC not in config[CONF_MQTT][CONF_LOG_TOPIC]: + _LOGGER.error("MQTT log topic not available, can't start MQTT logs") + return 1 topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC] elif CONF_TOPIC_PREFIX in config[CONF_MQTT]: topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug" diff --git a/esphome/pins.py b/esphome/pins.py index 2ac4cd4b54..724cd25d82 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,8 +1,12 @@ -import operator from functools import reduce +import operator +import esphome.config_validation as cv from esphome.const import ( + CONF_ALLOW_OTHER_USES, + CONF_IGNORE_STRAPPING_WARNING, CONF_INPUT, + CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OPEN_DRAIN, @@ -10,16 +14,120 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) -from esphome.util import SimpleRegistry from esphome.core import CORE -PIN_SCHEMA_REGISTRY = SimpleRegistry() +class PinRegistry(dict): + def __init__(self): + super().__init__() + self.pins_used = {} + + def reset(self): + self.pins_used = {} + + def get_count(self, key, id, number): + """ + Get the number of places a given pin is used. + :param key: The key of the registered pin schema. + :param id: The ID of the defining component + :param number: The pin number + :return: The number of places the pin is used. + """ + pin_key = (key, id, number) + return len(self.pins_used[pin_key]) if pin_key in self.pins_used else 0 + + def register(self, name, schema, final_validate=None): + """ + Register a pin schema + :param name: + :param schema: + :param final_validate: + :return: + """ + + def decorator(fun): + self[name] = (fun, schema, final_validate) + return fun + + return decorator + + def validate(self, conf, key=None): + """ + Validate a pin against a registered schema + :param conf The pin config + :param key: an optional scalar key (e.g. platform) + :return: The transformed result + """ + from esphome.config import path_context + + key = self.get_key(conf) if key is None else key + # Element 1 is the pin validation function + # evaluate here so a validation failure skips the rest + result = self[key][1](conf) + if CONF_NUMBER in result: + # key maps to the pin schema + if key != CORE.target_platform: + pin_key = (key, conf[key], result[CONF_NUMBER]) + else: + pin_key = (key, key, result[CONF_NUMBER]) + if pin_key not in self.pins_used: + self.pins_used[pin_key] = [] + # client_id identifies the instance of the providing component + client_id = result.get(key) + self.pins_used[pin_key].append((path_context.get(), client_id, result)) + # return the validated pin config + return result + + def get_key(self, conf): + """ + Is there a key in conf corresponding to a registered pin schema? + If not, fall back to the default platform schema. + :param conf The config for the component + :return: the schema key + """ + keys = list(filter(lambda k: k in conf, self)) + return keys[0] if keys else CORE.target_platform + + def get_to_code(self, key): + """ + Return the code generator function for a pin schema, stored as tuple element 0 + :param conf: The pin config + :param key An optional specific key + :return: The awaitable coroutine + """ + key = self.get_key(key) if isinstance(key, dict) else key + return self[key][0] + + def final_validate(self, fconf): + """ + Run the final validation for all pins, and check for reuse + :param fconf: The full config + """ + for (key, _, _), pin_list in self.pins_used.items(): + count = len(pin_list) # number of places same pin used. + final_val_fun = self[key][2] # final validation function + for pin_path, client_id, pin_config in pin_list: + with fconf.catch_error([cv.ROOT_CONFIG_PATH] + pin_path): + if final_val_fun is not None: + # Get the containing path of the config providing this pin. + parent_path = fconf.get_path_for_id(client_id)[:-1] + parent_config = fconf.get_config_for_path(parent_path) + final_val_fun(pin_config, parent_config) + allow_others = pin_config.get(CONF_ALLOW_OTHER_USES, False) + if count != 1 and not allow_others: + raise cv.Invalid( + f"Pin {pin_config[CONF_NUMBER]} is used in multiple places" + ) + if count == 1 and allow_others: + raise cv.Invalid( + f"Pin {pin_config[CONF_NUMBER]} incorrectly sets {CONF_ALLOW_OTHER_USES}: true" + ) + + +PIN_SCHEMA_REGISTRY = PinRegistry() def _set_mode(value, default_mode): - import esphome.config_validation as cv - if CONF_MODE not in value: return {**value, CONF_MODE: default_mode} mode = value[CONF_MODE] @@ -65,20 +173,26 @@ def _schema_creator(default_mode, internal: bool = False): if not isinstance(value, dict): return validator({CONF_NUMBER: value}) value = _set_mode(value, default_mode) - if not internal: - for key, entry in PIN_SCHEMA_REGISTRY.items(): - if key != CORE.target_platform and key in value: - return entry[1](value) - return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value) + if internal: + return PIN_SCHEMA_REGISTRY.validate(value, CORE.target_platform) + return PIN_SCHEMA_REGISTRY.validate(value) return validator def _internal_number_creator(mode): def validator(value): - value_d = {CONF_NUMBER: value} + if isinstance(value, dict): + if CONF_MODE in value or CONF_INVERTED in value: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + value_d = value + else: + value_d = {CONF_NUMBER: value} value_d = _set_mode(value_d, mode) - return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value_d)[CONF_NUMBER] + return PIN_SCHEMA_REGISTRY.validate(value_d, CORE.target_platform)[CONF_NUMBER] return validator @@ -146,3 +260,75 @@ internal_gpio_input_pullup_pin_number = _internal_number_creator( CONF_PULLUP: True, } ) + + +def check_strapping_pin(conf, strapping_pin_list, logger): + num = conf[CONF_NUMBER] + if num in strapping_pin_list and not conf.get(CONF_IGNORE_STRAPPING_WARNING): + 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", + ) + # mitigate undisciplined use of strapping: + if num not in strapping_pin_list and conf.get(CONF_IGNORE_STRAPPING_WARNING): + raise cv.Invalid(f"GPIO{num} is not a strapping pin") + + +GPIO_STANDARD_MODES = ( + CONF_INPUT, + CONF_OUTPUT, + CONF_OPEN_DRAIN, + CONF_PULLUP, + CONF_PULLDOWN, +) + + +def gpio_validate_modes(value): + if not value[CONF_INPUT] and not value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be input or output") + return value + + +def gpio_base_schema( + pin_type, + number_validator, + modes=GPIO_STANDARD_MODES, + mode_validator=gpio_validate_modes, + invertable=True, +): + """ + Generate a base gpio pin schema + :param pin_type: The type for the pin variable + :param number_validator: A validator for the pin number + :param modes: The available modes, default is all standard modes + :param mode_validator: A validator function for the pin mode + :param invertable: If the pin supports hardware inversion + :return: A schema for the pin + """ + mode_default = len(modes) == 1 + mode_dict = dict( + map(lambda m: (cv.Optional(m, default=mode_default), cv.boolean), modes) + ) + + def _number_validator(value): + if isinstance(value, str) and value.upper().startswith("GPIOX"): + raise cv.Invalid( + f"Found placeholder '{value}' when expecting a GPIO pin number.\n" + "You must replace this with an actual pin number." + ) + return number_validator(value) + + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(pin_type), + cv.Required(CONF_NUMBER): _number_validator, + cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean, + cv.Optional(CONF_MODE, default={}): cv.All(mode_dict, mode_validator), + } + ) + + if invertable: + return schema.extend({cv.Optional(CONF_INVERTED, default=False): cv.boolean}) + + return schema diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index c46a3fc767..b81ec4ab37 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -1,12 +1,11 @@ from dataclasses import dataclass import json -from typing import Union -from pathlib import Path - import logging import os +from pathlib import Path import re import subprocess +from typing import Union from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError @@ -20,9 +19,10 @@ def patch_structhash(): # removed/added. This might have unintended consequences, but this improves compile # times greatly when adding/removing components and a simple clean build solves # all issues - from platformio.run import helpers, cli - from os.path import join, isdir, getmtime from os import makedirs + from os.path import getmtime, isdir, join + + from platformio.run import cli, helpers def patched_clean_build_dir(build_dir, *args): from platformio import fs diff --git a/esphome/schema_extractors.py b/esphome/schema_extractors.py index 2280a84849..5491bc88c4 100644 --- a/esphome/schema_extractors.py +++ b/esphome/schema_extractors.py @@ -8,7 +8,6 @@ originally do. However there is a property to further disable decorator impact.""" - # This is set to true by script/build_language_schema.py # only, so data is collected (again functionality is not modified) EnableSchemaExtraction = False diff --git a/esphome/storage_json.py b/esphome/storage_json.py index acf525203d..97cf9ceadd 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -1,89 +1,93 @@ +from __future__ import annotations + import binascii import codecs from datetime import datetime import json import logging import os -from typing import Optional from esphome import const +from esphome.const import CONF_DISABLED, CONF_MDNS from esphome.core import CORE from esphome.helpers import write_file_if_changed - - -from esphome.const import ( - CONF_MDNS, - CONF_DISABLED, -) - from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) def storage_path() -> str: - return CORE.relative_internal_path(f"{CORE.config_filename}.json") + return os.path.join(CORE.data_dir, "storage", f"{CORE.config_filename}.json") -def ext_storage_path(base_path: str, config_filename: str) -> str: - return os.path.join(base_path, ".esphome", f"{config_filename}.json") +def ext_storage_path(config_filename: str) -> str: + return os.path.join(CORE.data_dir, "storage", f"{config_filename}.json") -def esphome_storage_path(base_path: str) -> str: - return os.path.join(base_path, ".esphome", "esphome.json") +def esphome_storage_path() -> str: + return os.path.join(CORE.data_dir, "esphome.json") -def trash_storage_path(base_path: str) -> str: - return os.path.join(base_path, ".esphome", "trash") +def ignored_devices_storage_path() -> str: + return os.path.join(CORE.data_dir, "ignored-devices.json") + + +def trash_storage_path() -> str: + return CORE.relative_config_path("trash") class StorageJSON: def __init__( self, - storage_version, - name, - friendly_name, - comment, - esphome_version, - src_version, - address, - web_port, - target_platform, - build_path, - firmware_bin_path, - loaded_integrations, - no_mdns, - ): + storage_version: int, + name: str, + friendly_name: str, + comment: str, + esphome_version: str, + src_version: int | None, + address: str, + web_port: int | None, + target_platform: str, + build_path: str, + firmware_bin_path: str, + loaded_integrations: set[str], + no_mdns: bool, + framework: str | None = None, + core_platform: str | None = None, + ) -> None: # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) - self.storage_version: int = storage_version + self.storage_version = storage_version # The name of the node - self.name: str = name + self.name = name # The friendly name of the node - self.friendly_name: str = friendly_name + self.friendly_name = friendly_name # The comment of the node - self.comment: str = comment + self.comment = comment # The esphome version this was compiled with - self.esphome_version: str = esphome_version + self.esphome_version = esphome_version # The version of the file in src/main.cpp - Used to migrate the file assert src_version is None or isinstance(src_version, int) - self.src_version: int = src_version + self.src_version = src_version # Address of the ESP, for example livingroom.local or a static IP - self.address: str = address + self.address = address # Web server port of the ESP, for example 80 assert web_port is None or isinstance(web_port, int) - self.web_port: int = web_port + self.web_port = web_port # The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc. - self.target_platform: str = target_platform + self.target_platform = target_platform # The absolute path to the platformio project - self.build_path: str = build_path + self.build_path = build_path # The absolute path to the firmware binary - self.firmware_bin_path: str = firmware_bin_path - # A list of strings of names of loaded integrations - self.loaded_integrations: list[str] = loaded_integrations - self.loaded_integrations.sort() + self.firmware_bin_path = firmware_bin_path + # A set of strings of names of loaded integrations + self.loaded_integrations = loaded_integrations # Is mDNS disabled self.no_mdns = no_mdns + # The framework used to compile the firmware + self.framework = framework + # The core platform of this firmware. Like "esp32", "rp2040", "host" etc. + self.core_platform = core_platform def as_dict(self): return { @@ -98,8 +102,10 @@ class StorageJSON: "esp_platform": self.target_platform, "build_path": self.build_path, "firmware_bin_path": self.firmware_bin_path, - "loaded_integrations": self.loaded_integrations, + "loaded_integrations": sorted(self.loaded_integrations), "no_mdns": self.no_mdns, + "framework": self.framework, + "core_platform": self.core_platform, } def to_json(self): @@ -109,9 +115,7 @@ class StorageJSON: write_file_if_changed(path, self.to_json()) @staticmethod - def from_esphome_core( - esph: CoreType, old: Optional["StorageJSON"] - ) -> "StorageJSON": + def from_esphome_core(esph: CoreType, old: StorageJSON | None) -> StorageJSON: hardware = esph.target_platform.upper() if esph.is_esp32: from esphome.components import esp32 @@ -129,18 +133,20 @@ class StorageJSON: target_platform=hardware, build_path=esph.build_path, firmware_bin_path=esph.firmware_bin, - loaded_integrations=list(esph.loaded_integrations), + loaded_integrations=esph.loaded_integrations, no_mdns=( CONF_MDNS in esph.config and CONF_DISABLED in esph.config[CONF_MDNS] and esph.config[CONF_MDNS][CONF_DISABLED] is True ), + framework=esph.target_framework, + core_platform=esph.target_platform, ) @staticmethod def from_wizard( name: str, friendly_name: str, address: str, platform: str - ) -> "StorageJSON": + ) -> StorageJSON: return StorageJSON( storage_version=1, name=name, @@ -153,12 +159,14 @@ class StorageJSON: target_platform=platform, build_path=None, firmware_bin_path=None, - loaded_integrations=[], + loaded_integrations=set(), no_mdns=False, + framework=None, + core_platform=platform.lower(), ) @staticmethod - def _load_impl(path: str) -> Optional["StorageJSON"]: + def _load_impl(path: str) -> StorageJSON | None: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -174,8 +182,10 @@ class StorageJSON: esp_platform = storage.get("esp_platform") build_path = storage.get("build_path") firmware_bin_path = storage.get("firmware_bin_path") - loaded_integrations = storage.get("loaded_integrations", []) + loaded_integrations = set(storage.get("loaded_integrations", [])) no_mdns = storage.get("no_mdns", False) + framework = storage.get("framework") + core_platform = storage.get("core_platform") return StorageJSON( storage_version, name, @@ -190,10 +200,12 @@ class StorageJSON: firmware_bin_path, loaded_integrations, no_mdns, + framework, + core_platform, ) @staticmethod - def load(path: str) -> Optional["StorageJSON"]: + def load(path: str) -> StorageJSON | None: try: return StorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except @@ -215,7 +227,7 @@ class EsphomeStorageJSON: # The last time ESPHome checked for an update as an isoformat encoded str self.last_update_check_str: str = last_update_check # Cache of the version gotten in the last version check - self.remote_version: Optional[str] = remote_version + self.remote_version: str | None = remote_version def as_dict(self) -> dict: return { @@ -226,7 +238,7 @@ class EsphomeStorageJSON: } @property - def last_update_check(self) -> Optional[datetime]: + def last_update_check(self) -> datetime | None: try: return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") except Exception: # pylint: disable=broad-except @@ -243,7 +255,7 @@ class EsphomeStorageJSON: write_file_if_changed(path, self.to_json()) @staticmethod - def _load_impl(path: str) -> Optional["EsphomeStorageJSON"]: + def _load_impl(path: str) -> EsphomeStorageJSON | None: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -255,14 +267,14 @@ class EsphomeStorageJSON: ) @staticmethod - def load(path: str) -> Optional["EsphomeStorageJSON"]: + def load(path: str) -> EsphomeStorageJSON | None: try: return EsphomeStorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except return None @staticmethod - def get_default() -> "EsphomeStorageJSON": + def get_default() -> EsphomeStorageJSON: return EsphomeStorageJSON( storage_version=1, cookie_secret=binascii.hexlify(os.urandom(64)).decode(), diff --git a/esphome/types.py b/esphome/types.py index adb16fa91b..4e69e3cbd7 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -1,7 +1,8 @@ """This helper module tracks commonly used types in the esphome python codebase.""" + from typing import Union -from esphome.core import ID, Lambda, EsphomeCore +from esphome.core import ID, EsphomeCore, Lambda ConfigFragmentType = Union[ str, diff --git a/esphome/util.py b/esphome/util.py index 0d60212f50..32fd90cd25 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,13 +1,12 @@ -from typing import Union - import collections import io import logging import os +from pathlib import Path import re import subprocess import sys -from pathlib import Path +from typing import Union from esphome import const @@ -57,7 +56,7 @@ class SimpleRegistry(dict): return decorator -def safe_print(message=""): +def safe_print(message="", end="\n"): from esphome.core import CORE if CORE.dashboard: @@ -67,20 +66,26 @@ def safe_print(message=""): pass try: - print(message) + print(message, end=end) return except UnicodeEncodeError: pass try: - print(message.encode("utf-8", "backslashreplace")) + print(message.encode("utf-8", "backslashreplace"), end=end) except UnicodeEncodeError: try: - print(message.encode("ascii", "backslashreplace")) + print(message.encode("ascii", "backslashreplace"), end=end) except UnicodeEncodeError: print("Cannot print line because of invalid locale!") +def safe_input(prompt=""): + if prompt: + safe_print(prompt, end="") + return input() + + def shlex_quote(s): if not s: return "''" @@ -190,7 +195,7 @@ def run_external_command( try: sys.argv = list(cmd) sys.exit = mock_exit - return func() or 0 + retval = func() or 0 except KeyboardInterrupt: # pylint: disable=try-except-raise raise except SystemExit as err: @@ -206,9 +211,10 @@ def run_external_command( sys.stdout = orig_stdout sys.stderr = orig_stderr - if capture_stdout: - # pylint: disable=lost-exception - return cap_stdout.getvalue() + if capture_stdout: + return cap_stdout.getvalue() + + return retval def run_external_process(*cmd, **kwargs): diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 281ef10964..15f9206f21 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -2,6 +2,7 @@ import difflib import itertools import voluptuous as vol + from esphome.schema_extractors import schema_extractor_extended @@ -64,7 +65,7 @@ class _Schema(vol.Schema): # Recursively compile schema _compiled_schema = {} - for skey, svalue in vol.iteritems(schema): + for skey, svalue in schema.items(): new_key = self._compile(skey) new_value = self._compile(svalue) _compiled_schema[skey] = (new_key, new_value) @@ -203,6 +204,11 @@ class _Schema(vol.Schema): self._extra_schemas.append(validator) return self + def prepend_extra(self, validator): + validator = _Schema(validator) + self._extra_schemas.insert(0, validator) + return self + @schema_extractor_extended def extend(self, *schemas, **kwargs): extra = kwargs.pop("extra", None) @@ -220,4 +226,6 @@ class _Schema(vol.Schema): if isinstance(schema, vol.Schema): schema = schema.schema ret = super().extend(schema, extra=extra) - return _Schema(ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas) + return _Schema( + ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas.copy() + ) diff --git a/esphome/vscode.py b/esphome/vscode.py index cb2f51976f..907ed88216 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -1,20 +1,23 @@ +from __future__ import annotations + +from io import StringIO import json import os +from typing import Any -from typing import Optional - -from esphome.config import load_config, _format_vol_invalid, Config -from esphome.core import CORE, DocumentRange +from esphome.config import Config, _format_vol_invalid, validate_config import esphome.config_validation as cv +from esphome.core import CORE, DocumentRange +from esphome.yaml_util import parse_yaml -def _get_invalid_range(res: Config, invalid: cv.Invalid) -> Optional[DocumentRange]: +def _get_invalid_range(res: Config, invalid: cv.Invalid) -> DocumentRange | None: return res.get_deepest_document_range_for_path( invalid.path, invalid.error_message == "extra keys not allowed" ) -def _dump_range(range: Optional[DocumentRange]) -> Optional[dict]: +def _dump_range(range: DocumentRange | None) -> dict | None: if range is None: return None return { @@ -56,6 +59,25 @@ class VSCodeResult: ) +def _read_file_content_from_json_on_stdin() -> str: + """Read the content of a json encoded file from stdin.""" + data = json.loads(input()) + assert data["type"] == "file_response" + return data["content"] + + +def _print_file_read_event(path: str) -> None: + """Print a file read event.""" + print( + json.dumps( + { + "type": "read_file", + "path": path, + } + ) + ) + + def read_config(args): while True: CORE.reset() @@ -68,9 +90,17 @@ def read_config(args): CORE.config_path = os.path.join(args.configuration, f) else: CORE.config_path = data["file"] + + file_name = CORE.config_path + _print_file_read_event(file_name) + raw_yaml = _read_file_content_from_json_on_stdin() + command_line_substitutions: dict[str, Any] = ( + dict(args.substitution) if args.substitution else {} + ) vs = VSCodeResult() try: - res = load_config(dict(args.substitution) if args.substitution else {}) + config = parse_yaml(file_name, StringIO(raw_yaml)) + res = validate_config(config, command_line_substitutions) except Exception as err: # pylint: disable=broad-except vs.add_yaml_error(str(err)) else: diff --git a/esphome/wizard.py b/esphome/wizard.py index fd661af639..eecbbdb172 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -6,12 +6,12 @@ import unicodedata import voluptuous as vol import esphome.config_validation as cv -from esphome.helpers import get_bool_env, write_file -from esphome.log import color, Fore - -from esphome.storage_json import StorageJSON, ext_storage_path -from esphome.util import safe_print from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD +from esphome.core import CORE +from esphome.helpers import get_bool_env, write_file +from esphome.log import Fore, color +from esphome.storage_json import StorageJSON, ext_storage_path +from esphome.util import safe_input, safe_print CORE_BIG = r""" _____ ____ _____ ______ / ____/ __ \| __ \| ____| @@ -51,10 +51,12 @@ BASE_CONFIG_FRIENDLY = """esphome: friendly_name: {friendly_name} """ -LOGGER_API_CONFIG = """ +LOGGER_CONFIG = """ # Enable logging logger: +""" +API_CONFIG = """ # Enable Home Assistant API api: """ @@ -88,9 +90,16 @@ esp32: RP2040_CONFIG = """ rp2040: board: {board} - framework: - # Required until https://github.com/platformio/platform-raspberrypi/pull/36 is merged - platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git +""" + +BK72XX_CONFIG = """ +bk72xx: + board: {board} +""" + +RTL87XX_CONFIG = """ +rtl87xx: + board: {board} """ HARDWARE_BASE_CONFIGS = { @@ -99,6 +108,8 @@ HARDWARE_BASE_CONFIGS = { "ESP32S2": ESP32S2_CONFIG, "ESP32C3": ESP32C3_CONFIG, "RP2040": RP2040_CONFIG, + "BK72XX": BK72XX_CONFIG, + "RTL87XX": RTL87XX_CONFIG, } @@ -124,7 +135,12 @@ def wizard_file(**kwargs): config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs) - config += LOGGER_API_CONFIG + config += LOGGER_CONFIG + + if kwargs["board"] == "rpipico": + return config + + config += API_CONFIG # Configure API if "password" in kwargs: @@ -134,10 +150,11 @@ def wizard_file(**kwargs): # Configure OTA config += "\nota:\n" + config += " - platform: esphome\n" if "ota_password" in kwargs: - config += f" password: \"{kwargs['ota_password']}\"" + config += f" password: \"{kwargs['ota_password']}\"" elif "password" in kwargs: - config += f" password: \"{kwargs['password']}\"" + config += f" password: \"{kwargs['password']}\"" # Configuring wifi config += "\n\nwifi:\n" @@ -156,7 +173,7 @@ def wizard_file(**kwargs): """ # pylint: disable=consider-using-f-string - if kwargs["platform"] in ["ESP8266", "ESP32"]: + if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "RTL87XX"]: config += """ # Enable fallback hotspot (captive portal) in case wifi connection fails ap: @@ -181,8 +198,11 @@ captive_portal: def wizard_write(path, **kwargs): + from esphome.components.bk72xx import boards as bk72xx_boards + from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards from esphome.components.rp2040 import boards as rp2040_boards + from esphome.components.rtl87xx import boards as rtl87xx_boards name = kwargs["name"] board = kwargs["board"] @@ -192,20 +212,29 @@ def wizard_write(path, **kwargs): kwargs[key] = sanitize_double_quotes(kwargs[key]) if "platform" not in kwargs: - if board in esp8266_boards.ESP8266_BOARD_PINS: + if board in esp8266_boards.BOARDS: platform = "ESP8266" - elif board in rp2040_boards.RP2040_BOARD_PINS: - platform = "RP2040" - else: + elif board in esp32_boards.BOARDS: platform = "ESP32" + elif board in rp2040_boards.BOARDS: + platform = "RP2040" + elif board in bk72xx_boards.BOARDS: + platform = "BK72XX" + elif board in rtl87xx_boards.BOARDS: + platform = "RTL87XX" + else: + safe_print(color(Fore.RED, f'The board "{board}" is unknown.')) + return False kwargs["platform"] = platform hardware = kwargs["platform"] write_file(path, wizard_file(**kwargs)) storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware) - storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) + storage_path = ext_storage_path(os.path.basename(path)) storage.save(storage_path) + return True + if get_bool_env(ENV_QUICKWIZARD): @@ -228,7 +257,7 @@ def safe_print_step(step, big): def default_input(text, default): safe_print() safe_print(f"Press ENTER for default ({default})") - return input(text.format(default)) or default + return safe_input(text.format(default)) or default # From https://stackoverflow.com/a/518232/8924614 @@ -241,8 +270,11 @@ def strip_accents(value): def wizard(path): + from esphome.components.bk72xx import boards as bk72xx_boards from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards + from esphome.components.rp2040 import boards as rp2040_boards + from esphome.components.rtl87xx import boards as rtl87xx_boards if not path.endswith(".yaml") and not path.endswith(".yml"): safe_print( @@ -254,6 +286,9 @@ def wizard(path): f"Uh oh, it seems like {color(Fore.CYAN, path)} already exists, please delete that file first or chose another configuration file." ) return 2 + + CORE.config_path = path + safe_print("Hi there!") sleep(1.5) safe_print("I'm the wizard of ESPHome :)") @@ -262,7 +297,7 @@ def wizard(path): sleep(2.0) safe_print( "In 4 steps I'm going to guide you through creating a basic " - "configuration file for your custom ESP8266/ESP32 firmware. Yay!" + "configuration file for your custom firmware. Yay!" ) sleep(3.0) safe_print() @@ -277,7 +312,7 @@ def wizard(path): ) safe_print() sleep(1) - name = input(color(Fore.BOLD_WHITE, "(name): ")) + name = safe_input(color(Fore.BOLD_WHITE, "(name): ")) while True: try: @@ -305,16 +340,20 @@ def wizard(path): "Now I'd like to know what microcontroller you're using so that I can compile " "firmwares for it." ) + + wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "RTL87XX", "RP2040"] safe_print( - f"Are you using an {color(Fore.GREEN, 'ESP32')} or {color(Fore.GREEN, 'ESP8266')} platform? (Choose ESP8266 for Sonoff devices)" + "Please choose one of the supported microcontrollers " + "(Use ESP8266 for Sonoff devices)." ) while True: sleep(0.5) safe_print() - safe_print("Please enter either ESP32 or ESP8266.") - platform = input(color(Fore.BOLD_WHITE, "(ESP32/ESP8266): ")) + platform = safe_input( + color(Fore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ") + ) try: - platform = vol.All(vol.Upper, vol.Any("ESP32", "ESP8266"))(platform) + platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper()) break except vol.Invalid: safe_print( @@ -328,10 +367,18 @@ def wizard(path): board_link = ( "http://docs.platformio.org/en/latest/platforms/espressif32.html#boards" ) - else: + elif platform == "ESP8266": board_link = ( "http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" ) + elif platform == "RP2040": + board_link = ( + "https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html" + ) + elif platform in ["BK72XX", "RTL87XX"]: + board_link = "https://docs.libretiny.eu/docs/status/supported/" + else: + raise NotImplementedError("Unknown platform!") safe_print(f"Next, I need to know what {color(Fore.GREEN, 'board')} you're using.") sleep(0.5) @@ -342,14 +389,31 @@ def wizard(path): # Don't sleep because user needs to copy link if platform == "ESP32": safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcu-32s')}\".") - boards = list(esp32_boards.ESP32_BOARD_PINS.keys()) - else: + boards_list = esp32_boards.BOARDS.items() + elif platform == "ESP8266": safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcuv2')}\".") - boards = list(esp8266_boards.ESP8266_BOARD_PINS.keys()) - safe_print(f"Options: {', '.join(sorted(boards))}") + boards_list = esp8266_boards.BOARDS.items() + elif platform == "BK72XX": + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'cb2s')}\".") + boards_list = bk72xx_boards.BOARDS.items() + elif platform == "RTL87XX": + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'wr3')}\".") + boards_list = rtl87xx_boards.BOARDS.items() + elif platform == "RP2040": + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'rpipicow')}\".") + boards_list = rp2040_boards.BOARDS.items() + + else: + raise NotImplementedError("Unknown platform!") + + boards = [] + safe_print("Options:") + for board_id, board_data in boards_list: + safe_print(f" - {board_id} - {board_data['name']}") + boards.append(board_id) while True: - board = input(color(Fore.BOLD_WHITE, "(board): ")) + board = safe_input(color(Fore.BOLD_WHITE, "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break @@ -365,62 +429,66 @@ def wizard(path): safe_print() sleep(1) - safe_print_step(3, WIFI_BIG) - safe_print("In this step, I'm going to create the configuration for WiFi.") - safe_print() - sleep(1) - safe_print( - f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" - ) - sleep(1.5) - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'Abraham Linksys')}\".") - while True: - ssid = input(color(Fore.BOLD_WHITE, "(ssid): ")) - try: - ssid = cv.ssid(ssid) - break - except vol.Invalid: - safe_print( - color( - Fore.RED, - f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.', + # Do not create wifi if the board does not support it + if board not in ["rpipico"]: + safe_print_step(3, WIFI_BIG) + safe_print("In this step, I'm going to create the configuration for WiFi.") + safe_print() + sleep(1) + safe_print( + f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" + ) + sleep(1.5) + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'Abraham Linksys')}\".") + while True: + ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): ")) + try: + ssid = cv.ssid(ssid) + break + except vol.Invalid: + safe_print( + color( + Fore.RED, + f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.', + ) ) - ) - safe_print() - sleep(1) + safe_print() + sleep(1) - safe_print( - f'Thank you very much! You\'ve just chosen "{color(Fore.CYAN, ssid)}" as your SSID.' - ) - safe_print() - sleep(0.75) + safe_print( + f'Thank you very much! You\'ve just chosen "{color(Fore.CYAN, ssid)}" as your SSID.' + ) + safe_print() + sleep(0.75) - safe_print( - f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)" - ) - safe_print() - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'PASSWORD42')}\"") - sleep(0.5) - psk = input(color(Fore.BOLD_WHITE, "(PSK): ")) - safe_print( - "Perfect! WiFi is now set up (you can create static IPs and so on later)." - ) - sleep(1.5) + safe_print( + f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)" + ) + safe_print() + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'PASSWORD42')}\"") + sleep(0.5) + psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): ")) + safe_print( + "Perfect! WiFi is now set up (you can create static IPs and so on later)." + ) + sleep(1.5) - safe_print_step(4, OTA_BIG) - safe_print( - "Almost there! ESPHome can automatically upload custom firmwares over WiFi " - "(over the air) and integrates into Home Assistant with a native API." - ) - safe_print( - f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(Fore.GREEN, 'password')} for connecting to this ESP?" - ) - safe_print() - sleep(0.25) - safe_print("Press ENTER for no password") - password = input(color(Fore.BOLD_WHITE, "(password): ")) + safe_print_step(4, OTA_BIG) + safe_print( + "Almost there! ESPHome can automatically upload custom firmwares over WiFi " + "(over the air) and integrates into Home Assistant with a native API." + ) + safe_print( + f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(Fore.GREEN, 'password')} for connecting to this ESP?" + ) + safe_print() + sleep(0.25) + safe_print("Press ENTER for no password") + password = safe_input(color(Fore.BOLD_WHITE, "(password): ")) + else: + ssid, password, psk = "", "", "" - wizard_write( + if not wizard_write( path=path, name=name, platform=platform, @@ -428,7 +496,8 @@ def wizard(path): ssid=ssid, psk=psk, password=password, - ) + ): + return 1 safe_print() safe_print( diff --git a/esphome/writer.py b/esphome/writer.py index ad506b6ae6..90446ae4b1 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,27 +1,29 @@ +import importlib import logging import os -import re from pathlib import Path +import re from typing import Union -from esphome.config import iter_components +from esphome import loader +from esphome.config import iter_component_configs, iter_components from esphome.const import ( + ENV_NOGITIGNORE, HEADER_FILE_EXTENSIONS, + PLATFORM_ESP32, SOURCE_FILE_EXTENSIONS, __version__, - ENV_NOGITIGNORE, ) from esphome.core import CORE, EsphomeError from esphome.helpers import ( - mkdir_p, - read_file, - write_file_if_changed, - walk_files, copy_file_if_changed, get_bool_env, + mkdir_p, + read_file, + walk_files, + write_file_if_changed, ) from esphome.storage_json import StorageJSON, storage_path -from esphome import loader _LOGGER = logging.getLogger(__name__) @@ -70,14 +72,14 @@ UPLOAD_SPEED_OVERRIDE = { def get_flags(key): flags = set() - for _, component, conf in iter_components(CORE.config): + for _, component, conf in iter_component_configs(CORE.config): flags |= getattr(component, key)(conf) return flags def get_include_text(): include_text = '#include "esphome.h"\nusing namespace esphome;\n' - for _, component, conf in iter_components(CORE.config): + for _, component, conf in iter_component_configs(CORE.config): if not hasattr(component, "includes"): continue includes = component.includes @@ -106,6 +108,11 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: return True if old.build_path != new.build_path: return True + if old.loaded_integrations != new.loaded_integrations: + if new.core_platform == PLATFORM_ESP32: + from esphome.components.esp32 import FRAMEWORK_ESP_IDF + + return new.framework == FRAMEWORK_ESP_IDF return False @@ -117,7 +124,9 @@ def update_storage_json(): return if storage_should_clean(old, new): - _LOGGER.info("Core config or version changed, cleaning build files...") + _LOGGER.info( + "Core config, version or integrations changed, cleaning build files..." + ) clean_build() new.save(path) @@ -203,7 +212,9 @@ def write_platformio_project(): write_platformio_ini(content) -DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ +DEFINES_H_FORMAT = ( + ESPHOME_H_FORMAT +) = """\ #pragma once #include "esphome/core/macros.h" {} @@ -230,7 +241,7 @@ the custom_components folder or the external_components feature. def copy_src_tree(): source_files: list[loader.FileResource] = [] - for _, component, _ in iter_components(CORE.config): + for _, component in iter_components(CORE.config): source_files += component.resources source_files_map = { Path(x.package.replace(".", "/") + "/" + x.resource): x for x in source_files @@ -289,25 +300,13 @@ def copy_src_tree(): CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h() ) - if CORE.is_esp32: - from esphome.components.esp32 import copy_files - + platform = "esphome.components." + CORE.target_platform + try: + module = importlib.import_module(platform) + copy_files = getattr(module, "copy_files") copy_files() - - elif CORE.is_esp8266: - from esphome.components.esp8266 import copy_files - - copy_files() - - elif CORE.is_rp2040: - from esphome.components.rp2040 import copy_files - - (pio) = copy_files() - if pio: - write_file_if_changed( - CORE.relative_src_path("esphome.h"), - ESPHOME_H_FORMAT.format(include_s + '\n#include "pio_includes.h"'), - ) + except AttributeError: + pass def generate_defines_h(): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 8a03c431a7..d67511dfec 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,23 +1,34 @@ +from __future__ import annotations + import fnmatch import functools import inspect +from io import TextIOWrapper import logging import math import os - +from typing import Any import uuid + import yaml +from yaml import SafeLoader as PurePythonLoader import yaml.constructor +try: + from yaml import CSafeLoader as FastestAvailableSafeLoader +except ImportError: + FastestAvailableSafeLoader = PurePythonLoader + from esphome import core -from esphome.config_helpers import read_config_file, Extend +from esphome.config_helpers import Extend, Remove from esphome.core import ( + CORE, + DocumentRange, EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, - DocumentRange, ) from esphome.helpers import add_class_to_obj from esphome.util import OrderedDict, filter_yaml_files @@ -88,7 +99,7 @@ def _add_data_ref(fn): return wrapped -class ESPHomeLoader(yaml.SafeLoader): +class ESPHomeLoaderMixin: """Loader class that keeps track of line numbers.""" @_add_data_ref @@ -240,7 +251,18 @@ class ESPHomeLoader(yaml.SafeLoader): @_add_data_ref def construct_secret(self, node): - secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) + try: + secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) + except EsphomeError as e: + if self.name == CORE.config_path: + raise e + try: + main_config_dir = os.path.dirname(CORE.config_path) + main_secret_yml = os.path.join(main_config_dir, SECRET_YAML) + secrets = _load_yaml_internal(main_secret_yml) + except EsphomeError as er: + raise EsphomeError(f"{e}\n{er}") from er + if node.value not in secrets: raise yaml.MarkedYAMLError( f"Secret '{node.value}' not defined", node.start_mark @@ -262,8 +284,8 @@ class ESPHomeLoader(yaml.SafeLoader): return file, vars def substitute_vars(config, vars): - from esphome.const import CONF_SUBSTITUTIONS from esphome.components import substitutions + from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS org_subs = None result = config @@ -274,7 +296,15 @@ class ESPHomeLoader(yaml.SafeLoader): elif CONF_SUBSTITUTIONS in result: org_subs = result.pop(CONF_SUBSTITUTIONS) + defaults = {} + if CONF_DEFAULTS in result: + defaults = result.pop(CONF_DEFAULTS) + result[CONF_SUBSTITUTIONS] = vars + for k, v in defaults.items(): + if k not in result[CONF_SUBSTITUTIONS]: + result[CONF_SUBSTITUTIONS][k] = v + # Ignore missing vars that refer to the top level substitutions substitutions.do_substitution_pass(result, None, ignore_missing=True) result.pop(CONF_SUBSTITUTIONS) @@ -291,8 +321,9 @@ class ESPHomeLoader(yaml.SafeLoader): file, vars = node.value, None result = _load_yaml_internal(self._rel_path(file)) - if vars: - result = substitute_vars(result, vars) + if not vars: + vars = {} + result = substitute_vars(result, vars) return result @_add_data_ref @@ -342,50 +373,81 @@ class ESPHomeLoader(yaml.SafeLoader): def construct_extend(self, node): return Extend(str(node.value)) - -ESPHomeLoader.add_constructor("tag:yaml.org,2002:int", ESPHomeLoader.construct_yaml_int) -ESPHomeLoader.add_constructor( - "tag:yaml.org,2002:float", ESPHomeLoader.construct_yaml_float -) -ESPHomeLoader.add_constructor( - "tag:yaml.org,2002:binary", ESPHomeLoader.construct_yaml_binary -) -ESPHomeLoader.add_constructor( - "tag:yaml.org,2002:omap", ESPHomeLoader.construct_yaml_omap -) -ESPHomeLoader.add_constructor("tag:yaml.org,2002:str", ESPHomeLoader.construct_yaml_str) -ESPHomeLoader.add_constructor("tag:yaml.org,2002:seq", ESPHomeLoader.construct_yaml_seq) -ESPHomeLoader.add_constructor("tag:yaml.org,2002:map", ESPHomeLoader.construct_yaml_map) -ESPHomeLoader.add_constructor("!env_var", ESPHomeLoader.construct_env_var) -ESPHomeLoader.add_constructor("!secret", ESPHomeLoader.construct_secret) -ESPHomeLoader.add_constructor("!include", ESPHomeLoader.construct_include) -ESPHomeLoader.add_constructor( - "!include_dir_list", ESPHomeLoader.construct_include_dir_list -) -ESPHomeLoader.add_constructor( - "!include_dir_merge_list", ESPHomeLoader.construct_include_dir_merge_list -) -ESPHomeLoader.add_constructor( - "!include_dir_named", ESPHomeLoader.construct_include_dir_named -) -ESPHomeLoader.add_constructor( - "!include_dir_merge_named", ESPHomeLoader.construct_include_dir_merge_named -) -ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda) -ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force) -ESPHomeLoader.add_constructor("!extend", ESPHomeLoader.construct_extend) + @_add_data_ref + def construct_remove(self, node): + return Remove(str(node.value)) -def load_yaml(fname, clear_secrets=True): +class ESPHomeLoader(ESPHomeLoaderMixin, FastestAvailableSafeLoader): + """Loader class that keeps track of line numbers.""" + + +class ESPHomePurePythonLoader(ESPHomeLoaderMixin, PurePythonLoader): + """Loader class that keeps track of line numbers.""" + + +for _loader in (ESPHomeLoader, ESPHomePurePythonLoader): + _loader.add_constructor("tag:yaml.org,2002:int", _loader.construct_yaml_int) + _loader.add_constructor("tag:yaml.org,2002:float", _loader.construct_yaml_float) + _loader.add_constructor("tag:yaml.org,2002:binary", _loader.construct_yaml_binary) + _loader.add_constructor("tag:yaml.org,2002:omap", _loader.construct_yaml_omap) + _loader.add_constructor("tag:yaml.org,2002:str", _loader.construct_yaml_str) + _loader.add_constructor("tag:yaml.org,2002:seq", _loader.construct_yaml_seq) + _loader.add_constructor("tag:yaml.org,2002:map", _loader.construct_yaml_map) + _loader.add_constructor("!env_var", _loader.construct_env_var) + _loader.add_constructor("!secret", _loader.construct_secret) + _loader.add_constructor("!include", _loader.construct_include) + _loader.add_constructor("!include_dir_list", _loader.construct_include_dir_list) + _loader.add_constructor( + "!include_dir_merge_list", _loader.construct_include_dir_merge_list + ) + _loader.add_constructor("!include_dir_named", _loader.construct_include_dir_named) + _loader.add_constructor( + "!include_dir_merge_named", _loader.construct_include_dir_merge_named + ) + _loader.add_constructor("!lambda", _loader.construct_lambda) + _loader.add_constructor("!force", _loader.construct_force) + _loader.add_constructor("!extend", _loader.construct_extend) + _loader.add_constructor("!remove", _loader.construct_remove) + + +def load_yaml(fname: str, clear_secrets: bool = True) -> Any: if clear_secrets: _SECRET_VALUES.clear() _SECRET_CACHE.clear() return _load_yaml_internal(fname) -def _load_yaml_internal(fname): - content = read_config_file(fname) - loader = ESPHomeLoader(content) +def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any: + """Parse a YAML file.""" + try: + return _load_yaml_internal_with_type(ESPHomeLoader, file_name, file_handle) + except EsphomeError: + # Loading failed, so we now load with the Python loader which has more + # readable exceptions + # Rewind the stream so we can try again + file_handle.seek(0, 0) + return _load_yaml_internal_with_type( + ESPHomePurePythonLoader, file_name, file_handle + ) + + +def _load_yaml_internal(fname: str) -> Any: + """Load a YAML file.""" + try: + with open(fname, encoding="utf-8") as f_handle: + return parse_yaml(fname, f_handle) + except (UnicodeDecodeError, OSError) as err: + raise EsphomeError(f"Error reading file {fname}: {err}") from err + + +def _load_yaml_internal_with_type( + loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader], + fname: str, + content: TextIOWrapper, +) -> Any: + """Load a YAML file.""" + loader = loader_type(content) loader.name = fname try: return loader.get_single_data() or OrderedDict() diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index b0dddfd152..b3ee64e259 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,117 +1,48 @@ -import socket -import threading -import time -from typing import Optional -import logging +from __future__ import annotations + +import asyncio from dataclasses import dataclass +import logging +from typing import Callable -from zeroconf import ( - DNSAddress, - DNSOutgoing, - DNSRecord, - DNSQuestion, - RecordUpdateListener, - Zeroconf, - ServiceBrowser, - ServiceStateChange, - current_time_millis, -) +from zeroconf import IPVersion, ServiceInfo, ServiceStateChange, Zeroconf +from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf + +from esphome.storage_json import StorageJSON, ext_storage_path -_CLASS_IN = 1 -_FLAGS_QR_QUERY = 0x0000 # query -_TYPE_A = 1 _LOGGER = logging.getLogger(__name__) -class HostResolver(RecordUpdateListener): - def __init__(self, name: str): - self.name = name - self.address: Optional[bytes] = None - - def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: - if record is None: - return - if record.type == _TYPE_A: - assert isinstance(record, DNSAddress) - if record.name == self.name: - self.address = record.address - - def request(self, zc: Zeroconf, timeout: float) -> bool: - now = time.time() - delay = 0.2 - next_ = now + delay - last = now + timeout - - try: - zc.add_listener(self, None) - while self.address is None: - if last <= now: - # Timeout - return False - if next_ <= now: - out = DNSOutgoing(_FLAGS_QR_QUERY) - out.add_question(DNSQuestion(self.name, _TYPE_A, _CLASS_IN)) - zc.send(out) - next_ = now + delay - delay *= 2 - - time.sleep(min(next_, last) - now) - now = time.time() - finally: - zc.remove_listener(self) - - return True +_BACKGROUND_TASKS: set[asyncio.Task] = set() -class DashboardStatus(threading.Thread): - PING_AFTER = 15 * 1000 # Send new mDNS request after 15 seconds - OFFLINE_AFTER = PING_AFTER * 2 # Offline if no mDNS response after 30 seconds +class HostResolver(ServiceInfo): + """Resolve a host name to an IP address.""" - def __init__(self, zc: Zeroconf, on_update) -> None: - threading.Thread.__init__(self) - self.zc = zc - self.query_hosts: set[str] = set() - self.key_to_host: dict[str, str] = {} - self.stop_event = threading.Event() - self.query_event = threading.Event() + @property + def _is_complete(self) -> bool: + """The ServiceInfo has all expected properties.""" + return bool(self._ipv4_addresses) + + +class DashboardStatus: + def __init__(self, on_update: Callable[[dict[str, bool | None], []]]) -> None: + """Initialize the dashboard status.""" self.on_update = on_update - def request_query(self, hosts: dict[str, str]) -> None: - self.query_hosts = set(hosts.values()) - self.key_to_host = hosts - self.query_event.set() - - def stop(self) -> None: - self.stop_event.set() - self.query_event.set() - - def host_status(self, key: str) -> bool: - entries = self.zc.cache.entries_with_name(key) - if not entries: - return False - now = current_time_millis() - - return any( - (entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries - ) - - def run(self) -> None: - while not self.stop_event.is_set(): - self.on_update( - {key: self.host_status(host) for key, host in self.key_to_host.items()} - ) - now = current_time_millis() - for host in self.query_hosts: - entries = self.zc.cache.entries_with_name(host) - if not entries or all( - (entry.created + DashboardStatus.PING_AFTER) <= now - for entry in entries - ): - out = DNSOutgoing(_FLAGS_QR_QUERY) - out.add_question(DNSQuestion(host, _TYPE_A, _CLASS_IN)) - self.zc.send(out) - self.query_event.wait() - self.query_event.clear() + def browser_callback( + self, + zeroconf: Zeroconf, + service_type: str, + name: str, + state_change: ServiceStateChange, + ) -> None: + """Handle a service update.""" + short_name = name.partition(".")[0] + if state_change == ServiceStateChange.Removed: + self.on_update({short_name: False}) + elif state_change in (ServiceStateChange.Updated, ServiceStateChange.Added): + self.on_update({short_name: True}) ESPHOME_SERVICE_TYPE = "_esphomelib._tcp.local." @@ -120,11 +51,12 @@ TXT_RECORD_PROJECT_NAME = b"project_name" TXT_RECORD_PROJECT_VERSION = b"project_version" TXT_RECORD_NETWORK = b"network" TXT_RECORD_FRIENDLY_NAME = b"friendly_name" +TXT_RECORD_VERSION = b"version" @dataclass class DiscoveredImport: - friendly_name: Optional[str] + friendly_name: str | None device_name: str package_import_url: str project_name: str @@ -132,15 +64,15 @@ class DiscoveredImport: network: str +class DashboardBrowser(AsyncServiceBrowser): + """A class to browse for ESPHome nodes.""" + + class DashboardImportDiscovery: - def __init__(self, zc: Zeroconf) -> None: - self.zc = zc - self.service_browser = ServiceBrowser( - self.zc, ESPHOME_SERVICE_TYPE, [self._on_update] - ) + def __init__(self) -> None: self.import_state: dict[str, DiscoveredImport] = {} - def _on_update( + def browser_callback( self, zeroconf: Zeroconf, service_type: str, @@ -153,8 +85,6 @@ class DashboardImportDiscovery: name, state_change, ) - if service_type != ESPHOME_SERVICE_TYPE: - return if state_change == ServiceStateChange.Removed: self.import_state.pop(name, None) return @@ -163,7 +93,28 @@ class DashboardImportDiscovery: # Ignore updates for devices that are not in the import state return - info = zeroconf.get_service_info(service_type, name) + info = AsyncServiceInfo( + service_type, + name, + ) + if info.load_from_cache(zeroconf): + self._process_service_info(name, info) + return + task = asyncio.create_task( + self._async_process_service_info(zeroconf, info, service_type, name) + ) + _BACKGROUND_TASKS.add(task) + task.add_done_callback(_BACKGROUND_TASKS.discard) + + async def _async_process_service_info( + self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str + ) -> None: + """Process a service info.""" + if await info.async_request(zeroconf, timeout=3000): + self._process_service_info(name, info) + + def _process_service_info(self, name: str, info: ServiceInfo) -> None: + """Process a service info.""" _LOGGER.debug("-> resolved info: %s", info) if info is None: return @@ -175,6 +126,10 @@ class DashboardImportDiscovery: ] if any(key not in info.properties for key in required_keys): # Not a dashboard import device + version = info.properties.get(TXT_RECORD_VERSION) + if version is not None: + version = version.decode() + self.update_device_mdns(node_name, version) return import_url = info.properties[TXT_RECORD_PACKAGE_IMPORT_URL].decode() @@ -194,13 +149,51 @@ class DashboardImportDiscovery: network=network, ) - def cancel(self) -> None: - self.service_browser.cancel() + def update_device_mdns(self, node_name: str, version: str): + storage_path = ext_storage_path(node_name + ".yaml") + storage_json = StorageJSON.load(storage_path) + + if storage_json is not None: + storage_version = storage_json.esphome_version + if version != storage_version: + storage_json.esphome_version = version + storage_json.save(storage_path) + _LOGGER.info( + "Updated %s with mdns version %s (was %s)", + node_name, + version, + storage_version, + ) + + +def _make_host_resolver(host: str) -> HostResolver: + """Create a new HostResolver for the given host name.""" + name = host.partition(".")[0] + info = HostResolver( + ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}", server=f"{name}.local." + ) + return info class EsphomeZeroconf(Zeroconf): - def resolve_host(self, host: str, timeout=3.0): - info = HostResolver(host) - if info.request(self, timeout): - return socket.inet_ntoa(info.address) + def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: + """Resolve a host name to an IP address.""" + info = _make_host_resolver(host) + if ( + info.load_from_cache(self) + or (timeout and info.request(self, timeout * 1000)) + ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): + return str(addresses[0]) + return None + + +class AsyncEsphomeZeroconf(AsyncZeroconf): + async def async_resolve_host(self, host: str, timeout: float = 3.0) -> str | None: + """Resolve a host name to an IP address.""" + info = _make_host_resolver(host) + if ( + info.load_from_cache(self.zeroconf) + or (timeout and await info.async_request(self.zeroconf, timeout * 1000)) + ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): + return str(addresses[0]) return None diff --git a/platformio.ini b/platformio.ini index 3565b15809..04afc059af 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,7 +4,7 @@ ; It's *not* used during runtime. [platformio] -default_envs = esp8266-arduino, esp32-arduino, esp32-idf +default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino ; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build ; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this ; being the root directory. Instead, set esphome/ as the source directory, all our sources are in @@ -35,12 +35,15 @@ build_flags = lib_deps = esphome/noise-c@0.1.4 ; api makuna/NeoPixelBus@2.7.3 ; neopixelbus - esphome/Improv@1.2.3 ; improv_serial / esp32_improv + improv/Improv@1.2.4 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code - functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 + functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 + pavlodn/HaierProtocol@0.9.31 ; haier + kikuchan98/pngle@1.0.2 ; 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 build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = @@ -56,15 +59,15 @@ lib_deps = ${common.lib_deps} SPI ; spi (Arduino built-in) Wire ; i2c (Arduino built-int) - ottowinter/AsyncMqttClient-esphome@0.8.6 ; mqtt - esphome/ESPAsyncWebServer-esphome@2.1.0 ; web_server_base + heman/AsyncMqttClient-esphome@1.0.0 ; mqtt + esphome/ESPAsyncWebServer-esphome@3.2.2 ; web_server_base fastled/FastLED@3.3.2 ; fastled_base mikalhart/TinyGPSPlus@1.0.2 ; gps freekode/TM1651@1.0.1 ; tm1651 - glmnet/Dsmr@0.5 ; dsmr + glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr - dudanov/MideaUART@1.1.8 ; midea - tonia/HeatpumpIR@1.0.20 ; heatpumpir + dudanov/MideaUART@1.1.9 ; midea + tonia/HeatpumpIR@1.0.27 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO @@ -79,20 +82,21 @@ build_flags = ; This are common settings for the ESP8266 using Arduino. [common:esp8266-arduino] extends = common:arduino -platform = platformio/espressif8266@3.2.0 +platform = platformio/espressif8266@4.2.1 platform_packages = - platformio/framework-arduinoespressif8266@~3.30002.0 + platformio/framework-arduinoespressif8266@~3.30102.0 framework = arduino lib_deps = ${common:arduino.lib_deps} ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) - esphome/ESPAsyncTCP-esphome@1.2.3 ; async_tcp + esphome/ESPAsyncTCP-esphome@2.0.0 ; async_tcp ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) - crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir + droscy/esp_wireguard@0.4.2 ; wireguard + build_flags = ${common:arduino.build_flags} -Wno-nonnull-compare @@ -103,7 +107,7 @@ 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 = platformio/espressif32@5.3.0 +platform = platformio/espressif32@5.4.0 platform_packages = platformio/framework-arduinoespressif32@~3.20005.0 @@ -115,13 +119,14 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - esphome/AsyncTCP-esphome@1.2.2 ; async_tcp + esphome/AsyncTCP-esphome@2.1.4 ; async_tcp WiFiClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) esphome/ESP32-audioI2S@2.0.7 ; i2s_audio - crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir + droscy/esp_wireguard@0.4.2 ; wireguard + build_flags = ${common:arduino.build_flags} -DUSE_ESP32 @@ -132,14 +137,15 @@ 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 = platformio/espressif32@5.3.0 +platform = platformio/espressif32@5.4.0 platform_packages = - platformio/framework-espidf@~3.40404.0 + platformio/framework-espidf@~3.40408.0 framework = espidf lib_deps = ${common:idf.lib_deps} - espressif/esp32-camera@1.0.0 ; esp32_camera + droscy/esp_wireguard@0.4.2 ; wireguard + kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = ${common:idf.build_flags} -Wno-nonnull-compare @@ -147,16 +153,22 @@ build_flags = -DUSE_ESP32_FRAMEWORK_ESP_IDF extra_scripts = post:esphome/components/esp32/post_build.py.script +; This are common settings for the ESP32 using the latest ESP-IDF version. +[common:esp32-idf-5_3] +extends = common:esp32-idf +platform = platformio/espressif32@6.8.0 +platform_packages = + platformio/framework-espidf@~3.50300.0 + ; These are common settings for the RP2040 using Arduino. [common:rp2040-arduino] extends = common:arduino -board_build.core = earlephilhower board_build.filesystem_size = 0.5m -platform = https://github.com/maxgerhardt/platform-raspberrypi.git +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.2.0-gcc12 platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/2.6.2/rp2040-2.6.2.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.9.4/rp2040-3.9.4.zip framework = arduino lib_deps = @@ -166,6 +178,18 @@ build_flags = -DUSE_RP2040 -DUSE_RP2040_FRAMEWORK_ARDUINO +; This are common settings for the LibreTiny (all variants) using Arduino. +[common:libretiny-arduino] +extends = common:arduino +platform = libretiny +framework = arduino +lib_deps = + droscy/esp_wireguard@0.4.2 ; wireguard +build_flags = + ${common:arduino.build_flags} + -DUSE_LIBRETINY +build_src_flags = -include Arduino.h + ; All the actual environments are defined below. ;;;;;;;; ESP8266 ;;;;;;;; @@ -212,6 +236,15 @@ build_flags = ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32 +[env:esp32-idf-5_3] +extends = common:esp32-idf-5_3 +board = esp32dev +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32 + [env:esp32-idf-tidy] extends = common:esp32-idf board = esp32dev @@ -248,6 +281,15 @@ build_flags = ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32C3 +[env:esp32c3-idf-5_3] +extends = common:esp32-idf-5_3 +board = esp32-c3-devkitm-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32C3 + [env:esp32c3-idf-tidy] extends = common:esp32-idf board = esp32-c3-devkitm-1 @@ -284,6 +326,15 @@ build_flags = ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S2 +[env:esp32s2-idf-5_3] +extends = common:esp32-idf-5_3 +board = esp32-s2-kaluga-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32S2 + [env:esp32s2-idf-tidy] extends = common:esp32-idf board = esp32-s2-kaluga-1 @@ -320,6 +371,15 @@ build_flags = ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S3 +[env:esp32s3-idf-5_3] +extends = common:esp32-idf-5_3 +board = esp32-s3-devkitc-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s3-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32S3 + [env:esp32s3-idf-tidy] extends = common:esp32-idf board = esp32-s3-devkitc-1 @@ -338,6 +398,35 @@ build_flags = ${common:rp2040-arduino.build_flags} ${flags:runtime.build_flags} +;;;;;;;; LibreTiny ;;;;;;;; + +[env:bk72xx-arduino] +extends = common:libretiny-arduino +board = generic-bk7231n-qfn32-tuya +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:runtime.build_flags} + -DUSE_BK72XX + -DUSE_LIBRETINY_VARIANT_BK7231N + +[env:rtl87xxb-arduino] +extends = common:libretiny-arduino +board = generic-rtl8710bn-2mb-788k +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:runtime.build_flags} + -DUSE_RTL87XX + -DUSE_LIBRETINY_VARIANT_RTL8710B + +[env:rtl87xxc-arduino] +extends = common:libretiny-arduino +board = generic-rtl8720cf-2mb-992k +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:runtime.build_flags} + -DUSE_RTL87XX + -DUSE_LIBRETINY_VARIANT_RTL8720C + [env:host] extends = common platform = platformio/native @@ -346,3 +435,4 @@ lib_deps = build_flags = ${common.build_flags} -DUSE_HOST + -std=c++17 diff --git a/pylintrc b/pylintrc deleted file mode 100644 index b70e5c7da9..0000000000 --- a/pylintrc +++ /dev/null @@ -1,30 +0,0 @@ -[MASTER] -reports=no -ignore=api_pb2.py - -disable= - format, - missing-docstring, - fixme, - unused-argument, - global-statement, - too-few-public-methods, - too-many-lines, - too-many-locals, - too-many-ancestors, - too-many-branches, - too-many-statements, - too-many-arguments, - too-many-return-statements, - too-many-instance-attributes, - duplicate-code, - invalid-name, - cyclic-import, - redefined-builtin, - undefined-loop-variable, - useless-object-inheritance, - stop-iteration-return, - import-outside-toplevel, - # Broken - unsupported-membership-test, - unsubscriptable-object, diff --git a/pyproject.toml b/pyproject.toml index a49abb7b3d..cfc279845f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,137 @@ +[build-system] +requires = ["setuptools==69.2.0", "wheel~=0.43.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "esphome" +license = {text = "MIT"} +description = "Make creating custom firmwares for ESP32/ESP8266 super easy." +readme = "README.md" +authors = [ + {name = "The ESPHome Authors", email = "esphome@nabucasa.com"} +] +keywords = ["home", "automation"] +classifiers = [ + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Topic :: Home Automation", +] +requires-python = ">=3.9.0" + +dynamic = ["dependencies", "optional-dependencies", "version"] + +[project.urls] +"Documentation" = "https://esphome.io" +"Source Code" = "https://github.com/esphome/esphome" +"Bug Tracker" = "https://github.com/esphome/issues/issues" +"Feature Request Tracker" = "https://github.com/esphome/feature-requests/issues" +"Discord" = "https://discord.gg/KhAMKrd" +"Forum" = "https://community.home-assistant.io/c/esphome" +"Twitter" = "https://twitter.com/esphome_" + +[project.scripts] +esphome = "esphome.__main__:main" + +[tool.setuptools] +platforms = ["any"] +zip-safe = false +include-package-data = true + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} +optional-dependencies.dev = { file = ["requirements_dev.txt"] } +optional-dependencies.test = { file = ["requirements_test.txt"] } +optional-dependencies.displays = { file = ["requirements_optional.txt"] } +version = {attr = "esphome.const.__version__"} + +[tool.setuptools.packages.find] +include = ["esphome*"] + [tool.black] target-version = ["py39", "py310"] exclude = 'generated' + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] +addopts = [ + "--cov=esphome", + "--cov-branch", +] + +[tool.pylint.MAIN] +py-version = "3.9" +ignore = [ + "api_pb2.py", +] +persistent = false + +[tool.pylint.REPORTS] +score = false + +[tool.pylint."MESSAGES CONTROL"] +disable = [ + "format", + "missing-docstring", + "fixme", + "unused-argument", + "global-statement", + "too-few-public-methods", + "too-many-lines", + "too-many-locals", + "too-many-ancestors", + "too-many-branches", + "too-many-statements", + "too-many-arguments", + "too-many-return-statements", + "too-many-instance-attributes", + "duplicate-code", + "invalid-name", + "cyclic-import", + "redefined-builtin", + "undefined-loop-variable", + "useless-object-inheritance", + "stop-iteration-return", + "import-outside-toplevel", + # Broken + "unsupported-membership-test", + "unsubscriptable-object", +] + +[tool.pylint.FORMAT] +expected-line-ending-format = "LF" + +[tool.ruff] +required-version = ">=0.5.0" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "F", # pyflakes/autoflake + "I", # isort + "PL", # pylint + "UP", # pyupgrade +] + +ignore = [ + "E501", # line too long + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target +] + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = [ + "esphome", +] +combine-as-imports = true +split-on-trailing-comma = false diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index a91a2ea200..0000000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -addopts = - --cov=esphome - --cov-branch diff --git a/requirements.txt b/requirements.txt index 2b5b2c7e1f..e11e629743 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,25 @@ -voluptuous==0.13.1 -PyYAML==6.0 +async_timeout==4.0.3; python_version <= "3.10" +cryptography==43.0.0 +voluptuous==0.14.2 +PyYAML==6.0.2 paho-mqtt==1.6.1 colorama==0.4.6 -tornado==6.3.2 -tzlocal==5.0.1 # from time +icmplib==3.0.4 +tornado==6.4 +tzlocal==5.2 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.7 # When updating platformio, also update Dockerfile -esptool==4.5.1 -click==8.1.3 -esphome-dashboard==20230516.0 -aioesphomeapi==13.7.5 -zeroconf==0.62.0 +platformio==6.1.16 # When updating platformio, also update Dockerfile +esptool==4.7.0 +click==8.1.7 +esphome-dashboard==20241025.0 +aioesphomeapi==24.6.2 +zeroconf==0.132.2 +puremagic==1.27 +ruamel.yaml==0.18.6 # dashboard_import +glyphsets==1.0.0 +pillow==10.4.0 +freetype-py==2.5.1 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 @@ -19,3 +27,6 @@ kconfiglib==13.7.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 + +# For autocompletion +argcomplete>=2.0.0 diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000000..eb749a861d --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,4 @@ +# Useful stuff when working in a development environment +clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating +clang-tidy==14.0.6 # When updating clang-tidy, also update Dockerfile +yamllint==1.35.1 # also change in .pre-commit-config.yaml when updating diff --git a/requirements_optional.txt b/requirements_optional.txt index 2c73430109..7416753d55 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,2 +1 @@ -pillow>4.0.0 -cryptography>=2.0.0,<4 +cairosvg==2.7.1 diff --git a/requirements_test.txt b/requirements_test.txt index 42aba7ddcf..5d94f7f640 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -pylint==2.17.4 -flake8==6.0.0 # also change in .pre-commit-config.yaml when updating -black==23.3.0 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.4.0 # also change in .pre-commit-config.yaml when updating +pylint==3.2.7 +flake8==7.0.0 # also change in .pre-commit-config.yaml when updating +black==24.4.2 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.15.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.3.1 -pytest-cov==4.0.0 -pytest-mock==3.10.0 -pytest-asyncio==0.21.0 +pytest==8.2.0 +pytest-cov==5.0.0 +pytest-mock==3.14.0 +pytest-asyncio==0.23.6 asyncmock==0.4.2 -hypothesis==5.49.0 +hypothesis==6.92.1 diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index dba6f47d43..a2bc3abf64 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -4,7 +4,7 @@ It's pretty crappy spaghetti code, but it works. you need to install protobuf-compiler: -running protc --version should return +running protoc --version should return libprotoc 3.6.1 then run this script with python3 and the files @@ -17,27 +17,22 @@ then run this script with python3 and the files will be generated, they still need to be formatted """ +import os import re +import sys +from abc import ABC, abstractmethod from pathlib import Path -from textwrap import dedent from subprocess import call +from textwrap import dedent # Generate with # protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto - import aioesphomeapi.api_options_pb2 as pb import google.protobuf.descriptor_pb2 as descriptor -file_header = "// This file was automatically generated with a tool.\n" -file_header += "// See scripts/api_protobuf/api_protobuf.py\n" - -cwd = Path(__file__).resolve().parent -root = cwd.parent.parent / "esphome" / "components" / "api" -prot = root / "api.protoc" -call(["protoc", "-o", str(prot), "-I", str(root), "api.proto"]) -content = prot.read_bytes() - -d = descriptor.FileDescriptorSet.FromString(content) +FILE_HEADER = """// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py +""" def indent_list(text, padding=" "): @@ -63,7 +58,7 @@ def camel_to_snake(name): return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() -class TypeInfo: +class TypeInfo(ABC): def __init__(self, field): self._field = field @@ -185,10 +180,12 @@ class TypeInfo: def dump_content(self): o = f'out.append(" {self.name}: ");\n' o += self.dump(f"this->{self.field_name}") + "\n" - o += f'out.append("\\n");\n' + o += 'out.append("\\n");\n' return o - dump = None + @abstractmethod + def dump(self, name: str): + pass TYPE_INFO = {} @@ -211,7 +208,7 @@ class DoubleType(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%g", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -224,7 +221,7 @@ class FloatType(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%g", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -237,7 +234,7 @@ class Int64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%lld", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -250,7 +247,7 @@ class UInt64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%llu", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -262,8 +259,8 @@ class Int32Type(TypeInfo): encode_func = "encode_int32" def dump(self, name): - o = f'sprintf(buffer, "%d", {name});\n' - o += f"out.append(buffer);" + o = f'sprintf(buffer, "%" PRId32, {name});\n' + o += "out.append(buffer);" return o @@ -276,7 +273,7 @@ class Fixed64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%llu", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -288,8 +285,8 @@ class Fixed32Type(TypeInfo): encode_func = "encode_fixed32" def dump(self, name): - o = f'sprintf(buffer, "%u", {name});\n' - o += f"out.append(buffer);" + o = f'sprintf(buffer, "%" PRIu32, {name});\n' + o += "out.append(buffer);" return o @@ -370,8 +367,8 @@ class UInt32Type(TypeInfo): encode_func = "encode_uint32" def dump(self, name): - o = f'sprintf(buffer, "%u", {name});\n' - o += f"out.append(buffer);" + o = f'sprintf(buffer, "%" PRIu32, {name});\n' + o += "out.append(buffer);" return o @@ -404,8 +401,8 @@ class SFixed32Type(TypeInfo): encode_func = "encode_sfixed32" def dump(self, name): - o = f'sprintf(buffer, "%d", {name});\n' - o += f"out.append(buffer);" + o = f'sprintf(buffer, "%" PRId32, {name});\n' + o += "out.append(buffer);" return o @@ -418,7 +415,7 @@ class SFixed64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%lld", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -430,8 +427,8 @@ class SInt32Type(TypeInfo): encode_func = "encode_sint32" def dump(self, name): - o = f'sprintf(buffer, "%d", {name});\n' - o += f"out.append(buffer);" + o = f'sprintf(buffer, "%" PRId32, {name});\n' + o += "out.append(buffer);" return o @@ -444,7 +441,7 @@ class SInt64Type(TypeInfo): def dump(self, name): o = f'sprintf(buffer, "%lld", {name});\n' - o += f"out.append(buffer);" + o += "out.append(buffer);" return o @@ -526,7 +523,7 @@ class RepeatedTypeInfo(TypeInfo): def encode_content(self): o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" - o += f"}}" + o += "}" return o @property @@ -534,10 +531,13 @@ class RepeatedTypeInfo(TypeInfo): o = f'for (const auto {"" if self._ti_is_bool else "&"}it : this->{self.field_name}) {{\n' o += f' out.append(" {self.name}: ");\n' o += indent(self._ti.dump("it")) + "\n" - o += f' out.append("\\n");\n' - o += f"}}\n" + o += ' out.append("\\n");\n' + o += "}\n" return o + def dump(self, _: str): + pass + def build_enum_type(desc): name = desc.name @@ -546,17 +546,17 @@ def build_enum_type(desc): out += f" {v.name} = {v.number},\n" out += "};\n" - cpp = f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + cpp = "#ifdef HAS_PROTO_MESSAGE_DUMP\n" cpp += f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n" - cpp += f" switch (value) {{\n" + cpp += " switch (value) {\n" for v in desc.value: cpp += f" case enums::{v.name}:\n" cpp += f' return "{v.name}";\n' - cpp += f" default:\n" - cpp += f' return "UNKNOWN";\n' - cpp += f" }}\n" - cpp += f"}}\n" - cpp += f"#endif\n" + cpp += " default:\n" + cpp += ' return "UNKNOWN";\n' + cpp += " }\n" + cpp += "}\n" + cpp += "#endif\n" return out, cpp @@ -651,10 +651,10 @@ def build_message_type(desc): o += f" {dump[0]} " else: o += "\n" - o += f" __attribute__((unused)) char buffer[64];\n" + o += " __attribute__((unused)) char buffer[64];\n" o += f' out.append("{desc.name} {{\\n");\n' o += indent("\n".join(dump)) + "\n" - o += f' out.append("}}");\n' + o += ' out.append("}");\n' else: o2 = f'out.append("{desc.name} {{}}");' if len(o) + len(o2) + 3 < 120: @@ -663,9 +663,9 @@ def build_message_type(desc): o += "\n" o += f" {o2}\n" o += "}\n" - cpp += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" cpp += o - cpp += f"#endif\n" + cpp += "#endif\n" prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n" prot += "void dump_to(std::string &out) const override;\n" prot += "#endif\n" @@ -683,69 +683,12 @@ def build_message_type(desc): return out, cpp -file = d.file[0] -content = file_header -content += """\ -#pragma once - -#include "proto.h" - -namespace esphome { -namespace api { - -""" - -cpp = file_header -cpp += """\ -#include "api_pb2.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace api { - -""" - -content += "namespace enums {\n\n" - -for enum in file.enum_type: - s, c = build_enum_type(enum) - content += s - cpp += c - -content += "\n} // namespace enums\n\n" - -mt = file.message_type - -for m in mt: - s, c = build_message_type(m) - content += s - cpp += c - -content += """\ - -} // namespace api -} // namespace esphome -""" -cpp += """\ - -} // namespace api -} // namespace esphome -""" - -with open(root / "api_pb2.h", "w") as f: - f.write(content) - -with open(root / "api_pb2.cpp", "w") as f: - f.write(cpp) - SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2 RECEIVE_CASES = {} -class_name = "APIServerConnectionBase" - ifdefs = {} @@ -765,7 +708,6 @@ def build_service_message_type(mt): ifdef = get_opt(mt, pb.ifdef) log = get_opt(mt, pb.log, True) - nodelay = get_opt(mt, pb.no_delay, False) hout = "" cout = "" @@ -778,14 +720,14 @@ def build_service_message_type(mt): # Generate send func = f"send_{snake}" hout += f"bool {func}(const {mt.name} &msg);\n" - cout += f"bool {class_name}::{func}(const {mt.name} &msg) {{\n" + cout += f"bool APIServerConnectionBase::{func}(const {mt.name} &msg) {{\n" if log: - cout += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + cout += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' - cout += f"#endif\n" + cout += "#endif\n" # cout += f' this->set_nodelay({str(nodelay).lower()});\n' cout += f" return this->send_message_<{mt.name}>(msg, {id_});\n" - cout += f"}}\n" + cout += "}\n" if source in (SOURCE_BOTH, SOURCE_CLIENT): # Generate receive func = f"on_{snake}" @@ -794,153 +736,242 @@ def build_service_message_type(mt): if ifdef is not None: case += f"#ifdef {ifdef}\n" case += f"{mt.name} msg;\n" - case += f"msg.decode(msg_data, msg_size);\n" + case += "msg.decode(msg_data, msg_size);\n" if log: - case += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' - case += f"#endif\n" + case += "#endif\n" case += f"this->{func}(msg);\n" if ifdef is not None: - case += f"#endif\n" + case += "#endif\n" case += "break;" RECEIVE_CASES[id_] = case if ifdef is not None: - hout += f"#endif\n" - cout += f"#endif\n" + hout += "#endif\n" + cout += "#endif\n" return hout, cout -hpp = file_header -hpp += """\ -#pragma once +def main(): + cwd = Path(__file__).resolve().parent + root = cwd.parent.parent / "esphome" / "components" / "api" + prot_file = root / "api.protoc" + call(["protoc", "-o", str(prot_file), "-I", str(root), "api.proto"]) + proto_content = prot_file.read_bytes() -#include "api_pb2.h" -#include "esphome/core/defines.h" + # pylint: disable-next=no-member + d = descriptor.FileDescriptorSet.FromString(proto_content) -namespace esphome { -namespace api { + file = d.file[0] + content = FILE_HEADER + content += """\ + #pragma once -""" + #include "proto.h" -cpp = file_header -cpp += """\ -#include "api_pb2_service.h" -#include "esphome/core/log.h" + namespace esphome { + namespace api { -namespace esphome { -namespace api { + """ -static const char *const TAG = "api.service"; + cpp = FILE_HEADER + cpp += """\ + #include "api_pb2.h" + #include "esphome/core/log.h" -""" + #include -hpp += f"class {class_name} : public ProtoService {{\n" -hpp += " public:\n" + namespace esphome { + namespace api { -for mt in file.message_type: - obj = build_service_message_type(mt) - if obj is None: - continue - hout, cout = obj - hpp += indent(hout) + "\n" - cpp += cout + """ -cases = list(RECEIVE_CASES.items()) -cases.sort() -hpp += " protected:\n" -hpp += f" bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" -out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" -out += f" switch (msg_type) {{\n" -for i, case in cases: - c = f"case {i}: {{\n" - c += indent(case) + "\n" - c += f"}}" - out += indent(c, " ") + "\n" -out += " default:\n" -out += " return false;\n" -out += " }\n" -out += " return true;\n" -out += "}\n" -cpp += out -hpp += "};\n" + content += "namespace enums {\n\n" -serv = file.service[0] -class_name = "APIServerConnection" -hpp += "\n" -hpp += f"class {class_name} : public {class_name}Base {{\n" -hpp += " public:\n" -hpp_protected = "" -cpp += "\n" + for enum in file.enum_type: + s, c = build_enum_type(enum) + content += s + cpp += c -m = serv.method[0] -for m in serv.method: - func = m.name - inp = m.input_type[1:] - ret = m.output_type[1:] - is_void = ret == "void" - snake = camel_to_snake(inp) - on_func = f"on_{snake}" - needs_conn = get_opt(m, pb.needs_setup_connection, True) - needs_auth = get_opt(m, pb.needs_authentication, True) + content += "\n} // namespace enums\n\n" - ifdef = ifdefs.get(inp, None) + mt = file.message_type - if ifdef is not None: - hpp += f"#ifdef {ifdef}\n" - hpp_protected += f"#ifdef {ifdef}\n" - cpp += f"#ifdef {ifdef}\n" + for m in mt: + s, c = build_message_type(m) + content += s + cpp += c - hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" - hpp += f" virtual {ret} {func}(const {inp} &msg) = 0;\n" - cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" - body = "" - if needs_conn: - body += "if (!this->is_connection_setup()) {\n" - body += " this->on_no_setup_connection();\n" - body += " return;\n" - body += "}\n" - if needs_auth: - body += "if (!this->is_authenticated()) {\n" - body += " this->on_unauthenticated_access();\n" - body += " return;\n" - body += "}\n" + content += """\ - if is_void: - body += f"this->{func}(msg);\n" - else: - body += f"{ret} ret = this->{func}(msg);\n" - ret_snake = camel_to_snake(ret) - body += f"if (!this->send_{ret_snake}(ret)) {{\n" - body += f" this->on_fatal_error();\n" - body += "}\n" - cpp += indent(body) + "\n" + "}\n" + } // namespace api + } // namespace esphome + """ + cpp += """\ - if ifdef is not None: - hpp += f"#endif\n" - hpp_protected += f"#endif\n" - cpp += f"#endif\n" + } // namespace api + } // namespace esphome + """ -hpp += " protected:\n" -hpp += hpp_protected -hpp += "};\n" + with open(root / "api_pb2.h", "w", encoding="utf-8") as f: + f.write(content) -hpp += """\ + with open(root / "api_pb2.cpp", "w", encoding="utf-8") as f: + f.write(cpp) -} // namespace api -} // namespace esphome -""" -cpp += """\ + hpp = FILE_HEADER + hpp += """\ + #pragma once -} // namespace api -} // namespace esphome -""" + #include "api_pb2.h" + #include "esphome/core/defines.h" -with open(root / "api_pb2_service.h", "w") as f: - f.write(hpp) + namespace esphome { + namespace api { -with open(root / "api_pb2_service.cpp", "w") as f: - f.write(cpp) + """ -prot.unlink() + cpp = FILE_HEADER + cpp += """\ + #include "api_pb2_service.h" + #include "esphome/core/log.h" + + namespace esphome { + namespace api { + + static const char *const TAG = "api.service"; + + """ + + class_name = "APIServerConnectionBase" + + hpp += f"class {class_name} : public ProtoService {{\n" + hpp += " public:\n" + + for mt in file.message_type: + obj = build_service_message_type(mt) + if obj is None: + continue + hout, cout = obj + hpp += indent(hout) + "\n" + cpp += cout + + cases = list(RECEIVE_CASES.items()) + cases.sort() + hpp += " protected:\n" + hpp += " bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + out += " switch (msg_type) {\n" + for i, case in cases: + c = f"case {i}: {{\n" + c += indent(case) + "\n" + c += "}" + out += indent(c, " ") + "\n" + out += " default:\n" + out += " return false;\n" + out += " }\n" + out += " return true;\n" + out += "}\n" + cpp += out + hpp += "};\n" + + serv = file.service[0] + class_name = "APIServerConnection" + hpp += "\n" + hpp += f"class {class_name} : public {class_name}Base {{\n" + hpp += " public:\n" + hpp_protected = "" + cpp += "\n" + + m = serv.method[0] + for m in serv.method: + func = m.name + inp = m.input_type[1:] + ret = m.output_type[1:] + is_void = ret == "void" + snake = camel_to_snake(inp) + on_func = f"on_{snake}" + needs_conn = get_opt(m, pb.needs_setup_connection, True) + needs_auth = get_opt(m, pb.needs_authentication, True) + + ifdef = ifdefs.get(inp, None) + + if ifdef is not None: + hpp += f"#ifdef {ifdef}\n" + hpp_protected += f"#ifdef {ifdef}\n" + cpp += f"#ifdef {ifdef}\n" + + hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" + hpp += f" virtual {ret} {func}(const {inp} &msg) = 0;\n" + cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" + body = "" + if needs_conn: + body += "if (!this->is_connection_setup()) {\n" + body += " this->on_no_setup_connection();\n" + body += " return;\n" + body += "}\n" + if needs_auth: + body += "if (!this->is_authenticated()) {\n" + body += " this->on_unauthenticated_access();\n" + body += " return;\n" + body += "}\n" + + if is_void: + body += f"this->{func}(msg);\n" + else: + body += f"{ret} ret = this->{func}(msg);\n" + ret_snake = camel_to_snake(ret) + body += f"if (!this->send_{ret_snake}(ret)) {{\n" + body += " this->on_fatal_error();\n" + body += "}\n" + cpp += indent(body) + "\n" + "}\n" + + if ifdef is not None: + hpp += "#endif\n" + hpp_protected += "#endif\n" + cpp += "#endif\n" + + hpp += " protected:\n" + hpp += hpp_protected + hpp += "};\n" + + hpp += """\ + + } // namespace api + } // namespace esphome + """ + cpp += """\ + + } // namespace api + } // namespace esphome + """ + + with open(root / "api_pb2_service.h", "w", encoding="utf-8") as f: + f.write(hpp) + + with open(root / "api_pb2_service.cpp", "w", encoding="utf-8") as f: + f.write(cpp) + + prot_file.unlink() + + try: + import clang_format + + def exec_clang_format(path): + clang_format_path = os.path.join( + os.path.dirname(clang_format.__file__), "data", "bin", "clang-format" + ) + call([clang_format_path, "-i", path]) + + exec_clang_format(root / "api_pb2_service.h") + exec_clang_format(root / "api_pb2_service.cpp") + exec_clang_format(root / "api_pb2.h") + exec_clang_format(root / "api_pb2.cpp") + except ImportError: + pass + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 2ee7521b91..db34ad7702 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 -from pathlib import Path -import sys import argparse from collections import defaultdict +from pathlib import Path +import sys -from esphome.helpers import write_file_if_changed from esphome.config import get_component, get_platform +from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK from esphome.core import CORE +from esphome.helpers import write_file_if_changed parser = argparse.ArgumentParser() parser.add_argument( @@ -27,7 +28,7 @@ BASE = """ # the integration's code owner is automatically notified. # Core Code -setup.py @esphome/core +pyproject.toml @esphome/core esphome/*.py @esphome/core esphome/core/* @esphome/core @@ -38,6 +39,7 @@ parts = [BASE] # Fake some directory so that get_component works CORE.config_path = str(root) +CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None} codeowners = defaultdict(list) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index dd8eccde93..8b2c28b06b 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -1,9 +1,10 @@ +import argparse +import glob import inspect import json -import argparse import os -import glob import re + import voluptuous as vol # NOTE: Cannot import other esphome components globally as a modification in vol_schema @@ -61,6 +62,7 @@ solve_registry = [] def get_component_names(): + # pylint: disable-next=redefined-outer-name,reimported from esphome.loader import CORE_COMPONENTS_PATH component_names = ["esphome", "sensor", "esp32", "esp8266"] @@ -82,18 +84,23 @@ def load_components(): components[domain] = get_component(domain) +# pylint: disable=wrong-import-position +from esphome.const import CONF_TYPE, KEY_CORE +from esphome.core import CORE + +# pylint: enable=wrong-import-position + +CORE.data[KEY_CORE] = {} load_components() # Import esphome after loading components (so schema is tracked) # pylint: disable=wrong-import-position -import esphome.core as esphome_core -import esphome.config_validation as cv -from esphome import automation -from esphome import pins +from esphome import automation, pins from esphome.components import remote_base -from esphome.const import CONF_TYPE -from esphome.loader import get_platform, CORE_COMPONENTS_PATH +import esphome.config_validation as cv +import esphome.core as esphome_core from esphome.helpers import write_file_if_changed +from esphome.loader import CORE_COMPONENTS_PATH, get_platform from esphome.util import Registry # pylint: enable=wrong-import-position @@ -111,7 +118,7 @@ def write_file(name, obj): def delete_extra_files(keep_names): for d in os.listdir(args.output_path): - if d.endswith(".json") and not d[:-5] in keep_names: + if d.endswith(".json") and d[:-5] not in keep_names: os.remove(os.path.join(args.output_path, d)) print(f"Deleted {d}") @@ -461,8 +468,10 @@ def merge(source, destination): def is_platform_schema(schema_name): # added mostly because of schema_name == "microphone.MICROPHONE_SCHEMA" + # and "alarm_control_panel" # which is shrunk because there is only one component of the schema (i2s_audio) - return schema_name == "microphone.MICROPHONE_SCHEMA" + component = schema_name.split(".")[0] + return component in components and components[component].is_platform_component def shrink(): @@ -530,6 +539,10 @@ def shrink(): elif not key_s: for target in paths: target_s = get_arr_path_schema(target) + if S_SCHEMA not in target_s: + # an empty schema like speaker.SPEAKER_SCHEMA + target_s[S_EXTENDS].remove(x) + continue assert target_s[S_SCHEMA][S_EXTENDS] == [x] target_s.pop(S_SCHEMA) target_s.pop(S_TYPE) # undefined @@ -543,11 +556,11 @@ def shrink(): s = f"{domain}.{schema_name}" if ( not s.endswith("." + S_CONFIG_SCHEMA) - and s not in referenced_schemas.keys() + and s not in referenced_schemas and not is_platform_schema(s) ): print(f"Removing {s}") - output[domain][S_SCHEMAS].pop(schema_name) + domain_schemas[S_SCHEMAS].pop(schema_name) def build_schema(): @@ -555,7 +568,7 @@ def build_schema(): # check esphome was not loaded globally (IDE auto imports) if len(ejs.extended_schemas) == 0: - raise Exception( + raise LookupError( "no data collected. Did you globally import an ESPHome component?" ) @@ -694,7 +707,7 @@ def convert(schema, config_var, path): if schema_instance is schema: assert S_CONFIG_VARS not in config_var assert S_EXTENDS not in config_var - if not S_TYPE in config_var: + if S_TYPE not in config_var: config_var[S_TYPE] = S_SCHEMA # assert config_var[S_TYPE] == S_SCHEMA @@ -756,9 +769,9 @@ def convert(schema, config_var, path): elif schema == automation.validate_potentially_and_condition: config_var[S_TYPE] = "registry" config_var["registry"] = "condition" - elif schema == cv.int_ or schema == cv.int_range: + elif schema in (cv.int_, cv.int_range): config_var[S_TYPE] = "integer" - elif schema == cv.string or schema == cv.string_strict or schema == cv.valid_name: + elif schema in (cv.string, cv.string_strict, cv.valid_name): config_var[S_TYPE] = "string" elif isinstance(schema, vol.Schema): @@ -770,6 +783,7 @@ def convert(schema, config_var, path): config_var |= pin_validators[repr_schema] config_var[S_TYPE] = "pin" + # pylint: disable-next=too-many-nested-blocks elif repr_schema in ejs.hidden_schemas: schema_type = ejs.hidden_schemas[repr_schema] @@ -835,9 +849,11 @@ def convert(schema, config_var, path): config_var["id_type"] = { "class": str(data.base), - "parents": [str(x.base) for x in parents] - if isinstance(parents, list) - else None, + "parents": ( + [str(x.base) for x in parents] + if isinstance(parents, list) + else None + ), } elif schema_type == "use_id": if inspect.ismodule(data): @@ -860,10 +876,13 @@ def convert(schema, config_var, path): config_var["use_id_type"] = str(data.base) config_var[S_TYPE] = "use_id" else: - raise Exception("Unknown extracted schema type") + raise TypeError("Unknown extracted schema type") elif config_var.get("key") == "GeneratedID": - if path == "i2c/CONFIG_SCHEMA/extL/all/id": - config_var["id_type"] = {"class": "i2c::I2CBus", "parents": ["Component"]} + if path.startswith("i2c/CONFIG_SCHEMA/") and path.endswith("/id"): + config_var["id_type"] = { + "class": "i2c::I2CBus", + "parents": ["Component"], + } elif path == "uart/CONFIG_SCHEMA/val 1/extL/all/id": config_var["id_type"] = { "class": "uart::UARTComponent", @@ -872,7 +891,7 @@ def convert(schema, config_var, path): elif path == "pins/esp32/val 1/id": config_var["id_type"] = "pin" else: - raise Exception("Cannot determine id_type for " + path) + raise TypeError("Cannot determine id_type for " + path) elif repr_schema in ejs.registry_schemas: solve_registry.append((ejs.registry_schemas[repr_schema], config_var)) @@ -936,11 +955,7 @@ def convert_keys(converted, schema, path): result["key"] = "GeneratedID" elif isinstance(k, cv.Required): result["key"] = "Required" - elif ( - isinstance(k, cv.Optional) - or isinstance(k, cv.Inclusive) - or isinstance(k, cv.Exclusive) - ): + elif isinstance(k, (cv.Optional, cv.Inclusive, cv.Exclusive)): result["key"] = "Optional" else: converted["key"] = "String" diff --git a/script/bump-version.py b/script/bump-version.py index 1f034344f9..8389d482b8 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 import argparse -import re -import subprocess from dataclasses import dataclass +import re import sys @@ -40,12 +39,12 @@ class Version: def sub(path, pattern, repl, expected_count=1): - with open(path) as fh: + with open(path, encoding="utf-8") as fh: content = fh.read() content, count = re.subn(pattern, repl, content, flags=re.MULTILINE) if expected_count is not None: assert count == expected_count, f"Pattern {pattern} replacement failed!" - with open(path, "wt") as fh: + with open(path, "w", encoding="utf-8") as fh: fh.write(content) diff --git a/script/ci-custom.py b/script/ci-custom.py index 44ed83f392..81e3da311a 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,10 +1,8 @@ #!/usr/bin/env python3 -from helpers import styled, print_error_for_file, git_ls_files, filter_changed import argparse import codecs import collections -import colorama import fnmatch import functools import os.path @@ -12,6 +10,9 @@ import re import sys import time +import colorama +from helpers import filter_changed, git_ls_files, print_error_for_file, styled + sys.path.append(os.path.dirname(__file__)) @@ -30,31 +31,6 @@ def find_all(a_str, sub): column += len(sub) -colorama.init() - -parser = argparse.ArgumentParser() -parser.add_argument( - "files", nargs="*", default=[], help="files to be processed (regex on path)" -) -parser.add_argument( - "-c", "--changed", action="store_true", help="Only run on changed files" -) -parser.add_argument( - "--print-slowest", action="store_true", help="Print the slowest checks" -) -args = parser.parse_args() - -EXECUTABLE_BIT = git_ls_files() -files = list(EXECUTABLE_BIT.keys()) -# Match against re -file_name_re = re.compile("|".join(args.files)) -files = [p for p in files if file_name_re.search(p)] - -if args.changed: - files = filter_changed(files) - -files.sort() - file_types = ( ".h", ".c", @@ -81,11 +57,36 @@ file_types = ( "", ) cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") -ignore_types = (".ico", ".png", ".woff", ".woff2", "") +py_include = ("*.py",) +ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf") LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] LINT_POST_CHECKS = [] +EXECUTABLE_BIT = {} + +errors = collections.defaultdict(list) + + +def add_errors(fname, errs): + if not isinstance(errs, list): + errs = [errs] + for err in errs: + if err is None: + continue + try: + lineno, col, msg = err + except ValueError: + lineno = 1 + col = 1 + msg = err + if not isinstance(msg, str): + raise ValueError("Error is not instance of string!") + if not isinstance(lineno, int): + raise ValueError("Line number is not an int!") + if not isinstance(col, int): + raise ValueError("Column number is not an int!") + errors[fname].append((lineno, col, msg)) def run_check(lint_obj, fname, *args): @@ -155,7 +156,7 @@ def lint_re_check(regex, **kwargs): def decorator(func): @functools.wraps(func) def new_func(fname, content): - errors = [] + errs = [] for match in prog.finditer(content): if "NOLINT" in match.group(0): continue @@ -165,8 +166,8 @@ def lint_re_check(regex, **kwargs): err = func(fname, match) if err is None: continue - errors.append((lineno, col + 1, err)) - return errors + errs.append((lineno, col + 1, err)) + return errs return decor(new_func) @@ -182,13 +183,13 @@ def lint_content_find_check(find, only_first=False, **kwargs): find_ = find if callable(find): find_ = find(fname, content) - errors = [] + errs = [] for line, col in find_all(content, find_): err = func(fname) - errors.append((line + 1, col + 1, err)) + errs.append((line + 1, col + 1, err)) if only_first: break - return errors + return errs return decor(new_func) @@ -228,15 +229,14 @@ def lint_ext_check(fname): "docker/ha-addon-rootfs/**", "docker/*.py", "script/*", - "setup.py", ] ) def lint_executable_bit(fname): ex = EXECUTABLE_BIT[fname] if ex != 100644: return ( - "File has invalid executable bit {}. If running from a windows machine please " - "see disabling executable bit in git.".format(ex) + f"File has invalid executable bit {ex}. If running from a windows machine please " + "see disabling executable bit in git." ) return None @@ -265,7 +265,8 @@ def lint_end_newline(fname, content): return None -CPP_RE_EOL = r"\s*?(?://.*?)?$" +CPP_RE_EOL = r".*?(?://.*?)?$" +PY_RE_EOL = r".*?(?:#.*?)?$" def highlight(s): @@ -273,7 +274,7 @@ def highlight(s): @lint_re_check( - r"^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)" + CPP_RE_EOL, + r"^#define\s+([a-zA-Z0-9_]+)\s+(0b[10]+|0x[0-9a-fA-F]+|\d+)\s*?(?:\/\/.*?)?$", include=cpp_include, exclude=[ "esphome/core/log.h", @@ -285,8 +286,8 @@ def lint_no_defines(fname, match): s = highlight(f"static const uint8_t {match.group(1)} = {match.group(2)};") return ( "#define macros for integer constants are not allowed, please use " - "{} style instead (replace uint8_t with the appropriate " - "datatype). See also Google style guide.".format(s) + f"{s} style instead (replace uint8_t with the appropriate " + "datatype). See also Google style guide." ) @@ -296,11 +297,11 @@ def lint_no_long_delays(fname, match): if duration_ms < 50: return None return ( - "{} - long calls to delay() are not allowed in ESPHome because everything executes " - "in one thread. Calling delay() will block the main thread and slow down ESPHome.\n" + f"{highlight(match.group(0).strip())} - long calls to delay() are not allowed " + "in ESPHome because everything executes in one thread. Calling delay() will " + "block the main thread and slow down ESPHome.\n" "If there's no way to work around the delay() and it doesn't execute often, please add " "a '// NOLINT' comment to the line." - "".format(highlight(match.group(0).strip())) ) @@ -311,28 +312,28 @@ def lint_const_ordered(fname, content): Reason: Otherwise people add it to the end, and then that results in merge conflicts. """ lines = content.splitlines() - errors = [] + errs = [] for start in ["CONF_", "ICON_", "UNIT_"]: matching = [ (i + 1, line) for i, line in enumerate(lines) if line.startswith(start) ] ordered = list(sorted(matching, key=lambda x: x[1].replace("_", " "))) ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)] - for (mi, ml), (oi, ol) in zip(matching, ordered): - if ml == ol: + for (mi, mline), (_, ol) in zip(matching, ordered): + if mline == ol: continue - target = next(i for i, l in ordered if l == ml) - target_text = next(l for i, l in matching if target == i) - errors.append( + target = next(i for i, line in ordered if line == mline) + target_text = next(line for i, line in matching if target == i) + errs.append( ( mi, 1, - f"Constant {highlight(ml)} is not ordered, please make sure all " + f"Constant {highlight(mline)} is not ordered, please make sure all " f"constants are ordered. See line {mi} (should go to line {target}, " f"{target_text})", ) ) - return errors + return errs @lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=["*.py"]) @@ -344,15 +345,14 @@ def lint_conf_matches(fname, match): if const_norm == value_norm: return None return ( - "Constant {} does not match value {}! Please make sure the constant's name matches its " - "value!" - "".format(highlight("CONF_" + const), highlight(value)) + f"Constant {highlight('CONF_' + const)} does not match value {highlight(value)}! " + "Please make sure the constant's name matches its value!" ) CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$' -with codecs.open("esphome/const.py", "r", encoding="utf-8") as f_handle: - constants_content = f_handle.read() +with codecs.open("esphome/const.py", "r", encoding="utf-8") as const_f_handle: + constants_content = const_f_handle.read() CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)] CONSTANTS_USES = collections.defaultdict(list) @@ -365,8 +365,8 @@ def lint_conf_from_const_py(fname, match): CONSTANTS_USES[name].append(fname) return None return ( - "Constant {} has already been defined in const.py - please import the constant from " - "const.py directly.".format(highlight(name)) + f"Constant {highlight(name)} has already been defined in const.py - " + "please import the constant from const.py directly." ) @@ -458,7 +458,7 @@ def lint_no_removed_in_idf_conversions(fname, match): @lint_re_check( - r"[^\w\d]byte\s+[\w\d]+\s*=", + r"[^\w\d]byte +[\w\d]+\s*=", include=cpp_include, exclude={ "esphome/components/tuya/tuya.h", @@ -473,16 +473,15 @@ def lint_no_byte_datatype(fname, match): @lint_post_check def lint_constants_usage(): - errors = [] + errs = [] for constant, uses in CONSTANTS_USES.items(): - if len(uses) < 4: + if len(uses) < 3: continue - errors.append( - "Constant {} is defined in {} files. Please move all definitions of the " - "constant to const.py (Uses: {})" - "".format(highlight(constant), len(uses), ", ".join(uses)) + errs.append( + f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " + f"constant to const.py (Uses: {', '.join(uses)})" ) - return errors + return errs def relative_cpp_search_text(fname, content): @@ -512,7 +511,10 @@ def relative_py_search_text(fname, content): @lint_content_find_check( relative_py_search_text, include=["esphome/components/*.py"], - exclude=["esphome/components/web_server/__init__.py"], + exclude=[ + "esphome/components/libretiny/generate_components.py", + "esphome/components/web_server/__init__.py", + ], ) def lint_relative_py_import(fname): return ( @@ -536,6 +538,7 @@ def lint_relative_py_import(fname): "esphome/components/esp32/core.cpp", "esphome/components/esp8266/core.cpp", "esphome/components/rp2040/core.cpp", + "esphome/components/libretiny/core.cpp", "esphome/components/host/core.cpp", ], ) @@ -549,7 +552,7 @@ def lint_namespace(fname, content): return ( "Invalid namespace found in C++ file. All integration C++ files should put all " "functions in a separate namespace that matches the integration's name. " - "Please make sure the file contains {}".format(highlight(search)) + f"Please make sure the file contains {highlight(search)}" ) @@ -572,11 +575,6 @@ def lint_pragma_once(fname, content): return None -@lint_re_check( - r"(whitelist|blacklist|slave)", - exclude=["script/ci-custom.py"], - flags=re.IGNORECASE | re.MULTILINE, -) def lint_inclusive_language(fname, match): # From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=49decddd39e5f6132ccd7d9fdc3d7c470b0061bb return ( @@ -594,6 +592,21 @@ def lint_inclusive_language(fname, match): ) +lint_re_check( + r"(whitelist|blacklist|slave)" + PY_RE_EOL, + include=py_include, + exclude=["script/ci-custom.py"], + flags=re.IGNORECASE | re.MULTILINE, +)(lint_inclusive_language) + + +lint_re_check( + r"(whitelist|blacklist|slave)" + CPP_RE_EOL, + include=cpp_include, + flags=re.IGNORECASE | re.MULTILINE, +)(lint_inclusive_language) + + @lint_re_check(r"[\t\r\f\v ]+$") def lint_trailing_whitespace(fname, match): return "Trailing whitespace detected" @@ -607,12 +620,17 @@ def lint_trailing_whitespace(fname, match): "esphome/components/button/button.h", "esphome/components/climate/climate.h", "esphome/components/cover/cover.h", - "esphome/components/display/display_buffer.h", + "esphome/components/datetime/date_entity.h", + "esphome/components/datetime/time_entity.h", + "esphome/components/datetime/datetime_entity.h", + "esphome/components/display/display.h", + "esphome/components/event/event.h", "esphome/components/fan/fan.h", "esphome/components/i2c/i2c.h", "esphome/components/lock/lock.h", "esphome/components/mqtt/mqtt_component.h", "esphome/components/number/number.h", + "esphome/components/one_wire/one_wire.h", "esphome/components/output/binary_output.h", "esphome/components/output/float_output.h", "esphome/components/nextion/nextion_base.h", @@ -620,7 +638,9 @@ def lint_trailing_whitespace(fname, match): "esphome/components/sensor/sensor.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", + "esphome/components/text/text.h", "esphome/components/text_sensor/text_sensor.h", + "esphome/components/valve/valve.h", "esphome/core/component.h", "esphome/core/gpio.h", "esphome/core/log.h", @@ -634,66 +654,73 @@ def lint_log_in_header(fname): ) -errors = collections.defaultdict(list) +def main(): + colorama.init() + parser = argparse.ArgumentParser() + parser.add_argument( + "files", nargs="*", default=[], help="files to be processed (regex on path)" + ) + parser.add_argument( + "-c", "--changed", action="store_true", help="Only run on changed files" + ) + parser.add_argument( + "--print-slowest", action="store_true", help="Print the slowest checks" + ) + args = parser.parse_args() -def add_errors(fname, errs): - if not isinstance(errs, list): - errs = [errs] - for err in errs: - if err is None: + global EXECUTABLE_BIT + EXECUTABLE_BIT = git_ls_files() + files = list(EXECUTABLE_BIT.keys()) + # Match against re + file_name_re = re.compile("|".join(args.files)) + files = [p for p in files if file_name_re.search(p)] + + if args.changed: + files = filter_changed(files) + + files.sort() + + for fname in files: + _, ext = os.path.splitext(fname) + run_checks(LINT_FILE_CHECKS, fname, fname) + if ext in ignore_types: continue try: - lineno, col, msg = err - except ValueError: - lineno = 1 - col = 1 - msg = err - if not isinstance(msg, str): - raise ValueError("Error is not instance of string!") - if not isinstance(lineno, int): - raise ValueError("Line number is not an int!") - if not isinstance(col, int): - raise ValueError("Column number is not an int!") - errors[fname].append((lineno, col, msg)) + with codecs.open(fname, "r", encoding="utf-8") as f_handle: + content = f_handle.read() + except UnicodeDecodeError: + add_errors( + fname, + "File is not readable as UTF-8. Please set your editor to UTF-8 mode.", + ) + continue + run_checks(LINT_CONTENT_CHECKS, fname, fname, content) + run_checks(LINT_POST_CHECKS, "POST") -for fname in files: - _, ext = os.path.splitext(fname) - run_checks(LINT_FILE_CHECKS, fname, fname) - if ext in ignore_types: - continue - try: - with codecs.open(fname, "r", encoding="utf-8") as f_handle: - content = f_handle.read() - except UnicodeDecodeError: - add_errors( - fname, - "File is not readable as UTF-8. Please set your editor to UTF-8 mode.", + for f, errs in sorted(errors.items()): + bold = functools.partial(styled, colorama.Style.BRIGHT) + bold_red = functools.partial(styled, (colorama.Style.BRIGHT, colorama.Fore.RED)) + err_str = ( + f"{bold(f'{f}:{lineno}:{col}:')} {bold_red('lint:')} {msg}\n" + for lineno, col, msg in errs ) - continue - run_checks(LINT_CONTENT_CHECKS, fname, fname, content) + print_error_for_file(f, "\n".join(err_str)) -run_checks(LINT_POST_CHECKS, "POST") + if args.print_slowest: + lint_times = [] + for lint in LINT_FILE_CHECKS + LINT_CONTENT_CHECKS + LINT_POST_CHECKS: + durations = lint.get("durations", []) + lint_times.append((sum(durations), len(durations), lint["func"].__name__)) + lint_times.sort(key=lambda x: -x[0]) + for i in range(min(len(lint_times), 10)): + dur, invocations, name = lint_times[i] + print(f" - '{name}' took {dur:.2f}s total (ran on {invocations} files)") + print(f"Total time measured: {sum(x[0] for x in lint_times):.2f}s") -for f, errs in sorted(errors.items()): - bold = functools.partial(styled, colorama.Style.BRIGHT) - bold_red = functools.partial(styled, (colorama.Style.BRIGHT, colorama.Fore.RED)) - err_str = ( - f"{bold(f'{f}:{lineno}:{col}:')} {bold_red('lint:')} {msg}\n" - for lineno, col, msg in errs - ) - print_error_for_file(f, "\n".join(err_str)) + return len(errors) -if args.print_slowest: - lint_times = [] - for lint in LINT_FILE_CHECKS + LINT_CONTENT_CHECKS + LINT_POST_CHECKS: - durations = lint.get("durations", []) - lint_times.append((sum(durations), len(durations), lint["func"].__name__)) - lint_times.sort(key=lambda x: -x[0]) - for i in range(min(len(lint_times), 10)): - dur, invocations, name = lint_times[i] - print(f" - '{name}' took {dur:.2f}s total (ran on {invocations} files)") - print(f"Total time measured: {sum(x[0] for x in lint_times):.2f}s") -sys.exit(len(errors)) +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/clang-format b/script/clang-format index 165fbd269f..d922c5b6f1 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,9 +1,6 @@ #!/usr/bin/env python3 -from helpers import print_error_for_file, get_output, git_ls_files, filter_changed import argparse -import click -import colorama import multiprocessing import os import queue @@ -12,12 +9,16 @@ import subprocess import sys import threading +import click +import colorama +from helpers import filter_changed, get_binary, git_ls_files, print_error_for_file -def run_format(args, queue, lock, failed_files): + +def run_format(executable, args, queue, lock, failed_files): """Takes filenames out of queue and runs clang-format on them.""" while True: path = queue.get() - invocation = ["clang-format-13"] + invocation = [executable] if args.inplace: invocation.append("-i") else: @@ -58,22 +59,6 @@ def main(): ) args = parser.parse_args() - try: - get_output("clang-format-13", "-version") - except: - print( - """ - Oops. It looks like clang-format is not installed. - - Please check you can run "clang-format-13 -version" in your terminal and install - clang-format (v13) if necessary. - - Note you can also upload your code as a pull request on GitHub and see the CI check - output to apply clang-format. - """ - ) - return 1 - files = [] for path in git_ls_files(["*.cpp", "*.h", "*.tcc"]): files.append(os.path.relpath(path, os.getcwd())) @@ -90,11 +75,12 @@ def main(): failed_files = [] try: + executable = get_binary("clang-format", 13) task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread( - target=run_format, args=(args, task_queue, lock, failed_files) + target=run_format, args=(executable, args, task_queue, lock, failed_files) ) t.daemon = True t.start() @@ -109,13 +95,18 @@ def main(): # Wait for all threads to be done. task_queue.join() + except FileNotFoundError as ex: + return 1 except KeyboardInterrupt: print() print("Ctrl-C detected, goodbye.") + # Kill subprocesses (and ourselves!) + # No simple, clean alternative appears to be available. os.kill(0, 9) + return 2 # Will not execute. - sys.exit(len(failed_files)) + return len(failed_files) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/script/clang-tidy b/script/clang-tidy index 5d2cba6eb5..61199edce3 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,20 +1,6 @@ #!/usr/bin/env python3 -from helpers import ( - print_error_for_file, - get_output, - filter_grep, - build_all_include, - temp_header_file, - git_ls_files, - filter_changed, - load_idedata, - root_path, - basepath, -) import argparse -import click -import colorama import multiprocessing import os import queue @@ -25,6 +11,21 @@ import sys import tempfile import threading +import click +import colorama +from helpers import ( + basepath, + build_all_include, + filter_changed, + filter_grep, + get_binary, + git_ls_files, + load_idedata, + print_error_for_file, + root_path, + temp_header_file, +) + def clang_options(idedata): cmd = [] @@ -72,7 +73,7 @@ def clang_options(idedata): # copy compiler flags, except those clang doesn't understand. cmd.extend( flag - for flag in idedata["cxx_flags"].split(" ") + for flag in idedata["cxx_flags"] if flag not in ( "-free", @@ -97,10 +98,12 @@ def clang_options(idedata): cmd.extend(["-isystem", directory]) # add library include directories using -isystem to suppress their errors - for directory in sorted(set(idedata["includes"]["build"])): + for directory in list(idedata["includes"]["build"]): # skip our own directories, we add those later - if not directory.startswith(f"{root_path}/") or directory.startswith( - f"{root_path}/.pio/" + if ( + not directory.startswith(f"{root_path}/") + or directory.startswith(f"{root_path}/.pio/") + or directory.startswith(f"{root_path}/managed_components/") ): cmd.extend(["-isystem", directory]) @@ -110,10 +113,13 @@ def clang_options(idedata): return cmd -def run_tidy(args, options, tmpdir, queue, lock, failed_files): +pids = set() + + +def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): while True: - path = queue.get() - invocation = ["clang-tidy-11"] + path = path_queue.get() + invocation = [executable] if tmpdir is not None: invocation.append("--export-fixes") @@ -134,17 +140,20 @@ def run_tidy(args, options, tmpdir, queue, lock, failed_files): invocation.append("--") invocation.extend(options) - proc = subprocess.run(invocation, capture_output=True, encoding="utf-8") + proc = subprocess.run( + invocation, capture_output=True, encoding="utf-8", check=False + ) if proc.returncode != 0: with lock: print_error_for_file(path, proc.stdout) failed_files.append(path) - queue.task_done() + path_queue.task_done() def progress_bar_show(value): if value is None: return "" + return None def split_list(a, n): @@ -193,22 +202,6 @@ def main(): ) args = parser.parse_args() - try: - get_output("clang-tidy-11", "-version") - except: - print( - """ - Oops. It looks like clang-tidy-11 is not installed. - - Please check you can run "clang-tidy-11 -version" in your terminal and install - clang-tidy (v11) if necessary. - - Note you can also upload your code as a pull request on GitHub and see the CI check - output to apply clang-tidy. - """ - ) - return 1 - idedata = load_idedata(args.environment) options = clang_options(idedata) @@ -242,12 +235,21 @@ def main(): failed_files = [] try: + executable = get_binary("clang-tidy", 14) task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread( target=run_tidy, - args=(args, options, tmpdir, task_queue, lock, failed_files), + args=( + executable, + args, + options, + tmpdir, + task_queue, + lock, + failed_files, + ), ) t.daemon = True t.start() @@ -255,30 +257,43 @@ def main(): # Fill the queue with files. with click.progressbar( files, width=30, file=sys.stderr, item_show_func=progress_bar_show - ) as bar: - for name in bar: + ) as progress_bar: + for name in progress_bar: task_queue.put(name) # Wait for all threads to be done. task_queue.join() + except FileNotFoundError: + return 1 except KeyboardInterrupt: print() print("Ctrl-C detected, goodbye.") if tmpdir: shutil.rmtree(tmpdir) + # Kill subprocesses (and ourselves!) + # No simple, clean alternative appears to be available. os.kill(0, 9) + return 2 # Will not execute. if args.fix and failed_files: print("Applying fixes ...") try: - subprocess.call(["clang-apply-replacements-11", tmpdir]) + try: + subprocess.call(["clang-apply-replacements-14", tmpdir]) + except FileNotFoundError: + subprocess.call(["clang-apply-replacements", tmpdir]) + except FileNotFoundError: + print( + "Error please install clang-apply-replacements-14 or clang-apply-replacements.\n", + file=sys.stderr, + ) except: print("Error applying fixes.\n", file=sys.stderr) raise - sys.exit(len(failed_files)) + return len(failed_files) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/script/devcontainer-post-create b/script/devcontainer-post-create index 120ab3307d..2d376786ac 100755 --- a/script/devcontainer-post-create +++ b/script/devcontainer-post-create @@ -1,8 +1,11 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # set -x +apt update +apt-get install avahi-utils -y + mkdir -p config script/setup diff --git a/script/extract_automations.py b/script/extract_automations.py new file mode 100755 index 0000000000..943eb7110a --- /dev/null +++ b/script/extract_automations.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +import json + +from helpers import git_ls_files + +from esphome.automation import ACTION_REGISTRY, CONDITION_REGISTRY +from esphome.pins import PIN_SCHEMA_REGISTRY + +list_components = __import__("list-components") + + +if __name__ == "__main__": + files = git_ls_files() + files = filter(list_components.filter_component_files, files) + + components = list_components.get_components(files, True) + + dump = { + "actions": sorted(list(ACTION_REGISTRY.keys())), + "conditions": sorted(list(CONDITION_REGISTRY.keys())), + "pin_providers": sorted(list(PIN_SCHEMA_REGISTRY.keys())), + } + + print(json.dumps(dump, indent=2)) diff --git a/script/fulltest b/script/fulltest index a605beebfe..6440401e97 100755 --- a/script/fulltest +++ b/script/fulltest @@ -12,3 +12,4 @@ script/lint-cpp script/unit_test script/component_test script/test +script/test_build_components diff --git a/script/helpers.py b/script/helpers.py index c042362aeb..6f36faaeb1 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,9 +1,10 @@ -import colorama +import json import os.path +from pathlib import Path import re import subprocess -import json -from pathlib import Path + +import colorama root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") @@ -44,7 +45,7 @@ def build_all_include(): content = "\n".join(headers) p = Path(temp_header_file) p.parent.mkdir(exist_ok=True) - p.write_text(content) + p.write_text(content, encoding="utf-8") def walk_files(path): @@ -54,14 +55,14 @@ def walk_files(path): def get_output(*args): - proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, err = proc.communicate() + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + output, _ = proc.communicate() return output.decode("utf-8") def get_err(*args): - proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, err = proc.communicate() + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + _, err = proc.communicate() return err.decode("utf-8") @@ -69,16 +70,16 @@ def splitlines_no_ends(string): return [s.strip() for s in string.splitlines()] -def changed_files(): +def changed_files(branch="dev"): check_remotes = ["upstream", "origin"] check_remotes.extend(splitlines_no_ends(get_output("git", "remote"))) for remote in check_remotes: - command = ["git", "merge-base", f"refs/remotes/{remote}/dev", "HEAD"] + command = ["git", "merge-base", f"refs/remotes/{remote}/{branch}", "HEAD"] try: merge_base = splitlines_no_ends(get_output(*command))[0] break # pylint: disable=bare-except - except: + except: # noqa: E722 pass else: raise ValueError("Git not configured") @@ -103,7 +104,7 @@ def filter_changed(files): def filter_grep(files, value): matched = [] for file in files: - with open(file) as handle: + with open(file, encoding="utf-8") as handle: contents = handle.read() if value in contents: matched.append(file) @@ -114,8 +115,8 @@ def git_ls_files(patterns=None): command = ["git", "ls-files", "-s"] if patterns is not None: command.extend(patterns) - proc = subprocess.Popen(command, stdout=subprocess.PIPE) - output, err = proc.communicate() + with subprocess.Popen(command, stdout=subprocess.PIPE) as proc: + output, _ = proc.communicate() lines = [x.split() for x in output.decode("utf-8").splitlines()] return {s[3].strip(): int(s[0]) for s in lines} @@ -152,3 +153,38 @@ def load_idedata(environment): temp_idedata.write_text(json.dumps(data, indent=2) + "\n") return data + + +def get_binary(name: str, version: str) -> str: + binary_file = f"{name}-{version}" + try: + result = subprocess.check_output([binary_file, "-version"]) + return binary_file + except FileNotFoundError: + pass + binary_file = name + try: + result = subprocess.run( + [binary_file, "-version"], text=True, capture_output=True, check=False + ) + if result.returncode == 0 and (f"version {version}") in result.stdout: + return binary_file + raise FileNotFoundError(f"{name} not found") + + except FileNotFoundError: + print( + f""" + Oops. It looks like {name} is not installed. It should be available under venv/bin + and in PATH after running in turn: + script/setup + source venv/bin/activate. + + Please confirm you can run "{name} -version" or "{name}-{version} -version" + in your terminal and install + {name} (v{version}) if necessary. + + Note you can also upload your code as a pull request on GitHub and see the CI check + output to apply {name} + """ + ) + raise diff --git a/script/lint-python b/script/lint-python index 7de1de80b0..01e5e76190 100755 --- a/script/lint-python +++ b/script/lint-python @@ -1,19 +1,20 @@ #!/usr/bin/env python3 -from helpers import ( - styled, - print_error_for_file, - get_output, - get_err, - git_ls_files, - filter_changed, -) import argparse -import colorama import os import re import sys +import colorama +from helpers import ( + filter_changed, + get_err, + get_output, + git_ls_files, + print_error_for_file, + styled, +) + curfile = None diff --git a/script/list-components.py b/script/list-components.py new file mode 100755 index 0000000000..0d4777436b --- /dev/null +++ b/script/list-components.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +import argparse +from pathlib import Path +import sys + +from helpers import changed_files, git_ls_files + +from esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_ESP32, + PLATFORM_ESP8266, +) +from esphome.core import CORE +from esphome.loader import get_component, get_platform + + +def filter_component_files(str): + return str.startswith("esphome/components/") | str.startswith("tests/components/") + + +def extract_component_names_array_from_files_array(files): + components = [] + for file in files: + file_parts = file.split("/") + if len(file_parts) >= 4: + component_name = file_parts[2] + if component_name not in components: + components.append(component_name) + return components + + +def add_item_to_components_graph(components_graph, parent, child): + if not parent.startswith("__") and parent != child: + if parent not in components_graph: + components_graph[parent] = [] + if child not in components_graph[parent]: + components_graph[parent].append(child) + + +def create_components_graph(): + # The root directory of the repo + root = Path(__file__).parent.parent + components_dir = root / "esphome" / "components" + # Fake some directory so that get_component works + CORE.config_path = str(root) + # Various configuration to capture different outcomes used by `AUTO_LOAD` function. + TARGET_CONFIGURATIONS = [ + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: "arduino", KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: "esp-idf", KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP32}, + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP8266}, + ] + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + components_graph = {} + + for path in components_dir.iterdir(): + if not path.is_dir(): + continue + if not (path / "__init__.py").is_file(): + continue + name = path.name + comp = get_component(name) + if comp is None: + print( + f"Cannot find component {name}. Make sure current path is pip installed ESPHome" + ) + sys.exit(1) + + for dependency in comp.dependencies: + add_item_to_components_graph( + components_graph, dependency.split(".")[0], name + ) + + for target_config in TARGET_CONFIGURATIONS: + CORE.data[KEY_CORE] = target_config + for auto_load in comp.auto_load: + add_item_to_components_graph(components_graph, auto_load, name) + # restore config + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + for platform_path in path.iterdir(): + platform_name = platform_path.stem + platform = get_platform(platform_name, name) + if platform is None: + continue + + add_item_to_components_graph(components_graph, platform_name, name) + + for dependency in platform.dependencies: + add_item_to_components_graph( + components_graph, dependency.split(".")[0], name + ) + + for target_config in TARGET_CONFIGURATIONS: + CORE.data[KEY_CORE] = target_config + for auto_load in platform.auto_load: + add_item_to_components_graph(components_graph, auto_load, name) + # restore config + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + return components_graph + + +def find_children_of_component(components_graph, component_name, depth=0): + if component_name not in components_graph: + return [] + + children = [] + + for child in components_graph[component_name]: + children.append(child) + if depth < 10: + children.extend( + find_children_of_component(components_graph, child, depth + 1) + ) + # Remove duplicate values + return list(set(children)) + + +def get_components(files: list[str], get_dependencies: bool = False): + components = extract_component_names_array_from_files_array(files) + + if get_dependencies: + components_graph = create_components_graph() + + all_components = components.copy() + for c in components: + all_components.extend(find_children_of_component(components_graph, c)) + # Remove duplicate values + all_changed_components = list(set(all_components)) + + return sorted(all_changed_components) + + return sorted(components) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-c", + "--changed", + action="store_true", + help="List all components required for testing based on changes", + ) + parser.add_argument( + "-b", "--branch", help="Branch to compare changed files against" + ) + args = parser.parse_args() + + if args.branch and not args.changed: + parser.error("--branch requires --changed") + + files = git_ls_files() + files = filter(filter_component_files, files) + + if args.changed: + if args.branch: + changed = changed_files(args.branch) + else: + changed = changed_files() + # If any base test file(s) changed, there's no need to filter out components + if not any("tests/test_build_components" in file for file in changed): + files = [f for f in files if f in changed] + + for c in get_components(files, args.changed): + print(c) + + +if __name__ == "__main__": + main() diff --git a/script/quicklint b/script/quicklint index a4fae98195..84e4c97667 100755 --- a/script/quicklint +++ b/script/quicklint @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e diff --git a/script/run-in-env.sh b/script/run-in-env.sh new file mode 100755 index 0000000000..2e05fe1d17 --- /dev/null +++ b/script/run-in-env.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh +set -eu + +my_path=$(git rev-parse --show-toplevel) + +for venv in venv .venv .; do + if [ -f "${my_path}/${venv}/bin/activate" ]; then + . "${my_path}/${venv}/bin/activate" + break + fi +done + +exec "$@" diff --git a/script/setup b/script/setup index 656e95eba6..824840c392 100755 --- a/script/setup +++ b/script/setup @@ -1,18 +1,26 @@ -#!/bin/bash +#!/usr/bin/env bash # Set up ESPHome dev environment set -e cd "$(dirname "$0")/.." - -if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ]; then +location="venv/bin/activate" +if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ] && [ ! "$ESPHOME_NO_VENV" ]; then python3 -m venv venv - source venv/bin/activate + if [ -f venv/Scripts/activate ]; then + location="venv/Scripts/activate" + fi + source $location fi -pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -pip3 install --no-use-pep517 -e . +pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -r requirements_dev.txt +pip3 install setuptools wheel +pip3 install -e ".[dev,test,displays]" --config-settings editable_mode=compat pre-commit install script/platformio_install_deps.py platformio.ini --libraries --tools --platforms + +echo +echo +echo "Virtual environment created. Run 'source $location' to use it." diff --git a/script/setup.bat b/script/setup.bat new file mode 100644 index 0000000000..0b49768139 --- /dev/null +++ b/script/setup.bat @@ -0,0 +1,28 @@ +@echo off + +if defined DEVCONTAINER goto :install +if defined VIRTUAL_ENV goto :install +if defined ESPHOME_NO_VENV goto :install + +echo Starting the Virtual Environment +python -m venv venv +call venv/Scripts/activate +echo Running the Virtual Environment + +:install + +echo Installing required packages... + +python.exe -m pip install --upgrade pip + +pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -r requirements_dev.txt +pip3 install setuptools wheel +pip3 install -e ".[dev,test,displays]" --config-settings editable_mode=compat + +pre-commit install + +python script/platformio_install_deps.py platformio.ini --libraries --tools --platforms + +echo . +echo . +echo Virtual environment created. Run 'venv/Scripts/activate' to use it. diff --git a/script/sync-device_class.py b/script/sync-device_class.py index ae6f4be0c8..121c89b8f9 100755 --- a/script/sync-device_class.py +++ b/script/sync-device_class.py @@ -2,12 +2,18 @@ import re +# pylint: disable=import-error from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass from homeassistant.components.cover import CoverDeviceClass +from homeassistant.components.event import EventDeviceClass from homeassistant.components.number import NumberDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass +from homeassistant.components.update import UpdateDeviceClass +from homeassistant.components.valve import ValveDeviceClass + +# pylint: enable=import-error BLOCKLIST = ( # requires special support on HA side @@ -18,17 +24,20 @@ DOMAINS = { "binary_sensor": BinarySensorDeviceClass, "button": ButtonDeviceClass, "cover": CoverDeviceClass, + "event": EventDeviceClass, "number": NumberDeviceClass, "sensor": SensorDeviceClass, "switch": SwitchDeviceClass, + "update": UpdateDeviceClass, + "valve": ValveDeviceClass, } def sub(path, pattern, repl): - with open(path) as handle: + with open(path, encoding="utf-8") as handle: content = handle.read() content = re.sub(pattern, repl, content, flags=re.MULTILINE) - with open(path, "w") as handle: + with open(path, "w", encoding="utf-8") as handle: handle.write(content) diff --git a/script/test b/script/test index 36be9118ed..e227c17f9f 100755 --- a/script/test +++ b/script/test @@ -6,9 +6,6 @@ cd "$(dirname "$0")/.." set -x -esphome compile tests/test1.yaml -esphome compile tests/test2.yaml -esphome compile tests/test3.yaml -esphome compile tests/test4.yaml -esphome compile tests/test5.yaml -esphome compile tests/test8.yaml +for f in ./tests/test*.yaml; do + esphome compile $f +done diff --git a/script/test_build_components b/script/test_build_components new file mode 100755 index 0000000000..62fe0f1b55 --- /dev/null +++ b/script/test_build_components @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +set -e + +help() { + echo "Usage: $0 [-e ] [-c ] [-t ]" 1>&2 + echo 1>&2 + echo " - e - Parameter for esphome command. Default compile. Common alternative is config." 1>&2 + echo " - c - Component folder name to test. Default *. E.g. '-c logger'." 1>&2 + echo " - t - Target name to test. Put '-t list' to display all possibilities. E.g. '-t esp32-s2-idf-51'." 1>&2 + exit 1 +} + +# Parse parameter: +# - `e` - Parameter for `esphome` command. Default `compile`. Common alternative is `config`. +# - `c` - Component folder name to test. Default `*`. +esphome_command="compile" +target_component="*" +while getopts e:c:t: flag +do + case $flag in + e) esphome_command=${OPTARG};; + c) target_component=${OPTARG};; + t) requested_target_platform=${OPTARG};; + \?) help;; + esac +done + +cd "$(dirname "$0")/.." + +if ! [ -d "./tests/test_build_components/build" ]; then + mkdir ./tests/test_build_components/build +fi + +start_esphome() { + if [ -n "$requested_target_platform" ] && [ "$requested_target_platform" != "$target_platform_with_version" ]; then + echo "Skipping $target_platform_with_version" + return + fi + # create dynamic yaml file in `build` folder. + # `./tests/test_build_components/build/[target_component].[test_name].[target_platform_with_version].yaml` + component_test_file="./tests/test_build_components/build/$target_component.$test_name.$target_platform_with_version.yaml" + + cp $target_platform_file $component_test_file + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS sed is...different + sed -i '' "s!\$component_test_file!../../.$f!g" $component_test_file + else + sed -i "s!\$component_test_file!../../.$f!g" $component_test_file + fi + + # Start esphome process + echo "> [$target_component] [$test_name] [$target_platform_with_version]" + set -x + # TODO: Validate escape of Command line substitution value + python -m esphome -s component_name $target_component -s component_dir ../../components/$target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file + { set +x; } 2>/dev/null +} + +# Find all test yaml files. +# - `./tests/components/[target_component]/[test_name].[target_platform].yaml` +# - `./tests/components/[target_component]/[test_name].all.yaml` +for f in ./tests/components/$target_component/*.*.yaml; do + [ -f "$f" ] || continue + IFS='/' read -r -a folder_name <<< "$f" + target_component="${folder_name[3]}" + + IFS='.' read -r -a file_name <<< "${folder_name[4]}" + test_name="${file_name[0]}" + target_platform="${file_name[1]}" + file_name_parts=${#file_name[@]} + + if [ "$target_platform" = "all" ] || [ $file_name_parts = 2 ]; then + # Test has *not* defined a specific target platform. Need to run tests for all possible target platforms. + + for target_platform_file in ./tests/test_build_components/build_components_base.*.yaml; do + IFS='/' read -r -a folder_name <<< "$target_platform_file" + IFS='.' read -r -a file_name <<< "${folder_name[3]}" + target_platform="${file_name[1]}" + + start_esphome + done + + else + # Test has defined a specific target platform. + + # Validate we have a base test yaml for selected platform. + # The target_platform is sourced from the following location. + # 1. `./tests/test_build_components/build_components_base.[target_platform].yaml` + # 2. `./tests/test_build_components/build_components_base.[target_platform]-ard.yaml` + target_platform_file="./tests/test_build_components/build_components_base.$target_platform.yaml" + if ! [ -f "$target_platform_file" ]; then + echo "No base test file [./tests/test_build_components/build_components_base.$target_platform.yaml] for component test [$f] found." + exit 1 + fi + + for target_platform_file in ./tests/test_build_components/build_components_base.$target_platform*.yaml; do + # trim off "./tests/test_build_components/build_components_base." prefix + target_platform_with_version=${target_platform_file:52} + # ...now remove suffix starting with "." leaving just the test target hardware and software platform (possibly with version) + # For example: "esp32-s3-idf-50" + target_platform_with_version=${target_platform_with_version%.*} + start_esphome + done + fi +done diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 755cef47c0..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,62 +0,0 @@ -[metadata] -license = MIT -license_file = LICENSE -platforms = any -description = Make creating custom firmwares for ESP32/ESP8266 super easy. -long_description = file: README.md -keywords = home, automation -classifier = - Environment :: Console - Intended Audience :: Developers - Intended Audience :: End Users/Desktop - License :: OSI Approved :: MIT License - Programming Language :: C++ - Programming Language :: Python :: 3 - Topic :: Home Automation -Topic :: Home Automation - -[flake8] -max-line-length = 120 -# Following 4 for black compatibility -# E501: line too long -# W503: Line break occurred before a binary operator -# E203: Whitespace before ':' -# D202 No blank lines allowed after function docstring - -# TODO fix flake8 -# D100 Missing docstring in public module -# D101 Missing docstring in public class -# D102 Missing docstring in public method -# D103 Missing docstring in public function -# D104 Missing docstring in public package -# D105 Missing docstring in magic method -# D107 Missing docstring in __init__ -# D200 One-line docstring should fit on one line with quotes -# D205 1 blank line required between summary line and description -# D209 Multi-line docstring closing quotes should be on a separate line -# D400 First line should end with a period -# D401 First line should be in imperative mood - -ignore = - E501, - W503, - E203, - D202, - - D100, - D101, - D102, - D103, - D104, - D105, - D107, - D200, - D205, - D209, - D400, - D401, - -exclude = api_pb2.py - -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100755 index 95453960ff..0000000000 --- a/setup.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -"""esphome setup script.""" -import os - -from setuptools import setup, find_packages - -from esphome import const - -PROJECT_NAME = "esphome" -PROJECT_PACKAGE_NAME = "esphome" -PROJECT_LICENSE = "MIT" -PROJECT_AUTHOR = "ESPHome" -PROJECT_COPYRIGHT = "2019, ESPHome" -PROJECT_URL = "https://esphome.io/" -PROJECT_EMAIL = "esphome@nabucasa.com" - -PROJECT_GITHUB_USERNAME = "esphome" -PROJECT_GITHUB_REPOSITORY = "esphome" - -PYPI_URL = f"https://pypi.python.org/pypi/{PROJECT_PACKAGE_NAME}" -GITHUB_PATH = f"{PROJECT_GITHUB_USERNAME}/{PROJECT_GITHUB_REPOSITORY}" -GITHUB_URL = f"https://github.com/{GITHUB_PATH}" - -DOWNLOAD_URL = f"{GITHUB_URL}/archive/{const.__version__}.zip" - -here = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(here, "requirements.txt")) as requirements_txt: - REQUIRES = requirements_txt.read().splitlines() - -with open(os.path.join(here, "README.md")) as readme: - LONG_DESCRIPTION = readme.read() - -# If you have problems importing platformio and esptool as modules you can set -# $ESPHOME_USE_SUBPROCESS to make ESPHome call their executables instead. -# This means they have to be in your $PATH. -if "ESPHOME_USE_SUBPROCESS" in os.environ: - # Remove platformio and esptool from requirements - REQUIRES = [ - req - for req in REQUIRES - if not any(req.startswith(prefix) for prefix in ["platformio", "esptool"]) - ] - -CLASSIFIERS = [ - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: MIT License", - "Programming Language :: C++", - "Programming Language :: Python :: 3", - "Topic :: Home Automation", -] - -setup( - name=PROJECT_PACKAGE_NAME, - version=const.__version__, - license=PROJECT_LICENSE, - url=GITHUB_URL, - project_urls={ - "Bug Tracker": "https://github.com/esphome/issues/issues", - "Feature Request Tracker": "https://github.com/esphome/feature-requests/issues", - "Source Code": "https://github.com/esphome/esphome", - "Documentation": "https://esphome.io", - "Twitter": "https://twitter.com/esphome_", - }, - download_url=DOWNLOAD_URL, - author=PROJECT_AUTHOR, - author_email=PROJECT_EMAIL, - description="Make creating custom firmwares for ESP32/ESP8266 super easy.", - long_description=LONG_DESCRIPTION, - long_description_content_type="text/markdown", - include_package_data=True, - zip_safe=False, - platforms="any", - test_suite="tests", - python_requires=">=3.9.0", - install_requires=REQUIRES, - keywords=["home", "automation"], - entry_points={"console_scripts": ["esphome = esphome.__main__:main"]}, - packages=find_packages(include="esphome.*"), -) diff --git a/tests/README.md b/tests/README.md index 6d83fc6886..5b312d00de 100644 --- a/tests/README.md +++ b/tests/README.md @@ -27,3 +27,4 @@ Current test_.yaml file contents. | test6.yaml | RP2040 | wifi | N/A | test7.yaml | ESP32-C3 | wifi | N/A | test8.yaml | ESP32-S3 | wifi | None +| test10.yaml | ESP32 | wifi | None diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 0e24d78f5c..01cf55872c 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -20,7 +20,7 @@ from esphome.const import ( CONF_WIFI, ) from esphome.components.packages import do_packages_pass -from esphome.config_helpers import Extend +from esphome.config_helpers import Extend, Remove import esphome.config_validation as cv # Test strings @@ -349,3 +349,165 @@ def test_package_merge_by_missing_id(): actual = do_packages_pass(config) assert actual == expected + + +def test_package_list_remove_by_id(): + """ + Ensures that components with matching IDs are removed correctly. + + In this test, two sensors are defined in a package, and one of them is removed at the top level. + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + }, + # "package2": { + # CONF_SENSOR: [ + # { + # CONF_ID: Remove(TEST_SENSOR_ID_1), + # } + # ], + # }, + }, + CONF_SENSOR: [ + { + CONF_ID: Remove(TEST_SENSOR_ID_1), + }, + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_multiple_package_list_remove_by_id(): + """ + Ensures that components with matching IDs are removed correctly. + + In this test, two sensors are defined in a package, and one of them is removed in another package. + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + }, + "package2": { + CONF_SENSOR: [ + { + CONF_ID: Remove(TEST_SENSOR_ID_1), + } + ], + }, + }, + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_dict_remove_by_id(basic_wifi, basic_esphome): + """ + 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. + + In this test, CONF_SSID should be overwritten by that defined in the top-level config. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + CONF_WIFI: Remove(), + } + + expected = { + CONF_ESPHOME: basic_esphome, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_remove_by_missing_id(): + """ + Ensures that components with missing IDs are not merged. + """ + + config = { + CONF_PACKAGES: { + "sensors": { + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]}, + ] + } + }, + "missing_key": Remove(), + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 10.0}]}, + {CONF_ID: Remove(TEST_SENSOR_ID_2), CONF_FILTERS: [{CONF_OFFSET: 146.0}]}, + ], + } + + expected = { + "missing_key": Remove(), + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 42.0}], + }, + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 10.0}], + }, + { + CONF_ID: Remove(TEST_SENSOR_ID_2), + CONF_FILTERS: [{CONF_OFFSET: 146.0}], + }, + ], + } + + actual = do_packages_pass(config) + assert actual == expected diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py new file mode 100644 index 0000000000..51fcb3d382 --- /dev/null +++ b/tests/component_tests/text/test_text.py @@ -0,0 +1,70 @@ +"""Tests for the binary sensor component.""" + + +def test_text_is_setup(generate_main): + """ + When the binary sensor is set in the yaml file, it should be registered in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + + # Then + assert "new template_::TemplateText();" in main_cpp + assert "App.register_text" in main_cpp + + +def test_text_sets_mandatory_fields(generate_main): + """ + When the mandatory fields are set in the yaml, they should be set in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + + # Then + assert 'it_1->set_name("test 1 text");' in main_cpp + + +def test_text_config_value_internal_set(generate_main): + """ + Test that the "internal" config value is correctly set + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + + # Then + assert "it_2->set_internal(false);" in main_cpp + assert "it_3->set_internal(true);" in main_cpp + + +def test_text_config_value_mode_set(generate_main): + """ + Test that the "internal" config value is correctly set + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + + # Then + assert "it_1->traits.set_mode(text::TEXT_MODE_TEXT);" in main_cpp + assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp + + +def test_text_config_lamda_is_set(generate_main): + """ + Test if lambda is set for lambda mode + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + + # Then + assert "it_4->set_template([=]() -> optional {" in main_cpp + assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/component_tests/text/test_text.yaml b/tests/component_tests/text/test_text.yaml new file mode 100644 index 0000000000..d81c909f9d --- /dev/null +++ b/tests/component_tests/text/test_text.yaml @@ -0,0 +1,45 @@ +esphome: + name: test + +esp32: + board: esp32dev + +text: + - platform: template + name: "test 1 text" + id: "it_1" + optimistic: true + mode: text + + - platform: template + name: "test 2 text" + id: "it_2" + icon: "mdi:text" + optimistic: true + min_length: 5 + max_length: 255 + internal: false + initial_value: "Welcome ESPHOME" + restore_value: true + mode: text + + - platform: template + name: "test 3 key" + id: "it_3" + icon: "mdi:text" + mode: "password" + optimistic: true + internal: true + max_length: 255 + + - platform: template + name: "test 4 key" + id: "it_4" + mode: text + set_action: + - then: + - logger.log: + format: Template text set to %s + args: ["x.c_str()"] + lambda: | + return std::string{"Hello"}; diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py new file mode 100644 index 0000000000..1c4ef6633d --- /dev/null +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -0,0 +1,58 @@ +"""Tests for the text sensor component.""" + + +def test_text_sensor_is_setup(generate_main): + """ + When the text is set in the yaml file, it should be registered in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") + + # Then + assert "new template_::TemplateTextSensor();" in main_cpp + assert "App.register_text_sensor" in main_cpp + + +def test_text_sensor_sets_mandatory_fields(generate_main): + """ + When the mandatory fields are set in the yaml, they should be set in main + """ + # Given + + # When + 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 + + +def test_text_sensor_config_value_internal_set(generate_main): + """ + Test that the "internal" config value is correctly set + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") + + # Then + assert "ts_2->set_internal(true);" in main_cpp + assert "ts_3->set_internal(false);" in main_cpp + + +def test_text_sensor_device_class_set(generate_main): + """ + When the device_class of text_sensor is set in the yaml file, it should be registered in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") + + # Then + assert 'ts_2->set_device_class("timestamp");' in main_cpp + assert 'ts_3->set_device_class("date");' in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.yaml b/tests/component_tests/text_sensor/test_text_sensor.yaml new file mode 100644 index 0000000000..b426cb102c --- /dev/null +++ b/tests/component_tests/text_sensor/test_text_sensor.yaml @@ -0,0 +1,26 @@ +--- +esphome: + name: test + platform: ESP8266 + board: d1_mini_lite + +text_sensor: + - platform: template + id: ts_1 + name: "Template Text Sensor 1" + lambda: |- + return {"Hello World"}; + - platform: template + id: ts_2 + name: "Template Text Sensor 2" + lambda: |- + return {"2023-06-22T18:43:52+00:00"}; + device_class: timestamp + internal: true + - platform: template + id: ts_3 + name: "Template Text Sensor 3" + lambda: |- + return {"2023-06-22T18:43:52+00:00"}; + device_class: date + internal: false diff --git a/tests/components/a01nyub/common.yaml b/tests/components/a01nyub/common.yaml new file mode 100644 index 0000000000..0717acfff7 --- /dev/null +++ b/tests/components/a01nyub/common.yaml @@ -0,0 +1,11 @@ +uart: + - id: uart_a01nyub + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + +sensor: + - platform: a01nyub + id: a01nyub_sensor + name: a01nyub Distance + uart_id: uart_a01nyub diff --git a/tests/components/a01nyub/test.esp32-ard.yaml b/tests/components/a01nyub/test.esp32-ard.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/a01nyub/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/a01nyub/test.esp32-c3-ard.yaml b/tests/components/a01nyub/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/a01nyub/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/a01nyub/test.esp32-c3-idf.yaml b/tests/components/a01nyub/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/a01nyub/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/a01nyub/test.esp32-idf.yaml b/tests/components/a01nyub/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/a01nyub/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/a01nyub/test.esp8266-ard.yaml b/tests/components/a01nyub/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/a01nyub/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/a01nyub/test.rp2040-ard.yaml b/tests/components/a01nyub/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/a01nyub/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/a02yyuw/test.esp32-ard.yaml b/tests/components/a02yyuw/test.esp32-ard.yaml new file mode 100644 index 0000000000..98d6a266b3 --- /dev/null +++ b/tests/components/a02yyuw/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp32-c3-ard.yaml b/tests/components/a02yyuw/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..76e1ad8ee1 --- /dev/null +++ b/tests/components/a02yyuw/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp32-c3-idf.yaml b/tests/components/a02yyuw/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..76e1ad8ee1 --- /dev/null +++ b/tests/components/a02yyuw/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp32-idf.yaml b/tests/components/a02yyuw/test.esp32-idf.yaml new file mode 100644 index 0000000000..98d6a266b3 --- /dev/null +++ b/tests/components/a02yyuw/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp8266-ard.yaml b/tests/components/a02yyuw/test.esp8266-ard.yaml new file mode 100644 index 0000000000..76e1ad8ee1 --- /dev/null +++ b/tests/components/a02yyuw/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.rp2040-ard.yaml b/tests/components/a02yyuw/test.rp2040-ard.yaml new file mode 100644 index 0000000000..76e1ad8ee1 --- /dev/null +++ b/tests/components/a02yyuw/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_a02yyuw + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: a02yyuw + id: a02yyuw_sensor + name: a02yyuw Distance + uart_id: uart_a02yyuw diff --git a/tests/components/a4988/test.esp32-ard.yaml b/tests/components/a4988/test.esp32-ard.yaml new file mode 100644 index 0000000000..0ca5e3f504 --- /dev/null +++ b/tests/components/a4988/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 22 + dir_pin: + number: 23 + sleep_pin: + number: 25 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.esp32-c3-ard.yaml b/tests/components/a4988/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..af4e4fa32b --- /dev/null +++ b/tests/components/a4988/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 2 + dir_pin: + number: 3 + sleep_pin: + number: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.esp32-c3-idf.yaml b/tests/components/a4988/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..af4e4fa32b --- /dev/null +++ b/tests/components/a4988/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 2 + dir_pin: + number: 3 + sleep_pin: + number: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.esp32-idf.yaml b/tests/components/a4988/test.esp32-idf.yaml new file mode 100644 index 0000000000..0ca5e3f504 --- /dev/null +++ b/tests/components/a4988/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 22 + dir_pin: + number: 23 + sleep_pin: + number: 25 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.esp8266-ard.yaml b/tests/components/a4988/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f4c1886fc5 --- /dev/null +++ b/tests/components/a4988/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 1 + dir_pin: + number: 2 + sleep_pin: + number: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/a4988/test.rp2040-ard.yaml b/tests/components/a4988/test.rp2040-ard.yaml new file mode 100644 index 0000000000..af4e4fa32b --- /dev/null +++ b/tests/components/a4988/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +stepper: + - platform: a4988 + id: a4988_stepper + step_pin: + number: 2 + dir_pin: + number: 3 + sleep_pin: + number: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/absolute_humidity/common.yaml b/tests/components/absolute_humidity/common.yaml new file mode 100644 index 0000000000..87a99f5206 --- /dev/null +++ b/tests/components/absolute_humidity/common.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: absolute_humidity + name: Absolute Humidity + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } diff --git a/tests/components/absolute_humidity/test.esp32-ard.yaml b/tests/components/absolute_humidity/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/absolute_humidity/test.esp32-c3-ard.yaml b/tests/components/absolute_humidity/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/absolute_humidity/test.esp32-c3-idf.yaml b/tests/components/absolute_humidity/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/absolute_humidity/test.esp32-idf.yaml b/tests/components/absolute_humidity/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/absolute_humidity/test.esp8266-ard.yaml b/tests/components/absolute_humidity/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/absolute_humidity/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/absolute_humidity/test.rp2040-ard.yaml b/tests/components/absolute_humidity/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/absolute_humidity/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ac_dimmer/test.esp32-ard.yaml b/tests/components/ac_dimmer/test.esp32-ard.yaml new file mode 100644 index 0000000000..cc17201666 --- /dev/null +++ b/tests/components/ac_dimmer/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +output: + - platform: ac_dimmer + id: ac_dimmer_1 + gate_pin: + number: 12 + zero_cross_pin: + number: 13 diff --git a/tests/components/ac_dimmer/test.esp32-c3-ard.yaml b/tests/components/ac_dimmer/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f411d376be --- /dev/null +++ b/tests/components/ac_dimmer/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +output: + - platform: ac_dimmer + id: ac_dimmer_1 + gate_pin: + number: 5 + zero_cross_pin: + number: 6 diff --git a/tests/components/ac_dimmer/test.esp8266-ard.yaml b/tests/components/ac_dimmer/test.esp8266-ard.yaml new file mode 100644 index 0000000000..af18d11c5f --- /dev/null +++ b/tests/components/ac_dimmer/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +output: + - platform: ac_dimmer + id: ac_dimmer_1 + gate_pin: + number: 5 + zero_cross_pin: + number: 4 diff --git a/tests/components/ac_dimmer/test.rp2040-ard.yaml b/tests/components/ac_dimmer/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f411d376be --- /dev/null +++ b/tests/components/ac_dimmer/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +output: + - platform: ac_dimmer + id: ac_dimmer_1 + gate_pin: + number: 5 + zero_cross_pin: + number: 6 diff --git a/tests/components/adc/test.esp32-ard.yaml b/tests/components/adc/test.esp32-ard.yaml new file mode 100644 index 0000000000..923fd0d706 --- /dev/null +++ b/tests/components/adc/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: adc + pin: A0 + name: Living Room Brightness + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-c3-ard.yaml b/tests/components/adc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..18e5ab3561 --- /dev/null +++ b/tests/components/adc/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 4 + attenuation: 11db diff --git a/tests/components/adc/test.esp32-idf.yaml b/tests/components/adc/test.esp32-idf.yaml new file mode 100644 index 0000000000..923fd0d706 --- /dev/null +++ b/tests/components/adc/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: adc + pin: A0 + name: Living Room Brightness + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-s2-ard.yaml b/tests/components/adc/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..0119ad5e4d --- /dev/null +++ b/tests/components/adc/test.esp32-s2-ard.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db diff --git a/tests/components/adc/test.esp32-s3-ard.yaml b/tests/components/adc/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..0119ad5e4d --- /dev/null +++ b/tests/components/adc/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db diff --git a/tests/components/adc/test.esp8266-ard.yaml b/tests/components/adc/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1ef79c7ca1 --- /dev/null +++ b/tests/components/adc/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + id: my_sensor + pin: VCC diff --git a/tests/components/adc/test.rp2040-ard.yaml b/tests/components/adc/test.rp2040-ard.yaml new file mode 100644 index 0000000000..200b802a4d --- /dev/null +++ b/tests/components/adc/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + pin: VCC + name: VSYS diff --git a/tests/components/adc128s102/test.esp32-ard.yaml b/tests/components/adc128s102/test.esp32-ard.yaml new file mode 100644 index 0000000000..005fbccc34 --- /dev/null +++ b/tests/components/adc128s102/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +adc128s102: + cs_pin: 12 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.esp32-c3-ard.yaml b/tests/components/adc128s102/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8edf745e58 --- /dev/null +++ b/tests/components/adc128s102/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +adc128s102: + cs_pin: 8 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.esp32-c3-idf.yaml b/tests/components/adc128s102/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8edf745e58 --- /dev/null +++ b/tests/components/adc128s102/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +adc128s102: + cs_pin: 8 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.esp32-idf.yaml b/tests/components/adc128s102/test.esp32-idf.yaml new file mode 100644 index 0000000000..005fbccc34 --- /dev/null +++ b/tests/components/adc128s102/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +adc128s102: + cs_pin: 12 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.esp8266-ard.yaml b/tests/components/adc128s102/test.esp8266-ard.yaml new file mode 100644 index 0000000000..09a51caec1 --- /dev/null +++ b/tests/components/adc128s102/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +adc128s102: + cs_pin: 15 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/adc128s102/test.rp2040-ard.yaml b/tests/components/adc128s102/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a7d54cbfe6 --- /dev/null +++ b/tests/components/adc128s102/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_adc128s102 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +adc128s102: + cs_pin: 5 + id: adc128s102_adc + +sensor: + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 diff --git a/tests/components/addressable_light/test.esp32-ard.yaml b/tests/components/addressable_light/test.esp32-ard.yaml new file mode 100644 index 0000000000..f7717be610 --- /dev/null +++ b/tests/components/addressable_light/test.esp32-ard.yaml @@ -0,0 +1,32 @@ +light: + - platform: fastled_clockless + id: led_matrix_32x8 + name: led_matrix_32x8 + chipset: WS2812B + pin: 2 + num_leds: 256 + rgb_order: GRB + default_transition_length: 0s + color_correct: [50%, 50%, 50%] + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms diff --git a/tests/components/addressable_light/test.esp32-c3-ard.yaml b/tests/components/addressable_light/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f587113fac --- /dev/null +++ b/tests/components/addressable_light/test.esp32-c3-ard.yaml @@ -0,0 +1,31 @@ +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms diff --git a/tests/components/addressable_light/test.esp32-c3-idf.yaml b/tests/components/addressable_light/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f587113fac --- /dev/null +++ b/tests/components/addressable_light/test.esp32-c3-idf.yaml @@ -0,0 +1,31 @@ +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms diff --git a/tests/components/addressable_light/test.esp32-idf.yaml b/tests/components/addressable_light/test.esp32-idf.yaml new file mode 100644 index 0000000000..f587113fac --- /dev/null +++ b/tests/components/addressable_light/test.esp32-idf.yaml @@ -0,0 +1,31 @@ +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms diff --git a/tests/components/ade7880/common.yaml b/tests/components/ade7880/common.yaml new file mode 100644 index 0000000000..0aa388a325 --- /dev/null +++ b/tests/components/ade7880/common.yaml @@ -0,0 +1,56 @@ +i2c: + - id: i2c_ade7880 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: ade7880 + i2c_id: i2c_ade7880 + irq0_pin: ${irq0_pin} + irq1_pin: ${irq1_pin} + reset_pin: ${reset_pin} + frequency: 60Hz + phase_a: + name: Channel A + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3116628 + voltage_gain: -757178 + power_gain: -1344457 + phase_angle: 188 + phase_b: + name: Channel B + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3133655 + voltage_gain: -755235 + power_gain: -1345638 + phase_angle: 188 + phase_c: + name: Channel C + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy + calibration: + current_gain: 3111158 + voltage_gain: -743813 + power_gain: -1351437 + phase_angle: 180 + neutral: + name: Neutral + current: Current + calibration: + current_gain: 3189 diff --git a/tests/components/ade7880/test.esp32-ard.yaml b/tests/components/ade7880/test.esp32-ard.yaml new file mode 100644 index 0000000000..685b49ff32 --- /dev/null +++ b/tests/components/ade7880/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO13 + irq1_pin: GPIO15 + reset_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-c3-ard.yaml b/tests/components/ade7880/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..87db3e9427 --- /dev/null +++ b/tests/components/ade7880/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO6 + irq1_pin: GPIO7 + reset_pin: GPIO10 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-c3-idf.yaml b/tests/components/ade7880/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..87db3e9427 --- /dev/null +++ b/tests/components/ade7880/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO6 + irq1_pin: GPIO7 + reset_pin: GPIO10 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-idf.yaml b/tests/components/ade7880/test.esp32-idf.yaml new file mode 100644 index 0000000000..685b49ff32 --- /dev/null +++ b/tests/components/ade7880/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO13 + irq1_pin: GPIO15 + reset_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp8266-ard.yaml b/tests/components/ade7880/test.esp8266-ard.yaml new file mode 100644 index 0000000000..685b49ff32 --- /dev/null +++ b/tests/components/ade7880/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO13 + irq1_pin: GPIO15 + reset_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ade7880/test.rp2040-ard.yaml b/tests/components/ade7880/test.rp2040-ard.yaml new file mode 100644 index 0000000000..685b49ff32 --- /dev/null +++ b/tests/components/ade7880/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + irq0_pin: GPIO13 + irq1_pin: GPIO15 + reset_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ade7953_i2c/test.esp32-ard.yaml b/tests/components/ade7953_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..71602f20a3 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp32-ard.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 16 + sda: 17 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.esp32-c3-ard.yaml b/tests/components/ade7953_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d7b365a7e1 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml b/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d7b365a7e1 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.esp32-idf.yaml b/tests/components/ade7953_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..71602f20a3 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp32-idf.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 16 + sda: 17 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.esp8266-ard.yaml b/tests/components/ade7953_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6903cd1953 --- /dev/null +++ b/tests/components/ade7953_i2c/test.esp8266-ard.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_i2c/test.rp2040-ard.yaml b/tests/components/ade7953_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d7b365a7e1 --- /dev/null +++ b/tests/components/ade7953_i2c/test.rp2040-ard.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp32-ard.yaml b/tests/components/ade7953_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..e9ef7e4116 --- /dev/null +++ b/tests/components/ade7953_spi/test.esp32-ard.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: ade7953_spi + cs_pin: 5 + irq_pin: 13 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp32-c3-ard.yaml b/tests/components/ade7953_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a967f28d9c --- /dev/null +++ b/tests/components/ade7953_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: ade7953_spi + cs_pin: 8 + irq_pin: 9 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp32-c3-idf.yaml b/tests/components/ade7953_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a967f28d9c --- /dev/null +++ b/tests/components/ade7953_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: ade7953_spi + cs_pin: 8 + irq_pin: 9 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp32-idf.yaml b/tests/components/ade7953_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..e9ef7e4116 --- /dev/null +++ b/tests/components/ade7953_spi/test.esp32-idf.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: ade7953_spi + cs_pin: 5 + irq_pin: 13 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.esp8266-ard.yaml b/tests/components/ade7953_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b36b4445ab --- /dev/null +++ b/tests/components/ade7953_spi/test.esp8266-ard.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: ade7953_spi + cs_pin: 15 + irq_pin: 5 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ade7953_spi/test.rp2040-ard.yaml b/tests/components/ade7953_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319abd4613 --- /dev/null +++ b/tests/components/ade7953_spi/test.rp2040-ard.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_ade7953 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: ade7953_spi + cs_pin: 5 + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s diff --git a/tests/components/ads1115/test.esp32-ard.yaml b/tests/components/ads1115/test.esp32-ard.yaml new file mode 100644 index 0000000000..a869f2379b --- /dev/null +++ b/tests/components/ads1115/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 16 + sda: 17 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32-c3-ard.yaml b/tests/components/ads1115/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7ac5a09f3f --- /dev/null +++ b/tests/components/ads1115/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 5 + sda: 4 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32-c3-idf.yaml b/tests/components/ads1115/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7ac5a09f3f --- /dev/null +++ b/tests/components/ads1115/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 5 + sda: 4 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32-idf.yaml b/tests/components/ads1115/test.esp32-idf.yaml new file mode 100644 index 0000000000..a869f2379b --- /dev/null +++ b/tests/components/ads1115/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 16 + sda: 17 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp8266-ard.yaml b/tests/components/ads1115/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7ac5a09f3f --- /dev/null +++ b/tests/components/ads1115/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 5 + sda: 4 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ads1115/test.rp2040-ard.yaml b/tests/components/ads1115/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7ac5a09f3f --- /dev/null +++ b/tests/components/ads1115/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ads1115 + scl: 5 + sda: 4 + +ads1115: + address: 0x48 + +sensor: + - platform: ads1115 + multiplexer: A0_A1 + gain: 1.024 + id: ads1115_sensor diff --git a/tests/components/ags10/test.esp32-ard.yaml b/tests/components/ags10/test.esp32-ard.yaml new file mode 100644 index 0000000000..b3b53c0d31 --- /dev/null +++ b/tests/components/ags10/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 16 + sda: 17 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/ags10/test.esp32-c3-ard.yaml b/tests/components/ags10/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e338fc78e0 --- /dev/null +++ b/tests/components/ags10/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 5 + sda: 4 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/ags10/test.esp32-c3-idf.yaml b/tests/components/ags10/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e338fc78e0 --- /dev/null +++ b/tests/components/ags10/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 5 + sda: 4 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/ags10/test.esp32-idf.yaml b/tests/components/ags10/test.esp32-idf.yaml new file mode 100644 index 0000000000..b3b53c0d31 --- /dev/null +++ b/tests/components/ags10/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 16 + sda: 17 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/ags10/test.esp8266-ard.yaml b/tests/components/ags10/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e338fc78e0 --- /dev/null +++ b/tests/components/ags10/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ags10 + scl: 5 + sda: 4 + frequency: 10kHz + +sensor: + - platform: ags10 + id: ags10_1 + tvoc: + name: AGS10 TVOC + update_interval: 60s diff --git a/tests/components/aht10/test.esp32-ard.yaml b/tests/components/aht10/test.esp32-ard.yaml new file mode 100644 index 0000000000..499e69e5d3 --- /dev/null +++ b/tests/components/aht10/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 16 + sda: 17 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.esp32-c3-ard.yaml b/tests/components/aht10/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2e5f505476 --- /dev/null +++ b/tests/components/aht10/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 5 + sda: 4 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.esp32-c3-idf.yaml b/tests/components/aht10/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2e5f505476 --- /dev/null +++ b/tests/components/aht10/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 5 + sda: 4 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.esp32-idf.yaml b/tests/components/aht10/test.esp32-idf.yaml new file mode 100644 index 0000000000..499e69e5d3 --- /dev/null +++ b/tests/components/aht10/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 16 + sda: 17 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.esp8266-ard.yaml b/tests/components/aht10/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2e5f505476 --- /dev/null +++ b/tests/components/aht10/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 5 + sda: 4 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aht10/test.rp2040-ard.yaml b/tests/components/aht10/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2e5f505476 --- /dev/null +++ b/tests/components/aht10/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_aht10 + scl: 5 + sda: 4 + +sensor: + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/aic3204/common.yaml b/tests/components/aic3204/common.yaml new file mode 100644 index 0000000000..6e939bd260 --- /dev/null +++ b/tests/components/aic3204/common.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - audio_dac.mute_off: + - audio_dac.mute_on: + - audio_dac.set_volume: + volume: 50% + +i2c: + - id: i2c_aic3204 + scl: ${scl_pin} + sda: ${sda_pin} + +audio_dac: + - platform: aic3204 diff --git a/tests/components/aic3204/test.esp32-ard.yaml b/tests/components/aic3204/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/aic3204/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp32-c3-ard.yaml b/tests/components/aic3204/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/aic3204/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp32-c3-idf.yaml b/tests/components/aic3204/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/aic3204/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp32-idf.yaml b/tests/components/aic3204/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/aic3204/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp8266-ard.yaml b/tests/components/aic3204/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/aic3204/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/airthings_wave_mini/common.yaml b/tests/components/airthings_wave_mini/common.yaml new file mode 100644 index 0000000000..87902e6c66 --- /dev/null +++ b/tests/components/airthings_wave_mini/common.yaml @@ -0,0 +1,22 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthingsmini01 + +sensor: + - id: airthingswm + platform: airthings_wave_mini + ble_client_id: airthingsmini01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Mini Temperature + humidity: + name: Wave Mini Humidity + pressure: + name: Wave Mini Pressure + tvoc: + name: Wave Mini VOC + battery_voltage: + name: Wave Mini Battery Voltage diff --git a/tests/components/airthings_wave_mini/test.esp32-ard.yaml b/tests/components/airthings_wave_mini/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/airthings_wave_mini/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-c3-ard.yaml b/tests/components/airthings_wave_mini/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/airthings_wave_mini/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml b/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-idf.yaml b/tests/components/airthings_wave_mini/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/airthings_wave_mini/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/airthings_wave_plus/common.yaml b/tests/components/airthings_wave_plus/common.yaml new file mode 100644 index 0000000000..2124fcdaec --- /dev/null +++ b/tests/components/airthings_wave_plus/common.yaml @@ -0,0 +1,28 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthings01 + +sensor: + - id: airthingswp + platform: airthings_wave_plus + ble_client_id: airthings01 + update_interval: 5min + battery_update_interval: 12h + temperature: + name: Wave Plus Temperature + radon: + name: Wave Plus Radon + radon_long_term: + name: Wave Plus Radon Long Term + pressure: + name: Wave Plus Pressure + humidity: + name: Wave Plus Humidity + co2: + name: Wave Plus CO2 + tvoc: + name: Wave Plus VOC + battery_voltage: + name: Wave Plus Battery Voltage diff --git a/tests/components/airthings_wave_plus/test.esp32-ard.yaml b/tests/components/airthings_wave_plus/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/airthings_wave_plus/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-c3-ard.yaml b/tests/components/airthings_wave_plus/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/airthings_wave_plus/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml b/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-idf.yaml b/tests/components/airthings_wave_plus/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/airthings_wave_plus/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/common.yaml b/tests/components/alarm_control_panel/common.yaml new file mode 100644 index 0000000000..218274bad4 --- /dev/null +++ b/tests/components/alarm_control_panel/common.yaml @@ -0,0 +1,64 @@ +binary_sensor: + - platform: gpio + id: bin1 + pin: 1 + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); + - platform: template + id: alarmcontrolpanel2 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_night_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + bypass_armed_night: true + on_disarmed: + then: + - logger.log: "### DISARMED ###" + on_pending: + then: + - logger.log: "### PENDING ###" + on_arming: + then: + - logger.log: "### ARMING ###" + on_armed_home: + then: + - logger.log: "### ARMED HOME ###" + on_armed_night: + then: + - logger.log: "### ARMED NIGHT ###" + on_armed_away: + then: + - logger.log: "### ARMED AWAY ###" + on_triggered: + then: + - logger.log: "### TRIGGERED ###" + on_cleared: + then: + - logger.log: "### CLEARED ###" diff --git a/tests/components/alarm_control_panel/test.esp32-ard.yaml b/tests/components/alarm_control_panel/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.esp32-c3-ard.yaml b/tests/components/alarm_control_panel/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml b/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.esp32-idf.yaml b/tests/components/alarm_control_panel/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.esp8266-ard.yaml b/tests/components/alarm_control_panel/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.rp2040-ard.yaml b/tests/components/alarm_control_panel/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alpha3/common.yaml b/tests/components/alpha3/common.yaml new file mode 100644 index 0000000000..913f086ac4 --- /dev/null +++ b/tests/components/alpha3/common.yaml @@ -0,0 +1,17 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: alpha3_blec + +sensor: + - platform: alpha3 + ble_client_id: alpha3_blec + flow: + name: "Radiator Pump Flow" + head: + name: "Radiator Pump Head" + power: + name: "Radiator Pump Power" + speed: + name: "Radiator Pump Speed" diff --git a/tests/components/alpha3/test.esp32-ard.yaml b/tests/components/alpha3/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alpha3/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alpha3/test.esp32-c3-ard.yaml b/tests/components/alpha3/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alpha3/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alpha3/test.esp32-c3-idf.yaml b/tests/components/alpha3/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alpha3/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alpha3/test.esp32-idf.yaml b/tests/components/alpha3/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alpha3/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/am2315c/test.esp32-ard.yaml b/tests/components/am2315c/test.esp32-ard.yaml new file mode 100644 index 0000000000..ed6b65f787 --- /dev/null +++ b/tests/components/am2315c/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 16 + sda: 17 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.esp32-c3-ard.yaml b/tests/components/am2315c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d09bffb7a4 --- /dev/null +++ b/tests/components/am2315c/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 5 + sda: 4 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.esp32-c3-idf.yaml b/tests/components/am2315c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d09bffb7a4 --- /dev/null +++ b/tests/components/am2315c/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 5 + sda: 4 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.esp32-idf.yaml b/tests/components/am2315c/test.esp32-idf.yaml new file mode 100644 index 0000000000..ed6b65f787 --- /dev/null +++ b/tests/components/am2315c/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 16 + sda: 17 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.esp8266-ard.yaml b/tests/components/am2315c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d09bffb7a4 --- /dev/null +++ b/tests/components/am2315c/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 5 + sda: 4 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2315c/test.rp2040-ard.yaml b/tests/components/am2315c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d09bffb7a4 --- /dev/null +++ b/tests/components/am2315c/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_am2315c + scl: 5 + sda: 4 + +sensor: + - platform: am2315c + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp32-ard.yaml b/tests/components/am2320/test.esp32-ard.yaml new file mode 100644 index 0000000000..99f4173b85 --- /dev/null +++ b/tests/components/am2320/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp32-c3-ard.yaml b/tests/components/am2320/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..6acfe8d4fd --- /dev/null +++ b/tests/components/am2320/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp32-c3-idf.yaml b/tests/components/am2320/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6acfe8d4fd --- /dev/null +++ b/tests/components/am2320/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp32-idf.yaml b/tests/components/am2320/test.esp32-idf.yaml new file mode 100644 index 0000000000..99f4173b85 --- /dev/null +++ b/tests/components/am2320/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.esp8266-ard.yaml b/tests/components/am2320/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6acfe8d4fd --- /dev/null +++ b/tests/components/am2320/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am2320/test.rp2040-ard.yaml b/tests/components/am2320/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6acfe8d4fd --- /dev/null +++ b/tests/components/am2320/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +sensor: + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/am43/common.yaml b/tests/components/am43/common.yaml new file mode 100644 index 0000000000..60b7d81a55 --- /dev/null +++ b/tests/components/am43/common.yaml @@ -0,0 +1,19 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: am43_blec + +cover: + - platform: am43 + name: Test AM43 Cover + id: am43_test + ble_client_id: am43_blec + +sensor: + - platform: am43 + ble_client_id: am43_blec + battery_level: + name: Kitchen blinds battery + illuminance: + name: Kitchen blinds light diff --git a/tests/components/am43/test.esp32-ard.yaml b/tests/components/am43/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/am43/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/am43/test.esp32-c3-ard.yaml b/tests/components/am43/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/am43/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/am43/test.esp32-c3-idf.yaml b/tests/components/am43/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/am43/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/am43/test.esp32-idf.yaml b/tests/components/am43/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/am43/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/analog_threshold/common.yaml b/tests/components/analog_threshold/common.yaml new file mode 100644 index 0000000000..b5c14dfe56 --- /dev/null +++ b/tests/components/analog_threshold/common.yaml @@ -0,0 +1,28 @@ +sensor: + - platform: template + id: template_sensor + name: Template Sensor + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 15s + +binary_sensor: + - platform: analog_threshold + name: Analog Threshold 1 + sensor_id: template_sensor + threshold: + upper: 110 + lower: 90 + filters: + - delayed_on: 0s + - delayed_off: 10s + - platform: analog_threshold + name: Analog Threshold 2 + sensor_id: template_sensor + threshold: 100 + filters: + - invert: diff --git a/tests/components/analog_threshold/test.esp32-ard.yaml b/tests/components/analog_threshold/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/analog_threshold/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/analog_threshold/test.esp32-c3-ard.yaml b/tests/components/analog_threshold/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/analog_threshold/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/analog_threshold/test.esp32-c3-idf.yaml b/tests/components/analog_threshold/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/analog_threshold/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/analog_threshold/test.esp32-idf.yaml b/tests/components/analog_threshold/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/analog_threshold/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/analog_threshold/test.esp8266-ard.yaml b/tests/components/analog_threshold/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/analog_threshold/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/analog_threshold/test.rp2040-ard.yaml b/tests/components/analog_threshold/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/analog_threshold/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/animation/test.esp32-ard.yaml b/tests/components/animation/test.esp32-ard.yaml new file mode 100644 index 0000000000..af6cd202dd --- /dev/null +++ b/tests/components/animation/test.esp32-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: false + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: false diff --git a/tests/components/animation/test.esp32-c3-ard.yaml b/tests/components/animation/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..10e8ccb47e --- /dev/null +++ b/tests/components/animation/test.esp32-c3-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: false diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..10e8ccb47e --- /dev/null +++ b/tests/components/animation/test.esp32-c3-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: false diff --git a/tests/components/animation/test.esp32-idf.yaml b/tests/components/animation/test.esp32-idf.yaml new file mode 100644 index 0000000000..af6cd202dd --- /dev/null +++ b/tests/components/animation/test.esp32-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: false + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: false diff --git a/tests/components/animation/test.esp8266-ard.yaml b/tests/components/animation/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ced4996f25 --- /dev/null +++ b/tests/components/animation/test.esp8266-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 5 + dc_pin: 15 + reset_pin: 16 + invert_colors: false + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: false diff --git a/tests/components/animation/test.rp2040-ard.yaml b/tests/components/animation/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0e33959cc6 --- /dev/null +++ b/tests/components/animation/test.rp2040-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_main_lcd + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 21 + reset_pin: 22 + invert_colors: false + +# Purposely test that `animation:` does auto-load `image:` +# Keep the `image:` undefined. +# image: + +animation: + - id: rgb565_animation + file: ../../pnglogo.png + type: RGB565 + use_transparency: false diff --git a/tests/components/anova/common.yaml b/tests/components/anova/common.yaml new file mode 100644 index 0000000000..c4162fe71e --- /dev/null +++ b/tests/components/anova/common.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: anova_blec + +climate: + - platform: anova + name: Anova cooker + ble_client_id: anova_blec + unit_of_measurement: c diff --git a/tests/components/anova/test.esp32-ard.yaml b/tests/components/anova/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/anova/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/anova/test.esp32-c3-ard.yaml b/tests/components/anova/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/anova/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/anova/test.esp32-c3-idf.yaml b/tests/components/anova/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/anova/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/anova/test.esp32-idf.yaml b/tests/components/anova/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/anova/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/apds9306/common.yaml b/tests/components/apds9306/common.yaml new file mode 100644 index 0000000000..b3828e62ff --- /dev/null +++ b/tests/components/apds9306/common.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_apds9306 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: apds9306 + name: "APDS9306 Light Level" + gain: 3 + bit_width: 16 + measurement_rate: 2000ms + update_interval: 60s diff --git a/tests/components/apds9306/test.esp32-ard.yaml b/tests/components/apds9306/test.esp32-ard.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/apds9306/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp32-c3-ard.yaml b/tests/components/apds9306/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/apds9306/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp32-c3-idf.yaml b/tests/components/apds9306/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/apds9306/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp32-idf.yaml b/tests/components/apds9306/test.esp32-idf.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/apds9306/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp8266-ard.yaml b/tests/components/apds9306/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/apds9306/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/apds9306/test.rp2040-ard.yaml b/tests/components/apds9306/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/apds9306/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/apds9960/test.esp32-ard.yaml b/tests/components/apds9960/test.esp32-ard.yaml new file mode 100644 index 0000000000..7ff70a4d47 --- /dev/null +++ b/tests/components/apds9960/test.esp32-ard.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.esp32-c3-ard.yaml b/tests/components/apds9960/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f6b6f7bac0 --- /dev/null +++ b/tests/components/apds9960/test.esp32-c3-ard.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.esp32-c3-idf.yaml b/tests/components/apds9960/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f6b6f7bac0 --- /dev/null +++ b/tests/components/apds9960/test.esp32-c3-idf.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.esp32-idf.yaml b/tests/components/apds9960/test.esp32-idf.yaml new file mode 100644 index 0000000000..7ff70a4d47 --- /dev/null +++ b/tests/components/apds9960/test.esp32-idf.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 16 + sda: 17 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.esp8266-ard.yaml b/tests/components/apds9960/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f6b6f7bac0 --- /dev/null +++ b/tests/components/apds9960/test.esp8266-ard.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/apds9960/test.rp2040-ard.yaml b/tests/components/apds9960/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f6b6f7bac0 --- /dev/null +++ b/tests/components/apds9960/test.rp2040-ard.yaml @@ -0,0 +1,48 @@ +i2c: + - id: i2c_bme280 + scl: 5 + sda: 4 + +apds9960: + address: 0x20 + update_interval: 60s + +binary_sensor: + - platform: apds9960 + id: apds9960_binary_sensor + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml new file mode 100644 index 0000000000..7ac11e4da6 --- /dev/null +++ b/tests/components/api/common.yaml @@ -0,0 +1,59 @@ +esphome: + on_boot: + then: + - homeassistant.event: + event: esphome.button_pressed + data: + message: Button was pressed + - homeassistant.action: + action: notify.html5 + data: + message: Button was pressed + - homeassistant.tag_scanned: pulse + +api: + port: 8000 + password: pwd + reboot_timeout: 0min + encryption: + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= + actions: + - action: hello_world + variables: + name: string + then: + - logger.log: + format: Hello World %s! + args: + - name.c_str() + - action: empty_action + then: + - logger.log: Action Called + - action: all_types + variables: + bool_: bool + int_: int + float_: float + string_: string + then: + - logger.log: Something happened + - action: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %ld (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - (long) int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() diff --git a/tests/components/api/test.esp32-ard.yaml b/tests/components/api/test.esp32-ard.yaml new file mode 100644 index 0000000000..46c01d926f --- /dev/null +++ b/tests/components/api/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-c3-ard.yaml b/tests/components/api/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..46c01d926f --- /dev/null +++ b/tests/components/api/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-c3-idf.yaml b/tests/components/api/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..46c01d926f --- /dev/null +++ b/tests/components/api/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-idf.yaml b/tests/components/api/test.esp32-idf.yaml new file mode 100644 index 0000000000..46c01d926f --- /dev/null +++ b/tests/components/api/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp8266-ard.yaml b/tests/components/api/test.esp8266-ard.yaml new file mode 100644 index 0000000000..46c01d926f --- /dev/null +++ b/tests/components/api/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.host.yaml b/tests/components/api/test.host.yaml new file mode 100644 index 0000000000..1ecafeab77 --- /dev/null +++ b/tests/components/api/test.host.yaml @@ -0,0 +1,3 @@ +<<: !include common.yaml + +network: diff --git a/tests/components/api/test.rp2040-ard.yaml b/tests/components/api/test.rp2040-ard.yaml new file mode 100644 index 0000000000..46c01d926f --- /dev/null +++ b/tests/components/api/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/as3935_i2c/test.esp32-ard.yaml b/tests/components/as3935_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..fad703bee5 --- /dev/null +++ b/tests/components/as3935_i2c/test.esp32-ard.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 16 + sda: 17 + +as3935_i2c: + irq_pin: 12 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.esp32-c3-ard.yaml b/tests/components/as3935_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c72556dbac --- /dev/null +++ b/tests/components/as3935_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 5 + sda: 4 + +as3935_i2c: + irq_pin: 6 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.esp32-c3-idf.yaml b/tests/components/as3935_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c72556dbac --- /dev/null +++ b/tests/components/as3935_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 5 + sda: 4 + +as3935_i2c: + irq_pin: 6 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.esp32-idf.yaml b/tests/components/as3935_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..fad703bee5 --- /dev/null +++ b/tests/components/as3935_i2c/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 16 + sda: 17 + +as3935_i2c: + irq_pin: 12 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.esp8266-ard.yaml b/tests/components/as3935_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..adba9e440f --- /dev/null +++ b/tests/components/as3935_i2c/test.esp8266-ard.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 5 + sda: 4 + +as3935_i2c: + irq_pin: 15 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_i2c/test.rp2040-ard.yaml b/tests/components/as3935_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c72556dbac --- /dev/null +++ b/tests/components/as3935_i2c/test.rp2040-ard.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_as3935 + scl: 5 + sda: 4 + +as3935_i2c: + irq_pin: 6 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp32-ard.yaml b/tests/components/as3935_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..813a39cb23 --- /dev/null +++ b/tests/components/as3935_spi/test.esp32-ard.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +as3935_spi: + cs_pin: 12 + irq_pin: 13 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp32-c3-ard.yaml b/tests/components/as3935_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7a4a01aeea --- /dev/null +++ b/tests/components/as3935_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +as3935_spi: + cs_pin: 2 + irq_pin: 3 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp32-c3-idf.yaml b/tests/components/as3935_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7a4a01aeea --- /dev/null +++ b/tests/components/as3935_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +as3935_spi: + cs_pin: 2 + irq_pin: 3 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp32-idf.yaml b/tests/components/as3935_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..813a39cb23 --- /dev/null +++ b/tests/components/as3935_spi/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +as3935_spi: + cs_pin: 12 + irq_pin: 13 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.esp8266-ard.yaml b/tests/components/as3935_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..38a40b0833 --- /dev/null +++ b/tests/components/as3935_spi/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +as3935_spi: + cs_pin: 15 + irq_pin: 16 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as3935_spi/test.rp2040-ard.yaml b/tests/components/as3935_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..528759d97d --- /dev/null +++ b/tests/components/as3935_spi/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_as3935 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +as3935_spi: + cs_pin: 6 + irq_pin: 7 + +binary_sensor: + - platform: as3935 + name: Storm Alert + +sensor: + - platform: as3935 + lightning_energy: + name: Lightning Energy + distance: + name: Distance Storm diff --git a/tests/components/as5600/test.esp32-ard.yaml b/tests/components/as5600/test.esp32-ard.yaml new file mode 100644 index 0000000000..312ee9ad04 --- /dev/null +++ b/tests/components/as5600/test.esp32-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 16 + sda: 17 + +as5600: + dir_pin: 12 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.esp32-c3-ard.yaml b/tests/components/as5600/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e074fa5e0c --- /dev/null +++ b/tests/components/as5600/test.esp32-c3-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +as5600: + dir_pin: 6 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.esp32-c3-idf.yaml b/tests/components/as5600/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e074fa5e0c --- /dev/null +++ b/tests/components/as5600/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +as5600: + dir_pin: 6 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.esp32-idf.yaml b/tests/components/as5600/test.esp32-idf.yaml new file mode 100644 index 0000000000..312ee9ad04 --- /dev/null +++ b/tests/components/as5600/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 16 + sda: 17 + +as5600: + dir_pin: 12 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.esp8266-ard.yaml b/tests/components/as5600/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a232d27305 --- /dev/null +++ b/tests/components/as5600/test.esp8266-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +as5600: + dir_pin: 15 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as5600/test.rp2040-ard.yaml b/tests/components/as5600/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e074fa5e0c --- /dev/null +++ b/tests/components/as5600/test.rp2040-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +as5600: + dir_pin: 6 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + +sensor: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status diff --git a/tests/components/as7341/test.esp32-ard.yaml b/tests/components/as7341/test.esp32-ard.yaml new file mode 100644 index 0000000000..d582a367ac --- /dev/null +++ b/tests/components/as7341/test.esp32-ard.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 16 + sda: 17 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.esp32-c3-ard.yaml b/tests/components/as7341/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..19965d1715 --- /dev/null +++ b/tests/components/as7341/test.esp32-c3-ard.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.esp32-c3-idf.yaml b/tests/components/as7341/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..19965d1715 --- /dev/null +++ b/tests/components/as7341/test.esp32-c3-idf.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.esp32-idf.yaml b/tests/components/as7341/test.esp32-idf.yaml new file mode 100644 index 0000000000..d582a367ac --- /dev/null +++ b/tests/components/as7341/test.esp32-idf.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 16 + sda: 17 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.esp8266-ard.yaml b/tests/components/as7341/test.esp8266-ard.yaml new file mode 100644 index 0000000000..19965d1715 --- /dev/null +++ b/tests/components/as7341/test.esp8266-ard.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/as7341/test.rp2040-ard.yaml b/tests/components/as7341/test.rp2040-ard.yaml new file mode 100644 index 0000000000..19965d1715 --- /dev/null +++ b/tests/components/as7341/test.rp2040-ard.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_as5600 + scl: 5 + sda: 4 + +sensor: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR diff --git a/tests/components/at581x/test.esp32-ard.yaml b/tests/components/at581x/test.esp32-ard.yaml new file mode 100644 index 0000000000..ff84e61e1e --- /dev/null +++ b/tests/components/at581x/test.esp32-ard.yaml @@ -0,0 +1,38 @@ +esphome: + on_boot: + then: + - at581x.settings: + id: "Waveradar" + hw_frontend_reset: false + frequency: 5800MHz + sensing_distance: 200 + poweron_selfcheck_time: 2s + protect_time: 1s + trigger_base: 500ms + trigger_keep: 10s + stage_gain: 3 + power_consumption: 70uA + - at581x.reset: + id: "Waveradar" + +at581x: + id: "Waveradar" + i2c_id: i2c_bus + +i2c: + sda: 14 + scl: 15 + scan: true + frequency: 100kHz + setup_priority: -100 + id: i2c_bus + +binary_sensor: + - platform: gpio + pin: GPIO21 + name: "Radar motion" + +switch: + - platform: at581x + at581x_id: "Waveradar" + name: "Enable Radar" diff --git a/tests/components/at581x/test.esp32-c3-ard.yaml b/tests/components/at581x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b49a283eca --- /dev/null +++ b/tests/components/at581x/test.esp32-c3-ard.yaml @@ -0,0 +1,38 @@ +esphome: + on_boot: + then: + - at581x.settings: + id: "Waveradar" + hw_frontend_reset: false + frequency: 5800MHz + sensing_distance: 200 + poweron_selfcheck_time: 2s + protect_time: 1s + trigger_base: 500ms + trigger_keep: 10s + stage_gain: 3 + power_consumption: 70uA + - at581x.reset: + id: "Waveradar" + +at581x: + id: "Waveradar" + i2c_id: i2c_bus + +i2c: + sda: 8 + scl: 9 + scan: true + frequency: 100kHz + setup_priority: -100 + id: i2c_bus + +binary_sensor: + - platform: gpio + pin: GPIO21 + name: "Radar motion" + +switch: + - platform: at581x + at581x_id: "Waveradar" + name: "Enable Radar" diff --git a/tests/components/at581x/test.esp32-c3-idf.yaml b/tests/components/at581x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b49a283eca --- /dev/null +++ b/tests/components/at581x/test.esp32-c3-idf.yaml @@ -0,0 +1,38 @@ +esphome: + on_boot: + then: + - at581x.settings: + id: "Waveradar" + hw_frontend_reset: false + frequency: 5800MHz + sensing_distance: 200 + poweron_selfcheck_time: 2s + protect_time: 1s + trigger_base: 500ms + trigger_keep: 10s + stage_gain: 3 + power_consumption: 70uA + - at581x.reset: + id: "Waveradar" + +at581x: + id: "Waveradar" + i2c_id: i2c_bus + +i2c: + sda: 8 + scl: 9 + scan: true + frequency: 100kHz + setup_priority: -100 + id: i2c_bus + +binary_sensor: + - platform: gpio + pin: GPIO21 + name: "Radar motion" + +switch: + - platform: at581x + at581x_id: "Waveradar" + name: "Enable Radar" diff --git a/tests/components/at581x/test.esp32-idf.yaml b/tests/components/at581x/test.esp32-idf.yaml new file mode 100644 index 0000000000..ff84e61e1e --- /dev/null +++ b/tests/components/at581x/test.esp32-idf.yaml @@ -0,0 +1,38 @@ +esphome: + on_boot: + then: + - at581x.settings: + id: "Waveradar" + hw_frontend_reset: false + frequency: 5800MHz + sensing_distance: 200 + poweron_selfcheck_time: 2s + protect_time: 1s + trigger_base: 500ms + trigger_keep: 10s + stage_gain: 3 + power_consumption: 70uA + - at581x.reset: + id: "Waveradar" + +at581x: + id: "Waveradar" + i2c_id: i2c_bus + +i2c: + sda: 14 + scl: 15 + scan: true + frequency: 100kHz + setup_priority: -100 + id: i2c_bus + +binary_sensor: + - platform: gpio + pin: GPIO21 + name: "Radar motion" + +switch: + - platform: at581x + at581x_id: "Waveradar" + name: "Enable Radar" diff --git a/tests/components/at581x/test.esp8266-ard.yaml b/tests/components/at581x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a7b0069045 --- /dev/null +++ b/tests/components/at581x/test.esp8266-ard.yaml @@ -0,0 +1,38 @@ +esphome: + on_boot: + then: + - at581x.settings: + id: "Waveradar" + hw_frontend_reset: false + frequency: 5800MHz + sensing_distance: 200 + poweron_selfcheck_time: 2s + protect_time: 1s + trigger_base: 500ms + trigger_keep: 10s + stage_gain: 3 + power_consumption: 70uA + - at581x.reset: + id: "Waveradar" + +at581x: + id: "Waveradar" + i2c_id: i2c_bus + +i2c: + sda: 14 + scl: 15 + scan: true + frequency: 100kHz + setup_priority: -100 + id: i2c_bus + +binary_sensor: + - platform: gpio + pin: GPIO4 + name: "Radar motion" + +switch: + - platform: at581x + at581x_id: "Waveradar" + name: "Enable Radar" diff --git a/tests/components/at581x/test.rp2040-ard.yaml b/tests/components/at581x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b49a283eca --- /dev/null +++ b/tests/components/at581x/test.rp2040-ard.yaml @@ -0,0 +1,38 @@ +esphome: + on_boot: + then: + - at581x.settings: + id: "Waveradar" + hw_frontend_reset: false + frequency: 5800MHz + sensing_distance: 200 + poweron_selfcheck_time: 2s + protect_time: 1s + trigger_base: 500ms + trigger_keep: 10s + stage_gain: 3 + power_consumption: 70uA + - at581x.reset: + id: "Waveradar" + +at581x: + id: "Waveradar" + i2c_id: i2c_bus + +i2c: + sda: 8 + scl: 9 + scan: true + frequency: 100kHz + setup_priority: -100 + id: i2c_bus + +binary_sensor: + - platform: gpio + pin: GPIO21 + name: "Radar motion" + +switch: + - platform: at581x + at581x_id: "Waveradar" + name: "Enable Radar" diff --git a/tests/components/atc_mithermometer/common.yaml b/tests/components/atc_mithermometer/common.yaml new file mode 100644 index 0000000000..0248090c23 --- /dev/null +++ b/tests/components/atc_mithermometer/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: atc_mithermometer + mac_address: A4:C1:38:4E:16:78 + temperature: + name: ATC Temperature + humidity: + name: ATC Humidity + battery_level: + name: ATC Battery-Level + battery_voltage: + name: ATC Battery-Voltage diff --git a/tests/components/atc_mithermometer/test.esp32-ard.yaml b/tests/components/atc_mithermometer/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/atc_mithermometer/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/atc_mithermometer/test.esp32-c3-ard.yaml b/tests/components/atc_mithermometer/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/atc_mithermometer/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml b/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/atc_mithermometer/test.esp32-idf.yaml b/tests/components/atc_mithermometer/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/atc_mithermometer/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/atm90e26/test.esp32-ard.yaml b/tests/components/atm90e26/test.esp32-ard.yaml new file mode 100644 index 0000000000..72fb3e5b24 --- /dev/null +++ b/tests/components/atm90e26/test.esp32-ard.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: atm90e26 + cs_pin: 13 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.esp32-c3-ard.yaml b/tests/components/atm90e26/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ce123bcf72 --- /dev/null +++ b/tests/components/atm90e26/test.esp32-c3-ard.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: atm90e26 + cs_pin: 8 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.esp32-c3-idf.yaml b/tests/components/atm90e26/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ce123bcf72 --- /dev/null +++ b/tests/components/atm90e26/test.esp32-c3-idf.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: atm90e26 + cs_pin: 8 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.esp32-idf.yaml b/tests/components/atm90e26/test.esp32-idf.yaml new file mode 100644 index 0000000000..72fb3e5b24 --- /dev/null +++ b/tests/components/atm90e26/test.esp32-idf.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: atm90e26 + cs_pin: 13 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.esp8266-ard.yaml b/tests/components/atm90e26/test.esp8266-ard.yaml new file mode 100644 index 0000000000..68d63cc278 --- /dev/null +++ b/tests/components/atm90e26/test.esp8266-ard.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: atm90e26 + cs_pin: 5 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e26/test.rp2040-ard.yaml b/tests/components/atm90e26/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f43277dbb1 --- /dev/null +++ b/tests/components/atm90e26/test.rp2040-ard.yaml @@ -0,0 +1,26 @@ +spi: + - id: spi_atm90e26 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: atm90e26 + cs_pin: 5 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 diff --git a/tests/components/atm90e32/test.esp32-ard.yaml b/tests/components/atm90e32/test.esp32-ard.yaml new file mode 100644 index 0000000000..3bdc2bcec6 --- /dev/null +++ b/tests/components/atm90e32/test.esp32-ard.yaml @@ -0,0 +1,60 @@ +spi: + - id: spi_atm90e32 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: atm90e32 + cs_pin: 13 + id: chip1 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_b: + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-c3-ard.yaml b/tests/components/atm90e32/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9ec0037a61 --- /dev/null +++ b/tests/components/atm90e32/test.esp32-c3-ard.yaml @@ -0,0 +1,60 @@ +spi: + - id: spi_atm90e32 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: atm90e32 + cs_pin: 8 + id: chip1 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_b: + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-c3-idf.yaml b/tests/components/atm90e32/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9ec0037a61 --- /dev/null +++ b/tests/components/atm90e32/test.esp32-c3-idf.yaml @@ -0,0 +1,60 @@ +spi: + - id: spi_atm90e32 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: atm90e32 + cs_pin: 8 + id: chip1 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_b: + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-idf.yaml b/tests/components/atm90e32/test.esp32-idf.yaml new file mode 100644 index 0000000000..3bdc2bcec6 --- /dev/null +++ b/tests/components/atm90e32/test.esp32-idf.yaml @@ -0,0 +1,60 @@ +spi: + - id: spi_atm90e32 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: atm90e32 + cs_pin: 13 + id: chip1 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_b: + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp8266-ard.yaml b/tests/components/atm90e32/test.esp8266-ard.yaml new file mode 100644 index 0000000000..fbb3368efa --- /dev/null +++ b/tests/components/atm90e32/test.esp8266-ard.yaml @@ -0,0 +1,91 @@ +spi: + - id: spi_atm90e32 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: atm90e32 + cs_pin: 5 + id: chip1 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_b: + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X + enable_offset_calibration: True + - platform: atm90e32 + cs_pin: 4 + id: chip2 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + voltage: + name: EMON Line Voltage C + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + line_frequency: 60Hz + current_phases: 2 +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.rp2040-ard.yaml b/tests/components/atm90e32/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a6b7956da7 --- /dev/null +++ b/tests/components/atm90e32/test.rp2040-ard.yaml @@ -0,0 +1,60 @@ +spi: + - id: spi_atm90e32 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: atm90e32 + cs_pin: 5 + id: chip1 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_b: + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + current: + name: EMON CT3 Current + power: + name: EMON Active Power CT3 + reactive_power: + name: EMON Reactive Power CT3 + power_factor: + name: EMON Power Factor CT3 + gain_voltage: 7305 + gain_ct: 27961 + frequency: + name: EMON Line Frequency + chip_temperature: + name: EMON Chip Temp A + line_frequency: 60Hz + current_phases: 3 + gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/axs15231/common.yaml b/tests/components/axs15231/common.yaml new file mode 100644 index 0000000000..1c0c79975f --- /dev/null +++ b/tests/components/axs15231/common.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_axs15231 + scl: 3 + sda: 21 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 19 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: axs15231 + display: ssd1306_display + interrupt_pin: 20 + reset_pin: 18 diff --git a/tests/components/axs15231/test.esp32-ard.yaml b/tests/components/axs15231/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/axs15231/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/axs15231/test.esp32-c3-ard.yaml b/tests/components/axs15231/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/axs15231/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/axs15231/test.esp32-c3-idf.yaml b/tests/components/axs15231/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/axs15231/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/axs15231/test.esp32-idf.yaml b/tests/components/axs15231/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/axs15231/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/axs15231/test.esp8266-ard.yaml b/tests/components/axs15231/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c09d139574 --- /dev/null +++ b/tests/components/axs15231/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_axs15231 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: axs15231 + display: ssd1306_display + interrupt_pin: 12 diff --git a/tests/components/axs15231/test.rp2040-ard.yaml b/tests/components/axs15231/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/axs15231/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/b_parasite/common.yaml b/tests/components/b_parasite/common.yaml new file mode 100644 index 0000000000..262e891bb2 --- /dev/null +++ b/tests/components/b_parasite/common.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + - platform: b_parasite + mac_address: F0:CA:F0:CA:01:01 + humidity: + name: b-parasite Air Humidity + temperature: + name: b-parasite Air Temperature + moisture: + name: b-parasite Soil Moisture + battery_voltage: + name: b-parasite Battery Voltage + illuminance: + name: b-parasite Illuminance diff --git a/tests/components/b_parasite/test.esp32-ard.yaml b/tests/components/b_parasite/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/b_parasite/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/b_parasite/test.esp32-c3-ard.yaml b/tests/components/b_parasite/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/b_parasite/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/b_parasite/test.esp32-c3-idf.yaml b/tests/components/b_parasite/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/b_parasite/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/b_parasite/test.esp32-idf.yaml b/tests/components/b_parasite/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/b_parasite/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ballu/test.esp32-ard.yaml b/tests/components/ballu/test.esp32-ard.yaml new file mode 100644 index 0000000000..bb7b9b0435 --- /dev/null +++ b/tests/components/ballu/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: ballu + horizontal_default: middle + vertical_default: middle + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/ballu/test.esp8266-ard.yaml b/tests/components/ballu/test.esp8266-ard.yaml new file mode 100644 index 0000000000..05aa446739 --- /dev/null +++ b/tests/components/ballu/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: ballu + horizontal_default: middle + vertical_default: middle + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/bang_bang/common.yaml b/tests/components/bang_bang/common.yaml new file mode 100644 index 0000000000..5882025191 --- /dev/null +++ b/tests/components/bang_bang/common.yaml @@ -0,0 +1,35 @@ +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +climate: + - platform: bang_bang + name: Bang Bang Climate + sensor: template_sensor1 + humidity_sensor: template_sensor1 + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: template_switch1 + cool_action: + - switch.turn_on: template_switch2 + heat_action: + - switch.turn_on: template_switch1 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C diff --git a/tests/components/bang_bang/test.esp32-ard.yaml b/tests/components/bang_bang/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bang_bang/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bang_bang/test.esp32-c3-ard.yaml b/tests/components/bang_bang/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bang_bang/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bang_bang/test.esp32-c3-idf.yaml b/tests/components/bang_bang/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bang_bang/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bang_bang/test.esp32-idf.yaml b/tests/components/bang_bang/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bang_bang/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bang_bang/test.esp8266-ard.yaml b/tests/components/bang_bang/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bang_bang/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bang_bang/test.rp2040-ard.yaml b/tests/components/bang_bang/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bang_bang/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bedjet/common.yaml b/tests/components/bedjet/common.yaml new file mode 100644 index 0000000000..1563fc9dae --- /dev/null +++ b/tests/components/bedjet/common.yaml @@ -0,0 +1,41 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: bedjet_blec + +bedjet: + - id: bedjet_hub + ble_client_id: bedjet_blec + time_id: sntp_time + +climate: + - platform: bedjet + name: My Bedjet + bedjet_id: bedjet_hub + heat_mode: extended + temperature_source: ambient + +fan: + - platform: bedjet + name: My Bedjet fan + bedjet_id: bedjet_hub + +sensor: + - platform: bedjet + ambient_temperature: + name: My BedJet Ambient Temperature + outlet_temperature: + name: My BedJet Outlet Temperature diff --git a/tests/components/bedjet/test.esp32-ard.yaml b/tests/components/bedjet/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bedjet/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bedjet/test.esp32-c3-ard.yaml b/tests/components/bedjet/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bedjet/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bedjet/test.esp32-c3-idf.yaml b/tests/components/bedjet/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bedjet/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bedjet/test.esp32-idf.yaml b/tests/components/bedjet/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bedjet/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/beken_spi_led_strip/test.bk72xx-ard.yaml b/tests/components/beken_spi_led_strip/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..15409caeaf --- /dev/null +++ b/tests/components/beken_spi_led_strip/test.bk72xx-ard.yaml @@ -0,0 +1,7 @@ +light: + - platform: beken_spi_led_strip + rgb_order: GRB + pin: P16 + num_leds: 30 + chipset: ws2812 + name: "My Light" diff --git a/tests/components/bh1750/test.esp32-ard.yaml b/tests/components/bh1750/test.esp32-ard.yaml new file mode 100644 index 0000000000..b10ec231ae --- /dev/null +++ b/tests/components/bh1750/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 16 + sda: 17 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.esp32-c3-ard.yaml b/tests/components/bh1750/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e367de3845 --- /dev/null +++ b/tests/components/bh1750/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 5 + sda: 4 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.esp32-c3-idf.yaml b/tests/components/bh1750/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e367de3845 --- /dev/null +++ b/tests/components/bh1750/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 5 + sda: 4 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.esp32-idf.yaml b/tests/components/bh1750/test.esp32-idf.yaml new file mode 100644 index 0000000000..b10ec231ae --- /dev/null +++ b/tests/components/bh1750/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 16 + sda: 17 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.esp8266-ard.yaml b/tests/components/bh1750/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e367de3845 --- /dev/null +++ b/tests/components/bh1750/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 5 + sda: 4 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/bh1750/test.rp2040-ard.yaml b/tests/components/bh1750/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e367de3845 --- /dev/null +++ b/tests/components/bh1750/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_bh1750 + scl: 5 + sda: 4 + +sensor: + - platform: bh1750 + name: Living Room Brightness + address: 0x23 + update_interval: 30s diff --git a/tests/components/binary_sensor_map/common.yaml b/tests/components/binary_sensor_map/common.yaml new file mode 100644 index 0000000000..8ffdd1f379 --- /dev/null +++ b/tests/components/binary_sensor_map/common.yaml @@ -0,0 +1,61 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + - platform: template + id: bin2 + lambda: |- + if (millis() > 20000) { + return true; + } else { + return false; + } + - platform: template + id: bin3 + lambda: |- + if (millis() > 30000) { + return true; + } else { + return false; + } + +sensor: + - platform: binary_sensor_map + name: Binary Sensor Map + type: group + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 diff --git a/tests/components/binary_sensor_map/test.esp32-ard.yaml b/tests/components/binary_sensor_map/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.esp32-c3-ard.yaml b/tests/components/binary_sensor_map/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml b/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.esp32-idf.yaml b/tests/components/binary_sensor_map/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.esp8266-ard.yaml b/tests/components/binary_sensor_map/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor_map/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.rp2040-ard.yaml b/tests/components/binary_sensor_map/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor_map/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bl0906/common.yaml b/tests/components/bl0906/common.yaml new file mode 100644 index 0000000000..29321a9471 --- /dev/null +++ b/tests/components/bl0906/common.yaml @@ -0,0 +1,69 @@ +uart: + - id: uart_bl0906 + tx_pin: + number: ${tx_pin} + rx_pin: + number: ${rx_pin} + baud_rate: 19200 + +sensor: + - platform: bl0906 + id: bl + frequency: + name: 'Frequency' + temperature: + name: 'Temperature' + voltage: + name: 'Voltage' + channel_1: + current: + name: 'Current_1' + power: + name: 'Power_1' + energy: + name: 'Energy_1' + channel_2: + current: + name: 'Current_2' + power: + name: 'Power_2' + energy: + name: 'Energy_2' + channel_3: + current: + name: 'Current_3' + power: + name: 'Power_3' + energy: + name: 'Energy_3' + channel_4: + current: + name: 'Current_4' + power: + name: 'Power_4' + energy: + name: 'Energy_4' + channel_5: + current: + name: 'Current_5' + power: + name: 'Power_5' + energy: + name: 'Energy_5' + channel_6: + current: + name: 'Current_6' + power: + name: 'Power_6' + energy: + name: 'Energy_6' + total_energy: + name: 'Total_Energy' + total_power: + name: 'Total_Power' + +button: + - platform: template + id: reset + on_press: + - bl0906.reset_energy: bl diff --git a/tests/components/bl0906/test.esp32-ard.yaml b/tests/components/bl0906/test.esp32-ard.yaml new file mode 100644 index 0000000000..811f6b72a6 --- /dev/null +++ b/tests/components/bl0906/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO12 + rx_pin: GPIO14 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-c3-ard.yaml b/tests/components/bl0906/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c79d14c740 --- /dev/null +++ b/tests/components/bl0906/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO7 + rx_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-c3-idf.yaml b/tests/components/bl0906/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c79d14c740 --- /dev/null +++ b/tests/components/bl0906/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO7 + rx_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-idf.yaml b/tests/components/bl0906/test.esp32-idf.yaml new file mode 100644 index 0000000000..811f6b72a6 --- /dev/null +++ b/tests/components/bl0906/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO12 + rx_pin: GPIO14 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp8266-ard.yaml b/tests/components/bl0906/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3b44f9c9c3 --- /dev/null +++ b/tests/components/bl0906/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO1 + rx_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.rp2040-ard.yaml b/tests/components/bl0906/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/bl0906/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bl0939/test.esp32-ard.yaml b/tests/components/bl0939/test.esp32-ard.yaml new file mode 100644 index 0000000000..df0e683b2f --- /dev/null +++ b/tests/components/bl0939/test.esp32-ard.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.esp32-c3-ard.yaml b/tests/components/bl0939/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4c92ccb7dd --- /dev/null +++ b/tests/components/bl0939/test.esp32-c3-ard.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.esp32-c3-idf.yaml b/tests/components/bl0939/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4c92ccb7dd --- /dev/null +++ b/tests/components/bl0939/test.esp32-c3-idf.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.esp32-idf.yaml b/tests/components/bl0939/test.esp32-idf.yaml new file mode 100644 index 0000000000..df0e683b2f --- /dev/null +++ b/tests/components/bl0939/test.esp32-idf.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.esp8266-ard.yaml b/tests/components/bl0939/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4c92ccb7dd --- /dev/null +++ b/tests/components/bl0939/test.esp8266-ard.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0939/test.rp2040-ard.yaml b/tests/components/bl0939/test.rp2040-ard.yaml new file mode 100644 index 0000000000..4c92ccb7dd --- /dev/null +++ b/tests/components/bl0939/test.rp2040-ard.yaml @@ -0,0 +1,26 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0939 + voltage: + name: BL0939 Voltage + current_1: + name: BL0939 Current 1 + current_2: + name: BL0939 Current 2 + active_power_1: + name: BL0939 Active Power 1 + active_power_2: + name: BL0939 Active Power 2 + energy_1: + name: BL0939 Energy 1 + energy_2: + name: BL0939 Energy 2 + energy_total: + name: BL0939 Total energy diff --git a/tests/components/bl0940/test.esp32-ard.yaml b/tests/components/bl0940/test.esp32-ard.yaml new file mode 100644 index 0000000000..c7d97ca3b9 --- /dev/null +++ b/tests/components/bl0940/test.esp32-ard.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.esp32-c3-ard.yaml b/tests/components/bl0940/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a20f785b02 --- /dev/null +++ b/tests/components/bl0940/test.esp32-c3-ard.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.esp32-c3-idf.yaml b/tests/components/bl0940/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a20f785b02 --- /dev/null +++ b/tests/components/bl0940/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.esp32-idf.yaml b/tests/components/bl0940/test.esp32-idf.yaml new file mode 100644 index 0000000000..c7d97ca3b9 --- /dev/null +++ b/tests/components/bl0940/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.esp8266-ard.yaml b/tests/components/bl0940/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a20f785b02 --- /dev/null +++ b/tests/components/bl0940/test.esp8266-ard.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0940/test.rp2040-ard.yaml b/tests/components/bl0940/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a20f785b02 --- /dev/null +++ b/tests/components/bl0940/test.rp2040-ard.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0940 + voltage: + name: BL0940 Voltage + current: + name: BL0940 Current + power: + name: BL0940 Power + energy: + name: BL0940 Energy + internal_temperature: + name: BL0940 Internal temperature + external_temperature: + name: BL0940 External temperature diff --git a/tests/components/bl0942/test.bk72xx-ard.yaml b/tests/components/bl0942/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..ea61734441 --- /dev/null +++ b/tests/components/bl0942/test.bk72xx-ard.yaml @@ -0,0 +1,27 @@ +uart: + - id: uart_bl0942 + tx_pin: + number: TX1 + rx_pin: + number: RX1 + baud_rate: 2400 + +sensor: + - platform: bl0942 + address: 0 + line_frequency: 50Hz + reset: false + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency + voltage_reference: 15968 + current_reference: 124180 + power_reference: 309.1 + energy_reference: 2653 diff --git a/tests/components/bl0942/test.esp32-ard.yaml b/tests/components/bl0942/test.esp32-ard.yaml new file mode 100644 index 0000000000..4138543967 --- /dev/null +++ b/tests/components/bl0942/test.esp32-ard.yaml @@ -0,0 +1,21 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0942 + reset: true + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.esp32-c3-ard.yaml b/tests/components/bl0942/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8d16efed4f --- /dev/null +++ b/tests/components/bl0942/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.esp32-c3-idf.yaml b/tests/components/bl0942/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8d16efed4f --- /dev/null +++ b/tests/components/bl0942/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.esp32-idf.yaml b/tests/components/bl0942/test.esp32-idf.yaml new file mode 100644 index 0000000000..45ac85aa2a --- /dev/null +++ b/tests/components/bl0942/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.esp8266-ard.yaml b/tests/components/bl0942/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8d16efed4f --- /dev/null +++ b/tests/components/bl0942/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency diff --git a/tests/components/bl0942/test.rp2040-ard.yaml b/tests/components/bl0942/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d07e0c4402 --- /dev/null +++ b/tests/components/bl0942/test.rp2040-ard.yaml @@ -0,0 +1,22 @@ +uart: + - id: uart_bl0939 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: bl0942 + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency + voltage_reference: 15968 + current_reference: 124180 diff --git a/tests/components/ble_client/common.yaml b/tests/components/ble_client/common.yaml new file mode 100644 index 0000000000..b5272d01f0 --- /dev/null +++ b/tests/components/ble_client/common.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: test_blec diff --git a/tests/components/ble_client/test.esp32-ard.yaml b/tests/components/ble_client/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_client/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_client/test.esp32-c3-ard.yaml b/tests/components/ble_client/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_client/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_client/test.esp32-c3-idf.yaml b/tests/components/ble_client/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_client/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_client/test.esp32-idf.yaml b/tests/components/ble_client/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_client/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_presence/common.yaml b/tests/components/ble_presence/common.yaml new file mode 100644 index 0000000000..6e5173eed8 --- /dev/null +++ b/tests/components/ble_presence/common.yaml @@ -0,0 +1,24 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: ble_presence + mac_address: AC:37:43:77:5F:4C + name: ESP32 BLE Tracker Google Home Mini + - platform: ble_presence + service_uuid: 11aa + name: BLE Test Service 16 Presence + - platform: ble_presence + service_uuid: "11223344" + name: BLE Test Service 32 Presence + - platform: ble_presence + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 Presence + - platform: ble_presence + ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + ibeacon_major: 100 + ibeacon_minor: 1 + name: BLE Test iBeacon Presence + - platform: ble_presence + irk: 1234567890abcdef1234567890abcdef + name: "ESP32 BLE Tracker with Identity Resolving Key" + diff --git a/tests/components/ble_presence/test.esp32-ard.yaml b/tests/components/ble_presence/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_presence/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_presence/test.esp32-c3-ard.yaml b/tests/components/ble_presence/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_presence/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_presence/test.esp32-c3-idf.yaml b/tests/components/ble_presence/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_presence/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_presence/test.esp32-idf.yaml b/tests/components/ble_presence/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_presence/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_rssi/common.yaml b/tests/components/ble_rssi/common.yaml new file mode 100644 index 0000000000..43bed1d0e7 --- /dev/null +++ b/tests/components/ble_rssi/common.yaml @@ -0,0 +1,21 @@ +esp32_ble_tracker: + +sensor: + - platform: ble_rssi + mac_address: AC:37:43:77:5F:4C + name: BLE Google Home Mini RSSI value + - platform: ble_rssi + service_uuid: 11aa + name: BLE Test Service 16 + - platform: ble_rssi + service_uuid: "11223344" + name: BLE Test Service 32 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 + - platform: ble_rssi + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test iBeacon UUID + - platform: ble_rssi + irk: 1234567890abcdef1234567890abcdef + name: "BLE Tracker with Identity Resolving Key" diff --git a/tests/components/ble_rssi/test.esp32-ard.yaml b/tests/components/ble_rssi/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_rssi/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_rssi/test.esp32-c3-ard.yaml b/tests/components/ble_rssi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_rssi/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_rssi/test.esp32-c3-idf.yaml b/tests/components/ble_rssi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_rssi/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_rssi/test.esp32-idf.yaml b/tests/components/ble_rssi/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_rssi/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_scanner/common.yaml b/tests/components/ble_scanner/common.yaml new file mode 100644 index 0000000000..935a5a5a19 --- /dev/null +++ b/tests/components/ble_scanner/common.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +text_sensor: + - platform: ble_scanner + name: Scanner diff --git a/tests/components/ble_scanner/test.esp32-ard.yaml b/tests/components/ble_scanner/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_scanner/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_scanner/test.esp32-c3-ard.yaml b/tests/components/ble_scanner/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_scanner/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_scanner/test.esp32-c3-idf.yaml b/tests/components/ble_scanner/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_scanner/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ble_scanner/test.esp32-idf.yaml b/tests/components/ble_scanner/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ble_scanner/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bme280_i2c/common.yaml b/tests/components/bme280_i2c/common.yaml new file mode 100644 index 0000000000..e74ce9bf6d --- /dev/null +++ b/tests/components/bme280_i2c/common.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_bme280 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: bme280_i2c + i2c_id: i2c_bme280 + address: 0x76 + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_i2c/test.esp32-ard.yaml b/tests/components/bme280_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme280_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp32-c3-ard.yaml b/tests/components/bme280_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bme280_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp32-c3-idf.yaml b/tests/components/bme280_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bme280_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp32-idf.yaml b/tests/components/bme280_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme280_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp8266-ard.yaml b/tests/components/bme280_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bme280_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.rp2040-ard.yaml b/tests/components/bme280_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bme280_i2c/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bme280_spi/common.yaml b/tests/components/bme280_spi/common.yaml new file mode 100644 index 0000000000..303ecf9f73 --- /dev/null +++ b/tests/components/bme280_spi/common.yaml @@ -0,0 +1,20 @@ +spi: + - id: spi_bme280 + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +sensor: + - platform: bme280_spi + spi_id: spi_bme280 + cs_pin: ${cs_pin} + temperature: + id: bme280_temperature + name: BME280 Temperature + humidity: + id: bme280_humidity + name: BME280 Humidity + pressure: + id: bme280_pressure + name: BME280 Pressure + update_interval: 15s diff --git a/tests/components/bme280_spi/test.esp32-ard.yaml b/tests/components/bme280_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/bme280_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp32-c3-ard.yaml b/tests/components/bme280_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/bme280_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp32-c3-idf.yaml b/tests/components/bme280_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/bme280_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp32-idf.yaml b/tests/components/bme280_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/bme280_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp8266-ard.yaml b/tests/components/bme280_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dbd158d030 --- /dev/null +++ b/tests/components/bme280_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO14 + mosi_pin: GPIO13 + miso_pin: GPIO12 + cs_pin: GPIO15 + +<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.rp2040-ard.yaml b/tests/components/bme280_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f6c3f1eeca --- /dev/null +++ b/tests/components/bme280_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bme680/test.esp32-ard.yaml b/tests/components/bme680/test.esp32-ard.yaml new file mode 100644 index 0000000000..04d0ed8fe4 --- /dev/null +++ b/tests/components/bme680/test.esp32-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 16 + sda: 17 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.esp32-c3-ard.yaml b/tests/components/bme680/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f12be09d20 --- /dev/null +++ b/tests/components/bme680/test.esp32-c3-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.esp32-c3-idf.yaml b/tests/components/bme680/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f12be09d20 --- /dev/null +++ b/tests/components/bme680/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.esp32-idf.yaml b/tests/components/bme680/test.esp32-idf.yaml new file mode 100644 index 0000000000..04d0ed8fe4 --- /dev/null +++ b/tests/components/bme680/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 16 + sda: 17 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.esp8266-ard.yaml b/tests/components/bme680/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f12be09d20 --- /dev/null +++ b/tests/components/bme680/test.esp8266-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680/test.rp2040-ard.yaml b/tests/components/bme680/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f12be09d20 --- /dev/null +++ b/tests/components/bme680/test.rp2040-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +sensor: + - platform: bme680 + temperature: + name: BME680 Temperature + oversampling: 16x + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + address: 0x77 + heater: + temperature: 320 + duration: 150ms + update_interval: 15s diff --git a/tests/components/bme680_bsec/test.esp32-ard.yaml b/tests/components/bme680_bsec/test.esp32-ard.yaml new file mode 100644 index 0000000000..4f62f13abb --- /dev/null +++ b/tests/components/bme680_bsec/test.esp32-ard.yaml @@ -0,0 +1,29 @@ +i2c: + - id: i2c_bme680 + scl: 16 + sda: 17 + +bme680_bsec: + address: 0x77 + +sensor: + - platform: bme680_bsec + temperature: + name: BME680 Temperature + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + iaq: + name: BME680 IAQ + co2_equivalent: + name: BME680 eCO2 + breath_voc_equivalent: + name: BME680 Breath eVOC + +text_sensor: + - platform: bme680_bsec + iaq_accuracy: + name: BME680 Accuracy diff --git a/tests/components/bme680_bsec/test.esp8266-ard.yaml b/tests/components/bme680_bsec/test.esp8266-ard.yaml new file mode 100644 index 0000000000..84b32d3635 --- /dev/null +++ b/tests/components/bme680_bsec/test.esp8266-ard.yaml @@ -0,0 +1,29 @@ +i2c: + - id: i2c_bme680 + scl: 5 + sda: 4 + +bme680_bsec: + address: 0x77 + +sensor: + - platform: bme680_bsec + temperature: + name: BME680 Temperature + pressure: + name: BME680 Pressure + humidity: + name: BME680 Humidity + gas_resistance: + name: BME680 Gas Sensor + iaq: + name: BME680 IAQ + co2_equivalent: + name: BME680 eCO2 + breath_voc_equivalent: + name: BME680 Breath eVOC + +text_sensor: + - platform: bme680_bsec + iaq_accuracy: + name: BME680 Accuracy diff --git a/tests/components/bme68x_bsec2_i2c/common.yaml b/tests/components/bme68x_bsec2_i2c/common.yaml new file mode 100644 index 0000000000..b8a16ee7bb --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/common.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_bme68x + scl: ${scl_pin} + sda: ${sda_pin} + +bme68x_bsec2_i2c: + address: 0x76 + model: bme688 + algorithm_output: classification + operating_age: 28d + sample_rate: LP + supply_voltage: 3.3V + +sensor: + - platform: bme68x_bsec2 + temperature: + name: BME68X Temperature + pressure: + name: BME68X Pressure + humidity: + name: BME68X Humidity + gas_resistance: + name: BME68X Gas Sensor + iaq: + name: BME68X IAQ + co2_equivalent: + name: BME68X eCO2 + breath_voc_equivalent: + name: BME68X Breath eVOC + +text_sensor: + - platform: bme68x_bsec2 + iaq_accuracy: + name: BME68X Accuracy diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..84a9dd4bb4 --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO6 + sda_pin: GPIO7 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmi160/test.esp32-ard.yaml b/tests/components/bmi160/test.esp32-ard.yaml new file mode 100644 index 0000000000..a8a90c8c87 --- /dev/null +++ b/tests/components/bmi160/test.esp32-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 16 + sda: 17 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.esp32-c3-ard.yaml b/tests/components/bmi160/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..3fd6441980 --- /dev/null +++ b/tests/components/bmi160/test.esp32-c3-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 5 + sda: 4 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.esp32-c3-idf.yaml b/tests/components/bmi160/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3fd6441980 --- /dev/null +++ b/tests/components/bmi160/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 5 + sda: 4 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.esp32-idf.yaml b/tests/components/bmi160/test.esp32-idf.yaml new file mode 100644 index 0000000000..a8a90c8c87 --- /dev/null +++ b/tests/components/bmi160/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 16 + sda: 17 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.esp8266-ard.yaml b/tests/components/bmi160/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3fd6441980 --- /dev/null +++ b/tests/components/bmi160/test.esp8266-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 5 + sda: 4 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmi160/test.rp2040-ard.yaml b/tests/components/bmi160/test.rp2040-ard.yaml new file mode 100644 index 0000000000..3fd6441980 --- /dev/null +++ b/tests/components/bmi160/test.rp2040-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_bmi160 + scl: 5 + sda: 4 + +sensor: + - platform: bmi160 + address: 0x68 + acceleration_x: + name: BMI160 Accel X + acceleration_y: + name: BMI160 Accel Y + acceleration_z: + name: BMI160 Accel z + gyroscope_x: + name: BMI160 Gyro X + gyroscope_y: + name: BMI160 Gyro Y + gyroscope_z: + name: BMI160 Gyro z + temperature: + name: BMI160 Temperature diff --git a/tests/components/bmp085/test.esp32-ard.yaml b/tests/components/bmp085/test.esp32-ard.yaml new file mode 100644 index 0000000000..8a4f714ddd --- /dev/null +++ b/tests/components/bmp085/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 16 + sda: 17 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.esp32-c3-ard.yaml b/tests/components/bmp085/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..76a9fd07ba --- /dev/null +++ b/tests/components/bmp085/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 5 + sda: 4 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.esp32-c3-idf.yaml b/tests/components/bmp085/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..76a9fd07ba --- /dev/null +++ b/tests/components/bmp085/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 5 + sda: 4 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.esp32-idf.yaml b/tests/components/bmp085/test.esp32-idf.yaml new file mode 100644 index 0000000000..8a4f714ddd --- /dev/null +++ b/tests/components/bmp085/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 16 + sda: 17 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.esp8266-ard.yaml b/tests/components/bmp085/test.esp8266-ard.yaml new file mode 100644 index 0000000000..76a9fd07ba --- /dev/null +++ b/tests/components/bmp085/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 5 + sda: 4 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp085/test.rp2040-ard.yaml b/tests/components/bmp085/test.rp2040-ard.yaml new file mode 100644 index 0000000000..76a9fd07ba --- /dev/null +++ b/tests/components/bmp085/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp085 + scl: 5 + sda: 4 + +sensor: + - platform: bmp085 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + filters: + - lambda: >- + return x / powf(1.0 - (x / 44330.0), 5.255); + update_interval: 15s diff --git a/tests/components/bmp280_i2c/common.yaml b/tests/components/bmp280_i2c/common.yaml new file mode 100644 index 0000000000..edf52b2cd4 --- /dev/null +++ b/tests/components/bmp280_i2c/common.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_bmp280 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: bmp280_i2c + i2c_id: i2c_bmp280 + address: 0x77 + temperature: + id: bmp280_temperature + name: Outside Temperature + pressure: + name: Outside Pressure + id: bmp280_pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp280_i2c/test.esp32-ard.yaml b/tests/components/bmp280_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml b/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-idf.yaml b/tests/components/bmp280_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp8266-ard.yaml b/tests/components/bmp280_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.rp2040-ard.yaml b/tests/components/bmp280_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp280_i2c/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/common.yaml b/tests/components/bmp280_spi/common.yaml new file mode 100644 index 0000000000..798804de5b --- /dev/null +++ b/tests/components/bmp280_spi/common.yaml @@ -0,0 +1,18 @@ +spi: + - id: spi_bmp280 + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +sensor: + - platform: bmp280_spi + spi_id: spi_bmp280 + cs_pin: ${cs_pin} + temperature: + id: bmp280_temperature + name: Outside Temperature + pressure: + name: Outside Pressure + id: bmp280_pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp280_spi/test.esp32-ard.yaml b/tests/components/bmp280_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-c3-ard.yaml b/tests/components/bmp280_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-c3-idf.yaml b/tests/components/bmp280_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-idf.yaml b/tests/components/bmp280_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp8266-ard.yaml b/tests/components/bmp280_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dbd158d030 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO14 + mosi_pin: GPIO13 + miso_pin: GPIO12 + cs_pin: GPIO15 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.rp2040-ard.yaml b/tests/components/bmp280_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f6c3f1eeca --- /dev/null +++ b/tests/components/bmp280_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/common.yaml b/tests/components/bmp3xx_i2c/common.yaml new file mode 100644 index 0000000000..6641b7a1b8 --- /dev/null +++ b/tests/components/bmp3xx_i2c/common.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_bmp3xx + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: bmp3xx_i2c + i2c_id: i2c_bmp3xx + address: 0x77 + temperature: + name: BMP Temperature + oversampling: 16x + pressure: + name: BMP Pressure + iir_filter: 2X diff --git a/tests/components/bmp3xx_i2c/test.esp32-ard.yaml b/tests/components/bmp3xx_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bmp3xx_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp32-c3-ard.yaml b/tests/components/bmp3xx_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp3xx_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp32-idf.yaml b/tests/components/bmp3xx_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bmp3xx_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp8266-ard.yaml b/tests/components/bmp3xx_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp3xx_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.rp2040-ard.yaml b/tests/components/bmp3xx_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp3xx_i2c/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/common.yaml b/tests/components/bmp3xx_spi/common.yaml new file mode 100644 index 0000000000..8d5f897661 --- /dev/null +++ b/tests/components/bmp3xx_spi/common.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_bmp3xx + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +sensor: + - platform: bmp3xx_spi + spi_id: spi_bmp3xx + cs_pin: ${cs_pin} + temperature: + name: BMP Temperature + oversampling: 16x + pressure: + name: BMP Pressure + iir_filter: 2X diff --git a/tests/components/bmp3xx_spi/test.esp32-ard.yaml b/tests/components/bmp3xx_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/bmp3xx_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp32-c3-ard.yaml b/tests/components/bmp3xx_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/bmp3xx_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml b/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp32-idf.yaml b/tests/components/bmp3xx_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/bmp3xx_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp8266-ard.yaml b/tests/components/bmp3xx_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dbd158d030 --- /dev/null +++ b/tests/components/bmp3xx_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO14 + mosi_pin: GPIO13 + miso_pin: GPIO12 + cs_pin: GPIO15 + +<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.rp2040-ard.yaml b/tests/components/bmp3xx_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f6c3f1eeca --- /dev/null +++ b/tests/components/bmp3xx_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bmp581/test.esp32-ard.yaml b/tests/components/bmp581/test.esp32-ard.yaml new file mode 100644 index 0000000000..a464b8ce6a --- /dev/null +++ b/tests/components/bmp581/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 16 + sda: 17 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.esp32-c3-ard.yaml b/tests/components/bmp581/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..29d27afb90 --- /dev/null +++ b/tests/components/bmp581/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 5 + sda: 4 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.esp32-c3-idf.yaml b/tests/components/bmp581/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..29d27afb90 --- /dev/null +++ b/tests/components/bmp581/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 5 + sda: 4 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.esp32-idf.yaml b/tests/components/bmp581/test.esp32-idf.yaml new file mode 100644 index 0000000000..a464b8ce6a --- /dev/null +++ b/tests/components/bmp581/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 16 + sda: 17 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.esp8266-ard.yaml b/tests/components/bmp581/test.esp8266-ard.yaml new file mode 100644 index 0000000000..29d27afb90 --- /dev/null +++ b/tests/components/bmp581/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 5 + sda: 4 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bmp581/test.rp2040-ard.yaml b/tests/components/bmp581/test.rp2040-ard.yaml new file mode 100644 index 0000000000..29d27afb90 --- /dev/null +++ b/tests/components/bmp581/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_bmp581 + scl: 5 + sda: 4 + +sensor: + - platform: bmp581 + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x diff --git a/tests/components/bp1658cj/test.esp32-ard.yaml b/tests/components/bp1658cj/test.esp32-ard.yaml new file mode 100644 index 0000000000..5f9e25d3bd --- /dev/null +++ b/tests/components/bp1658cj/test.esp32-ard.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 16 + data_pin: 17 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.esp32-c3-ard.yaml b/tests/components/bp1658cj/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..74d3155371 --- /dev/null +++ b/tests/components/bp1658cj/test.esp32-c3-ard.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 5 + data_pin: 4 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.esp32-c3-idf.yaml b/tests/components/bp1658cj/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..74d3155371 --- /dev/null +++ b/tests/components/bp1658cj/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 5 + data_pin: 4 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.esp32-idf.yaml b/tests/components/bp1658cj/test.esp32-idf.yaml new file mode 100644 index 0000000000..5f9e25d3bd --- /dev/null +++ b/tests/components/bp1658cj/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 16 + data_pin: 17 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.esp8266-ard.yaml b/tests/components/bp1658cj/test.esp8266-ard.yaml new file mode 100644 index 0000000000..74d3155371 --- /dev/null +++ b/tests/components/bp1658cj/test.esp8266-ard.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 5 + data_pin: 4 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp1658cj/test.rp2040-ard.yaml b/tests/components/bp1658cj/test.rp2040-ard.yaml new file mode 100644 index 0000000000..74d3155371 --- /dev/null +++ b/tests/components/bp1658cj/test.rp2040-ard.yaml @@ -0,0 +1,22 @@ +bp1658cj: + clock_pin: 5 + data_pin: 4 + max_power_color_channels: 4 + max_power_white_channels: 6 + +output: + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 diff --git a/tests/components/bp5758d/test.esp32-ard.yaml b/tests/components/bp5758d/test.esp32-ard.yaml new file mode 100644 index 0000000000..b7929a0518 --- /dev/null +++ b/tests/components/bp5758d/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 16 + data_pin: 17 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.esp32-c3-ard.yaml b/tests/components/bp5758d/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ec74e935cd --- /dev/null +++ b/tests/components/bp5758d/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 5 + data_pin: 4 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.esp32-c3-idf.yaml b/tests/components/bp5758d/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ec74e935cd --- /dev/null +++ b/tests/components/bp5758d/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 5 + data_pin: 4 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.esp32-idf.yaml b/tests/components/bp5758d/test.esp32-idf.yaml new file mode 100644 index 0000000000..b7929a0518 --- /dev/null +++ b/tests/components/bp5758d/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 16 + data_pin: 17 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.esp8266-ard.yaml b/tests/components/bp5758d/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ec74e935cd --- /dev/null +++ b/tests/components/bp5758d/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 5 + data_pin: 4 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/bp5758d/test.rp2040-ard.yaml b/tests/components/bp5758d/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ec74e935cd --- /dev/null +++ b/tests/components/bp5758d/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +bp5758d: + clock_pin: 5 + data_pin: 4 + +output: + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 diff --git a/tests/components/button/common.yaml b/tests/components/button/common.yaml new file mode 100644 index 0000000000..d5978601f4 --- /dev/null +++ b/tests/components/button/common.yaml @@ -0,0 +1,6 @@ +button: + - platform: template + name: Button + id: some_button + on_press: + - logger.log: Button pressed diff --git a/tests/components/button/test.esp32-ard.yaml b/tests/components/button/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.esp32-c3-ard.yaml b/tests/components/button/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.esp32-c3-idf.yaml b/tests/components/button/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.esp32-idf.yaml b/tests/components/button/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.esp8266-ard.yaml b/tests/components/button/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.rp2040-ard.yaml b/tests/components/button/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bytebuffer/common.yaml b/tests/components/bytebuffer/common.yaml new file mode 100644 index 0000000000..177f487e1e --- /dev/null +++ b/tests/components/bytebuffer/common.yaml @@ -0,0 +1,161 @@ +bytebuffer: + +esphome: + on_boot: + - lambda: |- + using namespace bytebuffer; + auto buf = ByteBuffer(16); + assert(buf.get_endianness() == LITTLE); + assert(buf.get_remaining() == 16); + buf.set_limit(10); + assert(buf.get_capacity() == 16); + buf.put_uint8(1); + assert(buf.get_remaining() == 9); + buf.put_uint16(0xABCD); + auto da = buf.get_data(); + assert(buf.get_uint8(0) == 1); + auto x = buf.get_uint16(1); + assert(buf.get_uint16(1) == 0xABCD); + assert(buf.get_remaining() == 7); + buf.put_uint32(0x12345678UL); + assert(buf.get_uint32(3) == 0x12345678UL); + assert(buf.get_remaining() == 3); + assert(buf.get_data()[1] == 0xCD); + assert(buf.get_data()[2] == 0xAB); + assert(buf.get_data()[3] == 0x78); + assert(buf.get_data()[4] == 0x56); + assert(buf.get_data()[5] == 0x34); + assert(buf.get_data()[6] == 0x12); + buf.flip(); + assert(buf.get_capacity() == 16); + assert(buf.get_uint32(3) == 0x12345678UL); + assert(buf.get_uint8(0) == 1); + assert(buf.get_uint16(1) == 0xABCD); + buf.put_uint16(0x1234, 1); + assert(buf.get_uint16(1) == 0x1234); + assert(buf.get_remaining() == 7); + assert(buf.get_uint8() == 1); + assert(buf.get_uint16() == 0x1234); + assert(buf.get_uint32() == 0x12345678ul); + assert(buf.get_remaining() == 0); + assert(buf.get_remaining() == 0); + buf.rewind(); + buf.big_endian(); + assert(buf.get_remaining() == 7); + assert(buf.get_uint8() == 1); + assert(buf.get_uint16() == 0x3412); + buf.mark(); + assert(buf.get_uint32() == 0x78563412ul); + assert(buf.get_remaining() == 0); + buf.reset(); + assert(buf.get_remaining() == 4); + assert(buf.get_uint32() == 0x78563412ul); + auto buf1 = ByteBuffer::wrap(buf.get_data().data(), buf.get_limit()); + buf.clear(); + assert(buf.get_position() == 0); + assert(buf.get_capacity() == 16); + assert(buf.get_limit() == 16); + assert(buf1.get_remaining() == 7); + assert(buf1.get_capacity() == 7); + buf1.set_position(3); + assert(buf1.get_uint32() == 0x12345678ul); + buf1.clear(); + assert(buf1.get_limit() == 7); + assert(buf1.get_capacity() == 7); + assert(buf1.get_position() == 0); + float f = 1.2345; + buf1.put_float(f); + buf1.flip(); + assert(buf1.get_remaining() == 4); + assert(buf1.get_float() == f); + buf1.clear(); + buf1.put_uint16(-32760); + buf1.put_uint24(-302760); + buf1.flip(); + assert(buf1.get_int16() == -32760); + assert(buf1.get_int24() == -302760); + uint8_t arr[4] = {0x10, 0x20, 0x30, 0x40}; + buf1 = ByteBuffer::wrap(arr, 4); + assert(buf1.get_capacity() == 4); + assert(buf1.get_limit() == 4); + assert(buf1.get_position() == 0); + assert(buf1.get_uint32() == 0x40302010UL); + assert(buf1.get_position() == 4); + assert(buf1.get_remaining() == 0); + std::vector vec{}; + vec.push_back(0x10); + vec.push_back(0x20); + vec.push_back(0x30); + vec.push_back(0x40); + buf1 = ByteBuffer::wrap(vec); + assert(buf1.get_capacity() == 4); + assert(buf1.get_limit() == 4); + assert(buf1.get_position() == 0); + buf1.mark(); + buf1.reset(); + assert(buf1.get_uint32() == 0x40302010UL); + buf = ByteBuffer::wrap(true); + assert(buf.get_bool() == true); + buf = ByteBuffer::wrap((uint8_t)0xFE); + assert(buf.get_uint8() == 0xFE); + buf = ByteBuffer::wrap((uint16_t)0xA5A6, BIG); + assert(buf.get_remaining() == 2); + assert(buf.get_position() == 0); + assert(buf.get_capacity() == 2); + assert(buf.get_endianness() == BIG); + assert(buf.get_data()[0] == 0xA5); + assert(buf.get_uint16() == 0xA5A6); + buf.flip(); + buf.little_endian(); + assert(buf.get_uint16() == 0xA6A5); + buf = ByteBuffer::wrap(f, BIG); + assert(buf.get_float() == f); + double d = 1.2345678E7; + buf = ByteBuffer::wrap(d, BIG); + assert(buf.get_double() == d); + buf = ByteBuffer::wrap({1, 2, 3, 4}, BIG); + assert(buf.get_endianness() == BIG); + assert(buf.get_remaining() == 4); + assert(buf.get_data()[2] == 3); + buf.little_endian(); + assert(buf.get_data()[2] == 3); + assert(buf.get_uint16() == 0x0201); + buf.big_endian(); + assert(buf.get_uint16() == 0x0304); + buf.rewind(); + vec = buf.get_vector(3); + assert(buf.get_remaining() == 1); + assert(vec[0] == 1); + assert(vec.size() == 3); + buf = ByteBuffer(10); + buf.put_vector(vec); + assert(buf.get_remaining() == 7); + buf.flip(); + assert(buf.get_remaining() == 3); + assert(buf.get_uint24() == 0x030201); + buf = ByteBuffer(64); + buf.put_uint8(1, 1); + buf.put_uint16(16, 2); + buf.put_uint32(1232, 4); + buf.put_uint64(123432ul, 8); + buf.put_float(1.2f, 16); + buf.put_int24(0x678, 20); + + assert(buf.get_uint8(1) == 1); + assert(buf.get(1) == 1); + assert(buf.get_uint16(2) == 16); + assert(buf.get(2) == 16); + assert(buf.get_uint32(4) == 1232); + assert(buf.get(4) == 1232); + assert(buf.get_uint64(8) == 123432ul); + assert(buf.get(8) == 123432ul); + assert(buf.get_float(16) == 1.2f); + assert(buf.get(16) == 1.2f); + assert(buf.get_int24(20) == 0x678); + buf.clear(); + buf.put(1.234, 10); + double dx = buf.get(10); + assert(dx == 1.234); + buf.put((uint16_t)1, 10); + assert(buf.get_uint16(10) == 1); + ESP_LOGD("bytebuffer", "******************** All tests succeeded"); diff --git a/tests/components/bytebuffer/test.esp32-ard.yaml b/tests/components/bytebuffer/test.esp32-ard.yaml new file mode 100644 index 0000000000..380ca87628 --- /dev/null +++ b/tests/components/bytebuffer/test.esp32-ard.yaml @@ -0,0 +1 @@ +!include common.yaml diff --git a/tests/components/bytebuffer/test.esp32-c3-ard.yaml b/tests/components/bytebuffer/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..380ca87628 --- /dev/null +++ b/tests/components/bytebuffer/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +!include common.yaml diff --git a/tests/components/bytebuffer/test.esp32-c3-idf.yaml b/tests/components/bytebuffer/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..380ca87628 --- /dev/null +++ b/tests/components/bytebuffer/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +!include common.yaml diff --git a/tests/components/bytebuffer/test.esp32-idf.yaml b/tests/components/bytebuffer/test.esp32-idf.yaml new file mode 100644 index 0000000000..380ca87628 --- /dev/null +++ b/tests/components/bytebuffer/test.esp32-idf.yaml @@ -0,0 +1 @@ +!include common.yaml diff --git a/tests/components/bytebuffer/test.esp8266-ard.yaml b/tests/components/bytebuffer/test.esp8266-ard.yaml new file mode 100644 index 0000000000..380ca87628 --- /dev/null +++ b/tests/components/bytebuffer/test.esp8266-ard.yaml @@ -0,0 +1 @@ +!include common.yaml diff --git a/tests/components/bytebuffer/test.host.yaml b/tests/components/bytebuffer/test.host.yaml new file mode 100644 index 0000000000..380ca87628 --- /dev/null +++ b/tests/components/bytebuffer/test.host.yaml @@ -0,0 +1 @@ +!include common.yaml diff --git a/tests/components/bytebuffer/test.rp2040-ard.yaml b/tests/components/bytebuffer/test.rp2040-ard.yaml new file mode 100644 index 0000000000..380ca87628 --- /dev/null +++ b/tests/components/bytebuffer/test.rp2040-ard.yaml @@ -0,0 +1 @@ +!include common.yaml diff --git a/tests/components/canbus/common.yaml b/tests/components/canbus/common.yaml new file mode 100644 index 0000000000..fd146cc3a3 --- /dev/null +++ b/tests/components/canbus/common.yaml @@ -0,0 +1,46 @@ +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 4 + tx_pin: 5 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: Truth + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + } + +button: + - platform: template + name: Canbus Actions + on_press: + - canbus.send: "abc" + - canbus.send: [0, 1, 2] + - canbus.send: !lambda return {0, 1, 2}; diff --git a/tests/components/canbus/test.esp32-ard.yaml b/tests/components/canbus/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/canbus/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/canbus/test.esp32-c3-ard.yaml b/tests/components/canbus/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/canbus/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/canbus/test.esp32-c3-idf.yaml b/tests/components/canbus/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/canbus/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/canbus/test.esp32-idf.yaml b/tests/components/canbus/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/canbus/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/cap1188/test.esp32-ard.yaml b/tests/components/cap1188/test.esp32-ard.yaml new file mode 100644 index 0000000000..efd1d60217 --- /dev/null +++ b/tests/components/cap1188/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 16 + sda: 17 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 15 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.esp32-c3-ard.yaml b/tests/components/cap1188/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c6d3c95942 --- /dev/null +++ b/tests/components/cap1188/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 5 + sda: 4 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 6 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.esp32-c3-idf.yaml b/tests/components/cap1188/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c6d3c95942 --- /dev/null +++ b/tests/components/cap1188/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 5 + sda: 4 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 6 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.esp32-idf.yaml b/tests/components/cap1188/test.esp32-idf.yaml new file mode 100644 index 0000000000..efd1d60217 --- /dev/null +++ b/tests/components/cap1188/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 16 + sda: 17 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 15 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.esp8266-ard.yaml b/tests/components/cap1188/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7573d45140 --- /dev/null +++ b/tests/components/cap1188/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 5 + sda: 4 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 15 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/cap1188/test.rp2040-ard.yaml b/tests/components/cap1188/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c6d3c95942 --- /dev/null +++ b/tests/components/cap1188/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_cap1188 + scl: 5 + sda: 4 + +cap1188: + id: cap1188_component + address: 0x29 + reset_pin: 6 + touch_threshold: 0x20 + allow_multiple_touches: true diff --git a/tests/components/captive_portal/common.yaml b/tests/components/captive_portal/common.yaml new file mode 100644 index 0000000000..25bc4a887a --- /dev/null +++ b/tests/components/captive_portal/common.yaml @@ -0,0 +1,5 @@ +wifi: + ssid: MySSID + password: password1 + +captive_portal: diff --git a/tests/components/captive_portal/test.esp32-ard.yaml b/tests/components/captive_portal/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/captive_portal/test.esp32-c3-ard.yaml b/tests/components/captive_portal/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/captive_portal/test.esp32-c3-idf.yaml b/tests/components/captive_portal/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/captive_portal/test.esp32-idf.yaml b/tests/components/captive_portal/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/captive_portal/test.esp8266-ard.yaml b/tests/components/captive_portal/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ccs811/test.esp32-ard.yaml b/tests/components/ccs811/test.esp32-ard.yaml new file mode 100644 index 0000000000..08b3a48cc7 --- /dev/null +++ b/tests/components/ccs811/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 16 + sda: 17 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.esp32-c3-ard.yaml b/tests/components/ccs811/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..26ec7807e4 --- /dev/null +++ b/tests/components/ccs811/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 5 + sda: 4 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.esp32-c3-idf.yaml b/tests/components/ccs811/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..26ec7807e4 --- /dev/null +++ b/tests/components/ccs811/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 5 + sda: 4 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.esp32-idf.yaml b/tests/components/ccs811/test.esp32-idf.yaml new file mode 100644 index 0000000000..08b3a48cc7 --- /dev/null +++ b/tests/components/ccs811/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 16 + sda: 17 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.esp8266-ard.yaml b/tests/components/ccs811/test.esp8266-ard.yaml new file mode 100644 index 0000000000..26ec7807e4 --- /dev/null +++ b/tests/components/ccs811/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 5 + sda: 4 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/ccs811/test.rp2040-ard.yaml b/tests/components/ccs811/test.rp2040-ard.yaml new file mode 100644 index 0000000000..26ec7807e4 --- /dev/null +++ b/tests/components/ccs811/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ccs811 + scl: 5 + sda: 4 + +sensor: + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + baseline: 0x4242 + update_interval: 30s diff --git a/tests/components/cd74hc4067/test.esp32-ard.yaml b/tests/components/cd74hc4067/test.esp32-ard.yaml new file mode 100644 index 0000000000..71a1238ccc --- /dev/null +++ b/tests/components/cd74hc4067/test.esp32-ard.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 12 + pin_s1: 13 + pin_s2: 14 + pin_s3: 15 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 39 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.esp32-c3-ard.yaml b/tests/components/cd74hc4067/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5aa653d26c --- /dev/null +++ b/tests/components/cd74hc4067/test.esp32-c3-ard.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 2 + pin_s1: 3 + pin_s2: 4 + pin_s3: 5 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 0 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.esp32-c3-idf.yaml b/tests/components/cd74hc4067/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5aa653d26c --- /dev/null +++ b/tests/components/cd74hc4067/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 2 + pin_s1: 3 + pin_s2: 4 + pin_s3: 5 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 0 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.esp32-idf.yaml b/tests/components/cd74hc4067/test.esp32-idf.yaml new file mode 100644 index 0000000000..71a1238ccc --- /dev/null +++ b/tests/components/cd74hc4067/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 12 + pin_s1: 13 + pin_s2: 14 + pin_s3: 15 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 39 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.esp8266-ard.yaml b/tests/components/cd74hc4067/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8bcce5bc17 --- /dev/null +++ b/tests/components/cd74hc4067/test.esp8266-ard.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 12 + pin_s1: 13 + pin_s2: 14 + pin_s3: 15 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: A0 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/cd74hc4067/test.rp2040-ard.yaml b/tests/components/cd74hc4067/test.rp2040-ard.yaml new file mode 100644 index 0000000000..75adcce796 --- /dev/null +++ b/tests/components/cd74hc4067/test.rp2040-ard.yaml @@ -0,0 +1,18 @@ +cd74hc4067: + pin_s0: 2 + pin_s1: 3 + pin_s2: 4 + pin_s3: 5 + +sensor: + - platform: adc + id: esp_adc_sensor + pin: 26 + - platform: cd74hc4067 + id: cd74hc4067_adc_0 + number: 0 + sensor: esp_adc_sensor + - platform: cd74hc4067 + id: cd74hc4067_adc_1 + number: 1 + sensor: esp_adc_sensor diff --git a/tests/components/ch422g/common.yaml b/tests/components/ch422g/common.yaml new file mode 100644 index 0000000000..d65956ecac --- /dev/null +++ b/tests/components/ch422g/common.yaml @@ -0,0 +1,27 @@ +ch422g: + - id: ch422g_hub + +binary_sensor: + - platform: gpio + id: ch422g_input + name: CH422G Binary Sensor + pin: + ch422g: ch422g_hub + number: 1 + mode: INPUT + inverted: true +output: + - platform: gpio + id: ch422_out_0 + pin: + ch422g: ch422g_hub + number: 0 + mode: OUTPUT + inverted: false + - platform: gpio + id: ch422_out_11 + pin: + ch422g: ch422g_hub + number: 11 + mode: OUTPUT_OPEN_DRAIN + inverted: true diff --git a/tests/components/ch422g/test.esp32-ard.yaml b/tests/components/ch422g/test.esp32-ard.yaml new file mode 100644 index 0000000000..cd3f1bbeef --- /dev/null +++ b/tests/components/ch422g/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-c3-ard.yaml b/tests/components/ch422g/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..cd822cb308 --- /dev/null +++ b/tests/components/ch422g/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-c3-idf.yaml b/tests/components/ch422g/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..cd822cb308 --- /dev/null +++ b/tests/components/ch422g/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-idf.yaml b/tests/components/ch422g/test.esp32-idf.yaml new file mode 100644 index 0000000000..cd3f1bbeef --- /dev/null +++ b/tests/components/ch422g/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp8266-ard.yaml b/tests/components/ch422g/test.esp8266-ard.yaml new file mode 100644 index 0000000000..cd822cb308 --- /dev/null +++ b/tests/components/ch422g/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.rp2040-ard.yaml b/tests/components/ch422g/test.rp2040-ard.yaml new file mode 100644 index 0000000000..cd822cb308 --- /dev/null +++ b/tests/components/ch422g/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/climate_ir_lg/test.esp32-ard.yaml b/tests/components/climate_ir_lg/test.esp32-ard.yaml new file mode 100644 index 0000000000..e714bf0686 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/climate_ir_lg/test.esp32-c3-ard.yaml b/tests/components/climate_ir_lg/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e714bf0686 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e714bf0686 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/climate_ir_lg/test.esp32-idf.yaml b/tests/components/climate_ir_lg/test.esp32-idf.yaml new file mode 100644 index 0000000000..e714bf0686 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/climate_ir_lg/test.esp8266-ard.yaml b/tests/components/climate_ir_lg/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7482bf0580 --- /dev/null +++ b/tests/components/climate_ir_lg/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate diff --git a/tests/components/color/common.yaml b/tests/components/color/common.yaml new file mode 100644 index 0000000000..88524e6a5f --- /dev/null +++ b/tests/components/color/common.yaml @@ -0,0 +1,20 @@ +color: + - id: kbx_red + red: 100% + green_int: 123 + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% + - id: kbx_green + hex: "3DEC55" + - id: kbx_green_1 + hex: 3DEC55 + - id: cps_red + hex: 800000 + - id: cps_green + hex: 008000 + - id: cps_blue + hex: 000080 + diff --git a/tests/components/color/test.esp32-ard.yaml b/tests/components/color/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/color/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/color/test.esp32-c3-ard.yaml b/tests/components/color/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/color/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/color/test.esp32-c3-idf.yaml b/tests/components/color/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/color/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/color/test.esp32-idf.yaml b/tests/components/color/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/color/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/color/test.esp8266-ard.yaml b/tests/components/color/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/color/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/color/test.rp2040-ard.yaml b/tests/components/color/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/color/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/color_temperature/test.esp32-ard.yaml b/tests/components/color_temperature/test.esp32-ard.yaml new file mode 100644 index 0000000000..608907d2fc --- /dev/null +++ b/tests/components/color_temperature/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.esp32-c3-ard.yaml b/tests/components/color_temperature/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8d3faa54ee --- /dev/null +++ b/tests/components/color_temperature/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.esp32-c3-idf.yaml b/tests/components/color_temperature/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8d3faa54ee --- /dev/null +++ b/tests/components/color_temperature/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.esp32-idf.yaml b/tests/components/color_temperature/test.esp32-idf.yaml new file mode 100644 index 0000000000..608907d2fc --- /dev/null +++ b/tests/components/color_temperature/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.esp8266-ard.yaml b/tests/components/color_temperature/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ed0bfb6aa4 --- /dev/null +++ b/tests/components/color_temperature/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 + - platform: esp8266_pwm + id: light_output_2 + pin: 13 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/color_temperature/test.rp2040-ard.yaml b/tests/components/color_temperature/test.rp2040-ard.yaml new file mode 100644 index 0000000000..887ad1c857 --- /dev/null +++ b/tests/components/color_temperature/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 + - platform: rp2040_pwm + id: light_output_2 + pin: 13 + +light: + - platform: color_temperature + name: Lights + color_temperature: light_output_1 + brightness: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml new file mode 100644 index 0000000000..62246190af --- /dev/null +++ b/tests/components/combination/common.yaml @@ -0,0 +1,76 @@ +sensor: + - platform: template + id: template_temperature1 + lambda: |- + if (millis() > 10000) { + return 0.6; + } else { + return 0.0; + } + - platform: template + id: template_temperature2 + lambda: |- + if (millis() > 20000) { + return 0.8; + } else { + return 0.0; + } + - platform: combination + type: kalman + name: Kalman-filtered temperature + process_std_dev: 0.00139 + sources: + - source: template_temperature1 + error: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: template_temperature1 + coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + - source: template_temperature2 + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: template_temperature1 + - source: template_temperature2 diff --git a/tests/components/combination/test.esp32-ard.yaml b/tests/components/combination/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/combination/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/combination/test.esp32-c3-ard.yaml b/tests/components/combination/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/combination/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/combination/test.esp32-c3-idf.yaml b/tests/components/combination/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/combination/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/combination/test.esp32-idf.yaml b/tests/components/combination/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/combination/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/combination/test.esp8266-ard.yaml b/tests/components/combination/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/combination/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/combination/test.rp2040-ard.yaml b/tests/components/combination/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/combination/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/coolix/test.esp32-ard.yaml b/tests/components/coolix/test.esp32-ard.yaml new file mode 100644 index 0000000000..0f9518d2cf --- /dev/null +++ b/tests/components/coolix/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/coolix/test.esp32-c3-ard.yaml b/tests/components/coolix/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0f9518d2cf --- /dev/null +++ b/tests/components/coolix/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/coolix/test.esp32-c3-idf.yaml b/tests/components/coolix/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0f9518d2cf --- /dev/null +++ b/tests/components/coolix/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/coolix/test.esp32-idf.yaml b/tests/components/coolix/test.esp32-idf.yaml new file mode 100644 index 0000000000..0f9518d2cf --- /dev/null +++ b/tests/components/coolix/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/coolix/test.esp8266-ard.yaml b/tests/components/coolix/test.esp8266-ard.yaml new file mode 100644 index 0000000000..61de8c7558 --- /dev/null +++ b/tests/components/coolix/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: coolix + name: Coolix Climate diff --git a/tests/components/copy/test.esp32-ard.yaml b/tests/components/copy/test.esp32-ard.yaml new file mode 100644 index 0000000000..806dbfe9f3 --- /dev/null +++ b/tests/components/copy/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.esp32-c3-ard.yaml b/tests/components/copy/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..554638f462 --- /dev/null +++ b/tests/components/copy/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 2 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.esp32-c3-idf.yaml b/tests/components/copy/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..554638f462 --- /dev/null +++ b/tests/components/copy/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 2 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.esp32-idf.yaml b/tests/components/copy/test.esp32-idf.yaml new file mode 100644 index 0000000000..806dbfe9f3 --- /dev/null +++ b/tests/components/copy/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.esp8266-ard.yaml b/tests/components/copy/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1521e5f279 --- /dev/null +++ b/tests/components/copy/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +output: + - platform: esp8266_pwm + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/copy/test.rp2040-ard.yaml b/tests/components/copy/test.rp2040-ard.yaml new file mode 100644 index 0000000000..42e5eb8000 --- /dev/null +++ b/tests/components/copy/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +output: + - platform: rp2040_pwm + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 + - platform: copy + source_id: fan_speed + name: Fan Speed Copy + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy diff --git a/tests/components/cs5460a/test.esp32-ard.yaml b/tests/components/cs5460a/test.esp32-ard.yaml new file mode 100644 index 0000000000..e7eb1cbd73 --- /dev/null +++ b/tests/components/cs5460a/test.esp32-ard.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 12 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.esp32-c3-ard.yaml b/tests/components/cs5460a/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4ce21783a3 --- /dev/null +++ b/tests/components/cs5460a/test.esp32-c3-ard.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 8 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.esp32-c3-idf.yaml b/tests/components/cs5460a/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4ce21783a3 --- /dev/null +++ b/tests/components/cs5460a/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 8 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.esp32-idf.yaml b/tests/components/cs5460a/test.esp32-idf.yaml new file mode 100644 index 0000000000..e7eb1cbd73 --- /dev/null +++ b/tests/components/cs5460a/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 12 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.esp8266-ard.yaml b/tests/components/cs5460a/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c5a458d0ec --- /dev/null +++ b/tests/components/cs5460a/test.esp8266-ard.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 15 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cs5460a/test.rp2040-ard.yaml b/tests/components/cs5460a/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f3daf7d72d --- /dev/null +++ b/tests/components/cs5460a/test.rp2040-ard.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_cs5460a + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: cs5460a + id: cs5460a1 + cs_pin: 6 + current: + name: Socket current + voltage: + name: Mains voltage + power: + name: Socket power + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: true + voltage_hpf: true + phase_offset: 20 + pulse_energy: 0.01 kWh diff --git a/tests/components/cse7761/test.esp32-ard.yaml b/tests/components/cse7761/test.esp32-ard.yaml new file mode 100644 index 0000000000..4174e9a92e --- /dev/null +++ b/tests/components/cse7761/test.esp32-ard.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.esp32-c3-ard.yaml b/tests/components/cse7761/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..581db24fd5 --- /dev/null +++ b/tests/components/cse7761/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.esp32-c3-idf.yaml b/tests/components/cse7761/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..581db24fd5 --- /dev/null +++ b/tests/components/cse7761/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.esp32-idf.yaml b/tests/components/cse7761/test.esp32-idf.yaml new file mode 100644 index 0000000000..4174e9a92e --- /dev/null +++ b/tests/components/cse7761/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.esp8266-ard.yaml b/tests/components/cse7761/test.esp8266-ard.yaml new file mode 100644 index 0000000000..581db24fd5 --- /dev/null +++ b/tests/components/cse7761/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7761/test.rp2040-ard.yaml b/tests/components/cse7761/test.rp2040-ard.yaml new file mode 100644 index 0000000000..581db24fd5 --- /dev/null +++ b/tests/components/cse7761/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +uart: + - id: uart_cse7761 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 38400 + +sensor: + - platform: cse7761 + voltage: + name: CSE7761 Voltage + current_1: + name: CSE7761 Current 1 + current_2: + name: CSE7761 Current 2 + active_power_1: + name: CSE7761 Active Power 1 + active_power_2: + name: CSE7761 Active Power 2 diff --git a/tests/components/cse7766/test.esp32-ard.yaml b/tests/components/cse7766/test.esp32-ard.yaml new file mode 100644 index 0000000000..5542b52824 --- /dev/null +++ b/tests/components/cse7766/test.esp32-ard.yaml @@ -0,0 +1,17 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 4800 + parity: EVEN + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.esp32-c3-ard.yaml b/tests/components/cse7766/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d27c9d4463 --- /dev/null +++ b/tests/components/cse7766/test.esp32-c3-ard.yaml @@ -0,0 +1,17 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + parity: EVEN + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.esp32-c3-idf.yaml b/tests/components/cse7766/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d27c9d4463 --- /dev/null +++ b/tests/components/cse7766/test.esp32-c3-idf.yaml @@ -0,0 +1,17 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + parity: EVEN + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.esp32-idf.yaml b/tests/components/cse7766/test.esp32-idf.yaml new file mode 100644 index 0000000000..5542b52824 --- /dev/null +++ b/tests/components/cse7766/test.esp32-idf.yaml @@ -0,0 +1,17 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 4800 + parity: EVEN + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.esp8266-ard.yaml b/tests/components/cse7766/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d27c9d4463 --- /dev/null +++ b/tests/components/cse7766/test.esp8266-ard.yaml @@ -0,0 +1,17 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + parity: EVEN + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cse7766/test.rp2040-ard.yaml b/tests/components/cse7766/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d27c9d4463 --- /dev/null +++ b/tests/components/cse7766/test.rp2040-ard.yaml @@ -0,0 +1,17 @@ +uart: + - id: uart_cse7766 + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + parity: EVEN + +sensor: + - platform: cse7766 + voltage: + name: CSE7766 Voltage + current: + name: CSE7766 Current + power: + name: CSE776 Power diff --git a/tests/components/cst226/common.yaml b/tests/components/cst226/common.yaml new file mode 100644 index 0000000000..7e1c5dde36 --- /dev/null +++ b/tests/components/cst226/common.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_id_1 + clk_pin: GPIO7 + mosi_pin: GPIO6 + interface: any + +display: + - platform: ili9xxx + id: displ8 + model: ili9342 + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: + number: GPIO21 + invert_colors: false + +i2c: + scl: GPIO18 + sda: GPIO8 + +touchscreen: + - platform: cst226 + interrupt_pin: GPIO3 + reset_pin: GPIO20 + diff --git a/tests/components/cst226/test.esp32-c3-ard.yaml b/tests/components/cst226/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/cst226/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/cst816/common.yaml b/tests/components/cst816/common.yaml new file mode 100644 index 0000000000..765a353d1d --- /dev/null +++ b/tests/components/cst816/common.yaml @@ -0,0 +1,38 @@ +touchscreen: + - platform: cst816 + id: my_touchscreen + interrupt_pin: + number: 21 + reset_pin: GPIO16 + skip_probe: false + transform: + mirror_x: false + mirror_y: false + swap_xy: false + +i2c: + sda: 3 + scl: 4 + +display: + - id: my_display + platform: ili9xxx + dimensions: 480x320 + model: ST7796 + cs_pin: 18 + dc_pin: 20 + reset_pin: 22 + transform: + swap_xy: true + mirror_x: true + mirror_y: true + auto_clear_enabled: false + invert_colors: false + +spi: + clk_pin: 14 + mosi_pin: 13 + +binary_sensor: + - platform: cst816 + name: Home Button diff --git a/tests/components/cst816/test.esp32-ard.yaml b/tests/components/cst816/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/cst816/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ct_clamp/test.esp32-ard.yaml b/tests/components/ct_clamp/test.esp32-ard.yaml new file mode 100644 index 0000000000..1ea964fa96 --- /dev/null +++ b/tests/components/ct_clamp/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 39 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.esp32-c3-ard.yaml b/tests/components/ct_clamp/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e25acc95e1 --- /dev/null +++ b/tests/components/ct_clamp/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 0 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.esp32-c3-idf.yaml b/tests/components/ct_clamp/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e25acc95e1 --- /dev/null +++ b/tests/components/ct_clamp/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 0 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.esp32-idf.yaml b/tests/components/ct_clamp/test.esp32-idf.yaml new file mode 100644 index 0000000000..1ea964fa96 --- /dev/null +++ b/tests/components/ct_clamp/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 39 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.esp8266-ard.yaml b/tests/components/ct_clamp/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9c7126480d --- /dev/null +++ b/tests/components/ct_clamp/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: A0 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/ct_clamp/test.rp2040-ard.yaml b/tests/components/ct_clamp/test.rp2040-ard.yaml new file mode 100644 index 0000000000..47077308aa --- /dev/null +++ b/tests/components/ct_clamp/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: esp_adc_sensor + pin: 26 + - platform: ct_clamp + sensor: esp_adc_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s diff --git a/tests/components/current_based/test.esp32-ard.yaml b/tests/components/current_based/test.esp32-ard.yaml new file mode 100644 index 0000000000..90781120bc --- /dev/null +++ b/tests/components/current_based/test.esp32-ard.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 16 + sda: 17 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.esp32-c3-ard.yaml b/tests/components/current_based/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..69ab8830c3 --- /dev/null +++ b/tests/components/current_based/test.esp32-c3-ard.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.esp32-c3-idf.yaml b/tests/components/current_based/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..69ab8830c3 --- /dev/null +++ b/tests/components/current_based/test.esp32-c3-idf.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.esp32-idf.yaml b/tests/components/current_based/test.esp32-idf.yaml new file mode 100644 index 0000000000..90781120bc --- /dev/null +++ b/tests/components/current_based/test.esp32-idf.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 16 + sda: 17 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.esp8266-ard.yaml b/tests/components/current_based/test.esp8266-ard.yaml new file mode 100644 index 0000000000..42d6d4676b --- /dev/null +++ b/tests/components/current_based/test.esp8266-ard.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 15 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/current_based/test.rp2040-ard.yaml b/tests/components/current_based/test.rp2040-ard.yaml new file mode 100644 index 0000000000..69ab8830c3 --- /dev/null +++ b/tests/components/current_based/test.rp2040-ard.yaml @@ -0,0 +1,68 @@ +i2c: + - id: i2c_ade7953 + scl: 5 + sda: 4 + +sensor: + - platform: ade7953_i2c + irq_pin: 6 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + power_factor_a: + name: ADE7953 Power Factor A + power_factor_b: + name: ADE7953 Power Factor B + apparent_power_a: + name: ADE7953 Apparent Power A + apparent_power_b: + name: ADE7953 Apparent Power B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + reactive_power_a: + name: ADE7953 Reactive Power A + reactive_power_b: + name: ADE7953 Reactive Power B + update_interval: 1s + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: template_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: template_switch2 + stop_action: + - switch.turn_off: template_switch1 + - switch.turn_off: template_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected diff --git a/tests/components/cwww/test.esp32-ard.yaml b/tests/components/cwww/test.esp32-ard.yaml new file mode 100644 index 0000000000..f108d96ad3 --- /dev/null +++ b/tests/components/cwww/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.esp32-c3-ard.yaml b/tests/components/cwww/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c829ca2a2b --- /dev/null +++ b/tests/components/cwww/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.esp32-c3-idf.yaml b/tests/components/cwww/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2760a167ee --- /dev/null +++ b/tests/components/cwww/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + channel: 0 + - platform: ledc + id: light_output_2 + pin: 2 + channel: 1 + phase_angle: 180° + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.esp32-idf.yaml b/tests/components/cwww/test.esp32-idf.yaml new file mode 100644 index 0000000000..27fa160e56 --- /dev/null +++ b/tests/components/cwww/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + channel: 0 + - platform: ledc + id: light_output_2 + pin: 13 + channel: 1 + phase_angle: 180° + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.esp8266-ard.yaml b/tests/components/cwww/test.esp8266-ard.yaml new file mode 100644 index 0000000000..50c311f616 --- /dev/null +++ b/tests/components/cwww/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 + - platform: esp8266_pwm + id: light_output_2 + pin: 13 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/cwww/test.rp2040-ard.yaml b/tests/components/cwww/test.rp2040-ard.yaml new file mode 100644 index 0000000000..505d67f862 --- /dev/null +++ b/tests/components/cwww/test.rp2040-ard.yaml @@ -0,0 +1,16 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 + - platform: rp2040_pwm + id: light_output_2 + pin: 13 + +light: + - platform: cwww + name: CWWW Light + cold_white: light_output_1 + warm_white: light_output_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true diff --git a/tests/components/dac7678/test.esp32-ard.yaml b/tests/components/dac7678/test.esp32-ard.yaml new file mode 100644 index 0000000000..946a7ca58d --- /dev/null +++ b/tests/components/dac7678/test.esp32-ard.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 16 + sda: 17 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.esp32-c3-ard.yaml b/tests/components/dac7678/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7e3455bb68 --- /dev/null +++ b/tests/components/dac7678/test.esp32-c3-ard.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 5 + sda: 4 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.esp32-c3-idf.yaml b/tests/components/dac7678/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7e3455bb68 --- /dev/null +++ b/tests/components/dac7678/test.esp32-c3-idf.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 5 + sda: 4 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.esp32-idf.yaml b/tests/components/dac7678/test.esp32-idf.yaml new file mode 100644 index 0000000000..946a7ca58d --- /dev/null +++ b/tests/components/dac7678/test.esp32-idf.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 16 + sda: 17 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.esp8266-ard.yaml b/tests/components/dac7678/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7e3455bb68 --- /dev/null +++ b/tests/components/dac7678/test.esp8266-ard.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 5 + sda: 4 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/dac7678/test.rp2040-ard.yaml b/tests/components/dac7678/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7e3455bb68 --- /dev/null +++ b/tests/components/dac7678/test.rp2040-ard.yaml @@ -0,0 +1,43 @@ +i2c: + - id: i2c_dac7678 + scl: 5 + sda: 4 + +dac7678: + address: 0x4A + id: dac7678_hub + internal_reference: true + +output: + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 0 + id: dac7678_1_ch0 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 1 + id: dac7678_1_ch1 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 2 + id: dac7678_1_ch2 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 3 + id: dac7678_1_ch3 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 4 + id: dac7678_1_ch4 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 5 + id: dac7678_1_ch5 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 6 + id: dac7678_1_ch6 + - platform: dac7678 + dac7678_id: dac7678_hub + channel: 7 + id: dac7678_1_ch7 diff --git a/tests/components/daikin/test.esp32-ard.yaml b/tests/components/daikin/test.esp32-ard.yaml new file mode 100644 index 0000000000..6672fe3e45 --- /dev/null +++ b/tests/components/daikin/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: daikin + horizontal_default: middle + vertical_default: middle + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/daikin/test.esp8266-ard.yaml b/tests/components/daikin/test.esp8266-ard.yaml new file mode 100644 index 0000000000..47f02bbad9 --- /dev/null +++ b/tests/components/daikin/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: daikin + horizontal_default: middle + vertical_default: middle + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/daikin_arc/test.esp32-ard.yaml b/tests/components/daikin_arc/test.esp32-ard.yaml new file mode 100644 index 0000000000..a8556e8576 --- /dev/null +++ b/tests/components/daikin_arc/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + id: tsvr + +remote_receiver: + id: rcvr + pin: + number: 27 + inverted: true + mode: + input: true + pullup: true + tolerance: 40% + +climate: + - platform: daikin_arc + name: "AC" + receiver_id: rcvr diff --git a/tests/components/daikin_arc/test.esp8266-ard.yaml b/tests/components/daikin_arc/test.esp8266-ard.yaml new file mode 100644 index 0000000000..abf1b34a6e --- /dev/null +++ b/tests/components/daikin_arc/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + id: tsvr + +remote_receiver: + id: rcvr + pin: + number: 2 + inverted: true + mode: + input: true + pullup: true + tolerance: 40% + +climate: + - platform: daikin_arc + name: "AC" + receiver_id: rcvr diff --git a/tests/components/daikin_brc/test.esp32-ard.yaml b/tests/components/daikin_brc/test.esp32-ard.yaml new file mode 100644 index 0000000000..89a5b00f5d --- /dev/null +++ b/tests/components/daikin_brc/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/daikin_brc/test.esp32-c3-ard.yaml b/tests/components/daikin_brc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..89a5b00f5d --- /dev/null +++ b/tests/components/daikin_brc/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/daikin_brc/test.esp32-c3-idf.yaml b/tests/components/daikin_brc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..89a5b00f5d --- /dev/null +++ b/tests/components/daikin_brc/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/daikin_brc/test.esp32-idf.yaml b/tests/components/daikin_brc/test.esp32-idf.yaml new file mode 100644 index 0000000000..89a5b00f5d --- /dev/null +++ b/tests/components/daikin_brc/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/daikin_brc/test.esp8266-ard.yaml b/tests/components/daikin_brc/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b8c74803a2 --- /dev/null +++ b/tests/components/daikin_brc/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: daikin_brc + name: Daikin_brc Climate diff --git a/tests/components/dallas_temp/common.yaml b/tests/components/dallas_temp/common.yaml new file mode 100644 index 0000000000..2f846ca278 --- /dev/null +++ b/tests/components/dallas_temp/common.yaml @@ -0,0 +1,11 @@ +one_wire: + - platform: gpio + pin: 4 + +sensor: + - platform: dallas_temp + address: 0x1C0000031EDD2A28 + name: Dallas Temperature + resolution: 9 + - platform: dallas_temp + name: Dallas Temperature diff --git a/tests/components/dallas_temp/test.esp32-ard.yaml b/tests/components/dallas_temp/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dallas_temp/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp32-c3-ard.yaml b/tests/components/dallas_temp/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dallas_temp/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp32-c3-idf.yaml b/tests/components/dallas_temp/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dallas_temp/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp32-idf.yaml b/tests/components/dallas_temp/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dallas_temp/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp8266-ard.yaml b/tests/components/dallas_temp/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dallas_temp/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.rp2040-ard.yaml b/tests/components/dallas_temp/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dallas_temp/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/daly_bms/test.esp32-ard.yaml b/tests/components/daly_bms/test.esp32-ard.yaml new file mode 100644 index 0000000000..ec9607334d --- /dev/null +++ b/tests/components/daly_bms/test.esp32-ard.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.esp32-c3-ard.yaml b/tests/components/daly_bms/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..237a6570b5 --- /dev/null +++ b/tests/components/daly_bms/test.esp32-c3-ard.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.esp32-c3-idf.yaml b/tests/components/daly_bms/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..237a6570b5 --- /dev/null +++ b/tests/components/daly_bms/test.esp32-c3-idf.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.esp32-idf.yaml b/tests/components/daly_bms/test.esp32-idf.yaml new file mode 100644 index 0000000000..ec9607334d --- /dev/null +++ b/tests/components/daly_bms/test.esp32-idf.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.esp8266-ard.yaml b/tests/components/daly_bms/test.esp8266-ard.yaml new file mode 100644 index 0000000000..237a6570b5 --- /dev/null +++ b/tests/components/daly_bms/test.esp8266-ard.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/daly_bms/test.rp2040-ard.yaml b/tests/components/daly_bms/test.rp2040-ard.yaml new file mode 100644 index 0000000000..237a6570b5 --- /dev/null +++ b/tests/components/daly_bms/test.rp2040-ard.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_daly_bms + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 4800 + +daly_bms: + update_interval: 20s + +binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: Charging MOS + discharging_mos_enabled: + name: Discharging MOS + +sensor: + - platform: daly_bms + voltage: + name: Battery Voltage + current: + name: Battery Current + battery_level: + name: Battery Level + max_cell_voltage: + name: Max Cell Voltage + max_cell_voltage_number: + name: Max Cell Voltage Number + min_cell_voltage: + name: Min Cell Voltage + min_cell_voltage_number: + name: Min Cell Voltage Number + max_temperature: + name: Max Temperature + max_temperature_probe_number: + name: Max Temperature Probe Number + min_temperature: + name: Min Temperature + min_temperature_probe_number: + name: Min Temperature Probe Number + remaining_capacity: + name: Remaining Capacity + cells_number: + name: Cells Number + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + +text_sensor: + - platform: daly_bms + status: + name: BMS Status diff --git a/tests/components/datetime/common.yaml b/tests/components/datetime/common.yaml new file mode 100644 index 0000000000..4e26b68121 --- /dev/null +++ b/tests/components/datetime/common.yaml @@ -0,0 +1,3 @@ +datetime: + +time: diff --git a/tests/components/datetime/test.all.yaml b/tests/components/datetime/test.all.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/datetime/test.all.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/common.yaml b/tests/components/debug/common.yaml new file mode 100644 index 0000000000..5845beaa80 --- /dev/null +++ b/tests/components/debug/common.yaml @@ -0,0 +1 @@ +debug: diff --git a/tests/components/debug/test.bk72xx-ard.yaml b/tests/components/debug/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-ard.yaml b/tests/components/debug/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-c3-ard.yaml b/tests/components/debug/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-c3-idf.yaml b/tests/components/debug/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-idf.yaml b/tests/components/debug/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp8266-ard.yaml b/tests/components/debug/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.host.yaml b/tests/components/debug/test.host.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.host.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.rp2040-ard.yaml b/tests/components/debug/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/deep_sleep/test.esp32-ard.yaml b/tests/components/deep_sleep/test.esp32-ard.yaml new file mode 100644 index 0000000000..94942fd5b0 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: 4 + wakeup_pin_mode: INVERT_WAKEUP diff --git a/tests/components/deep_sleep/test.esp32-c3-ard.yaml b/tests/components/deep_sleep/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..94942fd5b0 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: 4 + wakeup_pin_mode: INVERT_WAKEUP diff --git a/tests/components/deep_sleep/test.esp32-c3-idf.yaml b/tests/components/deep_sleep/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..94942fd5b0 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: 4 + wakeup_pin_mode: INVERT_WAKEUP diff --git a/tests/components/deep_sleep/test.esp32-idf.yaml b/tests/components/deep_sleep/test.esp32-idf.yaml new file mode 100644 index 0000000000..94942fd5b0 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: 4 + wakeup_pin_mode: INVERT_WAKEUP diff --git a/tests/components/deep_sleep/test.esp8266-ard.yaml b/tests/components/deep_sleep/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0992fa9696 --- /dev/null +++ b/tests/components/deep_sleep/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +esphome: + on_boot: + then: + - deep_sleep.prevent + - delay: 1s + - deep_sleep.allow + +deep_sleep: + run_duration: 10s + sleep_duration: 50s diff --git a/tests/components/delonghi/test.esp32-ard.yaml b/tests/components/delonghi/test.esp32-ard.yaml new file mode 100644 index 0000000000..cfe0f837f0 --- /dev/null +++ b/tests/components/delonghi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/delonghi/test.esp32-c3-ard.yaml b/tests/components/delonghi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..cfe0f837f0 --- /dev/null +++ b/tests/components/delonghi/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/delonghi/test.esp32-c3-idf.yaml b/tests/components/delonghi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..cfe0f837f0 --- /dev/null +++ b/tests/components/delonghi/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/delonghi/test.esp32-idf.yaml b/tests/components/delonghi/test.esp32-idf.yaml new file mode 100644 index 0000000000..cfe0f837f0 --- /dev/null +++ b/tests/components/delonghi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/delonghi/test.esp8266-ard.yaml b/tests/components/delonghi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..adc478a6e6 --- /dev/null +++ b/tests/components/delonghi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: delonghi + name: Delonghi Climate diff --git a/tests/components/dfplayer/test.esp32-ard.yaml b/tests/components/dfplayer/test.esp32-ard.yaml new file mode 100644 index 0000000000..03b44b8ca9 --- /dev/null +++ b/tests/components/dfplayer/test.esp32-ard.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.esp32-c3-ard.yaml b/tests/components/dfplayer/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..94355915a7 --- /dev/null +++ b/tests/components/dfplayer/test.esp32-c3-ard.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.esp32-c3-idf.yaml b/tests/components/dfplayer/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..94355915a7 --- /dev/null +++ b/tests/components/dfplayer/test.esp32-c3-idf.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.esp32-idf.yaml b/tests/components/dfplayer/test.esp32-idf.yaml new file mode 100644 index 0000000000..03b44b8ca9 --- /dev/null +++ b/tests/components/dfplayer/test.esp32-idf.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.esp8266-ard.yaml b/tests/components/dfplayer/test.esp8266-ard.yaml new file mode 100644 index 0000000000..94355915a7 --- /dev/null +++ b/tests/components/dfplayer/test.esp8266-ard.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfplayer/test.rp2040-ard.yaml b/tests/components/dfplayer/test.rp2040-ard.yaml new file mode 100644 index 0000000000..94355915a7 --- /dev/null +++ b/tests/components/dfplayer/test.rp2040-ard.yaml @@ -0,0 +1,42 @@ +esphome: + on_boot: + then: + - dfplayer.play: 5 + - dfplayer.play: + file: 4 + loop: true + - dfplayer.play_folder: + folder: 1 + file: 3 + - dfplayer.play_folder: + folder: 1 + loop: true + - dfplayer.set_device: + device: TF_CARD + - dfplayer.set_volume: 5 + - dfplayer.set_eq: ROCK + - dfplayer.play_next + - dfplayer.play_previous + - dfplayer.reset + - dfplayer.start + - dfplayer.pause + - dfplayer.stop + - dfplayer.random + - dfplayer.volume_up + - dfplayer.volume_down + - dfplayer.sleep + +uart: + - id: uart_dfplayer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: dfplayer.is_playing + then: + logger.log: Playback finished event diff --git a/tests/components/dfrobot_sen0395/test.esp32-ard.yaml b/tests/components/dfrobot_sen0395/test.esp32-ard.yaml new file mode 100644 index 0000000000..5c06fc6660 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp32-ard.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.esp32-c3-ard.yaml b/tests/components/dfrobot_sen0395/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..71b17cecd5 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp32-c3-ard.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml b/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..71b17cecd5 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.esp32-idf.yaml b/tests/components/dfrobot_sen0395/test.esp32-idf.yaml new file mode 100644 index 0000000000..5c06fc6660 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.esp8266-ard.yaml b/tests/components/dfrobot_sen0395/test.esp8266-ard.yaml new file mode 100644 index 0000000000..71b17cecd5 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.esp8266-ard.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/test.rp2040-ard.yaml b/tests/components/dfrobot_sen0395/test.rp2040-ard.yaml new file mode 100644 index 0000000000..71b17cecd5 --- /dev/null +++ b/tests/components/dfrobot_sen0395/test.rp2040-ard.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda "return 7;" + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - dfrobot_sen0395.reset + +uart: + - id: uart_dfrobot_sen0395 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dfrobot_sen0395: + - id: mmwave + +binary_sensor: + - platform: dfrobot_sen0395 + id: mmwave_detected diff --git a/tests/components/dht/common.yaml b/tests/components/dht/common.yaml new file mode 100644 index 0000000000..f134a324ca --- /dev/null +++ b/tests/components/dht/common.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: dht + pin: 4 + model: AM2302 + update_interval: 15s + temperature: + id: dht_temperature + name: DHT Temperature + humidity: + id: dht_humidity + name: DHT Humidity diff --git a/tests/components/dht/test.esp32-ard.yaml b/tests/components/dht/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dht/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dht/test.esp32-c3-ard.yaml b/tests/components/dht/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dht/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dht/test.esp32-c3-idf.yaml b/tests/components/dht/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dht/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dht/test.esp32-idf.yaml b/tests/components/dht/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dht/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dht/test.esp8266-ard.yaml b/tests/components/dht/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dht/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dht/test.rp2040-ard.yaml b/tests/components/dht/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dht/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dht12/test.esp32-ard.yaml b/tests/components/dht12/test.esp32-ard.yaml new file mode 100644 index 0000000000..02a00f5df7 --- /dev/null +++ b/tests/components/dht12/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 16 + sda: 17 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.esp32-c3-ard.yaml b/tests/components/dht12/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c06c20fd9f --- /dev/null +++ b/tests/components/dht12/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 5 + sda: 4 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.esp32-c3-idf.yaml b/tests/components/dht12/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c06c20fd9f --- /dev/null +++ b/tests/components/dht12/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 5 + sda: 4 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.esp32-idf.yaml b/tests/components/dht12/test.esp32-idf.yaml new file mode 100644 index 0000000000..02a00f5df7 --- /dev/null +++ b/tests/components/dht12/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 16 + sda: 17 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.esp8266-ard.yaml b/tests/components/dht12/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c06c20fd9f --- /dev/null +++ b/tests/components/dht12/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 5 + sda: 4 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/dht12/test.rp2040-ard.yaml b/tests/components/dht12/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c06c20fd9f --- /dev/null +++ b/tests/components/dht12/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dht12 + scl: 5 + sda: 4 + +sensor: + - platform: dht12 + temperature: + name: DHT12 Temperature + humidity: + name: DHT12 Humidity + update_interval: 15s diff --git a/tests/components/display/common.yaml b/tests/components/display/common.yaml new file mode 100644 index 0000000000..4fc4fafa25 --- /dev/null +++ b/tests/components/display/common.yaml @@ -0,0 +1,40 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: false + lambda: |- + // Draw an analog clock in the center of the screen + int centerX = it.get_width() / 2; + int centerY = it.get_height() / 2; + int radius = min(it.get_width(), it.get_height()) / 4; + + // Draw border + it.circle(centerX, centerY, radius); + + // Draw hour ticks + for(int h = 0; h < 12; h++) { + int hourAngle = (h * 30) - 90; + + it.line_at_angle(centerX, centerY, hourAngle, radius - 10, radius); + } + + // Draw minute ticks + for(int m = 0; m < 60; m++) { + int minuteAngle = (m * 6) - 90; + + it.line_at_angle(centerX, centerY, minuteAngle, radius - 5, radius); + } + + // Nice ring around and some gauge + it.filled_ring(centerX, centerY, radius+5, radius+8); + it.filled_gauge(centerX, centerY, radius/2, radius/2-5, 66); diff --git a/tests/components/display/test.esp32-ard.yaml b/tests/components/display/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/display/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dps310/test.esp32-ard.yaml b/tests/components/dps310/test.esp32-ard.yaml new file mode 100644 index 0000000000..417cab5c40 --- /dev/null +++ b/tests/components/dps310/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 16 + sda: 17 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.esp32-c3-ard.yaml b/tests/components/dps310/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0e15e9ccc5 --- /dev/null +++ b/tests/components/dps310/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 5 + sda: 4 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.esp32-c3-idf.yaml b/tests/components/dps310/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0e15e9ccc5 --- /dev/null +++ b/tests/components/dps310/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 5 + sda: 4 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.esp32-idf.yaml b/tests/components/dps310/test.esp32-idf.yaml new file mode 100644 index 0000000000..417cab5c40 --- /dev/null +++ b/tests/components/dps310/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 16 + sda: 17 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.esp8266-ard.yaml b/tests/components/dps310/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0e15e9ccc5 --- /dev/null +++ b/tests/components/dps310/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 5 + sda: 4 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/dps310/test.rp2040-ard.yaml b/tests/components/dps310/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0e15e9ccc5 --- /dev/null +++ b/tests/components/dps310/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_dps310 + scl: 5 + sda: 4 + +sensor: + - platform: dps310 + temperature: + name: DPS310 Temperature + pressure: + name: DPS310 Pressure + address: 0x77 diff --git a/tests/components/ds1307/test.esp32-ard.yaml b/tests/components/ds1307/test.esp32-ard.yaml new file mode 100644 index 0000000000..017c7aac92 --- /dev/null +++ b/tests/components/ds1307/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 16 + sda: 17 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.esp32-c3-ard.yaml b/tests/components/ds1307/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c309b9c212 --- /dev/null +++ b/tests/components/ds1307/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 5 + sda: 4 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.esp32-c3-idf.yaml b/tests/components/ds1307/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c309b9c212 --- /dev/null +++ b/tests/components/ds1307/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 5 + sda: 4 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.esp32-idf.yaml b/tests/components/ds1307/test.esp32-idf.yaml new file mode 100644 index 0000000000..017c7aac92 --- /dev/null +++ b/tests/components/ds1307/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 16 + sda: 17 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.esp8266-ard.yaml b/tests/components/ds1307/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c309b9c212 --- /dev/null +++ b/tests/components/ds1307/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 5 + sda: 4 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/ds1307/test.rp2040-ard.yaml b/tests/components/ds1307/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c309b9c212 --- /dev/null +++ b/tests/components/ds1307/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_ds1307 + scl: 5 + sda: 4 + +time: + - platform: ds1307 + id: ds1307_time + update_interval: never diff --git a/tests/components/dsmr/test.esp32-ard.yaml b/tests/components/dsmr/test.esp32-ard.yaml new file mode 100644 index 0000000000..1fd0448ab3 --- /dev/null +++ b/tests/components/dsmr/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_dsmr + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + max_telegram_length: 1000 + request_pin: 15 + request_interval: 20s + receive_timeout: 100ms diff --git a/tests/components/dsmr/test.esp32-c3-ard.yaml b/tests/components/dsmr/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e813556be8 --- /dev/null +++ b/tests/components/dsmr/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_dsmr + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + max_telegram_length: 1000 + request_pin: 6 + request_interval: 20s + receive_timeout: 100ms diff --git a/tests/components/dsmr/test.esp8266-ard.yaml b/tests/components/dsmr/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8037fb4b1a --- /dev/null +++ b/tests/components/dsmr/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_dsmr + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + max_telegram_length: 1000 + request_pin: 15 + request_interval: 20s + receive_timeout: 100ms diff --git a/tests/components/dsmr/test.rp2040-ard.yaml b/tests/components/dsmr/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e813556be8 --- /dev/null +++ b/tests/components/dsmr/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_dsmr + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + max_telegram_length: 1000 + request_pin: 6 + request_interval: 20s + receive_timeout: 100ms diff --git a/tests/components/duty_cycle/common.yaml b/tests/components/duty_cycle/common.yaml new file mode 100644 index 0000000000..2b7f31efbd --- /dev/null +++ b/tests/components/duty_cycle/common.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: duty_cycle + pin: 4 + name: Duty Cycle Sensor diff --git a/tests/components/duty_cycle/test.esp32-ard.yaml b/tests/components/duty_cycle/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp32-c3-ard.yaml b/tests/components/duty_cycle/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp32-c3-idf.yaml b/tests/components/duty_cycle/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp32-idf.yaml b/tests/components/duty_cycle/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp8266-ard.yaml b/tests/components/duty_cycle/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.rp2040-ard.yaml b/tests/components/duty_cycle/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/common.yaml b/tests/components/duty_time/common.yaml new file mode 100644 index 0000000000..28fa4afd1c --- /dev/null +++ b/tests/components/duty_time/common.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +sensor: + - platform: duty_time + name: Duty Time + sensor: bin1 diff --git a/tests/components/duty_time/test.esp32-ard.yaml b/tests/components/duty_time/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.esp32-c3-ard.yaml b/tests/components/duty_time/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.esp32-c3-idf.yaml b/tests/components/duty_time/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.esp32-idf.yaml b/tests/components/duty_time/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.esp8266-ard.yaml b/tests/components/duty_time/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.rp2040-ard.yaml b/tests/components/duty_time/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/e131/test.esp32-ard.yaml b/tests/components/e131/test.esp32-ard.yaml new file mode 100644 index 0000000000..25304cd3b4 --- /dev/null +++ b/tests/components/e131/test.esp32-ard.yaml @@ -0,0 +1,18 @@ +wifi: + ssid: MySSID + password: password1 + +e131: + +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + effects: + - e131: + universe: 1 diff --git a/tests/components/e131/test.esp32-c3-ard.yaml b/tests/components/e131/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..25304cd3b4 --- /dev/null +++ b/tests/components/e131/test.esp32-c3-ard.yaml @@ -0,0 +1,18 @@ +wifi: + ssid: MySSID + password: password1 + +e131: + +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + effects: + - e131: + universe: 1 diff --git a/tests/components/e131/test.esp32-c3-idf.yaml b/tests/components/e131/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25304cd3b4 --- /dev/null +++ b/tests/components/e131/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +wifi: + ssid: MySSID + password: password1 + +e131: + +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + effects: + - e131: + universe: 1 diff --git a/tests/components/e131/test.esp32-idf.yaml b/tests/components/e131/test.esp32-idf.yaml new file mode 100644 index 0000000000..25304cd3b4 --- /dev/null +++ b/tests/components/e131/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +wifi: + ssid: MySSID + password: password1 + +e131: + +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + effects: + - e131: + universe: 1 diff --git a/tests/components/e131/test.esp8266-ard.yaml b/tests/components/e131/test.esp8266-ard.yaml new file mode 100644 index 0000000000..54245014a5 --- /dev/null +++ b/tests/components/e131/test.esp8266-ard.yaml @@ -0,0 +1,17 @@ +wifi: + ssid: MySSID + password: password1 + +e131: + +light: + - platform: neopixelbus + name: Neopixelbus Light + pin: 1 + type: GRBW + variant: SK6812 + method: ESP8266_UART0 + num_leds: 256 + effects: + - e131: + universe: 1 diff --git a/tests/components/e131/test.rp2040-ard.yaml b/tests/components/e131/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0ae31f5403 --- /dev/null +++ b/tests/components/e131/test.rp2040-ard.yaml @@ -0,0 +1,17 @@ +wifi: + ssid: MySSID + password: password1 + +e131: + +light: + - platform: rp2040_pio_led_strip + id: led_strip + pin: 2 + pio: 0 + num_leds: 256 + rgb_order: GRB + chipset: WS2812 + effects: + - e131: + universe: 1 diff --git a/tests/components/ee895/test.esp32-ard.yaml b/tests/components/ee895/test.esp32-ard.yaml new file mode 100644 index 0000000000..241bdb9574 --- /dev/null +++ b/tests/components/ee895/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_ee895 + scl: 16 + sda: 17 + +sensor: + - platform: ee895 + address: 0x5F + co2: + name: EE895 CO2 + temperature: + name: EE895 Temperature + pressure: + name: EE895 Pressure diff --git a/tests/components/ee895/test.esp32-c3-ard.yaml b/tests/components/ee895/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4a36117fab --- /dev/null +++ b/tests/components/ee895/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_ee895 + scl: 5 + sda: 4 + +sensor: + - platform: ee895 + address: 0x5F + co2: + name: EE895 CO2 + temperature: + name: EE895 Temperature + pressure: + name: EE895 Pressure diff --git a/tests/components/ee895/test.esp32-c3-idf.yaml b/tests/components/ee895/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4a36117fab --- /dev/null +++ b/tests/components/ee895/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_ee895 + scl: 5 + sda: 4 + +sensor: + - platform: ee895 + address: 0x5F + co2: + name: EE895 CO2 + temperature: + name: EE895 Temperature + pressure: + name: EE895 Pressure diff --git a/tests/components/ee895/test.esp32-idf.yaml b/tests/components/ee895/test.esp32-idf.yaml new file mode 100644 index 0000000000..241bdb9574 --- /dev/null +++ b/tests/components/ee895/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_ee895 + scl: 16 + sda: 17 + +sensor: + - platform: ee895 + address: 0x5F + co2: + name: EE895 CO2 + temperature: + name: EE895 Temperature + pressure: + name: EE895 Pressure diff --git a/tests/components/ee895/test.esp8266-ard.yaml b/tests/components/ee895/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a36117fab --- /dev/null +++ b/tests/components/ee895/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_ee895 + scl: 5 + sda: 4 + +sensor: + - platform: ee895 + address: 0x5F + co2: + name: EE895 CO2 + temperature: + name: EE895 Temperature + pressure: + name: EE895 Pressure diff --git a/tests/components/ee895/test.rp2040-ard.yaml b/tests/components/ee895/test.rp2040-ard.yaml new file mode 100644 index 0000000000..4a36117fab --- /dev/null +++ b/tests/components/ee895/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_ee895 + scl: 5 + sda: 4 + +sensor: + - platform: ee895 + address: 0x5F + co2: + name: EE895 CO2 + temperature: + name: EE895 Temperature + pressure: + name: EE895 Pressure diff --git a/tests/components/ektf2232/test.esp32-ard.yaml b/tests/components/ektf2232/test.esp32-ard.yaml new file mode 100644 index 0000000000..9c6eef8bb3 --- /dev/null +++ b/tests/components/ektf2232/test.esp32-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ektf2232 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ektf2232 + interrupt_pin: 14 + rts_pin: 15 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ektf2232/test.esp32-c3-ard.yaml b/tests/components/ektf2232/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..371f2795a2 --- /dev/null +++ b/tests/components/ektf2232/test.esp32-c3-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ektf2232 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ektf2232 + interrupt_pin: 6 + rts_pin: 7 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ektf2232/test.esp32-c3-idf.yaml b/tests/components/ektf2232/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..371f2795a2 --- /dev/null +++ b/tests/components/ektf2232/test.esp32-c3-idf.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ektf2232 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ektf2232 + interrupt_pin: 6 + rts_pin: 7 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ektf2232/test.esp32-idf.yaml b/tests/components/ektf2232/test.esp32-idf.yaml new file mode 100644 index 0000000000..9c6eef8bb3 --- /dev/null +++ b/tests/components/ektf2232/test.esp32-idf.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ektf2232 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ektf2232 + interrupt_pin: 14 + rts_pin: 15 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ektf2232/test.esp8266-ard.yaml b/tests/components/ektf2232/test.esp8266-ard.yaml new file mode 100644 index 0000000000..03f18f7184 --- /dev/null +++ b/tests/components/ektf2232/test.esp8266-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ektf2232 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ektf2232 + interrupt_pin: 12 + rts_pin: 13 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ektf2232/test.rp2040-ard.yaml b/tests/components/ektf2232/test.rp2040-ard.yaml new file mode 100644 index 0000000000..371f2795a2 --- /dev/null +++ b/tests/components/ektf2232/test.rp2040-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ektf2232 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ektf2232 + interrupt_pin: 6 + rts_pin: 7 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/emc2101/test.esp32-ard.yaml b/tests/components/emc2101/test.esp32-ard.yaml new file mode 100644 index 0000000000..34a7d22b71 --- /dev/null +++ b/tests/components/emc2101/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_emc2101 + scl: 16 + sda: 17 + +emc2101: + pwm: + resolution: 8 + +output: + - platform: emc2101 + id: fan_duty_cycle + +sensor: + - platform: emc2101 + internal_temperature: + id: internal_temperature_sensor + name: Internal Temperature Sensor + speed: + id: speed_sensor + name: Speed Sensor + duty_cycle: + id: duty_cycle_sensor + name: Duty Cycle Sensor + update_interval: 5s diff --git a/tests/components/emc2101/test.esp32-c3-ard.yaml b/tests/components/emc2101/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d95bc1b001 --- /dev/null +++ b/tests/components/emc2101/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_emc2101 + scl: 5 + sda: 4 + +emc2101: + pwm: + resolution: 8 + +output: + - platform: emc2101 + id: fan_duty_cycle + +sensor: + - platform: emc2101 + internal_temperature: + id: internal_temperature_sensor + name: Internal Temperature Sensor + speed: + id: speed_sensor + name: Speed Sensor + duty_cycle: + id: duty_cycle_sensor + name: Duty Cycle Sensor + update_interval: 5s diff --git a/tests/components/emc2101/test.esp32-c3-idf.yaml b/tests/components/emc2101/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d95bc1b001 --- /dev/null +++ b/tests/components/emc2101/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_emc2101 + scl: 5 + sda: 4 + +emc2101: + pwm: + resolution: 8 + +output: + - platform: emc2101 + id: fan_duty_cycle + +sensor: + - platform: emc2101 + internal_temperature: + id: internal_temperature_sensor + name: Internal Temperature Sensor + speed: + id: speed_sensor + name: Speed Sensor + duty_cycle: + id: duty_cycle_sensor + name: Duty Cycle Sensor + update_interval: 5s diff --git a/tests/components/emc2101/test.esp32-idf.yaml b/tests/components/emc2101/test.esp32-idf.yaml new file mode 100644 index 0000000000..34a7d22b71 --- /dev/null +++ b/tests/components/emc2101/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_emc2101 + scl: 16 + sda: 17 + +emc2101: + pwm: + resolution: 8 + +output: + - platform: emc2101 + id: fan_duty_cycle + +sensor: + - platform: emc2101 + internal_temperature: + id: internal_temperature_sensor + name: Internal Temperature Sensor + speed: + id: speed_sensor + name: Speed Sensor + duty_cycle: + id: duty_cycle_sensor + name: Duty Cycle Sensor + update_interval: 5s diff --git a/tests/components/emc2101/test.esp8266-ard.yaml b/tests/components/emc2101/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d95bc1b001 --- /dev/null +++ b/tests/components/emc2101/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_emc2101 + scl: 5 + sda: 4 + +emc2101: + pwm: + resolution: 8 + +output: + - platform: emc2101 + id: fan_duty_cycle + +sensor: + - platform: emc2101 + internal_temperature: + id: internal_temperature_sensor + name: Internal Temperature Sensor + speed: + id: speed_sensor + name: Speed Sensor + duty_cycle: + id: duty_cycle_sensor + name: Duty Cycle Sensor + update_interval: 5s diff --git a/tests/components/emc2101/test.rp2040-ard.yaml b/tests/components/emc2101/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d95bc1b001 --- /dev/null +++ b/tests/components/emc2101/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_emc2101 + scl: 5 + sda: 4 + +emc2101: + pwm: + resolution: 8 + +output: + - platform: emc2101 + id: fan_duty_cycle + +sensor: + - platform: emc2101 + internal_temperature: + id: internal_temperature_sensor + name: Internal Temperature Sensor + speed: + id: speed_sensor + name: Speed Sensor + duty_cycle: + id: duty_cycle_sensor + name: Duty Cycle Sensor + update_interval: 5s diff --git a/tests/components/emmeti/common.yaml b/tests/components/emmeti/common.yaml new file mode 100644 index 0000000000..ac4201e19b --- /dev/null +++ b/tests/components/emmeti/common.yaml @@ -0,0 +1,14 @@ +remote_transmitter: + id: tx + pin: ${remote_transmitter_pin} + carrier_duty_percent: 100% + +remote_receiver: + id: rcvr + pin: ${remote_receiver_pin} + +climate: + - platform: emmeti + name: Emmeti + receiver_id: rcvr + transmitter_id: tx diff --git a/tests/components/emmeti/test.esp32-ard.yaml b/tests/components/emmeti/test.esp32-ard.yaml new file mode 100644 index 0000000000..2689ff279e --- /dev/null +++ b/tests/components/emmeti/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + remote_transmitter_pin: GPIO33 + remote_receiver_pin: GPIO32 + +<<: !include common.yaml diff --git a/tests/components/emmeti/test.esp32-idf.yaml b/tests/components/emmeti/test.esp32-idf.yaml new file mode 100644 index 0000000000..2689ff279e --- /dev/null +++ b/tests/components/emmeti/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + remote_transmitter_pin: GPIO33 + remote_receiver_pin: GPIO32 + +<<: !include common.yaml diff --git a/tests/components/emmeti/test.esp8266-ard.yaml b/tests/components/emmeti/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2fb00aea61 --- /dev/null +++ b/tests/components/emmeti/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + remote_transmitter_pin: GPIO4 + remote_receiver_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/endstop/common.yaml b/tests/components/endstop/common.yaml new file mode 100644 index 0000000000..341fbf7260 --- /dev/null +++ b/tests/components/endstop/common.yaml @@ -0,0 +1,33 @@ +binary_sensor: + - platform: template + id: bin1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +switch: + - platform: template + id: template_switch1 + optimistic: true + - platform: template + id: template_switch2 + optimistic: true + +cover: + - platform: endstop + id: endstop_cover + name: Endstop Cover + stop_action: + - switch.turn_on: template_switch1 + open_endstop: bin1 + open_action: + - switch.turn_on: template_switch1 + open_duration: 5min + close_endstop: bin1 + close_action: + - switch.turn_on: template_switch2 + close_duration: 4.5min + max_duration: 10min diff --git a/tests/components/endstop/test.esp32-ard.yaml b/tests/components/endstop/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/endstop/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/endstop/test.esp32-c3-ard.yaml b/tests/components/endstop/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/endstop/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/endstop/test.esp32-c3-idf.yaml b/tests/components/endstop/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/endstop/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/endstop/test.esp32-idf.yaml b/tests/components/endstop/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/endstop/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/endstop/test.esp8266-ard.yaml b/tests/components/endstop/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/endstop/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/endstop/test.rp2040-ard.yaml b/tests/components/endstop/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/endstop/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ens160_i2c/common.yaml b/tests/components/ens160_i2c/common.yaml new file mode 100644 index 0000000000..39a5b35067 --- /dev/null +++ b/tests/components/ens160_i2c/common.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_ens160 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: ens160_i2c + i2c_id: i2c_ens160 + address: 0x53 + eco2: + name: "ENS160 eCO2" + tvoc: + name: "ENS160 Total Volatile Organic Compounds" + aqi: + name: "ENS160 Air Quality Index" diff --git a/tests/components/ens160_i2c/test.esp32-ard.yaml b/tests/components/ens160_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/ens160_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp32-c3-ard.yaml b/tests/components/ens160_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/ens160_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp32-c3-idf.yaml b/tests/components/ens160_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/ens160_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp32-idf.yaml b/tests/components/ens160_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/ens160_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp8266-ard.yaml b/tests/components/ens160_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/ens160_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.rp2040-ard.yaml b/tests/components/ens160_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/ens160_i2c/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/ens160_spi/common.yaml b/tests/components/ens160_spi/common.yaml new file mode 100644 index 0000000000..7250ead228 --- /dev/null +++ b/tests/components/ens160_spi/common.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_ens160 + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +sensor: + - platform: ens160_spi + spi_id: spi_ens160 + cs_pin: ${cs_pin} + eco2: + name: "ENS160 eCO2" + tvoc: + name: "ENS160 Total Volatile Organic Compounds" + aqi: + name: "ENS160 Air Quality Index" + diff --git a/tests/components/ens160_spi/test.esp32-ard.yaml b/tests/components/ens160_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/ens160_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp32-c3-ard.yaml b/tests/components/ens160_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/ens160_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp32-c3-idf.yaml b/tests/components/ens160_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/ens160_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp32-idf.yaml b/tests/components/ens160_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/ens160_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp8266-ard.yaml b/tests/components/ens160_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dbd158d030 --- /dev/null +++ b/tests/components/ens160_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO14 + mosi_pin: GPIO13 + miso_pin: GPIO12 + cs_pin: GPIO15 + +<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.rp2040-ard.yaml b/tests/components/ens160_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f6c3f1eeca --- /dev/null +++ b/tests/components/ens160_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ens210/test.esp32-ard.yaml b/tests/components/ens210/test.esp32-ard.yaml new file mode 100644 index 0000000000..8b2d29cc25 --- /dev/null +++ b/tests/components/ens210/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ens210 + scl: 16 + sda: 17 + +sensor: + - platform: ens210 + temperature: + name: ENS210 Temperature + humidity: + name: ENS210 Humidity + update_interval: 15s diff --git a/tests/components/ens210/test.esp32-c3-ard.yaml b/tests/components/ens210/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..bacb71f9f8 --- /dev/null +++ b/tests/components/ens210/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ens210 + scl: 5 + sda: 4 + +sensor: + - platform: ens210 + temperature: + name: ENS210 Temperature + humidity: + name: ENS210 Humidity + update_interval: 15s diff --git a/tests/components/ens210/test.esp32-c3-idf.yaml b/tests/components/ens210/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..bacb71f9f8 --- /dev/null +++ b/tests/components/ens210/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ens210 + scl: 5 + sda: 4 + +sensor: + - platform: ens210 + temperature: + name: ENS210 Temperature + humidity: + name: ENS210 Humidity + update_interval: 15s diff --git a/tests/components/ens210/test.esp32-idf.yaml b/tests/components/ens210/test.esp32-idf.yaml new file mode 100644 index 0000000000..8b2d29cc25 --- /dev/null +++ b/tests/components/ens210/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ens210 + scl: 16 + sda: 17 + +sensor: + - platform: ens210 + temperature: + name: ENS210 Temperature + humidity: + name: ENS210 Humidity + update_interval: 15s diff --git a/tests/components/ens210/test.esp8266-ard.yaml b/tests/components/ens210/test.esp8266-ard.yaml new file mode 100644 index 0000000000..bacb71f9f8 --- /dev/null +++ b/tests/components/ens210/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ens210 + scl: 5 + sda: 4 + +sensor: + - platform: ens210 + temperature: + name: ENS210 Temperature + humidity: + name: ENS210 Humidity + update_interval: 15s diff --git a/tests/components/ens210/test.rp2040-ard.yaml b/tests/components/ens210/test.rp2040-ard.yaml new file mode 100644 index 0000000000..bacb71f9f8 --- /dev/null +++ b/tests/components/ens210/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_ens210 + scl: 5 + sda: 4 + +sensor: + - platform: ens210 + temperature: + name: ENS210 Temperature + humidity: + name: ENS210 Humidity + update_interval: 15s diff --git a/tests/components/es8311/common.yaml b/tests/components/es8311/common.yaml new file mode 100644 index 0000000000..d833d1c043 --- /dev/null +++ b/tests/components/es8311/common.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - audio_dac.mute_off: + - audio_dac.mute_on: + - audio_dac.set_volume: + volume: 50% + +i2c: + - id: i2c_aic3204 + scl: ${scl_pin} + sda: ${sda_pin} + +audio_dac: + - platform: es8311 diff --git a/tests/components/es8311/test.esp32-ard.yaml b/tests/components/es8311/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es8311/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-c3-ard.yaml b/tests/components/es8311/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8311/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-c3-idf.yaml b/tests/components/es8311/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8311/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-idf.yaml b/tests/components/es8311/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es8311/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es8311/test.esp8266-ard.yaml b/tests/components/es8311/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8311/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/esp32_ble/common.yaml b/tests/components/esp32_ble/common.yaml new file mode 100644 index 0000000000..76b35fc8f8 --- /dev/null +++ b/tests/components/esp32_ble/common.yaml @@ -0,0 +1,2 @@ +esp32_ble: + io_capability: keyboard_only diff --git a/tests/components/esp32_ble/test.esp32-ard.yaml b/tests/components/esp32_ble/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble/test.esp32-c3-ard.yaml b/tests/components/esp32_ble/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble/test.esp32-c3-idf.yaml b/tests/components/esp32_ble/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble/test.esp32-idf.yaml b/tests/components/esp32_ble/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_beacon/common.yaml b/tests/components/esp32_ble_beacon/common.yaml new file mode 100644 index 0000000000..aafb0341d7 --- /dev/null +++ b/tests/components/esp32_ble_beacon/common.yaml @@ -0,0 +1,3 @@ +esp32_ble_beacon: + type: iBeacon + uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' diff --git a/tests/components/esp32_ble_beacon/test.esp32-ard.yaml b/tests/components/esp32_ble_beacon/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_beacon/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_beacon/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_beacon/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_beacon/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_beacon/test.esp32-c3-idf.yaml b/tests/components/esp32_ble_beacon/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_beacon/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_beacon/test.esp32-idf.yaml b/tests/components/esp32_ble_beacon/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_beacon/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_client/common.yaml b/tests/components/esp32_ble_client/common.yaml new file mode 100644 index 0000000000..33b7205bf2 --- /dev/null +++ b/tests/components/esp32_ble_client/common.yaml @@ -0,0 +1,5 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: blec diff --git a/tests/components/esp32_ble_client/test.esp32-ard.yaml b/tests/components/esp32_ble_client/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_client/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_client/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_client/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_client/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_client/test.esp32-c3-idf.yaml b/tests/components/esp32_ble_client/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_client/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_client/test.esp32-idf.yaml b/tests/components/esp32_ble_client/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_client/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml new file mode 100644 index 0000000000..29a5407f84 --- /dev/null +++ b/tests/components/esp32_ble_server/common.yaml @@ -0,0 +1,3 @@ +esp32_ble_server: + id: ble + manufacturer_data: [0x72, 0x4, 0x00, 0x23] diff --git a/tests/components/esp32_ble_server/test.esp32-ard.yaml b/tests/components/esp32_ble_server/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_server/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_server/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_server/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_server/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_server/test.esp32-c3-idf.yaml b/tests/components/esp32_ble_server/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_server/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_server/test.esp32-idf.yaml b/tests/components/esp32_ble_server/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_server/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_tracker/common.yaml b/tests/components/esp32_ble_tracker/common.yaml new file mode 100644 index 0000000000..ef23635c9e --- /dev/null +++ b/tests/components/esp32_ble_tracker/common.yaml @@ -0,0 +1,41 @@ +esphome: + on_boot: + then: + - esp32_ble_tracker.start_scan + - esp32_ble_tracker.stop_scan + +esp32_ble_tracker: + on_ble_advertise: + - mac_address: + - AA:BB:CC:DD:EE:FF + - FF:EE:DD:CC:BB:AA + then: + # yamllint disable rule:line-length + - lambda: !lambda |- + ESP_LOGD("main", "The device address (%s) exists in list", x.address_str().c_str()); + # yamllint enable rule:line-length + - mac_address: AC:37:43:77:5F:4C + then: + # yamllint disable rule:line-length + - lambda: !lambda |- + ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); + # yamllint enable rule:line-length + - then: + # yamllint disable rule:line-length + - lambda: !lambda |- + ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); + # yamllint enable rule:line-length + on_ble_service_data_advertise: + - service_uuid: ABCD + then: + - lambda: !lambda |- + ESP_LOGD("main", "Length of service data is %i", x.size()); + on_ble_manufacturer_data_advertise: + - manufacturer_id: ABCD + then: + - lambda: !lambda |- + ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); + on_scan_end: + - then: + - lambda: |- + ESP_LOGD("ble_auto", "The scan has ended!"); diff --git a/tests/components/esp32_ble_tracker/test.esp32-ard.yaml b/tests/components/esp32_ble_tracker/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_ble_tracker/test.esp32-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_camera/common.yaml b/tests/components/esp32_camera/common.yaml new file mode 100644 index 0000000000..2f5f792f1c --- /dev/null +++ b/tests/components/esp32_camera/common.yaml @@ -0,0 +1,28 @@ +esp32_camera: + name: ESP32 Camera + data_pins: + - number: 17 + - number: 35 + - number: 34 + - number: 5 + - number: 39 + - number: 18 + - number: 36 + - number: 19 + vsync_pin: 22 + href_pin: 26 + pixel_clock_pin: 21 + external_clock: + pin: 27 + frequency: 20MHz + i2c_pins: + sda: 25 + scl: 23 + reset_pin: 15 + power_down_pin: 1 + resolution: 640x480 + jpeg_quality: 10 + on_image: + then: + - lambda: |- + ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); diff --git a/tests/components/esp32_camera/test.esp32-ard.yaml b/tests/components/esp32_camera/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_camera/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_camera/test.esp32-idf.yaml b/tests/components/esp32_camera/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_camera/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_camera_web_server/common.yaml b/tests/components/esp32_camera_web_server/common.yaml new file mode 100644 index 0000000000..5edefdf0a8 --- /dev/null +++ b/tests/components/esp32_camera_web_server/common.yaml @@ -0,0 +1,34 @@ +esp32_camera: + name: ESP32 Camera + data_pins: + - number: 17 + - number: 35 + - number: 34 + - number: 5 + - number: 39 + - number: 18 + - number: 36 + - number: 19 + vsync_pin: 22 + href_pin: 26 + pixel_clock_pin: 21 + external_clock: + pin: 27 + frequency: 20MHz + i2c_pins: + sda: 25 + scl: 23 + reset_pin: 15 + power_down_pin: 1 + resolution: 640x480 + jpeg_quality: 10 + on_image: + then: + - lambda: |- + ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); + +esp32_camera_web_server: + - port: 8080 + mode: stream + - port: 8081 + mode: snapshot diff --git a/tests/components/esp32_camera_web_server/test.esp32-ard.yaml b/tests/components/esp32_camera_web_server/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_camera_web_server/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_camera_web_server/test.esp32-idf.yaml b/tests/components/esp32_camera_web_server/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_camera_web_server/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_can/test.esp32-ard.yaml b/tests/components/esp32_can/test.esp32-ard.yaml new file mode 100644 index 0000000000..159a695853 --- /dev/null +++ b/tests/components/esp32_can/test.esp32-ard.yaml @@ -0,0 +1,45 @@ +esphome: + on_boot: + then: + - canbus.send: + # Extended ID explicit + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 13 + tx_pin: 14 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/esp32_can/test.esp32-c3-ard.yaml b/tests/components/esp32_can/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b4fd34cf51 --- /dev/null +++ b/tests/components/esp32_can/test.esp32-c3-ard.yaml @@ -0,0 +1,45 @@ +esphome: + on_boot: + then: + - canbus.send: + # Extended ID explicit + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 4 + tx_pin: 5 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/esp32_can/test.esp32-c3-idf.yaml b/tests/components/esp32_can/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b4fd34cf51 --- /dev/null +++ b/tests/components/esp32_can/test.esp32-c3-idf.yaml @@ -0,0 +1,45 @@ +esphome: + on_boot: + then: + - canbus.send: + # Extended ID explicit + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 4 + tx_pin: 5 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/esp32_can/test.esp32-idf.yaml b/tests/components/esp32_can/test.esp32-idf.yaml new file mode 100644 index 0000000000..159a695853 --- /dev/null +++ b/tests/components/esp32_can/test.esp32-idf.yaml @@ -0,0 +1,45 @@ +esphome: + on_boot: + then: + - canbus.send: + # Extended ID explicit + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: 13 + tx_pin: 14 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/esp32_dac/common.yaml b/tests/components/esp32_dac/common.yaml new file mode 100644 index 0000000000..225627f5af --- /dev/null +++ b/tests/components/esp32_dac/common.yaml @@ -0,0 +1,4 @@ +output: + - platform: esp32_dac + id: dac_output + pin: 25 diff --git a/tests/components/esp32_dac/test.esp32-ard.yaml b/tests/components/esp32_dac/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_dac/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_dac/test.esp32-idf.yaml b/tests/components/esp32_dac/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_dac/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_hall/test.esp32-ard.yaml b/tests/components/esp32_hall/test.esp32-ard.yaml new file mode 100644 index 0000000000..f8429f5aa0 --- /dev/null +++ b/tests/components/esp32_hall/test.esp32-ard.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: esp32_hall + name: ESP32 Hall Sensor diff --git a/tests/components/esp32_improv/common.yaml b/tests/components/esp32_improv/common.yaml new file mode 100644 index 0000000000..7eb3f9c0be --- /dev/null +++ b/tests/components/esp32_improv/common.yaml @@ -0,0 +1,18 @@ +wifi: + ssid: MySSID + password: password1 + +binary_sensor: + - platform: gpio + pin: 0 + id: io0_button + +output: + - platform: gpio + pin: 2 + id: built_in_led + +esp32_improv: + authorizer: io0_button + authorized_duration: 1min + status_indicator: built_in_led diff --git a/tests/components/esp32_improv/test.esp32-ard.yaml b/tests/components/esp32_improv/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_improv/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_improv/test.esp32-c3-ard.yaml b/tests/components/esp32_improv/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_improv/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_improv/test.esp32-c3-idf.yaml b/tests/components/esp32_improv/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_improv/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_improv/test.esp32-idf.yaml b/tests/components/esp32_improv/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_improv/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml new file mode 100644 index 0000000000..d51a66451f --- /dev/null +++ b/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml @@ -0,0 +1,18 @@ +light: + - platform: esp32_rmt_led_strip + id: led_strip + pin: 13 + num_leds: 60 + rmt_channel: 6 + rgb_order: GRB + chipset: ws2812 + - platform: esp32_rmt_led_strip + id: led_strip2 + pin: 14 + num_leds: 60 + rmt_channel: 2 + rgb_order: RGB + bit0_high: 100us + bit0_low: 100us + bit1_high: 100us + bit1_low: 100us diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b226d1de06 --- /dev/null +++ b/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml @@ -0,0 +1,18 @@ +light: + - platform: esp32_rmt_led_strip + id: led_strip + pin: 4 + num_leds: 60 + rmt_channel: 0 + rgb_order: GRB + chipset: ws2812 + - platform: esp32_rmt_led_strip + id: led_strip2 + pin: 5 + num_leds: 60 + rmt_channel: 1 + rgb_order: RGB + bit0_high: 100us + bit0_low: 100us + bit1_high: 100us + bit1_low: 100us diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8d04d3370b --- /dev/null +++ b/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +light: + - platform: esp32_rmt_led_strip + id: led_strip + pin: 4 + num_leds: 60 + rmt_channel: 0 + rgb_order: GRB + chipset: ws2812 + - platform: esp32_rmt_led_strip + id: led_strip2 + pin: 5 + num_leds: 60 + rmt_channel: 1 + rgb_order: RGB + bit0_high: 100µs + bit0_low: 100µs + bit1_high: 100µs + bit1_low: 100µs diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml new file mode 100644 index 0000000000..6e1763b339 --- /dev/null +++ b/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +light: + - platform: esp32_rmt_led_strip + id: led_strip + pin: 13 + num_leds: 60 + rmt_channel: 6 + rgb_order: GRB + chipset: ws2812 + - platform: esp32_rmt_led_strip + id: led_strip2 + pin: 14 + num_leds: 60 + rmt_channel: 2 + rgb_order: RGB + bit0_high: 100µs + bit0_low: 100µs + bit1_high: 100µs + bit1_low: 100µs diff --git a/tests/components/esp32_touch/common.yaml b/tests/components/esp32_touch/common.yaml new file mode 100644 index 0000000000..691cce8d86 --- /dev/null +++ b/tests/components/esp32_touch/common.yaml @@ -0,0 +1,16 @@ +esp32_touch: + setup_mode: false + iir_filter: 10ms + sleep_duration: 27ms + measurement_duration: 8ms + low_voltage_reference: 0.5V + high_voltage_reference: 2.7V + voltage_attenuation: 1.5V + +binary_sensor: + - platform: esp32_touch + name: ESP32 Touch Pad + pin: 27 + threshold: 1000 + on_press: + - logger.log: "I'm touched!" diff --git a/tests/components/esp32_touch/test.esp32-ard.yaml b/tests/components/esp32_touch/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_touch/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp32_touch/test.esp32-idf.yaml b/tests/components/esp32_touch/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_touch/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esp8266_pwm/common.yaml b/tests/components/esp8266_pwm/common.yaml new file mode 100644 index 0000000000..52b290f91b --- /dev/null +++ b/tests/components/esp8266_pwm/common.yaml @@ -0,0 +1,8 @@ +output: + - platform: esp8266_pwm + id: out + pin: 4 + frequency: 50Hz + - platform: esp8266_pwm + id: out2 + pin: 5 diff --git a/tests/components/esp8266_pwm/test.esp8266-ard.yaml b/tests/components/esp8266_pwm/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp8266_pwm/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ethernet/common-dp83848.yaml b/tests/components/ethernet/common-dp83848.yaml new file mode 100644 index 0000000000..5b6ed3e8d0 --- /dev/null +++ b/tests/components/ethernet/common-dp83848.yaml @@ -0,0 +1,12 @@ +ethernet: + type: DP83848 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/common-ip101.yaml b/tests/components/ethernet/common-ip101.yaml new file mode 100644 index 0000000000..5ca369cce1 --- /dev/null +++ b/tests/components/ethernet/common-ip101.yaml @@ -0,0 +1,12 @@ +ethernet: + type: IP101 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/common-jl1101.yaml b/tests/components/ethernet/common-jl1101.yaml new file mode 100644 index 0000000000..639542d807 --- /dev/null +++ b/tests/components/ethernet/common-jl1101.yaml @@ -0,0 +1,12 @@ +ethernet: + type: JL1101 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/common-ksz8081.yaml b/tests/components/ethernet/common-ksz8081.yaml new file mode 100644 index 0000000000..167606a1eb --- /dev/null +++ b/tests/components/ethernet/common-ksz8081.yaml @@ -0,0 +1,12 @@ +ethernet: + type: KSZ8081 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/common-ksz8081rna.yaml b/tests/components/ethernet/common-ksz8081rna.yaml new file mode 100644 index 0000000000..f506906b1b --- /dev/null +++ b/tests/components/ethernet/common-ksz8081rna.yaml @@ -0,0 +1,12 @@ +ethernet: + type: KSZ8081RNA + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/common-lan8720.yaml b/tests/components/ethernet/common-lan8720.yaml new file mode 100644 index 0000000000..b9ed9cb036 --- /dev/null +++ b/tests/components/ethernet/common-lan8720.yaml @@ -0,0 +1,12 @@ +ethernet: + type: LAN8720 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/common-openeth.yaml b/tests/components/ethernet/common-openeth.yaml new file mode 100644 index 0000000000..fbb7579598 --- /dev/null +++ b/tests/components/ethernet/common-openeth.yaml @@ -0,0 +1,2 @@ +ethernet: + type: OPENETH diff --git a/tests/components/ethernet/common-rtl8201.yaml b/tests/components/ethernet/common-rtl8201.yaml new file mode 100644 index 0000000000..43842e7c9f --- /dev/null +++ b/tests/components/ethernet/common-rtl8201.yaml @@ -0,0 +1,12 @@ +ethernet: + type: RTL8201 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/common-w5500.yaml b/tests/components/ethernet/common-w5500.yaml new file mode 100644 index 0000000000..76661a75c3 --- /dev/null +++ b/tests/components/ethernet/common-w5500.yaml @@ -0,0 +1,14 @@ +ethernet: + type: W5500 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/test-dp83848.esp32-ard.yaml b/tests/components/ethernet/test-dp83848.esp32-ard.yaml new file mode 100644 index 0000000000..906bfba17c --- /dev/null +++ b/tests/components/ethernet/test-dp83848.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-dp83848.yaml diff --git a/tests/components/ethernet/test-dp83848.esp32-idf.yaml b/tests/components/ethernet/test-dp83848.esp32-idf.yaml new file mode 100644 index 0000000000..906bfba17c --- /dev/null +++ b/tests/components/ethernet/test-dp83848.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-dp83848.yaml diff --git a/tests/components/ethernet/test-ip101.esp32-ard.yaml b/tests/components/ethernet/test-ip101.esp32-ard.yaml new file mode 100644 index 0000000000..e52329d7ea --- /dev/null +++ b/tests/components/ethernet/test-ip101.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-ip101.yaml diff --git a/tests/components/ethernet/test-ip101.esp32-idf.yaml b/tests/components/ethernet/test-ip101.esp32-idf.yaml new file mode 100644 index 0000000000..e52329d7ea --- /dev/null +++ b/tests/components/ethernet/test-ip101.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-ip101.yaml diff --git a/tests/components/ethernet/test-jl1101.esp32-ard.yaml b/tests/components/ethernet/test-jl1101.esp32-ard.yaml new file mode 100644 index 0000000000..95d8cd1f21 --- /dev/null +++ b/tests/components/ethernet/test-jl1101.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-jl1101.yaml diff --git a/tests/components/ethernet/test-jl1101.esp32-idf.yaml b/tests/components/ethernet/test-jl1101.esp32-idf.yaml new file mode 100644 index 0000000000..95d8cd1f21 --- /dev/null +++ b/tests/components/ethernet/test-jl1101.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-jl1101.yaml diff --git a/tests/components/ethernet/test-ksz8081.esp32-ard.yaml b/tests/components/ethernet/test-ksz8081.esp32-ard.yaml new file mode 100644 index 0000000000..8f3c750c77 --- /dev/null +++ b/tests/components/ethernet/test-ksz8081.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-ksz8081.yaml diff --git a/tests/components/ethernet/test-ksz8081.esp32-idf.yaml b/tests/components/ethernet/test-ksz8081.esp32-idf.yaml new file mode 100644 index 0000000000..8f3c750c77 --- /dev/null +++ b/tests/components/ethernet/test-ksz8081.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-ksz8081.yaml diff --git a/tests/components/ethernet/test-ksz8081rna.esp32-ard.yaml b/tests/components/ethernet/test-ksz8081rna.esp32-ard.yaml new file mode 100644 index 0000000000..a48e591996 --- /dev/null +++ b/tests/components/ethernet/test-ksz8081rna.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-ksz8081rna.yaml diff --git a/tests/components/ethernet/test-ksz8081rna.esp32-idf.yaml b/tests/components/ethernet/test-ksz8081rna.esp32-idf.yaml new file mode 100644 index 0000000000..a48e591996 --- /dev/null +++ b/tests/components/ethernet/test-ksz8081rna.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-ksz8081rna.yaml diff --git a/tests/components/ethernet/test-lan8720.esp32-ard.yaml b/tests/components/ethernet/test-lan8720.esp32-ard.yaml new file mode 100644 index 0000000000..3df9ac874a --- /dev/null +++ b/tests/components/ethernet/test-lan8720.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-lan8720.yaml diff --git a/tests/components/ethernet/test-lan8720.esp32-idf.yaml b/tests/components/ethernet/test-lan8720.esp32-idf.yaml new file mode 100644 index 0000000000..3df9ac874a --- /dev/null +++ b/tests/components/ethernet/test-lan8720.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-lan8720.yaml diff --git a/tests/components/ethernet/test-openeth.esp32-idf.yaml b/tests/components/ethernet/test-openeth.esp32-idf.yaml new file mode 100644 index 0000000000..220316f3ee --- /dev/null +++ b/tests/components/ethernet/test-openeth.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-openeth.yaml diff --git a/tests/components/ethernet/test-rtl8201.esp32-ard.yaml b/tests/components/ethernet/test-rtl8201.esp32-ard.yaml new file mode 100644 index 0000000000..e69f88dc94 --- /dev/null +++ b/tests/components/ethernet/test-rtl8201.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-rtl8201.yaml diff --git a/tests/components/ethernet/test-rtl8201.esp32-idf.yaml b/tests/components/ethernet/test-rtl8201.esp32-idf.yaml new file mode 100644 index 0000000000..e69f88dc94 --- /dev/null +++ b/tests/components/ethernet/test-rtl8201.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-rtl8201.yaml diff --git a/tests/components/ethernet/test-w5500.esp32-ard.yaml b/tests/components/ethernet/test-w5500.esp32-ard.yaml new file mode 100644 index 0000000000..36f1b5365f --- /dev/null +++ b/tests/components/ethernet/test-w5500.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-w5500.yaml diff --git a/tests/components/ethernet/test-w5500.esp32-idf.yaml b/tests/components/ethernet/test-w5500.esp32-idf.yaml new file mode 100644 index 0000000000..36f1b5365f --- /dev/null +++ b/tests/components/ethernet/test-w5500.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-w5500.yaml diff --git a/tests/components/ethernet_info/common.yaml b/tests/components/ethernet_info/common.yaml new file mode 100644 index 0000000000..d9a6f515b1 --- /dev/null +++ b/tests/components/ethernet_info/common.yaml @@ -0,0 +1,21 @@ +ethernet: + type: LAN8720 + mdc_pin: 23 + mdio_pin: 25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: 26 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + +text_sensor: + - platform: ethernet_info + ip_address: + name: IP Address + dns_address: + name: DNS Address + mac_address: + name: MAC Address diff --git a/tests/components/ethernet_info/test.esp32-ard.yaml b/tests/components/ethernet_info/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ethernet_info/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ethernet_info/test.esp32-idf.yaml b/tests/components/ethernet_info/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ethernet_info/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/common.yaml b/tests/components/event/common.yaml new file mode 100644 index 0000000000..71cc19a6b0 --- /dev/null +++ b/tests/components/event/common.yaml @@ -0,0 +1,9 @@ +event: + - platform: template + name: Event + id: some_event + event_types: + - template_event_type1 + - template_event_type2 + on_event: + - logger.log: Event fired diff --git a/tests/components/event/test.esp32-ard.yaml b/tests/components/event/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.esp32-c3-ard.yaml b/tests/components/event/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.esp32-c3-idf.yaml b/tests/components/event/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.esp32-idf.yaml b/tests/components/event/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.esp8266-ard.yaml b/tests/components/event/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.rp2040-ard.yaml b/tests/components/event/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/exposure_notifications/common.yaml b/tests/components/exposure_notifications/common.yaml new file mode 100644 index 0000000000..faba5bb2d1 --- /dev/null +++ b/tests/components/exposure_notifications/common.yaml @@ -0,0 +1,9 @@ +esp32_ble_tracker: + +exposure_notifications: + on_exposure_notification: + then: + - lambda: | + ESP_LOGD("main", "Got notification:"); + ESP_LOGD("main", " RPI: %s", format_hex(x.rolling_proximity_identifier).c_str()); + ESP_LOGD("main", " RSSI: %d", x.rssi); diff --git a/tests/components/exposure_notifications/test.esp32-ard.yaml b/tests/components/exposure_notifications/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/exposure_notifications/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/exposure_notifications/test.esp32-c3-ard.yaml b/tests/components/exposure_notifications/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/exposure_notifications/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/exposure_notifications/test.esp32-c3-idf.yaml b/tests/components/exposure_notifications/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/exposure_notifications/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/exposure_notifications/test.esp32-idf.yaml b/tests/components/exposure_notifications/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/exposure_notifications/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/external_components/common.yaml b/tests/components/external_components/common.yaml new file mode 100644 index 0000000000..2b51267ec6 --- /dev/null +++ b/tests/components/external_components/common.yaml @@ -0,0 +1,6 @@ +external_components: + - source: github://esphome/esphome@dev + refresh: 1d + components: [bh1750] + - source: ../../../esphome/components + components: [sntp] diff --git a/tests/components/external_components/test.esp32-ard.yaml b/tests/components/external_components/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/external_components/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/external_components/test.esp32-c3-ard.yaml b/tests/components/external_components/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/external_components/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/external_components/test.esp32-c3-idf.yaml b/tests/components/external_components/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/external_components/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/external_components/test.esp32-idf.yaml b/tests/components/external_components/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/external_components/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/external_components/test.esp8266-ard.yaml b/tests/components/external_components/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/external_components/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/external_components/test.rp2040-ard.yaml b/tests/components/external_components/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/external_components/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ezo/test.esp32-ard.yaml b/tests/components/ezo/test.esp32-ard.yaml new file mode 100644 index 0000000000..61a8d2b25f --- /dev/null +++ b/tests/components/ezo/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_ezo + scl: 16 + sda: 17 + +sensor: + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: pH diff --git a/tests/components/ezo/test.esp32-c3-ard.yaml b/tests/components/ezo/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..93ceb9efd3 --- /dev/null +++ b/tests/components/ezo/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_ezo + scl: 5 + sda: 4 + +sensor: + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: pH diff --git a/tests/components/ezo/test.esp32-c3-idf.yaml b/tests/components/ezo/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..93ceb9efd3 --- /dev/null +++ b/tests/components/ezo/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_ezo + scl: 5 + sda: 4 + +sensor: + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: pH diff --git a/tests/components/ezo/test.esp32-idf.yaml b/tests/components/ezo/test.esp32-idf.yaml new file mode 100644 index 0000000000..61a8d2b25f --- /dev/null +++ b/tests/components/ezo/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_ezo + scl: 16 + sda: 17 + +sensor: + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: pH diff --git a/tests/components/ezo/test.esp8266-ard.yaml b/tests/components/ezo/test.esp8266-ard.yaml new file mode 100644 index 0000000000..93ceb9efd3 --- /dev/null +++ b/tests/components/ezo/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_ezo + scl: 5 + sda: 4 + +sensor: + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: pH diff --git a/tests/components/ezo/test.rp2040-ard.yaml b/tests/components/ezo/test.rp2040-ard.yaml new file mode 100644 index 0000000000..93ceb9efd3 --- /dev/null +++ b/tests/components/ezo/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_ezo + scl: 5 + sda: 4 + +sensor: + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: pH diff --git a/tests/components/ezo_pmp/test.esp32-ard.yaml b/tests/components/ezo_pmp/test.esp32-ard.yaml new file mode 100644 index 0000000000..9fc929b365 --- /dev/null +++ b/tests/components/ezo_pmp/test.esp32-ard.yaml @@ -0,0 +1,61 @@ +i2c: + - id: i2c_ezo_pmp + scl: 16 + sda: 17 + +ezo_pmp: + id: hcl_pump + update_interval: 1s + +binary_sensor: + - platform: ezo_pmp + pump_state: + name: Pump State + is_paused: + name: Is Paused + +sensor: + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,? diff --git a/tests/components/ezo_pmp/test.esp32-c3-ard.yaml b/tests/components/ezo_pmp/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fa047de3de --- /dev/null +++ b/tests/components/ezo_pmp/test.esp32-c3-ard.yaml @@ -0,0 +1,61 @@ +i2c: + - id: i2c_ezo_pmp + scl: 5 + sda: 4 + +ezo_pmp: + id: hcl_pump + update_interval: 1s + +binary_sensor: + - platform: ezo_pmp + pump_state: + name: Pump State + is_paused: + name: Is Paused + +sensor: + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,? diff --git a/tests/components/ezo_pmp/test.esp32-c3-idf.yaml b/tests/components/ezo_pmp/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fa047de3de --- /dev/null +++ b/tests/components/ezo_pmp/test.esp32-c3-idf.yaml @@ -0,0 +1,61 @@ +i2c: + - id: i2c_ezo_pmp + scl: 5 + sda: 4 + +ezo_pmp: + id: hcl_pump + update_interval: 1s + +binary_sensor: + - platform: ezo_pmp + pump_state: + name: Pump State + is_paused: + name: Is Paused + +sensor: + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,? diff --git a/tests/components/ezo_pmp/test.esp32-idf.yaml b/tests/components/ezo_pmp/test.esp32-idf.yaml new file mode 100644 index 0000000000..9fc929b365 --- /dev/null +++ b/tests/components/ezo_pmp/test.esp32-idf.yaml @@ -0,0 +1,61 @@ +i2c: + - id: i2c_ezo_pmp + scl: 16 + sda: 17 + +ezo_pmp: + id: hcl_pump + update_interval: 1s + +binary_sensor: + - platform: ezo_pmp + pump_state: + name: Pump State + is_paused: + name: Is Paused + +sensor: + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,? diff --git a/tests/components/ezo_pmp/test.esp8266-ard.yaml b/tests/components/ezo_pmp/test.esp8266-ard.yaml new file mode 100644 index 0000000000..fa047de3de --- /dev/null +++ b/tests/components/ezo_pmp/test.esp8266-ard.yaml @@ -0,0 +1,61 @@ +i2c: + - id: i2c_ezo_pmp + scl: 5 + sda: 4 + +ezo_pmp: + id: hcl_pump + update_interval: 1s + +binary_sensor: + - platform: ezo_pmp + pump_state: + name: Pump State + is_paused: + name: Is Paused + +sensor: + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,? diff --git a/tests/components/ezo_pmp/test.rp2040-ard.yaml b/tests/components/ezo_pmp/test.rp2040-ard.yaml new file mode 100644 index 0000000000..fa047de3de --- /dev/null +++ b/tests/components/ezo_pmp/test.rp2040-ard.yaml @@ -0,0 +1,61 @@ +i2c: + - id: i2c_ezo_pmp + scl: 5 + sda: 4 + +ezo_pmp: + id: hcl_pump + update_interval: 1s + +binary_sensor: + - platform: ezo_pmp + pump_state: + name: Pump State + is_paused: + name: Is Paused + +sensor: + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,? diff --git a/tests/components/factory_reset/common.yaml b/tests/components/factory_reset/common.yaml new file mode 100644 index 0000000000..ad3abd603e --- /dev/null +++ b/tests/components/factory_reset/common.yaml @@ -0,0 +1,3 @@ +button: + - platform: factory_reset + name: Reset to Factory Default Settings diff --git a/tests/components/factory_reset/test.esp32-ard.yaml b/tests/components/factory_reset/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/factory_reset/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp32-c3-ard.yaml b/tests/components/factory_reset/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/factory_reset/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp32-c3-idf.yaml b/tests/components/factory_reset/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/factory_reset/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp32-idf.yaml b/tests/components/factory_reset/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/factory_reset/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp8266-ard.yaml b/tests/components/factory_reset/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/factory_reset/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/factory_reset/test.rp2040-ard.yaml b/tests/components/factory_reset/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/factory_reset/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/fastled_clockless/common.yaml b/tests/components/fastled_clockless/common.yaml new file mode 100644 index 0000000000..8b1447a17a --- /dev/null +++ b/tests/components/fastled_clockless/common.yaml @@ -0,0 +1,71 @@ +light: + - platform: fastled_clockless + id: addr1 + chipset: WS2811 + pin: 13 + num_leds: 100 + rgb_order: BRG + max_refresh_rate: 20ms + color_correct: [75%, 100%, 50%] + name: FastLED WS2811 Light + effects: + - addressable_color_wipe: + - addressable_color_wipe: + name: Color Wipe Effect With Custom Values + colors: + - red: 100% + green: 100% + blue: 100% + num_leds: 1 + - red: 0% + green: 0% + blue: 0% + num_leds: 1 + add_led_interval: 100ms + reverse: false + - addressable_scan: + - addressable_scan: + name: Scan Effect With Custom Values + move_interval: 100ms + - addressable_twinkle: + - addressable_twinkle: + name: Twinkle Effect With Custom Values + twinkle_probability: 5% + progress_interval: 4ms + - addressable_random_twinkle: + - addressable_random_twinkle: + name: Random Twinkle Effect With Custom Values + twinkle_probability: 5% + progress_interval: 32ms + - addressable_fireworks: + - addressable_fireworks: + name: Fireworks Effect With Custom Values + update_interval: 32ms + spark_probability: 10% + use_random_color: false + fade_out_rate: 120 + - addressable_flicker: + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% + - addressable_lambda: + name: Test For Custom Lambda Effect + lambda: |- + if (initial_run) { + it[0] = current_color; + } + - automation: + name: Custom Effect + sequence: + - light.addressable_set: + id: addr1 + red: 100% + green: 100% + blue: 0% + - delay: 100ms + - light.addressable_set: + id: addr1 + red: 0% + green: 100% + blue: 0% diff --git a/tests/components/fastled_clockless/test.esp32-ard.yaml b/tests/components/fastled_clockless/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/fastled_clockless/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/fastled_spi/common.yaml b/tests/components/fastled_spi/common.yaml new file mode 100644 index 0000000000..f6f7c5553b --- /dev/null +++ b/tests/components/fastled_spi/common.yaml @@ -0,0 +1,71 @@ +light: + - platform: fastled_spi + id: addr1 + chipset: WS2801 + clock_pin: 22 + data_pin: 23 + data_rate: 2MHz + num_leds: 60 + rgb_order: BRG + name: FastLED SPI Light + effects: + - addressable_color_wipe: + - addressable_color_wipe: + name: Color Wipe Effect With Custom Values + colors: + - red: 100% + green: 100% + blue: 100% + num_leds: 1 + - red: 0% + green: 0% + blue: 0% + num_leds: 1 + add_led_interval: 100ms + reverse: false + - addressable_scan: + - addressable_scan: + name: Scan Effect With Custom Values + move_interval: 100ms + - addressable_twinkle: + - addressable_twinkle: + name: Twinkle Effect With Custom Values + twinkle_probability: 5% + progress_interval: 4ms + - addressable_random_twinkle: + - addressable_random_twinkle: + name: Random Twinkle Effect With Custom Values + twinkle_probability: 5% + progress_interval: 32ms + - addressable_fireworks: + - addressable_fireworks: + name: Fireworks Effect With Custom Values + update_interval: 32ms + spark_probability: 10% + use_random_color: false + fade_out_rate: 120 + - addressable_flicker: + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% + - addressable_lambda: + name: Test For Custom Lambda Effect + lambda: |- + if (initial_run) { + it[0] = current_color; + } + - automation: + name: Custom Effect + sequence: + - light.addressable_set: + id: addr1 + red: 100% + green: 100% + blue: 0% + - delay: 100ms + - light.addressable_set: + id: addr1 + red: 0% + green: 100% + blue: 0% diff --git a/tests/components/fastled_spi/test.esp32-ard.yaml b/tests/components/fastled_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/fastled_spi/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/feedback/common.yaml b/tests/components/feedback/common.yaml new file mode 100644 index 0000000000..f93d54e8b6 --- /dev/null +++ b/tests/components/feedback/common.yaml @@ -0,0 +1,39 @@ +binary_sensor: + - platform: template + id: open_endstop_sensor + - platform: template + id: open_sensor + - platform: template + id: open_obstacle_sensor + - platform: template + id: close_endstop_sensor + - platform: template + id: close_sensor + - platform: template + id: close_obstacle_sensor + +cover: + - platform: feedback + name: Feedback Cover + id: gate + device_class: gate + infer_endstop_from_movement: false + has_built_in_endstop: false + max_duration: 30s + direction_change_wait_time: 300ms + acceleration_wait_time: 150ms + obstacle_rollback: 10% + open_duration: 22.1s + open_endstop: open_endstop_sensor + open_sensor: open_sensor + open_obstacle_sensor: open_obstacle_sensor + close_duration: 22.4s + close_endstop: close_endstop_sensor + close_sensor: close_sensor + close_obstacle_sensor: close_obstacle_sensor + open_action: + - logger.log: Open Action + close_action: + - logger.log: Close Action + stop_action: + - logger.log: Stop Action diff --git a/tests/components/feedback/test.esp32-ard.yaml b/tests/components/feedback/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/feedback/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/feedback/test.esp32-c3-ard.yaml b/tests/components/feedback/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/feedback/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/feedback/test.esp32-c3-idf.yaml b/tests/components/feedback/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/feedback/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/feedback/test.esp32-idf.yaml b/tests/components/feedback/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/feedback/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/feedback/test.esp8266-ard.yaml b/tests/components/feedback/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/feedback/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/feedback/test.rp2040-ard.yaml b/tests/components/feedback/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/feedback/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/fingerprint_grow/test.esp32-ard.yaml b/tests/components/fingerprint_grow/test.esp32-ard.yaml new file mode 100644 index 0000000000..0950145a05 --- /dev/null +++ b/tests/components/fingerprint_grow/test.esp32-ard.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - fingerprint_grow.enroll: + finger_id: 2 + num_scans: 2 + - fingerprint_grow.cancel_enroll: + - fingerprint_grow.delete: + finger_id: 2 + - fingerprint_grow.delete_all: + +uart: + - id: uart_fingerprint_grow + tx_pin: 17 + rx_pin: 16 + baud_rate: 57600 + +fingerprint_grow: + sensing_pin: 18 + password: 0x12FE37DC + new_password: 0xA65B9840 + on_finger_scan_start: + - logger.log: test_fingerprint_grow_finger_scan_start + on_finger_scan_invalid: + - logger.log: test_fingerprint_grow_finger_scan_invalid + on_finger_scan_matched: + - logger.log: test_fingerprint_grow_finger_scan_matched + on_finger_scan_unmatched: + - logger.log: test_fingerprint_grow_finger_scan_unmatched + on_finger_scan_misplaced: + - logger.log: test_fingerprint_grow_finger_scan_misplaced + on_enrollment_scan: + - logger.log: test_fingerprint_grow_enrollment_scan + on_enrollment_done: + - logger.log: test_fingerprint_grow_node_enrollment_done + on_enrollment_failed: + - logger.log: test_fingerprint_grow_enrollment_failed + +binary_sensor: + - platform: fingerprint_grow + name: Fingerprint Enrolling + +sensor: + - platform: fingerprint_grow + fingerprint_count: + name: Fingerprint Count + status: + name: Fingerprint Status + capacity: + name: Fingerprint Capacity + security_level: + name: Fingerprint Security Level + last_finger_id: + name: Fingerprint Last Finger ID + last_confidence: + name: Fingerprint Last Confidence diff --git a/tests/components/fingerprint_grow/test.esp32-c3-ard.yaml b/tests/components/fingerprint_grow/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e7ac08eb28 --- /dev/null +++ b/tests/components/fingerprint_grow/test.esp32-c3-ard.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - fingerprint_grow.enroll: + finger_id: 2 + num_scans: 2 + - fingerprint_grow.cancel_enroll: + - fingerprint_grow.delete: + finger_id: 2 + - fingerprint_grow.delete_all: + +uart: + - id: uart_fingerprint_grow + tx_pin: 4 + rx_pin: 5 + baud_rate: 57600 + +fingerprint_grow: + sensing_pin: 6 + password: 0x12FE37DC + new_password: 0xA65B9840 + on_finger_scan_start: + - logger.log: test_fingerprint_grow_finger_scan_start + on_finger_scan_invalid: + - logger.log: test_fingerprint_grow_finger_scan_invalid + on_finger_scan_matched: + - logger.log: test_fingerprint_grow_finger_scan_matched + on_finger_scan_unmatched: + - logger.log: test_fingerprint_grow_finger_scan_unmatched + on_finger_scan_misplaced: + - logger.log: test_fingerprint_grow_finger_scan_misplaced + on_enrollment_scan: + - logger.log: test_fingerprint_grow_enrollment_scan + on_enrollment_done: + - logger.log: test_fingerprint_grow_node_enrollment_done + on_enrollment_failed: + - logger.log: test_fingerprint_grow_enrollment_failed + +binary_sensor: + - platform: fingerprint_grow + name: Fingerprint Enrolling + +sensor: + - platform: fingerprint_grow + fingerprint_count: + name: Fingerprint Count + status: + name: Fingerprint Status + capacity: + name: Fingerprint Capacity + security_level: + name: Fingerprint Security Level + last_finger_id: + name: Fingerprint Last Finger ID + last_confidence: + name: Fingerprint Last Confidence diff --git a/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml b/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e7ac08eb28 --- /dev/null +++ b/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - fingerprint_grow.enroll: + finger_id: 2 + num_scans: 2 + - fingerprint_grow.cancel_enroll: + - fingerprint_grow.delete: + finger_id: 2 + - fingerprint_grow.delete_all: + +uart: + - id: uart_fingerprint_grow + tx_pin: 4 + rx_pin: 5 + baud_rate: 57600 + +fingerprint_grow: + sensing_pin: 6 + password: 0x12FE37DC + new_password: 0xA65B9840 + on_finger_scan_start: + - logger.log: test_fingerprint_grow_finger_scan_start + on_finger_scan_invalid: + - logger.log: test_fingerprint_grow_finger_scan_invalid + on_finger_scan_matched: + - logger.log: test_fingerprint_grow_finger_scan_matched + on_finger_scan_unmatched: + - logger.log: test_fingerprint_grow_finger_scan_unmatched + on_finger_scan_misplaced: + - logger.log: test_fingerprint_grow_finger_scan_misplaced + on_enrollment_scan: + - logger.log: test_fingerprint_grow_enrollment_scan + on_enrollment_done: + - logger.log: test_fingerprint_grow_node_enrollment_done + on_enrollment_failed: + - logger.log: test_fingerprint_grow_enrollment_failed + +binary_sensor: + - platform: fingerprint_grow + name: Fingerprint Enrolling + +sensor: + - platform: fingerprint_grow + fingerprint_count: + name: Fingerprint Count + status: + name: Fingerprint Status + capacity: + name: Fingerprint Capacity + security_level: + name: Fingerprint Security Level + last_finger_id: + name: Fingerprint Last Finger ID + last_confidence: + name: Fingerprint Last Confidence diff --git a/tests/components/fingerprint_grow/test.esp32-idf.yaml b/tests/components/fingerprint_grow/test.esp32-idf.yaml new file mode 100644 index 0000000000..0950145a05 --- /dev/null +++ b/tests/components/fingerprint_grow/test.esp32-idf.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - fingerprint_grow.enroll: + finger_id: 2 + num_scans: 2 + - fingerprint_grow.cancel_enroll: + - fingerprint_grow.delete: + finger_id: 2 + - fingerprint_grow.delete_all: + +uart: + - id: uart_fingerprint_grow + tx_pin: 17 + rx_pin: 16 + baud_rate: 57600 + +fingerprint_grow: + sensing_pin: 18 + password: 0x12FE37DC + new_password: 0xA65B9840 + on_finger_scan_start: + - logger.log: test_fingerprint_grow_finger_scan_start + on_finger_scan_invalid: + - logger.log: test_fingerprint_grow_finger_scan_invalid + on_finger_scan_matched: + - logger.log: test_fingerprint_grow_finger_scan_matched + on_finger_scan_unmatched: + - logger.log: test_fingerprint_grow_finger_scan_unmatched + on_finger_scan_misplaced: + - logger.log: test_fingerprint_grow_finger_scan_misplaced + on_enrollment_scan: + - logger.log: test_fingerprint_grow_enrollment_scan + on_enrollment_done: + - logger.log: test_fingerprint_grow_node_enrollment_done + on_enrollment_failed: + - logger.log: test_fingerprint_grow_enrollment_failed + +binary_sensor: + - platform: fingerprint_grow + name: Fingerprint Enrolling + +sensor: + - platform: fingerprint_grow + fingerprint_count: + name: Fingerprint Count + status: + name: Fingerprint Status + capacity: + name: Fingerprint Capacity + security_level: + name: Fingerprint Security Level + last_finger_id: + name: Fingerprint Last Finger ID + last_confidence: + name: Fingerprint Last Confidence diff --git a/tests/components/fingerprint_grow/test.esp8266-ard.yaml b/tests/components/fingerprint_grow/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1d00d977b9 --- /dev/null +++ b/tests/components/fingerprint_grow/test.esp8266-ard.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - fingerprint_grow.enroll: + finger_id: 2 + num_scans: 2 + - fingerprint_grow.cancel_enroll: + - fingerprint_grow.delete: + finger_id: 2 + - fingerprint_grow.delete_all: + +uart: + - id: uart_fingerprint_grow + tx_pin: 4 + rx_pin: 5 + baud_rate: 57600 + +fingerprint_grow: + sensing_pin: 16 + password: 0x12FE37DC + new_password: 0xA65B9840 + on_finger_scan_start: + - logger.log: test_fingerprint_grow_finger_scan_start + on_finger_scan_invalid: + - logger.log: test_fingerprint_grow_finger_scan_invalid + on_finger_scan_matched: + - logger.log: test_fingerprint_grow_finger_scan_matched + on_finger_scan_unmatched: + - logger.log: test_fingerprint_grow_finger_scan_unmatched + on_finger_scan_misplaced: + - logger.log: test_fingerprint_grow_finger_scan_misplaced + on_enrollment_scan: + - logger.log: test_fingerprint_grow_enrollment_scan + on_enrollment_done: + - logger.log: test_fingerprint_grow_node_enrollment_done + on_enrollment_failed: + - logger.log: test_fingerprint_grow_enrollment_failed + +binary_sensor: + - platform: fingerprint_grow + name: Fingerprint Enrolling + +sensor: + - platform: fingerprint_grow + fingerprint_count: + name: Fingerprint Count + status: + name: Fingerprint Status + capacity: + name: Fingerprint Capacity + security_level: + name: Fingerprint Security Level + last_finger_id: + name: Fingerprint Last Finger ID + last_confidence: + name: Fingerprint Last Confidence diff --git a/tests/components/fingerprint_grow/test.rp2040-ard.yaml b/tests/components/fingerprint_grow/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e7ac08eb28 --- /dev/null +++ b/tests/components/fingerprint_grow/test.rp2040-ard.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - fingerprint_grow.enroll: + finger_id: 2 + num_scans: 2 + - fingerprint_grow.cancel_enroll: + - fingerprint_grow.delete: + finger_id: 2 + - fingerprint_grow.delete_all: + +uart: + - id: uart_fingerprint_grow + tx_pin: 4 + rx_pin: 5 + baud_rate: 57600 + +fingerprint_grow: + sensing_pin: 6 + password: 0x12FE37DC + new_password: 0xA65B9840 + on_finger_scan_start: + - logger.log: test_fingerprint_grow_finger_scan_start + on_finger_scan_invalid: + - logger.log: test_fingerprint_grow_finger_scan_invalid + on_finger_scan_matched: + - logger.log: test_fingerprint_grow_finger_scan_matched + on_finger_scan_unmatched: + - logger.log: test_fingerprint_grow_finger_scan_unmatched + on_finger_scan_misplaced: + - logger.log: test_fingerprint_grow_finger_scan_misplaced + on_enrollment_scan: + - logger.log: test_fingerprint_grow_enrollment_scan + on_enrollment_done: + - logger.log: test_fingerprint_grow_node_enrollment_done + on_enrollment_failed: + - logger.log: test_fingerprint_grow_enrollment_failed + +binary_sensor: + - platform: fingerprint_grow + name: Fingerprint Enrolling + +sensor: + - platform: fingerprint_grow + fingerprint_count: + name: Fingerprint Count + status: + name: Fingerprint Status + capacity: + name: Fingerprint Capacity + security_level: + name: Fingerprint Security Level + last_finger_id: + name: Fingerprint Last Finger ID + last_confidence: + name: Fingerprint Last Confidence diff --git a/tests/components/font/.gitattributes b/tests/components/font/.gitattributes new file mode 100644 index 0000000000..18d9a389e8 --- /dev/null +++ b/tests/components/font/.gitattributes @@ -0,0 +1,2 @@ +*.pcf -text + diff --git a/tests/components/font/MatrixChunky8X.bdf b/tests/components/font/MatrixChunky8X.bdf new file mode 100644 index 0000000000..89b3683180 --- /dev/null +++ b/tests/components/font/MatrixChunky8X.bdf @@ -0,0 +1,7461 @@ +STARTFONT 2.1 +FONT -Trip5-MatrixChunky8X-Medium-R-Normal--8-80-75-75-P-40-ISO10646-1 +SIZE 8 75 75 +FONTBOUNDINGBOX 8 8 -1 0 +COMMENT "Generated by fontforge, http://fontforge.sourceforge.net" +COMMENT "Trip5" +COMMENT "Conventional Chaos" +COMMENT "CC-BY" +STARTPROPERTIES 25 +FOUNDRY "Conventional Chaos" +FAMILY_NAME "MatrixChunky8X" +FONT_NAME "MatrixChunky8X" +FACE_NAME "MatrixChunky8X" +COPYRIGHT "https://github.com/trip5/Matrix-Fonts" +FONT_VERSION "001.000" +WEIGHT_NAME "Medium" +SLANT "R" +SETWIDTH_NAME "Normal" +ADD_STYLE_NAME "" +PIXEL_SIZE 8 +POINT_SIZE 80 +RESOLUTION_X 75 +RESOLUTION_Y 75 +SPACING "P" +AVERAGE_WIDTH 40 +CHARSET_REGISTRY "ISO10646" +CHARSET_ENCODING "1" +CHARSET_COLLECTIONS "ISO8859-2 ISO8859-9 ISO8859-4 ISO10646-1" +FONT_ASCENT 8 +FONT_DESCENT 0 +UNDERLINE_POSITION 0 +UNDERLINE_THICKNESS 1 +X_HEIGHT 6 +CAP_HEIGHT 8 +ENDPROPERTIES +CHARS 535 +STARTCHAR uni0000 +ENCODING 0 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +A0 +00 +40 +00 +A0 +ENDCHAR +STARTCHAR space +ENCODING 32 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR exclam +ENCODING 33 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 8 0 0 +BITMAP +80 +80 +80 +80 +80 +80 +00 +80 +ENDCHAR +STARTCHAR quotedbl +ENCODING 34 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 2 0 6 +BITMAP +A0 +A0 +ENDCHAR +STARTCHAR numbersign +ENCODING 35 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +50 +50 +F8 +50 +50 +F8 +50 +50 +ENDCHAR +STARTCHAR dollar +ENCODING 36 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +E0 +80 +E0 +20 +20 +E0 +40 +ENDCHAR +STARTCHAR percent +ENCODING 37 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +20 +40 +40 +80 +A0 +A0 +ENDCHAR +STARTCHAR ampersand +ENCODING 38 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +A0 +A0 +40 +B0 +A0 +B0 +D0 +ENDCHAR +STARTCHAR quotesingle +ENCODING 39 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 3 0 5 +BITMAP +80 +80 +80 +ENDCHAR +STARTCHAR parenleft +ENCODING 40 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 8 0 0 +BITMAP +40 +40 +80 +80 +80 +80 +40 +40 +ENDCHAR +STARTCHAR parenright +ENCODING 41 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 8 0 0 +BITMAP +80 +80 +40 +40 +40 +40 +80 +80 +ENDCHAR +STARTCHAR asterisk +ENCODING 42 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 4 0 4 +BITMAP +90 +60 +60 +90 +ENDCHAR +STARTCHAR plus +ENCODING 43 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 3 +BITMAP +40 +E0 +40 +ENDCHAR +STARTCHAR comma +ENCODING 44 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 2 0 0 +BITMAP +80 +80 +ENDCHAR +STARTCHAR hyphen +ENCODING 45 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 1 0 4 +BITMAP +E0 +ENDCHAR +STARTCHAR period +ENCODING 46 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 1 0 0 +BITMAP +80 +ENDCHAR +STARTCHAR slash +ENCODING 47 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +20 +40 +40 +40 +80 +80 +80 +ENDCHAR +STARTCHAR zero +ENCODING 48 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR one +ENCODING 49 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +C0 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR two +ENCODING 50 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR three +ENCODING 51 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +E0 +20 +20 +20 +E0 +ENDCHAR +STARTCHAR four +ENCODING 52 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +E0 +20 +20 +20 +20 +ENDCHAR +STARTCHAR five +ENCODING 53 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +20 +20 +20 +E0 +ENDCHAR +STARTCHAR six +ENCODING 54 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR seven +ENCODING 55 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +20 +20 +20 +20 +20 +ENDCHAR +STARTCHAR eight +ENCODING 56 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR nine +ENCODING 57 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +20 +20 +20 +E0 +ENDCHAR +STARTCHAR colon +ENCODING 58 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 3 0 3 +BITMAP +80 +00 +80 +ENDCHAR +STARTCHAR semicolon +ENCODING 59 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 4 0 2 +BITMAP +80 +00 +80 +80 +ENDCHAR +STARTCHAR less +ENCODING 60 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +20 +40 +80 +40 +20 +ENDCHAR +STARTCHAR equal +ENCODING 61 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 3 +BITMAP +E0 +00 +E0 +ENDCHAR +STARTCHAR greater +ENCODING 62 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +80 +40 +20 +40 +80 +ENDCHAR +STARTCHAR question +ENCODING 63 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +20 +60 +40 +40 +00 +40 +ENDCHAR +STARTCHAR at +ENCODING 64 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +B0 +B0 +80 +80 +F0 +ENDCHAR +STARTCHAR A +ENCODING 65 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR B +ENCODING 66 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR C +ENCODING 67 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +80 +80 +80 +80 +A0 +E0 +ENDCHAR +STARTCHAR D +ENCODING 68 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +A0 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR E +ENCODING 69 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR F +ENCODING 70 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR G +ENCODING 71 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +80 +80 +B0 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR H +ENCODING 72 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +90 +F0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR I +ENCODING 73 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR J +ENCODING 74 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +20 +20 +20 +20 +A0 +A0 +E0 +ENDCHAR +STARTCHAR K +ENCODING 75 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +C0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR L +ENCODING 76 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR M +ENCODING 77 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +D8 +A8 +A8 +88 +88 +88 +88 +ENDCHAR +STARTCHAR N +ENCODING 78 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +D0 +B0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR O +ENCODING 79 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR P +ENCODING 80 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR Q +ENCODING 81 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +B0 +B0 +A0 +D0 +ENDCHAR +STARTCHAR R +ENCODING 82 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR S +ENCODING 83 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +80 +E0 +20 +20 +A0 +E0 +ENDCHAR +STARTCHAR T +ENCODING 84 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR U +ENCODING 85 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR V +ENCODING 86 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +A0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR W +ENCODING 87 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +A8 +A8 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR X +ENCODING 88 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +40 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Y +ENCODING 89 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +E0 +40 +40 +40 +40 +ENDCHAR +STARTCHAR Z +ENCODING 90 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +40 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR bracketleft +ENCODING 91 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR backslash +ENCODING 92 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +40 +40 +40 +20 +20 +20 +ENDCHAR +STARTCHAR bracketright +ENCODING 93 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +20 +20 +20 +20 +E0 +ENDCHAR +STARTCHAR asciicircum +ENCODING 94 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 5 +BITMAP +40 +E0 +A0 +ENDCHAR +STARTCHAR underscore +ENCODING 95 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 1 0 0 +BITMAP +E0 +ENDCHAR +STARTCHAR grave +ENCODING 96 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 2 0 6 +BITMAP +80 +40 +ENDCHAR +STARTCHAR a +ENCODING 97 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR b +ENCODING 98 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR c +ENCODING 99 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR d +ENCODING 100 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +20 +20 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR e +ENCODING 101 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR f +ENCODING 102 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +60 +40 +40 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR g +ENCODING 103 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR h +ENCODING 104 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR i +ENCODING 105 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR j +ENCODING 106 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +20 +00 +20 +20 +20 +A0 +E0 +ENDCHAR +STARTCHAR k +ENCODING 107 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +A0 +A0 +C0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR l +ENCODING 108 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR m +ENCODING 109 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +A8 +A8 +A8 +A8 +ENDCHAR +STARTCHAR n +ENCODING 110 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR o +ENCODING 111 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR p +ENCODING 112 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +80 +80 +ENDCHAR +STARTCHAR q +ENCODING 113 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +20 +20 +ENDCHAR +STARTCHAR r +ENCODING 114 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR s +ENCODING 115 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR t +ENCODING 116 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +40 +40 +E0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR u +ENCODING 117 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR v +ENCODING 118 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR w +ENCODING 119 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +A8 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR x +ENCODING 120 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +40 +A0 +A0 +ENDCHAR +STARTCHAR y +ENCODING 121 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR z +ENCODING 122 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR braceleft +ENCODING 123 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +60 +40 +40 +C0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR bar +ENCODING 124 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 8 0 0 +BITMAP +80 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR braceright +ENCODING 125 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +40 +60 +40 +40 +40 +C0 +ENDCHAR +STARTCHAR asciitilde +ENCODING 126 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 2 0 3 +BITMAP +60 +C0 +ENDCHAR +STARTCHAR exclamdown +ENCODING 161 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 8 0 0 +BITMAP +80 +80 +00 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR cent +ENCODING 162 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +E0 +80 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR sterling +ENCODING 163 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +70 +50 +40 +E0 +40 +40 +40 +F0 +ENDCHAR +STARTCHAR currency +ENCODING 164 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 6 0 1 +BITMAP +F0 +60 +90 +90 +60 +F0 +ENDCHAR +STARTCHAR yen +ENCODING 165 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +E0 +40 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR brokenbar +ENCODING 166 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 8 0 0 +BITMAP +80 +80 +80 +00 +80 +80 +80 +80 +ENDCHAR +STARTCHAR section +ENCODING 167 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +60 +80 +E0 +A0 +A0 +E0 +20 +C0 +ENDCHAR +STARTCHAR dieresis +ENCODING 168 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +E0 +A0 +C0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR copyright +ENCODING 169 +SWIDTH 1000 0 +DWIDTH 7 0 +BBX 6 7 0 1 +BITMAP +78 +CC +B4 +A4 +B4 +CC +78 +ENDCHAR +STARTCHAR ordfeminine +ENCODING 170 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 3 +BITMAP +60 +A0 +E0 +00 +E0 +ENDCHAR +STARTCHAR guillemotleft +ENCODING 171 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 3 0 3 +BITMAP +50 +F0 +50 +ENDCHAR +STARTCHAR logicalnot +ENCODING 172 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A8 +20 +20 +20 +28 +28 +38 +ENDCHAR +STARTCHAR uni00AD +ENCODING 173 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 1 0 4 +BITMAP +E0 +ENDCHAR +STARTCHAR registered +ENCODING 174 +SWIDTH 1000 0 +DWIDTH 7 0 +BBX 6 7 0 1 +BITMAP +78 +CC +B4 +A4 +A4 +CC +78 +ENDCHAR +STARTCHAR macron +ENCODING 175 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A0 +20 +38 +20 +20 +20 +20 +ENDCHAR +STARTCHAR degree +ENCODING 176 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 5 +BITMAP +E0 +A0 +E0 +ENDCHAR +STARTCHAR plusminus +ENCODING 177 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +40 +E0 +40 +00 +E0 +ENDCHAR +STARTCHAR uni00B2 +ENCODING 178 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 5 0 3 +BITMAP +C0 +40 +C0 +80 +C0 +ENDCHAR +STARTCHAR uni00B3 +ENCODING 179 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 5 0 3 +BITMAP +C0 +40 +C0 +40 +C0 +ENDCHAR +STARTCHAR acute +ENCODING 180 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 2 0 6 +BITMAP +40 +C0 +ENDCHAR +STARTCHAR mu +ENCODING 181 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +A0 +A0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR paragraph +ENCODING 182 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +D0 +F0 +30 +30 +30 +30 +30 +ENDCHAR +STARTCHAR periodcentered +ENCODING 183 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 1 0 4 +BITMAP +80 +ENDCHAR +STARTCHAR cedilla +ENCODING 184 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +00 +E0 +00 +00 +00 +E0 +ENDCHAR +STARTCHAR uni00B9 +ENCODING 185 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 5 0 3 +BITMAP +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR ordmasculine +ENCODING 186 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 3 +BITMAP +E0 +A0 +E0 +00 +E0 +ENDCHAR +STARTCHAR guillemotright +ENCODING 187 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 3 0 3 +BITMAP +A0 +F0 +A0 +ENDCHAR +STARTCHAR onequarter +ENCODING 188 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +30 +60 +40 +F0 +40 +40 +60 +30 +ENDCHAR +STARTCHAR onehalf +ENCODING 189 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +54 +54 +FE +54 +54 +54 +7C +28 +ENDCHAR +STARTCHAR threequarters +ENCODING 190 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +70 +50 +10 +F8 +40 +40 +50 +70 +ENDCHAR +STARTCHAR questiondown +ENCODING 191 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +40 +40 +C0 +80 +A0 +E0 +ENDCHAR +STARTCHAR Agrave +ENCODING 192 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +E0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR Aacute +ENCODING 193 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR Acircumflex +ENCODING 194 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR Atilde +ENCODING 195 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +A0 +E0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Adieresis +ENCODING 196 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +A0 +E0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Aring +ENCODING 197 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +A0 +E0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR AE +ENCODING 198 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +A0 +A0 +F8 +A0 +A0 +A0 +B8 +ENDCHAR +STARTCHAR Ccedilla +ENCODING 199 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR Egrave +ENCODING 200 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR Eacute +ENCODING 201 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR Ecircumflex +ENCODING 202 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR Edieresis +ENCODING 203 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +80 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR Igrave +ENCODING 204 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +E0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Iacute +ENCODING 205 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Icircumflex +ENCODING 206 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Idieresis +ENCODING 207 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Eth +ENCODING 208 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +60 +50 +50 +F0 +50 +50 +50 +60 +ENDCHAR +STARTCHAR Ntilde +ENCODING 209 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +60 +00 +90 +D0 +B0 +90 +90 +90 +ENDCHAR +STARTCHAR Ograve +ENCODING 210 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +20 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Oacute +ENCODING 211 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Ocircumflex +ENCODING 212 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Otilde +ENCODING 213 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Odieresis +ENCODING 214 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR multiply +ENCODING 215 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 3 +BITMAP +A0 +40 +A0 +ENDCHAR +STARTCHAR Oslash +ENCODING 216 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +B0 +D0 +90 +90 +F0 +ENDCHAR +STARTCHAR Ugrave +ENCODING 217 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Uacute +ENCODING 218 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Ucircumflex +ENCODING 219 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Udieresis +ENCODING 220 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Yacute +ENCODING 221 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +A0 +A0 +E0 +40 +40 +ENDCHAR +STARTCHAR Thorn +ENCODING 222 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +E0 +A0 +A0 +E0 +80 +80 +ENDCHAR +STARTCHAR germandbls +ENCODING 223 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +C0 +A0 +A0 +E0 +80 +80 +ENDCHAR +STARTCHAR agrave +ENCODING 224 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +20 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR aacute +ENCODING 225 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR acircumflex +ENCODING 226 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR atilde +ENCODING 227 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR adieresis +ENCODING 228 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR aring +ENCODING 229 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR ae +ENCODING 230 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +28 +F8 +A0 +F8 +ENDCHAR +STARTCHAR ccedilla +ENCODING 231 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR egrave +ENCODING 232 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +20 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR eacute +ENCODING 233 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR ecircumflex +ENCODING 234 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR edieresis +ENCODING 235 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR igrave +ENCODING 236 +SWIDTH 375 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR iacute +ENCODING 237 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR icircumflex +ENCODING 238 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR idieresis +ENCODING 239 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR eth +ENCODING 240 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +A0 +20 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ntilde +ENCODING 241 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR ograve +ENCODING 242 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +80 +40 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR oacute +ENCODING 243 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +20 +40 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ocircumflex +ENCODING 244 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR otilde +ENCODING 245 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR odieresis +ENCODING 246 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +A0 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR divide +ENCODING 247 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +40 +00 +E0 +00 +40 +ENDCHAR +STARTCHAR oslash +ENCODING 248 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +F0 +B0 +D0 +90 +F0 +ENDCHAR +STARTCHAR ugrave +ENCODING 249 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +80 +40 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uacute +ENCODING 250 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +20 +40 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ucircumflex +ENCODING 251 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR udieresis +ENCODING 252 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR yacute +ENCODING 253 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR thorn +ENCODING 254 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +E0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR ydieresis +ENCODING 255 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Amacron +ENCODING 256 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +A0 +E0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR amacron +ENCODING 257 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR Abreve +ENCODING 258 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR abreve +ENCODING 259 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR Aogonek +ENCODING 260 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 4 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +30 +ENDCHAR +STARTCHAR aogonek +ENCODING 261 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 4 6 0 0 +BITMAP +E0 +20 +E0 +A0 +E0 +30 +ENDCHAR +STARTCHAR Cacute +ENCODING 262 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR cacute +ENCODING 263 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +80 +00 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR Ccircumflex +ENCODING 264 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR ccircumflex +ENCODING 265 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +A0 +00 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR Cdotaccent +ENCODING 266 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR cdotaccent +ENCODING 267 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR Ccaron +ENCODING 268 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR ccaron +ENCODING 269 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +40 +00 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR Dcaron +ENCODING 270 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +C0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR dcaron +ENCODING 271 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +30 +30 +20 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Dcroat +ENCODING 272 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +60 +50 +50 +F0 +50 +50 +50 +70 +ENDCHAR +STARTCHAR dcroat +ENCODING 273 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +70 +20 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Emacron +ENCODING 274 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +80 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR emacron +ENCODING 275 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR Ebreve +ENCODING 276 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR ebreve +ENCODING 277 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR Edotaccent +ENCODING 278 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +80 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR edotaccent +ENCODING 279 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR Eogonek +ENCODING 280 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 4 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +E0 +30 +ENDCHAR +STARTCHAR eogonek +ENCODING 281 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +A0 +E0 +80 +E0 +40 +ENDCHAR +STARTCHAR Ecaron +ENCODING 282 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR ecaron +ENCODING 283 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR Gcircumflex +ENCODING 284 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +B0 +90 +F0 +ENDCHAR +STARTCHAR gcircumflex +ENCODING 285 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Gbreve +ENCODING 286 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +B0 +90 +F0 +ENDCHAR +STARTCHAR gbreve +ENCODING 287 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Gdotaccent +ENCODING 288 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +00 +E0 +80 +80 +B0 +90 +F0 +ENDCHAR +STARTCHAR gdotaccent +ENCODING 289 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR uni0122 +ENCODING 290 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +80 +80 +B0 +90 +90 +F0 +40 +ENDCHAR +STARTCHAR uni0123 +ENCODING 291 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +40 +00 +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Hcircumflex +ENCODING 292 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR hcircumflex +ENCODING 293 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +80 +80 +E0 +A0 +A0 +ENDCHAR +STARTCHAR Hbar +ENCODING 294 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +50 +F8 +50 +70 +50 +50 +50 +50 +ENDCHAR +STARTCHAR hbar +ENCODING 295 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +E0 +40 +40 +70 +50 +50 +50 +ENDCHAR +STARTCHAR Itilde +ENCODING 296 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR itilde +ENCODING 297 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Imacron +ENCODING 298 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR imacron +ENCODING 299 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Ibreve +ENCODING 300 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR ibreve +ENCODING 301 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Iogonek +ENCODING 302 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +E0 +40 +ENDCHAR +STARTCHAR iogonek +ENCODING 303 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +C0 +40 +40 +E0 +20 +ENDCHAR +STARTCHAR Idotaccent +ENCODING 304 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR dotlessi +ENCODING 305 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR IJ +ENCODING 306 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +E4 +44 +44 +44 +44 +54 +54 +FC +ENDCHAR +STARTCHAR ij +ENCODING 307 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 5 7 -1 0 +BITMAP +48 +00 +C8 +48 +48 +68 +F8 +ENDCHAR +STARTCHAR Jcircumflex +ENCODING 308 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +50 +00 +20 +20 +A0 +A0 +E0 +ENDCHAR +STARTCHAR jcircumflex +ENCODING 309 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 7 0 0 +BITMAP +20 +50 +00 +20 +20 +A0 +E0 +ENDCHAR +STARTCHAR uni0136 +ENCODING 310 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +C0 +A0 +A0 +A0 +40 +ENDCHAR +STARTCHAR uni0137 +ENCODING 311 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +A0 +A0 +C0 +A0 +A0 +40 +ENDCHAR +STARTCHAR kgreenlandic +ENCODING 312 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +A0 +A0 +C0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Lacute +ENCODING 313 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR lacute +ENCODING 314 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni013B +ENCODING 315 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +80 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR uni013C +ENCODING 316 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +40 +40 +40 +40 +E0 +40 +ENDCHAR +STARTCHAR Lcaron +ENCODING 317 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +80 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR lcaron +ENCODING 318 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +D0 +50 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Ldot +ENCODING 319 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +A0 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR ldot +ENCODING 320 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +60 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Lslash +ENCODING 321 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +40 +60 +C0 +40 +40 +40 +70 +ENDCHAR +STARTCHAR lslash +ENCODING 322 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +60 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Nacute +ENCODING 323 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +40 +00 +90 +D0 +B0 +90 +90 +ENDCHAR +STARTCHAR nacute +ENCODING 324 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0145 +ENCODING 325 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +D0 +B0 +90 +90 +90 +40 +ENDCHAR +STARTCHAR uni0146 +ENCODING 326 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +40 +ENDCHAR +STARTCHAR Ncaron +ENCODING 327 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +A0 +40 +00 +90 +D0 +B0 +90 +90 +ENDCHAR +STARTCHAR ncaron +ENCODING 328 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR napostrophe +ENCODING 329 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +80 +80 +00 +70 +50 +50 +50 +50 +ENDCHAR +STARTCHAR Eng +ENCODING 330 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +20 +ENDCHAR +STARTCHAR eng +ENCODING 331 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +A0 +A0 +A0 +A0 +20 +ENDCHAR +STARTCHAR Omacron +ENCODING 332 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR omacron +ENCODING 333 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Obreve +ENCODING 334 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR obreve +ENCODING 335 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Ohungarumlaut +ENCODING 336 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ohungarumlaut +ENCODING 337 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR OE +ENCODING 338 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +A0 +A0 +B8 +A0 +A0 +A0 +F8 +ENDCHAR +STARTCHAR oe +ENCODING 339 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +A8 +B8 +A0 +F8 +ENDCHAR +STARTCHAR Racute +ENCODING 340 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +C0 +A0 +A0 +ENDCHAR +STARTCHAR racute +ENCODING 341 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0156 +ENCODING 342 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +40 +ENDCHAR +STARTCHAR uni0157 +ENCODING 343 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +80 +80 +80 +80 +40 +ENDCHAR +STARTCHAR Rcaron +ENCODING 344 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +C0 +A0 +A0 +ENDCHAR +STARTCHAR rcaron +ENCODING 345 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR Sacute +ENCODING 346 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR sacute +ENCODING 347 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR Scircumflex +ENCODING 348 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR scircumflex +ENCODING 349 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR Scedilla +ENCODING 350 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +20 +20 +E0 +40 +ENDCHAR +STARTCHAR scedilla +ENCODING 351 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +80 +E0 +20 +E0 +40 +ENDCHAR +STARTCHAR Scaron +ENCODING 352 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR scaron +ENCODING 353 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR uni0162 +ENCODING 354 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +60 +20 +ENDCHAR +STARTCHAR uni0163 +ENCODING 355 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +40 +E0 +40 +40 +40 +60 +20 +ENDCHAR +STARTCHAR Tcaron +ENCODING 356 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR tcaron +ENCODING 357 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +50 +50 +40 +E0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR Tbar +ENCODING 358 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +E0 +40 +40 +40 +40 +ENDCHAR +STARTCHAR tbar +ENCODING 359 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +40 +E0 +40 +E0 +40 +40 +60 +ENDCHAR +STARTCHAR Utilde +ENCODING 360 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR utilde +ENCODING 361 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Umacron +ENCODING 362 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR umacron +ENCODING 363 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Ubreve +ENCODING 364 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ubreve +ENCODING 365 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +40 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Uring +ENCODING 366 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uring +ENCODING 367 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Uhungarumlaut +ENCODING 368 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uhungarumlaut +ENCODING 369 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Uogonek +ENCODING 370 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +A0 +A0 +A0 +E0 +20 +ENDCHAR +STARTCHAR uogonek +ENCODING 371 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +E0 +20 +ENDCHAR +STARTCHAR Wcircumflex +ENCODING 372 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +50 +00 +A8 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR wcircumflex +ENCODING 373 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +20 +50 +00 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR Ycircumflex +ENCODING 374 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +E0 +40 +40 +ENDCHAR +STARTCHAR ycircumflex +ENCODING 375 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Ydieresis +ENCODING 376 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR Zacute +ENCODING 377 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR zacute +ENCODING 378 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR Zdotaccent +ENCODING 379 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +20 +40 +80 +80 +E0 +ENDCHAR +STARTCHAR zdotaccent +ENCODING 380 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR Zcaron +ENCODING 381 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR zcaron +ENCODING 382 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR longs +ENCODING 383 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR Alphatonos +ENCODING 902 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A8 +28 +38 +28 +28 +28 +28 +ENDCHAR +STARTCHAR Epsilontonos +ENCODING 904 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A0 +20 +38 +20 +20 +20 +38 +ENDCHAR +STARTCHAR Etatonos +ENCODING 905 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +A4 +A4 +24 +3C +24 +24 +24 +24 +ENDCHAR +STARTCHAR Iotatonos +ENCODING 906 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +90 +10 +10 +10 +10 +10 +38 +ENDCHAR +STARTCHAR Omicrontonos +ENCODING 908 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +BC +A4 +24 +24 +24 +24 +24 +3C +ENDCHAR +STARTCHAR Upsilontonos +ENCODING 910 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +28 +38 +10 +10 +10 +10 +ENDCHAR +STARTCHAR Omegatonos +ENCODING 911 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +BE +A2 +22 +22 +22 +36 +14 +36 +ENDCHAR +STARTCHAR Alpha +ENCODING 913 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Beta +ENCODING 914 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Gamma +ENCODING 915 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0394 +ENCODING 916 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +20 +50 +50 +88 +88 +88 +F8 +ENDCHAR +STARTCHAR Epsilon +ENCODING 917 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR Zeta +ENCODING 918 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +40 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR Eta +ENCODING 919 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +90 +F0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR Theta +ENCODING 920 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +F0 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR Iota +ENCODING 921 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Kappa +ENCODING 922 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +C0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Lambda +ENCODING 923 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +20 +50 +50 +50 +88 +88 +88 +ENDCHAR +STARTCHAR Mu +ENCODING 924 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +D8 +A8 +A8 +88 +88 +88 +88 +ENDCHAR +STARTCHAR Nu +ENCODING 925 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +D0 +B0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR Xi +ENCODING 926 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +00 +E0 +00 +00 +00 +E0 +ENDCHAR +STARTCHAR Omicron +ENCODING 927 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR Pi +ENCODING 928 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +90 +ENDCHAR +STARTCHAR Rho +ENCODING 929 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR Sigma +ENCODING 931 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +40 +20 +40 +80 +80 +E0 +ENDCHAR +STARTCHAR Tau +ENCODING 932 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR Upsilon +ENCODING 933 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +E0 +40 +40 +40 +40 +ENDCHAR +STARTCHAR Phi +ENCODING 934 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +F8 +A8 +A8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR Chi +ENCODING 935 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +40 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Psi +ENCODING 936 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +A8 +F8 +20 +20 +20 +20 +ENDCHAR +STARTCHAR uni03A9 +ENCODING 937 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +88 +88 +88 +88 +D8 +50 +D8 +ENDCHAR +STARTCHAR Iotadieresis +ENCODING 938 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Upsilondieresis +ENCODING 939 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR alphatonos +ENCODING 940 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +40 +00 +D0 +A0 +A0 +A0 +D0 +ENDCHAR +STARTCHAR epsilontonos +ENCODING 941 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +80 +40 +80 +E0 +ENDCHAR +STARTCHAR etatonos +ENCODING 942 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +A0 +A0 +20 +ENDCHAR +STARTCHAR iotatonos +ENCODING 943 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +C0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR alpha +ENCODING 945 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +D0 +A0 +A0 +A0 +D0 +ENDCHAR +STARTCHAR beta +ENCODING 946 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +60 +A0 +A0 +C0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR gamma +ENCODING 947 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +D0 +50 +70 +20 +20 +ENDCHAR +STARTCHAR delta +ENCODING 948 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +40 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR epsilon +ENCODING 949 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +40 +80 +E0 +ENDCHAR +STARTCHAR zeta +ENCODING 950 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +C0 +80 +80 +80 +E0 +20 +ENDCHAR +STARTCHAR eta +ENCODING 951 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +20 +ENDCHAR +STARTCHAR theta +ENCODING 952 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR iota +ENCODING 953 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +C0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR kappa +ENCODING 954 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +C0 +A0 +A0 +ENDCHAR +STARTCHAR lambda +ENCODING 955 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +60 +20 +20 +50 +50 +88 +88 +88 +ENDCHAR +STARTCHAR uni03BC +ENCODING 956 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR nu +ENCODING 957 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR xi +ENCODING 958 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +E0 +80 +60 +80 +80 +E0 +20 +ENDCHAR +STARTCHAR omicron +ENCODING 959 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR pi +ENCODING 960 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +50 +50 +50 +58 +ENDCHAR +STARTCHAR rho +ENCODING 961 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR sigma1 +ENCODING 962 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +E0 +20 +ENDCHAR +STARTCHAR sigma +ENCODING 963 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +F0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR tau +ENCODING 964 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR upsilon +ENCODING 965 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR phi +ENCODING 966 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +B8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR chi +ENCODING 967 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +40 +A0 +A0 +ENDCHAR +STARTCHAR psi +ENCODING 968 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +A8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR omega +ENCODING 969 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +88 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR iotadieresis +ENCODING 970 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +C0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR upsilondieresis +ENCODING 971 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR omicrontonos +ENCODING 972 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR upsilontonos +ENCODING 973 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR omegatonos +ENCODING 974 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +10 +20 +00 +88 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR uni0401 +ENCODING 1025 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +80 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR uni0404 +ENCODING 1028 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +70 +80 +80 +E0 +80 +80 +80 +70 +ENDCHAR +STARTCHAR uni0406 +ENCODING 1030 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni0407 +ENCODING 1031 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni040E +ENCODING 1038 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +A0 +A0 +E0 +20 +20 +60 +ENDCHAR +STARTCHAR uni0410 +ENCODING 1040 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0411 +ENCODING 1041 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni0412 +ENCODING 1042 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni0413 +ENCODING 1043 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0414 +ENCODING 1044 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +30 +50 +50 +50 +50 +50 +F8 +88 +ENDCHAR +STARTCHAR uni0415 +ENCODING 1045 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR uni0416 +ENCODING 1046 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +A8 +F8 +A8 +A8 +A8 +A8 +ENDCHAR +STARTCHAR uni0417 +ENCODING 1047 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +20 +40 +20 +20 +A0 +E0 +ENDCHAR +STARTCHAR uni0418 +ENCODING 1048 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +88 +98 +A8 +C8 +88 +88 +88 +ENDCHAR +STARTCHAR uni0419 +ENCODING 1049 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +88 +98 +A8 +C8 +88 +88 +88 +ENDCHAR +STARTCHAR uni041A +ENCODING 1050 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +C0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni041B +ENCODING 1051 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +30 +50 +50 +50 +50 +50 +50 +D0 +ENDCHAR +STARTCHAR uni041C +ENCODING 1052 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +D8 +A8 +A8 +88 +88 +88 +88 +ENDCHAR +STARTCHAR uni041D +ENCODING 1053 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +90 +F0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR uni041E +ENCODING 1054 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR uni041F +ENCODING 1055 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +90 +ENDCHAR +STARTCHAR uni0420 +ENCODING 1056 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0421 +ENCODING 1057 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +80 +80 +80 +80 +A0 +E0 +ENDCHAR +STARTCHAR uni0422 +ENCODING 1058 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni0423 +ENCODING 1059 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +90 +F0 +10 +10 +10 +70 +ENDCHAR +STARTCHAR uni0424 +ENCODING 1060 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +F8 +A8 +A8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR uni0425 +ENCODING 1061 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +40 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0426 +ENCODING 1062 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +A0 +A0 +A0 +A0 +A0 +A0 +A0 +F0 +ENDCHAR +STARTCHAR uni0427 +ENCODING 1063 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +E0 +20 +20 +20 +20 +ENDCHAR +STARTCHAR uni0428 +ENCODING 1064 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +A8 +A8 +A8 +A8 +A8 +F8 +ENDCHAR +STARTCHAR uni0429 +ENCODING 1065 +SWIDTH 1000 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +A8 +A8 +A8 +A8 +A8 +A8 +A8 +FC +ENDCHAR +STARTCHAR uni042A +ENCODING 1066 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +C0 +40 +40 +70 +50 +50 +50 +70 +ENDCHAR +STARTCHAR uni042B +ENCODING 1067 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +88 +88 +E8 +A8 +A8 +A8 +E8 +ENDCHAR +STARTCHAR uni042C +ENCODING 1068 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni042D +ENCODING 1069 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +10 +10 +70 +10 +10 +10 +E0 +ENDCHAR +STARTCHAR uni042E +ENCODING 1070 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A8 +A8 +E8 +A8 +A8 +A8 +B8 +ENDCHAR +STARTCHAR uni042F +ENCODING 1071 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +60 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0430 +ENCODING 1072 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR uni0431 +ENCODING 1073 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +60 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni0432 +ENCODING 1074 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +C0 +A0 +E0 +ENDCHAR +STARTCHAR uni0433 +ENCODING 1075 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0434 +ENCODING 1076 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +30 +50 +50 +F8 +88 +ENDCHAR +STARTCHAR uni0435 +ENCODING 1077 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR uni0436 +ENCODING 1078 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +A8 +A8 +F8 +A8 +A8 +ENDCHAR +STARTCHAR uni0437 +ENCODING 1079 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +20 +40 +20 +E0 +ENDCHAR +STARTCHAR uni0438 +ENCODING 1080 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +90 +B0 +D0 +90 +90 +ENDCHAR +STARTCHAR uni0439 +ENCODING 1081 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 7 0 0 +BITMAP +60 +00 +90 +B0 +D0 +90 +90 +ENDCHAR +STARTCHAR uni043A +ENCODING 1082 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +C0 +A0 +A0 +ENDCHAR +STARTCHAR uni043B +ENCODING 1083 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +30 +50 +50 +50 +D0 +ENDCHAR +STARTCHAR uni043C +ENCODING 1084 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +88 +D8 +A8 +88 +88 +ENDCHAR +STARTCHAR uni043D +ENCODING 1085 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR uni043E +ENCODING 1086 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni043F +ENCODING 1087 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0440 +ENCODING 1088 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +80 +80 +ENDCHAR +STARTCHAR uni0441 +ENCODING 1089 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR uni0442 +ENCODING 1090 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni0443 +ENCODING 1091 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR uni0444 +ENCODING 1092 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +20 +20 +F8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR uni0445 +ENCODING 1093 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +40 +A0 +A0 +ENDCHAR +STARTCHAR uni0446 +ENCODING 1094 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +A0 +A0 +A0 +A0 +F0 +ENDCHAR +STARTCHAR uni0447 +ENCODING 1095 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +E0 +20 +20 +ENDCHAR +STARTCHAR uni0448 +ENCODING 1096 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +A8 +A8 +A8 +A8 +F8 +ENDCHAR +STARTCHAR uni0449 +ENCODING 1097 +SWIDTH 1000 0 +DWIDTH 7 0 +BBX 6 5 0 0 +BITMAP +A8 +A8 +A8 +A8 +FC +ENDCHAR +STARTCHAR uni044A +ENCODING 1098 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +C0 +40 +70 +50 +70 +ENDCHAR +STARTCHAR uni044B +ENCODING 1099 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +88 +88 +E8 +A8 +E8 +ENDCHAR +STARTCHAR uni044C +ENCODING 1100 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +80 +80 +E0 +A0 +E0 +ENDCHAR +STARTCHAR uni044D +ENCODING 1101 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +C0 +20 +E0 +20 +C0 +ENDCHAR +STARTCHAR uni044E +ENCODING 1102 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +B8 +A8 +E8 +A8 +B8 +ENDCHAR +STARTCHAR uni044F +ENCODING 1103 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +60 +A0 +A0 +ENDCHAR +STARTCHAR uni0451 +ENCODING 1105 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR uni0454 +ENCODING 1108 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +60 +80 +E0 +80 +60 +ENDCHAR +STARTCHAR uni0456 +ENCODING 1110 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni0457 +ENCODING 1111 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni045E +ENCODING 1118 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR uni0490 +ENCODING 1168 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +10 +E0 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0491 +ENCODING 1169 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +20 +C0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni2002 +ENCODING 8194 +SWIDTH 375 0 +DWIDTH 3 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2003 +ENCODING 8195 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2009 +ENCODING 8201 +SWIDTH 1000 0 +DWIDTH 1 0 +BBX 1 1 6 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2010 +ENCODING 8208 +SWIDTH 1000 0 +DWIDTH 1 0 +BBX 1 1 0 4 +BITMAP +80 +ENDCHAR +STARTCHAR endash +ENCODING 8211 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 1 0 4 +BITMAP +C0 +ENDCHAR +STARTCHAR emdash +ENCODING 8212 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 1 0 4 +BITMAP +F0 +ENDCHAR +STARTCHAR uni2015 +ENCODING 8213 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 4 1 0 4 +BITMAP +F0 +ENDCHAR +STARTCHAR bullet +ENCODING 8226 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 1 0 4 +BITMAP +80 +ENDCHAR +STARTCHAR colonmonetary +ENCODING 8353 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +10 +F0 +A0 +A0 +A0 +A0 +F0 +40 +ENDCHAR +STARTCHAR uni20A2 +ENCODING 8354 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +80 +80 +B0 +A0 +A0 +A0 +F0 +ENDCHAR +STARTCHAR uni20A6 +ENCODING 8358 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +48 +48 +EC +58 +CC +48 +48 +48 +ENDCHAR +STARTCHAR uni20A9 +ENCODING 8361 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +54 +54 +FE +54 +54 +54 +7C +28 +ENDCHAR +STARTCHAR uni20AA +ENCODING 8362 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 6 0 0 +BITMAP +F4 +94 +B4 +B4 +A4 +BC +ENDCHAR +STARTCHAR dong +ENCODING 8363 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +70 +20 +E0 +A0 +E0 +00 +E0 +ENDCHAR +STARTCHAR Euro +ENCODING 8364 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +30 +60 +40 +F0 +40 +40 +60 +30 +ENDCHAR +STARTCHAR uni20AD +ENCODING 8365 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +50 +50 +60 +F0 +60 +50 +50 +50 +ENDCHAR +STARTCHAR uni20AE +ENCODING 8366 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +60 +40 +C0 +40 +40 +40 +ENDCHAR +STARTCHAR uni20B1 +ENCODING 8369 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +70 +D8 +50 +70 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni20B2 +ENCODING 8370 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +E0 +80 +B0 +90 +90 +F0 +40 +ENDCHAR +STARTCHAR uni20B4 +ENCODING 8372 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +70 +50 +10 +F8 +40 +50 +50 +70 +ENDCHAR +STARTCHAR uni20B5 +ENCODING 8373 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +E0 +80 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR uni20B8 +ENCODING 8376 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni20B9 +ENCODING 8377 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +20 +F0 +80 +40 +20 +10 +10 +ENDCHAR +STARTCHAR uni20BA +ENCODING 8378 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +40 +E0 +40 +40 +50 +50 +70 +ENDCHAR +STARTCHAR uni20BC +ENCODING 8380 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +20 +20 +F8 +A8 +A8 +A8 +A8 +ENDCHAR +STARTCHAR uni20BD +ENCODING 8381 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +70 +50 +70 +40 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR uni20BE +ENCODING 8382 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +F8 +A8 +A8 +A0 +80 +40 +F8 +ENDCHAR +STARTCHAR uni20BF +ENCODING 8383 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +E0 +A0 +C0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR uni20C0 +ENCODING 8384 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +80 +80 +E0 +00 +E0 +ENDCHAR +STARTCHAR uni2103 +ENCODING 8451 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A8 +20 +20 +20 +20 +28 +38 +ENDCHAR +STARTCHAR uni2109 +ENCODING 8457 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A0 +20 +38 +20 +20 +20 +20 +ENDCHAR +STARTCHAR uni2460 +ENCODING 9312 +SWIDTH 125 0 +DWIDTH 1 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2461 +ENCODING 9313 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 1 1 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2462 +ENCODING 9314 +SWIDTH 375 0 +DWIDTH 3 0 +BBX 1 1 3 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2463 +ENCODING 9315 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2464 +ENCODING 9316 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2465 +ENCODING 9317 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 1 1 1 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2466 +ENCODING 9318 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 1 1 3 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2467 +ENCODING 9319 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2468 +ENCODING 9320 +SWIDTH 1125 0 +DWIDTH 9 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2469 +ENCODING 9321 +SWIDTH 1250 0 +DWIDTH 10 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni24EA +ENCODING 9450 +SWIDTH 0 0 +DWIDTH 0 0 +BBX 1 1 3 0 +BITMAP +00 +ENDCHAR +STARTCHAR H22073 +ENCODING 9633 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 1 +BITMAP +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni4E00 +ENCODING 19968 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 2 0 3 +BITMAP +04 +FE +ENDCHAR +STARTCHAR uni4E03 +ENCODING 19971 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +20 +20 +2E +F0 +20 +20 +22 +1E +ENDCHAR +STARTCHAR uni4E09 +ENCODING 19977 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +7C +00 +00 +38 +00 +00 +04 +FE +ENDCHAR +STARTCHAR uni4E0A +ENCODING 19978 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +40 +70 +40 +40 +40 +40 +F0 +ENDCHAR +STARTCHAR uni4E0B +ENCODING 19979 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +40 +60 +50 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni4E5D +ENCODING 20061 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +20 +F8 +28 +28 +48 +48 +4A +8E +ENDCHAR +STARTCHAR uni4E8C +ENCODING 20108 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 6 0 1 +BITMAP +7C +00 +00 +00 +04 +FE +ENDCHAR +STARTCHAR uni4E94 +ENCODING 20116 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +7C +10 +10 +7C +24 +24 +24 +FE +ENDCHAR +STARTCHAR uni516B +ENCODING 20843 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +28 +28 +28 +28 +44 +44 +44 +82 +ENDCHAR +STARTCHAR uni516D +ENCODING 20845 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +10 +FE +00 +28 +28 +44 +44 +82 +ENDCHAR +STARTCHAR uni5341 +ENCODING 21313 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +10 +10 +FE +10 +10 +10 +10 +10 +ENDCHAR +STARTCHAR uni5348 +ENCODING 21320 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 5 8 0 0 +BITMAP +40 +70 +A0 +20 +F8 +20 +20 +20 +ENDCHAR +STARTCHAR uni56DB +ENCODING 22235 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +FE +AA +AA +AE +C2 +82 +FE +82 +ENDCHAR +STARTCHAR uni5929 +ENCODING 22825 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +7C +10 +10 +FE +10 +10 +28 +C6 +ENDCHAR +STARTCHAR uni661F +ENCODING 26143 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +FC +84 +FC +5E +90 +7C +10 +FE +ENDCHAR +STARTCHAR uni6708 +ENCODING 26376 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +3E +22 +3E +22 +3E +22 +42 +86 +ENDCHAR +STARTCHAR uni671F +ENCODING 26399 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +5E +FA +5E +7A +5E +EA +12 +A6 +ENDCHAR +STARTCHAR uniAE08 +ENCODING 44552 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +08 +00 +F8 +00 +F8 +88 +F8 +ENDCHAR +STARTCHAR uniBAA9 +ENCODING 47785 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +88 +F8 +20 +F8 +00 +F8 +08 +ENDCHAR +STARTCHAR uniC218 +ENCODING 49688 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +20 +50 +88 +00 +F8 +20 +20 +ENDCHAR +STARTCHAR uniC624 +ENCODING 50724 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 7 0 1 +BITMAP +70 +88 +88 +70 +20 +20 +F8 +ENDCHAR +STARTCHAR uniC694 +ENCODING 50836 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 7 0 1 +BITMAP +70 +88 +88 +70 +50 +50 +F8 +ENDCHAR +STARTCHAR uniC6D4 +ENCODING 50900 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +48 +A8 +48 +E8 +58 +38 +C0 +F8 +ENDCHAR +STARTCHAR uniC77C +ENCODING 51068 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +48 +A8 +48 +00 +F8 +38 +C0 +F8 +ENDCHAR +STARTCHAR uniC804 +ENCODING 51204 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +F4 +24 +4C +A4 +94 +44 +40 +7C +ENDCHAR +STARTCHAR uniD1A0 +ENCODING 53664 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 7 0 1 +BITMAP +F8 +80 +F0 +80 +F8 +20 +F8 +ENDCHAR +STARTCHAR uniD654 +ENCODING 54868 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +24 +FC +54 +56 +24 +24 +FC +04 +ENDCHAR +STARTCHAR uniD6C4 +ENCODING 54980 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +F8 +50 +20 +00 +F8 +20 +20 +ENDCHAR +STARTCHAR uniFFE5 +ENCODING 65509 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +E0 +40 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR uniFFFD +ENCODING 65533 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 1 +BITMAP +A0 +A0 +40 +A0 +A0 +A0 +ENDCHAR +ENDFONT diff --git a/tests/components/font/Monocraft.ttf b/tests/components/font/Monocraft.ttf new file mode 100644 index 0000000000..4066b0a988 Binary files /dev/null and b/tests/components/font/Monocraft.ttf differ diff --git a/tests/components/font/common.yaml b/tests/components/font/common.yaml new file mode 100644 index 0000000000..5be9faf5be --- /dev/null +++ b/tests/components/font/common.yaml @@ -0,0 +1,62 @@ +font: + - file: + type: gfonts + family: "Roboto" + weight: bold + italic: true + size: 32 + id: roboto32 + + - file: "gfonts://Roboto" + id: roboto + size: 20 + glyphs: "0123456789." + extras: + - file: "gfonts://Roboto" + glyphs: ["\u00C4", "\u00C5", "\U000000C7"] + - file: "gfonts://Roboto" + id: roboto_web + size: 20 + - file: "gfonts://Roboto" + id: roboto_greek + size: 20 + glyphs: ["\u0300", "\u00C5", "\U000000C7"] + - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + id: monocraft + size: 20 + - file: + type: web + url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + id: monocraft2 + size: 24 + - file: $component_dir/Monocraft.ttf + id: monocraft3 + size: 28 + - file: $component_dir/MatrixChunky8X.bdf + id: special_font + glyphs: + - '"' + - "'" + - '#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz°' + + - file: $component_dir/MatrixChunky8X.bdf + id: default_font + - file: $component_dir/x11.pcf + id: pcf_font + +i2c: + scl: ${i2c_scl} + sda: ${i2c_sda} + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: ${display_reset_pin} + lambda: |- + it.print(0, 0, id(roboto), "Hello, World!"); + it.print(0, 20, id(roboto_web), "Hello, World!"); + it.print(0, 40, id(monocraft), "Hello, World!"); + it.print(0, 60, id(monocraft2), "Hello, World!"); + it.print(0, 80, id(monocraft3), "Hello, World!"); + it.print(0, 100, id(roboto_greek), "Hello κόσμε!"); diff --git a/tests/components/font/test.esp32-ard.yaml b/tests/components/font/test.esp32-ard.yaml new file mode 100644 index 0000000000..d98600a51b --- /dev/null +++ b/tests/components/font/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + i2c_scl: GPIO16 + i2c_sda: GPIO17 + display_reset_pin: GPIO13 + +packages: + common: !include common.yaml diff --git a/tests/components/font/test.esp32-c3-ard.yaml b/tests/components/font/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ad14a2e9a6 --- /dev/null +++ b/tests/components/font/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + display_reset_pin: GPIO3 + +packages: + common: !include common.yaml diff --git a/tests/components/font/test.esp32-c3-idf.yaml b/tests/components/font/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ad14a2e9a6 --- /dev/null +++ b/tests/components/font/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + display_reset_pin: GPIO3 + +packages: + common: !include common.yaml diff --git a/tests/components/font/test.esp32-idf.yaml b/tests/components/font/test.esp32-idf.yaml new file mode 100644 index 0000000000..d98600a51b --- /dev/null +++ b/tests/components/font/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + i2c_scl: GPIO16 + i2c_sda: GPIO17 + display_reset_pin: GPIO13 + +packages: + common: !include common.yaml diff --git a/tests/components/font/test.esp8266-ard.yaml b/tests/components/font/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ad14a2e9a6 --- /dev/null +++ b/tests/components/font/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + display_reset_pin: GPIO3 + +packages: + common: !include common.yaml diff --git a/tests/components/font/test.host.yaml b/tests/components/font/test.host.yaml new file mode 100644 index 0000000000..c5399f2826 --- /dev/null +++ b/tests/components/font/test.host.yaml @@ -0,0 +1,57 @@ +font: + - file: + type: gfonts + family: "Roboto" + weight: bold + italic: true + size: 32 + id: roboto32 + + - file: "gfonts://Roboto" + id: roboto + size: 20 + glyphs: "0123456789." + extras: + - file: "gfonts://Roboto" + glyphs: ["\u00C4", "\u00C5", "\U000000C7"] + - file: "gfonts://Roboto" + id: roboto_web + size: 20 + - file: "gfonts://Roboto" + id: roboto_greek + size: 20 + glyphs: ["\u0300", "\u00C5", "\U000000C7"] + - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + id: monocraft + size: 20 + - file: + type: web + url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + id: monocraft2 + size: 24 + - file: $component_dir/Monocraft.ttf + id: monocraft3 + size: 28 + - file: $component_dir/MatrixChunky8X.bdf + id: special_font + glyphs: + - '"' + - "'" + - '#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz°' + + - file: $component_dir/MatrixChunky8X.bdf + id: default_font + +display: + - platform: sdl + id: sdl_display + dimensions: + width: 800 + height: 600 + lambda: |- + it.print(0, 0, id(roboto), "Hello, World!"); + it.print(0, 20, id(roboto_web), "Hello, World!"); + it.print(0, 40, id(roboto_greek), "Hello κόσμε!"); + it.print(0, 60, id(monocraft), "Hello, World!"); + it.print(0, 80, id(monocraft2), "Hello, World!"); + it.print(0, 100, id(monocraft3), "Hello, World!"); diff --git a/tests/components/font/test.rp2040-ard.yaml b/tests/components/font/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ad14a2e9a6 --- /dev/null +++ b/tests/components/font/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + display_reset_pin: GPIO3 + +packages: + common: !include common.yaml diff --git a/tests/components/font/x11.pcf b/tests/components/font/x11.pcf new file mode 100644 index 0000000000..19a38d4e39 Binary files /dev/null and b/tests/components/font/x11.pcf differ diff --git a/tests/components/fs3000/test.esp32-ard.yaml b/tests/components/fs3000/test.esp32-ard.yaml new file mode 100644 index 0000000000..53b49cc9a2 --- /dev/null +++ b/tests/components/fs3000/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_fs3000 + scl: 16 + sda: 17 + +sensor: + - platform: fs3000 + name: Air Velocity + model: 1005 + update_interval: 60s diff --git a/tests/components/fs3000/test.esp32-c3-ard.yaml b/tests/components/fs3000/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..69de83b463 --- /dev/null +++ b/tests/components/fs3000/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_fs3000 + scl: 5 + sda: 4 + +sensor: + - platform: fs3000 + name: Air Velocity + model: 1005 + update_interval: 60s diff --git a/tests/components/fs3000/test.esp32-c3-idf.yaml b/tests/components/fs3000/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..69de83b463 --- /dev/null +++ b/tests/components/fs3000/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_fs3000 + scl: 5 + sda: 4 + +sensor: + - platform: fs3000 + name: Air Velocity + model: 1005 + update_interval: 60s diff --git a/tests/components/fs3000/test.esp32-idf.yaml b/tests/components/fs3000/test.esp32-idf.yaml new file mode 100644 index 0000000000..53b49cc9a2 --- /dev/null +++ b/tests/components/fs3000/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_fs3000 + scl: 16 + sda: 17 + +sensor: + - platform: fs3000 + name: Air Velocity + model: 1005 + update_interval: 60s diff --git a/tests/components/fs3000/test.esp8266-ard.yaml b/tests/components/fs3000/test.esp8266-ard.yaml new file mode 100644 index 0000000000..69de83b463 --- /dev/null +++ b/tests/components/fs3000/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_fs3000 + scl: 5 + sda: 4 + +sensor: + - platform: fs3000 + name: Air Velocity + model: 1005 + update_interval: 60s diff --git a/tests/components/fs3000/test.rp2040-ard.yaml b/tests/components/fs3000/test.rp2040-ard.yaml new file mode 100644 index 0000000000..69de83b463 --- /dev/null +++ b/tests/components/fs3000/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_fs3000 + scl: 5 + sda: 4 + +sensor: + - platform: fs3000 + name: Air Velocity + model: 1005 + update_interval: 60s diff --git a/tests/components/ft5x06/test.esp32-ard.yaml b/tests/components/ft5x06/test.esp32-ard.yaml new file mode 100644 index 0000000000..648929896d --- /dev/null +++ b/tests/components/ft5x06/test.esp32-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft5x06 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 18 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft5x06 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft5x06/test.esp32-c3-ard.yaml b/tests/components/ft5x06/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4fa9532f53 --- /dev/null +++ b/tests/components/ft5x06/test.esp32-c3-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft5x06 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft5x06 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft5x06/test.esp32-c3-idf.yaml b/tests/components/ft5x06/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4fa9532f53 --- /dev/null +++ b/tests/components/ft5x06/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft5x06 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft5x06 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft5x06/test.esp32-idf.yaml b/tests/components/ft5x06/test.esp32-idf.yaml new file mode 100644 index 0000000000..648929896d --- /dev/null +++ b/tests/components/ft5x06/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft5x06 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 18 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft5x06 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft5x06/test.esp8266-ard.yaml b/tests/components/ft5x06/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4fa9532f53 --- /dev/null +++ b/tests/components/ft5x06/test.esp8266-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft5x06 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft5x06 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft5x06/test.rp2040-ard.yaml b/tests/components/ft5x06/test.rp2040-ard.yaml new file mode 100644 index 0000000000..4fa9532f53 --- /dev/null +++ b/tests/components/ft5x06/test.rp2040-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft5x06 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft5x06 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft63x6/test.esp32-ard.yaml b/tests/components/ft63x6/test.esp32-ard.yaml new file mode 100644 index 0000000000..5c43cdff45 --- /dev/null +++ b/tests/components/ft63x6/test.esp32-ard.yaml @@ -0,0 +1,39 @@ +spi: + clk_pin: 14 + mosi_pin: 13 + +i2c: + sda: GPIO18 + scl: GPIO19 + +display: + - id: my_display + platform: ili9xxx + dimensions: 480x320 + model: ST7796 + cs_pin: 15 + dc_pin: 21 + reset_pin: 22 + transform: + swap_xy: true + mirror_x: true + mirror_y: true + auto_clear_enabled: false + invert_colors: false + +touchscreen: + - platform: ft63x6 + interrupt_pin: GPIO39 + transform: + swap_xy: true + mirror_x: false + mirror_y: true + on_touch: + - logger.log: + format: tp touched + on_update: + - logger.log: + format: to updated + on_release: + - logger.log: + format: to released diff --git a/tests/components/ft63x6/test.esp32-c3-ard.yaml b/tests/components/ft63x6/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..19ca4cfc19 --- /dev/null +++ b/tests/components/ft63x6/test.esp32-c3-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft63x6 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft63x6 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft63x6/test.esp32-c3-idf.yaml b/tests/components/ft63x6/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..19ca4cfc19 --- /dev/null +++ b/tests/components/ft63x6/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft63x6 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft63x6 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft63x6/test.esp32-idf.yaml b/tests/components/ft63x6/test.esp32-idf.yaml new file mode 100644 index 0000000000..5ceb107e31 --- /dev/null +++ b/tests/components/ft63x6/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft63x6 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 18 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft63x6 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft63x6/test.esp8266-ard.yaml b/tests/components/ft63x6/test.esp8266-ard.yaml new file mode 100644 index 0000000000..19ca4cfc19 --- /dev/null +++ b/tests/components/ft63x6/test.esp8266-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft63x6 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft63x6 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/ft63x6/test.rp2040-ard.yaml b/tests/components/ft63x6/test.rp2040-ard.yaml new file mode 100644 index 0000000000..19ca4cfc19 --- /dev/null +++ b/tests/components/ft63x6/test.rp2040-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_ft63x6 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: ft63x6 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/fujitsu_general/test.esp32-ard.yaml b/tests/components/fujitsu_general/test.esp32-ard.yaml new file mode 100644 index 0000000000..b4146f2a18 --- /dev/null +++ b/tests/components/fujitsu_general/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: fujitsu_general + name: Fujitsu General Climate diff --git a/tests/components/fujitsu_general/test.esp32-c3-ard.yaml b/tests/components/fujitsu_general/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b4146f2a18 --- /dev/null +++ b/tests/components/fujitsu_general/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: fujitsu_general + name: Fujitsu General Climate diff --git a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml b/tests/components/fujitsu_general/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b4146f2a18 --- /dev/null +++ b/tests/components/fujitsu_general/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: fujitsu_general + name: Fujitsu General Climate diff --git a/tests/components/fujitsu_general/test.esp32-idf.yaml b/tests/components/fujitsu_general/test.esp32-idf.yaml new file mode 100644 index 0000000000..b4146f2a18 --- /dev/null +++ b/tests/components/fujitsu_general/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: fujitsu_general + name: Fujitsu General Climate diff --git a/tests/components/fujitsu_general/test.esp8266-ard.yaml b/tests/components/fujitsu_general/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2a05bdde6b --- /dev/null +++ b/tests/components/fujitsu_general/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: fujitsu_general + name: Fujitsu General Climate diff --git a/tests/components/gcja5/test.esp32-ard.yaml b/tests/components/gcja5/test.esp32-ard.yaml new file mode 100644 index 0000000000..bc0f89eb9e --- /dev/null +++ b/tests/components/gcja5/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +uart: + - id: uart_gcja5 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + parity: EVEN + +sensor: + - platform: gcja5 + pm_1_0: + name: "Particulate Matter <1.0µm Concentration" + pm_2_5: + name: "Particulate Matter <2.5µm Concentration" + pm_10_0: + name: "Particulate Matter <10.0µm Concentration" + pmc_0_5: + name: "PMC 0.5" + pmc_1_0: + name: "PMC 1.0" + pmc_2_5: + name: "PMC 2.5" + pmc_5_0: + name: "PMC 5.0" + pmc_10_0: + name: "PMC 10.0" diff --git a/tests/components/gcja5/test.esp32-c3-ard.yaml b/tests/components/gcja5/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ec8765be52 --- /dev/null +++ b/tests/components/gcja5/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +uart: + - id: uart_gcja5 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + parity: EVEN + +sensor: + - platform: gcja5 + pm_1_0: + name: "Particulate Matter <1.0µm Concentration" + pm_2_5: + name: "Particulate Matter <2.5µm Concentration" + pm_10_0: + name: "Particulate Matter <10.0µm Concentration" + pmc_0_5: + name: "PMC 0.5" + pmc_1_0: + name: "PMC 1.0" + pmc_2_5: + name: "PMC 2.5" + pmc_5_0: + name: "PMC 5.0" + pmc_10_0: + name: "PMC 10.0" diff --git a/tests/components/gcja5/test.esp32-c3-idf.yaml b/tests/components/gcja5/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ec8765be52 --- /dev/null +++ b/tests/components/gcja5/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +uart: + - id: uart_gcja5 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + parity: EVEN + +sensor: + - platform: gcja5 + pm_1_0: + name: "Particulate Matter <1.0µm Concentration" + pm_2_5: + name: "Particulate Matter <2.5µm Concentration" + pm_10_0: + name: "Particulate Matter <10.0µm Concentration" + pmc_0_5: + name: "PMC 0.5" + pmc_1_0: + name: "PMC 1.0" + pmc_2_5: + name: "PMC 2.5" + pmc_5_0: + name: "PMC 5.0" + pmc_10_0: + name: "PMC 10.0" diff --git a/tests/components/gcja5/test.esp32-idf.yaml b/tests/components/gcja5/test.esp32-idf.yaml new file mode 100644 index 0000000000..bc0f89eb9e --- /dev/null +++ b/tests/components/gcja5/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +uart: + - id: uart_gcja5 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + parity: EVEN + +sensor: + - platform: gcja5 + pm_1_0: + name: "Particulate Matter <1.0µm Concentration" + pm_2_5: + name: "Particulate Matter <2.5µm Concentration" + pm_10_0: + name: "Particulate Matter <10.0µm Concentration" + pmc_0_5: + name: "PMC 0.5" + pmc_1_0: + name: "PMC 1.0" + pmc_2_5: + name: "PMC 2.5" + pmc_5_0: + name: "PMC 5.0" + pmc_10_0: + name: "PMC 10.0" diff --git a/tests/components/gcja5/test.esp8266-ard.yaml b/tests/components/gcja5/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ec8765be52 --- /dev/null +++ b/tests/components/gcja5/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +uart: + - id: uart_gcja5 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + parity: EVEN + +sensor: + - platform: gcja5 + pm_1_0: + name: "Particulate Matter <1.0µm Concentration" + pm_2_5: + name: "Particulate Matter <2.5µm Concentration" + pm_10_0: + name: "Particulate Matter <10.0µm Concentration" + pmc_0_5: + name: "PMC 0.5" + pmc_1_0: + name: "PMC 1.0" + pmc_2_5: + name: "PMC 2.5" + pmc_5_0: + name: "PMC 5.0" + pmc_10_0: + name: "PMC 10.0" diff --git a/tests/components/gcja5/test.rp2040-ard.yaml b/tests/components/gcja5/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ec8765be52 --- /dev/null +++ b/tests/components/gcja5/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +uart: + - id: uart_gcja5 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + parity: EVEN + +sensor: + - platform: gcja5 + pm_1_0: + name: "Particulate Matter <1.0µm Concentration" + pm_2_5: + name: "Particulate Matter <2.5µm Concentration" + pm_10_0: + name: "Particulate Matter <10.0µm Concentration" + pmc_0_5: + name: "PMC 0.5" + pmc_1_0: + name: "PMC 1.0" + pmc_2_5: + name: "PMC 2.5" + pmc_5_0: + name: "PMC 5.0" + pmc_10_0: + name: "PMC 10.0" diff --git a/tests/components/gdk101/common.yaml b/tests/components/gdk101/common.yaml new file mode 100644 index 0000000000..f886fc415b --- /dev/null +++ b/tests/components/gdk101/common.yaml @@ -0,0 +1,28 @@ +i2c: + id: i2c_bus + sda: ${i2c_sda} + scl: ${i2c_scl} + +gdk101: + id: my_gdk101 + i2c_id: i2c_bus + +sensor: + - platform: gdk101 + gdk101_id: my_gdk101 + radiation_dose_per_1m: + name: Radiation Dose @ 1 min + radiation_dose_per_10m: + name: Radiation Dose @ 10 min + status: + name: Status + version: + name: FW Version + measurement_duration: + name: Measuring Time + +binary_sensor: + - platform: gdk101 + gdk101_id: my_gdk101 + vibrations: + name: Vibrations diff --git a/tests/components/gdk101/test.esp32-ard.yaml b/tests/components/gdk101/test.esp32-ard.yaml new file mode 100644 index 0000000000..1037d5d35b --- /dev/null +++ b/tests/components/gdk101/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO16 + i2c_sda: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/gdk101/test.esp32-idf.yaml b/tests/components/gdk101/test.esp32-idf.yaml new file mode 100644 index 0000000000..1037d5d35b --- /dev/null +++ b/tests/components/gdk101/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO16 + i2c_sda: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/gdk101/test.esp8266-ard.yaml b/tests/components/gdk101/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d7ae0d5161 --- /dev/null +++ b/tests/components/gdk101/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/gdk101/test.rp2040-ard.yaml b/tests/components/gdk101/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d7ae0d5161 --- /dev/null +++ b/tests/components/gdk101/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/globals/common.yaml b/tests/components/globals/common.yaml new file mode 100644 index 0000000000..224a91a270 --- /dev/null +++ b/tests/components/globals/common.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - globals.set: + id: glob_int + value: "10" + +globals: + - id: glob_int + type: int + restore_value: true + initial_value: "0" + - id: glob_float + type: float + restore_value: true + initial_value: "0.0f" + - id: glob_bool + type: bool + restore_value: false + initial_value: "true" + - id: glob_string + type: std::string + restore_value: false + # initial_value: "" + - id: glob_bool_processed + type: bool + restore_value: false + initial_value: "false" diff --git a/tests/components/globals/test.esp32-ard.yaml b/tests/components/globals/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/globals/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/globals/test.esp32-c3-ard.yaml b/tests/components/globals/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/globals/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/globals/test.esp32-c3-idf.yaml b/tests/components/globals/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/globals/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/globals/test.esp32-idf.yaml b/tests/components/globals/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/globals/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/globals/test.esp8266-ard.yaml b/tests/components/globals/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/globals/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/globals/test.rp2040-ard.yaml b/tests/components/globals/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/globals/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/gp2y1010au0f/test.esp32-idf.yaml b/tests/components/gp2y1010au0f/test.esp32-idf.yaml new file mode 100644 index 0000000000..eb5ad0ea67 --- /dev/null +++ b/tests/components/gp2y1010au0f/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +sensor: + - platform: adc + pin: GPIO36 + id: adc_sensor + + - platform: gp2y1010au0f + sensor: adc_sensor + name: Dust Sensor + adc_voltage_offset: 0.2 + adc_voltage_multiplier: 3.3 + output: dust_sensor_led + +output: + - platform: gpio + id: dust_sensor_led + pin: GPIO32 diff --git a/tests/components/gp8403/test.esp32-ard.yaml b/tests/components/gp8403/test.esp32-ard.yaml new file mode 100644 index 0000000000..8470a303e1 --- /dev/null +++ b/tests/components/gp8403/test.esp32-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_gp8403 + scl: 16 + sda: 17 + +gp8403: + - id: gp8403_5v + voltage: 5V + - id: gp8403_10v + voltage: 10V + +output: + - platform: gp8403 + id: gp8403_output_0 + gp8403_id: gp8403_5v + channel: 0 + - platform: gp8403 + gp8403_id: gp8403_10v + id: gp8403_output_1 + channel: 1 diff --git a/tests/components/gp8403/test.esp32-c3-ard.yaml b/tests/components/gp8403/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fbc40b948b --- /dev/null +++ b/tests/components/gp8403/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_gp8403 + scl: 5 + sda: 4 + +gp8403: + - id: gp8403_5v + voltage: 5V + - id: gp8403_10v + voltage: 10V + +output: + - platform: gp8403 + id: gp8403_output_0 + gp8403_id: gp8403_5v + channel: 0 + - platform: gp8403 + gp8403_id: gp8403_10v + id: gp8403_output_1 + channel: 1 diff --git a/tests/components/gp8403/test.esp32-c3-idf.yaml b/tests/components/gp8403/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fbc40b948b --- /dev/null +++ b/tests/components/gp8403/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_gp8403 + scl: 5 + sda: 4 + +gp8403: + - id: gp8403_5v + voltage: 5V + - id: gp8403_10v + voltage: 10V + +output: + - platform: gp8403 + id: gp8403_output_0 + gp8403_id: gp8403_5v + channel: 0 + - platform: gp8403 + gp8403_id: gp8403_10v + id: gp8403_output_1 + channel: 1 diff --git a/tests/components/gp8403/test.esp32-idf.yaml b/tests/components/gp8403/test.esp32-idf.yaml new file mode 100644 index 0000000000..8470a303e1 --- /dev/null +++ b/tests/components/gp8403/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_gp8403 + scl: 16 + sda: 17 + +gp8403: + - id: gp8403_5v + voltage: 5V + - id: gp8403_10v + voltage: 10V + +output: + - platform: gp8403 + id: gp8403_output_0 + gp8403_id: gp8403_5v + channel: 0 + - platform: gp8403 + gp8403_id: gp8403_10v + id: gp8403_output_1 + channel: 1 diff --git a/tests/components/gp8403/test.esp8266-ard.yaml b/tests/components/gp8403/test.esp8266-ard.yaml new file mode 100644 index 0000000000..fbc40b948b --- /dev/null +++ b/tests/components/gp8403/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_gp8403 + scl: 5 + sda: 4 + +gp8403: + - id: gp8403_5v + voltage: 5V + - id: gp8403_10v + voltage: 10V + +output: + - platform: gp8403 + id: gp8403_output_0 + gp8403_id: gp8403_5v + channel: 0 + - platform: gp8403 + gp8403_id: gp8403_10v + id: gp8403_output_1 + channel: 1 diff --git a/tests/components/gp8403/test.rp2040-ard.yaml b/tests/components/gp8403/test.rp2040-ard.yaml new file mode 100644 index 0000000000..fbc40b948b --- /dev/null +++ b/tests/components/gp8403/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_gp8403 + scl: 5 + sda: 4 + +gp8403: + - id: gp8403_5v + voltage: 5V + - id: gp8403_10v + voltage: 10V + +output: + - platform: gp8403 + id: gp8403_output_0 + gp8403_id: gp8403_5v + channel: 0 + - platform: gp8403 + gp8403_id: gp8403_10v + id: gp8403_output_1 + channel: 1 diff --git a/tests/components/gpio/test.esp32-ard.yaml b/tests/components/gpio/test.esp32-ard.yaml new file mode 100644 index 0000000000..30dfa94b68 --- /dev/null +++ b/tests/components/gpio/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 12 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 13 + id: gpio_output + +switch: + - platform: gpio + pin: 14 + id: gpio_switch diff --git a/tests/components/gpio/test.esp32-c3-ard.yaml b/tests/components/gpio/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..3ca285117d --- /dev/null +++ b/tests/components/gpio/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 2 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 3 + id: gpio_output + +switch: + - platform: gpio + pin: 4 + id: gpio_switch diff --git a/tests/components/gpio/test.esp32-c3-idf.yaml b/tests/components/gpio/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3ca285117d --- /dev/null +++ b/tests/components/gpio/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 2 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 3 + id: gpio_output + +switch: + - platform: gpio + pin: 4 + id: gpio_switch diff --git a/tests/components/gpio/test.esp32-idf.yaml b/tests/components/gpio/test.esp32-idf.yaml new file mode 100644 index 0000000000..30dfa94b68 --- /dev/null +++ b/tests/components/gpio/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 12 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 13 + id: gpio_output + +switch: + - platform: gpio + pin: 14 + id: gpio_switch diff --git a/tests/components/gpio/test.esp8266-ard.yaml b/tests/components/gpio/test.esp8266-ard.yaml new file mode 100644 index 0000000000..30dfa94b68 --- /dev/null +++ b/tests/components/gpio/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 12 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 13 + id: gpio_output + +switch: + - platform: gpio + pin: 14 + id: gpio_switch diff --git a/tests/components/gpio/test.rp2040-ard.yaml b/tests/components/gpio/test.rp2040-ard.yaml new file mode 100644 index 0000000000..3ca285117d --- /dev/null +++ b/tests/components/gpio/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 2 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 3 + id: gpio_output + +switch: + - platform: gpio + pin: 4 + id: gpio_switch diff --git a/tests/components/gps/test.esp32-ard.yaml b/tests/components/gps/test.esp32-ard.yaml new file mode 100644 index 0000000000..c4e4cf9f6f --- /dev/null +++ b/tests/components/gps/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_gps + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + parity: EVEN + +gps: + +time: + - platform: gps + on_time_sync: + then: + logger.log: "It's time!" diff --git a/tests/components/gps/test.esp32-c3-ard.yaml b/tests/components/gps/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..031f45b873 --- /dev/null +++ b/tests/components/gps/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_gps + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + parity: EVEN + +gps: + +time: + - platform: gps + on_time_sync: + then: + logger.log: "It's time!" diff --git a/tests/components/gps/test.esp8266-ard.yaml b/tests/components/gps/test.esp8266-ard.yaml new file mode 100644 index 0000000000..031f45b873 --- /dev/null +++ b/tests/components/gps/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_gps + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + parity: EVEN + +gps: + +time: + - platform: gps + on_time_sync: + then: + logger.log: "It's time!" diff --git a/tests/components/gps/test.rp2040-ard.yaml b/tests/components/gps/test.rp2040-ard.yaml new file mode 100644 index 0000000000..031f45b873 --- /dev/null +++ b/tests/components/gps/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_gps + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + parity: EVEN + +gps: + +time: + - platform: gps + on_time_sync: + then: + logger.log: "It's time!" diff --git a/tests/components/graph/test.esp32-ard.yaml b/tests/components/graph/test.esp32-ard.yaml new file mode 100644 index 0000000000..8c0c0d4c9e --- /dev/null +++ b/tests/components/graph/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_graph + scl: 16 + sda: 17 + +sensor: + - platform: template + id: some_sensor + +graph: + - id: some_graph + sensor: some_sensor + duration: 1h + width: 100 + height: 100 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/graph/test.esp32-c3-ard.yaml b/tests/components/graph/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8ce40e84ac --- /dev/null +++ b/tests/components/graph/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_graph + scl: 5 + sda: 4 + +sensor: + - platform: template + id: some_sensor + +graph: + - id: some_graph + sensor: some_sensor + duration: 1h + width: 100 + height: 100 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/graph/test.esp32-c3-idf.yaml b/tests/components/graph/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8ce40e84ac --- /dev/null +++ b/tests/components/graph/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_graph + scl: 5 + sda: 4 + +sensor: + - platform: template + id: some_sensor + +graph: + - id: some_graph + sensor: some_sensor + duration: 1h + width: 100 + height: 100 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/graph/test.esp32-idf.yaml b/tests/components/graph/test.esp32-idf.yaml new file mode 100644 index 0000000000..8c0c0d4c9e --- /dev/null +++ b/tests/components/graph/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_graph + scl: 16 + sda: 17 + +sensor: + - platform: template + id: some_sensor + +graph: + - id: some_graph + sensor: some_sensor + duration: 1h + width: 100 + height: 100 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/graph/test.esp8266-ard.yaml b/tests/components/graph/test.esp8266-ard.yaml new file mode 100644 index 0000000000..33318355d5 --- /dev/null +++ b/tests/components/graph/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_graph + scl: 5 + sda: 4 + +sensor: + - platform: template + id: some_sensor + +graph: + - id: some_graph + sensor: some_sensor + duration: 1h + width: 100 + height: 100 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/graph/test.rp2040-ard.yaml b/tests/components/graph/test.rp2040-ard.yaml new file mode 100644 index 0000000000..8ce40e84ac --- /dev/null +++ b/tests/components/graph/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_graph + scl: 5 + sda: 4 + +sensor: + - platform: template + id: some_sensor + +graph: + - id: some_graph + sensor: some_sensor + duration: 1h + width: 100 + height: 100 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/graphical_display_menu/test.esp32-ard.yaml b/tests/components/graphical_display_menu/test.esp32-ard.yaml new file mode 100644 index 0000000000..a0897536d7 --- /dev/null +++ b/tests/components/graphical_display_menu/test.esp32-ard.yaml @@ -0,0 +1,120 @@ +i2c: + - id: i2c_graphical_display_menu + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + id: test_switch + optimistic: true + +graphical_display_menu: + id: test_graphical_display_menu + display: ssd1306_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: "Back" + - type: label + - type: menu + text: "Submenu 1" + items: + - type: back + text: "Back" + - type: menu + text: "Submenu 21" + items: + - type: back + text: "Back" + - type: command + text: "Show Main" + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: "Enum Item" + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: "Number" + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: "Hide" + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: "Switch" + switch: test_switch + on_text: "Bright" + off_text: "Dark" + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/graphical_display_menu/test.esp32-c3-ard.yaml b/tests/components/graphical_display_menu/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..23acd4e4d9 --- /dev/null +++ b/tests/components/graphical_display_menu/test.esp32-c3-ard.yaml @@ -0,0 +1,120 @@ +i2c: + - id: i2c_graphical_display_menu + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + id: test_switch + optimistic: true + +graphical_display_menu: + id: test_graphical_display_menu + display: ssd1306_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: "Back" + - type: label + - type: menu + text: "Submenu 1" + items: + - type: back + text: "Back" + - type: menu + text: "Submenu 21" + items: + - type: back + text: "Back" + - type: command + text: "Show Main" + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: "Enum Item" + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: "Number" + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: "Hide" + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: "Switch" + switch: test_switch + on_text: "Bright" + off_text: "Dark" + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml b/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..23acd4e4d9 --- /dev/null +++ b/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml @@ -0,0 +1,120 @@ +i2c: + - id: i2c_graphical_display_menu + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + id: test_switch + optimistic: true + +graphical_display_menu: + id: test_graphical_display_menu + display: ssd1306_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: "Back" + - type: label + - type: menu + text: "Submenu 1" + items: + - type: back + text: "Back" + - type: menu + text: "Submenu 21" + items: + - type: back + text: "Back" + - type: command + text: "Show Main" + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: "Enum Item" + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: "Number" + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: "Hide" + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: "Switch" + switch: test_switch + on_text: "Bright" + off_text: "Dark" + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/graphical_display_menu/test.esp32-idf.yaml b/tests/components/graphical_display_menu/test.esp32-idf.yaml new file mode 100644 index 0000000000..a0897536d7 --- /dev/null +++ b/tests/components/graphical_display_menu/test.esp32-idf.yaml @@ -0,0 +1,120 @@ +i2c: + - id: i2c_graphical_display_menu + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + id: test_switch + optimistic: true + +graphical_display_menu: + id: test_graphical_display_menu + display: ssd1306_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: "Back" + - type: label + - type: menu + text: "Submenu 1" + items: + - type: back + text: "Back" + - type: menu + text: "Submenu 21" + items: + - type: back + text: "Back" + - type: command + text: "Show Main" + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: "Enum Item" + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: "Number" + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: "Hide" + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: "Switch" + switch: test_switch + on_text: "Bright" + off_text: "Dark" + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/graphical_display_menu/test.esp8266-ard.yaml b/tests/components/graphical_display_menu/test.esp8266-ard.yaml new file mode 100644 index 0000000000..28c1a7298d --- /dev/null +++ b/tests/components/graphical_display_menu/test.esp8266-ard.yaml @@ -0,0 +1,120 @@ +i2c: + - id: i2c_graphical_display_menu + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + id: test_switch + optimistic: true + +graphical_display_menu: + id: test_graphical_display_menu + display: ssd1306_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: "Back" + - type: label + - type: menu + text: "Submenu 1" + items: + - type: back + text: "Back" + - type: menu + text: "Submenu 21" + items: + - type: back + text: "Back" + - type: command + text: "Show Main" + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: "Enum Item" + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: "Number" + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: "Hide" + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: "Switch" + switch: test_switch + on_text: "Bright" + off_text: "Dark" + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/graphical_display_menu/test.rp2040-ard.yaml b/tests/components/graphical_display_menu/test.rp2040-ard.yaml new file mode 100644 index 0000000000..23acd4e4d9 --- /dev/null +++ b/tests/components/graphical_display_menu/test.rp2040-ard.yaml @@ -0,0 +1,120 @@ +i2c: + - id: i2c_graphical_display_menu + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + id: test_switch + optimistic: true + +graphical_display_menu: + id: test_graphical_display_menu + display: ssd1306_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: "Back" + - type: label + - type: menu + text: "Submenu 1" + items: + - type: back + text: "Back" + - type: menu + text: "Submenu 21" + items: + - type: back + text: "Back" + - type: command + text: "Show Main" + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: "Enum Item" + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: "Number" + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: "Hide" + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: "Switch" + switch: test_switch + on_text: "Bright" + off_text: "Dark" + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/gree/test.esp32-ard.yaml b/tests/components/gree/test.esp32-ard.yaml new file mode 100644 index 0000000000..91491d7e16 --- /dev/null +++ b/tests/components/gree/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: gree + name: GREE + model: generic diff --git a/tests/components/gree/test.esp32-c3-ard.yaml b/tests/components/gree/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..91491d7e16 --- /dev/null +++ b/tests/components/gree/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: gree + name: GREE + model: generic diff --git a/tests/components/gree/test.esp32-c3-idf.yaml b/tests/components/gree/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..91491d7e16 --- /dev/null +++ b/tests/components/gree/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: gree + name: GREE + model: generic diff --git a/tests/components/gree/test.esp32-idf.yaml b/tests/components/gree/test.esp32-idf.yaml new file mode 100644 index 0000000000..91491d7e16 --- /dev/null +++ b/tests/components/gree/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: gree + name: GREE + model: generic diff --git a/tests/components/gree/test.esp8266-ard.yaml b/tests/components/gree/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d0542973ce --- /dev/null +++ b/tests/components/gree/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: gree + name: GREE + model: generic diff --git a/tests/components/grove_gas_mc_v2/common.yaml b/tests/components/grove_gas_mc_v2/common.yaml new file mode 100644 index 0000000000..0729e6b9c7 --- /dev/null +++ b/tests/components/grove_gas_mc_v2/common.yaml @@ -0,0 +1,13 @@ +sensor: + - platform: grove_gas_mc_v2 + i2c_id: i2c_bus + nitrogen_dioxide: + name: "Nitrogen Dioxide" + ethanol: + name: "Ethanol" + carbon_monoxide: + name: "Carbon Monoxide" + tvoc: + name: "Volatile Organic Compounds" + update_interval: 30s + address: 0xAD diff --git a/tests/components/grove_gas_mc_v2/test.esp32-ard.yaml b/tests/components/grove_gas_mc_v2/test.esp32-ard.yaml new file mode 100644 index 0000000000..00c7856c36 --- /dev/null +++ b/tests/components/grove_gas_mc_v2/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + sda: 21 + scl: 22 + id: i2c_bus + +<<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.esp32-idf.yaml b/tests/components/grove_gas_mc_v2/test.esp32-idf.yaml new file mode 100644 index 0000000000..00c7856c36 --- /dev/null +++ b/tests/components/grove_gas_mc_v2/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + sda: 21 + scl: 22 + id: i2c_bus + +<<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.esp8266-ard.yaml b/tests/components/grove_gas_mc_v2/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2de18bdf39 --- /dev/null +++ b/tests/components/grove_gas_mc_v2/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + sda: 4 + scl: 5 + id: i2c_bus + +<<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.rp2040-ard.yaml b/tests/components/grove_gas_mc_v2/test.rp2040-ard.yaml new file mode 100644 index 0000000000..00c7856c36 --- /dev/null +++ b/tests/components/grove_gas_mc_v2/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + sda: 21 + scl: 22 + id: i2c_bus + +<<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/test.esp32-ard.yaml b/tests/components/grove_tb6612fng/test.esp32-ard.yaml new file mode 100644 index 0000000000..3271fb754f --- /dev/null +++ b/tests/components/grove_tb6612fng/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - grove_tb6612fng.run: + channel: 1 + speed: 255 + direction: BACKWARD + id: test_motor + - grove_tb6612fng.stop: + channel: 1 + id: test_motor + - grove_tb6612fng.break: + channel: 1 + id: test_motor + +i2c: + - id: i2c_grove_tb6612fng + scl: 16 + sda: 17 + +grove_tb6612fng: + id: test_motor + address: 0x14 diff --git a/tests/components/grove_tb6612fng/test.esp32-c3-ard.yaml b/tests/components/grove_tb6612fng/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ef6dff6539 --- /dev/null +++ b/tests/components/grove_tb6612fng/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - grove_tb6612fng.run: + channel: 1 + speed: 255 + direction: BACKWARD + id: test_motor + - grove_tb6612fng.stop: + channel: 1 + id: test_motor + - grove_tb6612fng.break: + channel: 1 + id: test_motor + +i2c: + - id: i2c_grove_tb6612fng + scl: 5 + sda: 4 + +grove_tb6612fng: + id: test_motor + address: 0x14 diff --git a/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml b/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ef6dff6539 --- /dev/null +++ b/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - grove_tb6612fng.run: + channel: 1 + speed: 255 + direction: BACKWARD + id: test_motor + - grove_tb6612fng.stop: + channel: 1 + id: test_motor + - grove_tb6612fng.break: + channel: 1 + id: test_motor + +i2c: + - id: i2c_grove_tb6612fng + scl: 5 + sda: 4 + +grove_tb6612fng: + id: test_motor + address: 0x14 diff --git a/tests/components/grove_tb6612fng/test.esp32-idf.yaml b/tests/components/grove_tb6612fng/test.esp32-idf.yaml new file mode 100644 index 0000000000..3271fb754f --- /dev/null +++ b/tests/components/grove_tb6612fng/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - grove_tb6612fng.run: + channel: 1 + speed: 255 + direction: BACKWARD + id: test_motor + - grove_tb6612fng.stop: + channel: 1 + id: test_motor + - grove_tb6612fng.break: + channel: 1 + id: test_motor + +i2c: + - id: i2c_grove_tb6612fng + scl: 16 + sda: 17 + +grove_tb6612fng: + id: test_motor + address: 0x14 diff --git a/tests/components/grove_tb6612fng/test.esp8266-ard.yaml b/tests/components/grove_tb6612fng/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ef6dff6539 --- /dev/null +++ b/tests/components/grove_tb6612fng/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - grove_tb6612fng.run: + channel: 1 + speed: 255 + direction: BACKWARD + id: test_motor + - grove_tb6612fng.stop: + channel: 1 + id: test_motor + - grove_tb6612fng.break: + channel: 1 + id: test_motor + +i2c: + - id: i2c_grove_tb6612fng + scl: 5 + sda: 4 + +grove_tb6612fng: + id: test_motor + address: 0x14 diff --git a/tests/components/grove_tb6612fng/test.rp2040-ard.yaml b/tests/components/grove_tb6612fng/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ef6dff6539 --- /dev/null +++ b/tests/components/grove_tb6612fng/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - grove_tb6612fng.run: + channel: 1 + speed: 255 + direction: BACKWARD + id: test_motor + - grove_tb6612fng.stop: + channel: 1 + id: test_motor + - grove_tb6612fng.break: + channel: 1 + id: test_motor + +i2c: + - id: i2c_grove_tb6612fng + scl: 5 + sda: 4 + +grove_tb6612fng: + id: test_motor + address: 0x14 diff --git a/tests/components/growatt_solar/test.esp32-ard.yaml b/tests/components/growatt_solar/test.esp32-ard.yaml new file mode 100644 index 0000000000..654f2ccedf --- /dev/null +++ b/tests/components/growatt_solar/test.esp32-ard.yaml @@ -0,0 +1,62 @@ +uart: + - id: uart_growatt_solar + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + flow_control_pin: 13 + +sensor: + - platform: growatt_solar + update_interval: 10s + protocol_version: RTU + inverter_status: + name: Growatt Status Code + phase_a: + voltage: + name: Growatt Voltage Phase A + current: + name: Growatt Current Phase A + active_power: + name: Growatt Power Phase A + phase_b: + voltage: + name: Growatt Voltage Phase B + current: + name: Growatt Current Phase B + active_power: + name: Growatt Power Phase B + phase_c: + voltage: + name: Growatt Voltage Phase C + current: + name: Growatt Current Phase C + active_power: + name: Growatt Power Phase C + pv1: + voltage: + name: Growatt PV1 Voltage + current: + name: Growatt PV1 Current + active_power: + name: Growatt PV1 Active Power + pv2: + voltage: + name: Growatt PV2 Voltage + current: + name: Growatt PV2 Current + active_power: + name: Growatt PV2 Active Power + active_power: + name: Growatt Grid Active Power + pv_active_power: + name: Growatt PV Active Power + frequency: + name: Growatt Frequency + energy_production_day: + name: Growatt Today's Generation + total_energy_production: + name: Growatt Total Energy Production + inverter_module_temp: + name: Growatt Inverter Module Temp diff --git a/tests/components/growatt_solar/test.esp32-c3-ard.yaml b/tests/components/growatt_solar/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7e73897856 --- /dev/null +++ b/tests/components/growatt_solar/test.esp32-c3-ard.yaml @@ -0,0 +1,62 @@ +uart: + - id: uart_growatt_solar + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: growatt_solar + update_interval: 10s + protocol_version: RTU + inverter_status: + name: Growatt Status Code + phase_a: + voltage: + name: Growatt Voltage Phase A + current: + name: Growatt Current Phase A + active_power: + name: Growatt Power Phase A + phase_b: + voltage: + name: Growatt Voltage Phase B + current: + name: Growatt Current Phase B + active_power: + name: Growatt Power Phase B + phase_c: + voltage: + name: Growatt Voltage Phase C + current: + name: Growatt Current Phase C + active_power: + name: Growatt Power Phase C + pv1: + voltage: + name: Growatt PV1 Voltage + current: + name: Growatt PV1 Current + active_power: + name: Growatt PV1 Active Power + pv2: + voltage: + name: Growatt PV2 Voltage + current: + name: Growatt PV2 Current + active_power: + name: Growatt PV2 Active Power + active_power: + name: Growatt Grid Active Power + pv_active_power: + name: Growatt PV Active Power + frequency: + name: Growatt Frequency + energy_production_day: + name: Growatt Today's Generation + total_energy_production: + name: Growatt Total Energy Production + inverter_module_temp: + name: Growatt Inverter Module Temp diff --git a/tests/components/growatt_solar/test.esp32-c3-idf.yaml b/tests/components/growatt_solar/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7e73897856 --- /dev/null +++ b/tests/components/growatt_solar/test.esp32-c3-idf.yaml @@ -0,0 +1,62 @@ +uart: + - id: uart_growatt_solar + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: growatt_solar + update_interval: 10s + protocol_version: RTU + inverter_status: + name: Growatt Status Code + phase_a: + voltage: + name: Growatt Voltage Phase A + current: + name: Growatt Current Phase A + active_power: + name: Growatt Power Phase A + phase_b: + voltage: + name: Growatt Voltage Phase B + current: + name: Growatt Current Phase B + active_power: + name: Growatt Power Phase B + phase_c: + voltage: + name: Growatt Voltage Phase C + current: + name: Growatt Current Phase C + active_power: + name: Growatt Power Phase C + pv1: + voltage: + name: Growatt PV1 Voltage + current: + name: Growatt PV1 Current + active_power: + name: Growatt PV1 Active Power + pv2: + voltage: + name: Growatt PV2 Voltage + current: + name: Growatt PV2 Current + active_power: + name: Growatt PV2 Active Power + active_power: + name: Growatt Grid Active Power + pv_active_power: + name: Growatt PV Active Power + frequency: + name: Growatt Frequency + energy_production_day: + name: Growatt Today's Generation + total_energy_production: + name: Growatt Total Energy Production + inverter_module_temp: + name: Growatt Inverter Module Temp diff --git a/tests/components/growatt_solar/test.esp32-idf.yaml b/tests/components/growatt_solar/test.esp32-idf.yaml new file mode 100644 index 0000000000..654f2ccedf --- /dev/null +++ b/tests/components/growatt_solar/test.esp32-idf.yaml @@ -0,0 +1,62 @@ +uart: + - id: uart_growatt_solar + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + flow_control_pin: 13 + +sensor: + - platform: growatt_solar + update_interval: 10s + protocol_version: RTU + inverter_status: + name: Growatt Status Code + phase_a: + voltage: + name: Growatt Voltage Phase A + current: + name: Growatt Current Phase A + active_power: + name: Growatt Power Phase A + phase_b: + voltage: + name: Growatt Voltage Phase B + current: + name: Growatt Current Phase B + active_power: + name: Growatt Power Phase B + phase_c: + voltage: + name: Growatt Voltage Phase C + current: + name: Growatt Current Phase C + active_power: + name: Growatt Power Phase C + pv1: + voltage: + name: Growatt PV1 Voltage + current: + name: Growatt PV1 Current + active_power: + name: Growatt PV1 Active Power + pv2: + voltage: + name: Growatt PV2 Voltage + current: + name: Growatt PV2 Current + active_power: + name: Growatt PV2 Active Power + active_power: + name: Growatt Grid Active Power + pv_active_power: + name: Growatt PV Active Power + frequency: + name: Growatt Frequency + energy_production_day: + name: Growatt Today's Generation + total_energy_production: + name: Growatt Total Energy Production + inverter_module_temp: + name: Growatt Inverter Module Temp diff --git a/tests/components/growatt_solar/test.esp8266-ard.yaml b/tests/components/growatt_solar/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a1cf8267ae --- /dev/null +++ b/tests/components/growatt_solar/test.esp8266-ard.yaml @@ -0,0 +1,62 @@ +uart: + - id: uart_growatt_solar + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 13 + +sensor: + - platform: growatt_solar + update_interval: 10s + protocol_version: RTU + inverter_status: + name: Growatt Status Code + phase_a: + voltage: + name: Growatt Voltage Phase A + current: + name: Growatt Current Phase A + active_power: + name: Growatt Power Phase A + phase_b: + voltage: + name: Growatt Voltage Phase B + current: + name: Growatt Current Phase B + active_power: + name: Growatt Power Phase B + phase_c: + voltage: + name: Growatt Voltage Phase C + current: + name: Growatt Current Phase C + active_power: + name: Growatt Power Phase C + pv1: + voltage: + name: Growatt PV1 Voltage + current: + name: Growatt PV1 Current + active_power: + name: Growatt PV1 Active Power + pv2: + voltage: + name: Growatt PV2 Voltage + current: + name: Growatt PV2 Current + active_power: + name: Growatt PV2 Active Power + active_power: + name: Growatt Grid Active Power + pv_active_power: + name: Growatt PV Active Power + frequency: + name: Growatt Frequency + energy_production_day: + name: Growatt Today's Generation + total_energy_production: + name: Growatt Total Energy Production + inverter_module_temp: + name: Growatt Inverter Module Temp diff --git a/tests/components/growatt_solar/test.rp2040-ard.yaml b/tests/components/growatt_solar/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7e73897856 --- /dev/null +++ b/tests/components/growatt_solar/test.rp2040-ard.yaml @@ -0,0 +1,62 @@ +uart: + - id: uart_growatt_solar + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: growatt_solar + update_interval: 10s + protocol_version: RTU + inverter_status: + name: Growatt Status Code + phase_a: + voltage: + name: Growatt Voltage Phase A + current: + name: Growatt Current Phase A + active_power: + name: Growatt Power Phase A + phase_b: + voltage: + name: Growatt Voltage Phase B + current: + name: Growatt Current Phase B + active_power: + name: Growatt Power Phase B + phase_c: + voltage: + name: Growatt Voltage Phase C + current: + name: Growatt Current Phase C + active_power: + name: Growatt Power Phase C + pv1: + voltage: + name: Growatt PV1 Voltage + current: + name: Growatt PV1 Current + active_power: + name: Growatt PV1 Active Power + pv2: + voltage: + name: Growatt PV2 Voltage + current: + name: Growatt PV2 Current + active_power: + name: Growatt PV2 Active Power + active_power: + name: Growatt Grid Active Power + pv_active_power: + name: Growatt PV Active Power + frequency: + name: Growatt Frequency + energy_production_day: + name: Growatt Today's Generation + total_energy_production: + name: Growatt Total Energy Production + inverter_module_temp: + name: Growatt Inverter Module Temp diff --git a/tests/components/gt911/common.yaml b/tests/components/gt911/common.yaml new file mode 100644 index 0000000000..7bb88108da --- /dev/null +++ b/tests/components/gt911/common.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_gt911 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 10 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: gt911 + display: ssd1306_display + interrupt_pin: 20 + reset_pin: 21 + +binary_sensor: + - platform: gt911 + id: touch_key_911 + index: 0 diff --git a/tests/components/gt911/test.esp32-ard.yaml b/tests/components/gt911/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/gt911/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-c3-ard.yaml b/tests/components/gt911/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/gt911/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-c3-idf.yaml b/tests/components/gt911/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/gt911/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-idf.yaml b/tests/components/gt911/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/gt911/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/gt911/test.esp8266-ard.yaml b/tests/components/gt911/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8b76eff29e --- /dev/null +++ b/tests/components/gt911/test.esp8266-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_gt911 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: gt911 + display: ssd1306_display + interrupt_pin: 12 + +binary_sensor: + - platform: gt911 + id: touch_key_911 + index: 0 diff --git a/tests/components/gt911/test.rp2040-ard.yaml b/tests/components/gt911/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/gt911/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/haier/common.yaml b/tests/components/haier/common.yaml new file mode 100644 index 0000000000..368b88b69c --- /dev/null +++ b/tests/components/haier/common.yaml @@ -0,0 +1,124 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_haier + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + +climate: + - platform: haier + id: haier_ac + uart_id: uart_haier + protocol: hOn + name: Haier AC + wifi_signal: true + answer_timeout: 200ms + visual: + min_temperature: 16 °C + max_temperature: 30 °C + temperature_step: + target_temperature: 1 + current_temperature: 0.5 + supported_modes: + - 'OFF' + - HEAT_COOL + - COOL + - HEAT + - DRY + - FAN_ONLY + supported_swing_modes: + - 'OFF' + - VERTICAL + - HORIZONTAL + - BOTH + supported_presets: + - AWAY + - BOOST + - SLEEP + on_alarm_start: + then: + - logger.log: + level: DEBUG + format: "Alarm activated. Code: %d. Message: \"%s\"" + args: [code, message] + on_alarm_end: + then: + - logger.log: + level: DEBUG + format: "Alarm deactivated. Code: %d. Message: \"%s\"" + args: [code, message] + +sensor: + - platform: haier + haier_id: haier_ac + outdoor_temperature: + name: Haier outdoor temperature + humidity: + name: Haier Indoor Humidity + compressor_current: + name: Haier Compressor Current + compressor_frequency: + name: Haier Compressor Frequency + expansion_valve_open_degree: + name: Haier Expansion Valve Open Degree + indoor_coil_temperature: + name: Haier Indoor Coil Temperature + outdoor_coil_temperature: + name: Haier Outdoor Coil Temperature + outdoor_defrost_temperature: + name: Haier Outdoor Defrost Temperature + outdoor_in_air_temperature: + name: Haier Outdoor In Air Temperature + outdoor_out_air_temperature: + name: Haier Outdoor Out Air Temperature + power: + name: Haier Power + +binary_sensor: + - platform: haier + haier_id: haier_ac + compressor_status: + name: Haier Outdoor Compressor Status + defrost_status: + name: Haier Defrost Status + four_way_valve_status: + name: Haier Four Way Valve Status + indoor_electric_heating_status: + name: Haier Indoor Electric Heating Status + indoor_fan_status: + name: Haier Indoor Fan Status + outdoor_fan_status: + name: Haier Outdoor Fan Status + +button: + - platform: haier + haier_id: haier_ac + self_cleaning: + name: Haier start self cleaning + steri_cleaning: + name: Haier start 56°C steri-cleaning + +text_sensor: + - platform: haier + haier_id: haier_ac + appliance_name: + name: Haier appliance name + cleaning_status: + name: Haier cleaning status + protocol_version: + name: Haier protocol version + +switch: + - platform: haier + haier_id: haier_ac + beeper: + name: Haier beeper + display: + name: Haier display + health_mode: + name: Haier health mode + quiet_mode: + name: Haier quiet mode diff --git a/tests/components/haier/test.esp32-ard.yaml b/tests/components/haier/test.esp32-ard.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/haier/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/haier/test.esp32-c3-ard.yaml b/tests/components/haier/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/haier/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/haier/test.esp32-c3-idf.yaml b/tests/components/haier/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/haier/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/haier/test.esp32-idf.yaml b/tests/components/haier/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/haier/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/haier/test.esp8266-ard.yaml b/tests/components/haier/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/haier/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/haier/test.rp2040-ard.yaml b/tests/components/haier/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/haier/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/havells_solar/test.esp32-ard.yaml b/tests/components/havells_solar/test.esp32-ard.yaml new file mode 100644 index 0000000000..2cda8e37be --- /dev/null +++ b/tests/components/havells_solar/test.esp32-ard.yaml @@ -0,0 +1,79 @@ +uart: + - id: uart_havells_solar + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: havells_solar + update_interval: 60s + phase_a: + voltage: + name: Phase A Voltage + current: + name: Phase A Current + phase_b: + voltage: + name: Voltage Phase B + current: + name: Current Phase B + phase_c: + voltage: + name: Voltage Phase C + current: + name: Current Phase C + pv1: + voltage: + name: PV1 Voltage + current: + name: PV1 Current + active_power: + name: PV1 Active Power + voltage_sampled_by_secondary_cpu: + name: PV1 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV1 Insulation Of +VE To Ground + pv2: + voltage: + name: PV2 Voltage + current: + name: PV2 Current + active_power: + name: PV2 Active Power + voltage_sampled_by_secondary_cpu: + name: PV2 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV2 Insulation Of +VE To Ground + active_power: + name: Active Power + reactive_power: + name: Reactive Power + frequency: + name: Frequency + energy_production_day: + name: Today's Generation + total_energy_production: + name: Total Energy Production + total_generation_time: + name: Total Generation Time + today_generation_time: + name: Today Generation Time + inverter_module_temp: + name: Inverter Module Temp + inverter_inner_temp: + name: Inverter Inner Temp + inverter_bus_voltage: + name: Inverter BUS Voltage + insulation_of_pv_n_to_ground: + name: Insulation Of PV- To Ground + gfci_value: + name: GFCI Value + dci_of_r: + name: DCI Of R + dci_of_s: + name: DCI Of S + dci_of_t: + name: DCI Of T diff --git a/tests/components/havells_solar/test.esp32-c3-ard.yaml b/tests/components/havells_solar/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5cb911cf71 --- /dev/null +++ b/tests/components/havells_solar/test.esp32-c3-ard.yaml @@ -0,0 +1,79 @@ +uart: + - id: uart_havells_solar + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: havells_solar + update_interval: 60s + phase_a: + voltage: + name: Phase A Voltage + current: + name: Phase A Current + phase_b: + voltage: + name: Voltage Phase B + current: + name: Current Phase B + phase_c: + voltage: + name: Voltage Phase C + current: + name: Current Phase C + pv1: + voltage: + name: PV1 Voltage + current: + name: PV1 Current + active_power: + name: PV1 Active Power + voltage_sampled_by_secondary_cpu: + name: PV1 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV1 Insulation Of +VE To Ground + pv2: + voltage: + name: PV2 Voltage + current: + name: PV2 Current + active_power: + name: PV2 Active Power + voltage_sampled_by_secondary_cpu: + name: PV2 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV2 Insulation Of +VE To Ground + active_power: + name: Active Power + reactive_power: + name: Reactive Power + frequency: + name: Frequency + energy_production_day: + name: Today's Generation + total_energy_production: + name: Total Energy Production + total_generation_time: + name: Total Generation Time + today_generation_time: + name: Today Generation Time + inverter_module_temp: + name: Inverter Module Temp + inverter_inner_temp: + name: Inverter Inner Temp + inverter_bus_voltage: + name: Inverter BUS Voltage + insulation_of_pv_n_to_ground: + name: Insulation Of PV- To Ground + gfci_value: + name: GFCI Value + dci_of_r: + name: DCI Of R + dci_of_s: + name: DCI Of S + dci_of_t: + name: DCI Of T diff --git a/tests/components/havells_solar/test.esp32-c3-idf.yaml b/tests/components/havells_solar/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5cb911cf71 --- /dev/null +++ b/tests/components/havells_solar/test.esp32-c3-idf.yaml @@ -0,0 +1,79 @@ +uart: + - id: uart_havells_solar + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: havells_solar + update_interval: 60s + phase_a: + voltage: + name: Phase A Voltage + current: + name: Phase A Current + phase_b: + voltage: + name: Voltage Phase B + current: + name: Current Phase B + phase_c: + voltage: + name: Voltage Phase C + current: + name: Current Phase C + pv1: + voltage: + name: PV1 Voltage + current: + name: PV1 Current + active_power: + name: PV1 Active Power + voltage_sampled_by_secondary_cpu: + name: PV1 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV1 Insulation Of +VE To Ground + pv2: + voltage: + name: PV2 Voltage + current: + name: PV2 Current + active_power: + name: PV2 Active Power + voltage_sampled_by_secondary_cpu: + name: PV2 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV2 Insulation Of +VE To Ground + active_power: + name: Active Power + reactive_power: + name: Reactive Power + frequency: + name: Frequency + energy_production_day: + name: Today's Generation + total_energy_production: + name: Total Energy Production + total_generation_time: + name: Total Generation Time + today_generation_time: + name: Today Generation Time + inverter_module_temp: + name: Inverter Module Temp + inverter_inner_temp: + name: Inverter Inner Temp + inverter_bus_voltage: + name: Inverter BUS Voltage + insulation_of_pv_n_to_ground: + name: Insulation Of PV- To Ground + gfci_value: + name: GFCI Value + dci_of_r: + name: DCI Of R + dci_of_s: + name: DCI Of S + dci_of_t: + name: DCI Of T diff --git a/tests/components/havells_solar/test.esp32-idf.yaml b/tests/components/havells_solar/test.esp32-idf.yaml new file mode 100644 index 0000000000..2cda8e37be --- /dev/null +++ b/tests/components/havells_solar/test.esp32-idf.yaml @@ -0,0 +1,79 @@ +uart: + - id: uart_havells_solar + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: havells_solar + update_interval: 60s + phase_a: + voltage: + name: Phase A Voltage + current: + name: Phase A Current + phase_b: + voltage: + name: Voltage Phase B + current: + name: Current Phase B + phase_c: + voltage: + name: Voltage Phase C + current: + name: Current Phase C + pv1: + voltage: + name: PV1 Voltage + current: + name: PV1 Current + active_power: + name: PV1 Active Power + voltage_sampled_by_secondary_cpu: + name: PV1 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV1 Insulation Of +VE To Ground + pv2: + voltage: + name: PV2 Voltage + current: + name: PV2 Current + active_power: + name: PV2 Active Power + voltage_sampled_by_secondary_cpu: + name: PV2 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV2 Insulation Of +VE To Ground + active_power: + name: Active Power + reactive_power: + name: Reactive Power + frequency: + name: Frequency + energy_production_day: + name: Today's Generation + total_energy_production: + name: Total Energy Production + total_generation_time: + name: Total Generation Time + today_generation_time: + name: Today Generation Time + inverter_module_temp: + name: Inverter Module Temp + inverter_inner_temp: + name: Inverter Inner Temp + inverter_bus_voltage: + name: Inverter BUS Voltage + insulation_of_pv_n_to_ground: + name: Insulation Of PV- To Ground + gfci_value: + name: GFCI Value + dci_of_r: + name: DCI Of R + dci_of_s: + name: DCI Of S + dci_of_t: + name: DCI Of T diff --git a/tests/components/havells_solar/test.esp8266-ard.yaml b/tests/components/havells_solar/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5cb911cf71 --- /dev/null +++ b/tests/components/havells_solar/test.esp8266-ard.yaml @@ -0,0 +1,79 @@ +uart: + - id: uart_havells_solar + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: havells_solar + update_interval: 60s + phase_a: + voltage: + name: Phase A Voltage + current: + name: Phase A Current + phase_b: + voltage: + name: Voltage Phase B + current: + name: Current Phase B + phase_c: + voltage: + name: Voltage Phase C + current: + name: Current Phase C + pv1: + voltage: + name: PV1 Voltage + current: + name: PV1 Current + active_power: + name: PV1 Active Power + voltage_sampled_by_secondary_cpu: + name: PV1 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV1 Insulation Of +VE To Ground + pv2: + voltage: + name: PV2 Voltage + current: + name: PV2 Current + active_power: + name: PV2 Active Power + voltage_sampled_by_secondary_cpu: + name: PV2 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV2 Insulation Of +VE To Ground + active_power: + name: Active Power + reactive_power: + name: Reactive Power + frequency: + name: Frequency + energy_production_day: + name: Today's Generation + total_energy_production: + name: Total Energy Production + total_generation_time: + name: Total Generation Time + today_generation_time: + name: Today Generation Time + inverter_module_temp: + name: Inverter Module Temp + inverter_inner_temp: + name: Inverter Inner Temp + inverter_bus_voltage: + name: Inverter BUS Voltage + insulation_of_pv_n_to_ground: + name: Insulation Of PV- To Ground + gfci_value: + name: GFCI Value + dci_of_r: + name: DCI Of R + dci_of_s: + name: DCI Of S + dci_of_t: + name: DCI Of T diff --git a/tests/components/havells_solar/test.rp2040-ard.yaml b/tests/components/havells_solar/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5cb911cf71 --- /dev/null +++ b/tests/components/havells_solar/test.rp2040-ard.yaml @@ -0,0 +1,79 @@ +uart: + - id: uart_havells_solar + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: havells_solar + update_interval: 60s + phase_a: + voltage: + name: Phase A Voltage + current: + name: Phase A Current + phase_b: + voltage: + name: Voltage Phase B + current: + name: Current Phase B + phase_c: + voltage: + name: Voltage Phase C + current: + name: Current Phase C + pv1: + voltage: + name: PV1 Voltage + current: + name: PV1 Current + active_power: + name: PV1 Active Power + voltage_sampled_by_secondary_cpu: + name: PV1 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV1 Insulation Of +VE To Ground + pv2: + voltage: + name: PV2 Voltage + current: + name: PV2 Current + active_power: + name: PV2 Active Power + voltage_sampled_by_secondary_cpu: + name: PV2 Voltage Sampled By Secondary CPU + insulation_of_p_to_ground: + name: PV2 Insulation Of +VE To Ground + active_power: + name: Active Power + reactive_power: + name: Reactive Power + frequency: + name: Frequency + energy_production_day: + name: Today's Generation + total_energy_production: + name: Total Energy Production + total_generation_time: + name: Total Generation Time + today_generation_time: + name: Today Generation Time + inverter_module_temp: + name: Inverter Module Temp + inverter_inner_temp: + name: Inverter Inner Temp + inverter_bus_voltage: + name: Inverter BUS Voltage + insulation_of_pv_n_to_ground: + name: Insulation Of PV- To Ground + gfci_value: + name: GFCI Value + dci_of_r: + name: DCI Of R + dci_of_s: + name: DCI Of S + dci_of_t: + name: DCI Of T diff --git a/tests/components/hbridge/test.esp32-ard.yaml b/tests/components/hbridge/test.esp32-ard.yaml new file mode 100644 index 0000000000..6a80aaaf3b --- /dev/null +++ b/tests/components/hbridge/test.esp32-ard.yaml @@ -0,0 +1,33 @@ +output: + - platform: ledc + pin: 14 + id: gpio_output1 + - platform: ledc + pin: 15 + id: gpio_output2 + - platform: ledc + pin: 12 + id: gpio_output3 + - platform: ledc + pin: 13 + id: gpio_output4 + +light: + - platform: hbridge + name: Icicle Lights + pin_a: gpio_output3 + pin_b: gpio_output4 + +fan: + - platform: hbridge + id: fan_hbridge + speed_count: 4 + name: H-bridge Fan with Presets + pin_a: gpio_output1 + pin_b: gpio_output2 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! diff --git a/tests/components/hbridge/test.esp32-c3-ard.yaml b/tests/components/hbridge/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..70cfd6ab6f --- /dev/null +++ b/tests/components/hbridge/test.esp32-c3-ard.yaml @@ -0,0 +1,33 @@ +output: + - platform: ledc + pin: 4 + id: gpio_output1 + - platform: ledc + pin: 5 + id: gpio_output2 + - platform: ledc + pin: 6 + id: gpio_output3 + - platform: ledc + pin: 7 + id: gpio_output4 + +light: + - platform: hbridge + name: Icicle Lights + pin_a: gpio_output3 + pin_b: gpio_output4 + +fan: + - platform: hbridge + id: fan_hbridge + speed_count: 4 + name: H-bridge Fan with Presets + pin_a: gpio_output1 + pin_b: gpio_output2 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! diff --git a/tests/components/hbridge/test.esp32-c3-idf.yaml b/tests/components/hbridge/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..70cfd6ab6f --- /dev/null +++ b/tests/components/hbridge/test.esp32-c3-idf.yaml @@ -0,0 +1,33 @@ +output: + - platform: ledc + pin: 4 + id: gpio_output1 + - platform: ledc + pin: 5 + id: gpio_output2 + - platform: ledc + pin: 6 + id: gpio_output3 + - platform: ledc + pin: 7 + id: gpio_output4 + +light: + - platform: hbridge + name: Icicle Lights + pin_a: gpio_output3 + pin_b: gpio_output4 + +fan: + - platform: hbridge + id: fan_hbridge + speed_count: 4 + name: H-bridge Fan with Presets + pin_a: gpio_output1 + pin_b: gpio_output2 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! diff --git a/tests/components/hbridge/test.esp32-idf.yaml b/tests/components/hbridge/test.esp32-idf.yaml new file mode 100644 index 0000000000..6a80aaaf3b --- /dev/null +++ b/tests/components/hbridge/test.esp32-idf.yaml @@ -0,0 +1,33 @@ +output: + - platform: ledc + pin: 14 + id: gpio_output1 + - platform: ledc + pin: 15 + id: gpio_output2 + - platform: ledc + pin: 12 + id: gpio_output3 + - platform: ledc + pin: 13 + id: gpio_output4 + +light: + - platform: hbridge + name: Icicle Lights + pin_a: gpio_output3 + pin_b: gpio_output4 + +fan: + - platform: hbridge + id: fan_hbridge + speed_count: 4 + name: H-bridge Fan with Presets + pin_a: gpio_output1 + pin_b: gpio_output2 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! diff --git a/tests/components/hbridge/test.esp8266-ard.yaml b/tests/components/hbridge/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4f8915879d --- /dev/null +++ b/tests/components/hbridge/test.esp8266-ard.yaml @@ -0,0 +1,33 @@ +output: + - platform: esp8266_pwm + pin: 4 + id: gpio_output1 + - platform: esp8266_pwm + pin: 5 + id: gpio_output2 + - platform: esp8266_pwm + pin: 12 + id: gpio_output3 + - platform: esp8266_pwm + pin: 13 + id: gpio_output4 + +light: + - platform: hbridge + name: Icicle Lights + pin_a: gpio_output3 + pin_b: gpio_output4 + +fan: + - platform: hbridge + id: fan_hbridge + speed_count: 4 + name: H-bridge Fan with Presets + pin_a: gpio_output1 + pin_b: gpio_output2 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! diff --git a/tests/components/hbridge/test.rp2040-ard.yaml b/tests/components/hbridge/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e21b55091d --- /dev/null +++ b/tests/components/hbridge/test.rp2040-ard.yaml @@ -0,0 +1,33 @@ +output: + - platform: rp2040_pwm + pin: 4 + id: gpio_output1 + - platform: rp2040_pwm + pin: 5 + id: gpio_output2 + - platform: rp2040_pwm + pin: 6 + id: gpio_output3 + - platform: rp2040_pwm + pin: 7 + id: gpio_output4 + +light: + - platform: hbridge + name: Icicle Lights + pin_a: gpio_output3 + pin_b: gpio_output4 + +fan: + - platform: hbridge + id: fan_hbridge + speed_count: 4 + name: H-bridge Fan with Presets + pin_a: gpio_output1 + pin_b: gpio_output2 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! diff --git a/tests/components/hdc1080/test.esp32-ard.yaml b/tests/components/hdc1080/test.esp32-ard.yaml new file mode 100644 index 0000000000..8e313dfa40 --- /dev/null +++ b/tests/components/hdc1080/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hdc1080 + scl: 16 + sda: 17 + +sensor: + - platform: hdc1080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc1080/test.esp32-c3-ard.yaml b/tests/components/hdc1080/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7bf7af6fa7 --- /dev/null +++ b/tests/components/hdc1080/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hdc1080 + scl: 5 + sda: 4 + +sensor: + - platform: hdc1080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc1080/test.esp32-c3-idf.yaml b/tests/components/hdc1080/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7bf7af6fa7 --- /dev/null +++ b/tests/components/hdc1080/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hdc1080 + scl: 5 + sda: 4 + +sensor: + - platform: hdc1080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc1080/test.esp32-idf.yaml b/tests/components/hdc1080/test.esp32-idf.yaml new file mode 100644 index 0000000000..8e313dfa40 --- /dev/null +++ b/tests/components/hdc1080/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hdc1080 + scl: 16 + sda: 17 + +sensor: + - platform: hdc1080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc1080/test.esp8266-ard.yaml b/tests/components/hdc1080/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7bf7af6fa7 --- /dev/null +++ b/tests/components/hdc1080/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hdc1080 + scl: 5 + sda: 4 + +sensor: + - platform: hdc1080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc1080/test.rp2040-ard.yaml b/tests/components/hdc1080/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7bf7af6fa7 --- /dev/null +++ b/tests/components/hdc1080/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hdc1080 + scl: 5 + sda: 4 + +sensor: + - platform: hdc1080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/he60r/test.esp32-ard.yaml b/tests/components/he60r/test.esp32-ard.yaml new file mode 100644 index 0000000000..840387ae36 --- /dev/null +++ b/tests/components/he60r/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_he60r + tx_pin: 17 + rx_pin: 16 + baud_rate: 1200 + parity: EVEN + +cover: + - platform: he60r + id: garage_door + name: Garage Door + open_duration: 14s + close_duration: 14s diff --git a/tests/components/he60r/test.esp32-c3-ard.yaml b/tests/components/he60r/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..bcbb544442 --- /dev/null +++ b/tests/components/he60r/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_he60r + tx_pin: 4 + rx_pin: 5 + baud_rate: 1200 + parity: EVEN + +cover: + - platform: he60r + id: garage_door + name: Garage Door + open_duration: 14s + close_duration: 14s diff --git a/tests/components/he60r/test.esp32-c3-idf.yaml b/tests/components/he60r/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..bcbb544442 --- /dev/null +++ b/tests/components/he60r/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_he60r + tx_pin: 4 + rx_pin: 5 + baud_rate: 1200 + parity: EVEN + +cover: + - platform: he60r + id: garage_door + name: Garage Door + open_duration: 14s + close_duration: 14s diff --git a/tests/components/he60r/test.esp32-idf.yaml b/tests/components/he60r/test.esp32-idf.yaml new file mode 100644 index 0000000000..840387ae36 --- /dev/null +++ b/tests/components/he60r/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_he60r + tx_pin: 17 + rx_pin: 16 + baud_rate: 1200 + parity: EVEN + +cover: + - platform: he60r + id: garage_door + name: Garage Door + open_duration: 14s + close_duration: 14s diff --git a/tests/components/he60r/test.esp8266-ard.yaml b/tests/components/he60r/test.esp8266-ard.yaml new file mode 100644 index 0000000000..bcbb544442 --- /dev/null +++ b/tests/components/he60r/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_he60r + tx_pin: 4 + rx_pin: 5 + baud_rate: 1200 + parity: EVEN + +cover: + - platform: he60r + id: garage_door + name: Garage Door + open_duration: 14s + close_duration: 14s diff --git a/tests/components/he60r/test.rp2040-ard.yaml b/tests/components/he60r/test.rp2040-ard.yaml new file mode 100644 index 0000000000..bcbb544442 --- /dev/null +++ b/tests/components/he60r/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +uart: + - id: uart_he60r + tx_pin: 4 + rx_pin: 5 + baud_rate: 1200 + parity: EVEN + +cover: + - platform: he60r + id: garage_door + name: Garage Door + open_duration: 14s + close_duration: 14s diff --git a/tests/components/heatpumpir/test.bk72xx-ard.yaml b/tests/components/heatpumpir/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..b616f9157c --- /dev/null +++ b/tests/components/heatpumpir/test.bk72xx-ard.yaml @@ -0,0 +1,26 @@ +remote_transmitter: + pin: 6 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: mitsubishi_heavy_zm + horizontal_default: left + vertical_default: up + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 + - platform: heatpumpir + protocol: daikin + horizontal_default: mleft + vertical_default: mup + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 + - platform: heatpumpir + protocol: panasonic_altdke + horizontal_default: mright + vertical_default: mdown + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/heatpumpir/test.esp32-ard.yaml b/tests/components/heatpumpir/test.esp32-ard.yaml new file mode 100644 index 0000000000..db3f81f6a0 --- /dev/null +++ b/tests/components/heatpumpir/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: mitsubishi_heavy_zm + horizontal_default: left + vertical_default: up + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 + - platform: heatpumpir + protocol: greeyt + horizontal_default: left + vertical_default: up + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/heatpumpir/test.esp8266-ard.yaml b/tests/components/heatpumpir/test.esp8266-ard.yaml new file mode 100644 index 0000000000..26a01cb198 --- /dev/null +++ b/tests/components/heatpumpir/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: heatpumpir + protocol: mitsubishi_heavy_zm + horizontal_default: left + vertical_default: up + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 + - platform: heatpumpir + protocol: greeyt + horizontal_default: left + vertical_default: up + name: HeatpumpIR Climate + min_temperature: 18 + max_temperature: 30 diff --git a/tests/components/hitachi_ac344/test.esp32-ard.yaml b/tests/components/hitachi_ac344/test.esp32-ard.yaml new file mode 100644 index 0000000000..684d5899de --- /dev/null +++ b/tests/components/hitachi_ac344/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac344 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac344/test.esp32-c3-ard.yaml b/tests/components/hitachi_ac344/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..684d5899de --- /dev/null +++ b/tests/components/hitachi_ac344/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac344 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..684d5899de --- /dev/null +++ b/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac344 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac344/test.esp32-idf.yaml b/tests/components/hitachi_ac344/test.esp32-idf.yaml new file mode 100644 index 0000000000..684d5899de --- /dev/null +++ b/tests/components/hitachi_ac344/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac344 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac344/test.esp8266-ard.yaml b/tests/components/hitachi_ac344/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e6203e3084 --- /dev/null +++ b/tests/components/hitachi_ac344/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac344 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac424/test.esp32-ard.yaml b/tests/components/hitachi_ac424/test.esp32-ard.yaml new file mode 100644 index 0000000000..a09821b9c6 --- /dev/null +++ b/tests/components/hitachi_ac424/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac424 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac424/test.esp32-c3-ard.yaml b/tests/components/hitachi_ac424/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a09821b9c6 --- /dev/null +++ b/tests/components/hitachi_ac424/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac424 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a09821b9c6 --- /dev/null +++ b/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac424 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac424/test.esp32-idf.yaml b/tests/components/hitachi_ac424/test.esp32-idf.yaml new file mode 100644 index 0000000000..a09821b9c6 --- /dev/null +++ b/tests/components/hitachi_ac424/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac424 + name: Hitachi Climate diff --git a/tests/components/hitachi_ac424/test.esp8266-ard.yaml b/tests/components/hitachi_ac424/test.esp8266-ard.yaml new file mode 100644 index 0000000000..78b9e7c98c --- /dev/null +++ b/tests/components/hitachi_ac424/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: hitachi_ac424 + name: Hitachi Climate diff --git a/tests/components/hlw8012/test.esp32-ard.yaml b/tests/components/hlw8012/test.esp32-ard.yaml new file mode 100644 index 0000000000..5b2d865722 --- /dev/null +++ b/tests/components/hlw8012/test.esp32-ard.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: hlw8012 + model: hlw8012 + sel_pin: 12 + cf_pin: 14 + cf1_pin: 13 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE diff --git a/tests/components/hlw8012/test.esp32-c3-ard.yaml b/tests/components/hlw8012/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..da6053a1b9 --- /dev/null +++ b/tests/components/hlw8012/test.esp32-c3-ard.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: hlw8012 + model: hlw8012 + sel_pin: 2 + cf_pin: 4 + cf1_pin: 3 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE diff --git a/tests/components/hlw8012/test.esp32-c3-idf.yaml b/tests/components/hlw8012/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..da6053a1b9 --- /dev/null +++ b/tests/components/hlw8012/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: hlw8012 + model: hlw8012 + sel_pin: 2 + cf_pin: 4 + cf1_pin: 3 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE diff --git a/tests/components/hlw8012/test.esp32-idf.yaml b/tests/components/hlw8012/test.esp32-idf.yaml new file mode 100644 index 0000000000..5b2d865722 --- /dev/null +++ b/tests/components/hlw8012/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: hlw8012 + model: hlw8012 + sel_pin: 12 + cf_pin: 14 + cf1_pin: 13 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE diff --git a/tests/components/hlw8012/test.esp8266-ard.yaml b/tests/components/hlw8012/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5b2d865722 --- /dev/null +++ b/tests/components/hlw8012/test.esp8266-ard.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: hlw8012 + model: hlw8012 + sel_pin: 12 + cf_pin: 14 + cf1_pin: 13 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE diff --git a/tests/components/hlw8012/test.rp2040-ard.yaml b/tests/components/hlw8012/test.rp2040-ard.yaml new file mode 100644 index 0000000000..da6053a1b9 --- /dev/null +++ b/tests/components/hlw8012/test.rp2040-ard.yaml @@ -0,0 +1,21 @@ +sensor: + - platform: hlw8012 + model: hlw8012 + sel_pin: 2 + cf_pin: 4 + cf1_pin: 3 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE diff --git a/tests/components/hm3301/test.esp32-ard.yaml b/tests/components/hm3301/test.esp32-ard.yaml new file mode 100644 index 0000000000..413e88a265 --- /dev/null +++ b/tests/components/hm3301/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_hm3301 + scl: 16 + sda: 17 + +sensor: + - platform: hm3301 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 + aqi: + name: AQI + calculation_type: CAQI diff --git a/tests/components/hm3301/test.esp32-c3-ard.yaml b/tests/components/hm3301/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..eb6c4a14e6 --- /dev/null +++ b/tests/components/hm3301/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_hm3301 + scl: 5 + sda: 4 + +sensor: + - platform: hm3301 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 + aqi: + name: AQI + calculation_type: CAQI diff --git a/tests/components/hm3301/test.esp32-c3-idf.yaml b/tests/components/hm3301/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..eb6c4a14e6 --- /dev/null +++ b/tests/components/hm3301/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_hm3301 + scl: 5 + sda: 4 + +sensor: + - platform: hm3301 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 + aqi: + name: AQI + calculation_type: CAQI diff --git a/tests/components/hm3301/test.esp32-idf.yaml b/tests/components/hm3301/test.esp32-idf.yaml new file mode 100644 index 0000000000..413e88a265 --- /dev/null +++ b/tests/components/hm3301/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_hm3301 + scl: 16 + sda: 17 + +sensor: + - platform: hm3301 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 + aqi: + name: AQI + calculation_type: CAQI diff --git a/tests/components/hm3301/test.esp8266-ard.yaml b/tests/components/hm3301/test.esp8266-ard.yaml new file mode 100644 index 0000000000..eb6c4a14e6 --- /dev/null +++ b/tests/components/hm3301/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_hm3301 + scl: 5 + sda: 4 + +sensor: + - platform: hm3301 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 + aqi: + name: AQI + calculation_type: CAQI diff --git a/tests/components/hm3301/test.rp2040-ard.yaml b/tests/components/hm3301/test.rp2040-ard.yaml new file mode 100644 index 0000000000..eb6c4a14e6 --- /dev/null +++ b/tests/components/hm3301/test.rp2040-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_hm3301 + scl: 5 + sda: 4 + +sensor: + - platform: hm3301 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 + aqi: + name: AQI + calculation_type: CAQI diff --git a/tests/components/hmc5883l/test.esp32-ard.yaml b/tests/components/hmc5883l/test.esp32-ard.yaml new file mode 100644 index 0000000000..db632fc411 --- /dev/null +++ b/tests/components/hmc5883l/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_hmc5883l + scl: 16 + sda: 17 + +sensor: + - platform: hmc5883l + address: 0x68 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z + heading: + name: HMC5883L Heading + range: 130uT + oversampling: 8x + update_interval: 15s diff --git a/tests/components/hmc5883l/test.esp32-c3-ard.yaml b/tests/components/hmc5883l/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e65b2e5c5b --- /dev/null +++ b/tests/components/hmc5883l/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_hmc5883l + scl: 5 + sda: 4 + +sensor: + - platform: hmc5883l + address: 0x68 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z + heading: + name: HMC5883L Heading + range: 130uT + oversampling: 8x + update_interval: 15s diff --git a/tests/components/hmc5883l/test.esp32-c3-idf.yaml b/tests/components/hmc5883l/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e65b2e5c5b --- /dev/null +++ b/tests/components/hmc5883l/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_hmc5883l + scl: 5 + sda: 4 + +sensor: + - platform: hmc5883l + address: 0x68 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z + heading: + name: HMC5883L Heading + range: 130uT + oversampling: 8x + update_interval: 15s diff --git a/tests/components/hmc5883l/test.esp32-idf.yaml b/tests/components/hmc5883l/test.esp32-idf.yaml new file mode 100644 index 0000000000..db632fc411 --- /dev/null +++ b/tests/components/hmc5883l/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_hmc5883l + scl: 16 + sda: 17 + +sensor: + - platform: hmc5883l + address: 0x68 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z + heading: + name: HMC5883L Heading + range: 130uT + oversampling: 8x + update_interval: 15s diff --git a/tests/components/hmc5883l/test.esp8266-ard.yaml b/tests/components/hmc5883l/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e65b2e5c5b --- /dev/null +++ b/tests/components/hmc5883l/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_hmc5883l + scl: 5 + sda: 4 + +sensor: + - platform: hmc5883l + address: 0x68 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z + heading: + name: HMC5883L Heading + range: 130uT + oversampling: 8x + update_interval: 15s diff --git a/tests/components/hmc5883l/test.rp2040-ard.yaml b/tests/components/hmc5883l/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e65b2e5c5b --- /dev/null +++ b/tests/components/hmc5883l/test.rp2040-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_hmc5883l + scl: 5 + sda: 4 + +sensor: + - platform: hmc5883l + address: 0x68 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z + heading: + name: HMC5883L Heading + range: 130uT + oversampling: 8x + update_interval: 15s diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml new file mode 100644 index 0000000000..9c6cb71b8b --- /dev/null +++ b/tests/components/homeassistant/common.yaml @@ -0,0 +1,98 @@ +esphome: + on_boot: + then: + - homeassistant.event: + event: esphome.button_pressed + data: + message: Button was pressed + - homeassistant.event: + event: esphome.html5 + data: + message: New Humidity + data_template: + message: The humidity is {{ my_variable }}%. + variables: + my_variable: "return id(ha_hello_world_temperature).state;" + - homeassistant.action: + action: notify.html5 + data: + message: Button was pressed + - homeassistant.action: + action: notify.html5 + data: + title: New Humidity + data_template: + message: The humidity is {{ my_variable }}%. + variables: + my_variable: "return id(ha_hello_world_temperature).state;" + +wifi: + ssid: MySSID + password: password1 + +api: + +switch: + - platform: homeassistant + entity_id: automation.my_cool_automation + id: my_cool_automation + - platform: homeassistant + entity_id: fan.my_cool_fan + id: my_cool_fan + - platform: homeassistant + entity_id: humidifier.my_cool_humidifier + id: my_cool_humidifier + - platform: homeassistant + entity_id: input_boolean.my_cool_input_boolean + id: my_cool_input_boolean + - platform: homeassistant + entity_id: light.my_cool_light + id: my_cool_light + - platform: homeassistant + entity_id: remote.my_cool_remote + id: my_cool_remote + - platform: homeassistant + entity_id: siren.my_cool_siren + id: my_cool_siren + - platform: homeassistant + entity_id: switch.my_cool_switch + id: my_cool_switch + +binary_sensor: + - platform: homeassistant + entity_id: binary_sensor.hello_world + id: ha_hello_world_binary + - platform: homeassistant + entity_id: binary_sensor.hello + attribute: world + id: ha_hello_world_binary_attribute + +number: + - platform: homeassistant + entity_id: number.hello_world + id: ha_hello_world_number + +sensor: + - platform: homeassistant + entity_id: sensor.hello_world + id: ha_hello_world + - platform: homeassistant + entity_id: climate.living_room + attribute: temperature + id: ha_hello_world_temperature + +text_sensor: + - platform: homeassistant + entity_id: sensor.hello_world + id: ha_hello_world_text + - platform: homeassistant + entity_id: sensor.hello_world1 + id: ha_hello_world_text2 + attribute: some_attribute + +time: + - platform: homeassistant + on_time: + - at: "16:00:00" + then: + - logger.log: It's 16:00 diff --git a/tests/components/homeassistant/test.bk72xx-ard.yaml b/tests/components/homeassistant/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/homeassistant/test.esp32-ard.yaml b/tests/components/homeassistant/test.esp32-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.esp32-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/homeassistant/test.esp32-c3-ard.yaml b/tests/components/homeassistant/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.esp32-c3-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/homeassistant/test.esp32-c3-idf.yaml b/tests/components/homeassistant/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.esp32-c3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/homeassistant/test.esp32-idf.yaml b/tests/components/homeassistant/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/homeassistant/test.esp8266-ard.yaml b/tests/components/homeassistant/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/homeassistant/test.rp2040-ard.yaml b/tests/components/homeassistant/test.rp2040-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/test.esp32-ard.yaml b/tests/components/honeywell_hih_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..0119aec3f3 --- /dev/null +++ b/tests/components/honeywell_hih_i2c/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_honeywell_hih + scl: 16 + sda: 17 + +sensor: + - platform: honeywell_hih_i2c + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/honeywell_hih_i2c/test.esp32-c3-ard.yaml b/tests/components/honeywell_hih_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5dae3d5d52 --- /dev/null +++ b/tests/components/honeywell_hih_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_honeywell_hih + scl: 5 + sda: 4 + +sensor: + - platform: honeywell_hih_i2c + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml b/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5dae3d5d52 --- /dev/null +++ b/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_honeywell_hih + scl: 5 + sda: 4 + +sensor: + - platform: honeywell_hih_i2c + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/honeywell_hih_i2c/test.esp32-idf.yaml b/tests/components/honeywell_hih_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..0119aec3f3 --- /dev/null +++ b/tests/components/honeywell_hih_i2c/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_honeywell_hih + scl: 16 + sda: 17 + +sensor: + - platform: honeywell_hih_i2c + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/honeywell_hih_i2c/test.esp8266-ard.yaml b/tests/components/honeywell_hih_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5dae3d5d52 --- /dev/null +++ b/tests/components/honeywell_hih_i2c/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_honeywell_hih + scl: 5 + sda: 4 + +sensor: + - platform: honeywell_hih_i2c + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/honeywell_hih_i2c/test.rp2040-ard.yaml b/tests/components/honeywell_hih_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5dae3d5d52 --- /dev/null +++ b/tests/components/honeywell_hih_i2c/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_honeywell_hih + scl: 5 + sda: 4 + +sensor: + - platform: honeywell_hih_i2c + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/honeywellabp/test.esp32-ard.yaml b/tests/components/honeywellabp/test.esp32-ard.yaml new file mode 100644 index 0000000000..6bf9fa0f4d --- /dev/null +++ b/tests/components/honeywellabp/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_bme280 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: honeywellabp + cs_pin: 12 + pressure: + name: Honeywell pressure + min_pressure: 0 + max_pressure: 15 + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp/test.esp32-c3-ard.yaml b/tests/components/honeywellabp/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a53e3296dd --- /dev/null +++ b/tests/components/honeywellabp/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_honeywellabp + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: honeywellabp + cs_pin: 8 + pressure: + name: Honeywell pressure + min_pressure: 0 + max_pressure: 15 + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp/test.esp32-c3-idf.yaml b/tests/components/honeywellabp/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a53e3296dd --- /dev/null +++ b/tests/components/honeywellabp/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_honeywellabp + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: honeywellabp + cs_pin: 8 + pressure: + name: Honeywell pressure + min_pressure: 0 + max_pressure: 15 + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp/test.esp32-idf.yaml b/tests/components/honeywellabp/test.esp32-idf.yaml new file mode 100644 index 0000000000..6bf9fa0f4d --- /dev/null +++ b/tests/components/honeywellabp/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_bme280 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: honeywellabp + cs_pin: 12 + pressure: + name: Honeywell pressure + min_pressure: 0 + max_pressure: 15 + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp/test.esp8266-ard.yaml b/tests/components/honeywellabp/test.esp8266-ard.yaml new file mode 100644 index 0000000000..31988c035e --- /dev/null +++ b/tests/components/honeywellabp/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_bme280 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: honeywellabp + cs_pin: 15 + pressure: + name: Honeywell pressure + min_pressure: 0 + max_pressure: 15 + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp/test.rp2040-ard.yaml b/tests/components/honeywellabp/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2e0c471fa0 --- /dev/null +++ b/tests/components/honeywellabp/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_bme280 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: honeywellabp + cs_pin: 6 + pressure: + name: Honeywell pressure + min_pressure: 0 + max_pressure: 15 + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp2_i2c/test.esp32-ard.yaml b/tests/components/honeywellabp2_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..0f0d61ca06 --- /dev/null +++ b/tests/components/honeywellabp2_i2c/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_honeywellabp2 + scl: 16 + sda: 17 + +sensor: + - platform: honeywellabp2_i2c + address: 0x28 + pressure: + name: Honeywell2 pressure + min_pressure: 0 + max_pressure: 16000 + transfer_function: A + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp2_i2c/test.esp32-c3-ard.yaml b/tests/components/honeywellabp2_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b47411c238 --- /dev/null +++ b/tests/components/honeywellabp2_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_honeywellabp2 + scl: 5 + sda: 4 + +sensor: + - platform: honeywellabp2_i2c + address: 0x28 + pressure: + name: Honeywell2 pressure + min_pressure: 0 + max_pressure: 16000 + transfer_function: A + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml b/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b47411c238 --- /dev/null +++ b/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_honeywellabp2 + scl: 5 + sda: 4 + +sensor: + - platform: honeywellabp2_i2c + address: 0x28 + pressure: + name: Honeywell2 pressure + min_pressure: 0 + max_pressure: 16000 + transfer_function: A + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp2_i2c/test.esp32-idf.yaml b/tests/components/honeywellabp2_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..0f0d61ca06 --- /dev/null +++ b/tests/components/honeywellabp2_i2c/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_honeywellabp2 + scl: 16 + sda: 17 + +sensor: + - platform: honeywellabp2_i2c + address: 0x28 + pressure: + name: Honeywell2 pressure + min_pressure: 0 + max_pressure: 16000 + transfer_function: A + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp2_i2c/test.esp8266-ard.yaml b/tests/components/honeywellabp2_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b47411c238 --- /dev/null +++ b/tests/components/honeywellabp2_i2c/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_honeywellabp2 + scl: 5 + sda: 4 + +sensor: + - platform: honeywellabp2_i2c + address: 0x28 + pressure: + name: Honeywell2 pressure + min_pressure: 0 + max_pressure: 16000 + transfer_function: A + temperature: + name: Honeywell temperature diff --git a/tests/components/honeywellabp2_i2c/test.rp2040-ard.yaml b/tests/components/honeywellabp2_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b47411c238 --- /dev/null +++ b/tests/components/honeywellabp2_i2c/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_honeywellabp2 + scl: 5 + sda: 4 + +sensor: + - platform: honeywellabp2_i2c + address: 0x28 + pressure: + name: Honeywell2 pressure + min_pressure: 0 + max_pressure: 16000 + transfer_function: A + temperature: + name: Honeywell temperature diff --git a/tests/components/host/common.yaml b/tests/components/host/common.yaml new file mode 100644 index 0000000000..fca0c5d597 --- /dev/null +++ b/tests/components/host/common.yaml @@ -0,0 +1,10 @@ +time: + - platform: host + id: esptime + timezone: Australia/Sydney + +logger: + level: VERBOSE + +host: + mac_address: "62:23:45:AF:B3:DD" diff --git a/tests/components/host/test.host.yaml b/tests/components/host/test.host.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/host/test.host.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-ard.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-ard.yaml new file mode 100644 index 0000000000..da283cc072 --- /dev/null +++ b/tests/components/hrxl_maxsonar_wr/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_hrxl_maxsonar_wr + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +sensor: + - platform: hrxl_maxsonar_wr + id: hrxl_maxsonar_wr_sensor + name: Rainwater Tank Level diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-ard.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..729cb96120 --- /dev/null +++ b/tests/components/hrxl_maxsonar_wr/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_hrxl_maxsonar_wr + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: hrxl_maxsonar_wr + id: hrxl_maxsonar_wr_sensor + name: Rainwater Tank Level diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..729cb96120 --- /dev/null +++ b/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_hrxl_maxsonar_wr + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: hrxl_maxsonar_wr + id: hrxl_maxsonar_wr_sensor + name: Rainwater Tank Level diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-idf.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-idf.yaml new file mode 100644 index 0000000000..da283cc072 --- /dev/null +++ b/tests/components/hrxl_maxsonar_wr/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_hrxl_maxsonar_wr + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +sensor: + - platform: hrxl_maxsonar_wr + id: hrxl_maxsonar_wr_sensor + name: Rainwater Tank Level diff --git a/tests/components/hrxl_maxsonar_wr/test.esp8266-ard.yaml b/tests/components/hrxl_maxsonar_wr/test.esp8266-ard.yaml new file mode 100644 index 0000000000..729cb96120 --- /dev/null +++ b/tests/components/hrxl_maxsonar_wr/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_hrxl_maxsonar_wr + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: hrxl_maxsonar_wr + id: hrxl_maxsonar_wr_sensor + name: Rainwater Tank Level diff --git a/tests/components/hrxl_maxsonar_wr/test.rp2040-ard.yaml b/tests/components/hrxl_maxsonar_wr/test.rp2040-ard.yaml new file mode 100644 index 0000000000..729cb96120 --- /dev/null +++ b/tests/components/hrxl_maxsonar_wr/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_hrxl_maxsonar_wr + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: hrxl_maxsonar_wr + id: hrxl_maxsonar_wr_sensor + name: Rainwater Tank Level diff --git a/tests/components/hte501/test.esp32-ard.yaml b/tests/components/hte501/test.esp32-ard.yaml new file mode 100644 index 0000000000..83e4d26603 --- /dev/null +++ b/tests/components/hte501/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hte501 + scl: 16 + sda: 17 + +sensor: + - platform: hte501 + address: 0x40 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hte501/test.esp32-c3-ard.yaml b/tests/components/hte501/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e14b589dbd --- /dev/null +++ b/tests/components/hte501/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hte501 + scl: 5 + sda: 4 + +sensor: + - platform: hte501 + address: 0x40 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hte501/test.esp32-c3-idf.yaml b/tests/components/hte501/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e14b589dbd --- /dev/null +++ b/tests/components/hte501/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hte501 + scl: 5 + sda: 4 + +sensor: + - platform: hte501 + address: 0x40 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hte501/test.esp32-idf.yaml b/tests/components/hte501/test.esp32-idf.yaml new file mode 100644 index 0000000000..83e4d26603 --- /dev/null +++ b/tests/components/hte501/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hte501 + scl: 16 + sda: 17 + +sensor: + - platform: hte501 + address: 0x40 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hte501/test.esp8266-ard.yaml b/tests/components/hte501/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e14b589dbd --- /dev/null +++ b/tests/components/hte501/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hte501 + scl: 5 + sda: 4 + +sensor: + - platform: hte501 + address: 0x40 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hte501/test.rp2040-ard.yaml b/tests/components/hte501/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e14b589dbd --- /dev/null +++ b/tests/components/hte501/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_hte501 + scl: 5 + sda: 4 + +sensor: + - platform: hte501 + address: 0x40 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml new file mode 100644 index 0000000000..593b85e435 --- /dev/null +++ b/tests/components/http_request/common.yaml @@ -0,0 +1,83 @@ +substitutions: + verify_ssl: "true" + +wifi: + ssid: MySSID + password: password1 + +esphome: + on_boot: + then: + - http_request.get: + url: https://esphome.io + headers: + Content-Type: application/json + on_error: + logger.log: "Request failed" + on_response: + then: + - logger.log: + format: "Response status: %d, Duration: %lu ms" + args: + - response->status_code + - (long) response->duration_ms + - http_request.post: + url: https://esphome.io + headers: + Content-Type: application/json + json: + key: value + - http_request.send: + method: PUT + url: https://esphome.io + headers: + Content-Type: application/json + body: "Some data" + +http_request: + useragent: esphome/tagreader + timeout: 10s + verify_ssl: ${verify_ssl} + +ota: + - platform: http_request + on_begin: + then: + - logger.log: "OTA start" + on_progress: + then: + - logger.log: + format: "OTA progress %0.1f%%" + args: ["x"] + on_end: + then: + - logger.log: "OTA end" + on_error: + then: + - logger.log: + format: "OTA update error %d" + args: ["x"] + on_state_change: + then: + lambda: 'ESP_LOGD("ota", "State %d", state);' + +button: + - platform: template + name: Firmware update + on_press: + then: + - ota.http_request.flash: + md5_url: http://my.ha.net:8123/local/esphome/firmware.md5 + url: http://my.ha.net:8123/local/esphome/firmware.bin + + - ota.http_request.flash: + md5: 0123456789abcdef0123456789abcdef + url: http://my.ha.net:8123/local/esphome/firmware.bin + + - logger.log: "This message should be not displayed (reboot)" + +update: + - platform: http_request + name: OTA Update + id: ota_update + source: http://my.ha.net:8123/local/esphome/manifest.json diff --git a/tests/components/http_request/test-nossl.esp8266-ard.yaml b/tests/components/http_request/test-nossl.esp8266-ard.yaml new file mode 100644 index 0000000000..9fc4706c89 --- /dev/null +++ b/tests/components/http_request/test-nossl.esp8266-ard.yaml @@ -0,0 +1,4 @@ +<<: !include common.yaml + +http_request: + esp8266_disable_ssl_support: true diff --git a/tests/components/http_request/test.esp32-ard.yaml b/tests/components/http_request/test.esp32-ard.yaml new file mode 100644 index 0000000000..c1937b5a10 --- /dev/null +++ b/tests/components/http_request/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "false" + +<<: !include common.yaml diff --git a/tests/components/http_request/test.esp32-c3-ard.yaml b/tests/components/http_request/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c1937b5a10 --- /dev/null +++ b/tests/components/http_request/test.esp32-c3-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "false" + +<<: !include common.yaml diff --git a/tests/components/http_request/test.esp32-c3-idf.yaml b/tests/components/http_request/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2f5aa59b --- /dev/null +++ b/tests/components/http_request/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "true" + +<<: !include common.yaml diff --git a/tests/components/http_request/test.esp32-idf.yaml b/tests/components/http_request/test.esp32-idf.yaml new file mode 100644 index 0000000000..ee2f5aa59b --- /dev/null +++ b/tests/components/http_request/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "true" + +<<: !include common.yaml diff --git a/tests/components/http_request/test.esp8266-ard.yaml b/tests/components/http_request/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c1937b5a10 --- /dev/null +++ b/tests/components/http_request/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "false" + +<<: !include common.yaml diff --git a/tests/components/http_request/test.rp2040-ard.yaml b/tests/components/http_request/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c1937b5a10 --- /dev/null +++ b/tests/components/http_request/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "false" + +<<: !include common.yaml diff --git a/tests/components/htu21d/test.esp32-ard.yaml b/tests/components/htu21d/test.esp32-ard.yaml new file mode 100644 index 0000000000..6655a1cc1a --- /dev/null +++ b/tests/components/htu21d/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_htu21d + scl: 16 + sda: 17 + +sensor: + - platform: htu21d + model: htu21d + temperature: + name: Temperature + humidity: + name: Humidity + heater: + name: Heater + update_interval: 15s diff --git a/tests/components/htu21d/test.esp32-c3-ard.yaml b/tests/components/htu21d/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8131d13661 --- /dev/null +++ b/tests/components/htu21d/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_htu21d + scl: 5 + sda: 4 + +sensor: + - platform: htu21d + model: htu21d + temperature: + name: Temperature + humidity: + name: Humidity + heater: + name: Heater + update_interval: 15s diff --git a/tests/components/htu21d/test.esp32-c3-idf.yaml b/tests/components/htu21d/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8131d13661 --- /dev/null +++ b/tests/components/htu21d/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_htu21d + scl: 5 + sda: 4 + +sensor: + - platform: htu21d + model: htu21d + temperature: + name: Temperature + humidity: + name: Humidity + heater: + name: Heater + update_interval: 15s diff --git a/tests/components/htu21d/test.esp32-idf.yaml b/tests/components/htu21d/test.esp32-idf.yaml new file mode 100644 index 0000000000..6655a1cc1a --- /dev/null +++ b/tests/components/htu21d/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_htu21d + scl: 16 + sda: 17 + +sensor: + - platform: htu21d + model: htu21d + temperature: + name: Temperature + humidity: + name: Humidity + heater: + name: Heater + update_interval: 15s diff --git a/tests/components/htu21d/test.esp8266-ard.yaml b/tests/components/htu21d/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8131d13661 --- /dev/null +++ b/tests/components/htu21d/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_htu21d + scl: 5 + sda: 4 + +sensor: + - platform: htu21d + model: htu21d + temperature: + name: Temperature + humidity: + name: Humidity + heater: + name: Heater + update_interval: 15s diff --git a/tests/components/htu21d/test.rp2040-ard.yaml b/tests/components/htu21d/test.rp2040-ard.yaml new file mode 100644 index 0000000000..8131d13661 --- /dev/null +++ b/tests/components/htu21d/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_htu21d + scl: 5 + sda: 4 + +sensor: + - platform: htu21d + model: htu21d + temperature: + name: Temperature + humidity: + name: Humidity + heater: + name: Heater + update_interval: 15s diff --git a/tests/components/htu31d/common.yaml b/tests/components/htu31d/common.yaml new file mode 100644 index 0000000000..c8ef2fede9 --- /dev/null +++ b/tests/components/htu31d/common.yaml @@ -0,0 +1,9 @@ +i2c: + +sensor: + - platform: htu31d + temperature: + name: Living Room Temperature 7 + humidity: + name: Living Room Humidity 7 + update_interval: 15s diff --git a/tests/components/htu31d/test.esp32-ard.yaml b/tests/components/htu31d/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/htu31d/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/htu31d/test.esp32-idf.yaml b/tests/components/htu31d/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/htu31d/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/htu31d/test.esp8266-ard.yaml b/tests/components/htu31d/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/htu31d/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hx711/test.esp32-ard.yaml b/tests/components/hx711/test.esp32-ard.yaml new file mode 100644 index 0000000000..554b184422 --- /dev/null +++ b/tests/components/hx711/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hx711 + name: HX711 Value + dout_pin: 14 + clk_pin: 15 + gain: 128 + update_interval: 15s diff --git a/tests/components/hx711/test.esp32-c3-ard.yaml b/tests/components/hx711/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9850417440 --- /dev/null +++ b/tests/components/hx711/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hx711 + name: HX711 Value + dout_pin: 4 + clk_pin: 5 + gain: 128 + update_interval: 15s diff --git a/tests/components/hx711/test.esp32-c3-idf.yaml b/tests/components/hx711/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9850417440 --- /dev/null +++ b/tests/components/hx711/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hx711 + name: HX711 Value + dout_pin: 4 + clk_pin: 5 + gain: 128 + update_interval: 15s diff --git a/tests/components/hx711/test.esp32-idf.yaml b/tests/components/hx711/test.esp32-idf.yaml new file mode 100644 index 0000000000..554b184422 --- /dev/null +++ b/tests/components/hx711/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hx711 + name: HX711 Value + dout_pin: 14 + clk_pin: 15 + gain: 128 + update_interval: 15s diff --git a/tests/components/hx711/test.esp8266-ard.yaml b/tests/components/hx711/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9850417440 --- /dev/null +++ b/tests/components/hx711/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hx711 + name: HX711 Value + dout_pin: 4 + clk_pin: 5 + gain: 128 + update_interval: 15s diff --git a/tests/components/hx711/test.rp2040-ard.yaml b/tests/components/hx711/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9850417440 --- /dev/null +++ b/tests/components/hx711/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hx711 + name: HX711 Value + dout_pin: 4 + clk_pin: 5 + gain: 128 + update_interval: 15s diff --git a/tests/components/hydreon_rgxx/test.esp32-ard.yaml b/tests/components/hydreon_rgxx/test.esp32-ard.yaml new file mode 100644 index 0000000000..b6f9486d86 --- /dev/null +++ b/tests/components/hydreon_rgxx/test.esp32-ard.yaml @@ -0,0 +1,37 @@ +uart: + - id: uart_hydreon_rgxx + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +binary_sensor: + - platform: hydreon_rgxx + hydreon_rgxx_id: hydreon_rg9 + too_cold: + name: rg9_toocold + em_sat: + name: rg9_emsat + lens_bad: + name: rg9_lens_bad + +sensor: + - platform: hydreon_rgxx + id: hydreon_rg9 + model: RG 9 + moisture: + name: hydreon_rain + id: hydreon_rain + temperature: + name: hydreon_temperature + disable_led: true + - platform: hydreon_rgxx + id: hydreon_rg15 + model: RG_15 + acc: + name: hydreon_acc + event_acc: + name: hydreon_event_acc + total_acc: + name: hydreon_total_acc + r_int: + name: hydreon_r_int diff --git a/tests/components/hydreon_rgxx/test.esp32-c3-ard.yaml b/tests/components/hydreon_rgxx/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f62f4942db --- /dev/null +++ b/tests/components/hydreon_rgxx/test.esp32-c3-ard.yaml @@ -0,0 +1,37 @@ +uart: + - id: uart_hydreon_rgxx + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +binary_sensor: + - platform: hydreon_rgxx + hydreon_rgxx_id: hydreon_rg9 + too_cold: + name: rg9_toocold + em_sat: + name: rg9_emsat + lens_bad: + name: rg9_lens_bad + +sensor: + - platform: hydreon_rgxx + id: hydreon_rg9 + model: RG 9 + moisture: + name: hydreon_rain + id: hydreon_rain + temperature: + name: hydreon_temperature + disable_led: true + - platform: hydreon_rgxx + id: hydreon_rg15 + model: RG_15 + acc: + name: hydreon_acc + event_acc: + name: hydreon_event_acc + total_acc: + name: hydreon_total_acc + r_int: + name: hydreon_r_int diff --git a/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml b/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f62f4942db --- /dev/null +++ b/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml @@ -0,0 +1,37 @@ +uart: + - id: uart_hydreon_rgxx + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +binary_sensor: + - platform: hydreon_rgxx + hydreon_rgxx_id: hydreon_rg9 + too_cold: + name: rg9_toocold + em_sat: + name: rg9_emsat + lens_bad: + name: rg9_lens_bad + +sensor: + - platform: hydreon_rgxx + id: hydreon_rg9 + model: RG 9 + moisture: + name: hydreon_rain + id: hydreon_rain + temperature: + name: hydreon_temperature + disable_led: true + - platform: hydreon_rgxx + id: hydreon_rg15 + model: RG_15 + acc: + name: hydreon_acc + event_acc: + name: hydreon_event_acc + total_acc: + name: hydreon_total_acc + r_int: + name: hydreon_r_int diff --git a/tests/components/hydreon_rgxx/test.esp32-idf.yaml b/tests/components/hydreon_rgxx/test.esp32-idf.yaml new file mode 100644 index 0000000000..b6f9486d86 --- /dev/null +++ b/tests/components/hydreon_rgxx/test.esp32-idf.yaml @@ -0,0 +1,37 @@ +uart: + - id: uart_hydreon_rgxx + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +binary_sensor: + - platform: hydreon_rgxx + hydreon_rgxx_id: hydreon_rg9 + too_cold: + name: rg9_toocold + em_sat: + name: rg9_emsat + lens_bad: + name: rg9_lens_bad + +sensor: + - platform: hydreon_rgxx + id: hydreon_rg9 + model: RG 9 + moisture: + name: hydreon_rain + id: hydreon_rain + temperature: + name: hydreon_temperature + disable_led: true + - platform: hydreon_rgxx + id: hydreon_rg15 + model: RG_15 + acc: + name: hydreon_acc + event_acc: + name: hydreon_event_acc + total_acc: + name: hydreon_total_acc + r_int: + name: hydreon_r_int diff --git a/tests/components/hydreon_rgxx/test.esp8266-ard.yaml b/tests/components/hydreon_rgxx/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f62f4942db --- /dev/null +++ b/tests/components/hydreon_rgxx/test.esp8266-ard.yaml @@ -0,0 +1,37 @@ +uart: + - id: uart_hydreon_rgxx + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +binary_sensor: + - platform: hydreon_rgxx + hydreon_rgxx_id: hydreon_rg9 + too_cold: + name: rg9_toocold + em_sat: + name: rg9_emsat + lens_bad: + name: rg9_lens_bad + +sensor: + - platform: hydreon_rgxx + id: hydreon_rg9 + model: RG 9 + moisture: + name: hydreon_rain + id: hydreon_rain + temperature: + name: hydreon_temperature + disable_led: true + - platform: hydreon_rgxx + id: hydreon_rg15 + model: RG_15 + acc: + name: hydreon_acc + event_acc: + name: hydreon_event_acc + total_acc: + name: hydreon_total_acc + r_int: + name: hydreon_r_int diff --git a/tests/components/hydreon_rgxx/test.rp2040-ard.yaml b/tests/components/hydreon_rgxx/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f62f4942db --- /dev/null +++ b/tests/components/hydreon_rgxx/test.rp2040-ard.yaml @@ -0,0 +1,37 @@ +uart: + - id: uart_hydreon_rgxx + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +binary_sensor: + - platform: hydreon_rgxx + hydreon_rgxx_id: hydreon_rg9 + too_cold: + name: rg9_toocold + em_sat: + name: rg9_emsat + lens_bad: + name: rg9_lens_bad + +sensor: + - platform: hydreon_rgxx + id: hydreon_rg9 + model: RG 9 + moisture: + name: hydreon_rain + id: hydreon_rain + temperature: + name: hydreon_temperature + disable_led: true + - platform: hydreon_rgxx + id: hydreon_rg15 + model: RG_15 + acc: + name: hydreon_acc + event_acc: + name: hydreon_event_acc + total_acc: + name: hydreon_total_acc + r_int: + name: hydreon_r_int diff --git a/tests/components/hyt271/test.esp32-ard.yaml b/tests/components/hyt271/test.esp32-ard.yaml new file mode 100644 index 0000000000..297611a89b --- /dev/null +++ b/tests/components/hyt271/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_hyt271 + scl: 16 + sda: 17 + +sensor: + - platform: hyt271 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hyt271/test.esp32-c3-ard.yaml b/tests/components/hyt271/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c44f0a1f5d --- /dev/null +++ b/tests/components/hyt271/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_hyt271 + scl: 5 + sda: 4 + +sensor: + - platform: hyt271 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hyt271/test.esp32-c3-idf.yaml b/tests/components/hyt271/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c44f0a1f5d --- /dev/null +++ b/tests/components/hyt271/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_hyt271 + scl: 5 + sda: 4 + +sensor: + - platform: hyt271 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hyt271/test.esp32-idf.yaml b/tests/components/hyt271/test.esp32-idf.yaml new file mode 100644 index 0000000000..297611a89b --- /dev/null +++ b/tests/components/hyt271/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_hyt271 + scl: 16 + sda: 17 + +sensor: + - platform: hyt271 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hyt271/test.esp8266-ard.yaml b/tests/components/hyt271/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c44f0a1f5d --- /dev/null +++ b/tests/components/hyt271/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_hyt271 + scl: 5 + sda: 4 + +sensor: + - platform: hyt271 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hyt271/test.rp2040-ard.yaml b/tests/components/hyt271/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c44f0a1f5d --- /dev/null +++ b/tests/components/hyt271/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_hyt271 + scl: 5 + sda: 4 + +sensor: + - platform: hyt271 + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/i2c/test.esp32-ard.yaml b/tests/components/i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..19114a9e5d --- /dev/null +++ b/tests/components/i2c/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +i2c: + - id: i2c_i2c + scl: 16 + sda: 17 diff --git a/tests/components/i2c/test.esp32-c3-ard.yaml b/tests/components/i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a881438faa --- /dev/null +++ b/tests/components/i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,4 @@ +i2c: + - id: i2c_i2c + scl: 5 + sda: 4 diff --git a/tests/components/i2c/test.esp32-c3-idf.yaml b/tests/components/i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a881438faa --- /dev/null +++ b/tests/components/i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +i2c: + - id: i2c_i2c + scl: 5 + sda: 4 diff --git a/tests/components/i2c/test.esp32-idf.yaml b/tests/components/i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..19114a9e5d --- /dev/null +++ b/tests/components/i2c/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +i2c: + - id: i2c_i2c + scl: 16 + sda: 17 diff --git a/tests/components/i2c/test.esp8266-ard.yaml b/tests/components/i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a881438faa --- /dev/null +++ b/tests/components/i2c/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +i2c: + - id: i2c_i2c + scl: 5 + sda: 4 diff --git a/tests/components/i2c/test.rp2040-ard.yaml b/tests/components/i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a881438faa --- /dev/null +++ b/tests/components/i2c/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +i2c: + - id: i2c_i2c + scl: 5 + sda: 4 diff --git a/tests/components/i2c_device/test.esp32-ard.yaml b/tests/components/i2c_device/test.esp32-ard.yaml new file mode 100644 index 0000000000..6169d113f8 --- /dev/null +++ b/tests/components/i2c_device/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_i2c + scl: 16 + sda: 17 + +i2c_device: + id: i2cdev + address: 0x2C diff --git a/tests/components/i2c_device/test.esp32-c3-ard.yaml b/tests/components/i2c_device/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5d53d12208 --- /dev/null +++ b/tests/components/i2c_device/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_i2c + scl: 5 + sda: 4 + +i2c_device: + id: i2cdev + address: 0x2C diff --git a/tests/components/i2c_device/test.esp32-c3-idf.yaml b/tests/components/i2c_device/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5d53d12208 --- /dev/null +++ b/tests/components/i2c_device/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_i2c + scl: 5 + sda: 4 + +i2c_device: + id: i2cdev + address: 0x2C diff --git a/tests/components/i2c_device/test.esp32-idf.yaml b/tests/components/i2c_device/test.esp32-idf.yaml new file mode 100644 index 0000000000..6169d113f8 --- /dev/null +++ b/tests/components/i2c_device/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_i2c + scl: 16 + sda: 17 + +i2c_device: + id: i2cdev + address: 0x2C diff --git a/tests/components/i2c_device/test.esp8266-ard.yaml b/tests/components/i2c_device/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5d53d12208 --- /dev/null +++ b/tests/components/i2c_device/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_i2c + scl: 5 + sda: 4 + +i2c_device: + id: i2cdev + address: 0x2C diff --git a/tests/components/i2c_device/test.rp2040-ard.yaml b/tests/components/i2c_device/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5d53d12208 --- /dev/null +++ b/tests/components/i2c_device/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_i2c + scl: 5 + sda: 4 + +i2c_device: + id: i2cdev + address: 0x2C diff --git a/tests/components/i2s_audio/test.esp32-ard.yaml b/tests/components/i2s_audio/test.esp32-ard.yaml new file mode 100644 index 0000000000..938dd5c25f --- /dev/null +++ b/tests/components/i2s_audio/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +i2s_audio: + i2s_bclk_pin: 27 + i2s_lrclk_pin: 26 + i2s_mclk_pin: 25 diff --git a/tests/components/i2s_audio/test.esp32-c3-ard.yaml b/tests/components/i2s_audio/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..08cd56b1a7 --- /dev/null +++ b/tests/components/i2s_audio/test.esp32-c3-ard.yaml @@ -0,0 +1,4 @@ +i2s_audio: + i2s_bclk_pin: 7 + i2s_lrclk_pin: 6 + i2s_mclk_pin: 5 diff --git a/tests/components/i2s_audio/test.esp32-c3-idf.yaml b/tests/components/i2s_audio/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..08cd56b1a7 --- /dev/null +++ b/tests/components/i2s_audio/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +i2s_audio: + i2s_bclk_pin: 7 + i2s_lrclk_pin: 6 + i2s_mclk_pin: 5 diff --git a/tests/components/i2s_audio/test.esp32-idf.yaml b/tests/components/i2s_audio/test.esp32-idf.yaml new file mode 100644 index 0000000000..938dd5c25f --- /dev/null +++ b/tests/components/i2s_audio/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +i2s_audio: + i2s_bclk_pin: 27 + i2s_lrclk_pin: 26 + i2s_mclk_pin: 25 diff --git a/tests/components/iaqcore/test.esp32-ard.yaml b/tests/components/iaqcore/test.esp32-ard.yaml new file mode 100644 index 0000000000..26b01dadf9 --- /dev/null +++ b/tests/components/iaqcore/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_iaqcore + scl: 16 + sda: 17 + +sensor: + - platform: iaqcore + co2: + name: iAQ Core CO2 Sensor + tvoc: + name: iAQ Core TVOC Sensor diff --git a/tests/components/iaqcore/test.esp32-c3-ard.yaml b/tests/components/iaqcore/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a1809dffd7 --- /dev/null +++ b/tests/components/iaqcore/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_iaqcore + scl: 5 + sda: 4 + +sensor: + - platform: iaqcore + co2: + name: iAQ Core CO2 Sensor + tvoc: + name: iAQ Core TVOC Sensor diff --git a/tests/components/iaqcore/test.esp32-c3-idf.yaml b/tests/components/iaqcore/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a1809dffd7 --- /dev/null +++ b/tests/components/iaqcore/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_iaqcore + scl: 5 + sda: 4 + +sensor: + - platform: iaqcore + co2: + name: iAQ Core CO2 Sensor + tvoc: + name: iAQ Core TVOC Sensor diff --git a/tests/components/iaqcore/test.esp32-idf.yaml b/tests/components/iaqcore/test.esp32-idf.yaml new file mode 100644 index 0000000000..26b01dadf9 --- /dev/null +++ b/tests/components/iaqcore/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_iaqcore + scl: 16 + sda: 17 + +sensor: + - platform: iaqcore + co2: + name: iAQ Core CO2 Sensor + tvoc: + name: iAQ Core TVOC Sensor diff --git a/tests/components/iaqcore/test.esp8266-ard.yaml b/tests/components/iaqcore/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a1809dffd7 --- /dev/null +++ b/tests/components/iaqcore/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_iaqcore + scl: 5 + sda: 4 + +sensor: + - platform: iaqcore + co2: + name: iAQ Core CO2 Sensor + tvoc: + name: iAQ Core TVOC Sensor diff --git a/tests/components/iaqcore/test.rp2040-ard.yaml b/tests/components/iaqcore/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a1809dffd7 --- /dev/null +++ b/tests/components/iaqcore/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_iaqcore + scl: 5 + sda: 4 + +sensor: + - platform: iaqcore + co2: + name: iAQ Core CO2 Sensor + tvoc: + name: iAQ Core TVOC Sensor diff --git a/tests/components/ili9xxx/test.esp32-ard.yaml b/tests/components/ili9xxx/test.esp32-ard.yaml new file mode 100644 index 0000000000..850273230a --- /dev/null +++ b/tests/components/ili9xxx/test.esp32-ard.yaml @@ -0,0 +1,35 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + +display: + - platform: ili9xxx + invert_colors: true + dimensions: 320x240 + transform: + swap_xy: true + mirror_x: true + mirror_y: false + model: TFT 2.4 + color_palette: GRAYSCALE + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ili9xxx + invert_colors: false + dimensions: + width: 320 + height: 240 + offset_width: 20 + offset_height: 10 + model: TFT 2.4 + cs_pin: 25 + dc_pin: 26 + reset_pin: 27 + auto_clear_enabled: false + rotation: 90 + lambda: |- + it.fill(Color::WHITE); diff --git a/tests/components/ili9xxx/test.esp32-c3-ard.yaml b/tests/components/ili9xxx/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fd03bd54b7 --- /dev/null +++ b/tests/components/ili9xxx/test.esp32-c3-ard.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + invert_colors: true + dimensions: 320x240 + transform: + swap_xy: true + mirror_x: true + mirror_y: false + model: TFT 2.4 + color_palette: GRAYSCALE + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ili9xxx + invert_colors: false + dimensions: + width: 320 + height: 240 + offset_width: 20 + offset_height: 10 + model: TFT 2.4 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + auto_clear_enabled: false + rotation: 90 + lambda: |- + it.fill(Color::WHITE); diff --git a/tests/components/ili9xxx/test.esp32-c3-idf.yaml b/tests/components/ili9xxx/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fd03bd54b7 --- /dev/null +++ b/tests/components/ili9xxx/test.esp32-c3-idf.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + invert_colors: true + dimensions: 320x240 + transform: + swap_xy: true + mirror_x: true + mirror_y: false + model: TFT 2.4 + color_palette: GRAYSCALE + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ili9xxx + invert_colors: false + dimensions: + width: 320 + height: 240 + offset_width: 20 + offset_height: 10 + model: TFT 2.4 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + auto_clear_enabled: false + rotation: 90 + lambda: |- + it.fill(Color::WHITE); diff --git a/tests/components/ili9xxx/test.esp32-idf.yaml b/tests/components/ili9xxx/test.esp32-idf.yaml new file mode 100644 index 0000000000..6da62f69d2 --- /dev/null +++ b/tests/components/ili9xxx/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + invert_colors: true + dimensions: 320x240 + transform: + swap_xy: true + mirror_x: true + mirror_y: false + model: custom + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + init_sequence: + - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x10] + + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/ili9xxx/test.esp8266-ard.yaml b/tests/components/ili9xxx/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b8192e69d1 --- /dev/null +++ b/tests/components/ili9xxx/test.esp8266-ard.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + invert_colors: true + dimensions: 320x240 + transform: + swap_xy: true + mirror_x: true + mirror_y: false + model: TFT 2.4 + color_palette: GRAYSCALE + cs_pin: 5 + dc_pin: 15 + reset_pin: 16 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ili9xxx + invert_colors: false + dimensions: + width: 320 + height: 240 + offset_width: 20 + offset_height: 10 + model: TFT 2.4 + cs_pin: 2 + dc_pin: 4 + reset_pin: 0 + auto_clear_enabled: false + rotation: 90 + lambda: |- + it.fill(Color::WHITE); diff --git a/tests/components/ili9xxx/test.rp2040-ard.yaml b/tests/components/ili9xxx/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0423f41a1c --- /dev/null +++ b/tests/components/ili9xxx/test.rp2040-ard.yaml @@ -0,0 +1,36 @@ +spi: + - id: spi_main_lcd + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ili9xxx + invert_colors: true + dimensions: 320x240 + transform: + swap_xy: true + mirror_x: true + mirror_y: false + model: TFT 2.4 + color_palette: GRAYSCALE + cs_pin: 5 + dc_pin: 15 + reset_pin: 16 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ili9xxx + invert_colors: false + dimensions: + width: 320 + height: 240 + offset_width: 20 + offset_height: 10 + model: TFT 2.4 + cs_pin: 20 + dc_pin: 21 + reset_pin: 22 + auto_clear_enabled: false + rotation: 90 + lambda: |- + it.fill(Color::WHITE); diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml new file mode 100644 index 0000000000..313da6bc0b --- /dev/null +++ b/tests/components/image/common.yaml @@ -0,0 +1,38 @@ +image: + - id: binary_image + file: ../../pnglogo.png + type: BINARY + dither: FloydSteinberg + - id: transparent_transparent_image + file: ../../pnglogo.png + type: TRANSPARENT_BINARY + - id: rgba_image + file: ../../pnglogo.png + type: RGBA + resize: 50x50 + - id: rgb24_image + file: ../../pnglogo.png + type: RGB24 + use_transparency: yes + - id: rgb565_image + file: ../../pnglogo.png + type: RGB565 + use_transparency: no + - id: web_svg_image + file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + resize: 256x48 + type: TRANSPARENT_BINARY + - id: web_tiff_image + file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + type: RGB24 + resize: 48x48 + - id: web_redirect_image + file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + type: RGB24 + resize: 48x48 + - id: mdi_alert + file: mdi:alert-circle-outline + resize: 50x50 + - id: another_alert_icon + file: mdi:alert-outline + type: BINARY diff --git a/tests/components/image/test.esp32-ard.yaml b/tests/components/image/test.esp32-ard.yaml new file mode 100644 index 0000000000..818e720221 --- /dev/null +++ b/tests/components/image/test.esp32-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 32 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 14 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + +<<: !include common.yaml + diff --git a/tests/components/image/test.esp32-c3-ard.yaml b/tests/components/image/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4dae9cd5ec --- /dev/null +++ b/tests/components/image/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 3 + dc_pin: 11 + reset_pin: 10 + invert_colors: true + +<<: !include common.yaml diff --git a/tests/components/image/test.esp32-c3-idf.yaml b/tests/components/image/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4dae9cd5ec --- /dev/null +++ b/tests/components/image/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 3 + dc_pin: 11 + reset_pin: 10 + invert_colors: true + +<<: !include common.yaml diff --git a/tests/components/image/test.esp32-idf.yaml b/tests/components/image/test.esp32-idf.yaml new file mode 100644 index 0000000000..814f16c36c --- /dev/null +++ b/tests/components/image/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 18 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 19 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + +<<: !include common.yaml diff --git a/tests/components/image/test.esp8266-ard.yaml b/tests/components/image/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f963022ff4 --- /dev/null +++ b/tests/components/image/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 5 + dc_pin: 15 + reset_pin: 16 + invert_colors: true + +<<: !include common.yaml diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml new file mode 100644 index 0000000000..29509db66c --- /dev/null +++ b/tests/components/image/test.host.yaml @@ -0,0 +1,8 @@ +display: + - platform: sdl + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +<<: !include common.yaml diff --git a/tests/components/image/test.rp2040-ard.yaml b/tests/components/image/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5167c99a7d --- /dev/null +++ b/tests/components/image/test.rp2040-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_main_lcd + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 21 + reset_pin: 22 + invert_colors: true + +<<: !include common.yaml diff --git a/tests/components/improv_serial/common-uart0.yaml b/tests/components/improv_serial/common-uart0.yaml new file mode 100644 index 0000000000..7b7730fd46 --- /dev/null +++ b/tests/components/improv_serial/common-uart0.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +logger: + hardware_uart: UART0 + +improv_serial: diff --git a/tests/components/improv_serial/common-usb_cdc.yaml b/tests/components/improv_serial/common-usb_cdc.yaml new file mode 100644 index 0000000000..fc72b6aa7e --- /dev/null +++ b/tests/components/improv_serial/common-usb_cdc.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +logger: + hardware_uart: USB_CDC + +improv_serial: diff --git a/tests/components/improv_serial/common-usb_serial_jtag.yaml b/tests/components/improv_serial/common-usb_serial_jtag.yaml new file mode 100644 index 0000000000..0abcc07168 --- /dev/null +++ b/tests/components/improv_serial/common-usb_serial_jtag.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +logger: + hardware_uart: USB_SERIAL_JTAG + +improv_serial: diff --git a/tests/components/improv_serial/test-uart0.esp32-ard.yaml b/tests/components/improv_serial/test-uart0.esp32-ard.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-c3-ard.yaml b/tests/components/improv_serial/test-uart0.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-c3-idf.yaml b/tests/components/improv_serial/test-uart0.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-idf.yaml b/tests/components/improv_serial/test-uart0.esp32-idf.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-s2-ard.yaml b/tests/components/improv_serial/test-uart0.esp32-s2-ard.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-s2-idf.yaml b/tests/components/improv_serial/test-uart0.esp32-s2-idf.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-s3-ard.yaml b/tests/components/improv_serial/test-uart0.esp32-s3-ard.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-s3-idf.yaml b/tests/components/improv_serial/test-uart0.esp32-s3-idf.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp8266-ard.yaml b/tests/components/improv_serial/test-uart0.esp8266-ard.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.rp2040-ard.yaml b/tests/components/improv_serial/test-uart0.rp2040-ard.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_serial/test-uart0.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.esp32-c3-ard.yaml b/tests/components/improv_serial/test-usb_cdc.esp32-c3-ard.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/improv_serial/test-usb_cdc.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.esp32-s2-ard.yaml b/tests/components/improv_serial/test-usb_cdc.esp32-s2-ard.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/improv_serial/test-usb_cdc.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.esp32-s2-idf.yaml b/tests/components/improv_serial/test-usb_cdc.esp32-s2-idf.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/improv_serial/test-usb_cdc.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.esp32-s3-ard.yaml b/tests/components/improv_serial/test-usb_cdc.esp32-s3-ard.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/improv_serial/test-usb_cdc.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.rp2040-ard.yaml b/tests/components/improv_serial/test-usb_cdc.rp2040-ard.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/improv_serial/test-usb_cdc.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_serial_jtag.esp32-c3-idf.yaml b/tests/components/improv_serial/test-usb_serial_jtag.esp32-c3-idf.yaml new file mode 100644 index 0000000000..46a927ff10 --- /dev/null +++ b/tests/components/improv_serial/test-usb_serial_jtag.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-usb_serial_jtag.yaml diff --git a/tests/components/improv_serial/test-usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/improv_serial/test-usb_serial_jtag.esp32-s3-idf.yaml new file mode 100644 index 0000000000..46a927ff10 --- /dev/null +++ b/tests/components/improv_serial/test-usb_serial_jtag.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-usb_serial_jtag.yaml diff --git a/tests/components/ina219/test.esp32-ard.yaml b/tests/components/ina219/test.esp32-ard.yaml new file mode 100644 index 0000000000..affbec67c4 --- /dev/null +++ b/tests/components/ina219/test.esp32-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ina219 + scl: 16 + sda: 17 + +sensor: + - platform: ina219 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA219 Current + power: + name: INA219 Power + bus_voltage: + name: INA219 Bus Voltage + shunt_voltage: + name: INA219 Shunt Voltage + max_voltage: 32.0V + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina219/test.esp32-c3-ard.yaml b/tests/components/ina219/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..586add9d16 --- /dev/null +++ b/tests/components/ina219/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ina219 + scl: 5 + sda: 4 + +sensor: + - platform: ina219 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA219 Current + power: + name: INA219 Power + bus_voltage: + name: INA219 Bus Voltage + shunt_voltage: + name: INA219 Shunt Voltage + max_voltage: 32.0V + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina219/test.esp32-c3-idf.yaml b/tests/components/ina219/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..586add9d16 --- /dev/null +++ b/tests/components/ina219/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ina219 + scl: 5 + sda: 4 + +sensor: + - platform: ina219 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA219 Current + power: + name: INA219 Power + bus_voltage: + name: INA219 Bus Voltage + shunt_voltage: + name: INA219 Shunt Voltage + max_voltage: 32.0V + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina219/test.esp32-idf.yaml b/tests/components/ina219/test.esp32-idf.yaml new file mode 100644 index 0000000000..affbec67c4 --- /dev/null +++ b/tests/components/ina219/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ina219 + scl: 16 + sda: 17 + +sensor: + - platform: ina219 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA219 Current + power: + name: INA219 Power + bus_voltage: + name: INA219 Bus Voltage + shunt_voltage: + name: INA219 Shunt Voltage + max_voltage: 32.0V + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina219/test.esp8266-ard.yaml b/tests/components/ina219/test.esp8266-ard.yaml new file mode 100644 index 0000000000..586add9d16 --- /dev/null +++ b/tests/components/ina219/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ina219 + scl: 5 + sda: 4 + +sensor: + - platform: ina219 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA219 Current + power: + name: INA219 Power + bus_voltage: + name: INA219 Bus Voltage + shunt_voltage: + name: INA219 Shunt Voltage + max_voltage: 32.0V + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina219/test.rp2040-ard.yaml b/tests/components/ina219/test.rp2040-ard.yaml new file mode 100644 index 0000000000..586add9d16 --- /dev/null +++ b/tests/components/ina219/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ina219 + scl: 5 + sda: 4 + +sensor: + - platform: ina219 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA219 Current + power: + name: INA219 Power + bus_voltage: + name: INA219 Bus Voltage + shunt_voltage: + name: INA219 Shunt Voltage + max_voltage: 32.0V + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina226/test.esp32-ard.yaml b/tests/components/ina226/test.esp32-ard.yaml new file mode 100644 index 0000000000..feab5e146c --- /dev/null +++ b/tests/components/ina226/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina226 + scl: 16 + sda: 17 + +sensor: + - platform: ina226 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA226 Current + power: + name: INA226 Power + bus_voltage: + name: INA226 Bus Voltage + shunt_voltage: + name: INA226 Shunt Voltage + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina226/test.esp32-c3-ard.yaml b/tests/components/ina226/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..6581763294 --- /dev/null +++ b/tests/components/ina226/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina226 + scl: 5 + sda: 4 + +sensor: + - platform: ina226 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA226 Current + power: + name: INA226 Power + bus_voltage: + name: INA226 Bus Voltage + shunt_voltage: + name: INA226 Shunt Voltage + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina226/test.esp32-c3-idf.yaml b/tests/components/ina226/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6581763294 --- /dev/null +++ b/tests/components/ina226/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina226 + scl: 5 + sda: 4 + +sensor: + - platform: ina226 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA226 Current + power: + name: INA226 Power + bus_voltage: + name: INA226 Bus Voltage + shunt_voltage: + name: INA226 Shunt Voltage + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina226/test.esp32-idf.yaml b/tests/components/ina226/test.esp32-idf.yaml new file mode 100644 index 0000000000..feab5e146c --- /dev/null +++ b/tests/components/ina226/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina226 + scl: 16 + sda: 17 + +sensor: + - platform: ina226 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA226 Current + power: + name: INA226 Power + bus_voltage: + name: INA226 Bus Voltage + shunt_voltage: + name: INA226 Shunt Voltage + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina226/test.esp8266-ard.yaml b/tests/components/ina226/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6581763294 --- /dev/null +++ b/tests/components/ina226/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina226 + scl: 5 + sda: 4 + +sensor: + - platform: ina226 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA226 Current + power: + name: INA226 Power + bus_voltage: + name: INA226 Bus Voltage + shunt_voltage: + name: INA226 Shunt Voltage + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina226/test.rp2040-ard.yaml b/tests/components/ina226/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6581763294 --- /dev/null +++ b/tests/components/ina226/test.rp2040-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina226 + scl: 5 + sda: 4 + +sensor: + - platform: ina226 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: INA226 Current + power: + name: INA226 Power + bus_voltage: + name: INA226 Bus Voltage + shunt_voltage: + name: INA226 Shunt Voltage + max_current: 3.2A + update_interval: 15s diff --git a/tests/components/ina260/test.esp32-ard.yaml b/tests/components/ina260/test.esp32-ard.yaml new file mode 100644 index 0000000000..be6cf73bff --- /dev/null +++ b/tests/components/ina260/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_ina260 + scl: 16 + sda: 17 + +sensor: + - platform: ina260 + address: 0x40 + current: + name: INA260 Current + power: + name: INA260 Power + bus_voltage: + name: INA260 Voltage + update_interval: 60s diff --git a/tests/components/ina260/test.esp32-c3-ard.yaml b/tests/components/ina260/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a1da63351d --- /dev/null +++ b/tests/components/ina260/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_ina260 + scl: 5 + sda: 4 + +sensor: + - platform: ina260 + address: 0x40 + current: + name: INA260 Current + power: + name: INA260 Power + bus_voltage: + name: INA260 Voltage + update_interval: 60s diff --git a/tests/components/ina260/test.esp32-c3-idf.yaml b/tests/components/ina260/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a1da63351d --- /dev/null +++ b/tests/components/ina260/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_ina260 + scl: 5 + sda: 4 + +sensor: + - platform: ina260 + address: 0x40 + current: + name: INA260 Current + power: + name: INA260 Power + bus_voltage: + name: INA260 Voltage + update_interval: 60s diff --git a/tests/components/ina260/test.esp32-idf.yaml b/tests/components/ina260/test.esp32-idf.yaml new file mode 100644 index 0000000000..be6cf73bff --- /dev/null +++ b/tests/components/ina260/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_ina260 + scl: 16 + sda: 17 + +sensor: + - platform: ina260 + address: 0x40 + current: + name: INA260 Current + power: + name: INA260 Power + bus_voltage: + name: INA260 Voltage + update_interval: 60s diff --git a/tests/components/ina260/test.esp8266-ard.yaml b/tests/components/ina260/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a1da63351d --- /dev/null +++ b/tests/components/ina260/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_ina260 + scl: 5 + sda: 4 + +sensor: + - platform: ina260 + address: 0x40 + current: + name: INA260 Current + power: + name: INA260 Power + bus_voltage: + name: INA260 Voltage + update_interval: 60s diff --git a/tests/components/ina260/test.rp2040-ard.yaml b/tests/components/ina260/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a1da63351d --- /dev/null +++ b/tests/components/ina260/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_ina260 + scl: 5 + sda: 4 + +sensor: + - platform: ina260 + address: 0x40 + current: + name: INA260 Current + power: + name: INA260 Power + bus_voltage: + name: INA260 Voltage + update_interval: 60s diff --git a/tests/components/ina2xx_i2c/common.yaml b/tests/components/ina2xx_i2c/common.yaml new file mode 100644 index 0000000000..320b680b6b --- /dev/null +++ b/tests/components/ina2xx_i2c/common.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ina2xx + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: ina2xx_i2c + i2c_id: i2c_ina2xx + address: 0x40 + model: INA228 + shunt_resistance: 0.001130 ohm + max_current: 40 A + adc_range: 1 + temperature_coefficient: 50 + shunt_voltage: "INA2xx Shunt Voltage" + bus_voltage: "INA2xx Bus Voltage" + current: "INA2xx Current" + power: "INA2xx Power" + energy: "INA2xx Energy" + charge: "INA2xx Charge" diff --git a/tests/components/ina2xx_i2c/test.esp32-ard.yaml b/tests/components/ina2xx_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/ina2xx_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp32-c3-ard.yaml b/tests/components/ina2xx_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/ina2xx_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml b/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp32-idf.yaml b/tests/components/ina2xx_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/ina2xx_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp8266-ard.yaml b/tests/components/ina2xx_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/ina2xx_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.rp2040-ard.yaml b/tests/components/ina2xx_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/ina2xx_i2c/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/common.yaml b/tests/components/ina2xx_spi/common.yaml new file mode 100644 index 0000000000..3eab7e6f0a --- /dev/null +++ b/tests/components/ina2xx_spi/common.yaml @@ -0,0 +1,21 @@ +spi: + - id: spi_ina2xx + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +sensor: + - platform: ina2xx_spi + spi_id: spi_ina2xx + cs_pin: ${cs_pin} + model: INA229 + shunt_resistance: 0.001130 ohm + max_current: 40 A + adc_range: 1 + temperature_coefficient: 50 + shunt_voltage: "INA2xx Shunt Voltage" + bus_voltage: "INA2xx Bus Voltage" + current: "INA2xx Current" + power: "INA2xx Power" + energy: "INA2xx Energy" + charge: "INA2xx Charge" diff --git a/tests/components/ina2xx_spi/test.esp32-ard.yaml b/tests/components/ina2xx_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/ina2xx_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp32-c3-ard.yaml b/tests/components/ina2xx_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/ina2xx_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml b/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp32-idf.yaml b/tests/components/ina2xx_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/ina2xx_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp8266-ard.yaml b/tests/components/ina2xx_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dbd158d030 --- /dev/null +++ b/tests/components/ina2xx_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO14 + mosi_pin: GPIO13 + miso_pin: GPIO12 + cs_pin: GPIO15 + +<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.rp2040-ard.yaml b/tests/components/ina2xx_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f6c3f1eeca --- /dev/null +++ b/tests/components/ina2xx_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ina3221/test.esp32-ard.yaml b/tests/components/ina3221/test.esp32-ard.yaml new file mode 100644 index 0000000000..ad9cf79e38 --- /dev/null +++ b/tests/components/ina3221/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina3221 + scl: 16 + sda: 17 + +sensor: + - platform: ina3221 + address: 0x40 + channel_1: + shunt_resistance: 0.1 ohm + current: + name: INA3221 Channel 1 Current + power: + name: INA3221 Channel 1 Power + bus_voltage: + name: INA3221 Channel 1 Bus Voltage + shunt_voltage: + name: INA3221 Channel 1 Shunt Voltage + update_interval: 15s diff --git a/tests/components/ina3221/test.esp32-c3-ard.yaml b/tests/components/ina3221/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..55990871a0 --- /dev/null +++ b/tests/components/ina3221/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina3221 + scl: 5 + sda: 4 + +sensor: + - platform: ina3221 + address: 0x40 + channel_1: + shunt_resistance: 0.1 ohm + current: + name: INA3221 Channel 1 Current + power: + name: INA3221 Channel 1 Power + bus_voltage: + name: INA3221 Channel 1 Bus Voltage + shunt_voltage: + name: INA3221 Channel 1 Shunt Voltage + update_interval: 15s diff --git a/tests/components/ina3221/test.esp32-c3-idf.yaml b/tests/components/ina3221/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..55990871a0 --- /dev/null +++ b/tests/components/ina3221/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina3221 + scl: 5 + sda: 4 + +sensor: + - platform: ina3221 + address: 0x40 + channel_1: + shunt_resistance: 0.1 ohm + current: + name: INA3221 Channel 1 Current + power: + name: INA3221 Channel 1 Power + bus_voltage: + name: INA3221 Channel 1 Bus Voltage + shunt_voltage: + name: INA3221 Channel 1 Shunt Voltage + update_interval: 15s diff --git a/tests/components/ina3221/test.esp32-idf.yaml b/tests/components/ina3221/test.esp32-idf.yaml new file mode 100644 index 0000000000..ad9cf79e38 --- /dev/null +++ b/tests/components/ina3221/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina3221 + scl: 16 + sda: 17 + +sensor: + - platform: ina3221 + address: 0x40 + channel_1: + shunt_resistance: 0.1 ohm + current: + name: INA3221 Channel 1 Current + power: + name: INA3221 Channel 1 Power + bus_voltage: + name: INA3221 Channel 1 Bus Voltage + shunt_voltage: + name: INA3221 Channel 1 Shunt Voltage + update_interval: 15s diff --git a/tests/components/ina3221/test.esp8266-ard.yaml b/tests/components/ina3221/test.esp8266-ard.yaml new file mode 100644 index 0000000000..55990871a0 --- /dev/null +++ b/tests/components/ina3221/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina3221 + scl: 5 + sda: 4 + +sensor: + - platform: ina3221 + address: 0x40 + channel_1: + shunt_resistance: 0.1 ohm + current: + name: INA3221 Channel 1 Current + power: + name: INA3221 Channel 1 Power + bus_voltage: + name: INA3221 Channel 1 Bus Voltage + shunt_voltage: + name: INA3221 Channel 1 Shunt Voltage + update_interval: 15s diff --git a/tests/components/ina3221/test.rp2040-ard.yaml b/tests/components/ina3221/test.rp2040-ard.yaml new file mode 100644 index 0000000000..55990871a0 --- /dev/null +++ b/tests/components/ina3221/test.rp2040-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_ina3221 + scl: 5 + sda: 4 + +sensor: + - platform: ina3221 + address: 0x40 + channel_1: + shunt_resistance: 0.1 ohm + current: + name: INA3221 Channel 1 Current + power: + name: INA3221 Channel 1 Power + bus_voltage: + name: INA3221 Channel 1 Bus Voltage + shunt_voltage: + name: INA3221 Channel 1 Shunt Voltage + update_interval: 15s diff --git a/tests/components/inkbird_ibsth1_mini/common.yaml b/tests/components/inkbird_ibsth1_mini/common.yaml new file mode 100644 index 0000000000..ba46b7dbf6 --- /dev/null +++ b/tests/components/inkbird_ibsth1_mini/common.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +sensor: + - platform: inkbird_ibsth1_mini + mac_address: 38:81:D7:0A:9C:11 + temperature: + name: Inkbird IBS-TH1 Temperature + humidity: + name: Inkbird IBS-TH1 Humidity + battery_level: + name: Inkbird IBS-TH1 Battery Level diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-ard.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/inkbird_ibsth1_mini/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-ard.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/inkbird_ibsth1_mini/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-idf.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/inkbird_ibsth1_mini/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/inkplate6/common.yaml b/tests/components/inkplate6/common.yaml new file mode 100644 index 0000000000..31b14e6c73 --- /dev/null +++ b/tests/components/inkplate6/common.yaml @@ -0,0 +1,62 @@ +i2c: + - id: i2c_inkplate6 + scl: 16 + sda: 17 + +display: + - platform: inkplate6 + id: inkplate_display + greyscale: false + partial_updating: false + update_interval: 60s + display_data_0_pin: + number: 1 + allow_other_uses: true + display_data_1_pin: + number: 1 + allow_other_uses: true + display_data_2_pin: + number: 1 + allow_other_uses: true + display_data_3_pin: + number: 1 + allow_other_uses: true + display_data_5_pin: + number: 1 + allow_other_uses: true + display_data_4_pin: + number: 1 + allow_other_uses: true + display_data_6_pin: + number: 1 + allow_other_uses: true + display_data_7_pin: + number: 1 + allow_other_uses: true + ckv_pin: + number: 1 + allow_other_uses: true + sph_pin: + number: 1 + allow_other_uses: true + gmod_pin: + number: 1 + allow_other_uses: true + gpio0_enable_pin: + number: 1 + allow_other_uses: true + oe_pin: + number: 1 + allow_other_uses: true + spv_pin: + number: 1 + allow_other_uses: true + powerup_pin: + number: 1 + allow_other_uses: true + wakeup_pin: + number: 1 + allow_other_uses: true + vcom_pin: + number: 1 + allow_other_uses: true diff --git a/tests/components/inkplate6/test.esp32-ard.yaml b/tests/components/inkplate6/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/inkplate6/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/integration/test.esp32-ard.yaml b/tests/components/integration/test.esp32-ard.yaml new file mode 100644 index 0000000000..0095fdb1ff --- /dev/null +++ b/tests/components/integration/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: my_sensor + pin: A0 + attenuation: 2.5db + - platform: integration + sensor: my_sensor + name: Integration Sensor + time_unit: s diff --git a/tests/components/integration/test.esp32-c3-ard.yaml b/tests/components/integration/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b68cb9f87d --- /dev/null +++ b/tests/components/integration/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: my_sensor + pin: 4 + attenuation: 11db + - platform: integration + sensor: my_sensor + name: Integration Sensor + time_unit: s diff --git a/tests/components/integration/test.esp32-idf.yaml b/tests/components/integration/test.esp32-idf.yaml new file mode 100644 index 0000000000..0095fdb1ff --- /dev/null +++ b/tests/components/integration/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: my_sensor + pin: A0 + attenuation: 2.5db + - platform: integration + sensor: my_sensor + name: Integration Sensor + time_unit: s diff --git a/tests/components/integration/test.esp32-s2-ard.yaml b/tests/components/integration/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..1415952571 --- /dev/null +++ b/tests/components/integration/test.esp32-s2-ard.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db + - platform: integration + sensor: my_sensor + name: Integration Sensor + time_unit: s diff --git a/tests/components/integration/test.esp32-s3-ard.yaml b/tests/components/integration/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..1415952571 --- /dev/null +++ b/tests/components/integration/test.esp32-s3-ard.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db + - platform: integration + sensor: my_sensor + name: Integration Sensor + time_unit: s diff --git a/tests/components/integration/test.esp8266-ard.yaml b/tests/components/integration/test.esp8266-ard.yaml new file mode 100644 index 0000000000..51d3e19077 --- /dev/null +++ b/tests/components/integration/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +sensor: + - platform: adc + id: my_sensor + pin: VCC + - platform: integration + sensor: my_sensor + name: Integration Sensor + time_unit: s diff --git a/tests/components/integration/test.rp2040-ard.yaml b/tests/components/integration/test.rp2040-ard.yaml new file mode 100644 index 0000000000..51d3e19077 --- /dev/null +++ b/tests/components/integration/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +sensor: + - platform: adc + id: my_sensor + pin: VCC + - platform: integration + sensor: my_sensor + name: Integration Sensor + time_unit: s diff --git a/tests/components/internal_temperature/test.bk72xx-ard.yaml b/tests/components/internal_temperature/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..28df4a6d9f --- /dev/null +++ b/tests/components/internal_temperature/test.bk72xx-ard.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: internal_temperature + name: "Internal Temperature" diff --git a/tests/components/internal_temperature/test.esp32-ard.yaml b/tests/components/internal_temperature/test.esp32-ard.yaml new file mode 100644 index 0000000000..19f740339d --- /dev/null +++ b/tests/components/internal_temperature/test.esp32-ard.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: internal_temperature + name: Internal Temperature diff --git a/tests/components/internal_temperature/test.esp32-c3-ard.yaml b/tests/components/internal_temperature/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..19f740339d --- /dev/null +++ b/tests/components/internal_temperature/test.esp32-c3-ard.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: internal_temperature + name: Internal Temperature diff --git a/tests/components/internal_temperature/test.esp32-c3-idf.yaml b/tests/components/internal_temperature/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..28df4a6d9f --- /dev/null +++ b/tests/components/internal_temperature/test.esp32-c3-idf.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: internal_temperature + name: "Internal Temperature" diff --git a/tests/components/internal_temperature/test.esp32-idf.yaml b/tests/components/internal_temperature/test.esp32-idf.yaml new file mode 100644 index 0000000000..19f740339d --- /dev/null +++ b/tests/components/internal_temperature/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: internal_temperature + name: Internal Temperature diff --git a/tests/components/internal_temperature/test.esp32-s2-ard.yaml b/tests/components/internal_temperature/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..19f740339d --- /dev/null +++ b/tests/components/internal_temperature/test.esp32-s2-ard.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: internal_temperature + name: Internal Temperature diff --git a/tests/components/internal_temperature/test.esp32-s3-ard.yaml b/tests/components/internal_temperature/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..9eb1ec0b0f --- /dev/null +++ b/tests/components/internal_temperature/test.esp32-s3-ard.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: internal_temperature + name: Internal Temperature + +esp32: + framework: + version: 2.0.9 diff --git a/tests/components/internal_temperature/test.rp2040-ard.yaml b/tests/components/internal_temperature/test.rp2040-ard.yaml new file mode 100644 index 0000000000..19f740339d --- /dev/null +++ b/tests/components/internal_temperature/test.rp2040-ard.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: internal_temperature + name: Internal Temperature diff --git a/tests/components/interval/common.yaml b/tests/components/interval/common.yaml new file mode 100644 index 0000000000..2a3c979ae2 --- /dev/null +++ b/tests/components/interval/common.yaml @@ -0,0 +1,4 @@ +interval: + - interval: 1s + then: + - logger.log: Tick diff --git a/tests/components/interval/test.esp32-ard.yaml b/tests/components/interval/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.esp32-c3-ard.yaml b/tests/components/interval/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.esp32-c3-idf.yaml b/tests/components/interval/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.esp32-idf.yaml b/tests/components/interval/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.esp8266-ard.yaml b/tests/components/interval/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.rp2040-ard.yaml b/tests/components/interval/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/jsn_sr04t/test.esp32-ard.yaml b/tests/components/jsn_sr04t/test.esp32-ard.yaml new file mode 100644 index 0000000000..32b4221b3f --- /dev/null +++ b/tests/components/jsn_sr04t/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_jsn_sr04t + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: jsn_sr04t + id: jsn_sr04t_sensor + name: "jsn_sr04t Distance" + uart_id: uart_jsn_sr04t + update_interval: 1s diff --git a/tests/components/jsn_sr04t/test.esp32-c3-ard.yaml b/tests/components/jsn_sr04t/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5a37418a7d --- /dev/null +++ b/tests/components/jsn_sr04t/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_jsn_sr04t + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: jsn_sr04t + id: jsn_sr04t_sensor + name: "jsn_sr04t Distance" + uart_id: uart_jsn_sr04t + update_interval: 1s diff --git a/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml b/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5a37418a7d --- /dev/null +++ b/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_jsn_sr04t + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: jsn_sr04t + id: jsn_sr04t_sensor + name: "jsn_sr04t Distance" + uart_id: uart_jsn_sr04t + update_interval: 1s diff --git a/tests/components/jsn_sr04t/test.esp32-idf.yaml b/tests/components/jsn_sr04t/test.esp32-idf.yaml new file mode 100644 index 0000000000..32b4221b3f --- /dev/null +++ b/tests/components/jsn_sr04t/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_jsn_sr04t + tx_pin: + number: 17 + rx_pin: + number: 16 + baud_rate: 9600 + +sensor: + - platform: jsn_sr04t + id: jsn_sr04t_sensor + name: "jsn_sr04t Distance" + uart_id: uart_jsn_sr04t + update_interval: 1s diff --git a/tests/components/jsn_sr04t/test.esp8266-ard.yaml b/tests/components/jsn_sr04t/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5a37418a7d --- /dev/null +++ b/tests/components/jsn_sr04t/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_jsn_sr04t + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: jsn_sr04t + id: jsn_sr04t_sensor + name: "jsn_sr04t Distance" + uart_id: uart_jsn_sr04t + update_interval: 1s diff --git a/tests/components/jsn_sr04t/test.rp2040-ard.yaml b/tests/components/jsn_sr04t/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5a37418a7d --- /dev/null +++ b/tests/components/jsn_sr04t/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_jsn_sr04t + tx_pin: + number: 4 + rx_pin: + number: 5 + baud_rate: 9600 + +sensor: + - platform: jsn_sr04t + id: jsn_sr04t_sensor + name: "jsn_sr04t Distance" + uart_id: uart_jsn_sr04t + update_interval: 1s diff --git a/tests/components/kamstrup_kmp/common.yaml b/tests/components/kamstrup_kmp/common.yaml new file mode 100644 index 0000000000..b348d03c72 --- /dev/null +++ b/tests/components/kamstrup_kmp/common.yaml @@ -0,0 +1,25 @@ +uart: + tx_pin: ${uart_tx_pin} + rx_pin: ${uart_rx_pin} + baud_rate: 1200 + stop_bits: 2 + +sensor: + - platform: kamstrup_kmp + heat_energy: + name: Heat Energy + power: + name: Power + temp1: + name: Temperature 1 + temp2: + name: Temperature 2 + temp_diff: + name: Temperature Difference + flow: + name: Flow + volume: + name: Volume + custom: + - name: Custom 1 + command: 0x1234 diff --git a/tests/components/kamstrup_kmp/test.esp32-ard.yaml b/tests/components/kamstrup_kmp/test.esp32-ard.yaml new file mode 100644 index 0000000000..adc2c4d24a --- /dev/null +++ b/tests/components/kamstrup_kmp/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + uart_tx_pin: GPIO1 + uart_rx_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/kamstrup_kmp/test.esp32-idf.yaml b/tests/components/kamstrup_kmp/test.esp32-idf.yaml new file mode 100644 index 0000000000..adc2c4d24a --- /dev/null +++ b/tests/components/kamstrup_kmp/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + uart_tx_pin: GPIO1 + uart_rx_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/kamstrup_kmp/test.esp8266-ard.yaml b/tests/components/kamstrup_kmp/test.esp8266-ard.yaml new file mode 100644 index 0000000000..adc2c4d24a --- /dev/null +++ b/tests/components/kamstrup_kmp/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + uart_tx_pin: GPIO1 + uart_rx_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/key_collector/test.esp32-ard.yaml b/tests/components/key_collector/test.esp32-ard.yaml new file mode 100644 index 0000000000..7cbe9c0fc1 --- /dev/null +++ b/tests/components/key_collector/test.esp32-ard.yaml @@ -0,0 +1,28 @@ +matrix_keypad: + id: keypad + rows: + - pin: 12 + - pin: 13 + columns: + - pin: 14 + - pin: 15 + keys: "1234" + has_pulldowns: true + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 + on_progress: + - logger.log: + format: "input progress: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] + on_result: + - logger.log: + format: "input result: '%s', started by '%c', ended by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + on_timeout: + - logger.log: + format: "input timeout: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] diff --git a/tests/components/key_collector/test.esp32-c3-ard.yaml b/tests/components/key_collector/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..1f133c5cd8 --- /dev/null +++ b/tests/components/key_collector/test.esp32-c3-ard.yaml @@ -0,0 +1,28 @@ +matrix_keypad: + id: keypad + rows: + - pin: 1 + - pin: 2 + columns: + - pin: 3 + - pin: 4 + keys: "1234" + has_pulldowns: true + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 + on_progress: + - logger.log: + format: "input progress: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] + on_result: + - logger.log: + format: "input result: '%s', started by '%c', ended by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + on_timeout: + - logger.log: + format: "input timeout: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] diff --git a/tests/components/key_collector/test.esp32-c3-idf.yaml b/tests/components/key_collector/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..1f133c5cd8 --- /dev/null +++ b/tests/components/key_collector/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +matrix_keypad: + id: keypad + rows: + - pin: 1 + - pin: 2 + columns: + - pin: 3 + - pin: 4 + keys: "1234" + has_pulldowns: true + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 + on_progress: + - logger.log: + format: "input progress: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] + on_result: + - logger.log: + format: "input result: '%s', started by '%c', ended by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + on_timeout: + - logger.log: + format: "input timeout: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] diff --git a/tests/components/key_collector/test.esp32-idf.yaml b/tests/components/key_collector/test.esp32-idf.yaml new file mode 100644 index 0000000000..7cbe9c0fc1 --- /dev/null +++ b/tests/components/key_collector/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +matrix_keypad: + id: keypad + rows: + - pin: 12 + - pin: 13 + columns: + - pin: 14 + - pin: 15 + keys: "1234" + has_pulldowns: true + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 + on_progress: + - logger.log: + format: "input progress: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] + on_result: + - logger.log: + format: "input result: '%s', started by '%c', ended by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + on_timeout: + - logger.log: + format: "input timeout: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] diff --git a/tests/components/key_collector/test.esp8266-ard.yaml b/tests/components/key_collector/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7cbe9c0fc1 --- /dev/null +++ b/tests/components/key_collector/test.esp8266-ard.yaml @@ -0,0 +1,28 @@ +matrix_keypad: + id: keypad + rows: + - pin: 12 + - pin: 13 + columns: + - pin: 14 + - pin: 15 + keys: "1234" + has_pulldowns: true + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 + on_progress: + - logger.log: + format: "input progress: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] + on_result: + - logger.log: + format: "input result: '%s', started by '%c', ended by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + on_timeout: + - logger.log: + format: "input timeout: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] diff --git a/tests/components/key_collector/test.rp2040-ard.yaml b/tests/components/key_collector/test.rp2040-ard.yaml new file mode 100644 index 0000000000..1f133c5cd8 --- /dev/null +++ b/tests/components/key_collector/test.rp2040-ard.yaml @@ -0,0 +1,28 @@ +matrix_keypad: + id: keypad + rows: + - pin: 1 + - pin: 2 + columns: + - pin: 3 + - pin: 4 + keys: "1234" + has_pulldowns: true + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 + on_progress: + - logger.log: + format: "input progress: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] + on_result: + - logger.log: + format: "input result: '%s', started by '%c', ended by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + on_timeout: + - logger.log: + format: "input timeout: '%s', started by '%c'" + args: ['x.c_str()', "(start == 0 ? '~' : start)"] diff --git a/tests/components/kmeteriso/test.esp32-ard.yaml b/tests/components/kmeteriso/test.esp32-ard.yaml new file mode 100644 index 0000000000..2c375dda31 --- /dev/null +++ b/tests/components/kmeteriso/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_kmeteriso + scl: 16 + sda: 17 + +sensor: + - platform: kmeteriso + temperature: + name: Outside Temperature + internal_temperature: + name: Internal Temperature + update_interval: 15s diff --git a/tests/components/kmeteriso/test.esp32-c3-ard.yaml b/tests/components/kmeteriso/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7780cfea32 --- /dev/null +++ b/tests/components/kmeteriso/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_kmeteriso + scl: 5 + sda: 4 + +sensor: + - platform: kmeteriso + temperature: + name: Outside Temperature + internal_temperature: + name: Internal Temperature + update_interval: 15s diff --git a/tests/components/kmeteriso/test.esp32-c3-idf.yaml b/tests/components/kmeteriso/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7780cfea32 --- /dev/null +++ b/tests/components/kmeteriso/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_kmeteriso + scl: 5 + sda: 4 + +sensor: + - platform: kmeteriso + temperature: + name: Outside Temperature + internal_temperature: + name: Internal Temperature + update_interval: 15s diff --git a/tests/components/kmeteriso/test.esp32-idf.yaml b/tests/components/kmeteriso/test.esp32-idf.yaml new file mode 100644 index 0000000000..2c375dda31 --- /dev/null +++ b/tests/components/kmeteriso/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_kmeteriso + scl: 16 + sda: 17 + +sensor: + - platform: kmeteriso + temperature: + name: Outside Temperature + internal_temperature: + name: Internal Temperature + update_interval: 15s diff --git a/tests/components/kmeteriso/test.esp8266-ard.yaml b/tests/components/kmeteriso/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7780cfea32 --- /dev/null +++ b/tests/components/kmeteriso/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_kmeteriso + scl: 5 + sda: 4 + +sensor: + - platform: kmeteriso + temperature: + name: Outside Temperature + internal_temperature: + name: Internal Temperature + update_interval: 15s diff --git a/tests/components/kmeteriso/test.rp2040-ard.yaml b/tests/components/kmeteriso/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7780cfea32 --- /dev/null +++ b/tests/components/kmeteriso/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_kmeteriso + scl: 5 + sda: 4 + +sensor: + - platform: kmeteriso + temperature: + name: Outside Temperature + internal_temperature: + name: Internal Temperature + update_interval: 15s diff --git a/tests/components/kuntze/test.esp32-ard.yaml b/tests/components/kuntze/test.esp32-ard.yaml new file mode 100644 index 0000000000..6b6c638971 --- /dev/null +++ b/tests/components/kuntze/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +uart: + - id: uart_kuntze + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + flow_control_pin: 13 + +sensor: + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature diff --git a/tests/components/kuntze/test.esp32-c3-ard.yaml b/tests/components/kuntze/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..08278c3c82 --- /dev/null +++ b/tests/components/kuntze/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +uart: + - id: uart_kuntze + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature diff --git a/tests/components/kuntze/test.esp32-c3-idf.yaml b/tests/components/kuntze/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..08278c3c82 --- /dev/null +++ b/tests/components/kuntze/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +uart: + - id: uart_kuntze + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature diff --git a/tests/components/kuntze/test.esp32-idf.yaml b/tests/components/kuntze/test.esp32-idf.yaml new file mode 100644 index 0000000000..6b6c638971 --- /dev/null +++ b/tests/components/kuntze/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +uart: + - id: uart_kuntze + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + flow_control_pin: 13 + +sensor: + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature diff --git a/tests/components/kuntze/test.esp8266-ard.yaml b/tests/components/kuntze/test.esp8266-ard.yaml new file mode 100644 index 0000000000..eba6cddc2d --- /dev/null +++ b/tests/components/kuntze/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +uart: + - id: uart_kuntze + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 13 + +sensor: + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature diff --git a/tests/components/kuntze/test.rp2040-ard.yaml b/tests/components/kuntze/test.rp2040-ard.yaml new file mode 100644 index 0000000000..08278c3c82 --- /dev/null +++ b/tests/components/kuntze/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +uart: + - id: uart_kuntze + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + flow_control_pin: 3 + +sensor: + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature diff --git a/tests/components/lcd_gpio/test.esp32-ard.yaml b/tests/components/lcd_gpio/test.esp32-ard.yaml new file mode 100644 index 0000000000..d2b33aeb3a --- /dev/null +++ b/tests/components/lcd_gpio/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 12 + - number: 13 + - number: 14 + - number: 15 + enable_pin: 16 + rs_pin: 5 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_gpio/test.esp32-c3-ard.yaml b/tests/components/lcd_gpio/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b89715a755 --- /dev/null +++ b/tests/components/lcd_gpio/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + enable_pin: 5 + rs_pin: 6 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_gpio/test.esp32-c3-idf.yaml b/tests/components/lcd_gpio/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b89715a755 --- /dev/null +++ b/tests/components/lcd_gpio/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + enable_pin: 5 + rs_pin: 6 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_gpio/test.esp32-idf.yaml b/tests/components/lcd_gpio/test.esp32-idf.yaml new file mode 100644 index 0000000000..d2b33aeb3a --- /dev/null +++ b/tests/components/lcd_gpio/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 12 + - number: 13 + - number: 14 + - number: 15 + enable_pin: 16 + rs_pin: 5 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_gpio/test.esp8266-ard.yaml b/tests/components/lcd_gpio/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d2b33aeb3a --- /dev/null +++ b/tests/components/lcd_gpio/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 12 + - number: 13 + - number: 14 + - number: 15 + enable_pin: 16 + rs_pin: 5 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_gpio/test.rp2040-ard.yaml b/tests/components/lcd_gpio/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b89715a755 --- /dev/null +++ b/tests/components/lcd_gpio/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + enable_pin: 5 + rs_pin: 6 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_menu/test.esp32-ard.yaml b/tests/components/lcd_menu/test.esp32-ard.yaml new file mode 100644 index 0000000000..833ea2169a --- /dev/null +++ b/tests/components/lcd_menu/test.esp32-ard.yaml @@ -0,0 +1,118 @@ +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + name: Template Switch + id: my_switch + optimistic: true + +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 12 + - number: 13 + - number: 14 + - number: 15 + enable_pin: 16 + rs_pin: 5 + lambda: |- + it.print("Hello World!"); + +lcd_menu: + id: test_lcd_menu + display_id: my_lcd_gpio + mark_back: 0x5e + mark_selected: 0x3e + mark_editing: 0x2a + mark_submenu: 0x7e + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "root leave");' + items: + - type: back + text: Back + - type: label + - type: menu + text: Submenu 1 + items: + - type: back + text: Back + - type: menu + text: Submenu 21 + items: + - type: back + text: Back + - type: command + text: Show Main + on_value: + then: + - display_menu.show_main: test_lcd_menu + - type: select + text: Enum Item + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: Number + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: Hide + on_value: + then: + - display_menu.hide: test_lcd_menu + - type: switch + text: Switch + switch: my_switch + on_text: Bright + off_text: Dark + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/lcd_menu/test.esp32-c3-ard.yaml b/tests/components/lcd_menu/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..39d2278d3d --- /dev/null +++ b/tests/components/lcd_menu/test.esp32-c3-ard.yaml @@ -0,0 +1,118 @@ +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + name: Template Switch + id: my_switch + optimistic: true + +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + enable_pin: 5 + rs_pin: 6 + lambda: |- + it.print("Hello World!"); + +lcd_menu: + id: test_lcd_menu + display_id: my_lcd_gpio + mark_back: 0x5e + mark_selected: 0x3e + mark_editing: 0x2a + mark_submenu: 0x7e + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "root leave");' + items: + - type: back + text: Back + - type: label + - type: menu + text: Submenu 1 + items: + - type: back + text: Back + - type: menu + text: Submenu 21 + items: + - type: back + text: Back + - type: command + text: Show Main + on_value: + then: + - display_menu.show_main: test_lcd_menu + - type: select + text: Enum Item + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: Number + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: Hide + on_value: + then: + - display_menu.hide: test_lcd_menu + - type: switch + text: Switch + switch: my_switch + on_text: Bright + off_text: Dark + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/lcd_menu/test.esp32-c3-idf.yaml b/tests/components/lcd_menu/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..39d2278d3d --- /dev/null +++ b/tests/components/lcd_menu/test.esp32-c3-idf.yaml @@ -0,0 +1,118 @@ +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + name: Template Switch + id: my_switch + optimistic: true + +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + enable_pin: 5 + rs_pin: 6 + lambda: |- + it.print("Hello World!"); + +lcd_menu: + id: test_lcd_menu + display_id: my_lcd_gpio + mark_back: 0x5e + mark_selected: 0x3e + mark_editing: 0x2a + mark_submenu: 0x7e + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "root leave");' + items: + - type: back + text: Back + - type: label + - type: menu + text: Submenu 1 + items: + - type: back + text: Back + - type: menu + text: Submenu 21 + items: + - type: back + text: Back + - type: command + text: Show Main + on_value: + then: + - display_menu.show_main: test_lcd_menu + - type: select + text: Enum Item + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: Number + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: Hide + on_value: + then: + - display_menu.hide: test_lcd_menu + - type: switch + text: Switch + switch: my_switch + on_text: Bright + off_text: Dark + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/lcd_menu/test.esp32-idf.yaml b/tests/components/lcd_menu/test.esp32-idf.yaml new file mode 100644 index 0000000000..833ea2169a --- /dev/null +++ b/tests/components/lcd_menu/test.esp32-idf.yaml @@ -0,0 +1,118 @@ +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + name: Template Switch + id: my_switch + optimistic: true + +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 12 + - number: 13 + - number: 14 + - number: 15 + enable_pin: 16 + rs_pin: 5 + lambda: |- + it.print("Hello World!"); + +lcd_menu: + id: test_lcd_menu + display_id: my_lcd_gpio + mark_back: 0x5e + mark_selected: 0x3e + mark_editing: 0x2a + mark_submenu: 0x7e + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "root leave");' + items: + - type: back + text: Back + - type: label + - type: menu + text: Submenu 1 + items: + - type: back + text: Back + - type: menu + text: Submenu 21 + items: + - type: back + text: Back + - type: command + text: Show Main + on_value: + then: + - display_menu.show_main: test_lcd_menu + - type: select + text: Enum Item + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: Number + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: Hide + on_value: + then: + - display_menu.hide: test_lcd_menu + - type: switch + text: Switch + switch: my_switch + on_text: Bright + off_text: Dark + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/lcd_menu/test.esp8266-ard.yaml b/tests/components/lcd_menu/test.esp8266-ard.yaml new file mode 100644 index 0000000000..833ea2169a --- /dev/null +++ b/tests/components/lcd_menu/test.esp8266-ard.yaml @@ -0,0 +1,118 @@ +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + name: Template Switch + id: my_switch + optimistic: true + +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 12 + - number: 13 + - number: 14 + - number: 15 + enable_pin: 16 + rs_pin: 5 + lambda: |- + it.print("Hello World!"); + +lcd_menu: + id: test_lcd_menu + display_id: my_lcd_gpio + mark_back: 0x5e + mark_selected: 0x3e + mark_editing: 0x2a + mark_submenu: 0x7e + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "root leave");' + items: + - type: back + text: Back + - type: label + - type: menu + text: Submenu 1 + items: + - type: back + text: Back + - type: menu + text: Submenu 21 + items: + - type: back + text: Back + - type: command + text: Show Main + on_value: + then: + - display_menu.show_main: test_lcd_menu + - type: select + text: Enum Item + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: Number + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: Hide + on_value: + then: + - display_menu.hide: test_lcd_menu + - type: switch + text: Switch + switch: my_switch + on_text: Bright + off_text: Dark + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/lcd_menu/test.rp2040-ard.yaml b/tests/components/lcd_menu/test.rp2040-ard.yaml new file mode 100644 index 0000000000..39d2278d3d --- /dev/null +++ b/tests/components/lcd_menu/test.rp2040-ard.yaml @@ -0,0 +1,118 @@ +number: + - platform: template + id: test_number + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + options: + - one + - two + optimistic: true + +switch: + - platform: template + name: Template Switch + id: my_switch + optimistic: true + +display: + - platform: lcd_gpio + id: my_lcd_gpio + dimensions: 18x4 + data_pins: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + enable_pin: 5 + rs_pin: 6 + lambda: |- + it.print("Hello World!"); + +lcd_menu: + id: test_lcd_menu + display_id: my_lcd_gpio + mark_back: 0x5e + mark_selected: 0x3e + mark_editing: 0x2a + mark_submenu: 0x7e + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "root leave");' + items: + - type: back + text: Back + - type: label + - type: menu + text: Submenu 1 + items: + - type: back + text: Back + - type: menu + text: Submenu 21 + items: + - type: back + text: Back + - type: command + text: Show Main + on_value: + then: + - display_menu.show_main: test_lcd_menu + - type: select + text: Enum Item + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: Number + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: Hide + on_value: + then: + - display_menu.hide: test_lcd_menu + - type: switch + text: Switch + switch: my_switch + on_text: Bright + off_text: Dark + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/components/lcd_pcf8574/test.esp32-ard.yaml b/tests/components/lcd_pcf8574/test.esp32-ard.yaml new file mode 100644 index 0000000000..9d7d475f30 --- /dev/null +++ b/tests/components/lcd_pcf8574/test.esp32-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_lcd_pcf8574 + scl: 16 + sda: 17 + +display: + - platform: lcd_pcf8574 + dimensions: 18x4 + address: 0x3F + user_characters: + - position: 0 + data: + - 0b00000 + - 0b01010 + - 0b00000 + - 0b00100 + - 0b00100 + - 0b10001 + - 0b01110 + - 0b00000 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_pcf8574/test.esp32-c3-ard.yaml b/tests/components/lcd_pcf8574/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..41eba26950 --- /dev/null +++ b/tests/components/lcd_pcf8574/test.esp32-c3-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_lcd_pcf8574 + scl: 5 + sda: 4 + +display: + - platform: lcd_pcf8574 + dimensions: 18x4 + address: 0x3F + user_characters: + - position: 0 + data: + - 0b00000 + - 0b01010 + - 0b00000 + - 0b00100 + - 0b00100 + - 0b10001 + - 0b01110 + - 0b00000 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml b/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..41eba26950 --- /dev/null +++ b/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_lcd_pcf8574 + scl: 5 + sda: 4 + +display: + - platform: lcd_pcf8574 + dimensions: 18x4 + address: 0x3F + user_characters: + - position: 0 + data: + - 0b00000 + - 0b01010 + - 0b00000 + - 0b00100 + - 0b00100 + - 0b10001 + - 0b01110 + - 0b00000 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_pcf8574/test.esp32-idf.yaml b/tests/components/lcd_pcf8574/test.esp32-idf.yaml new file mode 100644 index 0000000000..9d7d475f30 --- /dev/null +++ b/tests/components/lcd_pcf8574/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_lcd_pcf8574 + scl: 16 + sda: 17 + +display: + - platform: lcd_pcf8574 + dimensions: 18x4 + address: 0x3F + user_characters: + - position: 0 + data: + - 0b00000 + - 0b01010 + - 0b00000 + - 0b00100 + - 0b00100 + - 0b10001 + - 0b01110 + - 0b00000 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_pcf8574/test.esp8266-ard.yaml b/tests/components/lcd_pcf8574/test.esp8266-ard.yaml new file mode 100644 index 0000000000..41eba26950 --- /dev/null +++ b/tests/components/lcd_pcf8574/test.esp8266-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_lcd_pcf8574 + scl: 5 + sda: 4 + +display: + - platform: lcd_pcf8574 + dimensions: 18x4 + address: 0x3F + user_characters: + - position: 0 + data: + - 0b00000 + - 0b01010 + - 0b00000 + - 0b00100 + - 0b00100 + - 0b10001 + - 0b01110 + - 0b00000 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/lcd_pcf8574/test.rp2040-ard.yaml b/tests/components/lcd_pcf8574/test.rp2040-ard.yaml new file mode 100644 index 0000000000..41eba26950 --- /dev/null +++ b/tests/components/lcd_pcf8574/test.rp2040-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_lcd_pcf8574 + scl: 5 + sda: 4 + +display: + - platform: lcd_pcf8574 + dimensions: 18x4 + address: 0x3F + user_characters: + - position: 0 + data: + - 0b00000 + - 0b01010 + - 0b00000 + - 0b00100 + - 0b00100 + - 0b10001 + - 0b01110 + - 0b00000 + lambda: |- + it.print("Hello World!"); diff --git a/tests/components/ld2410/test.esp32-ard.yaml b/tests/components/ld2410/test.esp32-ard.yaml new file mode 100644 index 0000000000..48ed179d93 --- /dev/null +++ b/tests/components/ld2410/test.esp32-ard.yaml @@ -0,0 +1,169 @@ +uart: + - id: uart_ld2410 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +ld2410: + id: my_ld2410 + +binary_sensor: + - platform: ld2410 + has_target: + name: presence + has_moving_target: + name: movement + has_still_target: + name: still + out_pin_presence_status: + name: out pin presence status + +button: + - platform: ld2410 + factory_reset: + name: factory reset + restart: + name: restart + query_params: + name: query params + +number: + - platform: ld2410 + light_threshold: + name: light threshold + timeout: + name: timeout + max_move_distance_gate: + name: max move distance gate + max_still_distance_gate: + name: max still distance gate + g0: + move_threshold: + name: g0 move threshold + still_threshold: + name: g0 still threshold + g1: + move_threshold: + name: g1 move threshold + still_threshold: + name: g1 still threshold + g2: + move_threshold: + name: g2 move threshold + still_threshold: + name: g2 still threshold + g3: + move_threshold: + name: g3 move threshold + still_threshold: + name: g3 still threshold + g4: + move_threshold: + name: g4 move threshold + still_threshold: + name: g4 still threshold + g5: + move_threshold: + name: g5 move threshold + still_threshold: + name: g5 still threshold + g6: + move_threshold: + name: g6 move threshold + still_threshold: + name: g6 still threshold + g7: + move_threshold: + name: g7 move threshold + still_threshold: + name: g7 still threshold + g8: + move_threshold: + name: g8 move threshold + still_threshold: + name: g8 still threshold + +select: + - platform: ld2410 + distance_resolution: + name: distance resolution + baud_rate: + name: baud rate + light_function: + name: light function + out_pin_level: + name: out ping level + +sensor: + - platform: ld2410 + light: + name: light + moving_distance: + name: Moving distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Distance Detection + g0: + move_energy: + name: g0 move energy + still_energy: + name: g0 still energy + g1: + move_energy: + name: g1 move energy + still_energy: + name: g1 still energy + g2: + move_energy: + name: g2 move energy + still_energy: + name: g2 still energy + g3: + move_energy: + name: g3 move energy + still_energy: + name: g3 still energy + g4: + move_energy: + name: g4 move energy + still_energy: + name: g4 still energy + g5: + move_energy: + name: g5 move energy + still_energy: + name: g5 still energy + g6: + move_energy: + name: g6 move energy + still_energy: + name: g6 still energy + g7: + move_energy: + name: g7 move energy + still_energy: + name: g7 still energy + g8: + move_energy: + name: g8 move energy + still_energy: + name: g8 still energy + +switch: + - platform: ld2410 + engineering_mode: + name: control ld2410 engineering mode + bluetooth: + name: control ld2410 bluetooth + +text_sensor: + - platform: ld2410 + version: + name: presenece sensor version + mac_address: + name: presenece sensor mac address diff --git a/tests/components/ld2410/test.esp32-c3-ard.yaml b/tests/components/ld2410/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..afcaa81b3e --- /dev/null +++ b/tests/components/ld2410/test.esp32-c3-ard.yaml @@ -0,0 +1,169 @@ +uart: + - id: uart_ld2410 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +ld2410: + id: my_ld2410 + +binary_sensor: + - platform: ld2410 + has_target: + name: presence + has_moving_target: + name: movement + has_still_target: + name: still + out_pin_presence_status: + name: out pin presence status + +button: + - platform: ld2410 + factory_reset: + name: factory reset + restart: + name: restart + query_params: + name: query params + +number: + - platform: ld2410 + light_threshold: + name: light threshold + timeout: + name: timeout + max_move_distance_gate: + name: max move distance gate + max_still_distance_gate: + name: max still distance gate + g0: + move_threshold: + name: g0 move threshold + still_threshold: + name: g0 still threshold + g1: + move_threshold: + name: g1 move threshold + still_threshold: + name: g1 still threshold + g2: + move_threshold: + name: g2 move threshold + still_threshold: + name: g2 still threshold + g3: + move_threshold: + name: g3 move threshold + still_threshold: + name: g3 still threshold + g4: + move_threshold: + name: g4 move threshold + still_threshold: + name: g4 still threshold + g5: + move_threshold: + name: g5 move threshold + still_threshold: + name: g5 still threshold + g6: + move_threshold: + name: g6 move threshold + still_threshold: + name: g6 still threshold + g7: + move_threshold: + name: g7 move threshold + still_threshold: + name: g7 still threshold + g8: + move_threshold: + name: g8 move threshold + still_threshold: + name: g8 still threshold + +select: + - platform: ld2410 + distance_resolution: + name: distance resolution + baud_rate: + name: baud rate + light_function: + name: light function + out_pin_level: + name: out ping level + +sensor: + - platform: ld2410 + light: + name: light + moving_distance: + name: Moving distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Distance Detection + g0: + move_energy: + name: g0 move energy + still_energy: + name: g0 still energy + g1: + move_energy: + name: g1 move energy + still_energy: + name: g1 still energy + g2: + move_energy: + name: g2 move energy + still_energy: + name: g2 still energy + g3: + move_energy: + name: g3 move energy + still_energy: + name: g3 still energy + g4: + move_energy: + name: g4 move energy + still_energy: + name: g4 still energy + g5: + move_energy: + name: g5 move energy + still_energy: + name: g5 still energy + g6: + move_energy: + name: g6 move energy + still_energy: + name: g6 still energy + g7: + move_energy: + name: g7 move energy + still_energy: + name: g7 still energy + g8: + move_energy: + name: g8 move energy + still_energy: + name: g8 still energy + +switch: + - platform: ld2410 + engineering_mode: + name: control ld2410 engineering mode + bluetooth: + name: control ld2410 bluetooth + +text_sensor: + - platform: ld2410 + version: + name: presenece sensor version + mac_address: + name: presenece sensor mac address diff --git a/tests/components/ld2410/test.esp32-c3-idf.yaml b/tests/components/ld2410/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..afcaa81b3e --- /dev/null +++ b/tests/components/ld2410/test.esp32-c3-idf.yaml @@ -0,0 +1,169 @@ +uart: + - id: uart_ld2410 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +ld2410: + id: my_ld2410 + +binary_sensor: + - platform: ld2410 + has_target: + name: presence + has_moving_target: + name: movement + has_still_target: + name: still + out_pin_presence_status: + name: out pin presence status + +button: + - platform: ld2410 + factory_reset: + name: factory reset + restart: + name: restart + query_params: + name: query params + +number: + - platform: ld2410 + light_threshold: + name: light threshold + timeout: + name: timeout + max_move_distance_gate: + name: max move distance gate + max_still_distance_gate: + name: max still distance gate + g0: + move_threshold: + name: g0 move threshold + still_threshold: + name: g0 still threshold + g1: + move_threshold: + name: g1 move threshold + still_threshold: + name: g1 still threshold + g2: + move_threshold: + name: g2 move threshold + still_threshold: + name: g2 still threshold + g3: + move_threshold: + name: g3 move threshold + still_threshold: + name: g3 still threshold + g4: + move_threshold: + name: g4 move threshold + still_threshold: + name: g4 still threshold + g5: + move_threshold: + name: g5 move threshold + still_threshold: + name: g5 still threshold + g6: + move_threshold: + name: g6 move threshold + still_threshold: + name: g6 still threshold + g7: + move_threshold: + name: g7 move threshold + still_threshold: + name: g7 still threshold + g8: + move_threshold: + name: g8 move threshold + still_threshold: + name: g8 still threshold + +select: + - platform: ld2410 + distance_resolution: + name: distance resolution + baud_rate: + name: baud rate + light_function: + name: light function + out_pin_level: + name: out ping level + +sensor: + - platform: ld2410 + light: + name: light + moving_distance: + name: Moving distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Distance Detection + g0: + move_energy: + name: g0 move energy + still_energy: + name: g0 still energy + g1: + move_energy: + name: g1 move energy + still_energy: + name: g1 still energy + g2: + move_energy: + name: g2 move energy + still_energy: + name: g2 still energy + g3: + move_energy: + name: g3 move energy + still_energy: + name: g3 still energy + g4: + move_energy: + name: g4 move energy + still_energy: + name: g4 still energy + g5: + move_energy: + name: g5 move energy + still_energy: + name: g5 still energy + g6: + move_energy: + name: g6 move energy + still_energy: + name: g6 still energy + g7: + move_energy: + name: g7 move energy + still_energy: + name: g7 still energy + g8: + move_energy: + name: g8 move energy + still_energy: + name: g8 still energy + +switch: + - platform: ld2410 + engineering_mode: + name: control ld2410 engineering mode + bluetooth: + name: control ld2410 bluetooth + +text_sensor: + - platform: ld2410 + version: + name: presenece sensor version + mac_address: + name: presenece sensor mac address diff --git a/tests/components/ld2410/test.esp32-idf.yaml b/tests/components/ld2410/test.esp32-idf.yaml new file mode 100644 index 0000000000..48ed179d93 --- /dev/null +++ b/tests/components/ld2410/test.esp32-idf.yaml @@ -0,0 +1,169 @@ +uart: + - id: uart_ld2410 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +ld2410: + id: my_ld2410 + +binary_sensor: + - platform: ld2410 + has_target: + name: presence + has_moving_target: + name: movement + has_still_target: + name: still + out_pin_presence_status: + name: out pin presence status + +button: + - platform: ld2410 + factory_reset: + name: factory reset + restart: + name: restart + query_params: + name: query params + +number: + - platform: ld2410 + light_threshold: + name: light threshold + timeout: + name: timeout + max_move_distance_gate: + name: max move distance gate + max_still_distance_gate: + name: max still distance gate + g0: + move_threshold: + name: g0 move threshold + still_threshold: + name: g0 still threshold + g1: + move_threshold: + name: g1 move threshold + still_threshold: + name: g1 still threshold + g2: + move_threshold: + name: g2 move threshold + still_threshold: + name: g2 still threshold + g3: + move_threshold: + name: g3 move threshold + still_threshold: + name: g3 still threshold + g4: + move_threshold: + name: g4 move threshold + still_threshold: + name: g4 still threshold + g5: + move_threshold: + name: g5 move threshold + still_threshold: + name: g5 still threshold + g6: + move_threshold: + name: g6 move threshold + still_threshold: + name: g6 still threshold + g7: + move_threshold: + name: g7 move threshold + still_threshold: + name: g7 still threshold + g8: + move_threshold: + name: g8 move threshold + still_threshold: + name: g8 still threshold + +select: + - platform: ld2410 + distance_resolution: + name: distance resolution + baud_rate: + name: baud rate + light_function: + name: light function + out_pin_level: + name: out ping level + +sensor: + - platform: ld2410 + light: + name: light + moving_distance: + name: Moving distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Distance Detection + g0: + move_energy: + name: g0 move energy + still_energy: + name: g0 still energy + g1: + move_energy: + name: g1 move energy + still_energy: + name: g1 still energy + g2: + move_energy: + name: g2 move energy + still_energy: + name: g2 still energy + g3: + move_energy: + name: g3 move energy + still_energy: + name: g3 still energy + g4: + move_energy: + name: g4 move energy + still_energy: + name: g4 still energy + g5: + move_energy: + name: g5 move energy + still_energy: + name: g5 still energy + g6: + move_energy: + name: g6 move energy + still_energy: + name: g6 still energy + g7: + move_energy: + name: g7 move energy + still_energy: + name: g7 still energy + g8: + move_energy: + name: g8 move energy + still_energy: + name: g8 still energy + +switch: + - platform: ld2410 + engineering_mode: + name: control ld2410 engineering mode + bluetooth: + name: control ld2410 bluetooth + +text_sensor: + - platform: ld2410 + version: + name: presenece sensor version + mac_address: + name: presenece sensor mac address diff --git a/tests/components/ld2410/test.esp8266-ard.yaml b/tests/components/ld2410/test.esp8266-ard.yaml new file mode 100644 index 0000000000..afcaa81b3e --- /dev/null +++ b/tests/components/ld2410/test.esp8266-ard.yaml @@ -0,0 +1,169 @@ +uart: + - id: uart_ld2410 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +ld2410: + id: my_ld2410 + +binary_sensor: + - platform: ld2410 + has_target: + name: presence + has_moving_target: + name: movement + has_still_target: + name: still + out_pin_presence_status: + name: out pin presence status + +button: + - platform: ld2410 + factory_reset: + name: factory reset + restart: + name: restart + query_params: + name: query params + +number: + - platform: ld2410 + light_threshold: + name: light threshold + timeout: + name: timeout + max_move_distance_gate: + name: max move distance gate + max_still_distance_gate: + name: max still distance gate + g0: + move_threshold: + name: g0 move threshold + still_threshold: + name: g0 still threshold + g1: + move_threshold: + name: g1 move threshold + still_threshold: + name: g1 still threshold + g2: + move_threshold: + name: g2 move threshold + still_threshold: + name: g2 still threshold + g3: + move_threshold: + name: g3 move threshold + still_threshold: + name: g3 still threshold + g4: + move_threshold: + name: g4 move threshold + still_threshold: + name: g4 still threshold + g5: + move_threshold: + name: g5 move threshold + still_threshold: + name: g5 still threshold + g6: + move_threshold: + name: g6 move threshold + still_threshold: + name: g6 still threshold + g7: + move_threshold: + name: g7 move threshold + still_threshold: + name: g7 still threshold + g8: + move_threshold: + name: g8 move threshold + still_threshold: + name: g8 still threshold + +select: + - platform: ld2410 + distance_resolution: + name: distance resolution + baud_rate: + name: baud rate + light_function: + name: light function + out_pin_level: + name: out ping level + +sensor: + - platform: ld2410 + light: + name: light + moving_distance: + name: Moving distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Distance Detection + g0: + move_energy: + name: g0 move energy + still_energy: + name: g0 still energy + g1: + move_energy: + name: g1 move energy + still_energy: + name: g1 still energy + g2: + move_energy: + name: g2 move energy + still_energy: + name: g2 still energy + g3: + move_energy: + name: g3 move energy + still_energy: + name: g3 still energy + g4: + move_energy: + name: g4 move energy + still_energy: + name: g4 still energy + g5: + move_energy: + name: g5 move energy + still_energy: + name: g5 still energy + g6: + move_energy: + name: g6 move energy + still_energy: + name: g6 still energy + g7: + move_energy: + name: g7 move energy + still_energy: + name: g7 still energy + g8: + move_energy: + name: g8 move energy + still_energy: + name: g8 still energy + +switch: + - platform: ld2410 + engineering_mode: + name: control ld2410 engineering mode + bluetooth: + name: control ld2410 bluetooth + +text_sensor: + - platform: ld2410 + version: + name: presenece sensor version + mac_address: + name: presenece sensor mac address diff --git a/tests/components/ld2410/test.rp2040-ard.yaml b/tests/components/ld2410/test.rp2040-ard.yaml new file mode 100644 index 0000000000..afcaa81b3e --- /dev/null +++ b/tests/components/ld2410/test.rp2040-ard.yaml @@ -0,0 +1,169 @@ +uart: + - id: uart_ld2410 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +ld2410: + id: my_ld2410 + +binary_sensor: + - platform: ld2410 + has_target: + name: presence + has_moving_target: + name: movement + has_still_target: + name: still + out_pin_presence_status: + name: out pin presence status + +button: + - platform: ld2410 + factory_reset: + name: factory reset + restart: + name: restart + query_params: + name: query params + +number: + - platform: ld2410 + light_threshold: + name: light threshold + timeout: + name: timeout + max_move_distance_gate: + name: max move distance gate + max_still_distance_gate: + name: max still distance gate + g0: + move_threshold: + name: g0 move threshold + still_threshold: + name: g0 still threshold + g1: + move_threshold: + name: g1 move threshold + still_threshold: + name: g1 still threshold + g2: + move_threshold: + name: g2 move threshold + still_threshold: + name: g2 still threshold + g3: + move_threshold: + name: g3 move threshold + still_threshold: + name: g3 still threshold + g4: + move_threshold: + name: g4 move threshold + still_threshold: + name: g4 still threshold + g5: + move_threshold: + name: g5 move threshold + still_threshold: + name: g5 still threshold + g6: + move_threshold: + name: g6 move threshold + still_threshold: + name: g6 still threshold + g7: + move_threshold: + name: g7 move threshold + still_threshold: + name: g7 still threshold + g8: + move_threshold: + name: g8 move threshold + still_threshold: + name: g8 still threshold + +select: + - platform: ld2410 + distance_resolution: + name: distance resolution + baud_rate: + name: baud rate + light_function: + name: light function + out_pin_level: + name: out ping level + +sensor: + - platform: ld2410 + light: + name: light + moving_distance: + name: Moving distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Distance Detection + g0: + move_energy: + name: g0 move energy + still_energy: + name: g0 still energy + g1: + move_energy: + name: g1 move energy + still_energy: + name: g1 still energy + g2: + move_energy: + name: g2 move energy + still_energy: + name: g2 still energy + g3: + move_energy: + name: g3 move energy + still_energy: + name: g3 still energy + g4: + move_energy: + name: g4 move energy + still_energy: + name: g4 still energy + g5: + move_energy: + name: g5 move energy + still_energy: + name: g5 still energy + g6: + move_energy: + name: g6 move energy + still_energy: + name: g6 still energy + g7: + move_energy: + name: g7 move energy + still_energy: + name: g7 still energy + g8: + move_energy: + name: g8 move energy + still_energy: + name: g8 still energy + +switch: + - platform: ld2410 + engineering_mode: + name: control ld2410 engineering mode + bluetooth: + name: control ld2410 bluetooth + +text_sensor: + - platform: ld2410 + version: + name: presenece sensor version + mac_address: + name: presenece sensor mac address diff --git a/tests/components/ld2420/test.esp32-ard.yaml b/tests/components/ld2420/test.esp32-ard.yaml new file mode 100644 index 0000000000..8c883664ec --- /dev/null +++ b/tests/components/ld2420/test.esp32-ard.yaml @@ -0,0 +1,132 @@ +uart: + - id: uart_ld2420 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +ld2420: + id: my_ld2420 + +binary_sensor: + - platform: ld2420 + has_target: + name: Presence + +button: + - platform: ld2420 + apply_config: + name: Apply Config + factory_reset: + name: Factory Reset + restart_module: + name: Restart Module + revert_config: + name: Undo Edits + +number: + - platform: ld2420 + presence_timeout: + name: Detection Presence Timeout + min_gate_distance: + name: Detection Gate Minimum + max_gate_distance: + name: Detection Gate Maximum + gate_move_sensitivity: + name: Move Calibration Sensitivity Factor + gate_still_sensitivity: + name: Still Calibration Sensitivity Factor + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + gate_14: + move_threshold: + name: Gate 14 Move Threshold + still_threshold: + name: Gate 14 Still Threshold + gate_15: + move_threshold: + name: Gate 15 Move Threshold + still_threshold: + name: Gate 15 Still Threshold + +select: + - platform: ld2420 + operating_mode: + name: Operating Mode + +sensor: + - platform: ld2420 + moving_distance: + name: "Moving distance (cm)" + +text_sensor: + - platform: ld2420 + fw_version: + name: LD2420 Firmware diff --git a/tests/components/ld2420/test.esp32-c3-ard.yaml b/tests/components/ld2420/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5e0b9db1c2 --- /dev/null +++ b/tests/components/ld2420/test.esp32-c3-ard.yaml @@ -0,0 +1,132 @@ +uart: + - id: uart_ld2420 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +ld2420: + id: my_ld2420 + +binary_sensor: + - platform: ld2420 + has_target: + name: Presence + +button: + - platform: ld2420 + apply_config: + name: Apply Config + factory_reset: + name: Factory Reset + restart_module: + name: Restart Module + revert_config: + name: Undo Edits + +number: + - platform: ld2420 + presence_timeout: + name: Detection Presence Timeout + min_gate_distance: + name: Detection Gate Minimum + max_gate_distance: + name: Detection Gate Maximum + gate_move_sensitivity: + name: Move Calibration Sensitivity Factor + gate_still_sensitivity: + name: Still Calibration Sensitivity Factor + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + gate_14: + move_threshold: + name: Gate 14 Move Threshold + still_threshold: + name: Gate 14 Still Threshold + gate_15: + move_threshold: + name: Gate 15 Move Threshold + still_threshold: + name: Gate 15 Still Threshold + +select: + - platform: ld2420 + operating_mode: + name: Operating Mode + +sensor: + - platform: ld2420 + moving_distance: + name: "Moving distance (cm)" + +text_sensor: + - platform: ld2420 + fw_version: + name: LD2420 Firmware diff --git a/tests/components/ld2420/test.esp32-c3-idf.yaml b/tests/components/ld2420/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5e0b9db1c2 --- /dev/null +++ b/tests/components/ld2420/test.esp32-c3-idf.yaml @@ -0,0 +1,132 @@ +uart: + - id: uart_ld2420 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +ld2420: + id: my_ld2420 + +binary_sensor: + - platform: ld2420 + has_target: + name: Presence + +button: + - platform: ld2420 + apply_config: + name: Apply Config + factory_reset: + name: Factory Reset + restart_module: + name: Restart Module + revert_config: + name: Undo Edits + +number: + - platform: ld2420 + presence_timeout: + name: Detection Presence Timeout + min_gate_distance: + name: Detection Gate Minimum + max_gate_distance: + name: Detection Gate Maximum + gate_move_sensitivity: + name: Move Calibration Sensitivity Factor + gate_still_sensitivity: + name: Still Calibration Sensitivity Factor + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + gate_14: + move_threshold: + name: Gate 14 Move Threshold + still_threshold: + name: Gate 14 Still Threshold + gate_15: + move_threshold: + name: Gate 15 Move Threshold + still_threshold: + name: Gate 15 Still Threshold + +select: + - platform: ld2420 + operating_mode: + name: Operating Mode + +sensor: + - platform: ld2420 + moving_distance: + name: "Moving distance (cm)" + +text_sensor: + - platform: ld2420 + fw_version: + name: LD2420 Firmware diff --git a/tests/components/ld2420/test.esp32-idf.yaml b/tests/components/ld2420/test.esp32-idf.yaml new file mode 100644 index 0000000000..8c883664ec --- /dev/null +++ b/tests/components/ld2420/test.esp32-idf.yaml @@ -0,0 +1,132 @@ +uart: + - id: uart_ld2420 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +ld2420: + id: my_ld2420 + +binary_sensor: + - platform: ld2420 + has_target: + name: Presence + +button: + - platform: ld2420 + apply_config: + name: Apply Config + factory_reset: + name: Factory Reset + restart_module: + name: Restart Module + revert_config: + name: Undo Edits + +number: + - platform: ld2420 + presence_timeout: + name: Detection Presence Timeout + min_gate_distance: + name: Detection Gate Minimum + max_gate_distance: + name: Detection Gate Maximum + gate_move_sensitivity: + name: Move Calibration Sensitivity Factor + gate_still_sensitivity: + name: Still Calibration Sensitivity Factor + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + gate_14: + move_threshold: + name: Gate 14 Move Threshold + still_threshold: + name: Gate 14 Still Threshold + gate_15: + move_threshold: + name: Gate 15 Move Threshold + still_threshold: + name: Gate 15 Still Threshold + +select: + - platform: ld2420 + operating_mode: + name: Operating Mode + +sensor: + - platform: ld2420 + moving_distance: + name: "Moving distance (cm)" + +text_sensor: + - platform: ld2420 + fw_version: + name: LD2420 Firmware diff --git a/tests/components/ld2420/test.esp8266-ard.yaml b/tests/components/ld2420/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5e0b9db1c2 --- /dev/null +++ b/tests/components/ld2420/test.esp8266-ard.yaml @@ -0,0 +1,132 @@ +uart: + - id: uart_ld2420 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +ld2420: + id: my_ld2420 + +binary_sensor: + - platform: ld2420 + has_target: + name: Presence + +button: + - platform: ld2420 + apply_config: + name: Apply Config + factory_reset: + name: Factory Reset + restart_module: + name: Restart Module + revert_config: + name: Undo Edits + +number: + - platform: ld2420 + presence_timeout: + name: Detection Presence Timeout + min_gate_distance: + name: Detection Gate Minimum + max_gate_distance: + name: Detection Gate Maximum + gate_move_sensitivity: + name: Move Calibration Sensitivity Factor + gate_still_sensitivity: + name: Still Calibration Sensitivity Factor + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + gate_14: + move_threshold: + name: Gate 14 Move Threshold + still_threshold: + name: Gate 14 Still Threshold + gate_15: + move_threshold: + name: Gate 15 Move Threshold + still_threshold: + name: Gate 15 Still Threshold + +select: + - platform: ld2420 + operating_mode: + name: Operating Mode + +sensor: + - platform: ld2420 + moving_distance: + name: "Moving distance (cm)" + +text_sensor: + - platform: ld2420 + fw_version: + name: LD2420 Firmware diff --git a/tests/components/ld2420/test.rp2040-ard.yaml b/tests/components/ld2420/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5e0b9db1c2 --- /dev/null +++ b/tests/components/ld2420/test.rp2040-ard.yaml @@ -0,0 +1,132 @@ +uart: + - id: uart_ld2420 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +ld2420: + id: my_ld2420 + +binary_sensor: + - platform: ld2420 + has_target: + name: Presence + +button: + - platform: ld2420 + apply_config: + name: Apply Config + factory_reset: + name: Factory Reset + restart_module: + name: Restart Module + revert_config: + name: Undo Edits + +number: + - platform: ld2420 + presence_timeout: + name: Detection Presence Timeout + min_gate_distance: + name: Detection Gate Minimum + max_gate_distance: + name: Detection Gate Maximum + gate_move_sensitivity: + name: Move Calibration Sensitivity Factor + gate_still_sensitivity: + name: Still Calibration Sensitivity Factor + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + gate_14: + move_threshold: + name: Gate 14 Move Threshold + still_threshold: + name: Gate 14 Still Threshold + gate_15: + move_threshold: + name: Gate 15 Move Threshold + still_threshold: + name: Gate 15 Still Threshold + +select: + - platform: ld2420 + operating_mode: + name: Operating Mode + +sensor: + - platform: ld2420 + moving_distance: + name: "Moving distance (cm)" + +text_sensor: + - platform: ld2420 + fw_version: + name: LD2420 Firmware diff --git a/tests/components/ledc/common.yaml b/tests/components/ledc/common.yaml new file mode 100644 index 0000000000..70352b4519 --- /dev/null +++ b/tests/components/ledc/common.yaml @@ -0,0 +1,11 @@ +esphome: + on_boot: + then: + - output.ledc.set_frequency: + id: test_ledc + frequency: 100Hz + +output: + - platform: ledc + id: test_ledc + pin: 4 diff --git a/tests/components/ledc/test.esp32-ard.yaml b/tests/components/ledc/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ledc/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ledc/test.esp32-c3-ard.yaml b/tests/components/ledc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ledc/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ledc/test.esp32-c3-idf.yaml b/tests/components/ledc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ledc/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ledc/test.esp32-idf.yaml b/tests/components/ledc/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ledc/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml new file mode 100644 index 0000000000..a224dbe8bc --- /dev/null +++ b/tests/components/light/common.yaml @@ -0,0 +1,125 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + - light.turn_off: test_rgb_light + - light.turn_on: + id: test_rgb_light + brightness: 100% + red: 100% + green: 100% + blue: 1.0 + - light.control: + id: test_monochromatic_light + state: on + - light.dim_relative: + id: test_monochromatic_light + relative_brightness: 5% + brightness_limits: + max_brightness: 90% + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed + - platform: monochromatic + id: test_monochromatic_light + name: Monochromatic Light + output: test_ledc_1 + gamma_correct: 2.8 + default_transition_length: 2s + effects: + - strobe: + - flicker: + - flicker: + name: My Flicker + alpha: 98% + intensity: 1.5% + - lambda: + name: My Custom Effect + update_interval: 1s + lambda: |- + static int state = 0; + state += 1; + if (state == 4) + state = 0; + - pulse: + transition_length: 10s + update_interval: 20s + min_brightness: 10% + max_brightness: 90% + - pulse: + name: pulse2 + transition_length: + on_length: 10s + off_length: 5s + update_interval: 15s + min_brightness: 10% + max_brightness: 90% + - platform: rgb + id: test_rgb_light + name: RGB Light + red: test_ledc_1 + green: test_ledc_2 + blue: test_ledc_3 + - platform: rgbw + id: test_rgbw_light + name: RGBW Light + red: test_ledc_1 + green: test_ledc_2 + blue: test_ledc_3 + white: test_ledc_4 + color_interlock: true + - platform: rgbww + id: test_rgbww_light + name: RGBWW Light + red: test_ledc_1 + green: test_ledc_2 + blue: test_ledc_3 + cold_white: test_ledc_4 + warm_white: test_ledc_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true + - platform: rgbct + id: test_rgbct_light + name: RGBCT Light + red: test_ledc_1 + green: test_ledc_2 + blue: test_ledc_3 + color_temperature: test_ledc_4 + white_brightness: test_ledc_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true + - platform: cwww + id: test_cwww_light + name: CWWW Light + cold_white: test_ledc_1 + warm_white: test_ledc_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + constant_brightness: true + - platform: color_temperature + id: test_color_temperature_light + name: CT Light + color_temperature: test_ledc_1 + brightness: test_ledc_2 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + - platform: rgb + id: test_rgb_light_initial_state + name: RGB Light + red: test_ledc_1 + green: test_ledc_2 + blue: test_ledc_3 + initial_state: + color_mode: rgb + red: 100% + green: 50% + blue: 50% diff --git a/tests/components/light/test.esp32-ard.yaml b/tests/components/light/test.esp32-ard.yaml new file mode 100644 index 0000000000..925197182c --- /dev/null +++ b/tests/components/light/test.esp32-ard.yaml @@ -0,0 +1,21 @@ +output: + - platform: gpio + id: test_binary + pin: 12 + - platform: ledc + id: test_ledc_1 + pin: 13 + - platform: ledc + id: test_ledc_2 + pin: 14 + - platform: ledc + id: test_ledc_3 + pin: 15 + - platform: ledc + id: test_ledc_4 + pin: 16 + - platform: ledc + id: test_ledc_5 + pin: 17 + +<<: !include common.yaml diff --git a/tests/components/light/test.esp32-c3-ard.yaml b/tests/components/light/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..317d5748a3 --- /dev/null +++ b/tests/components/light/test.esp32-c3-ard.yaml @@ -0,0 +1,21 @@ +output: + - platform: gpio + id: test_binary + pin: 0 + - platform: ledc + id: test_ledc_1 + pin: 1 + - platform: ledc + id: test_ledc_2 + pin: 2 + - platform: ledc + id: test_ledc_3 + pin: 3 + - platform: ledc + id: test_ledc_4 + pin: 4 + - platform: ledc + id: test_ledc_5 + pin: 5 + +<<: !include common.yaml diff --git a/tests/components/light/test.esp32-c3-idf.yaml b/tests/components/light/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..317d5748a3 --- /dev/null +++ b/tests/components/light/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +output: + - platform: gpio + id: test_binary + pin: 0 + - platform: ledc + id: test_ledc_1 + pin: 1 + - platform: ledc + id: test_ledc_2 + pin: 2 + - platform: ledc + id: test_ledc_3 + pin: 3 + - platform: ledc + id: test_ledc_4 + pin: 4 + - platform: ledc + id: test_ledc_5 + pin: 5 + +<<: !include common.yaml diff --git a/tests/components/light/test.esp32-idf.yaml b/tests/components/light/test.esp32-idf.yaml new file mode 100644 index 0000000000..925197182c --- /dev/null +++ b/tests/components/light/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +output: + - platform: gpio + id: test_binary + pin: 12 + - platform: ledc + id: test_ledc_1 + pin: 13 + - platform: ledc + id: test_ledc_2 + pin: 14 + - platform: ledc + id: test_ledc_3 + pin: 15 + - platform: ledc + id: test_ledc_4 + pin: 16 + - platform: ledc + id: test_ledc_5 + pin: 17 + +<<: !include common.yaml diff --git a/tests/components/light/test.esp8266-ard.yaml b/tests/components/light/test.esp8266-ard.yaml new file mode 100644 index 0000000000..518011e925 --- /dev/null +++ b/tests/components/light/test.esp8266-ard.yaml @@ -0,0 +1,21 @@ +output: + - platform: gpio + id: test_binary + pin: 4 + - platform: esp8266_pwm + id: test_ledc_1 + pin: 12 + - platform: esp8266_pwm + id: test_ledc_2 + pin: 13 + - platform: esp8266_pwm + id: test_ledc_3 + pin: 14 + - platform: esp8266_pwm + id: test_ledc_4 + pin: 15 + - platform: esp8266_pwm + id: test_ledc_5 + pin: 16 + +<<: !include common.yaml diff --git a/tests/components/light/test.rp2040-ard.yaml b/tests/components/light/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a5a37fd559 --- /dev/null +++ b/tests/components/light/test.rp2040-ard.yaml @@ -0,0 +1,21 @@ +output: + - platform: gpio + id: test_binary + pin: 0 + - platform: rp2040_pwm + id: test_ledc_1 + pin: 1 + - platform: rp2040_pwm + id: test_ledc_2 + pin: 2 + - platform: rp2040_pwm + id: test_ledc_3 + pin: 3 + - platform: rp2040_pwm + id: test_ledc_4 + pin: 4 + - platform: rp2040_pwm + id: test_ledc_5 + pin: 5 + +<<: !include common.yaml diff --git a/tests/components/lightwaverf/common.yaml b/tests/components/lightwaverf/common.yaml new file mode 100644 index 0000000000..7ed8000271 --- /dev/null +++ b/tests/components/lightwaverf/common.yaml @@ -0,0 +1,13 @@ +lightwaverf: + read_pin: 5 + write_pin: 4 + +button: + - platform: template + name: "Turn off sofa" + id: light_off_ceiling_sofa + on_press: + lightwaverf.send_raw: + code: [0x04, 0x00, 0x00, 0x00, 0x0f, 0x03, 0x0d, 0x09, 0x08, 0x08] + name: "Sofa" + repeat: 1 diff --git a/tests/components/lightwaverf/test.esp8266-ard.yaml b/tests/components/lightwaverf/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lightwaverf/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lilygo_t5_47/test.esp32-ard.yaml b/tests/components/lilygo_t5_47/test.esp32-ard.yaml new file mode 100644 index 0000000000..35eb3df022 --- /dev/null +++ b/tests/components/lilygo_t5_47/test.esp32-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_lilygo_t5_47 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: lilygo_t5_47 + id: lilygo_touchscreen + interrupt_pin: 14 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/lilygo_t5_47/test.esp32-c3-ard.yaml b/tests/components/lilygo_t5_47/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..41e81103ac --- /dev/null +++ b/tests/components/lilygo_t5_47/test.esp32-c3-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_lilygo_t5_47 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: lilygo_t5_47 + id: lilygo_touchscreen + interrupt_pin: 6 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml b/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..41e81103ac --- /dev/null +++ b/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_lilygo_t5_47 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: lilygo_t5_47 + id: lilygo_touchscreen + interrupt_pin: 6 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/lilygo_t5_47/test.esp32-idf.yaml b/tests/components/lilygo_t5_47/test.esp32-idf.yaml new file mode 100644 index 0000000000..35eb3df022 --- /dev/null +++ b/tests/components/lilygo_t5_47/test.esp32-idf.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_lilygo_t5_47 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: lilygo_t5_47 + id: lilygo_touchscreen + interrupt_pin: 14 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/lilygo_t5_47/test.esp8266-ard.yaml b/tests/components/lilygo_t5_47/test.esp8266-ard.yaml new file mode 100644 index 0000000000..766c492b12 --- /dev/null +++ b/tests/components/lilygo_t5_47/test.esp8266-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_lilygo_t5_47 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: lilygo_t5_47 + id: lilygo_touchscreen + interrupt_pin: 12 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/lilygo_t5_47/test.rp2040-ard.yaml b/tests/components/lilygo_t5_47/test.rp2040-ard.yaml new file mode 100644 index 0000000000..41e81103ac --- /dev/null +++ b/tests/components/lilygo_t5_47/test.rp2040-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_lilygo_t5_47 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: lilygo_t5_47 + id: lilygo_touchscreen + interrupt_pin: 6 + display: ssd1306_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/lock/common.yaml b/tests/components/lock/common.yaml new file mode 100644 index 0000000000..82297a3da4 --- /dev/null +++ b/tests/components/lock/common.yaml @@ -0,0 +1,36 @@ +esphome: + on_boot: + then: + - lock.lock: test_lock1 + - lock.unlock: test_lock1 + - lock.open: test_lock1 + +output: + - platform: gpio + id: test_binary + pin: 4 + +lock: + - platform: template + id: test_lock1 + name: Template Lock + lambda: |- + if (millis() > 10000) { + return LOCK_STATE_LOCKED; + } else { + return LOCK_STATE_UNLOCKED; + } + optimistic: true + assumed_state: false + on_unlock: + - lock.template.publish: + id: test_lock1 + state: !lambda "return LOCK_STATE_UNLOCKED;" + on_lock: + - lock.template.publish: + id: test_lock1 + state: !lambda "return LOCK_STATE_LOCKED;" + - platform: output + name: Generic Output Lock + id: test_lock2 + output: test_binary diff --git a/tests/components/lock/test.esp32-ard.yaml b/tests/components/lock/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lock/test.esp32-c3-ard.yaml b/tests/components/lock/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lock/test.esp32-c3-idf.yaml b/tests/components/lock/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lock/test.esp32-idf.yaml b/tests/components/lock/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lock/test.esp8266-ard.yaml b/tests/components/lock/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lock/test.rp2040-ard.yaml b/tests/components/lock/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/logger/common-default_uart.yaml b/tests/components/logger/common-default_uart.yaml new file mode 100644 index 0000000000..70b485daac --- /dev/null +++ b/tests/components/logger/common-default_uart.yaml @@ -0,0 +1,7 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG diff --git a/tests/components/logger/common-usb_cdc.yaml b/tests/components/logger/common-usb_cdc.yaml new file mode 100644 index 0000000000..4df320527d --- /dev/null +++ b/tests/components/logger/common-usb_cdc.yaml @@ -0,0 +1,8 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG + hardware_uart: USB_CDC diff --git a/tests/components/logger/common-usb_serial_jtag.yaml b/tests/components/logger/common-usb_serial_jtag.yaml new file mode 100644 index 0000000000..5891c9ea40 --- /dev/null +++ b/tests/components/logger/common-usb_serial_jtag.yaml @@ -0,0 +1,8 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG + hardware_uart: USB_SERIAL_JTAG diff --git a/tests/components/logger/test-usb_cdc.esp32-c3-ard.yaml b/tests/components/logger/test-usb_cdc.esp32-c3-ard.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/logger/test-usb_cdc.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/logger/test-usb_cdc.esp32-s2-ard.yaml b/tests/components/logger/test-usb_cdc.esp32-s2-ard.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/logger/test-usb_cdc.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/logger/test-usb_cdc.esp32-s2-idf.yaml b/tests/components/logger/test-usb_cdc.esp32-s2-idf.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/logger/test-usb_cdc.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/logger/test-usb_cdc.esp32-s3-ard.yaml b/tests/components/logger/test-usb_cdc.esp32-s3-ard.yaml new file mode 100644 index 0000000000..cfdaec9771 --- /dev/null +++ b/tests/components/logger/test-usb_cdc.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-usb_cdc.yaml diff --git a/tests/components/logger/test-usb_serial_jtag.esp32-c3-idf.yaml b/tests/components/logger/test-usb_serial_jtag.esp32-c3-idf.yaml new file mode 100644 index 0000000000..46a927ff10 --- /dev/null +++ b/tests/components/logger/test-usb_serial_jtag.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-usb_serial_jtag.yaml diff --git a/tests/components/logger/test-usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/logger/test-usb_serial_jtag.esp32-s3-idf.yaml new file mode 100644 index 0000000000..46a927ff10 --- /dev/null +++ b/tests/components/logger/test-usb_serial_jtag.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-usb_serial_jtag.yaml diff --git a/tests/components/logger/test.esp32-ard.yaml b/tests/components/logger/test.esp32-ard.yaml new file mode 100644 index 0000000000..3fe04e18a3 --- /dev/null +++ b/tests/components/logger/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-default_uart.yaml diff --git a/tests/components/logger/test.esp32-c3-ard.yaml b/tests/components/logger/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..3fe04e18a3 --- /dev/null +++ b/tests/components/logger/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-default_uart.yaml diff --git a/tests/components/logger/test.esp32-c3-idf.yaml b/tests/components/logger/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3fe04e18a3 --- /dev/null +++ b/tests/components/logger/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-default_uart.yaml diff --git a/tests/components/logger/test.esp32-idf.yaml b/tests/components/logger/test.esp32-idf.yaml new file mode 100644 index 0000000000..3fe04e18a3 --- /dev/null +++ b/tests/components/logger/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-default_uart.yaml diff --git a/tests/components/logger/test.esp8266-ard.yaml b/tests/components/logger/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3fe04e18a3 --- /dev/null +++ b/tests/components/logger/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common-default_uart.yaml diff --git a/tests/components/logger/test.rp2040-ard.yaml b/tests/components/logger/test.rp2040-ard.yaml new file mode 100644 index 0000000000..3fe04e18a3 --- /dev/null +++ b/tests/components/logger/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-default_uart.yaml diff --git a/tests/components/ltr390/test.esp32-ard.yaml b/tests/components/ltr390/test.esp32-ard.yaml new file mode 100644 index 0000000000..bdfe349b77 --- /dev/null +++ b/tests/components/ltr390/test.esp32-ard.yaml @@ -0,0 +1,38 @@ +i2c: + - id: i2c_ltr390 + scl: 16 + sda: 17 + +sensor: + - platform: ltr390 + uv: + name: LTR390 UV + uv_index: + name: LTR390 UVI + light: + name: LTR390 Light + ambient_light: + name: LTR390 ALS + gain: X3 + resolution: 18 + window_correction_factor: 1.0 + address: 0x53 + update_interval: 60s + - platform: ltr390 + uv: + name: LTR390 UV + uv_index: + name: LTR390 UVI + light: + name: LTR390 Light + ambient_light: + name: LTR390 ALS + gain: + ambient_light: X9 + uv: X3 + resolution: + ambient_light: 18 + uv: 13 + window_correction_factor: 1.0 + address: 0x53 + update_interval: 60s diff --git a/tests/components/ltr390/test.esp32-c3-ard.yaml b/tests/components/ltr390/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fee0f37ce1 --- /dev/null +++ b/tests/components/ltr390/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ltr390 + scl: 5 + sda: 4 + +sensor: + - platform: ltr390 + uv: + name: LTR390 UV + uv_index: + name: LTR390 UVI + light: + name: LTR390 Light + ambient_light: + name: LTR390 ALS + gain: X3 + resolution: 18 + window_correction_factor: 1.0 + address: 0x53 + update_interval: 60s diff --git a/tests/components/ltr390/test.esp32-c3-idf.yaml b/tests/components/ltr390/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fee0f37ce1 --- /dev/null +++ b/tests/components/ltr390/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ltr390 + scl: 5 + sda: 4 + +sensor: + - platform: ltr390 + uv: + name: LTR390 UV + uv_index: + name: LTR390 UVI + light: + name: LTR390 Light + ambient_light: + name: LTR390 ALS + gain: X3 + resolution: 18 + window_correction_factor: 1.0 + address: 0x53 + update_interval: 60s diff --git a/tests/components/ltr390/test.esp32-idf.yaml b/tests/components/ltr390/test.esp32-idf.yaml new file mode 100644 index 0000000000..9786c7dac3 --- /dev/null +++ b/tests/components/ltr390/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ltr390 + scl: 16 + sda: 17 + +sensor: + - platform: ltr390 + uv: + name: LTR390 UV + uv_index: + name: LTR390 UVI + light: + name: LTR390 Light + ambient_light: + name: LTR390 ALS + gain: X3 + resolution: 18 + window_correction_factor: 1.0 + address: 0x53 + update_interval: 60s diff --git a/tests/components/ltr390/test.esp8266-ard.yaml b/tests/components/ltr390/test.esp8266-ard.yaml new file mode 100644 index 0000000000..149f46f9c8 --- /dev/null +++ b/tests/components/ltr390/test.esp8266-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_ltr390 + scl: 5 + sda: 4 + +sensor: + - platform: ltr390 + uv: + name: LTR390 UV + uv_index: + name: LTR390 UVI + light: + name: LTR390 Light + ambient_light: + name: LTR390 ALS + gain: + ambient_light: X9 + uv: X3 + resolution: 18 + window_correction_factor: 1.0 + address: 0x53 + update_interval: 60s diff --git a/tests/components/ltr390/test.rp2040-ard.yaml b/tests/components/ltr390/test.rp2040-ard.yaml new file mode 100644 index 0000000000..fee0f37ce1 --- /dev/null +++ b/tests/components/ltr390/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_ltr390 + scl: 5 + sda: 4 + +sensor: + - platform: ltr390 + uv: + name: LTR390 UV + uv_index: + name: LTR390 UVI + light: + name: LTR390 Light + ambient_light: + name: LTR390 ALS + gain: X3 + resolution: 18 + window_correction_factor: 1.0 + address: 0x53 + update_interval: 60s diff --git a/tests/components/ltr501/common.yaml b/tests/components/ltr501/common.yaml new file mode 100644 index 0000000000..b7074f52f2 --- /dev/null +++ b/tests/components/ltr501/common.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: ltr501 + address: 0x23 + i2c_id: i2c_ltr501 + type: ALS_PS + gain: 1X + integration_time: 100ms + ambient_light: "Ambient light" + ps_counts: "Proximity counts" diff --git a/tests/components/ltr501/test.esp32-ard.yaml b/tests/components/ltr501/test.esp32-ard.yaml new file mode 100644 index 0000000000..4c710c74fe --- /dev/null +++ b/tests/components/ltr501/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-c3-ard.yaml b/tests/components/ltr501/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9e7de2768d --- /dev/null +++ b/tests/components/ltr501/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-c3-idf.yaml b/tests/components/ltr501/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9e7de2768d --- /dev/null +++ b/tests/components/ltr501/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-idf.yaml b/tests/components/ltr501/test.esp32-idf.yaml new file mode 100644 index 0000000000..4c710c74fe --- /dev/null +++ b/tests/components/ltr501/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp8266-ard.yaml b/tests/components/ltr501/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9e7de2768d --- /dev/null +++ b/tests/components/ltr501/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.rp2040-ard.yaml b/tests/components/ltr501/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9e7de2768d --- /dev/null +++ b/tests/components/ltr501/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/common.yaml b/tests/components/ltr_als_ps/common.yaml new file mode 100644 index 0000000000..aa5c8abed7 --- /dev/null +++ b/tests/components/ltr_als_ps/common.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: ltr_als_ps + address: 0x23 + i2c_id: i2c_als_ps + gain: 1x + integration_time: 100ms + ps_cooldown: 5 s + ambient_light: "Ambient light" + full_spectrum_counts: "Full spectrum counts" + infrared_counts: "Infrared counts" + actual_gain: "Actual gain" diff --git a/tests/components/ltr_als_ps/test.esp32-ard.yaml b/tests/components/ltr_als_ps/test.esp32-ard.yaml new file mode 100644 index 0000000000..2349292a64 --- /dev/null +++ b/tests/components/ltr_als_ps/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_als_ps + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp32-c3-ard.yaml b/tests/components/ltr_als_ps/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d64d70f018 --- /dev/null +++ b/tests/components/ltr_als_ps/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_als_ps + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml b/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d64d70f018 --- /dev/null +++ b/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_als_ps + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp32-idf.yaml b/tests/components/ltr_als_ps/test.esp32-idf.yaml new file mode 100644 index 0000000000..2349292a64 --- /dev/null +++ b/tests/components/ltr_als_ps/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_als_ps + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp8266-ard.yaml b/tests/components/ltr_als_ps/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d64d70f018 --- /dev/null +++ b/tests/components/ltr_als_ps/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_als_ps + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.rp2040-ard.yaml b/tests/components/ltr_als_ps/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d64d70f018 --- /dev/null +++ b/tests/components/ltr_als_ps/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_als_ps + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lvgl/.gitattributes b/tests/components/lvgl/.gitattributes new file mode 100644 index 0000000000..75e7a44254 --- /dev/null +++ b/tests/components/lvgl/.gitattributes @@ -0,0 +1,2 @@ +*.ttf -text + diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml new file mode 100644 index 0000000000..c7d635db1c --- /dev/null +++ b/tests/components/lvgl/common.yaml @@ -0,0 +1,110 @@ +touchscreen: + - platform: ft63x6 + id: tft_touch + display: tft_display + update_interval: 50ms + threshold: 1 + +font: + - file: "$component_dir/roboto.ttf" + id: roboto20 + size: 20 + + - file: "$component_dir/helvetica.ttf" + id: helvetica20 + - file: "$component_dir/roboto.ttf" + id: roboto10 + size: 10 + bpp: 4 + +sensor: + - platform: lvgl + id: lvgl_sensor_id + name: "LVGL Arc Sensor" + widget: lv_arc + - platform: lvgl + widget: slider_id + name: LVGL Slider + - platform: lvgl + widget: bar_id + id: lvgl_bar_sensor + name: LVGL Bar + - platform: lvgl + widget: spinbox_id + name: LVGL Spinbox + +number: + - platform: lvgl + widget: slider_id + name: LVGL Slider + update_on_release: true + - platform: lvgl + widget: lv_arc + id: lvgl_arc_number + name: LVGL Arc + - platform: lvgl + widget: bar_id + id: lvgl_bar_number + name: LVGL Bar + - platform: lvgl + widget: spinbox_id + id: lvgl_spinbox_number + name: LVGL Spinbox + +light: + - platform: lvgl + name: LVGL LED + id: lv_light + widget: lv_led + +binary_sensor: + - platform: lvgl + id: lvgl_pressbutton + name: Pressbutton + widget: spin_up + publish_initial_state: true + - platform: lvgl + name: ButtonMatrix button + widget: button_a + - platform: lvgl + id: switch_d + name: Matrix switch D + widget: button_d + on_click: + then: + - lvgl.page.previous: + animation: move_right + time: 600ms + - platform: lvgl + id: button_checker + name: LVGL button + widget: spin_up + on_state: + then: + - lvgl.checkbox.update: + id: checkbox_id + state: + checked: !lambda return x; + text: Unchecked + - platform: lvgl + name: LVGL checkbox + widget: checkbox_id + on_state: + then: + - lvgl.image.update: + id: lv_image + src: !lambda if (x) return id(cat_image); else return id(dog_image); + +wifi: + ssid: SSID + password: PASSWORD123 + +time: + platform: sntp + id: time_id + +text: + - id: lvgl_text + platform: lvgl + widget: hello_label + mode: text diff --git a/tests/components/lvgl/helvetica.ttf b/tests/components/lvgl/helvetica.ttf new file mode 100644 index 0000000000..7aec6f3f3c Binary files /dev/null and b/tests/components/lvgl/helvetica.ttf differ diff --git a/tests/components/lvgl/logo-text.svg b/tests/components/lvgl/logo-text.svg new file mode 100644 index 0000000000..4950806a36 --- /dev/null +++ b/tests/components/lvgl/logo-text.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml new file mode 100644 index 0000000000..9bfbb5fc95 --- /dev/null +++ b/tests/components/lvgl/lvgl-package.yaml @@ -0,0 +1,790 @@ +substitutions: + light_recessed: "\U000F179B" + wall_sconce_round: "\U000F0748" + gas_burner: "\U000F1A1B" + home_icon: "\U000F02DC" + menu_left: "\U000F0A02" + menu_right: "\U000F035F" + close: "\U000F0156" + delete: "\U000F01B4" + backspace: "\U000F006E" + check: "\U000F012C" + arrow_down: "\U000F004B" + +lvgl: + log_level: debug + resume_on_input: true + on_pause: + logger.log: LVGL is Paused + on_resume: + logger.log: LVGL has resumed + bg_color: light_blue + disp_bg_color: color_id + disp_bg_image: cat_image + theme: + obj: + border_width: 1 + + gradients: + - id: color_bar + direction: hor + dither: err_diff + stops: + - color: 0xFF0000 + position: 0 + - color: 0xFFFF00 + position: 42 + - color: 0x00FF00 + position: 84 + - color: 0x00FFFF + position: 127 + - color: 0x0000FF + position: 169 + - color: 0xFF00FF + position: 212 + - color: 0xFF0000 + position: 255 + + style_definitions: + - id: style_test + bg_color: 0x2F8CD8 + - id: header_footer + bg_color: 0x20214F + bg_grad_color: 0x005782 + bg_grad_dir: VER + bg_opa: cover + border_width: 0 + radius: 0 + pad_all: 0 + border_color: tomato + text_color: springgreen + width: 100% + height: 30 + border_side: [left, top] + text_decor: [underline, strikethrough] + - id: style_line + line_color: light_blue + line_width: 8 + line_rounded: true + - id: date_style + text_font: roboto10 + align: center + text_color: color_id2 + bg_opa: cover + radius: 4 + pad_all: 2 + - id: spin_button + height: 40 + width: 40 + - id: spin_label + align: center + text_align: center + text_font: space16 + - id: bdr_style + border_color: 0x808080 + border_width: 2 + pad_all: 4 + align: center + - id: image_recolor + image_recolor: 0x10ca1e + image_recolor_opa: cover + + touchscreens: + - touchscreen_id: tft_touch + long_press_repeat_time: 200ms + long_press_time: 500ms + + msgboxes: + - id: message_box + close_button: true + title: Messagebox + bg_color: 0xffff + body: + text: This is a sample messagebox + bg_color: 0x808080 + button_style: + bg_color: 0xff00 + border_width: 4 + buttons: + - id: msgbox_button + text: Button + - id: msgbox_apply + text: "Close" + on_click: + then: + - lvgl.widget.hide: message_box + - id: simple_msgbox + title: Simple + + pages: + - id: page1 + on_load: + - logger.log: page loaded + - lvgl.widget.focus: + action: restore + on_unload: + - logger.log: page unloaded + - lvgl.widget.focus: mark + - lvgl.widget.redraw: hello_label + - lvgl.widget.redraw: + on_all_events: + logger.log: + format: "Event %s" + args: ['lv_event_code_name_for(event->code).c_str()'] + skip: true + layout: + type: flex + pad_row: 4 + pad_column: 4px + flex_align_main: center + flex_align_cross: start + flex_align_track: end + widgets: + - roller: + id: lv_roller + visible_row_count: 2 + anim_time: 500ms + options: + - Nov + - Dec + selected_index: 1 + on_value: + then: + - logger.log: + format: "Roller changed = %d: %s" + args: [x, text.c_str()] + - animimg: + height: 60 + id: anim_img + src: [cat_image, dog_image] + repeat_count: 10 + duration: 1s + auto_start: true + on_all_events: + logger.log: + format: "Event %s" + args: ['lv_event_code_name_for(event->code).c_str()'] + - label: + id: hello_label + text: Hello world + text_color: 0xFF8000 + align: center + text_font: montserrat_40 + border_post: true + on_press: + lvgl.label.update: + id: hello_label + text: Goodbye + on_click: + then: + - lvgl.animimg.stop: anim_img + - lvgl.update: + disp_bg_color: 0xffff00 + disp_bg_image: cat_image + - lvgl.widget.show: message_box + - label: + text: "Hello shiny day" + text_color: 0xFFFFFF + align: bottom_mid + text_font: space16 + - obj: + align: center + arc_opa: COVER + arc_color: 0xFF0000 + arc_rounded: false + arc_width: 3 + anim_time: 1s + bg_color: light_blue + bg_grad_color: light_blue + bg_dither_mode: ordered + bg_grad_dir: hor + bg_grad_stop: 128 + bg_image_opa: transp + bg_image_recolor: light_blue + bg_image_recolor_opa: 50% + bg_main_stop: 0 + bg_opa: 20% + border_color: 0x00FF00 + border_opa: cover + border_post: true + border_side: [bottom, left] + border_width: 4 + clip_corner: false + color_filter_opa: transp + height: 50% + image_recolor: light_blue + image_recolor_opa: cover + line_width: 10 + line_dash_width: 10 + line_dash_gap: 10 + line_rounded: false + line_color: light_blue + opa: cover + opa_layered: cover + outline_color: light_blue + outline_opa: cover + outline_pad: 10px + outline_width: 10px + pad_all: 10px + pad_bottom: 10px + pad_left: 10px + pad_right: 10px + pad_top: 10px + shadow_color: light_blue + shadow_ofs_x: 5 + shadow_ofs_y: 5 + shadow_opa: cover + shadow_spread: 5 + shadow_width: 10 + text_align: auto + text_color: light_blue + text_decor: [underline, strikethrough] + text_font: montserrat_18 + text_letter_space: 4 + text_line_space: 4 + text_opa: cover + transform_angle: 180 + transform_height: 100 + transform_pivot_x: 50% + transform_pivot_y: 50% + transform_zoom: 0.5 + translate_x: 10 + translate_y: 10 + max_height: 100 + max_width: 200 + min_height: 20% + min_width: 20% + radius: circle + width: 10px + x: 100 + y: 120 + - buttonmatrix: + on_press: + then: + - logger.log: + format: "matrix button pressed: %d" + args: ["x"] + - lvgl.widget.show: b_matrix + - lvgl.widget.redraw: + + on_long_press: + lvgl.matrix.button.update: + id: [button_a, button_e, button_c] + control: + disabled: true + on_click: + logger.log: + format: "matrix button clicked: %d, is button_a = %u" + args: ["x", "id(button_a) == x"] + items: + checked: + bg_color: 0xFFFF00 + id: b_matrix + + rows: + - buttons: + - id: button_a + text: home icon + width: 2 + control: + checkable: true + on_value: + logger.log: + format: "button_a value %d" + args: [x] + - id: button_b + text: B + width: 1 + on_value: + logger.log: + format: "button_b value %d" + args: [x] + on_click: + then: + - lvgl.page.previous: + control: + hidden: false + - buttons: + - id: button_c + text: C + control: + checkable: false + - id: button_d + text: menu left + on_long_press: + then: + logger.log: Long pressed + on_long_press_repeat: + then: + logger.log: Long pressed repeated + - buttons: + - id: button_e + - button: + id: button_button + width: 20% + height: 10% + transform_angle: !lambda return 180*100; + arc_width: !lambda return 4; + border_width: !lambda return 6; + shadow_ofs_x: !lambda return 6; + shadow_ofs_y: !lambda return 6; + shadow_spread: !lambda return 6; + shadow_width: !lambda return 6; + pressed: + bg_color: light_blue + checkable: true + checked: + bg_color: 0x000000 + widgets: + - label: + text: Button + on_click: + - lvgl.widget.focus: spin_up + - lvgl.widget.focus: next + - lvgl.widget.focus: previous + - lvgl.widget.focus: + action: previous + freeze: true + - lvgl.widget.focus: + id: spin_up + freeze: true + editing: true + - lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + - lvgl.label.update: + id: hello_label + text: !lambda return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda 'return str_sprintf("Hello space");' + - lvgl.label.update: + id: hello_label + text: + format: "sprintf format %s" + args: ['x ? "checked" : "unchecked"'] + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: time_id + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda return id(time_id).now(); + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return id(time_id).now(); + on_value: + logger.log: + format: "state now %d" + args: [x] + on_short_click: + lvgl.widget.hide: hello_label + on_long_press: + lvgl.widget.show: hello_label + on_cancel: + lvgl.widget.enable: hello_label + on_ready: + lvgl.widget.disable: hello_label + on_defocus: + lvgl.widget.hide: hello_label + on_focus: + logger.log: Button clicked + on_scroll: + logger.log: Button clicked + on_scroll_end: + logger.log: Button clicked + on_scroll_begin: + logger.log: Button clicked + on_release: + logger.log: Button clicked + on_long_press_repeat: + logger.log: Button clicked + - led: + id: lv_led + color: 0x00FF00 + brightness: 50% + align: right_mid + - spinner: + arc_length: 120 + spin_time: 2s + align: left_mid + - image: + id: lv_image + src: cat_image + align: top_left + y: "50" + - tileview: + id: tileview_id + scrollbar_mode: active + on_value: + then: + - if: + condition: + lambda: return tile == id(tile_1); + then: + - logger.log: "tile 1 is now showing" + tiles: + - id: tile_1 + row: 0 + column: 0 + dir: ALL + widgets: + - obj: + bg_color: 0x000000 + - id: tile_2 + row: 1 + column: 0 + dir: [VER, HOR] + widgets: + - obj: + bg_color: 0x000000 + + - id: page2 + widgets: + - qrcode: + id: lv_qr + align: left_mid + size: 100 + light_color: whitesmoke + dark_color: steelblue + text: esphome.io + on_click: + lvgl.qrcode.update: + id: lv_qr + text: homeassistant.io + + - slider: + min_value: 0 + max_value: 255 + bg_opa: cover + bg_grad: color_bar + radius: 0 + indicator: + bg_opa: transp + knob: + radius: 1 + width: "4" + height: 10% + bg_color: 0x000000 + width: 100% + height: 10% + align: top_mid + - button: + styles: spin_button + id: spin_up + on_click: + - lvgl.spinbox.increment: spinbox_id + widgets: + - label: + styles: spin_label + text: "+" + - spinbox: + text_font: space16 + id: spinbox_id + align: center + width: 120 + range_from: -10 + range_to: 1000 + step: 5.0 + rollover: false + digits: 6 + decimal_places: 2 + value: 15 + on_value: + then: + - logger.log: + format: "Spinbox value is %f" + args: [x] + - button: + styles: spin_button + id: spin_down + on_click: + - lvgl.spinbox.decrement: spinbox_id + widgets: + - label: + styles: spin_label + text: "-" + - arc: + align: left_mid + id: lv_arc + adjustable: true + on_value: + then: + - logger.log: + format: "Arc value is %f" + args: [x] + scroll_on_focus: true + value: 75 + min_value: 1 + max_value: 100 + arc_color: 0xFF0000 + indicator: + arc_color: 0xF000FF + pressed: + arc_color: 0xFFFF00 + focused: + arc_color: 0x808080 + - bar: + id: bar_id + align: top_mid + y: 20 + value: 30 + max_value: 100 + min_value: 10 + mode: range + on_click: + then: + - lvgl.bar.update: + id: bar_id + value: !lambda return (int)((float)rand() / RAND_MAX * 100); + - logger.log: + format: "bar value %f" + args: [x] + - line: + id: lv_line_id + align: center + points: + - 5, 5 + - 70, 70 + - 120, 10 + - 180, 60 + - 240, 10 + on_click: + - lvgl.widget.update: + id: lv_line_id + line_color: 0xFFFF + - lvgl.page.next: + - switch: + align: right_mid + - checkbox: + id: checkbox_id + text: Checkbox + align: bottom_right + - slider: + id: slider_id + align: top_mid + y: 40 + value: 30 + max_value: 100 + min_value: 10 + mode: normal + on_value: + then: + - logger.log: + format: "slider value %f" + args: [x] + on_click: + then: + - lvgl.slider.update: + id: slider_id + value: !lambda return (int)((float)rand() / RAND_MAX * 100); + - tabview: + id: tabview_id + width: 100% + height: 80% + position: top + on_value: + then: + - if: + condition: + lambda: return tab == id(tabview_tab_1); + then: + - logger.log: "Dog tab is now showing" + tabs: + - name: Dog + id: tabview_tab_1 + border_width: 2 + border_color: 0xff0000 + width: 100% + pad_all: 8 + layout: + type: grid + grid_row_align: end + grid_rows: [25px, fr(1), content] + grid_columns: [40, fr(1), fr(1)] + pad_row: 6px + pad_column: 0 + widgets: + - image: + grid_cell_row_pos: 0 + grid_cell_column_pos: 0 + src: dog_image + on_click: + then: + - lvgl.tabview.select: + id: tabview_id + index: 1 + animated: true + - label: + styles: bdr_style + grid_cell_x_align: center + grid_cell_y_align: stretch + grid_cell_row_pos: 0 + grid_cell_column_pos: 1 + grid_cell_column_span: 1 + text: "Grid cell 0/1" + - label: + grid_cell_x_align: end + styles: bdr_style + grid_cell_row_pos: 1 + grid_cell_column_pos: 0 + text: "Grid cell 1/0" + - label: + styles: bdr_style + grid_cell_row_pos: 1 + grid_cell_column_pos: 1 + text: "Grid cell 1/1" + - label: + id: cell_1_3 + styles: bdr_style + grid_cell_row_pos: 1 + grid_cell_column_pos: 2 + text: "Grid cell 1/2" + - name: Cat + id: tabview_tab_2 + widgets: + - dropdown: + indicator: + text_font: helvetica20 + id: lv_dropdown + options: + - First + - Second + - Third + - 6th + - 7th + - 8th + - 9th + selected_index: 2 + dir: top + symbol: ${arrow_down} + dropdown_list: + max_height: 100px + bg_color: 0x000080 + text_color: 0xFF00 + selected: + bg_color: 0xFFFF00 + checked: + bg_color: 0x00 + text_color: 0xFF0000 + scrollbar: + bg_color: 0xFF + on_value: + logger.log: + format: "Dropdown changed = %d: %s" + args: [x, text.c_str()] + on_cancel: + logger.log: + format: "Dropdown closed = %d" + args: [x] + - image: + src: cat_image + on_click: + then: + - lvgl.dropdown.update: + id: lv_dropdown + options: + ["First", "Second", "Third", "4th", "5th", "6th", "7th", "8th", "9th", "10th", "11th"] + selected_index: 3 + - logger.log: Cat image clicked + - lvgl.tabview.select: + id: tabview_id + index: 0 + animated: true + - meter: + height: 200px + width: 200px + indicator: + bg_color: 0xFF + radius: 0 + bg_opa: TRANSP + text_color: 0xFFFFFF + scales: + - ticks: + width: 1 + count: 61 + length: 20 + color: 0xFFFFFF + range_from: 0 + range_to: 60 + angle_range: 360 + rotation: 270 + indicators: + - line: + opa: 50% + id: minute_hand + color: 0xFF0000 + r_mod: -1 + width: 3 + - + angle_range: 330 + rotation: 300 + range_from: 1 + range_to: 12 + ticks: + width: 1 + count: 12 + length: 1 + major: + stride: 1 + width: 4 + length: 8 + color: 0xC0C0C0 + label_gap: 6 + - angle_range: 360 + rotation: 270 + range_from: 0 + range_to: 720 + indicators: + - line: + id: hour_hand + value: 180 + width: 4 + color: 0xA0A0A0 + r_mod: -20 + opa: 0% + +font: + - file: "gfonts://Roboto" + id: space16 + bpp: 4 + +image: + - id: cat_image + resize: 256x48 + file: $component_dir/logo-text.svg + - id: dog_image + file: $component_dir/logo-text.svg + resize: 256x48 + type: TRANSPARENT_BINARY + +color: + - id: light_blue + hex: "3340FF" + - id: color_id + red: 0.5 + green: 0.5 + blue: 0.5 + white: 0.5 + - id: color_id2 + red_int: 0xFF + green_int: 123 + blue_int: 64 + white_int: 255 diff --git a/tests/components/lvgl/materialdesignicons-webfont.ttf b/tests/components/lvgl/materialdesignicons-webfont.ttf new file mode 100644 index 0000000000..eb4f4c45a7 Binary files /dev/null and b/tests/components/lvgl/materialdesignicons-webfont.ttf differ diff --git a/tests/components/lvgl/roboto.ttf b/tests/components/lvgl/roboto.ttf new file mode 100644 index 0000000000..2c97eeadff Binary files /dev/null and b/tests/components/lvgl/roboto.ttf differ diff --git a/tests/components/lvgl/test.esp32-ard.yaml b/tests/components/lvgl/test.esp32-ard.yaml new file mode 100644 index 0000000000..80d5ce503f --- /dev/null +++ b/tests/components/lvgl/test.esp32-ard.yaml @@ -0,0 +1,59 @@ +spi: + clk_pin: 14 + mosi_pin: 13 + +i2c: + sda: GPIO18 + scl: GPIO19 + +display: + - platform: ili9xxx + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + transform: + swap_xy: false + mirror_x: true + mirror_y: true + data_rate: 80MHz + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +binary_sensor: + - platform: gpio + internal: true + id: up_button + pin: + number: GPIO38 + inverted: true + - platform: gpio + internal: true + id: down_button + pin: + number: GPIO37 + inverted: true + - platform: gpio + internal: true + id: select_button + pin: + number: GPIO39 + inverted: true +lvgl: + draw_rounding: 8 + encoders: + group: switches + initial_focus: button_button + enter_button: select_button + sensor: + left_button: up_button + right_button: down_button + +packages: + lvgl: !include lvgl-package.yaml + +<<: !include common.yaml diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml new file mode 100644 index 0000000000..05a1f243ed --- /dev/null +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -0,0 +1,75 @@ +spi: + clk_pin: 14 + mosi_pin: 13 + +i2c: + sda: GPIO18 + scl: GPIO19 + +sensor: + - platform: rotary_encoder + name: "Rotary Encoder" + id: encoder + pin_a: 3 + pin_b: 1 + internal: true + +binary_sensor: + - platform: gpio + id: pushbutton + name: Pushbutton + pin: + number: 0 + inverted: true + ignore_strapping_warning: true + +display: + - platform: ili9xxx + model: st7789v + id: second_display + dimensions: + width: 240 + height: 320 + transform: + swap_xy: false + mirror_x: true + mirror_y: true + data_rate: 80MHz + cs_pin: GPIO20 + dc_pin: + number: GPIO15 + ignore_strapping_warning: true + auto_clear_enabled: false + invert_colors: false + update_interval: never + + - platform: ili9xxx + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + transform: + swap_xy: false + mirror_x: true + mirror_y: true + data_rate: 80MHz + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +packages: + lvgl: !include lvgl-package.yaml + +lvgl: + displays: + - tft_display + - second_display + encoders: + sensor: encoder + enter_button: pushbutton + group: general + +<<: !include common.yaml diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml new file mode 100644 index 0000000000..3a490bbe15 --- /dev/null +++ b/tests/components/lvgl/test.host.yaml @@ -0,0 +1,11 @@ +display: + - platform: sdl + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +touchscreen: + - platform: sdl + +lvgl: diff --git a/tests/components/m5stack_8angle/common.yaml b/tests/components/m5stack_8angle/common.yaml new file mode 100644 index 0000000000..d7f988ed3a --- /dev/null +++ b/tests/components/m5stack_8angle/common.yaml @@ -0,0 +1,30 @@ +i2c: + sda: 0 + scl: 1 + id: bus_external + +m5stack_8angle: + i2c_id: bus_external + id: m5stack_8angle_base + +light: + - platform: m5stack_8angle + m5stack_8angle_id: m5stack_8angle_base + id: m8_angle_leds + name: Lights + effects: + - addressable_scan: + +sensor: + - platform: m5stack_8angle + m5stack_8angle_id: m5stack_8angle_base + channel: 1 + name: Knob 1 + - platform: m5stack_8angle + m5stack_8angle_id: m5stack_8angle_base + channel: 2 + name: Knob 2 +binary_sensor: + - platform: m5stack_8angle + m5stack_8angle_id: m5stack_8angle_base + name: Switch diff --git a/tests/components/m5stack_8angle/test.esp32-ard.yaml b/tests/components/m5stack_8angle/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/m5stack_8angle/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp32-c3-ard.yaml b/tests/components/m5stack_8angle/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/m5stack_8angle/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml b/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp32-idf.yaml b/tests/components/m5stack_8angle/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/m5stack_8angle/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp8266-ard.yaml b/tests/components/m5stack_8angle/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/m5stack_8angle/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.rp2040-ard.yaml b/tests/components/m5stack_8angle/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/m5stack_8angle/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/matrix_keypad/test.esp32-ard.yaml b/tests/components/matrix_keypad/test.esp32-ard.yaml new file mode 100644 index 0000000000..c8e9b54534 --- /dev/null +++ b/tests/components/matrix_keypad/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +binary_sensor: + - platform: matrix_keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + +matrix_keypad: + id: keypad + rows: + - pin: 12 + - pin: 13 + columns: + - pin: 14 + - pin: 15 + keys: "1234" + has_pulldowns: true diff --git a/tests/components/matrix_keypad/test.esp32-c3-ard.yaml b/tests/components/matrix_keypad/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d15e6af21a --- /dev/null +++ b/tests/components/matrix_keypad/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +binary_sensor: + - platform: matrix_keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + +matrix_keypad: + id: keypad + rows: + - pin: 1 + - pin: 2 + columns: + - pin: 3 + - pin: 4 + keys: "1234" + has_pulldowns: true diff --git a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml b/tests/components/matrix_keypad/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d15e6af21a --- /dev/null +++ b/tests/components/matrix_keypad/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +binary_sensor: + - platform: matrix_keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + +matrix_keypad: + id: keypad + rows: + - pin: 1 + - pin: 2 + columns: + - pin: 3 + - pin: 4 + keys: "1234" + has_pulldowns: true diff --git a/tests/components/matrix_keypad/test.esp32-idf.yaml b/tests/components/matrix_keypad/test.esp32-idf.yaml new file mode 100644 index 0000000000..c8e9b54534 --- /dev/null +++ b/tests/components/matrix_keypad/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +binary_sensor: + - platform: matrix_keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + +matrix_keypad: + id: keypad + rows: + - pin: 12 + - pin: 13 + columns: + - pin: 14 + - pin: 15 + keys: "1234" + has_pulldowns: true diff --git a/tests/components/matrix_keypad/test.esp8266-ard.yaml b/tests/components/matrix_keypad/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c8e9b54534 --- /dev/null +++ b/tests/components/matrix_keypad/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +binary_sensor: + - platform: matrix_keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + +matrix_keypad: + id: keypad + rows: + - pin: 12 + - pin: 13 + columns: + - pin: 14 + - pin: 15 + keys: "1234" + has_pulldowns: true diff --git a/tests/components/matrix_keypad/test.rp2040-ard.yaml b/tests/components/matrix_keypad/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d15e6af21a --- /dev/null +++ b/tests/components/matrix_keypad/test.rp2040-ard.yaml @@ -0,0 +1,19 @@ +binary_sensor: + - platform: matrix_keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + +matrix_keypad: + id: keypad + rows: + - pin: 1 + - pin: 2 + columns: + - pin: 3 + - pin: 4 + keys: "1234" + has_pulldowns: true diff --git a/tests/components/max17043/common.yaml b/tests/components/max17043/common.yaml new file mode 100644 index 0000000000..c2f324212e --- /dev/null +++ b/tests/components/max17043/common.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - max17043.sleep_mode: max17043_id + +i2c: + - id: i2c_id + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: max17043 + id: max17043_id + i2c_id: i2c_id + battery_voltage: + name: "Battery Voltage" + battery_level: + name: Battery + update_interval: 10s diff --git a/tests/components/max17043/test.esp32-ard.yaml b/tests/components/max17043/test.esp32-ard.yaml new file mode 100644 index 0000000000..c84e0a5c2e --- /dev/null +++ b/tests/components/max17043/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + sda_pin: GPIO21 + scl_pin: GPIO22 + +<<: !include common.yaml + diff --git a/tests/components/max17043/test.esp32-c3-ard.yaml b/tests/components/max17043/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9a1477d4b9 --- /dev/null +++ b/tests/components/max17043/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO8 + scl_pin: GPIO10 + +<<: !include common.yaml diff --git a/tests/components/max17043/test.esp32-c3-idf.yaml b/tests/components/max17043/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9a1477d4b9 --- /dev/null +++ b/tests/components/max17043/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO8 + scl_pin: GPIO10 + +<<: !include common.yaml diff --git a/tests/components/max17043/test.esp32-idf.yaml b/tests/components/max17043/test.esp32-idf.yaml new file mode 100644 index 0000000000..c6615f51cd --- /dev/null +++ b/tests/components/max17043/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO21 + scl_pin: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/max17043/test.esp8266-ard.yaml b/tests/components/max17043/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a87353b78b --- /dev/null +++ b/tests/components/max17043/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO4 + scl_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/max17043/test.rp2040-ard.yaml b/tests/components/max17043/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c6615f51cd --- /dev/null +++ b/tests/components/max17043/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO21 + scl_pin: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/max31855/test.esp32-ard.yaml b/tests/components/max31855/test.esp32-ard.yaml new file mode 100644 index 0000000000..25fee986d2 --- /dev/null +++ b/tests/components/max31855/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31855 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: max31855 + name: MAX31855 Temperature + cs_pin: 12 + update_interval: 15s + reference_temperature: + name: MAX31855 Internal Temperature diff --git a/tests/components/max31855/test.esp32-c3-ard.yaml b/tests/components/max31855/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e7c8f3f824 --- /dev/null +++ b/tests/components/max31855/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31855 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: max31855 + name: MAX31855 Temperature + cs_pin: 8 + update_interval: 15s + reference_temperature: + name: MAX31855 Internal Temperature diff --git a/tests/components/max31855/test.esp32-c3-idf.yaml b/tests/components/max31855/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e7c8f3f824 --- /dev/null +++ b/tests/components/max31855/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31855 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: max31855 + name: MAX31855 Temperature + cs_pin: 8 + update_interval: 15s + reference_temperature: + name: MAX31855 Internal Temperature diff --git a/tests/components/max31855/test.esp32-idf.yaml b/tests/components/max31855/test.esp32-idf.yaml new file mode 100644 index 0000000000..25fee986d2 --- /dev/null +++ b/tests/components/max31855/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31855 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: max31855 + name: MAX31855 Temperature + cs_pin: 12 + update_interval: 15s + reference_temperature: + name: MAX31855 Internal Temperature diff --git a/tests/components/max31855/test.esp8266-ard.yaml b/tests/components/max31855/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7e02d41fce --- /dev/null +++ b/tests/components/max31855/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31855 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: max31855 + name: MAX31855 Temperature + cs_pin: 15 + update_interval: 15s + reference_temperature: + name: MAX31855 Internal Temperature diff --git a/tests/components/max31855/test.rp2040-ard.yaml b/tests/components/max31855/test.rp2040-ard.yaml new file mode 100644 index 0000000000..379d4d33d6 --- /dev/null +++ b/tests/components/max31855/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31855 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: max31855 + name: MAX31855 Temperature + cs_pin: 6 + update_interval: 15s + reference_temperature: + name: MAX31855 Internal Temperature diff --git a/tests/components/max31856/test.esp32-ard.yaml b/tests/components/max31856/test.esp32-ard.yaml new file mode 100644 index 0000000000..9a4da6b2a2 --- /dev/null +++ b/tests/components/max31856/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31856 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: max31856 + name: MAX31856 Temperature + cs_pin: 12 + update_interval: 15s + mains_filter: 50Hz + thermocouple_type: N diff --git a/tests/components/max31856/test.esp32-c3-ard.yaml b/tests/components/max31856/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..71bbfffb7b --- /dev/null +++ b/tests/components/max31856/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_max31856 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: max31856 + name: MAX31856 Temperature + cs_pin: 8 + update_interval: 15s + mains_filter: 50Hz + thermocouple_type: N + diff --git a/tests/components/max31856/test.esp32-c3-idf.yaml b/tests/components/max31856/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..71bbfffb7b --- /dev/null +++ b/tests/components/max31856/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_max31856 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: max31856 + name: MAX31856 Temperature + cs_pin: 8 + update_interval: 15s + mains_filter: 50Hz + thermocouple_type: N + diff --git a/tests/components/max31856/test.esp32-idf.yaml b/tests/components/max31856/test.esp32-idf.yaml new file mode 100644 index 0000000000..9a4da6b2a2 --- /dev/null +++ b/tests/components/max31856/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31856 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: max31856 + name: MAX31856 Temperature + cs_pin: 12 + update_interval: 15s + mains_filter: 50Hz + thermocouple_type: N diff --git a/tests/components/max31856/test.esp8266-ard.yaml b/tests/components/max31856/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b9c42542fd --- /dev/null +++ b/tests/components/max31856/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_max31856 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: max31856 + name: MAX31856 Temperature + cs_pin: 15 + update_interval: 15s + mains_filter: 50Hz + thermocouple_type: N + diff --git a/tests/components/max31856/test.rp2040-ard.yaml b/tests/components/max31856/test.rp2040-ard.yaml new file mode 100644 index 0000000000..8607eb18cf --- /dev/null +++ b/tests/components/max31856/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_max31856 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: max31856 + name: MAX31856 Temperature + cs_pin: 6 + update_interval: 15s + mains_filter: 50Hz + thermocouple_type: N + diff --git a/tests/components/max31865/test.esp32-ard.yaml b/tests/components/max31865/test.esp32-ard.yaml new file mode 100644 index 0000000000..8326a578ee --- /dev/null +++ b/tests/components/max31865/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31865 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: max31865 + name: MAX31865 Temperature + cs_pin: 12 + update_interval: 15s + reference_resistance: 430 Ω + rtd_nominal_resistance: 100 Ω diff --git a/tests/components/max31865/test.esp32-c3-ard.yaml b/tests/components/max31865/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..45de22331e --- /dev/null +++ b/tests/components/max31865/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31865 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: max31865 + name: MAX31865 Temperature + cs_pin: 8 + update_interval: 15s + reference_resistance: 430 Ω + rtd_nominal_resistance: 100 Ω diff --git a/tests/components/max31865/test.esp32-c3-idf.yaml b/tests/components/max31865/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..45de22331e --- /dev/null +++ b/tests/components/max31865/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31865 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: max31865 + name: MAX31865 Temperature + cs_pin: 8 + update_interval: 15s + reference_resistance: 430 Ω + rtd_nominal_resistance: 100 Ω diff --git a/tests/components/max31865/test.esp32-idf.yaml b/tests/components/max31865/test.esp32-idf.yaml new file mode 100644 index 0000000000..8326a578ee --- /dev/null +++ b/tests/components/max31865/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31865 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: max31865 + name: MAX31865 Temperature + cs_pin: 12 + update_interval: 15s + reference_resistance: 430 Ω + rtd_nominal_resistance: 100 Ω diff --git a/tests/components/max31865/test.esp8266-ard.yaml b/tests/components/max31865/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4828019acc --- /dev/null +++ b/tests/components/max31865/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31865 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: max31865 + name: MAX31865 Temperature + cs_pin: 15 + update_interval: 15s + reference_resistance: 430 Ω + rtd_nominal_resistance: 100 Ω diff --git a/tests/components/max31865/test.rp2040-ard.yaml b/tests/components/max31865/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5af64b41ad --- /dev/null +++ b/tests/components/max31865/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_max31865 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: max31865 + name: MAX31865 Temperature + cs_pin: 6 + update_interval: 15s + reference_resistance: 430 Ω + rtd_nominal_resistance: 100 Ω diff --git a/tests/components/max44009/test.esp32-ard.yaml b/tests/components/max44009/test.esp32-ard.yaml new file mode 100644 index 0000000000..56eecebc4a --- /dev/null +++ b/tests/components/max44009/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_max44009 + scl: 16 + sda: 17 + +sensor: + - platform: max44009 + name: MAX44009 Brightness + internal: true + mode: low_power + address: 0x4A + update_interval: 30s diff --git a/tests/components/max44009/test.esp32-c3-ard.yaml b/tests/components/max44009/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..593d4bd48c --- /dev/null +++ b/tests/components/max44009/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_max44009 + scl: 5 + sda: 4 + +sensor: + - platform: max44009 + name: MAX44009 Brightness + internal: true + mode: low_power + address: 0x4A + update_interval: 30s diff --git a/tests/components/max44009/test.esp32-c3-idf.yaml b/tests/components/max44009/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..593d4bd48c --- /dev/null +++ b/tests/components/max44009/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_max44009 + scl: 5 + sda: 4 + +sensor: + - platform: max44009 + name: MAX44009 Brightness + internal: true + mode: low_power + address: 0x4A + update_interval: 30s diff --git a/tests/components/max44009/test.esp32-idf.yaml b/tests/components/max44009/test.esp32-idf.yaml new file mode 100644 index 0000000000..56eecebc4a --- /dev/null +++ b/tests/components/max44009/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_max44009 + scl: 16 + sda: 17 + +sensor: + - platform: max44009 + name: MAX44009 Brightness + internal: true + mode: low_power + address: 0x4A + update_interval: 30s diff --git a/tests/components/max44009/test.esp8266-ard.yaml b/tests/components/max44009/test.esp8266-ard.yaml new file mode 100644 index 0000000000..593d4bd48c --- /dev/null +++ b/tests/components/max44009/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_max44009 + scl: 5 + sda: 4 + +sensor: + - platform: max44009 + name: MAX44009 Brightness + internal: true + mode: low_power + address: 0x4A + update_interval: 30s diff --git a/tests/components/max44009/test.rp2040-ard.yaml b/tests/components/max44009/test.rp2040-ard.yaml new file mode 100644 index 0000000000..593d4bd48c --- /dev/null +++ b/tests/components/max44009/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_max44009 + scl: 5 + sda: 4 + +sensor: + - platform: max44009 + name: MAX44009 Brightness + internal: true + mode: low_power + address: 0x4A + update_interval: 30s diff --git a/tests/components/max6675/test.esp32-ard.yaml b/tests/components/max6675/test.esp32-ard.yaml new file mode 100644 index 0000000000..9771bf9d5f --- /dev/null +++ b/tests/components/max6675/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_max6675 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: max6675 + name: Temperature + cs_pin: 12 + update_interval: 15s diff --git a/tests/components/max6675/test.esp32-c3-ard.yaml b/tests/components/max6675/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2f05102ca1 --- /dev/null +++ b/tests/components/max6675/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_max6675 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: max6675 + name: Temperature + cs_pin: 8 + update_interval: 15s diff --git a/tests/components/max6675/test.esp32-c3-idf.yaml b/tests/components/max6675/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2f05102ca1 --- /dev/null +++ b/tests/components/max6675/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_max6675 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sensor: + - platform: max6675 + name: Temperature + cs_pin: 8 + update_interval: 15s diff --git a/tests/components/max6675/test.esp32-idf.yaml b/tests/components/max6675/test.esp32-idf.yaml new file mode 100644 index 0000000000..9771bf9d5f --- /dev/null +++ b/tests/components/max6675/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_max6675 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sensor: + - platform: max6675 + name: Temperature + cs_pin: 12 + update_interval: 15s diff --git a/tests/components/max6675/test.esp8266-ard.yaml b/tests/components/max6675/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f67e9e04a8 --- /dev/null +++ b/tests/components/max6675/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_max6675 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sensor: + - platform: max6675 + name: Temperature + cs_pin: 15 + update_interval: 15s diff --git a/tests/components/max6675/test.rp2040-ard.yaml b/tests/components/max6675/test.rp2040-ard.yaml new file mode 100644 index 0000000000..89c0932f94 --- /dev/null +++ b/tests/components/max6675/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_max6675 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +sensor: + - platform: max6675 + name: Temperature + cs_pin: 6 + update_interval: 15s diff --git a/tests/components/max6956/test.esp32-ard.yaml b/tests/components/max6956/test.esp32-ard.yaml new file mode 100644 index 0000000000..abd1404634 --- /dev/null +++ b/tests/components/max6956/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_max6956 + scl: 16 + sda: 17 + +max6956: + - id: max6956_1 + address: 0x40 + +binary_sensor: + - platform: gpio + name: Max Input Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false diff --git a/tests/components/max6956/test.esp32-c3-ard.yaml b/tests/components/max6956/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..690941784c --- /dev/null +++ b/tests/components/max6956/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_max6956 + scl: 5 + sda: 4 + +max6956: + - id: max6956_1 + address: 0x40 + +binary_sensor: + - platform: gpio + name: Max Input Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false diff --git a/tests/components/max6956/test.esp32-c3-idf.yaml b/tests/components/max6956/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..690941784c --- /dev/null +++ b/tests/components/max6956/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_max6956 + scl: 5 + sda: 4 + +max6956: + - id: max6956_1 + address: 0x40 + +binary_sensor: + - platform: gpio + name: Max Input Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false diff --git a/tests/components/max6956/test.esp32-idf.yaml b/tests/components/max6956/test.esp32-idf.yaml new file mode 100644 index 0000000000..abd1404634 --- /dev/null +++ b/tests/components/max6956/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_max6956 + scl: 16 + sda: 17 + +max6956: + - id: max6956_1 + address: 0x40 + +binary_sensor: + - platform: gpio + name: Max Input Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false diff --git a/tests/components/max6956/test.esp8266-ard.yaml b/tests/components/max6956/test.esp8266-ard.yaml new file mode 100644 index 0000000000..690941784c --- /dev/null +++ b/tests/components/max6956/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_max6956 + scl: 5 + sda: 4 + +max6956: + - id: max6956_1 + address: 0x40 + +binary_sensor: + - platform: gpio + name: Max Input Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false diff --git a/tests/components/max6956/test.rp2040-ard.yaml b/tests/components/max6956/test.rp2040-ard.yaml new file mode 100644 index 0000000000..690941784c --- /dev/null +++ b/tests/components/max6956/test.rp2040-ard.yaml @@ -0,0 +1,19 @@ +i2c: + - id: i2c_max6956 + scl: 5 + sda: 4 + +max6956: + - id: max6956_1 + address: 0x40 + +binary_sensor: + - platform: gpio + name: Max Input Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false diff --git a/tests/components/max7219/test.esp32-ard.yaml b/tests/components/max7219/test.esp32-ard.yaml new file mode 100644 index 0000000000..2985345a48 --- /dev/null +++ b/tests/components/max7219/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +spi: + - id: spi_max6675 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: max7219 + cs_pin: 12 + num_chips: 1 + lambda: |- + it.print("01234567"); diff --git a/tests/components/max7219/test.esp32-c3-ard.yaml b/tests/components/max7219/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fa1ac15f33 --- /dev/null +++ b/tests/components/max7219/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +spi: + - id: spi_max7219 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: max7219 + cs_pin: 8 + num_chips: 1 + lambda: |- + it.print("01234567"); diff --git a/tests/components/max7219/test.esp32-c3-idf.yaml b/tests/components/max7219/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fa1ac15f33 --- /dev/null +++ b/tests/components/max7219/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +spi: + - id: spi_max7219 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: max7219 + cs_pin: 8 + num_chips: 1 + lambda: |- + it.print("01234567"); diff --git a/tests/components/max7219/test.esp32-idf.yaml b/tests/components/max7219/test.esp32-idf.yaml new file mode 100644 index 0000000000..2985345a48 --- /dev/null +++ b/tests/components/max7219/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +spi: + - id: spi_max6675 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: max7219 + cs_pin: 12 + num_chips: 1 + lambda: |- + it.print("01234567"); diff --git a/tests/components/max7219/test.esp8266-ard.yaml b/tests/components/max7219/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a8c280daff --- /dev/null +++ b/tests/components/max7219/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +spi: + - id: spi_max6675 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: max7219 + cs_pin: 15 + num_chips: 1 + lambda: |- + it.print("01234567"); diff --git a/tests/components/max7219/test.rp2040-ard.yaml b/tests/components/max7219/test.rp2040-ard.yaml new file mode 100644 index 0000000000..37b2220649 --- /dev/null +++ b/tests/components/max7219/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +spi: + - id: spi_max6675 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: max7219 + cs_pin: 6 + num_chips: 1 + lambda: |- + it.print("01234567"); diff --git a/tests/components/max7219digit/test.esp32-ard.yaml b/tests/components/max7219digit/test.esp32-ard.yaml new file mode 100644 index 0000000000..7f3aed964a --- /dev/null +++ b/tests/components/max7219digit/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_max7219digit + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: max7219digit + cs_pin: 12 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: STOP + id: my_matrix + lambda: |- + it.printdigit("hello"); diff --git a/tests/components/max7219digit/test.esp32-c3-ard.yaml b/tests/components/max7219digit/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0c04784380 --- /dev/null +++ b/tests/components/max7219digit/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_max7219digit + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: max7219digit + cs_pin: 8 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: STOP + id: my_matrix + lambda: |- + it.printdigit("hello"); diff --git a/tests/components/max7219digit/test.esp32-c3-idf.yaml b/tests/components/max7219digit/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0c04784380 --- /dev/null +++ b/tests/components/max7219digit/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_max7219digit + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: max7219digit + cs_pin: 8 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: STOP + id: my_matrix + lambda: |- + it.printdigit("hello"); diff --git a/tests/components/max7219digit/test.esp32-idf.yaml b/tests/components/max7219digit/test.esp32-idf.yaml new file mode 100644 index 0000000000..7f3aed964a --- /dev/null +++ b/tests/components/max7219digit/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_max7219digit + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: max7219digit + cs_pin: 12 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: STOP + id: my_matrix + lambda: |- + it.printdigit("hello"); diff --git a/tests/components/max7219digit/test.esp8266-ard.yaml b/tests/components/max7219digit/test.esp8266-ard.yaml new file mode 100644 index 0000000000..52587e8b0e --- /dev/null +++ b/tests/components/max7219digit/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_max7219digit + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: max7219digit + cs_pin: 15 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: STOP + id: my_matrix + lambda: |- + it.printdigit("hello"); diff --git a/tests/components/max7219digit/test.rp2040-ard.yaml b/tests/components/max7219digit/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f986483ec2 --- /dev/null +++ b/tests/components/max7219digit/test.rp2040-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_max7219digit + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: max7219digit + cs_pin: 6 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: STOP + id: my_matrix + lambda: |- + it.printdigit("hello"); diff --git a/tests/components/max9611/test.esp32-ard.yaml b/tests/components/max9611/test.esp32-ard.yaml new file mode 100644 index 0000000000..5c480cc815 --- /dev/null +++ b/tests/components/max9611/test.esp32-ard.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_max9611 + scl: 16 + sda: 17 + +sensor: + - platform: max9611 + shunt_resistance: 0.2 ohm + gain: 1X + voltage: + name: Max9611 Voltage + current: + name: Max9611 Current + power: + name: Max9611 Watts + temperature: + name: Max9611 Temp + update_interval: 1s diff --git a/tests/components/max9611/test.esp32-c3-ard.yaml b/tests/components/max9611/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..00f8330280 --- /dev/null +++ b/tests/components/max9611/test.esp32-c3-ard.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_max9611 + scl: 5 + sda: 4 + +sensor: + - platform: max9611 + shunt_resistance: 0.2 ohm + gain: 1X + voltage: + name: Max9611 Voltage + current: + name: Max9611 Current + power: + name: Max9611 Watts + temperature: + name: Max9611 Temp + update_interval: 1s diff --git a/tests/components/max9611/test.esp32-c3-idf.yaml b/tests/components/max9611/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..00f8330280 --- /dev/null +++ b/tests/components/max9611/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_max9611 + scl: 5 + sda: 4 + +sensor: + - platform: max9611 + shunt_resistance: 0.2 ohm + gain: 1X + voltage: + name: Max9611 Voltage + current: + name: Max9611 Current + power: + name: Max9611 Watts + temperature: + name: Max9611 Temp + update_interval: 1s diff --git a/tests/components/max9611/test.esp32-idf.yaml b/tests/components/max9611/test.esp32-idf.yaml new file mode 100644 index 0000000000..5c480cc815 --- /dev/null +++ b/tests/components/max9611/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_max9611 + scl: 16 + sda: 17 + +sensor: + - platform: max9611 + shunt_resistance: 0.2 ohm + gain: 1X + voltage: + name: Max9611 Voltage + current: + name: Max9611 Current + power: + name: Max9611 Watts + temperature: + name: Max9611 Temp + update_interval: 1s diff --git a/tests/components/max9611/test.esp8266-ard.yaml b/tests/components/max9611/test.esp8266-ard.yaml new file mode 100644 index 0000000000..00f8330280 --- /dev/null +++ b/tests/components/max9611/test.esp8266-ard.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_max9611 + scl: 5 + sda: 4 + +sensor: + - platform: max9611 + shunt_resistance: 0.2 ohm + gain: 1X + voltage: + name: Max9611 Voltage + current: + name: Max9611 Current + power: + name: Max9611 Watts + temperature: + name: Max9611 Temp + update_interval: 1s diff --git a/tests/components/max9611/test.rp2040-ard.yaml b/tests/components/max9611/test.rp2040-ard.yaml new file mode 100644 index 0000000000..00f8330280 --- /dev/null +++ b/tests/components/max9611/test.rp2040-ard.yaml @@ -0,0 +1,18 @@ +i2c: + - id: i2c_max9611 + scl: 5 + sda: 4 + +sensor: + - platform: max9611 + shunt_resistance: 0.2 ohm + gain: 1X + voltage: + name: Max9611 Voltage + current: + name: Max9611 Current + power: + name: Max9611 Watts + temperature: + name: Max9611 Temp + update_interval: 1s diff --git a/tests/components/mcp23008/test.esp32-ard.yaml b/tests/components/mcp23008/test.esp32-ard.yaml new file mode 100644 index 0000000000..cbf03f371c --- /dev/null +++ b/tests/components/mcp23008/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23008 + scl: 16 + sda: 17 + +mcp23008: + id: mcp23008_hub + +binary_sensor: + - platform: gpio + id: mcp23008_binary_sensor + pin: + mcp23xxx: mcp23008_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23008_switch + pin: + mcp23xxx: mcp23008_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23008/test.esp32-c3-ard.yaml b/tests/components/mcp23008/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..eabd5a7311 --- /dev/null +++ b/tests/components/mcp23008/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23008 + scl: 5 + sda: 4 + +mcp23008: + id: mcp23008_hub + +binary_sensor: + - platform: gpio + id: mcp23008_binary_sensor + pin: + mcp23xxx: mcp23008_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23008_switch + pin: + mcp23xxx: mcp23008_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23008/test.esp32-c3-idf.yaml b/tests/components/mcp23008/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..eabd5a7311 --- /dev/null +++ b/tests/components/mcp23008/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23008 + scl: 5 + sda: 4 + +mcp23008: + id: mcp23008_hub + +binary_sensor: + - platform: gpio + id: mcp23008_binary_sensor + pin: + mcp23xxx: mcp23008_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23008_switch + pin: + mcp23xxx: mcp23008_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23008/test.esp32-idf.yaml b/tests/components/mcp23008/test.esp32-idf.yaml new file mode 100644 index 0000000000..cbf03f371c --- /dev/null +++ b/tests/components/mcp23008/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23008 + scl: 16 + sda: 17 + +mcp23008: + id: mcp23008_hub + +binary_sensor: + - platform: gpio + id: mcp23008_binary_sensor + pin: + mcp23xxx: mcp23008_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23008_switch + pin: + mcp23xxx: mcp23008_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23008/test.esp8266-ard.yaml b/tests/components/mcp23008/test.esp8266-ard.yaml new file mode 100644 index 0000000000..eabd5a7311 --- /dev/null +++ b/tests/components/mcp23008/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23008 + scl: 5 + sda: 4 + +mcp23008: + id: mcp23008_hub + +binary_sensor: + - platform: gpio + id: mcp23008_binary_sensor + pin: + mcp23xxx: mcp23008_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23008_switch + pin: + mcp23xxx: mcp23008_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23008/test.rp2040-ard.yaml b/tests/components/mcp23008/test.rp2040-ard.yaml new file mode 100644 index 0000000000..eabd5a7311 --- /dev/null +++ b/tests/components/mcp23008/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23008 + scl: 5 + sda: 4 + +mcp23008: + id: mcp23008_hub + +binary_sensor: + - platform: gpio + id: mcp23008_binary_sensor + pin: + mcp23xxx: mcp23008_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23008_switch + pin: + mcp23xxx: mcp23008_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23016/test.esp32-ard.yaml b/tests/components/mcp23016/test.esp32-ard.yaml new file mode 100644 index 0000000000..48574a9b01 --- /dev/null +++ b/tests/components/mcp23016/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23016 + scl: 16 + sda: 17 + +mcp23016: + id: mcp23016_hub + +binary_sensor: + - platform: gpio + id: mcp23016_binary_sensor + pin: + mcp23016: mcp23016_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23016_switch + pin: + mcp23016: mcp23016_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23016/test.esp32-c3-ard.yaml b/tests/components/mcp23016/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2211931e3d --- /dev/null +++ b/tests/components/mcp23016/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23016 + scl: 5 + sda: 4 + +mcp23016: + id: mcp23016_hub + +binary_sensor: + - platform: gpio + id: mcp23016_binary_sensor + pin: + mcp23016: mcp23016_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23016_switch + pin: + mcp23016: mcp23016_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23016/test.esp32-c3-idf.yaml b/tests/components/mcp23016/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2211931e3d --- /dev/null +++ b/tests/components/mcp23016/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23016 + scl: 5 + sda: 4 + +mcp23016: + id: mcp23016_hub + +binary_sensor: + - platform: gpio + id: mcp23016_binary_sensor + pin: + mcp23016: mcp23016_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23016_switch + pin: + mcp23016: mcp23016_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23016/test.esp32-idf.yaml b/tests/components/mcp23016/test.esp32-idf.yaml new file mode 100644 index 0000000000..48574a9b01 --- /dev/null +++ b/tests/components/mcp23016/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23016 + scl: 16 + sda: 17 + +mcp23016: + id: mcp23016_hub + +binary_sensor: + - platform: gpio + id: mcp23016_binary_sensor + pin: + mcp23016: mcp23016_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23016_switch + pin: + mcp23016: mcp23016_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23016/test.esp8266-ard.yaml b/tests/components/mcp23016/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2211931e3d --- /dev/null +++ b/tests/components/mcp23016/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23016 + scl: 5 + sda: 4 + +mcp23016: + id: mcp23016_hub + +binary_sensor: + - platform: gpio + id: mcp23016_binary_sensor + pin: + mcp23016: mcp23016_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23016_switch + pin: + mcp23016: mcp23016_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23016/test.rp2040-ard.yaml b/tests/components/mcp23016/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2211931e3d --- /dev/null +++ b/tests/components/mcp23016/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23016 + scl: 5 + sda: 4 + +mcp23016: + id: mcp23016_hub + +binary_sensor: + - platform: gpio + id: mcp23016_binary_sensor + pin: + mcp23016: mcp23016_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23016_switch + pin: + mcp23016: mcp23016_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23017/test.esp32-ard.yaml b/tests/components/mcp23017/test.esp32-ard.yaml new file mode 100644 index 0000000000..9b7c45eb8a --- /dev/null +++ b/tests/components/mcp23017/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23017 + scl: 16 + sda: 17 + +mcp23017: + id: mcp23017_hub + +binary_sensor: + - platform: gpio + id: mcp23017_binary_sensor + pin: + mcp23xxx: mcp23017_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23017_switch + pin: + mcp23xxx: mcp23017_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23017/test.esp32-c3-ard.yaml b/tests/components/mcp23017/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..863b2b8f0b --- /dev/null +++ b/tests/components/mcp23017/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23017 + scl: 5 + sda: 4 + +mcp23017: + id: mcp23017_hub + +binary_sensor: + - platform: gpio + id: mcp23017_binary_sensor + pin: + mcp23xxx: mcp23017_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23017_switch + pin: + mcp23xxx: mcp23017_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23017/test.esp32-c3-idf.yaml b/tests/components/mcp23017/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..863b2b8f0b --- /dev/null +++ b/tests/components/mcp23017/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23017 + scl: 5 + sda: 4 + +mcp23017: + id: mcp23017_hub + +binary_sensor: + - platform: gpio + id: mcp23017_binary_sensor + pin: + mcp23xxx: mcp23017_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23017_switch + pin: + mcp23xxx: mcp23017_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23017/test.esp32-idf.yaml b/tests/components/mcp23017/test.esp32-idf.yaml new file mode 100644 index 0000000000..9b7c45eb8a --- /dev/null +++ b/tests/components/mcp23017/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23017 + scl: 16 + sda: 17 + +mcp23017: + id: mcp23017_hub + +binary_sensor: + - platform: gpio + id: mcp23017_binary_sensor + pin: + mcp23xxx: mcp23017_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23017_switch + pin: + mcp23xxx: mcp23017_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23017/test.esp8266-ard.yaml b/tests/components/mcp23017/test.esp8266-ard.yaml new file mode 100644 index 0000000000..863b2b8f0b --- /dev/null +++ b/tests/components/mcp23017/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23017 + scl: 5 + sda: 4 + +mcp23017: + id: mcp23017_hub + +binary_sensor: + - platform: gpio + id: mcp23017_binary_sensor + pin: + mcp23xxx: mcp23017_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23017_switch + pin: + mcp23xxx: mcp23017_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23017/test.rp2040-ard.yaml b/tests/components/mcp23017/test.rp2040-ard.yaml new file mode 100644 index 0000000000..863b2b8f0b --- /dev/null +++ b/tests/components/mcp23017/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_mcp23017 + scl: 5 + sda: 4 + +mcp23017: + id: mcp23017_hub + +binary_sensor: + - platform: gpio + id: mcp23017_binary_sensor + pin: + mcp23xxx: mcp23017_hub + number: 0 + mode: INPUT + +switch: + - platform: gpio + id: mcp23017_switch + pin: + mcp23xxx: mcp23017_hub + number: 1 + mode: OUTPUT diff --git a/tests/components/mcp23s08/test.esp32-ard.yaml b/tests/components/mcp23s08/test.esp32-ard.yaml new file mode 100644 index 0000000000..0b26035c3e --- /dev/null +++ b/tests/components/mcp23s08/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s08 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +mcp23s08: + - id: mcp23s08_hub + cs_pin: 12 + deviceaddress: 0 diff --git a/tests/components/mcp23s08/test.esp32-c3-ard.yaml b/tests/components/mcp23s08/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f1af8a71a9 --- /dev/null +++ b/tests/components/mcp23s08/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s08 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +mcp23s08: + - id: mcp23s08_hub + cs_pin: 8 + deviceaddress: 0 diff --git a/tests/components/mcp23s08/test.esp32-c3-idf.yaml b/tests/components/mcp23s08/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f1af8a71a9 --- /dev/null +++ b/tests/components/mcp23s08/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s08 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +mcp23s08: + - id: mcp23s08_hub + cs_pin: 8 + deviceaddress: 0 diff --git a/tests/components/mcp23s08/test.esp32-idf.yaml b/tests/components/mcp23s08/test.esp32-idf.yaml new file mode 100644 index 0000000000..0b26035c3e --- /dev/null +++ b/tests/components/mcp23s08/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s08 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +mcp23s08: + - id: mcp23s08_hub + cs_pin: 12 + deviceaddress: 0 diff --git a/tests/components/mcp23s08/test.esp8266-ard.yaml b/tests/components/mcp23s08/test.esp8266-ard.yaml new file mode 100644 index 0000000000..eff856aca9 --- /dev/null +++ b/tests/components/mcp23s08/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s08 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +mcp23s08: + - id: mcp23s08_hub + cs_pin: 15 + deviceaddress: 0 diff --git a/tests/components/mcp23s08/test.rp2040-ard.yaml b/tests/components/mcp23s08/test.rp2040-ard.yaml new file mode 100644 index 0000000000..1b23d2d3b5 --- /dev/null +++ b/tests/components/mcp23s08/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s08 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +mcp23s08: + - id: mcp23s08_hub + cs_pin: 6 + deviceaddress: 0 diff --git a/tests/components/mcp23s17/test.esp32-ard.yaml b/tests/components/mcp23s17/test.esp32-ard.yaml new file mode 100644 index 0000000000..9a42c12e85 --- /dev/null +++ b/tests/components/mcp23s17/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s17 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +mcp23s17: + - id: mcp23s17_hub + cs_pin: 12 + deviceaddress: 0 diff --git a/tests/components/mcp23s17/test.esp32-c3-ard.yaml b/tests/components/mcp23s17/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d83f66d3b1 --- /dev/null +++ b/tests/components/mcp23s17/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s17 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +mcp23s17: + - id: mcp23s17_hub + cs_pin: 8 + deviceaddress: 0 diff --git a/tests/components/mcp23s17/test.esp32-c3-idf.yaml b/tests/components/mcp23s17/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d83f66d3b1 --- /dev/null +++ b/tests/components/mcp23s17/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s17 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +mcp23s17: + - id: mcp23s17_hub + cs_pin: 8 + deviceaddress: 0 diff --git a/tests/components/mcp23s17/test.esp32-idf.yaml b/tests/components/mcp23s17/test.esp32-idf.yaml new file mode 100644 index 0000000000..9a42c12e85 --- /dev/null +++ b/tests/components/mcp23s17/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s17 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +mcp23s17: + - id: mcp23s17_hub + cs_pin: 12 + deviceaddress: 0 diff --git a/tests/components/mcp23s17/test.esp8266-ard.yaml b/tests/components/mcp23s17/test.esp8266-ard.yaml new file mode 100644 index 0000000000..36dac63f6f --- /dev/null +++ b/tests/components/mcp23s17/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s17 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +mcp23s17: + - id: mcp23s17_hub + cs_pin: 15 + deviceaddress: 0 diff --git a/tests/components/mcp23s17/test.rp2040-ard.yaml b/tests/components/mcp23s17/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2730f6a9d6 --- /dev/null +++ b/tests/components/mcp23s17/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +spi: + - id: spi_mcp23s17 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +mcp23s17: + - id: mcp23s17_hub + cs_pin: 6 + deviceaddress: 0 diff --git a/tests/components/mcp2515/test.esp32-ard.yaml b/tests/components/mcp2515/test.esp32-ard.yaml new file mode 100644 index 0000000000..07fae36cc3 --- /dev/null +++ b/tests/components/mcp2515/test.esp32-ard.yaml @@ -0,0 +1,44 @@ +spi: + - id: spi_mcp2515 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +canbus: + - platform: mcp2515 + id: mcp2515_can + cs_pin: 12 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("can_id 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: "x[0] == 0x11" + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/mcp2515/test.esp32-c3-ard.yaml b/tests/components/mcp2515/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..3ceeea268f --- /dev/null +++ b/tests/components/mcp2515/test.esp32-c3-ard.yaml @@ -0,0 +1,44 @@ +spi: + - id: spi_mcp2515 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +canbus: + - platform: mcp2515 + id: mcp2515_can + cs_pin: 8 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("can_id 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: "x[0] == 0x11" + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/mcp2515/test.esp32-c3-idf.yaml b/tests/components/mcp2515/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3ceeea268f --- /dev/null +++ b/tests/components/mcp2515/test.esp32-c3-idf.yaml @@ -0,0 +1,44 @@ +spi: + - id: spi_mcp2515 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +canbus: + - platform: mcp2515 + id: mcp2515_can + cs_pin: 8 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("can_id 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: "x[0] == 0x11" + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/mcp2515/test.esp32-idf.yaml b/tests/components/mcp2515/test.esp32-idf.yaml new file mode 100644 index 0000000000..07fae36cc3 --- /dev/null +++ b/tests/components/mcp2515/test.esp32-idf.yaml @@ -0,0 +1,44 @@ +spi: + - id: spi_mcp2515 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +canbus: + - platform: mcp2515 + id: mcp2515_can + cs_pin: 12 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("can_id 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: "x[0] == 0x11" + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/mcp2515/test.esp8266-ard.yaml b/tests/components/mcp2515/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1096a0e809 --- /dev/null +++ b/tests/components/mcp2515/test.esp8266-ard.yaml @@ -0,0 +1,44 @@ +spi: + - id: spi_mcp2515 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +canbus: + - platform: mcp2515 + id: mcp2515_can + cs_pin: 15 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("can_id 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: "x[0] == 0x11" + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/mcp2515/test.rp2040-ard.yaml b/tests/components/mcp2515/test.rp2040-ard.yaml new file mode 100644 index 0000000000..678c817d3d --- /dev/null +++ b/tests/components/mcp2515/test.rp2040-ard.yaml @@ -0,0 +1,44 @@ +spi: + - id: spi_mcp2515 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +canbus: + - platform: mcp2515 + id: mcp2515_can + cs_pin: 6 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("can_id 500", "%s", b.c_str()); + - can_id: 23 + then: + - if: + condition: + lambda: "return x[0] == 0x11;" + then: + logger.log: "x[0] == 0x11" + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/mcp3008/test.esp32-ard.yaml b/tests/components/mcp3008/test.esp32-ard.yaml new file mode 100644 index 0000000000..a66fbeb7a1 --- /dev/null +++ b/tests/components/mcp3008/test.esp32-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_mcp3008 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +mcp3008: + - id: mcp3008_hub + cs_pin: 12 + +sensor: + - platform: mcp3008 + id: mcp3008_sensor + mcp3008_id: mcp3008_hub + number: 0 + reference_voltage: 3.19 + update_interval: 5s diff --git a/tests/components/mcp3008/test.esp32-c3-ard.yaml b/tests/components/mcp3008/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9e66372e4f --- /dev/null +++ b/tests/components/mcp3008/test.esp32-c3-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_mcp3008 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +mcp3008: + - id: mcp3008_hub + cs_pin: 8 + +sensor: + - platform: mcp3008 + id: mcp3008_sensor + mcp3008_id: mcp3008_hub + number: 0 + reference_voltage: 3.19 + update_interval: 5s diff --git a/tests/components/mcp3008/test.esp32-c3-idf.yaml b/tests/components/mcp3008/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9e66372e4f --- /dev/null +++ b/tests/components/mcp3008/test.esp32-c3-idf.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_mcp3008 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +mcp3008: + - id: mcp3008_hub + cs_pin: 8 + +sensor: + - platform: mcp3008 + id: mcp3008_sensor + mcp3008_id: mcp3008_hub + number: 0 + reference_voltage: 3.19 + update_interval: 5s diff --git a/tests/components/mcp3008/test.esp32-idf.yaml b/tests/components/mcp3008/test.esp32-idf.yaml new file mode 100644 index 0000000000..a66fbeb7a1 --- /dev/null +++ b/tests/components/mcp3008/test.esp32-idf.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_mcp3008 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +mcp3008: + - id: mcp3008_hub + cs_pin: 12 + +sensor: + - platform: mcp3008 + id: mcp3008_sensor + mcp3008_id: mcp3008_hub + number: 0 + reference_voltage: 3.19 + update_interval: 5s diff --git a/tests/components/mcp3008/test.esp8266-ard.yaml b/tests/components/mcp3008/test.esp8266-ard.yaml new file mode 100644 index 0000000000..eaccca0765 --- /dev/null +++ b/tests/components/mcp3008/test.esp8266-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_mcp3008 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +mcp3008: + - id: mcp3008_hub + cs_pin: 15 + +sensor: + - platform: mcp3008 + id: mcp3008_sensor + mcp3008_id: mcp3008_hub + number: 0 + reference_voltage: 3.19 + update_interval: 5s diff --git a/tests/components/mcp3008/test.rp2040-ard.yaml b/tests/components/mcp3008/test.rp2040-ard.yaml new file mode 100644 index 0000000000..8ab9630553 --- /dev/null +++ b/tests/components/mcp3008/test.rp2040-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_mcp3008 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +mcp3008: + - id: mcp3008_hub + cs_pin: 6 + +sensor: + - platform: mcp3008 + id: mcp3008_sensor + mcp3008_id: mcp3008_hub + number: 0 + reference_voltage: 3.19 + update_interval: 5s diff --git a/tests/components/mcp3204/test.esp32-ard.yaml b/tests/components/mcp3204/test.esp32-ard.yaml new file mode 100644 index 0000000000..c340797c8e --- /dev/null +++ b/tests/components/mcp3204/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_mcp3204 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +mcp3204: + - id: mcp3204_hub + cs_pin: 12 + +sensor: + - platform: mcp3204 + id: mcp3204_sensor + mcp3204_id: mcp3204_hub + number: 0 + update_interval: 5s diff --git a/tests/components/mcp3204/test.esp32-c3-ard.yaml b/tests/components/mcp3204/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5bf5ba81e1 --- /dev/null +++ b/tests/components/mcp3204/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_mcp3204 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +mcp3204: + - id: mcp3204_hub + cs_pin: 8 + +sensor: + - platform: mcp3204 + id: mcp3204_sensor + mcp3204_id: mcp3204_hub + number: 0 + update_interval: 5s diff --git a/tests/components/mcp3204/test.esp32-c3-idf.yaml b/tests/components/mcp3204/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5bf5ba81e1 --- /dev/null +++ b/tests/components/mcp3204/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_mcp3204 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +mcp3204: + - id: mcp3204_hub + cs_pin: 8 + +sensor: + - platform: mcp3204 + id: mcp3204_sensor + mcp3204_id: mcp3204_hub + number: 0 + update_interval: 5s diff --git a/tests/components/mcp3204/test.esp32-idf.yaml b/tests/components/mcp3204/test.esp32-idf.yaml new file mode 100644 index 0000000000..c340797c8e --- /dev/null +++ b/tests/components/mcp3204/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_mcp3204 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +mcp3204: + - id: mcp3204_hub + cs_pin: 12 + +sensor: + - platform: mcp3204 + id: mcp3204_sensor + mcp3204_id: mcp3204_hub + number: 0 + update_interval: 5s diff --git a/tests/components/mcp3204/test.esp8266-ard.yaml b/tests/components/mcp3204/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d208e3e06c --- /dev/null +++ b/tests/components/mcp3204/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_mcp3204 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +mcp3204: + - id: mcp3204_hub + cs_pin: 15 + +sensor: + - platform: mcp3204 + id: mcp3204_sensor + mcp3204_id: mcp3204_hub + number: 0 + update_interval: 5s diff --git a/tests/components/mcp3204/test.rp2040-ard.yaml b/tests/components/mcp3204/test.rp2040-ard.yaml new file mode 100644 index 0000000000..63f30e3621 --- /dev/null +++ b/tests/components/mcp3204/test.rp2040-ard.yaml @@ -0,0 +1,16 @@ +spi: + - id: spi_mcp3204 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +mcp3204: + - id: mcp3204_hub + cs_pin: 6 + +sensor: + - platform: mcp3204 + id: mcp3204_sensor + mcp3204_id: mcp3204_hub + number: 0 + update_interval: 5s diff --git a/tests/components/mcp4725/test.esp32-ard.yaml b/tests/components/mcp4725/test.esp32-ard.yaml new file mode 100644 index 0000000000..a523ad95e1 --- /dev/null +++ b/tests/components/mcp4725/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp4725 + scl: 16 + sda: 17 + +output: + - platform: mcp4725 + id: mcp4725_dac_output diff --git a/tests/components/mcp4725/test.esp32-c3-ard.yaml b/tests/components/mcp4725/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5fc799203d --- /dev/null +++ b/tests/components/mcp4725/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp4725 + scl: 5 + sda: 4 + +output: + - platform: mcp4725 + id: mcp4725_dac_output diff --git a/tests/components/mcp4725/test.esp32-c3-idf.yaml b/tests/components/mcp4725/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5fc799203d --- /dev/null +++ b/tests/components/mcp4725/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp4725 + scl: 5 + sda: 4 + +output: + - platform: mcp4725 + id: mcp4725_dac_output diff --git a/tests/components/mcp4725/test.esp32-idf.yaml b/tests/components/mcp4725/test.esp32-idf.yaml new file mode 100644 index 0000000000..a523ad95e1 --- /dev/null +++ b/tests/components/mcp4725/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp4725 + scl: 16 + sda: 17 + +output: + - platform: mcp4725 + id: mcp4725_dac_output diff --git a/tests/components/mcp4725/test.esp8266-ard.yaml b/tests/components/mcp4725/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5fc799203d --- /dev/null +++ b/tests/components/mcp4725/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp4725 + scl: 5 + sda: 4 + +output: + - platform: mcp4725 + id: mcp4725_dac_output diff --git a/tests/components/mcp4725/test.rp2040-ard.yaml b/tests/components/mcp4725/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5fc799203d --- /dev/null +++ b/tests/components/mcp4725/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp4725 + scl: 5 + sda: 4 + +output: + - platform: mcp4725 + id: mcp4725_dac_output diff --git a/tests/components/mcp4728/test.esp32-ard.yaml b/tests/components/mcp4728/test.esp32-ard.yaml new file mode 100644 index 0000000000..b29a6ee53c --- /dev/null +++ b/tests/components/mcp4728/test.esp32-ard.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_mcp4728 + scl: 16 + sda: 17 + +mcp4728: + - id: mcp4728_dac + +output: + - platform: mcp4728 + id: mcp4728_dac_output_a + channel: A + vref: vdd + power_down: normal + - platform: mcp4728 + id: mcp4728_dac_output_b + channel: B + vref: internal + gain: X1 + power_down: gnd_1k + - platform: mcp4728 + id: mcp4728_dac_output_c + channel: C + vref: vdd + power_down: gnd_100k + - platform: mcp4728 + id: mcp4728_dac_output_d + channel: D + vref: internal + gain: X2 + power_down: gnd_500k diff --git a/tests/components/mcp4728/test.esp32-c3-ard.yaml b/tests/components/mcp4728/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2f24dd0b8c --- /dev/null +++ b/tests/components/mcp4728/test.esp32-c3-ard.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_mcp4728 + scl: 5 + sda: 4 + +mcp4728: + - id: mcp4728_dac + +output: + - platform: mcp4728 + id: mcp4728_dac_output_a + channel: A + vref: vdd + power_down: normal + - platform: mcp4728 + id: mcp4728_dac_output_b + channel: B + vref: internal + gain: X1 + power_down: gnd_1k + - platform: mcp4728 + id: mcp4728_dac_output_c + channel: C + vref: vdd + power_down: gnd_100k + - platform: mcp4728 + id: mcp4728_dac_output_d + channel: D + vref: internal + gain: X2 + power_down: gnd_500k diff --git a/tests/components/mcp4728/test.esp32-c3-idf.yaml b/tests/components/mcp4728/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2f24dd0b8c --- /dev/null +++ b/tests/components/mcp4728/test.esp32-c3-idf.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_mcp4728 + scl: 5 + sda: 4 + +mcp4728: + - id: mcp4728_dac + +output: + - platform: mcp4728 + id: mcp4728_dac_output_a + channel: A + vref: vdd + power_down: normal + - platform: mcp4728 + id: mcp4728_dac_output_b + channel: B + vref: internal + gain: X1 + power_down: gnd_1k + - platform: mcp4728 + id: mcp4728_dac_output_c + channel: C + vref: vdd + power_down: gnd_100k + - platform: mcp4728 + id: mcp4728_dac_output_d + channel: D + vref: internal + gain: X2 + power_down: gnd_500k diff --git a/tests/components/mcp4728/test.esp32-idf.yaml b/tests/components/mcp4728/test.esp32-idf.yaml new file mode 100644 index 0000000000..b29a6ee53c --- /dev/null +++ b/tests/components/mcp4728/test.esp32-idf.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_mcp4728 + scl: 16 + sda: 17 + +mcp4728: + - id: mcp4728_dac + +output: + - platform: mcp4728 + id: mcp4728_dac_output_a + channel: A + vref: vdd + power_down: normal + - platform: mcp4728 + id: mcp4728_dac_output_b + channel: B + vref: internal + gain: X1 + power_down: gnd_1k + - platform: mcp4728 + id: mcp4728_dac_output_c + channel: C + vref: vdd + power_down: gnd_100k + - platform: mcp4728 + id: mcp4728_dac_output_d + channel: D + vref: internal + gain: X2 + power_down: gnd_500k diff --git a/tests/components/mcp4728/test.esp8266-ard.yaml b/tests/components/mcp4728/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2f24dd0b8c --- /dev/null +++ b/tests/components/mcp4728/test.esp8266-ard.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_mcp4728 + scl: 5 + sda: 4 + +mcp4728: + - id: mcp4728_dac + +output: + - platform: mcp4728 + id: mcp4728_dac_output_a + channel: A + vref: vdd + power_down: normal + - platform: mcp4728 + id: mcp4728_dac_output_b + channel: B + vref: internal + gain: X1 + power_down: gnd_1k + - platform: mcp4728 + id: mcp4728_dac_output_c + channel: C + vref: vdd + power_down: gnd_100k + - platform: mcp4728 + id: mcp4728_dac_output_d + channel: D + vref: internal + gain: X2 + power_down: gnd_500k diff --git a/tests/components/mcp4728/test.rp2040-ard.yaml b/tests/components/mcp4728/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2f24dd0b8c --- /dev/null +++ b/tests/components/mcp4728/test.rp2040-ard.yaml @@ -0,0 +1,31 @@ +i2c: + - id: i2c_mcp4728 + scl: 5 + sda: 4 + +mcp4728: + - id: mcp4728_dac + +output: + - platform: mcp4728 + id: mcp4728_dac_output_a + channel: A + vref: vdd + power_down: normal + - platform: mcp4728 + id: mcp4728_dac_output_b + channel: B + vref: internal + gain: X1 + power_down: gnd_1k + - platform: mcp4728 + id: mcp4728_dac_output_c + channel: C + vref: vdd + power_down: gnd_100k + - platform: mcp4728 + id: mcp4728_dac_output_d + channel: D + vref: internal + gain: X2 + power_down: gnd_500k diff --git a/tests/components/mcp47a1/test.esp32-ard.yaml b/tests/components/mcp47a1/test.esp32-ard.yaml new file mode 100644 index 0000000000..9e2133de66 --- /dev/null +++ b/tests/components/mcp47a1/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp47a1 + scl: 16 + sda: 17 + +output: + - platform: mcp47a1 + id: output_mcp47a1 diff --git a/tests/components/mcp47a1/test.esp32-c3-ard.yaml b/tests/components/mcp47a1/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..68273e00eb --- /dev/null +++ b/tests/components/mcp47a1/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp47a1 + scl: 5 + sda: 4 + +output: + - platform: mcp47a1 + id: output_mcp47a1 diff --git a/tests/components/mcp47a1/test.esp32-c3-idf.yaml b/tests/components/mcp47a1/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..68273e00eb --- /dev/null +++ b/tests/components/mcp47a1/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp47a1 + scl: 5 + sda: 4 + +output: + - platform: mcp47a1 + id: output_mcp47a1 diff --git a/tests/components/mcp47a1/test.esp32-idf.yaml b/tests/components/mcp47a1/test.esp32-idf.yaml new file mode 100644 index 0000000000..9e2133de66 --- /dev/null +++ b/tests/components/mcp47a1/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp47a1 + scl: 16 + sda: 17 + +output: + - platform: mcp47a1 + id: output_mcp47a1 diff --git a/tests/components/mcp47a1/test.esp8266-ard.yaml b/tests/components/mcp47a1/test.esp8266-ard.yaml new file mode 100644 index 0000000000..68273e00eb --- /dev/null +++ b/tests/components/mcp47a1/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp47a1 + scl: 5 + sda: 4 + +output: + - platform: mcp47a1 + id: output_mcp47a1 diff --git a/tests/components/mcp47a1/test.rp2040-ard.yaml b/tests/components/mcp47a1/test.rp2040-ard.yaml new file mode 100644 index 0000000000..68273e00eb --- /dev/null +++ b/tests/components/mcp47a1/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp47a1 + scl: 5 + sda: 4 + +output: + - platform: mcp47a1 + id: output_mcp47a1 diff --git a/tests/components/mcp9600/test.esp32-ard.yaml b/tests/components/mcp9600/test.esp32-ard.yaml new file mode 100644 index 0000000000..0c94f099ae --- /dev/null +++ b/tests/components/mcp9600/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp9600 + scl: 16 + sda: 17 + +sensor: + - platform: mcp9600 + thermocouple_type: K + hot_junction: + name: Thermocouple Temperature + cold_junction: + name: Ambient Temperature diff --git a/tests/components/mcp9600/test.esp32-c3-ard.yaml b/tests/components/mcp9600/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b07f4589ce --- /dev/null +++ b/tests/components/mcp9600/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp9600 + scl: 5 + sda: 4 + +sensor: + - platform: mcp9600 + thermocouple_type: K + hot_junction: + name: Thermocouple Temperature + cold_junction: + name: Ambient Temperature diff --git a/tests/components/mcp9600/test.esp32-c3-idf.yaml b/tests/components/mcp9600/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b07f4589ce --- /dev/null +++ b/tests/components/mcp9600/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp9600 + scl: 5 + sda: 4 + +sensor: + - platform: mcp9600 + thermocouple_type: K + hot_junction: + name: Thermocouple Temperature + cold_junction: + name: Ambient Temperature diff --git a/tests/components/mcp9600/test.esp32-idf.yaml b/tests/components/mcp9600/test.esp32-idf.yaml new file mode 100644 index 0000000000..0c94f099ae --- /dev/null +++ b/tests/components/mcp9600/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp9600 + scl: 16 + sda: 17 + +sensor: + - platform: mcp9600 + thermocouple_type: K + hot_junction: + name: Thermocouple Temperature + cold_junction: + name: Ambient Temperature diff --git a/tests/components/mcp9600/test.esp8266-ard.yaml b/tests/components/mcp9600/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b07f4589ce --- /dev/null +++ b/tests/components/mcp9600/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp9600 + scl: 5 + sda: 4 + +sensor: + - platform: mcp9600 + thermocouple_type: K + hot_junction: + name: Thermocouple Temperature + cold_junction: + name: Ambient Temperature diff --git a/tests/components/mcp9600/test.rp2040-ard.yaml b/tests/components/mcp9600/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b07f4589ce --- /dev/null +++ b/tests/components/mcp9600/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp9600 + scl: 5 + sda: 4 + +sensor: + - platform: mcp9600 + thermocouple_type: K + hot_junction: + name: Thermocouple Temperature + cold_junction: + name: Ambient Temperature diff --git a/tests/components/mcp9808/test.esp32-ard.yaml b/tests/components/mcp9808/test.esp32-ard.yaml new file mode 100644 index 0000000000..1e5affdac0 --- /dev/null +++ b/tests/components/mcp9808/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp9808 + scl: 16 + sda: 17 + +sensor: + - platform: mcp9808 + name: MCP9808 Temperature diff --git a/tests/components/mcp9808/test.esp32-c3-ard.yaml b/tests/components/mcp9808/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..86b4d7f181 --- /dev/null +++ b/tests/components/mcp9808/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp9808 + scl: 5 + sda: 4 + +sensor: + - platform: mcp9808 + name: MCP9808 Temperature diff --git a/tests/components/mcp9808/test.esp32-c3-idf.yaml b/tests/components/mcp9808/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..86b4d7f181 --- /dev/null +++ b/tests/components/mcp9808/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp9808 + scl: 5 + sda: 4 + +sensor: + - platform: mcp9808 + name: MCP9808 Temperature diff --git a/tests/components/mcp9808/test.esp32-idf.yaml b/tests/components/mcp9808/test.esp32-idf.yaml new file mode 100644 index 0000000000..1e5affdac0 --- /dev/null +++ b/tests/components/mcp9808/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp9808 + scl: 16 + sda: 17 + +sensor: + - platform: mcp9808 + name: MCP9808 Temperature diff --git a/tests/components/mcp9808/test.esp8266-ard.yaml b/tests/components/mcp9808/test.esp8266-ard.yaml new file mode 100644 index 0000000000..86b4d7f181 --- /dev/null +++ b/tests/components/mcp9808/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp9808 + scl: 5 + sda: 4 + +sensor: + - platform: mcp9808 + name: MCP9808 Temperature diff --git a/tests/components/mcp9808/test.rp2040-ard.yaml b/tests/components/mcp9808/test.rp2040-ard.yaml new file mode 100644 index 0000000000..86b4d7f181 --- /dev/null +++ b/tests/components/mcp9808/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_mcp9808 + scl: 5 + sda: 4 + +sensor: + - platform: mcp9808 + name: MCP9808 Temperature diff --git a/tests/components/mdns/common-enabled.yaml b/tests/components/mdns/common-enabled.yaml new file mode 100644 index 0000000000..bc31e32783 --- /dev/null +++ b/tests/components/mdns/common-enabled.yaml @@ -0,0 +1,6 @@ +wifi: + ssid: MySSID + password: password1 + +mdns: + disabled: false diff --git a/tests/components/mdns/test-disabled.esp32-idf.yaml b/tests/components/mdns/test-disabled.esp32-idf.yaml new file mode 100644 index 0000000000..07c70bf248 --- /dev/null +++ b/tests/components/mdns/test-disabled.esp32-idf.yaml @@ -0,0 +1,6 @@ +wifi: + ssid: MySSID + password: password1 + +mdns: + disabled: true diff --git a/tests/components/mdns/test-enabled.esp32-ard.yaml b/tests/components/mdns/test-enabled.esp32-ard.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/mdns/test-enabled.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.esp32-c3-ard.yaml b/tests/components/mdns/test-enabled.esp32-c3-ard.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/mdns/test-enabled.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.esp32-c3-idf.yaml b/tests/components/mdns/test-enabled.esp32-c3-idf.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/mdns/test-enabled.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.esp32-idf.yaml b/tests/components/mdns/test-enabled.esp32-idf.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/mdns/test-enabled.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.esp8266-ard.yaml b/tests/components/mdns/test-enabled.esp8266-ard.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/mdns/test-enabled.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.rp2040-ard.yaml b/tests/components/mdns/test-enabled.rp2040-ard.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/mdns/test-enabled.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml new file mode 100644 index 0000000000..af0d5c7765 --- /dev/null +++ b/tests/components/media_player/common.yaml @@ -0,0 +1,36 @@ +wifi: + ssid: MySSID + password: password1 + +i2s_audio: + i2s_lrclk_pin: 13 + i2s_bclk_pin: 14 + i2s_mclk_pin: 15 + +media_player: + - platform: i2s_audio + name: None + dac_type: external + i2s_dout_pin: 18 + mute_pin: 19 + on_state: + - media_player.play: + - media_player.play_media: http://localhost/media.mp3 + - media_player.play_media: !lambda 'return "http://localhost/media.mp3";' + on_idle: + - media_player.pause: + on_play: + - media_player.stop: + on_pause: + - media_player.toggle: + - wait_until: + media_player.is_idle: + - wait_until: + media_player.is_playing: + - wait_until: + media_player.is_announcing: + - wait_until: + media_player.is_paused: + - media_player.volume_up: + - media_player.volume_down: + - media_player.volume_set: 50% diff --git a/tests/components/media_player/test.esp32-ard.yaml b/tests/components/media_player/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/media_player/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mhz19/test.esp32-ard.yaml b/tests/components/mhz19/test.esp32-ard.yaml new file mode 100644 index 0000000000..0e30713b54 --- /dev/null +++ b/tests/components/mhz19/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_mhz19 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: mhz19 + co2: + name: MH-Z19 CO2 Value + temperature: + name: MH-Z19 Temperature + automatic_baseline_calibration: false + update_interval: 15s diff --git a/tests/components/mhz19/test.esp32-c3-ard.yaml b/tests/components/mhz19/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..1edfa49c23 --- /dev/null +++ b/tests/components/mhz19/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_mhz19 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: mhz19 + co2: + name: MH-Z19 CO2 Value + temperature: + name: MH-Z19 Temperature + automatic_baseline_calibration: false + update_interval: 15s diff --git a/tests/components/mhz19/test.esp32-c3-idf.yaml b/tests/components/mhz19/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..1edfa49c23 --- /dev/null +++ b/tests/components/mhz19/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_mhz19 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: mhz19 + co2: + name: MH-Z19 CO2 Value + temperature: + name: MH-Z19 Temperature + automatic_baseline_calibration: false + update_interval: 15s diff --git a/tests/components/mhz19/test.esp32-idf.yaml b/tests/components/mhz19/test.esp32-idf.yaml new file mode 100644 index 0000000000..0e30713b54 --- /dev/null +++ b/tests/components/mhz19/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_mhz19 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: mhz19 + co2: + name: MH-Z19 CO2 Value + temperature: + name: MH-Z19 Temperature + automatic_baseline_calibration: false + update_interval: 15s diff --git a/tests/components/mhz19/test.esp8266-ard.yaml b/tests/components/mhz19/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1edfa49c23 --- /dev/null +++ b/tests/components/mhz19/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_mhz19 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: mhz19 + co2: + name: MH-Z19 CO2 Value + temperature: + name: MH-Z19 Temperature + automatic_baseline_calibration: false + update_interval: 15s diff --git a/tests/components/mhz19/test.rp2040-ard.yaml b/tests/components/mhz19/test.rp2040-ard.yaml new file mode 100644 index 0000000000..1edfa49c23 --- /dev/null +++ b/tests/components/mhz19/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_mhz19 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: mhz19 + co2: + name: MH-Z19 CO2 Value + temperature: + name: MH-Z19 Temperature + automatic_baseline_calibration: false + update_interval: 15s diff --git a/tests/components/micro_wake_word/common.yaml b/tests/components/micro_wake_word/common.yaml new file mode 100644 index 0000000000..8bd7345307 --- /dev/null +++ b/tests/components/micro_wake_word/common.yaml @@ -0,0 +1,19 @@ +i2s_audio: + i2s_lrclk_pin: GPIO18 + i2s_bclk_pin: GPIO19 + +microphone: + - platform: i2s_audio + id: echo_microphone + i2s_din_pin: GPIO17 + adc_type: external + pdm: true + +micro_wake_word: + on_wake_word_detected: + - logger.log: "Wake word detected" + models: + - model: hey_jarvis + probability_cutoff: 0.7 + - model: okay_nabu + sliding_window_size: 5 diff --git a/tests/components/micro_wake_word/test.esp32-idf.yaml b/tests/components/micro_wake_word/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/micro_wake_word/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/micro_wake_word/test.esp32-s3-idf.yaml b/tests/components/micro_wake_word/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/micro_wake_word/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/micronova/test.esp32-ard.yaml b/tests/components/micronova/test.esp32-ard.yaml new file mode 100644 index 0000000000..9156f7d6a9 --- /dev/null +++ b/tests/components/micronova/test.esp32-ard.yaml @@ -0,0 +1,49 @@ +uart: + - id: uart_micronova + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +micronova: + enable_rx_pin: 18 + +button: + - platform: micronova + custom_button: + name: Custom Micronova Button + memory_location: 0xA0 + memory_address: 0x7D + memory_data: 0x0F + +number: + - platform: micronova + thermostat_temperature: + name: Micronova Thermostaat + step: 1 + power_level: + name: Micronova Power level + +sensor: + - platform: micronova + room_temperature: + name: Room Temperature + fumes_temperature: + name: Fumes Temperature + water_temperature: + name: Water temperature + water_pressure: + name: Water pressure + stove_power: + name: Stove Power + fan_speed: + fan_rpm_offset: 240 + name: Fan RPM + memory_address_sensor: + memory_location: 0x20 + memory_address: 0x7d + name: Adres sensor + +switch: + - platform: micronova + stove: + name: Stove on/off diff --git a/tests/components/micronova/test.esp32-c3-ard.yaml b/tests/components/micronova/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ec9699909e --- /dev/null +++ b/tests/components/micronova/test.esp32-c3-ard.yaml @@ -0,0 +1,49 @@ +uart: + - id: uart_micronova + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +micronova: + enable_rx_pin: 6 + +button: + - platform: micronova + custom_button: + name: Custom Micronova Button + memory_location: 0xA0 + memory_address: 0x7D + memory_data: 0x0F + +number: + - platform: micronova + thermostat_temperature: + name: Micronova Thermostaat + step: 1 + power_level: + name: Micronova Power level + +sensor: + - platform: micronova + room_temperature: + name: Room Temperature + fumes_temperature: + name: Fumes Temperature + water_temperature: + name: Water temperature + water_pressure: + name: Water pressure + stove_power: + name: Stove Power + fan_speed: + fan_rpm_offset: 240 + name: Fan RPM + memory_address_sensor: + memory_location: 0x20 + memory_address: 0x7d + name: Adres sensor + +switch: + - platform: micronova + stove: + name: Stove on/off diff --git a/tests/components/micronova/test.esp32-c3-idf.yaml b/tests/components/micronova/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ec9699909e --- /dev/null +++ b/tests/components/micronova/test.esp32-c3-idf.yaml @@ -0,0 +1,49 @@ +uart: + - id: uart_micronova + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +micronova: + enable_rx_pin: 6 + +button: + - platform: micronova + custom_button: + name: Custom Micronova Button + memory_location: 0xA0 + memory_address: 0x7D + memory_data: 0x0F + +number: + - platform: micronova + thermostat_temperature: + name: Micronova Thermostaat + step: 1 + power_level: + name: Micronova Power level + +sensor: + - platform: micronova + room_temperature: + name: Room Temperature + fumes_temperature: + name: Fumes Temperature + water_temperature: + name: Water temperature + water_pressure: + name: Water pressure + stove_power: + name: Stove Power + fan_speed: + fan_rpm_offset: 240 + name: Fan RPM + memory_address_sensor: + memory_location: 0x20 + memory_address: 0x7d + name: Adres sensor + +switch: + - platform: micronova + stove: + name: Stove on/off diff --git a/tests/components/micronova/test.esp32-idf.yaml b/tests/components/micronova/test.esp32-idf.yaml new file mode 100644 index 0000000000..9156f7d6a9 --- /dev/null +++ b/tests/components/micronova/test.esp32-idf.yaml @@ -0,0 +1,49 @@ +uart: + - id: uart_micronova + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +micronova: + enable_rx_pin: 18 + +button: + - platform: micronova + custom_button: + name: Custom Micronova Button + memory_location: 0xA0 + memory_address: 0x7D + memory_data: 0x0F + +number: + - platform: micronova + thermostat_temperature: + name: Micronova Thermostaat + step: 1 + power_level: + name: Micronova Power level + +sensor: + - platform: micronova + room_temperature: + name: Room Temperature + fumes_temperature: + name: Fumes Temperature + water_temperature: + name: Water temperature + water_pressure: + name: Water pressure + stove_power: + name: Stove Power + fan_speed: + fan_rpm_offset: 240 + name: Fan RPM + memory_address_sensor: + memory_location: 0x20 + memory_address: 0x7d + name: Adres sensor + +switch: + - platform: micronova + stove: + name: Stove on/off diff --git a/tests/components/micronova/test.esp8266-ard.yaml b/tests/components/micronova/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d10ab7ad7a --- /dev/null +++ b/tests/components/micronova/test.esp8266-ard.yaml @@ -0,0 +1,49 @@ +uart: + - id: uart_micronova + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +micronova: + enable_rx_pin: 16 + +button: + - platform: micronova + custom_button: + name: Custom Micronova Button + memory_location: 0xA0 + memory_address: 0x7D + memory_data: 0x0F + +number: + - platform: micronova + thermostat_temperature: + name: Micronova Thermostaat + step: 1 + power_level: + name: Micronova Power level + +sensor: + - platform: micronova + room_temperature: + name: Room Temperature + fumes_temperature: + name: Fumes Temperature + water_temperature: + name: Water temperature + water_pressure: + name: Water pressure + stove_power: + name: Stove Power + fan_speed: + fan_rpm_offset: 240 + name: Fan RPM + memory_address_sensor: + memory_location: 0x20 + memory_address: 0x7d + name: Adres sensor + +switch: + - platform: micronova + stove: + name: Stove on/off diff --git a/tests/components/micronova/test.rp2040-ard.yaml b/tests/components/micronova/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ec9699909e --- /dev/null +++ b/tests/components/micronova/test.rp2040-ard.yaml @@ -0,0 +1,49 @@ +uart: + - id: uart_micronova + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +micronova: + enable_rx_pin: 6 + +button: + - platform: micronova + custom_button: + name: Custom Micronova Button + memory_location: 0xA0 + memory_address: 0x7D + memory_data: 0x0F + +number: + - platform: micronova + thermostat_temperature: + name: Micronova Thermostaat + step: 1 + power_level: + name: Micronova Power level + +sensor: + - platform: micronova + room_temperature: + name: Room Temperature + fumes_temperature: + name: Fumes Temperature + water_temperature: + name: Water temperature + water_pressure: + name: Water pressure + stove_power: + name: Stove Power + fan_speed: + fan_rpm_offset: 240 + name: Fan RPM + memory_address_sensor: + memory_location: 0x20 + memory_address: 0x7d + name: Adres sensor + +switch: + - platform: micronova + stove: + name: Stove on/off diff --git a/tests/components/microphone/test.esp32-ard.yaml b/tests/components/microphone/test.esp32-ard.yaml new file mode 100644 index 0000000000..166eedb54d --- /dev/null +++ b/tests/components/microphone/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2s_audio: + i2s_lrclk_pin: 13 + i2s_bclk_pin: 14 + i2s_mclk_pin: 15 + +microphone: + - platform: i2s_audio + id: mic_id_adc + adc_pin: 32 + adc_type: internal + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: 33 + adc_type: external + pdm: false diff --git a/tests/components/microphone/test.esp32-c3-ard.yaml b/tests/components/microphone/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..706a38f910 --- /dev/null +++ b/tests/components/microphone/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2s_audio: + i2s_lrclk_pin: 6 + i2s_bclk_pin: 7 + i2s_mclk_pin: 8 + +microphone: + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: 3 + adc_type: external + pdm: false diff --git a/tests/components/microphone/test.esp32-c3-idf.yaml b/tests/components/microphone/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..706a38f910 --- /dev/null +++ b/tests/components/microphone/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2s_audio: + i2s_lrclk_pin: 6 + i2s_bclk_pin: 7 + i2s_mclk_pin: 8 + +microphone: + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: 3 + adc_type: external + pdm: false diff --git a/tests/components/microphone/test.esp32-idf.yaml b/tests/components/microphone/test.esp32-idf.yaml new file mode 100644 index 0000000000..166eedb54d --- /dev/null +++ b/tests/components/microphone/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2s_audio: + i2s_lrclk_pin: 13 + i2s_bclk_pin: 14 + i2s_mclk_pin: 15 + +microphone: + - platform: i2s_audio + id: mic_id_adc + adc_pin: 32 + adc_type: internal + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: 33 + adc_type: external + pdm: false diff --git a/tests/components/mics_4514/test.esp32-ard.yaml b/tests/components/mics_4514/test.esp32-ard.yaml new file mode 100644 index 0000000000..234839c91c --- /dev/null +++ b/tests/components/mics_4514/test.esp32-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mics_4514 + scl: 16 + sda: 17 + +sensor: + - platform: mics_4514 + update_interval: 60s + nitrogen_dioxide: + name: MICS-4514 NO2 + carbon_monoxide: + name: MICS-4514 CO + methane: + name: MICS-4514 CH4 + hydrogen: + name: MICS-4514 H2 + ethanol: + name: MICS-4514 C2H5OH + ammonia: + name: MICS-4514 NH3 diff --git a/tests/components/mics_4514/test.esp32-c3-ard.yaml b/tests/components/mics_4514/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..72369bec01 --- /dev/null +++ b/tests/components/mics_4514/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mics_4514 + scl: 5 + sda: 4 + +sensor: + - platform: mics_4514 + update_interval: 60s + nitrogen_dioxide: + name: MICS-4514 NO2 + carbon_monoxide: + name: MICS-4514 CO + methane: + name: MICS-4514 CH4 + hydrogen: + name: MICS-4514 H2 + ethanol: + name: MICS-4514 C2H5OH + ammonia: + name: MICS-4514 NH3 diff --git a/tests/components/mics_4514/test.esp32-c3-idf.yaml b/tests/components/mics_4514/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..72369bec01 --- /dev/null +++ b/tests/components/mics_4514/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mics_4514 + scl: 5 + sda: 4 + +sensor: + - platform: mics_4514 + update_interval: 60s + nitrogen_dioxide: + name: MICS-4514 NO2 + carbon_monoxide: + name: MICS-4514 CO + methane: + name: MICS-4514 CH4 + hydrogen: + name: MICS-4514 H2 + ethanol: + name: MICS-4514 C2H5OH + ammonia: + name: MICS-4514 NH3 diff --git a/tests/components/mics_4514/test.esp32-idf.yaml b/tests/components/mics_4514/test.esp32-idf.yaml new file mode 100644 index 0000000000..234839c91c --- /dev/null +++ b/tests/components/mics_4514/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mics_4514 + scl: 16 + sda: 17 + +sensor: + - platform: mics_4514 + update_interval: 60s + nitrogen_dioxide: + name: MICS-4514 NO2 + carbon_monoxide: + name: MICS-4514 CO + methane: + name: MICS-4514 CH4 + hydrogen: + name: MICS-4514 H2 + ethanol: + name: MICS-4514 C2H5OH + ammonia: + name: MICS-4514 NH3 diff --git a/tests/components/mics_4514/test.esp8266-ard.yaml b/tests/components/mics_4514/test.esp8266-ard.yaml new file mode 100644 index 0000000000..72369bec01 --- /dev/null +++ b/tests/components/mics_4514/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mics_4514 + scl: 5 + sda: 4 + +sensor: + - platform: mics_4514 + update_interval: 60s + nitrogen_dioxide: + name: MICS-4514 NO2 + carbon_monoxide: + name: MICS-4514 CO + methane: + name: MICS-4514 CH4 + hydrogen: + name: MICS-4514 H2 + ethanol: + name: MICS-4514 C2H5OH + ammonia: + name: MICS-4514 NH3 diff --git a/tests/components/mics_4514/test.rp2040-ard.yaml b/tests/components/mics_4514/test.rp2040-ard.yaml new file mode 100644 index 0000000000..72369bec01 --- /dev/null +++ b/tests/components/mics_4514/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mics_4514 + scl: 5 + sda: 4 + +sensor: + - platform: mics_4514 + update_interval: 60s + nitrogen_dioxide: + name: MICS-4514 NO2 + carbon_monoxide: + name: MICS-4514 CO + methane: + name: MICS-4514 CH4 + hydrogen: + name: MICS-4514 H2 + ethanol: + name: MICS-4514 C2H5OH + ammonia: + name: MICS-4514 NH3 diff --git a/tests/components/midea/test.esp32-ard.yaml b/tests/components/midea/test.esp32-ard.yaml new file mode 100644 index 0000000000..5c638b9613 --- /dev/null +++ b/tests/components/midea/test.esp32-ard.yaml @@ -0,0 +1,59 @@ +wifi: + ssid: MySSID + password: password1 + +remote_transmitter: + pin: 18 + carrier_duty_percent: 50% + +uart: + - id: uart_midea + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +climate: + - platform: midea + id: midea_unit + name: Midea Climate + on_control: + - logger.log: Control message received! + - lambda: |- + x.set_mode(CLIMATE_MODE_FAN_ONLY); + on_state: + - logger.log: State changed! + transmitter_id: + period: 1s + num_attempts: 5 + timeout: 2s + beeper: false + autoconf: true + visual: + min_temperature: 17 °C + max_temperature: 30 °C + temperature_step: 0.5 °C + supported_modes: + - FAN_ONLY + - HEAT_COOL + - COOL + - HEAT + - DRY + custom_fan_modes: + - SILENT + - TURBO + supported_presets: + - ECO + - BOOST + - SLEEP + custom_presets: + - FREEZE_PROTECTION + supported_swing_modes: + - VERTICAL + - HORIZONTAL + - BOTH + outdoor_temperature: + name: Temp + power_usage: + name: Power + humidity_setpoint: + name: Humidity diff --git a/tests/components/midea/test.esp32-c3-ard.yaml b/tests/components/midea/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..bcb8635eaf --- /dev/null +++ b/tests/components/midea/test.esp32-c3-ard.yaml @@ -0,0 +1,59 @@ +wifi: + ssid: MySSID + password: password1 + +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +uart: + - id: uart_midea + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +climate: + - platform: midea + id: midea_unit + name: Midea Climate + on_control: + - logger.log: Control message received! + - lambda: |- + x.set_mode(CLIMATE_MODE_FAN_ONLY); + on_state: + - logger.log: State changed! + transmitter_id: + period: 1s + num_attempts: 5 + timeout: 2s + beeper: false + autoconf: true + visual: + min_temperature: 17 °C + max_temperature: 30 °C + temperature_step: 0.5 °C + supported_modes: + - FAN_ONLY + - HEAT_COOL + - COOL + - HEAT + - DRY + custom_fan_modes: + - SILENT + - TURBO + supported_presets: + - ECO + - BOOST + - SLEEP + custom_presets: + - FREEZE_PROTECTION + supported_swing_modes: + - VERTICAL + - HORIZONTAL + - BOTH + outdoor_temperature: + name: Temp + power_usage: + name: Power + humidity_setpoint: + name: Humidity diff --git a/tests/components/midea/test.esp8266-ard.yaml b/tests/components/midea/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b0ed7eb472 --- /dev/null +++ b/tests/components/midea/test.esp8266-ard.yaml @@ -0,0 +1,59 @@ +wifi: + ssid: MySSID + password: password1 + +remote_transmitter: + pin: 12 + carrier_duty_percent: 50% + +uart: + - id: uart_midea + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +climate: + - platform: midea + id: midea_unit + name: Midea Climate + on_control: + - logger.log: Control message received! + - lambda: |- + x.set_mode(CLIMATE_MODE_FAN_ONLY); + on_state: + - logger.log: State changed! + transmitter_id: + period: 1s + num_attempts: 5 + timeout: 2s + beeper: false + autoconf: true + visual: + min_temperature: 17 °C + max_temperature: 30 °C + temperature_step: 0.5 °C + supported_modes: + - FAN_ONLY + - HEAT_COOL + - COOL + - HEAT + - DRY + custom_fan_modes: + - SILENT + - TURBO + supported_presets: + - ECO + - BOOST + - SLEEP + custom_presets: + - FREEZE_PROTECTION + supported_swing_modes: + - VERTICAL + - HORIZONTAL + - BOTH + outdoor_temperature: + name: Temp + power_usage: + name: Power + humidity_setpoint: + name: Humidity diff --git a/tests/components/midea_ir/common.yaml b/tests/components/midea_ir/common.yaml new file mode 100644 index 0000000000..e8d89cecc2 --- /dev/null +++ b/tests/components/midea_ir/common.yaml @@ -0,0 +1,8 @@ +remote_transmitter: + pin: 4 + carrier_duty_percent: 50% + +climate: + - platform: midea_ir + name: Midea IR + use_fahrenheit: true diff --git a/tests/components/midea_ir/test.esp32-ard.yaml b/tests/components/midea_ir/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/midea_ir/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-c3-ard.yaml b/tests/components/midea_ir/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/midea_ir/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-c3-idf.yaml b/tests/components/midea_ir/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/midea_ir/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-idf.yaml b/tests/components/midea_ir/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/midea_ir/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp8266-ard.yaml b/tests/components/midea_ir/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/midea_ir/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mitsubishi/common.yaml b/tests/components/mitsubishi/common.yaml new file mode 100644 index 0000000000..c0fc959c5b --- /dev/null +++ b/tests/components/mitsubishi/common.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 4 + carrier_duty_percent: 50% + +climate: + - platform: mitsubishi + name: Mitsubishi diff --git a/tests/components/mitsubishi/test.esp32-ard.yaml b/tests/components/mitsubishi/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mitsubishi/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp32-c3-ard.yaml b/tests/components/mitsubishi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mitsubishi/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp32-c3-idf.yaml b/tests/components/mitsubishi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mitsubishi/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp32-idf.yaml b/tests/components/mitsubishi/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mitsubishi/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp8266-ard.yaml b/tests/components/mitsubishi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mitsubishi/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-ard.yaml b/tests/components/mlx90393/test.esp32-ard.yaml new file mode 100644 index 0000000000..089fd136f4 --- /dev/null +++ b/tests/components/mlx90393/test.esp32-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mlx90393 + scl: 16 + sda: 17 + +sensor: + - platform: mlx90393 + oversampling: 1 + filter: 0 + gain: 3X + x_axis: + name: mlxxaxis + y_axis: + name: mlxyaxis + z_axis: + name: mlxzaxis + resolution: 17BIT + temperature: + name: mlxtemp + oversampling: 2 diff --git a/tests/components/mlx90393/test.esp32-c3-ard.yaml b/tests/components/mlx90393/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..549eea8032 --- /dev/null +++ b/tests/components/mlx90393/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mlx90393 + scl: 5 + sda: 4 + +sensor: + - platform: mlx90393 + oversampling: 1 + filter: 0 + gain: 3X + x_axis: + name: mlxxaxis + y_axis: + name: mlxyaxis + z_axis: + name: mlxzaxis + resolution: 17BIT + temperature: + name: mlxtemp + oversampling: 2 diff --git a/tests/components/mlx90393/test.esp32-c3-idf.yaml b/tests/components/mlx90393/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..549eea8032 --- /dev/null +++ b/tests/components/mlx90393/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mlx90393 + scl: 5 + sda: 4 + +sensor: + - platform: mlx90393 + oversampling: 1 + filter: 0 + gain: 3X + x_axis: + name: mlxxaxis + y_axis: + name: mlxyaxis + z_axis: + name: mlxzaxis + resolution: 17BIT + temperature: + name: mlxtemp + oversampling: 2 diff --git a/tests/components/mlx90393/test.esp32-idf.yaml b/tests/components/mlx90393/test.esp32-idf.yaml new file mode 100644 index 0000000000..089fd136f4 --- /dev/null +++ b/tests/components/mlx90393/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mlx90393 + scl: 16 + sda: 17 + +sensor: + - platform: mlx90393 + oversampling: 1 + filter: 0 + gain: 3X + x_axis: + name: mlxxaxis + y_axis: + name: mlxyaxis + z_axis: + name: mlxzaxis + resolution: 17BIT + temperature: + name: mlxtemp + oversampling: 2 diff --git a/tests/components/mlx90393/test.esp8266-ard.yaml b/tests/components/mlx90393/test.esp8266-ard.yaml new file mode 100644 index 0000000000..549eea8032 --- /dev/null +++ b/tests/components/mlx90393/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mlx90393 + scl: 5 + sda: 4 + +sensor: + - platform: mlx90393 + oversampling: 1 + filter: 0 + gain: 3X + x_axis: + name: mlxxaxis + y_axis: + name: mlxyaxis + z_axis: + name: mlxzaxis + resolution: 17BIT + temperature: + name: mlxtemp + oversampling: 2 diff --git a/tests/components/mlx90393/test.rp2040-ard.yaml b/tests/components/mlx90393/test.rp2040-ard.yaml new file mode 100644 index 0000000000..549eea8032 --- /dev/null +++ b/tests/components/mlx90393/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_mlx90393 + scl: 5 + sda: 4 + +sensor: + - platform: mlx90393 + oversampling: 1 + filter: 0 + gain: 3X + x_axis: + name: mlxxaxis + y_axis: + name: mlxyaxis + z_axis: + name: mlxzaxis + resolution: 17BIT + temperature: + name: mlxtemp + oversampling: 2 diff --git a/tests/components/mlx90614/test.esp32-ard.yaml b/tests/components/mlx90614/test.esp32-ard.yaml new file mode 100644 index 0000000000..8c1ee68f42 --- /dev/null +++ b/tests/components/mlx90614/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mlx90614 + scl: 16 + sda: 17 + +sensor: + - platform: mlx90614 + ambient: + name: Ambient + object: + name: Object + emissivity: 1.0 diff --git a/tests/components/mlx90614/test.esp32-c3-ard.yaml b/tests/components/mlx90614/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a863e0ee2e --- /dev/null +++ b/tests/components/mlx90614/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mlx90614 + scl: 5 + sda: 4 + +sensor: + - platform: mlx90614 + ambient: + name: Ambient + object: + name: Object + emissivity: 1.0 diff --git a/tests/components/mlx90614/test.esp32-c3-idf.yaml b/tests/components/mlx90614/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a863e0ee2e --- /dev/null +++ b/tests/components/mlx90614/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mlx90614 + scl: 5 + sda: 4 + +sensor: + - platform: mlx90614 + ambient: + name: Ambient + object: + name: Object + emissivity: 1.0 diff --git a/tests/components/mlx90614/test.esp32-idf.yaml b/tests/components/mlx90614/test.esp32-idf.yaml new file mode 100644 index 0000000000..8c1ee68f42 --- /dev/null +++ b/tests/components/mlx90614/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mlx90614 + scl: 16 + sda: 17 + +sensor: + - platform: mlx90614 + ambient: + name: Ambient + object: + name: Object + emissivity: 1.0 diff --git a/tests/components/mlx90614/test.esp8266-ard.yaml b/tests/components/mlx90614/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a863e0ee2e --- /dev/null +++ b/tests/components/mlx90614/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mlx90614 + scl: 5 + sda: 4 + +sensor: + - platform: mlx90614 + ambient: + name: Ambient + object: + name: Object + emissivity: 1.0 diff --git a/tests/components/mlx90614/test.rp2040-ard.yaml b/tests/components/mlx90614/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a863e0ee2e --- /dev/null +++ b/tests/components/mlx90614/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mlx90614 + scl: 5 + sda: 4 + +sensor: + - platform: mlx90614 + ambient: + name: Ambient + object: + name: Object + emissivity: 1.0 diff --git a/tests/components/mmc5603/test.esp32-ard.yaml b/tests/components/mmc5603/test.esp32-ard.yaml new file mode 100644 index 0000000000..fbb83cd9f8 --- /dev/null +++ b/tests/components/mmc5603/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_mmc5603 + scl: 16 + sda: 17 + +sensor: + - platform: mmc5603 + address: 0x30 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z diff --git a/tests/components/mmc5603/test.esp32-c3-ard.yaml b/tests/components/mmc5603/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..834591bb39 --- /dev/null +++ b/tests/components/mmc5603/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_mmc5603 + scl: 5 + sda: 4 + +sensor: + - platform: mmc5603 + address: 0x30 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z diff --git a/tests/components/mmc5603/test.esp32-c3-idf.yaml b/tests/components/mmc5603/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..834591bb39 --- /dev/null +++ b/tests/components/mmc5603/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_mmc5603 + scl: 5 + sda: 4 + +sensor: + - platform: mmc5603 + address: 0x30 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z diff --git a/tests/components/mmc5603/test.esp32-idf.yaml b/tests/components/mmc5603/test.esp32-idf.yaml new file mode 100644 index 0000000000..fbb83cd9f8 --- /dev/null +++ b/tests/components/mmc5603/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_mmc5603 + scl: 16 + sda: 17 + +sensor: + - platform: mmc5603 + address: 0x30 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z diff --git a/tests/components/mmc5603/test.esp8266-ard.yaml b/tests/components/mmc5603/test.esp8266-ard.yaml new file mode 100644 index 0000000000..834591bb39 --- /dev/null +++ b/tests/components/mmc5603/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_mmc5603 + scl: 5 + sda: 4 + +sensor: + - platform: mmc5603 + address: 0x30 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z diff --git a/tests/components/mmc5603/test.rp2040-ard.yaml b/tests/components/mmc5603/test.rp2040-ard.yaml new file mode 100644 index 0000000000..834591bb39 --- /dev/null +++ b/tests/components/mmc5603/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +i2c: + - id: i2c_mmc5603 + scl: 5 + sda: 4 + +sensor: + - platform: mmc5603 + address: 0x30 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z diff --git a/tests/components/mmc5983/test.esp32-ard.yaml b/tests/components/mmc5983/test.esp32-ard.yaml new file mode 100644 index 0000000000..6104be9b83 --- /dev/null +++ b/tests/components/mmc5983/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_mmc5983 + scl: 16 + sda: 17 + +sensor: + - platform: mmc5983 + field_strength_x: + name: "Magnet X" + id: magnet_x + field_strength_y: + name: "Magnet Y" + id: magnet_y + field_strength_z: + name: "Magnet Z" + id: magnet_z diff --git a/tests/components/mmc5983/test.esp32-c3-ard.yaml b/tests/components/mmc5983/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..68d821e9a5 --- /dev/null +++ b/tests/components/mmc5983/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_mmc5983 + scl: 5 + sda: 4 + +sensor: + - platform: mmc5983 + field_strength_x: + name: "Magnet X" + id: magnet_x + field_strength_y: + name: "Magnet Y" + id: magnet_y + field_strength_z: + name: "Magnet Z" + id: magnet_z diff --git a/tests/components/mmc5983/test.esp32-c3-idf.yaml b/tests/components/mmc5983/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..68d821e9a5 --- /dev/null +++ b/tests/components/mmc5983/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_mmc5983 + scl: 5 + sda: 4 + +sensor: + - platform: mmc5983 + field_strength_x: + name: "Magnet X" + id: magnet_x + field_strength_y: + name: "Magnet Y" + id: magnet_y + field_strength_z: + name: "Magnet Z" + id: magnet_z diff --git a/tests/components/mmc5983/test.esp32-idf.yaml b/tests/components/mmc5983/test.esp32-idf.yaml new file mode 100644 index 0000000000..6104be9b83 --- /dev/null +++ b/tests/components/mmc5983/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_mmc5983 + scl: 16 + sda: 17 + +sensor: + - platform: mmc5983 + field_strength_x: + name: "Magnet X" + id: magnet_x + field_strength_y: + name: "Magnet Y" + id: magnet_y + field_strength_z: + name: "Magnet Z" + id: magnet_z diff --git a/tests/components/mmc5983/test.esp8266-ard.yaml b/tests/components/mmc5983/test.esp8266-ard.yaml new file mode 100644 index 0000000000..68d821e9a5 --- /dev/null +++ b/tests/components/mmc5983/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_mmc5983 + scl: 5 + sda: 4 + +sensor: + - platform: mmc5983 + field_strength_x: + name: "Magnet X" + id: magnet_x + field_strength_y: + name: "Magnet Y" + id: magnet_y + field_strength_z: + name: "Magnet Z" + id: magnet_z diff --git a/tests/components/mmc5983/test.rp2040-ard.yaml b/tests/components/mmc5983/test.rp2040-ard.yaml new file mode 100644 index 0000000000..68d821e9a5 --- /dev/null +++ b/tests/components/mmc5983/test.rp2040-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_mmc5983 + scl: 5 + sda: 4 + +sensor: + - platform: mmc5983 + field_strength_x: + name: "Magnet X" + id: magnet_x + field_strength_y: + name: "Magnet Y" + id: magnet_y + field_strength_z: + name: "Magnet Z" + id: magnet_z diff --git a/tests/components/modbus/test.esp32-ard.yaml b/tests/components/modbus/test.esp32-ard.yaml new file mode 100644 index 0000000000..20cf238b1b --- /dev/null +++ b/tests/components/modbus/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +uart: + - id: uart_modbus + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 15 diff --git a/tests/components/modbus/test.esp32-c3-ard.yaml b/tests/components/modbus/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d22b507be0 --- /dev/null +++ b/tests/components/modbus/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +uart: + - id: uart_modbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 6 diff --git a/tests/components/modbus/test.esp32-c3-idf.yaml b/tests/components/modbus/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d22b507be0 --- /dev/null +++ b/tests/components/modbus/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +uart: + - id: uart_modbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 6 diff --git a/tests/components/modbus/test.esp32-idf.yaml b/tests/components/modbus/test.esp32-idf.yaml new file mode 100644 index 0000000000..20cf238b1b --- /dev/null +++ b/tests/components/modbus/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +uart: + - id: uart_modbus + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 15 diff --git a/tests/components/modbus/test.esp8266-ard.yaml b/tests/components/modbus/test.esp8266-ard.yaml new file mode 100644 index 0000000000..560c044766 --- /dev/null +++ b/tests/components/modbus/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +uart: + - id: uart_modbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 12 diff --git a/tests/components/modbus/test.rp2040-ard.yaml b/tests/components/modbus/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d22b507be0 --- /dev/null +++ b/tests/components/modbus/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +uart: + - id: uart_modbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 6 diff --git a/tests/components/modbus_controller/test.esp32-ard.yaml b/tests/components/modbus_controller/test.esp32-ard.yaml new file mode 100644 index 0000000000..cd95d149cb --- /dev/null +++ b/tests/components/modbus_controller/test.esp32-ard.yaml @@ -0,0 +1,32 @@ +uart: + - id: uart_modbus_client + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + - id: uart_modbus_server + tx_pin: 1 + rx_pin: 3 + baud_rate: 9600 + +modbus: + - id: mod_bus1 + uart_id: uart_modbus_client + flow_control_pin: 15 + - id: mod_bus2 + uart_id: uart_modbus_server + role: server + +modbus_controller: + - id: modbus_controller1 + address: 0x2 + modbus_id: mod_bus1 + allow_duplicate_commands: false + - id: modbus_controller2 + address: 0x2 + modbus_id: mod_bus2 + server_registers: + - address: 0x0000 + value_type: S_DWORD_R + read_lambda: |- + return 42.3; + max_cmd_retries: 0 diff --git a/tests/components/modbus_controller/test.esp32-c3-ard.yaml b/tests/components/modbus_controller/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..476e65ecb0 --- /dev/null +++ b/tests/components/modbus_controller/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_modbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 6 + +modbus_controller: + - id: modbus_controller1 + address: 0x2 + modbus_id: mod_bus1 diff --git a/tests/components/modbus_controller/test.esp32-c3-idf.yaml b/tests/components/modbus_controller/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..476e65ecb0 --- /dev/null +++ b/tests/components/modbus_controller/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_modbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 6 + +modbus_controller: + - id: modbus_controller1 + address: 0x2 + modbus_id: mod_bus1 diff --git a/tests/components/modbus_controller/test.esp32-idf.yaml b/tests/components/modbus_controller/test.esp32-idf.yaml new file mode 100644 index 0000000000..ba28e94d73 --- /dev/null +++ b/tests/components/modbus_controller/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +uart: + - id: uart_modbus + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 15 + +modbus_controller: + - id: modbus_controller1 + address: 0x2 + modbus_id: mod_bus1 + allow_duplicate_commands: true + max_cmd_retries: 10 diff --git a/tests/components/modbus_controller/test.esp8266-ard.yaml b/tests/components/modbus_controller/test.esp8266-ard.yaml new file mode 100644 index 0000000000..67cac65d1b --- /dev/null +++ b/tests/components/modbus_controller/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_modbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 12 + +modbus_controller: + - id: modbus_controller1 + address: 0x2 + modbus_id: mod_bus1 diff --git a/tests/components/modbus_controller/test.rp2040-ard.yaml b/tests/components/modbus_controller/test.rp2040-ard.yaml new file mode 100644 index 0000000000..476e65ecb0 --- /dev/null +++ b/tests/components/modbus_controller/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_modbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + id: mod_bus1 + flow_control_pin: 6 + +modbus_controller: + - id: modbus_controller1 + address: 0x2 + modbus_id: mod_bus1 diff --git a/tests/components/monochromatic/test.esp32-ard.yaml b/tests/components/monochromatic/test.esp32-ard.yaml new file mode 100644 index 0000000000..9524efcb2d --- /dev/null +++ b/tests/components/monochromatic/test.esp32-ard.yaml @@ -0,0 +1,40 @@ +output: + - platform: ledc + id: light_output_1 + pin: 4 + +light: + - platform: monochromatic + name: Monochromatic Light + id: monochromatic_light + output: light_output_1 + gamma_correct: 2.8 + default_transition_length: 2s + effects: + - strobe: + - flicker: + - flicker: + name: My Flicker + alpha: 98% + intensity: 1.5% + - lambda: + name: My Custom Effect + update_interval: 1s + lambda: |- + static int state = 0; + state += 1; + if (state == 4) + state = 0; + - pulse: + transition_length: 10s + update_interval: 20s + min_brightness: 10% + max_brightness: 90% + - pulse: + name: pulse2 + transition_length: + on_length: 10s + off_length: 5s + update_interval: 15s + min_brightness: 10% + max_brightness: 90% diff --git a/tests/components/monochromatic/test.esp32-c3-ard.yaml b/tests/components/monochromatic/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9524efcb2d --- /dev/null +++ b/tests/components/monochromatic/test.esp32-c3-ard.yaml @@ -0,0 +1,40 @@ +output: + - platform: ledc + id: light_output_1 + pin: 4 + +light: + - platform: monochromatic + name: Monochromatic Light + id: monochromatic_light + output: light_output_1 + gamma_correct: 2.8 + default_transition_length: 2s + effects: + - strobe: + - flicker: + - flicker: + name: My Flicker + alpha: 98% + intensity: 1.5% + - lambda: + name: My Custom Effect + update_interval: 1s + lambda: |- + static int state = 0; + state += 1; + if (state == 4) + state = 0; + - pulse: + transition_length: 10s + update_interval: 20s + min_brightness: 10% + max_brightness: 90% + - pulse: + name: pulse2 + transition_length: + on_length: 10s + off_length: 5s + update_interval: 15s + min_brightness: 10% + max_brightness: 90% diff --git a/tests/components/monochromatic/test.esp32-c3-idf.yaml b/tests/components/monochromatic/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9524efcb2d --- /dev/null +++ b/tests/components/monochromatic/test.esp32-c3-idf.yaml @@ -0,0 +1,40 @@ +output: + - platform: ledc + id: light_output_1 + pin: 4 + +light: + - platform: monochromatic + name: Monochromatic Light + id: monochromatic_light + output: light_output_1 + gamma_correct: 2.8 + default_transition_length: 2s + effects: + - strobe: + - flicker: + - flicker: + name: My Flicker + alpha: 98% + intensity: 1.5% + - lambda: + name: My Custom Effect + update_interval: 1s + lambda: |- + static int state = 0; + state += 1; + if (state == 4) + state = 0; + - pulse: + transition_length: 10s + update_interval: 20s + min_brightness: 10% + max_brightness: 90% + - pulse: + name: pulse2 + transition_length: + on_length: 10s + off_length: 5s + update_interval: 15s + min_brightness: 10% + max_brightness: 90% diff --git a/tests/components/monochromatic/test.esp32-idf.yaml b/tests/components/monochromatic/test.esp32-idf.yaml new file mode 100644 index 0000000000..9524efcb2d --- /dev/null +++ b/tests/components/monochromatic/test.esp32-idf.yaml @@ -0,0 +1,40 @@ +output: + - platform: ledc + id: light_output_1 + pin: 4 + +light: + - platform: monochromatic + name: Monochromatic Light + id: monochromatic_light + output: light_output_1 + gamma_correct: 2.8 + default_transition_length: 2s + effects: + - strobe: + - flicker: + - flicker: + name: My Flicker + alpha: 98% + intensity: 1.5% + - lambda: + name: My Custom Effect + update_interval: 1s + lambda: |- + static int state = 0; + state += 1; + if (state == 4) + state = 0; + - pulse: + transition_length: 10s + update_interval: 20s + min_brightness: 10% + max_brightness: 90% + - pulse: + name: pulse2 + transition_length: + on_length: 10s + off_length: 5s + update_interval: 15s + min_brightness: 10% + max_brightness: 90% diff --git a/tests/components/monochromatic/test.esp8266-ard.yaml b/tests/components/monochromatic/test.esp8266-ard.yaml new file mode 100644 index 0000000000..94d849581d --- /dev/null +++ b/tests/components/monochromatic/test.esp8266-ard.yaml @@ -0,0 +1,40 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 4 + +light: + - platform: monochromatic + name: Monochromatic Light + id: monochromatic_light + output: light_output_1 + gamma_correct: 2.8 + default_transition_length: 2s + effects: + - strobe: + - flicker: + - flicker: + name: My Flicker + alpha: 98% + intensity: 1.5% + - lambda: + name: My Custom Effect + update_interval: 1s + lambda: |- + static int state = 0; + state += 1; + if (state == 4) + state = 0; + - pulse: + transition_length: 10s + update_interval: 20s + min_brightness: 10% + max_brightness: 90% + - pulse: + name: pulse2 + transition_length: + on_length: 10s + off_length: 5s + update_interval: 15s + min_brightness: 10% + max_brightness: 90% diff --git a/tests/components/monochromatic/test.rp2040-ard.yaml b/tests/components/monochromatic/test.rp2040-ard.yaml new file mode 100644 index 0000000000..093577e256 --- /dev/null +++ b/tests/components/monochromatic/test.rp2040-ard.yaml @@ -0,0 +1,40 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 4 + +light: + - platform: monochromatic + name: Monochromatic Light + id: monochromatic_light + output: light_output_1 + gamma_correct: 2.8 + default_transition_length: 2s + effects: + - strobe: + - flicker: + - flicker: + name: My Flicker + alpha: 98% + intensity: 1.5% + - lambda: + name: My Custom Effect + update_interval: 1s + lambda: |- + static int state = 0; + state += 1; + if (state == 4) + state = 0; + - pulse: + transition_length: 10s + update_interval: 20s + min_brightness: 10% + max_brightness: 90% + - pulse: + name: pulse2 + transition_length: + on_length: 10s + off_length: 5s + update_interval: 15s + min_brightness: 10% + max_brightness: 90% diff --git a/tests/components/mopeka_ble/common.yaml b/tests/components/mopeka_ble/common.yaml new file mode 100644 index 0000000000..a115404f1c --- /dev/null +++ b/tests/components/mopeka_ble/common.yaml @@ -0,0 +1,3 @@ +esp32_ble_tracker: + +mopeka_ble: diff --git a/tests/components/mopeka_ble/test.esp32-ard.yaml b/tests/components/mopeka_ble/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_ble/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-c3-ard.yaml b/tests/components/mopeka_ble/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_ble/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-c3-idf.yaml b/tests/components/mopeka_ble/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_ble/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-idf.yaml b/tests/components/mopeka_ble/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_ble/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/common.yaml b/tests/components/mopeka_pro_check/common.yaml new file mode 100644 index 0000000000..3533ecf631 --- /dev/null +++ b/tests/components/mopeka_pro_check/common.yaml @@ -0,0 +1,33 @@ +esp32_ble_tracker: + +sensor: + - platform: mopeka_pro_check + mac_address: D3:75:F2:DC:16:91 + tank_type: CUSTOM + custom_distance_full: 40cm + custom_distance_empty: 10mm + temperature: + name: Propane test temp + level: + name: Propane test level + distance: + name: Propane test distance + battery_level: + name: Propane test battery level + + - platform: mopeka_pro_check + mac_address: AA:BB:CC:DD:EE:FF + tank_type: 20LB_V + temperature: + name: "Propane test2 temp" + level: + name: "Propane test2 level" + distance: + name: "Propane test2 distance" + battery_level: + name: "Propane test2 battery level" + signal_quality: + name: "propane test2 read quality" + ignored_reads: + name: "propane test2 ignored reads" + minimum_signal_quality: "LOW" diff --git a/tests/components/mopeka_pro_check/test.esp32-ard.yaml b/tests/components/mopeka_pro_check/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_pro_check/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-c3-ard.yaml b/tests/components/mopeka_pro_check/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_pro_check/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml b/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-idf.yaml b/tests/components/mopeka_pro_check/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_pro_check/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/common.yaml b/tests/components/mopeka_std_check/common.yaml new file mode 100644 index 0000000000..383e2e2a19 --- /dev/null +++ b/tests/components/mopeka_std_check/common.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + # Example using 11kg 100% propane tank. + - platform: mopeka_std_check + mac_address: D3:75:F2:DC:16:91 + tank_type: Europe_11kg + temperature: + name: "Propane test temp" + level: + name: "Propane test level" + distance: + name: "Propane test distance" + battery_level: + name: "Propane test battery level" diff --git a/tests/components/mopeka_std_check/test.esp32-ard.yaml b/tests/components/mopeka_std_check/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_std_check/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-c3-ard.yaml b/tests/components/mopeka_std_check/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_std_check/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml b/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-idf.yaml b/tests/components/mopeka_std_check/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/mopeka_std_check/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mpl3115a2/test.esp32-ard.yaml b/tests/components/mpl3115a2/test.esp32-ard.yaml new file mode 100644 index 0000000000..5e9d6d190d --- /dev/null +++ b/tests/components/mpl3115a2/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mpl3115a2 + scl: 16 + sda: 17 + +sensor: + - platform: mpl3115a2 + temperature: + name: MPL3115A2 Temperature + pressure: + name: MPL3115A2 Pressure + update_interval: 10s diff --git a/tests/components/mpl3115a2/test.esp32-c3-ard.yaml b/tests/components/mpl3115a2/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9cbe08d920 --- /dev/null +++ b/tests/components/mpl3115a2/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mpl3115a2 + scl: 5 + sda: 4 + +sensor: + - platform: mpl3115a2 + temperature: + name: MPL3115A2 Temperature + pressure: + name: MPL3115A2 Pressure + update_interval: 10s diff --git a/tests/components/mpl3115a2/test.esp32-c3-idf.yaml b/tests/components/mpl3115a2/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9cbe08d920 --- /dev/null +++ b/tests/components/mpl3115a2/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mpl3115a2 + scl: 5 + sda: 4 + +sensor: + - platform: mpl3115a2 + temperature: + name: MPL3115A2 Temperature + pressure: + name: MPL3115A2 Pressure + update_interval: 10s diff --git a/tests/components/mpl3115a2/test.esp32-idf.yaml b/tests/components/mpl3115a2/test.esp32-idf.yaml new file mode 100644 index 0000000000..5e9d6d190d --- /dev/null +++ b/tests/components/mpl3115a2/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mpl3115a2 + scl: 16 + sda: 17 + +sensor: + - platform: mpl3115a2 + temperature: + name: MPL3115A2 Temperature + pressure: + name: MPL3115A2 Pressure + update_interval: 10s diff --git a/tests/components/mpl3115a2/test.esp8266-ard.yaml b/tests/components/mpl3115a2/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9cbe08d920 --- /dev/null +++ b/tests/components/mpl3115a2/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mpl3115a2 + scl: 5 + sda: 4 + +sensor: + - platform: mpl3115a2 + temperature: + name: MPL3115A2 Temperature + pressure: + name: MPL3115A2 Pressure + update_interval: 10s diff --git a/tests/components/mpl3115a2/test.rp2040-ard.yaml b/tests/components/mpl3115a2/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9cbe08d920 --- /dev/null +++ b/tests/components/mpl3115a2/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mpl3115a2 + scl: 5 + sda: 4 + +sensor: + - platform: mpl3115a2 + temperature: + name: MPL3115A2 Temperature + pressure: + name: MPL3115A2 Pressure + update_interval: 10s diff --git a/tests/components/mpr121/common.yaml b/tests/components/mpr121/common.yaml new file mode 100644 index 0000000000..fcf61b57f3 --- /dev/null +++ b/tests/components/mpr121/common.yaml @@ -0,0 +1,41 @@ +i2c: + - id: i2c_mpr121 + scl: ${i2c_scl} + sda: ${i2c_sda} + +mpr121: + id: mpr121_first + address: 0x5A + +binary_sensor: + - platform: mpr121 + id: touchkey0 + name: touchkey0 + channel: 0 + - platform: mpr121 + id: bin1 + name: touchkey1 + channel: 1 + - platform: mpr121 + id: bin2 + name: touchkey2 + channel: 2 + - platform: mpr121 + id: bin3 + name: touchkey3 + channel: 6 + +output: + - platform: gpio + id: gpio1 + pin: + mpr121: mpr121_first + number: 7 + mode: OUTPUT + - platform: gpio + id: gpio2 + pin: + mpr121: mpr121_first + number: 11 + mode: OUTPUT + inverted: true diff --git a/tests/components/mpr121/test.esp32-ard.yaml b/tests/components/mpr121/test.esp32-ard.yaml new file mode 100644 index 0000000000..1037d5d35b --- /dev/null +++ b/tests/components/mpr121/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO16 + i2c_sda: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp32-c3-ard.yaml b/tests/components/mpr121/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d7ae0d5161 --- /dev/null +++ b/tests/components/mpr121/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp32-c3-idf.yaml b/tests/components/mpr121/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d7ae0d5161 --- /dev/null +++ b/tests/components/mpr121/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp32-idf.yaml b/tests/components/mpr121/test.esp32-idf.yaml new file mode 100644 index 0000000000..1037d5d35b --- /dev/null +++ b/tests/components/mpr121/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO16 + i2c_sda: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp8266-ard.yaml b/tests/components/mpr121/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d7ae0d5161 --- /dev/null +++ b/tests/components/mpr121/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/mpr121/test.rp2040-ard.yaml b/tests/components/mpr121/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d7ae0d5161 --- /dev/null +++ b/tests/components/mpr121/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/mpu6050/test.esp32-ard.yaml b/tests/components/mpu6050/test.esp32-ard.yaml new file mode 100644 index 0000000000..45bca55dea --- /dev/null +++ b/tests/components/mpu6050/test.esp32-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6050 + scl: 16 + sda: 17 + +sensor: + - platform: mpu6050 + address: 0x68 + accel_x: + name: MPU6050 Accel X + accel_y: + name: MPU6050 Accel Y + accel_z: + name: MPU6050 Accel z + gyro_x: + name: MPU6050 Gyro X + gyro_y: + name: MPU6050 Gyro Y + gyro_z: + name: MPU6050 Gyro z + temperature: + name: MPU6050 Temperature diff --git a/tests/components/mpu6050/test.esp32-c3-ard.yaml b/tests/components/mpu6050/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..39c8506d2b --- /dev/null +++ b/tests/components/mpu6050/test.esp32-c3-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6050 + scl: 5 + sda: 4 + +sensor: + - platform: mpu6050 + address: 0x68 + accel_x: + name: MPU6050 Accel X + accel_y: + name: MPU6050 Accel Y + accel_z: + name: MPU6050 Accel z + gyro_x: + name: MPU6050 Gyro X + gyro_y: + name: MPU6050 Gyro Y + gyro_z: + name: MPU6050 Gyro z + temperature: + name: MPU6050 Temperature diff --git a/tests/components/mpu6050/test.esp32-c3-idf.yaml b/tests/components/mpu6050/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..39c8506d2b --- /dev/null +++ b/tests/components/mpu6050/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6050 + scl: 5 + sda: 4 + +sensor: + - platform: mpu6050 + address: 0x68 + accel_x: + name: MPU6050 Accel X + accel_y: + name: MPU6050 Accel Y + accel_z: + name: MPU6050 Accel z + gyro_x: + name: MPU6050 Gyro X + gyro_y: + name: MPU6050 Gyro Y + gyro_z: + name: MPU6050 Gyro z + temperature: + name: MPU6050 Temperature diff --git a/tests/components/mpu6050/test.esp32-idf.yaml b/tests/components/mpu6050/test.esp32-idf.yaml new file mode 100644 index 0000000000..45bca55dea --- /dev/null +++ b/tests/components/mpu6050/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6050 + scl: 16 + sda: 17 + +sensor: + - platform: mpu6050 + address: 0x68 + accel_x: + name: MPU6050 Accel X + accel_y: + name: MPU6050 Accel Y + accel_z: + name: MPU6050 Accel z + gyro_x: + name: MPU6050 Gyro X + gyro_y: + name: MPU6050 Gyro Y + gyro_z: + name: MPU6050 Gyro z + temperature: + name: MPU6050 Temperature diff --git a/tests/components/mpu6050/test.esp8266-ard.yaml b/tests/components/mpu6050/test.esp8266-ard.yaml new file mode 100644 index 0000000000..39c8506d2b --- /dev/null +++ b/tests/components/mpu6050/test.esp8266-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6050 + scl: 5 + sda: 4 + +sensor: + - platform: mpu6050 + address: 0x68 + accel_x: + name: MPU6050 Accel X + accel_y: + name: MPU6050 Accel Y + accel_z: + name: MPU6050 Accel z + gyro_x: + name: MPU6050 Gyro X + gyro_y: + name: MPU6050 Gyro Y + gyro_z: + name: MPU6050 Gyro z + temperature: + name: MPU6050 Temperature diff --git a/tests/components/mpu6050/test.rp2040-ard.yaml b/tests/components/mpu6050/test.rp2040-ard.yaml new file mode 100644 index 0000000000..39c8506d2b --- /dev/null +++ b/tests/components/mpu6050/test.rp2040-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6050 + scl: 5 + sda: 4 + +sensor: + - platform: mpu6050 + address: 0x68 + accel_x: + name: MPU6050 Accel X + accel_y: + name: MPU6050 Accel Y + accel_z: + name: MPU6050 Accel z + gyro_x: + name: MPU6050 Gyro X + gyro_y: + name: MPU6050 Gyro Y + gyro_z: + name: MPU6050 Gyro z + temperature: + name: MPU6050 Temperature diff --git a/tests/components/mpu6886/test.esp32-ard.yaml b/tests/components/mpu6886/test.esp32-ard.yaml new file mode 100644 index 0000000000..84e4d56739 --- /dev/null +++ b/tests/components/mpu6886/test.esp32-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6886 + scl: 16 + sda: 17 + +sensor: + - platform: mpu6886 + address: 0x68 + accel_x: + name: MPU6886 Accel X + accel_y: + name: MPU6886 Accel Y + accel_z: + name: MPU6886 Accel z + gyro_x: + name: MPU6886 Gyro X + gyro_y: + name: MPU6886 Gyro Y + gyro_z: + name: MPU6886 Gyro z + temperature: + name: MPU6886 Temperature diff --git a/tests/components/mpu6886/test.esp32-c3-ard.yaml b/tests/components/mpu6886/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fad51a80b4 --- /dev/null +++ b/tests/components/mpu6886/test.esp32-c3-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6886 + scl: 5 + sda: 4 + +sensor: + - platform: mpu6886 + address: 0x68 + accel_x: + name: MPU6886 Accel X + accel_y: + name: MPU6886 Accel Y + accel_z: + name: MPU6886 Accel z + gyro_x: + name: MPU6886 Gyro X + gyro_y: + name: MPU6886 Gyro Y + gyro_z: + name: MPU6886 Gyro z + temperature: + name: MPU6886 Temperature diff --git a/tests/components/mpu6886/test.esp32-c3-idf.yaml b/tests/components/mpu6886/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fad51a80b4 --- /dev/null +++ b/tests/components/mpu6886/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6886 + scl: 5 + sda: 4 + +sensor: + - platform: mpu6886 + address: 0x68 + accel_x: + name: MPU6886 Accel X + accel_y: + name: MPU6886 Accel Y + accel_z: + name: MPU6886 Accel z + gyro_x: + name: MPU6886 Gyro X + gyro_y: + name: MPU6886 Gyro Y + gyro_z: + name: MPU6886 Gyro z + temperature: + name: MPU6886 Temperature diff --git a/tests/components/mpu6886/test.esp32-idf.yaml b/tests/components/mpu6886/test.esp32-idf.yaml new file mode 100644 index 0000000000..84e4d56739 --- /dev/null +++ b/tests/components/mpu6886/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6886 + scl: 16 + sda: 17 + +sensor: + - platform: mpu6886 + address: 0x68 + accel_x: + name: MPU6886 Accel X + accel_y: + name: MPU6886 Accel Y + accel_z: + name: MPU6886 Accel z + gyro_x: + name: MPU6886 Gyro X + gyro_y: + name: MPU6886 Gyro Y + gyro_z: + name: MPU6886 Gyro z + temperature: + name: MPU6886 Temperature diff --git a/tests/components/mpu6886/test.esp8266-ard.yaml b/tests/components/mpu6886/test.esp8266-ard.yaml new file mode 100644 index 0000000000..fad51a80b4 --- /dev/null +++ b/tests/components/mpu6886/test.esp8266-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6886 + scl: 5 + sda: 4 + +sensor: + - platform: mpu6886 + address: 0x68 + accel_x: + name: MPU6886 Accel X + accel_y: + name: MPU6886 Accel Y + accel_z: + name: MPU6886 Accel z + gyro_x: + name: MPU6886 Gyro X + gyro_y: + name: MPU6886 Gyro Y + gyro_z: + name: MPU6886 Gyro z + temperature: + name: MPU6886 Temperature diff --git a/tests/components/mpu6886/test.rp2040-ard.yaml b/tests/components/mpu6886/test.rp2040-ard.yaml new file mode 100644 index 0000000000..fad51a80b4 --- /dev/null +++ b/tests/components/mpu6886/test.rp2040-ard.yaml @@ -0,0 +1,22 @@ +i2c: + - id: i2c_mpu6886 + scl: 5 + sda: 4 + +sensor: + - platform: mpu6886 + address: 0x68 + accel_x: + name: MPU6886 Accel X + accel_y: + name: MPU6886 Accel Y + accel_z: + name: MPU6886 Accel z + gyro_x: + name: MPU6886 Gyro X + gyro_y: + name: MPU6886 Gyro Y + gyro_z: + name: MPU6886 Gyro z + temperature: + name: MPU6886 Temperature diff --git a/tests/components/mqtt/common-update.yaml b/tests/components/mqtt/common-update.yaml new file mode 100644 index 0000000000..25f57cfef2 --- /dev/null +++ b/tests/components/mqtt/common-update.yaml @@ -0,0 +1,13 @@ +substitutions: + verify_ssl: "true" + +http_request: + verify_ssl: ${verify_ssl} + +ota: + - platform: http_request + +update: + - platform: http_request + name: "OTA Update" + source: https://example.com/ota.json diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml new file mode 100644 index 0000000000..d22fe9579f --- /dev/null +++ b/tests/components/mqtt/common.yaml @@ -0,0 +1,444 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + +mqtt: + broker: "192.168.178.84" + port: 1883 + username: debug + password: debug + enable_on_boot: false + clean_session: True + client_id: someclient + use_abbreviations: false + discovery: true + discovery_retain: false + discovery_prefix: discovery + discovery_unique_id_generator: legacy + topic_prefix: helloworld + log_topic: + topic: helloworld/hi + level: INFO + birth_message: + will_message: + shutdown_message: + topic: topic/to/send/to + payload: hi + qos: 2 + retain: true + keepalive: 60s + reboot_timeout: 60s + on_message: + - topic: my/custom/topic + qos: 0 + then: + - lambda: >- + ESP_LOGD("main", "Got message %s", x.c_str()); + - topic: bedroom/ota_mode + then: + - logger.log: Got bedroom/ota_mode + - topic: livingroom/ota_mode + then: + - logger.log: Got livingroom/ota_mode + on_json_message: + topic: the/topic + then: + - if: + condition: + - wifi.connected: + - mqtt.connected: + then: + - logger.log: on_json_message + on_connect: + - mqtt.publish: + topic: some/topic + payload: Hello + on_disconnect: + - mqtt.publish: + topic: some/topic + payload: Good-bye + +binary_sensor: + - platform: template + id: some_binary_sensor + name: Garage Door Open + state_topic: some/topic/binary_sensor + qos: 2 + lambda: |- + if (id(template_sens).state > 30) { + // Garage Door is open. + return true; + } else { + // Garage Door is closed. + return false; + } + on_state: + - mqtt.publish: + topic: some/topic/binary_sensor + payload: Hello + qos: 2 + retain: true + +button: + - platform: template + name: "Template Button" + state_topic: some/topic/button + qos: 2 + on_press: + - mqtt.disable + - mqtt.enable + - mqtt.publish: + topic: some/topic/button + payload: Hello + qos: 2 + retain: true + +climate: + - platform: thermostat + name: Test Thermostat + sensor: template_sens + humidity_sensor: template_sens + action_state_topic: some/topicaction_state + current_temperature_state_topic: some/topiccurrent_temperature_state + current_humidity_state_topic: some/topiccurrent_humidity_state + fan_mode_state_topic: some/topicfan_mode_state + fan_mode_command_topic: some/topicfan_mode_command + mode_state_topic: some/topicmode_state + mode_command_topic: some/topicmode_command + preset_state_topic: some/topicpreset_state + preset_command_topic: some/topicpreset_command + swing_mode_state_topic: some/topicswing_mode_state + swing_mode_command_topic: some/topicswing_mode_command + target_temperature_state_topic: some/topictarget_temperature_state + target_temperature_command_topic: some/topictarget_temperature_command + target_temperature_high_state_topic: some/topictarget_temperature_high_state + target_temperature_high_command_topic: some/topictarget_temperature_high_command + target_temperature_low_state_topic: some/topictarget_temperature_low_state + target_temperature_low_command_topic: some/topictarget_temperature_low_command + target_humidity_state_topic: some/topictarget_humidity_state + target_humidity_command_topic: some/topictarget_humidity_command + preset: + - name: Default Preset + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + - name: Away + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C + idle_action: + - logger.log: idle_action + cool_action: + - logger.log: cool_action + supplemental_cooling_action: + - logger.log: supplemental_cooling_action + heat_action: + - logger.log: heat_action + supplemental_heating_action: + - logger.log: supplemental_heating_action + dry_action: + - logger.log: dry_action + fan_only_action: + - logger.log: fan_only_action + auto_mode: + - logger.log: auto_mode + off_mode: + - logger.log: off_mode + heat_mode: + - logger.log: heat_mode + cool_mode: + - logger.log: cool_mode + dry_mode: + - logger.log: dry_mode + fan_only_mode: + - logger.log: fan_only_mode + fan_mode_auto_action: + - logger.log: fan_mode_auto_action + fan_mode_on_action: + - logger.log: fan_mode_on_action + fan_mode_off_action: + - logger.log: fan_mode_off_action + fan_mode_low_action: + - logger.log: fan_mode_low_action + fan_mode_medium_action: + - logger.log: fan_mode_medium_action + fan_mode_high_action: + - logger.log: fan_mode_high_action + fan_mode_middle_action: + - logger.log: fan_mode_middle_action + fan_mode_focus_action: + - logger.log: fan_mode_focus_action + fan_mode_diffuse_action: + - logger.log: fan_mode_diffuse_action + fan_mode_quiet_action: + - logger.log: fan_mode_quiet_action + swing_off_action: + - logger.log: swing_off_action + swing_horizontal_action: + - logger.log: swing_horizontal_action + swing_vertical_action: + - logger.log: swing_vertical_action + swing_both_action: + - logger.log: swing_both_action + startup_delay: true + supplemental_cooling_delta: 2.0 + cool_deadband: 0.5 + cool_overrun: 0.5 + min_cooling_off_time: 300s + min_cooling_run_time: 300s + max_cooling_run_time: 600s + supplemental_heating_delta: 2.0 + heat_deadband: 0.5 + heat_overrun: 0.5 + min_heating_off_time: 300s + min_heating_run_time: 300s + max_heating_run_time: 600s + min_fanning_off_time: 30s + min_fanning_run_time: 30s + min_fan_mode_switching_time: 15s + min_idle_time: 30s + set_point_minimum_differential: 0.5 + fan_only_action_uses_fan_mode_timer: true + fan_only_cooling: true + fan_with_cooling: true + fan_with_heating: true + visual: + temperature_step: + target_temperature: 0.1 + current_temperature: 0.1 + +cover: + - platform: template + name: Template Cover + state_topic: some/topic/cover + qos: 2 + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } else { + return COVER_CLOSED; + } + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + +datetime: + - platform: template + name: Date + id: test_date + type: date + state_topic: some/topic/date + command_topic: test_date/custom_command_topic + qos: 2 + subscribe_qos: 2 + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "Date: %04d-%02d-%02d" + args: + - x.year + - x.month + - x.day_of_month + - platform: template + name: Time + id: test_time + type: time + state_topic: some/topic/time + qos: 2 + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "Time: %02d:%02d:%02d" + args: + - x.hour + - x.minute + - x.second + - platform: template + name: DateTime + id: test_datetime + type: datetime + state_topic: some/topic/datetime + qos: 2 + set_action: + - logger.log: set_value + on_value: + - logger.log: + format: "DateTime: %04d-%02d-%02d %02d:%02d:%02d" + args: + - x.year + - x.month + - x.day_of_month + - x.hour + - x.minute + - x.second + +event: + - platform: template + name: Template Event + state_topic: some/topic/event + qos: 2 + event_types: + - "custom_event_1" + - "custom_event_2" + +fan: + - platform: template + name: Template Fan + state_topic: some/topic/fan + qos: 2 + on_state: + - logger.log: on_state + on_speed_set: + - logger.log: on_speed_set + +light: + - platform: binary + name: Desk Lamp + output: light_output + state_topic: some/topic/light + qos: 2 + +output: + - id: light_output + platform: gpio + pin: 0 + +lock: + - platform: template + name: "Template Lock" + state_topic: some/topic/lock + qos: 2 + lambda: |- + if (id(some_binary_sensor).state) { + return LOCK_STATE_LOCKED; + } else { + return LOCK_STATE_UNLOCKED; + } + lock_action: + - logger.log: lock_action + unlock_action: + - logger.log: unlock_action + open_action: + - logger.log: open_action + +number: + - platform: template + name: "Template number" + state_topic: some/topic/number + qos: 2 + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: "Template select" + state_topic: some/topic/select + qos: 2 + optimistic: true + options: + - one + - two + - three + initial_option: two + +sensor: + - platform: template + name: Template Sensor + id: template_sens + lambda: |- + if (id(some_binary_sensor).state) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + on_value: + - mqtt.publish: + topic: some/topic/sensor + payload: Hello + qos: 2 + retain: true + - platform: mqtt_subscribe + name: MQTT Subscribe Sensor + topic: mqtt/topic + id: the_sensor + qos: 2 + on_value: + - mqtt.publish_json: + topic: the/topic + payload: |- + root["key"] = id(template_sens).state; + root["greeting"] = "Hello World"; + +switch: + - platform: template + name: Template Switch + state_topic: some/topic/switch + qos: 2 + lambda: |- + if (id(some_binary_sensor).state) { + return true; + } else { + return false; + } + turn_on_action: + - logger.log: turn_on_action + turn_off_action: + - logger.log: turn_off_action + +text_sensor: + - platform: template + name: Template Text Sensor + id: tts_text + state_topic: some/topic/text_sensor + qos: 2 + - platform: mqtt_subscribe + name: MQTT Subscribe Text + topic: some/topic/text_sensor + qos: 2 + on_value: + - text_sensor.template.publish: + id: tts_text + state: Hello World + - text_sensor.template.publish: + id: tts_text + state: |- + return "Hello World2"; + +text: + - platform: template + name: Template Text + optimistic: true + min_length: 0 + max_length: 100 + mode: text + state_topic: some/topic/text + qos: 2 + +valve: + - platform: template + name: Template Valve + state_topic: some/topic/valve + qos: 2 + optimistic: true + lambda: |- + if (id(some_binary_sensor).state) { + return VALVE_OPEN; + } else { + return VALVE_CLOSED; + } + +alarm_control_panel: + - platform: template + name: Alarm Control Panel + binary_sensors: + - input: some_binary_sensor diff --git a/tests/components/mqtt/test.bk72xx-ard.yaml b/tests/components/mqtt/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/mqtt/test.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/mqtt/test.esp32-ard.yaml b/tests/components/mqtt/test.esp32-ard.yaml new file mode 100644 index 0000000000..4c70fb37d9 --- /dev/null +++ b/tests/components/mqtt/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + verify_ssl: "false" + +packages: + common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp32-c3-ard.yaml b/tests/components/mqtt/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4c70fb37d9 --- /dev/null +++ b/tests/components/mqtt/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + verify_ssl: "false" + +packages: + common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp32-c3-idf.yaml b/tests/components/mqtt/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d19609b55e --- /dev/null +++ b/tests/components/mqtt/test.esp32-c3-idf.yaml @@ -0,0 +1,3 @@ +packages: + common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp32-idf.yaml b/tests/components/mqtt/test.esp32-idf.yaml new file mode 100644 index 0000000000..d19609b55e --- /dev/null +++ b/tests/components/mqtt/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp8266-ard.yaml b/tests/components/mqtt/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4c70fb37d9 --- /dev/null +++ b/tests/components/mqtt/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + verify_ssl: "false" + +packages: + common: !include common.yaml + update: !include common-update.yaml diff --git a/tests/components/mqtt_subscribe/test.esp32-ard.yaml b/tests/components/mqtt_subscribe/test.esp32-ard.yaml new file mode 100644 index 0000000000..13ed311b17 --- /dev/null +++ b/tests/components/mqtt_subscribe/test.esp32-ard.yaml @@ -0,0 +1,36 @@ +wifi: + ssid: MySSID + password: password1 + +mqtt: + broker: test.mosquitto.org + port: 1883 + discovery: true + discovery_prefix: homeassistant + log_topic: + on_message: + topic: testing/sensor/testing_sensor/state + qos: 0 + then: + - logger.log: Mqtt Test + +sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Sensor + topic: mqtt/topic + id: the_sensor + qos: 2 + on_value: + - mqtt.publish_json: + topic: the/topic + payload: |- + root["key"] = id(the_sensor).state; + root["greeting"] = "Hello World"; + +text_sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Text + topic: "the/topic" + qos: 2 + on_value: + - logger.log: "Text sensor got value" diff --git a/tests/components/mqtt_subscribe/test.esp32-c3-ard.yaml b/tests/components/mqtt_subscribe/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..13ed311b17 --- /dev/null +++ b/tests/components/mqtt_subscribe/test.esp32-c3-ard.yaml @@ -0,0 +1,36 @@ +wifi: + ssid: MySSID + password: password1 + +mqtt: + broker: test.mosquitto.org + port: 1883 + discovery: true + discovery_prefix: homeassistant + log_topic: + on_message: + topic: testing/sensor/testing_sensor/state + qos: 0 + then: + - logger.log: Mqtt Test + +sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Sensor + topic: mqtt/topic + id: the_sensor + qos: 2 + on_value: + - mqtt.publish_json: + topic: the/topic + payload: |- + root["key"] = id(the_sensor).state; + root["greeting"] = "Hello World"; + +text_sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Text + topic: "the/topic" + qos: 2 + on_value: + - logger.log: "Text sensor got value" diff --git a/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml b/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..070672f15c --- /dev/null +++ b/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml @@ -0,0 +1,37 @@ +wifi: + ssid: MySSID + password: password1 + +mqtt: + broker: test.mosquitto.org + port: 1883 + discovery: true + discovery_prefix: homeassistant + idf_send_async: false + log_topic: + on_message: + topic: testing/sensor/testing_sensor/state + qos: 0 + then: + - logger.log: Mqtt Test + +sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Sensor + topic: mqtt/topic + id: the_sensor + qos: 2 + on_value: + - mqtt.publish_json: + topic: the/topic + payload: |- + root["key"] = id(the_sensor).state; + root["greeting"] = "Hello World"; + +text_sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Text + topic: "the/topic" + qos: 2 + on_value: + - logger.log: "Text sensor got value" diff --git a/tests/components/mqtt_subscribe/test.esp32-idf.yaml b/tests/components/mqtt_subscribe/test.esp32-idf.yaml new file mode 100644 index 0000000000..070672f15c --- /dev/null +++ b/tests/components/mqtt_subscribe/test.esp32-idf.yaml @@ -0,0 +1,37 @@ +wifi: + ssid: MySSID + password: password1 + +mqtt: + broker: test.mosquitto.org + port: 1883 + discovery: true + discovery_prefix: homeassistant + idf_send_async: false + log_topic: + on_message: + topic: testing/sensor/testing_sensor/state + qos: 0 + then: + - logger.log: Mqtt Test + +sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Sensor + topic: mqtt/topic + id: the_sensor + qos: 2 + on_value: + - mqtt.publish_json: + topic: the/topic + payload: |- + root["key"] = id(the_sensor).state; + root["greeting"] = "Hello World"; + +text_sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Text + topic: "the/topic" + qos: 2 + on_value: + - logger.log: "Text sensor got value" diff --git a/tests/components/mqtt_subscribe/test.esp8266-ard.yaml b/tests/components/mqtt_subscribe/test.esp8266-ard.yaml new file mode 100644 index 0000000000..13ed311b17 --- /dev/null +++ b/tests/components/mqtt_subscribe/test.esp8266-ard.yaml @@ -0,0 +1,36 @@ +wifi: + ssid: MySSID + password: password1 + +mqtt: + broker: test.mosquitto.org + port: 1883 + discovery: true + discovery_prefix: homeassistant + log_topic: + on_message: + topic: testing/sensor/testing_sensor/state + qos: 0 + then: + - logger.log: Mqtt Test + +sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Sensor + topic: mqtt/topic + id: the_sensor + qos: 2 + on_value: + - mqtt.publish_json: + topic: the/topic + payload: |- + root["key"] = id(the_sensor).state; + root["greeting"] = "Hello World"; + +text_sensor: + - platform: mqtt_subscribe + name: MQTT Subscribe Text + topic: "the/topic" + qos: 2 + on_value: + - logger.log: "Text sensor got value" diff --git a/tests/components/ms5611/test.esp32-ard.yaml b/tests/components/ms5611/test.esp32-ard.yaml new file mode 100644 index 0000000000..b090eeaa93 --- /dev/null +++ b/tests/components/ms5611/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ms5611 + scl: 16 + sda: 17 + +sensor: + - platform: ms5611 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + address: 0x77 + update_interval: 15s diff --git a/tests/components/ms5611/test.esp32-c3-ard.yaml b/tests/components/ms5611/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8f18501eef --- /dev/null +++ b/tests/components/ms5611/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ms5611 + scl: 5 + sda: 4 + +sensor: + - platform: ms5611 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + address: 0x77 + update_interval: 15s diff --git a/tests/components/ms5611/test.esp32-c3-idf.yaml b/tests/components/ms5611/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8f18501eef --- /dev/null +++ b/tests/components/ms5611/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ms5611 + scl: 5 + sda: 4 + +sensor: + - platform: ms5611 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + address: 0x77 + update_interval: 15s diff --git a/tests/components/ms5611/test.esp32-idf.yaml b/tests/components/ms5611/test.esp32-idf.yaml new file mode 100644 index 0000000000..b090eeaa93 --- /dev/null +++ b/tests/components/ms5611/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ms5611 + scl: 16 + sda: 17 + +sensor: + - platform: ms5611 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + address: 0x77 + update_interval: 15s diff --git a/tests/components/ms5611/test.esp8266-ard.yaml b/tests/components/ms5611/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8f18501eef --- /dev/null +++ b/tests/components/ms5611/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ms5611 + scl: 5 + sda: 4 + +sensor: + - platform: ms5611 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + address: 0x77 + update_interval: 15s diff --git a/tests/components/ms5611/test.rp2040-ard.yaml b/tests/components/ms5611/test.rp2040-ard.yaml new file mode 100644 index 0000000000..8f18501eef --- /dev/null +++ b/tests/components/ms5611/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_ms5611 + scl: 5 + sda: 4 + +sensor: + - platform: ms5611 + temperature: + name: Outside Temperature + pressure: + name: Outside Pressure + address: 0x77 + update_interval: 15s diff --git a/tests/components/my9231/common.yaml b/tests/components/my9231/common.yaml new file mode 100644 index 0000000000..3f2e81ef98 --- /dev/null +++ b/tests/components/my9231/common.yaml @@ -0,0 +1,26 @@ +my9231: + clock_pin: 5 + data_pin: 4 + num_channels: 6 + num_chips: 2 + bit_depth: 16 + +output: + - platform: my9231 + id: my_0 + channel: 0 + - platform: my9231 + id: my_1 + channel: 1 + - platform: my9231 + id: my_2 + channel: 2 + - platform: my9231 + id: my_3 + channel: 3 + - platform: my9231 + id: my_4 + channel: 4 + - platform: my9231 + id: my_5 + channel: 5 diff --git a/tests/components/my9231/test.esp32-ard.yaml b/tests/components/my9231/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/my9231/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/my9231/test.esp32-c3-ard.yaml b/tests/components/my9231/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/my9231/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/my9231/test.esp32-c3-idf.yaml b/tests/components/my9231/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/my9231/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/my9231/test.esp32-idf.yaml b/tests/components/my9231/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/my9231/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/my9231/test.esp8266-ard.yaml b/tests/components/my9231/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/my9231/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/my9231/test.rp2040-ard.yaml b/tests/components/my9231/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/my9231/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/nau7802/common.yaml b/tests/components/nau7802/common.yaml new file mode 100644 index 0000000000..2501911a3f --- /dev/null +++ b/tests/components/nau7802/common.yaml @@ -0,0 +1,13 @@ +sensor: + - platform: nau7802 + id: test_id + name: weight + i2c_id: i2c_nau7802 + gain: 32 + ldo_voltage: "3.0v" + samples_per_second: 10 + on_value: + then: + - nau7802.calibrate_external_offset: test_id + - nau7802.calibrate_internal_offset: test_id + - nau7802.calibrate_gain: test_id diff --git a/tests/components/nau7802/test.esp32-ard.yaml b/tests/components/nau7802/test.esp32-ard.yaml new file mode 100644 index 0000000000..73a4aa4251 --- /dev/null +++ b/tests/components/nau7802/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_nau7802 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/nau7802/test.esp32-c3-ard.yaml b/tests/components/nau7802/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..769468f9ec --- /dev/null +++ b/tests/components/nau7802/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_nau7802 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/nau7802/test.esp32-c3-idf.yaml b/tests/components/nau7802/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..769468f9ec --- /dev/null +++ b/tests/components/nau7802/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_nau7802 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/nau7802/test.esp32-idf.yaml b/tests/components/nau7802/test.esp32-idf.yaml new file mode 100644 index 0000000000..73a4aa4251 --- /dev/null +++ b/tests/components/nau7802/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_nau7802 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/nau7802/test.esp8266-ard.yaml b/tests/components/nau7802/test.esp8266-ard.yaml new file mode 100644 index 0000000000..769468f9ec --- /dev/null +++ b/tests/components/nau7802/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_nau7802 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/nau7802/test.rp2040-ard.yaml b/tests/components/nau7802/test.rp2040-ard.yaml new file mode 100644 index 0000000000..769468f9ec --- /dev/null +++ b/tests/components/nau7802/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_nau7802 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/neopixelbus/test.esp32-ard.yaml b/tests/components/neopixelbus/test.esp32-ard.yaml new file mode 100644 index 0000000000..fd468586e0 --- /dev/null +++ b/tests/components/neopixelbus/test.esp32-ard.yaml @@ -0,0 +1,17 @@ +light: + - platform: neopixelbus + id: addr3 + name: Neopixelbus Light + gamma_correct: 2.8 + color_correct: [0.0, 0.0, 0.0, 0.0] + default_transition_length: 10s + effects: + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% + type: GRBW + variant: SK6812 + method: ESP32_I2S_0 + num_leds: 5 + pin: 4 diff --git a/tests/components/neopixelbus/test.esp32-c3-ard.yaml b/tests/components/neopixelbus/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f2b53ab1e9 --- /dev/null +++ b/tests/components/neopixelbus/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +light: + - platform: neopixelbus + id: addr3 + name: Neopixelbus Light + gamma_correct: 2.8 + color_correct: [0.0, 0.0, 0.0, 0.0] + default_transition_length: 10s + effects: + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% + type: GRBW + variant: SK6812 + method: + type: esp32_rmt + channel: 0 + num_leds: 5 + pin: 4 diff --git a/tests/components/neopixelbus/test.esp8266-ard.yaml b/tests/components/neopixelbus/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2c1f16a38c --- /dev/null +++ b/tests/components/neopixelbus/test.esp8266-ard.yaml @@ -0,0 +1,17 @@ +light: + - platform: neopixelbus + id: addr3 + name: Neopixelbus Light + gamma_correct: 2.8 + color_correct: [0.0, 0.0, 0.0, 0.0] + default_transition_length: 10s + effects: + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% + type: GRBW + variant: SK6812 + method: esp8266_uart + num_leds: 5 + pin: 2 diff --git a/tests/components/network/common.yaml b/tests/components/network/common.yaml new file mode 100644 index 0000000000..dca00cbeb6 --- /dev/null +++ b/tests/components/network/common.yaml @@ -0,0 +1,9 @@ +substitutions: + network_enable_ipv6: "false" + +wifi: + ssid: MySSID + password: password1 + +network: + enable_ipv6: ${network_enable_ipv6} diff --git a/tests/components/network/test-ipv6.bk72xx-ard.yaml b/tests/components/network/test-ipv6.bk72xx-ard.yaml new file mode 100644 index 0000000000..d0c4bbfcb9 --- /dev/null +++ b/tests/components/network/test-ipv6.bk72xx-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + network_enable_ipv6: "true" + +bk72xx: + framework: + version: 1.7.0 + +<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-ard.yaml b/tests/components/network/test-ipv6.esp32-ard.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/network/test-ipv6.esp32-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-c3-ard.yaml b/tests/components/network/test-ipv6.esp32-c3-ard.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/network/test-ipv6.esp32-c3-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-c3-idf.yaml b/tests/components/network/test-ipv6.esp32-c3-idf.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/network/test-ipv6.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-idf.yaml b/tests/components/network/test-ipv6.esp32-idf.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/network/test-ipv6.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp8266-ard.yaml b/tests/components/network/test-ipv6.esp8266-ard.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/network/test-ipv6.esp8266-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.rp2040-ard.yaml b/tests/components/network/test-ipv6.rp2040-ard.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/network/test-ipv6.rp2040-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/network/test.esp32-ard.yaml b/tests/components/network/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/network/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/network/test.esp32-c3-ard.yaml b/tests/components/network/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/network/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/network/test.esp32-c3-idf.yaml b/tests/components/network/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/network/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/network/test.esp32-idf.yaml b/tests/components/network/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/network/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/network/test.esp8266-ard.yaml b/tests/components/network/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/network/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/network/test.host.yaml b/tests/components/network/test.host.yaml new file mode 100644 index 0000000000..61889b0361 --- /dev/null +++ b/tests/components/network/test.host.yaml @@ -0,0 +1 @@ +network: diff --git a/tests/components/network/test.rp2040-ard.yaml b/tests/components/network/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/network/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/nextion/test.esp32-ard.yaml b/tests/components/nextion/test.esp32-ard.yaml new file mode 100644 index 0000000000..27568ebc2a --- /dev/null +++ b/tests/components/nextion/test.esp32-ard.yaml @@ -0,0 +1,60 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_nextion + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +binary_sensor: + - platform: nextion + page_id: 0 + component_id: 2 + name: Nextion Touch Component + - platform: nextion + id: r0_sensor + name: R0 Sensor + component_name: page0.r0 + +sensor: + - platform: nextion + id: testnumber + name: testnumber + variable_name: testnumber + - platform: nextion + id: testwave + name: testwave + component_id: 2 + wave_channel_id: 1 + +switch: + - platform: nextion + id: r0 + name: R0 Switch + component_name: page0.r0 + +text_sensor: + - platform: nextion + name: text0 + id: text0 + update_interval: 4s + component_name: text0 + +display: + - platform: nextion + tft_url: http://esphome.io/default35.tft + update_interval: 5s + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' diff --git a/tests/components/nextion/test.esp32-c3-ard.yaml b/tests/components/nextion/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5881d6e165 --- /dev/null +++ b/tests/components/nextion/test.esp32-c3-ard.yaml @@ -0,0 +1,60 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_nextion + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +binary_sensor: + - platform: nextion + page_id: 0 + component_id: 2 + name: Nextion Touch Component + - platform: nextion + id: r0_sensor + name: R0 Sensor + component_name: page0.r0 + +sensor: + - platform: nextion + id: testnumber + name: testnumber + variable_name: testnumber + - platform: nextion + id: testwave + name: testwave + component_id: 2 + wave_channel_id: 1 + +switch: + - platform: nextion + id: r0 + name: R0 Switch + component_name: page0.r0 + +text_sensor: + - platform: nextion + name: text0 + id: text0 + update_interval: 4s + component_name: text0 + +display: + - platform: nextion + tft_url: http://esphome.io/default35.tft + update_interval: 5s + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' diff --git a/tests/components/nextion/test.esp32-c3-idf.yaml b/tests/components/nextion/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5881d6e165 --- /dev/null +++ b/tests/components/nextion/test.esp32-c3-idf.yaml @@ -0,0 +1,60 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_nextion + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +binary_sensor: + - platform: nextion + page_id: 0 + component_id: 2 + name: Nextion Touch Component + - platform: nextion + id: r0_sensor + name: R0 Sensor + component_name: page0.r0 + +sensor: + - platform: nextion + id: testnumber + name: testnumber + variable_name: testnumber + - platform: nextion + id: testwave + name: testwave + component_id: 2 + wave_channel_id: 1 + +switch: + - platform: nextion + id: r0 + name: R0 Switch + component_name: page0.r0 + +text_sensor: + - platform: nextion + name: text0 + id: text0 + update_interval: 4s + component_name: text0 + +display: + - platform: nextion + tft_url: http://esphome.io/default35.tft + update_interval: 5s + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' diff --git a/tests/components/nextion/test.esp32-idf.yaml b/tests/components/nextion/test.esp32-idf.yaml new file mode 100644 index 0000000000..27568ebc2a --- /dev/null +++ b/tests/components/nextion/test.esp32-idf.yaml @@ -0,0 +1,60 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_nextion + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +binary_sensor: + - platform: nextion + page_id: 0 + component_id: 2 + name: Nextion Touch Component + - platform: nextion + id: r0_sensor + name: R0 Sensor + component_name: page0.r0 + +sensor: + - platform: nextion + id: testnumber + name: testnumber + variable_name: testnumber + - platform: nextion + id: testwave + name: testwave + component_id: 2 + wave_channel_id: 1 + +switch: + - platform: nextion + id: r0 + name: R0 Switch + component_name: page0.r0 + +text_sensor: + - platform: nextion + name: text0 + id: text0 + update_interval: 4s + component_name: text0 + +display: + - platform: nextion + tft_url: http://esphome.io/default35.tft + update_interval: 5s + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' diff --git a/tests/components/nextion/test.esp8266-ard.yaml b/tests/components/nextion/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5881d6e165 --- /dev/null +++ b/tests/components/nextion/test.esp8266-ard.yaml @@ -0,0 +1,60 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_nextion + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +binary_sensor: + - platform: nextion + page_id: 0 + component_id: 2 + name: Nextion Touch Component + - platform: nextion + id: r0_sensor + name: R0 Sensor + component_name: page0.r0 + +sensor: + - platform: nextion + id: testnumber + name: testnumber + variable_name: testnumber + - platform: nextion + id: testwave + name: testwave + component_id: 2 + wave_channel_id: 1 + +switch: + - platform: nextion + id: r0 + name: R0 Switch + component_name: page0.r0 + +text_sensor: + - platform: nextion + name: text0 + id: text0 + update_interval: 4s + component_name: text0 + +display: + - platform: nextion + tft_url: http://esphome.io/default35.tft + update_interval: 5s + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' diff --git a/tests/components/nextion/test.rp2040-ard.yaml b/tests/components/nextion/test.rp2040-ard.yaml new file mode 100644 index 0000000000..a1c5848ce6 --- /dev/null +++ b/tests/components/nextion/test.rp2040-ard.yaml @@ -0,0 +1,55 @@ +uart: + - id: uart_nextion + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +binary_sensor: + - platform: nextion + page_id: 0 + component_id: 2 + name: Nextion Touch Component + - platform: nextion + id: r0_sensor + name: R0 Sensor + component_name: page0.r0 + +sensor: + - platform: nextion + id: testnumber + name: testnumber + variable_name: testnumber + - platform: nextion + id: testwave + name: testwave + component_id: 2 + wave_channel_id: 1 + +switch: + - platform: nextion + id: r0 + name: R0 Switch + component_name: page0.r0 + +text_sensor: + - platform: nextion + name: text0 + id: text0 + update_interval: 4s + component_name: text0 + +display: + - platform: nextion + update_interval: 5s + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' diff --git a/tests/components/noblex/common.yaml b/tests/components/noblex/common.yaml new file mode 100644 index 0000000000..f5e471a9a7 --- /dev/null +++ b/tests/components/noblex/common.yaml @@ -0,0 +1,20 @@ +remote_receiver: + id: rcvr + pin: 4 + dump: all + +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +sensor: + - platform: template + id: noblex_ac_sensor + lambda: "return 21;" + +climate: + - platform: noblex + name: AC Living + id: noblex_ac + sensor: noblex_ac_sensor + receiver_id: rcvr diff --git a/tests/components/noblex/test.esp32-ard.yaml b/tests/components/noblex/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/noblex/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/noblex/test.esp32-c3-ard.yaml b/tests/components/noblex/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/noblex/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/noblex/test.esp32-c3-idf.yaml b/tests/components/noblex/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/noblex/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/noblex/test.esp32-idf.yaml b/tests/components/noblex/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/noblex/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/noblex/test.esp8266-ard.yaml b/tests/components/noblex/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/noblex/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/npi19/common.yaml b/tests/components/npi19/common.yaml new file mode 100644 index 0000000000..012e05695e --- /dev/null +++ b/tests/components/npi19/common.yaml @@ -0,0 +1,16 @@ +i2c: + id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + frequency: 200kHz + +sensor: + - platform: npi19 + update_interval: 1s + i2c_id: i2c_bus + + temperature: + name: water temperature + + raw_pressure: + name: water pressure diff --git a/tests/components/npi19/test.esp32-ard.yaml b/tests/components/npi19/test.esp32-ard.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/npi19/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-idf.yaml b/tests/components/npi19/test.esp32-idf.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/npi19/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-s3-ard.yaml b/tests/components/npi19/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/npi19/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-s3-idf.yaml b/tests/components/npi19/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/npi19/test.esp32-s3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/npi19/test.esp8266-ard.yaml b/tests/components/npi19/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3be5e53dcb --- /dev/null +++ b/tests/components/npi19/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO05 + sda_pin: GPIO04 + +<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-ard.yaml b/tests/components/ntc/test.esp32-ard.yaml new file mode 100644 index 0000000000..3e0e901b6e --- /dev/null +++ b/tests/components/ntc/test.esp32-ard.yaml @@ -0,0 +1,26 @@ +sensor: + - platform: adc + id: my_sensor + pin: 32 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C diff --git a/tests/components/ntc/test.esp32-c3-ard.yaml b/tests/components/ntc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c0edb83d9d --- /dev/null +++ b/tests/components/ntc/test.esp32-c3-ard.yaml @@ -0,0 +1,26 @@ +sensor: + - platform: adc + id: my_sensor + pin: 4 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C diff --git a/tests/components/ntc/test.esp32-idf.yaml b/tests/components/ntc/test.esp32-idf.yaml new file mode 100644 index 0000000000..3e0e901b6e --- /dev/null +++ b/tests/components/ntc/test.esp32-idf.yaml @@ -0,0 +1,26 @@ +sensor: + - platform: adc + id: my_sensor + pin: 32 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C diff --git a/tests/components/ntc/test.esp32-s2-ard.yaml b/tests/components/ntc/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..c0edb83d9d --- /dev/null +++ b/tests/components/ntc/test.esp32-s2-ard.yaml @@ -0,0 +1,26 @@ +sensor: + - platform: adc + id: my_sensor + pin: 4 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C diff --git a/tests/components/ntc/test.esp32-s3-ard.yaml b/tests/components/ntc/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..c0edb83d9d --- /dev/null +++ b/tests/components/ntc/test.esp32-s3-ard.yaml @@ -0,0 +1,26 @@ +sensor: + - platform: adc + id: my_sensor + pin: 4 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C diff --git a/tests/components/ntc/test.esp8266-ard.yaml b/tests/components/ntc/test.esp8266-ard.yaml new file mode 100644 index 0000000000..370d16d3f7 --- /dev/null +++ b/tests/components/ntc/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +sensor: + - platform: adc + id: my_sensor + pin: A0 + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C diff --git a/tests/components/ntc/test.rp2040-ard.yaml b/tests/components/ntc/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9c7ba7b539 --- /dev/null +++ b/tests/components/ntc/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +sensor: + - platform: adc + id: my_sensor + pin: 26 + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C diff --git a/tests/components/online_image/common-esp32.yaml b/tests/components/online_image/common-esp32.yaml new file mode 100644 index 0000000000..787a1ad368 --- /dev/null +++ b/tests/components/online_image/common-esp32.yaml @@ -0,0 +1,19 @@ +<<: !include common.yaml + +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 18 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/common-esp8266.yaml b/tests/components/online_image/common-esp8266.yaml new file mode 100644 index 0000000000..ba15b5025c --- /dev/null +++ b/tests/components/online_image/common-esp8266.yaml @@ -0,0 +1,19 @@ +<<: !include common.yaml + +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 15 + dc_pin: 3 + reset_pin: 1 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml new file mode 100644 index 0000000000..5c6feb4c81 --- /dev/null +++ b/tests/components/online_image/common.yaml @@ -0,0 +1,45 @@ +wifi: + ssid: MySSID + password: password1 + +# Purposely test that `online_image:` does auto-load `image:` +# Keep the `image:` undefined. +# image: +online_image: + - id: online_binary_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: BINARY + resize: 50x50 + - id: online_binary_transparent_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + type: TRANSPARENT_BINARY + format: png + - id: online_rgba_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: RGBA + - id: online_rgb24_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: RGB24 + use_transparency: true + +# Check the set_url action +time: + - platform: sntp + on_time: + - at: "13:37:42" + then: + - online_image.set_url: + id: online_rgba_image + url: http://www.example.org/example.png + - online_image.set_url: + id: online_rgba_image + url: !lambda |- + return "http://www.example.org/example.png"; + - online_image.set_url: + id: online_rgba_image + url: !lambda |- + return str_sprintf("http://homeassistant.local:8123"); + diff --git a/tests/components/online_image/test.esp32-ard.yaml b/tests/components/online_image/test.esp32-ard.yaml new file mode 100644 index 0000000000..4111cbd0ad --- /dev/null +++ b/tests/components/online_image/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +<<: !include common-esp32.yaml + +http_request: + verify_ssl: false diff --git a/tests/components/online_image/test.esp32-idf.yaml b/tests/components/online_image/test.esp32-idf.yaml new file mode 100644 index 0000000000..3f01009812 --- /dev/null +++ b/tests/components/online_image/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +<<: !include common-esp32.yaml + +http_request: + diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml new file mode 100644 index 0000000000..27cbae280a --- /dev/null +++ b/tests/components/opentherm/common.yaml @@ -0,0 +1,76 @@ +api: +wifi: + ap: + ssid: "Thermostat" + password: "MySecretThemostat" + +opentherm: + in_pin: 4 + out_pin: 5 + ch_enable: true + dhw_enable: false + cooling_enable: false + otc_active: false + ch2_active: true + summer_mode_active: true + dhw_block: true + sync_mode: true + +sensor: + - platform: opentherm + rel_mod_level: + name: "Boiler Relative modulation level" + ch_pressure: + name: "Boiler Water pressure in CH circuit" + dhw_flow_rate: + name: "Boiler Water flow rate in DHW circuit" + t_boiler: + name: "Boiler water temperature" + t_dhw: + name: "Boiler DHW temperature" + t_outside: + name: "Boiler Outside temperature" + t_ret: + name: "Boiler Return water temperature" + t_storage: + name: "Boiler Solar storage temperature" + t_collector: + name: "Boiler Solar collector temperature" + t_flow_ch2: + name: "Boiler Flow water temperature CH2 circuit" + t_dhw2: + name: "Boiler Domestic hot water temperature 2" + t_exhaust: + name: "Boiler Exhaust temperature" + burner_starts: + name: "Boiler Number of starts burner" + ch_pump_starts: + name: "Boiler Number of starts CH pump" + dhw_pump_valve_starts: + name: "Boiler Number of starts DHW pump/valve" + dhw_burner_starts: + name: "Boiler Number of starts burner during DHW mode" + burner_operation_hours: + name: "Boiler Number of hours that burner is in operation (i.e. flame on)" + ch_pump_operation_hours: + name: "Boiler Number of hours that CH pump has been running" + dhw_pump_valve_operation_hours: + name: "Boiler Number of hours that DHW pump has been running or DHW valve has been opened" + dhw_burner_operation_hours: + name: "Boiler Number of hours that burner is in operation during DHW mode" + t_dhw_set_ub: + name: "Boiler Upper bound for adjustement of DHW setpoint" + t_dhw_set_lb: + name: "Boiler Lower bound for adjustement of DHW setpoint" + max_t_set_ub: + name: "Boiler Upper bound for adjustement of max CH setpoint" + max_t_set_lb: + name: "Boiler Lower bound for adjustement of max CH setpoint" + t_dhw_set: + name: "Boiler Domestic hot water temperature setpoint" + max_t_set: + name: "Boiler Maximum allowable CH water setpoint" + otc_hc_ratio_ub: + name: "OTC heat curve ratio upper bound" + otc_hc_ratio_lb: + name: "OTC heat curve ratio lower bound" diff --git a/tests/components/opentherm/test.esp32-ard.yaml b/tests/components/opentherm/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp32-c3-ard.yaml b/tests/components/opentherm/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp32-c3-idf.yaml b/tests/components/opentherm/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp32-idf.yaml b/tests/components/opentherm/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp8266-ard.yaml b/tests/components/opentherm/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ota/common.yaml b/tests/components/ota/common.yaml new file mode 100644 index 0000000000..1433dada1f --- /dev/null +++ b/tests/components/ota/common.yaml @@ -0,0 +1,28 @@ +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: esphome + password: "superlongpasswordthatnoonewillknow" + port: 3286 + on_begin: + then: + - logger.log: "OTA start" + on_progress: + then: + - logger.log: + format: "OTA progress %0.1f%%" + args: ["x"] + on_end: + then: + - logger.log: "OTA end" + on_error: + then: + - logger.log: + format: "OTA update error %d" + args: ["x"] + on_state_change: + then: + lambda: >- + ESP_LOGD("ota", "State %d", state); diff --git a/tests/components/ota/test.esp32-ard.yaml b/tests/components/ota/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ota/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ota/test.esp32-c3-ard.yaml b/tests/components/ota/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ota/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ota/test.esp32-c3-idf.yaml b/tests/components/ota/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ota/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ota/test.esp32-idf.yaml b/tests/components/ota/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ota/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ota/test.esp8266-ard.yaml b/tests/components/ota/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ota/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ota/test.rp2040-ard.yaml b/tests/components/ota/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ota/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/output/test.esp32-ard.yaml b/tests/components/output/test.esp32-ard.yaml new file mode 100644 index 0000000000..480f9dfe1f --- /dev/null +++ b/tests/components/output/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - output.turn_off: light_output_1 + - output.turn_on: light_output_1 + - output.set_level: + id: light_output_1 + level: 50% + +output: + - platform: ledc + id: light_output_1 + pin: 12 diff --git a/tests/components/output/test.esp32-c3-ard.yaml b/tests/components/output/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c56d85c296 --- /dev/null +++ b/tests/components/output/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - output.turn_off: light_output_1 + - output.turn_on: light_output_1 + - output.set_level: + id: light_output_1 + level: 50% + +output: + - platform: ledc + id: light_output_1 + pin: 1 diff --git a/tests/components/output/test.esp32-c3-idf.yaml b/tests/components/output/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c56d85c296 --- /dev/null +++ b/tests/components/output/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - output.turn_off: light_output_1 + - output.turn_on: light_output_1 + - output.set_level: + id: light_output_1 + level: 50% + +output: + - platform: ledc + id: light_output_1 + pin: 1 diff --git a/tests/components/output/test.esp32-idf.yaml b/tests/components/output/test.esp32-idf.yaml new file mode 100644 index 0000000000..480f9dfe1f --- /dev/null +++ b/tests/components/output/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - output.turn_off: light_output_1 + - output.turn_on: light_output_1 + - output.set_level: + id: light_output_1 + level: 50% + +output: + - platform: ledc + id: light_output_1 + pin: 12 diff --git a/tests/components/output/test.esp8266-ard.yaml b/tests/components/output/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d9cb353636 --- /dev/null +++ b/tests/components/output/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - output.turn_off: light_output_1 + - output.turn_on: light_output_1 + - output.set_level: + id: light_output_1 + level: 50% + +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 diff --git a/tests/components/output/test.rp2040-ard.yaml b/tests/components/output/test.rp2040-ard.yaml new file mode 100644 index 0000000000..399259fdd9 --- /dev/null +++ b/tests/components/output/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - output.turn_off: light_output_1 + - output.turn_on: light_output_1 + - output.set_level: + id: light_output_1 + level: 50% + +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 diff --git a/tests/components/partition/test.esp32-ard.yaml b/tests/components/partition/test.esp32-ard.yaml new file mode 100644 index 0000000000..c8eae67d40 --- /dev/null +++ b/tests/components/partition/test.esp32-ard.yaml @@ -0,0 +1,22 @@ +light: + - platform: fastled_clockless + id: part_leds + chipset: WS2812B + pin: 2 + num_leds: 256 + rgb_order: GRB + default_transition_length: 0s + color_correct: [50%, 50%, 50%] + - platform: partition + name: Partition Light + segments: + - id: part_leds + from: 0 + to: 0 + - id: part_leds + from: 1 + to: 10 + - id: part_leds + from: 20 + to: 25 + - single_light_id: part_leds diff --git a/tests/components/partition/test.esp32-c3-ard.yaml b/tests/components/partition/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..77cfc5ad44 --- /dev/null +++ b/tests/components/partition/test.esp32-c3-ard.yaml @@ -0,0 +1,22 @@ +light: + - platform: esp32_rmt_led_strip + id: part_leds + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + - platform: partition + name: Partition Light + segments: + - id: part_leds + from: 0 + to: 0 + - id: part_leds + from: 1 + to: 10 + - id: part_leds + from: 20 + to: 25 + - single_light_id: part_leds diff --git a/tests/components/partition/test.esp32-c3-idf.yaml b/tests/components/partition/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..77cfc5ad44 --- /dev/null +++ b/tests/components/partition/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +light: + - platform: esp32_rmt_led_strip + id: part_leds + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + - platform: partition + name: Partition Light + segments: + - id: part_leds + from: 0 + to: 0 + - id: part_leds + from: 1 + to: 10 + - id: part_leds + from: 20 + to: 25 + - single_light_id: part_leds diff --git a/tests/components/partition/test.esp32-idf.yaml b/tests/components/partition/test.esp32-idf.yaml new file mode 100644 index 0000000000..77cfc5ad44 --- /dev/null +++ b/tests/components/partition/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +light: + - platform: esp32_rmt_led_strip + id: part_leds + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + - platform: partition + name: Partition Light + segments: + - id: part_leds + from: 0 + to: 0 + - id: part_leds + from: 1 + to: 10 + - id: part_leds + from: 20 + to: 25 + - single_light_id: part_leds diff --git a/tests/components/pca6416a/test.esp32-ard.yaml b/tests/components/pca6416a/test.esp32-ard.yaml new file mode 100644 index 0000000000..669e9416e4 --- /dev/null +++ b/tests/components/pca6416a/test.esp32-ard.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_pca6416a + scl: 16 + sda: 17 + +pca6416a: + - id: pca6416a_hub + address: 0x21 + +binary_sensor: + - platform: gpio + name: PCA6416A Binary Sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true diff --git a/tests/components/pca6416a/test.esp32-c3-ard.yaml b/tests/components/pca6416a/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fe940c44cc --- /dev/null +++ b/tests/components/pca6416a/test.esp32-c3-ard.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_pca6416a + scl: 5 + sda: 4 + +pca6416a: + - id: pca6416a_hub + address: 0x21 + +binary_sensor: + - platform: gpio + name: PCA6416A Binary Sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true diff --git a/tests/components/pca6416a/test.esp32-c3-idf.yaml b/tests/components/pca6416a/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fe940c44cc --- /dev/null +++ b/tests/components/pca6416a/test.esp32-c3-idf.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_pca6416a + scl: 5 + sda: 4 + +pca6416a: + - id: pca6416a_hub + address: 0x21 + +binary_sensor: + - platform: gpio + name: PCA6416A Binary Sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true diff --git a/tests/components/pca6416a/test.esp32-idf.yaml b/tests/components/pca6416a/test.esp32-idf.yaml new file mode 100644 index 0000000000..669e9416e4 --- /dev/null +++ b/tests/components/pca6416a/test.esp32-idf.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_pca6416a + scl: 16 + sda: 17 + +pca6416a: + - id: pca6416a_hub + address: 0x21 + +binary_sensor: + - platform: gpio + name: PCA6416A Binary Sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true diff --git a/tests/components/pca6416a/test.esp8266-ard.yaml b/tests/components/pca6416a/test.esp8266-ard.yaml new file mode 100644 index 0000000000..fe940c44cc --- /dev/null +++ b/tests/components/pca6416a/test.esp8266-ard.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_pca6416a + scl: 5 + sda: 4 + +pca6416a: + - id: pca6416a_hub + address: 0x21 + +binary_sensor: + - platform: gpio + name: PCA6416A Binary Sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true diff --git a/tests/components/pca6416a/test.rp2040-ard.yaml b/tests/components/pca6416a/test.rp2040-ard.yaml new file mode 100644 index 0000000000..fe940c44cc --- /dev/null +++ b/tests/components/pca6416a/test.rp2040-ard.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_pca6416a + scl: 5 + sda: 4 + +pca6416a: + - id: pca6416a_hub + address: 0x21 + +binary_sensor: + - platform: gpio + name: PCA6416A Binary Sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true diff --git a/tests/components/pca9554/test.esp32-ard.yaml b/tests/components/pca9554/test.esp32-ard.yaml new file mode 100644 index 0000000000..8fe9686303 --- /dev/null +++ b/tests/components/pca9554/test.esp32-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_pca9554 + scl: 16 + sda: 17 + +pca9554: + - id: pca9554_hub + pin_count: 8 + address: 0x3F + +binary_sensor: + - platform: gpio + id: pca9554_input + name: PCA9554 Binary Sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: pca9554_output + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pca9554/test.esp32-c3-ard.yaml b/tests/components/pca9554/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0ff453e64f --- /dev/null +++ b/tests/components/pca9554/test.esp32-c3-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_pca9554 + scl: 5 + sda: 4 + +pca9554: + - id: pca9554_hub + pin_count: 8 + address: 0x3F + +binary_sensor: + - platform: gpio + id: pca9554_input + name: PCA9554 Binary Sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: pca9554_output + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pca9554/test.esp32-c3-idf.yaml b/tests/components/pca9554/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0ff453e64f --- /dev/null +++ b/tests/components/pca9554/test.esp32-c3-idf.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_pca9554 + scl: 5 + sda: 4 + +pca9554: + - id: pca9554_hub + pin_count: 8 + address: 0x3F + +binary_sensor: + - platform: gpio + id: pca9554_input + name: PCA9554 Binary Sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: pca9554_output + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pca9554/test.esp32-idf.yaml b/tests/components/pca9554/test.esp32-idf.yaml new file mode 100644 index 0000000000..8fe9686303 --- /dev/null +++ b/tests/components/pca9554/test.esp32-idf.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_pca9554 + scl: 16 + sda: 17 + +pca9554: + - id: pca9554_hub + pin_count: 8 + address: 0x3F + +binary_sensor: + - platform: gpio + id: pca9554_input + name: PCA9554 Binary Sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: pca9554_output + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pca9554/test.esp8266-ard.yaml b/tests/components/pca9554/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0ff453e64f --- /dev/null +++ b/tests/components/pca9554/test.esp8266-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_pca9554 + scl: 5 + sda: 4 + +pca9554: + - id: pca9554_hub + pin_count: 8 + address: 0x3F + +binary_sensor: + - platform: gpio + id: pca9554_input + name: PCA9554 Binary Sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: pca9554_output + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pca9554/test.rp2040-ard.yaml b/tests/components/pca9554/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0ff453e64f --- /dev/null +++ b/tests/components/pca9554/test.rp2040-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_pca9554 + scl: 5 + sda: 4 + +pca9554: + - id: pca9554_hub + pin_count: 8 + address: 0x3F + +binary_sensor: + - platform: gpio + id: pca9554_input + name: PCA9554 Binary Sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: pca9554_output + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pca9685/test.esp32-ard.yaml b/tests/components/pca9685/test.esp32-ard.yaml new file mode 100644 index 0000000000..d02a16bcd1 --- /dev/null +++ b/tests/components/pca9685/test.esp32-ard.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_pca9685 + scl: 16 + sda: 17 + +pca9685: + frequency: 500 + address: 0x0 + +output: + - platform: pca9685 + id: pca_0 + channel: 0 + - platform: pca9685 + id: pca_1 + channel: 1 + - platform: pca9685 + id: pca_2 + channel: 2 + - platform: pca9685 + id: pca_3 + channel: 3 + - platform: pca9685 + id: pca_4 + channel: 4 + - platform: pca9685 + id: pca_5 + channel: 5 + - platform: pca9685 + id: pca_6 + channel: 6 + - platform: pca9685 + id: pca_7 + channel: 7 diff --git a/tests/components/pca9685/test.esp32-c3-ard.yaml b/tests/components/pca9685/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e532f323be --- /dev/null +++ b/tests/components/pca9685/test.esp32-c3-ard.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_pca9685 + scl: 5 + sda: 4 + +pca9685: + frequency: 500 + address: 0x0 + +output: + - platform: pca9685 + id: pca_0 + channel: 0 + - platform: pca9685 + id: pca_1 + channel: 1 + - platform: pca9685 + id: pca_2 + channel: 2 + - platform: pca9685 + id: pca_3 + channel: 3 + - platform: pca9685 + id: pca_4 + channel: 4 + - platform: pca9685 + id: pca_5 + channel: 5 + - platform: pca9685 + id: pca_6 + channel: 6 + - platform: pca9685 + id: pca_7 + channel: 7 diff --git a/tests/components/pca9685/test.esp32-c3-idf.yaml b/tests/components/pca9685/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e532f323be --- /dev/null +++ b/tests/components/pca9685/test.esp32-c3-idf.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_pca9685 + scl: 5 + sda: 4 + +pca9685: + frequency: 500 + address: 0x0 + +output: + - platform: pca9685 + id: pca_0 + channel: 0 + - platform: pca9685 + id: pca_1 + channel: 1 + - platform: pca9685 + id: pca_2 + channel: 2 + - platform: pca9685 + id: pca_3 + channel: 3 + - platform: pca9685 + id: pca_4 + channel: 4 + - platform: pca9685 + id: pca_5 + channel: 5 + - platform: pca9685 + id: pca_6 + channel: 6 + - platform: pca9685 + id: pca_7 + channel: 7 diff --git a/tests/components/pca9685/test.esp32-idf.yaml b/tests/components/pca9685/test.esp32-idf.yaml new file mode 100644 index 0000000000..d02a16bcd1 --- /dev/null +++ b/tests/components/pca9685/test.esp32-idf.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_pca9685 + scl: 16 + sda: 17 + +pca9685: + frequency: 500 + address: 0x0 + +output: + - platform: pca9685 + id: pca_0 + channel: 0 + - platform: pca9685 + id: pca_1 + channel: 1 + - platform: pca9685 + id: pca_2 + channel: 2 + - platform: pca9685 + id: pca_3 + channel: 3 + - platform: pca9685 + id: pca_4 + channel: 4 + - platform: pca9685 + id: pca_5 + channel: 5 + - platform: pca9685 + id: pca_6 + channel: 6 + - platform: pca9685 + id: pca_7 + channel: 7 diff --git a/tests/components/pca9685/test.esp8266-ard.yaml b/tests/components/pca9685/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e532f323be --- /dev/null +++ b/tests/components/pca9685/test.esp8266-ard.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_pca9685 + scl: 5 + sda: 4 + +pca9685: + frequency: 500 + address: 0x0 + +output: + - platform: pca9685 + id: pca_0 + channel: 0 + - platform: pca9685 + id: pca_1 + channel: 1 + - platform: pca9685 + id: pca_2 + channel: 2 + - platform: pca9685 + id: pca_3 + channel: 3 + - platform: pca9685 + id: pca_4 + channel: 4 + - platform: pca9685 + id: pca_5 + channel: 5 + - platform: pca9685 + id: pca_6 + channel: 6 + - platform: pca9685 + id: pca_7 + channel: 7 diff --git a/tests/components/pca9685/test.rp2040-ard.yaml b/tests/components/pca9685/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0ff453e64f --- /dev/null +++ b/tests/components/pca9685/test.rp2040-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_pca9554 + scl: 5 + sda: 4 + +pca9554: + - id: pca9554_hub + pin_count: 8 + address: 0x3F + +binary_sensor: + - platform: gpio + id: pca9554_input + name: PCA9554 Binary Sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: pca9554_output + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pcd8544/test.esp32-ard.yaml b/tests/components/pcd8544/test.esp32-ard.yaml new file mode 100644 index 0000000000..20c05c407f --- /dev/null +++ b/tests/components/pcd8544/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_pcd8544 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: pcd8544 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + contrast: 60 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/pcd8544/test.esp32-c3-ard.yaml b/tests/components/pcd8544/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..57771d2d73 --- /dev/null +++ b/tests/components/pcd8544/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_pcd8544 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: pcd8544 + cs_pin: 2 + dc_pin: 3 + reset_pin: 1 + contrast: 60 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/pcd8544/test.esp32-c3-idf.yaml b/tests/components/pcd8544/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..57771d2d73 --- /dev/null +++ b/tests/components/pcd8544/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_pcd8544 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: pcd8544 + cs_pin: 2 + dc_pin: 3 + reset_pin: 1 + contrast: 60 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/pcd8544/test.esp32-idf.yaml b/tests/components/pcd8544/test.esp32-idf.yaml new file mode 100644 index 0000000000..20c05c407f --- /dev/null +++ b/tests/components/pcd8544/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_pcd8544 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: pcd8544 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + contrast: 60 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/pcd8544/test.esp8266-ard.yaml b/tests/components/pcd8544/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6e6022c6d2 --- /dev/null +++ b/tests/components/pcd8544/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_pcd8544 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: pcd8544 + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + contrast: 60 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/pcd8544/test.rp2040-ard.yaml b/tests/components/pcd8544/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7181f99fb1 --- /dev/null +++ b/tests/components/pcd8544/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +spi: + - id: spi_pcd8544 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: pcd8544 + cs_pin: 6 + dc_pin: 5 + reset_pin: 7 + contrast: 60 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/pcf85063/test.esp32-ard.yaml b/tests/components/pcf85063/test.esp32-ard.yaml new file mode 100644 index 0000000000..9cce415103 --- /dev/null +++ b/tests/components/pcf85063/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf85063.read_time + - pcf85063.write_time + +i2c: + - id: i2c_pcf85063 + scl: 16 + sda: 17 + +time: + - platform: pcf85063 diff --git a/tests/components/pcf85063/test.esp32-c3-ard.yaml b/tests/components/pcf85063/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9e1a3da81e --- /dev/null +++ b/tests/components/pcf85063/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf85063.read_time + - pcf85063.write_time + +i2c: + - id: i2c_pcf85063 + scl: 5 + sda: 4 + +time: + - platform: pcf85063 diff --git a/tests/components/pcf85063/test.esp32-c3-idf.yaml b/tests/components/pcf85063/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9e1a3da81e --- /dev/null +++ b/tests/components/pcf85063/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf85063.read_time + - pcf85063.write_time + +i2c: + - id: i2c_pcf85063 + scl: 5 + sda: 4 + +time: + - platform: pcf85063 diff --git a/tests/components/pcf85063/test.esp32-idf.yaml b/tests/components/pcf85063/test.esp32-idf.yaml new file mode 100644 index 0000000000..9cce415103 --- /dev/null +++ b/tests/components/pcf85063/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf85063.read_time + - pcf85063.write_time + +i2c: + - id: i2c_pcf85063 + scl: 16 + sda: 17 + +time: + - platform: pcf85063 diff --git a/tests/components/pcf85063/test.esp8266-ard.yaml b/tests/components/pcf85063/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9e1a3da81e --- /dev/null +++ b/tests/components/pcf85063/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf85063.read_time + - pcf85063.write_time + +i2c: + - id: i2c_pcf85063 + scl: 5 + sda: 4 + +time: + - platform: pcf85063 diff --git a/tests/components/pcf85063/test.rp2040-ard.yaml b/tests/components/pcf85063/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9e1a3da81e --- /dev/null +++ b/tests/components/pcf85063/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf85063.read_time + - pcf85063.write_time + +i2c: + - id: i2c_pcf85063 + scl: 5 + sda: 4 + +time: + - platform: pcf85063 diff --git a/tests/components/pcf8563/test.esp32-ard.yaml b/tests/components/pcf8563/test.esp32-ard.yaml new file mode 100644 index 0000000000..e95b487b19 --- /dev/null +++ b/tests/components/pcf8563/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf8563.read_time + - pcf8563.write_time + +i2c: + - id: i2c_pcf8563 + scl: 16 + sda: 17 + +time: + - platform: pcf8563 diff --git a/tests/components/pcf8563/test.esp32-c3-ard.yaml b/tests/components/pcf8563/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f91a465e0f --- /dev/null +++ b/tests/components/pcf8563/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf8563.read_time + - pcf8563.write_time + +i2c: + - id: i2c_pcf8563 + scl: 5 + sda: 4 + +time: + - platform: pcf8563 diff --git a/tests/components/pcf8563/test.esp32-c3-idf.yaml b/tests/components/pcf8563/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f91a465e0f --- /dev/null +++ b/tests/components/pcf8563/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf8563.read_time + - pcf8563.write_time + +i2c: + - id: i2c_pcf8563 + scl: 5 + sda: 4 + +time: + - platform: pcf8563 diff --git a/tests/components/pcf8563/test.esp32-idf.yaml b/tests/components/pcf8563/test.esp32-idf.yaml new file mode 100644 index 0000000000..e95b487b19 --- /dev/null +++ b/tests/components/pcf8563/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf8563.read_time + - pcf8563.write_time + +i2c: + - id: i2c_pcf8563 + scl: 16 + sda: 17 + +time: + - platform: pcf8563 diff --git a/tests/components/pcf8563/test.esp8266-ard.yaml b/tests/components/pcf8563/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f91a465e0f --- /dev/null +++ b/tests/components/pcf8563/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf8563.read_time + - pcf8563.write_time + +i2c: + - id: i2c_pcf8563 + scl: 5 + sda: 4 + +time: + - platform: pcf8563 diff --git a/tests/components/pcf8563/test.rp2040-ard.yaml b/tests/components/pcf8563/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f91a465e0f --- /dev/null +++ b/tests/components/pcf8563/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + - pcf8563.read_time + - pcf8563.write_time + +i2c: + - id: i2c_pcf8563 + scl: 5 + sda: 4 + +time: + - platform: pcf8563 diff --git a/tests/components/pcf8574/test.esp32-ard.yaml b/tests/components/pcf8574/test.esp32-ard.yaml new file mode 100644 index 0000000000..aeed55f4fe --- /dev/null +++ b/tests/components/pcf8574/test.esp32-ard.yaml @@ -0,0 +1,28 @@ +i2c: + - id: i2c_pcf8563 + scl: 16 + sda: 17 + +pcf8574: + - id: pcf8574_hub + address: 0x21 + pcf8575: false + +binary_sensor: + - platform: gpio + id: pcf8574_binary_sensor + name: PCF Binary Sensor + pin: + pcf8574: pcf8574_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: pcf8574_output + pin: + pcf8574: pcf8574_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pcf8574/test.esp32-c3-ard.yaml b/tests/components/pcf8574/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..551e425892 --- /dev/null +++ b/tests/components/pcf8574/test.esp32-c3-ard.yaml @@ -0,0 +1,28 @@ +i2c: + - id: i2c_pcf8563 + scl: 5 + sda: 4 + +pcf8574: + - id: pcf8574_hub + address: 0x21 + pcf8575: false + +binary_sensor: + - platform: gpio + id: pcf8574_binary_sensor + name: PCF Binary Sensor + pin: + pcf8574: pcf8574_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: pcf8574_output + pin: + pcf8574: pcf8574_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pcf8574/test.esp32-c3-idf.yaml b/tests/components/pcf8574/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..551e425892 --- /dev/null +++ b/tests/components/pcf8574/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +i2c: + - id: i2c_pcf8563 + scl: 5 + sda: 4 + +pcf8574: + - id: pcf8574_hub + address: 0x21 + pcf8575: false + +binary_sensor: + - platform: gpio + id: pcf8574_binary_sensor + name: PCF Binary Sensor + pin: + pcf8574: pcf8574_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: pcf8574_output + pin: + pcf8574: pcf8574_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pcf8574/test.esp32-idf.yaml b/tests/components/pcf8574/test.esp32-idf.yaml new file mode 100644 index 0000000000..aeed55f4fe --- /dev/null +++ b/tests/components/pcf8574/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +i2c: + - id: i2c_pcf8563 + scl: 16 + sda: 17 + +pcf8574: + - id: pcf8574_hub + address: 0x21 + pcf8575: false + +binary_sensor: + - platform: gpio + id: pcf8574_binary_sensor + name: PCF Binary Sensor + pin: + pcf8574: pcf8574_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: pcf8574_output + pin: + pcf8574: pcf8574_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pcf8574/test.esp8266-ard.yaml b/tests/components/pcf8574/test.esp8266-ard.yaml new file mode 100644 index 0000000000..551e425892 --- /dev/null +++ b/tests/components/pcf8574/test.esp8266-ard.yaml @@ -0,0 +1,28 @@ +i2c: + - id: i2c_pcf8563 + scl: 5 + sda: 4 + +pcf8574: + - id: pcf8574_hub + address: 0x21 + pcf8575: false + +binary_sensor: + - platform: gpio + id: pcf8574_binary_sensor + name: PCF Binary Sensor + pin: + pcf8574: pcf8574_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: pcf8574_output + pin: + pcf8574: pcf8574_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pcf8574/test.rp2040-ard.yaml b/tests/components/pcf8574/test.rp2040-ard.yaml new file mode 100644 index 0000000000..551e425892 --- /dev/null +++ b/tests/components/pcf8574/test.rp2040-ard.yaml @@ -0,0 +1,28 @@ +i2c: + - id: i2c_pcf8563 + scl: 5 + sda: 4 + +pcf8574: + - id: pcf8574_hub + address: 0x21 + pcf8575: false + +binary_sensor: + - platform: gpio + id: pcf8574_binary_sensor + name: PCF Binary Sensor + pin: + pcf8574: pcf8574_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: pcf8574_output + pin: + pcf8574: pcf8574_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/pid/common.yaml b/tests/components/pid/common.yaml new file mode 100644 index 0000000000..5f7762872f --- /dev/null +++ b/tests/components/pid/common.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - climate.pid.autotune: pid_climate + - climate.pid.autotune: + id: pid_climate + noiseband: 0.25 + positive_output: 25% + negative_output: -25% + - climate.pid.set_control_parameters: + id: pid_climate + kp: 0.0 + ki: 0.0 + kd: 0.0 + - climate.pid.reset_integral_term: pid_climate + +output: + - platform: slow_pwm + pin: 4 + id: pid_slow_pwm + period: 15s + restart_cycle_on_state_change: false + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +climate: + - platform: pid + id: pid_climate + name: PID Climate Controller + sensor: template_sensor1 + humidity_sensor: template_sensor1 + default_target_temperature: 21°C + heat_output: pid_slow_pwm + control_parameters: + kp: 0.0 + ki: 0.0 + kd: 0.0 + max_integral: 0.0 + output_averaging_samples: 1 + derivative_averaging_samples: 1 + deadband_parameters: + threshold_high: 0.4 + threshold_low: -2.0 + kp_multiplier: 0.0 + ki_multiplier: 0.0 + kd_multiplier: 0.0 + deadband_output_averaging_samples: 1 diff --git a/tests/components/pid/test.esp32-ard.yaml b/tests/components/pid/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pid/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pid/test.esp32-c3-ard.yaml b/tests/components/pid/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pid/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pid/test.esp32-c3-idf.yaml b/tests/components/pid/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pid/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pid/test.esp32-idf.yaml b/tests/components/pid/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pid/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pid/test.esp8266-ard.yaml b/tests/components/pid/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pid/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pid/test.rp2040-ard.yaml b/tests/components/pid/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pid/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp32-ard.yaml b/tests/components/pipsolar/test.esp32-ard.yaml new file mode 100644 index 0000000000..b7a7e0cbd9 --- /dev/null +++ b/tests/components/pipsolar/test.esp32-ard.yaml @@ -0,0 +1,249 @@ +esphome: + on_boot: + then: + - output.pipsolar.set_level: + id: inverter0_battery_recharge_voltage_out + value: 48.0 + +uart: + - id: uart_pipsolar + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +pipsolar: + id: inverter0 + +binary_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out + +sensor: + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power + +switch: + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority_utility: + name: inverter0_output_source_priority_utility + output_source_priority_solar: + name: inverter0_output_source_priority_solar + output_source_priority_battery: + name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid + input_voltage_range: + name: inverter0_input_voltage_range + pv_ok_condition_for_parallel: + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + name: inverter0_pv_power_balance + +text_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + device_mode: + id: inverter0_device_mode + name: inverter0_device_mode + last_qpigs: + id: inverter0_last_qpigs + name: inverter0_last_qpigs + last_qpiri: + id: inverter0_last_qpiri + name: inverter0_last_qpiri + last_qmod: + id: inverter0_last_qmod + name: inverter0_last_qmod + last_qflag: + id: inverter0_last_qflag + name: inverter0_last_qflag diff --git a/tests/components/pipsolar/test.esp32-c3-ard.yaml b/tests/components/pipsolar/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..83d7070669 --- /dev/null +++ b/tests/components/pipsolar/test.esp32-c3-ard.yaml @@ -0,0 +1,249 @@ +esphome: + on_boot: + then: + - output.pipsolar.set_level: + id: inverter0_battery_recharge_voltage_out + value: 48.0 + +uart: + - id: uart_pipsolar + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +pipsolar: + id: inverter0 + +binary_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out + +sensor: + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power + +switch: + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority_utility: + name: inverter0_output_source_priority_utility + output_source_priority_solar: + name: inverter0_output_source_priority_solar + output_source_priority_battery: + name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid + input_voltage_range: + name: inverter0_input_voltage_range + pv_ok_condition_for_parallel: + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + name: inverter0_pv_power_balance + +text_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + device_mode: + id: inverter0_device_mode + name: inverter0_device_mode + last_qpigs: + id: inverter0_last_qpigs + name: inverter0_last_qpigs + last_qpiri: + id: inverter0_last_qpiri + name: inverter0_last_qpiri + last_qmod: + id: inverter0_last_qmod + name: inverter0_last_qmod + last_qflag: + id: inverter0_last_qflag + name: inverter0_last_qflag diff --git a/tests/components/pipsolar/test.esp32-c3-idf.yaml b/tests/components/pipsolar/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..83d7070669 --- /dev/null +++ b/tests/components/pipsolar/test.esp32-c3-idf.yaml @@ -0,0 +1,249 @@ +esphome: + on_boot: + then: + - output.pipsolar.set_level: + id: inverter0_battery_recharge_voltage_out + value: 48.0 + +uart: + - id: uart_pipsolar + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +pipsolar: + id: inverter0 + +binary_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out + +sensor: + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power + +switch: + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority_utility: + name: inverter0_output_source_priority_utility + output_source_priority_solar: + name: inverter0_output_source_priority_solar + output_source_priority_battery: + name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid + input_voltage_range: + name: inverter0_input_voltage_range + pv_ok_condition_for_parallel: + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + name: inverter0_pv_power_balance + +text_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + device_mode: + id: inverter0_device_mode + name: inverter0_device_mode + last_qpigs: + id: inverter0_last_qpigs + name: inverter0_last_qpigs + last_qpiri: + id: inverter0_last_qpiri + name: inverter0_last_qpiri + last_qmod: + id: inverter0_last_qmod + name: inverter0_last_qmod + last_qflag: + id: inverter0_last_qflag + name: inverter0_last_qflag diff --git a/tests/components/pipsolar/test.esp32-idf.yaml b/tests/components/pipsolar/test.esp32-idf.yaml new file mode 100644 index 0000000000..b7a7e0cbd9 --- /dev/null +++ b/tests/components/pipsolar/test.esp32-idf.yaml @@ -0,0 +1,249 @@ +esphome: + on_boot: + then: + - output.pipsolar.set_level: + id: inverter0_battery_recharge_voltage_out + value: 48.0 + +uart: + - id: uart_pipsolar + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +pipsolar: + id: inverter0 + +binary_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out + +sensor: + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power + +switch: + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority_utility: + name: inverter0_output_source_priority_utility + output_source_priority_solar: + name: inverter0_output_source_priority_solar + output_source_priority_battery: + name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid + input_voltage_range: + name: inverter0_input_voltage_range + pv_ok_condition_for_parallel: + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + name: inverter0_pv_power_balance + +text_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + device_mode: + id: inverter0_device_mode + name: inverter0_device_mode + last_qpigs: + id: inverter0_last_qpigs + name: inverter0_last_qpigs + last_qpiri: + id: inverter0_last_qpiri + name: inverter0_last_qpiri + last_qmod: + id: inverter0_last_qmod + name: inverter0_last_qmod + last_qflag: + id: inverter0_last_qflag + name: inverter0_last_qflag diff --git a/tests/components/pipsolar/test.esp8266-ard.yaml b/tests/components/pipsolar/test.esp8266-ard.yaml new file mode 100644 index 0000000000..83d7070669 --- /dev/null +++ b/tests/components/pipsolar/test.esp8266-ard.yaml @@ -0,0 +1,249 @@ +esphome: + on_boot: + then: + - output.pipsolar.set_level: + id: inverter0_battery_recharge_voltage_out + value: 48.0 + +uart: + - id: uart_pipsolar + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +pipsolar: + id: inverter0 + +binary_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out + +sensor: + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power + +switch: + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority_utility: + name: inverter0_output_source_priority_utility + output_source_priority_solar: + name: inverter0_output_source_priority_solar + output_source_priority_battery: + name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid + input_voltage_range: + name: inverter0_input_voltage_range + pv_ok_condition_for_parallel: + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + name: inverter0_pv_power_balance + +text_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + device_mode: + id: inverter0_device_mode + name: inverter0_device_mode + last_qpigs: + id: inverter0_last_qpigs + name: inverter0_last_qpigs + last_qpiri: + id: inverter0_last_qpiri + name: inverter0_last_qpiri + last_qmod: + id: inverter0_last_qmod + name: inverter0_last_qmod + last_qflag: + id: inverter0_last_qflag + name: inverter0_last_qflag diff --git a/tests/components/pipsolar/test.rp2040-ard.yaml b/tests/components/pipsolar/test.rp2040-ard.yaml new file mode 100644 index 0000000000..83d7070669 --- /dev/null +++ b/tests/components/pipsolar/test.rp2040-ard.yaml @@ -0,0 +1,249 @@ +esphome: + on_boot: + then: + - output.pipsolar.set_level: + id: inverter0_battery_recharge_voltage_out + value: 48.0 + +uart: + - id: uart_pipsolar + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +pipsolar: + id: inverter0 + +binary_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out + +sensor: + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power + +switch: + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority_utility: + name: inverter0_output_source_priority_utility + output_source_priority_solar: + name: inverter0_output_source_priority_solar + output_source_priority_battery: + name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid + input_voltage_range: + name: inverter0_input_voltage_range + pv_ok_condition_for_parallel: + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + name: inverter0_pv_power_balance + +text_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + device_mode: + id: inverter0_device_mode + name: inverter0_device_mode + last_qpigs: + id: inverter0_last_qpigs + name: inverter0_last_qpigs + last_qpiri: + id: inverter0_last_qpiri + name: inverter0_last_qpiri + last_qmod: + id: inverter0_last_qmod + name: inverter0_last_qmod + last_qflag: + id: inverter0_last_qflag + name: inverter0_last_qflag diff --git a/tests/components/pm1006/test.esp32-ard.yaml b/tests/components/pm1006/test.esp32-ard.yaml new file mode 100644 index 0000000000..635af37b25 --- /dev/null +++ b/tests/components/pm1006/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_pm1006 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: pm1006 + pm_2_5: + name: Particulate Matter 2.5µm Concentration diff --git a/tests/components/pm1006/test.esp32-c3-ard.yaml b/tests/components/pm1006/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..15ee077f3e --- /dev/null +++ b/tests/components/pm1006/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_pm1006 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: pm1006 + pm_2_5: + name: Particulate Matter 2.5µm Concentration diff --git a/tests/components/pm1006/test.esp32-c3-idf.yaml b/tests/components/pm1006/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..15ee077f3e --- /dev/null +++ b/tests/components/pm1006/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_pm1006 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: pm1006 + pm_2_5: + name: Particulate Matter 2.5µm Concentration diff --git a/tests/components/pm1006/test.esp32-idf.yaml b/tests/components/pm1006/test.esp32-idf.yaml new file mode 100644 index 0000000000..635af37b25 --- /dev/null +++ b/tests/components/pm1006/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_pm1006 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: pm1006 + pm_2_5: + name: Particulate Matter 2.5µm Concentration diff --git a/tests/components/pm1006/test.esp8266-ard.yaml b/tests/components/pm1006/test.esp8266-ard.yaml new file mode 100644 index 0000000000..15ee077f3e --- /dev/null +++ b/tests/components/pm1006/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_pm1006 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: pm1006 + pm_2_5: + name: Particulate Matter 2.5µm Concentration diff --git a/tests/components/pm1006/test.rp2040-ard.yaml b/tests/components/pm1006/test.rp2040-ard.yaml new file mode 100644 index 0000000000..15ee077f3e --- /dev/null +++ b/tests/components/pm1006/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_pm1006 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: pm1006 + pm_2_5: + name: Particulate Matter 2.5µm Concentration diff --git a/tests/components/pmsa003i/test.esp32-ard.yaml b/tests/components/pmsa003i/test.esp32-ard.yaml new file mode 100644 index 0000000000..d8d96400f6 --- /dev/null +++ b/tests/components/pmsa003i/test.esp32-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_pmsa003i + scl: 16 + sda: 17 + +sensor: + - platform: pmsa003i + pm_1_0: + name: PMSA003i PM1.0 + pm_2_5: + name: PMSA003i PM2.5 + pm_10_0: + name: PMSA003i PM10.0 + pmc_0_3: + name: PMSA003i PMC <0.3µm + pmc_0_5: + name: PMSA003i PMC <0.5µm + pmc_1_0: + name: PMSA003i PMC <1µm + pmc_2_5: + name: PMSA003i PMC <2.5µm + pmc_5_0: + name: PMSA003i PMC <5µm + pmc_10_0: + name: PMSA003i PMC <10µm + address: 0x12 + standard_units: true diff --git a/tests/components/pmsa003i/test.esp32-c3-ard.yaml b/tests/components/pmsa003i/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..70e28303a2 --- /dev/null +++ b/tests/components/pmsa003i/test.esp32-c3-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_pmsa003i + scl: 5 + sda: 4 + +sensor: + - platform: pmsa003i + pm_1_0: + name: PMSA003i PM1.0 + pm_2_5: + name: PMSA003i PM2.5 + pm_10_0: + name: PMSA003i PM10.0 + pmc_0_3: + name: PMSA003i PMC <0.3µm + pmc_0_5: + name: PMSA003i PMC <0.5µm + pmc_1_0: + name: PMSA003i PMC <1µm + pmc_2_5: + name: PMSA003i PMC <2.5µm + pmc_5_0: + name: PMSA003i PMC <5µm + pmc_10_0: + name: PMSA003i PMC <10µm + address: 0x12 + standard_units: true diff --git a/tests/components/pmsa003i/test.esp32-c3-idf.yaml b/tests/components/pmsa003i/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..70e28303a2 --- /dev/null +++ b/tests/components/pmsa003i/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_pmsa003i + scl: 5 + sda: 4 + +sensor: + - platform: pmsa003i + pm_1_0: + name: PMSA003i PM1.0 + pm_2_5: + name: PMSA003i PM2.5 + pm_10_0: + name: PMSA003i PM10.0 + pmc_0_3: + name: PMSA003i PMC <0.3µm + pmc_0_5: + name: PMSA003i PMC <0.5µm + pmc_1_0: + name: PMSA003i PMC <1µm + pmc_2_5: + name: PMSA003i PMC <2.5µm + pmc_5_0: + name: PMSA003i PMC <5µm + pmc_10_0: + name: PMSA003i PMC <10µm + address: 0x12 + standard_units: true diff --git a/tests/components/pmsa003i/test.esp32-idf.yaml b/tests/components/pmsa003i/test.esp32-idf.yaml new file mode 100644 index 0000000000..d8d96400f6 --- /dev/null +++ b/tests/components/pmsa003i/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_pmsa003i + scl: 16 + sda: 17 + +sensor: + - platform: pmsa003i + pm_1_0: + name: PMSA003i PM1.0 + pm_2_5: + name: PMSA003i PM2.5 + pm_10_0: + name: PMSA003i PM10.0 + pmc_0_3: + name: PMSA003i PMC <0.3µm + pmc_0_5: + name: PMSA003i PMC <0.5µm + pmc_1_0: + name: PMSA003i PMC <1µm + pmc_2_5: + name: PMSA003i PMC <2.5µm + pmc_5_0: + name: PMSA003i PMC <5µm + pmc_10_0: + name: PMSA003i PMC <10µm + address: 0x12 + standard_units: true diff --git a/tests/components/pmsa003i/test.esp8266-ard.yaml b/tests/components/pmsa003i/test.esp8266-ard.yaml new file mode 100644 index 0000000000..70e28303a2 --- /dev/null +++ b/tests/components/pmsa003i/test.esp8266-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_pmsa003i + scl: 5 + sda: 4 + +sensor: + - platform: pmsa003i + pm_1_0: + name: PMSA003i PM1.0 + pm_2_5: + name: PMSA003i PM2.5 + pm_10_0: + name: PMSA003i PM10.0 + pmc_0_3: + name: PMSA003i PMC <0.3µm + pmc_0_5: + name: PMSA003i PMC <0.5µm + pmc_1_0: + name: PMSA003i PMC <1µm + pmc_2_5: + name: PMSA003i PMC <2.5µm + pmc_5_0: + name: PMSA003i PMC <5µm + pmc_10_0: + name: PMSA003i PMC <10µm + address: 0x12 + standard_units: true diff --git a/tests/components/pmsa003i/test.rp2040-ard.yaml b/tests/components/pmsa003i/test.rp2040-ard.yaml new file mode 100644 index 0000000000..70e28303a2 --- /dev/null +++ b/tests/components/pmsa003i/test.rp2040-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_pmsa003i + scl: 5 + sda: 4 + +sensor: + - platform: pmsa003i + pm_1_0: + name: PMSA003i PM1.0 + pm_2_5: + name: PMSA003i PM2.5 + pm_10_0: + name: PMSA003i PM10.0 + pmc_0_3: + name: PMSA003i PMC <0.3µm + pmc_0_5: + name: PMSA003i PMC <0.5µm + pmc_1_0: + name: PMSA003i PMC <1µm + pmc_2_5: + name: PMSA003i PMC <2.5µm + pmc_5_0: + name: PMSA003i PMC <5µm + pmc_10_0: + name: PMSA003i PMC <10µm + address: 0x12 + standard_units: true diff --git a/tests/components/pmsx003/test.esp32-ard.yaml b/tests/components/pmsx003/test.esp32-ard.yaml new file mode 100644 index 0000000000..5e7ebbbb2e --- /dev/null +++ b/tests/components/pmsx003/test.esp32-ard.yaml @@ -0,0 +1,34 @@ +uart: + - id: uart_pmsx003 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: pmsx003 + type: PMSX003 + pm_1_0: + name: PM 1.0 Concentration + pm_2_5: + name: PM 2.5 Concentration + pm_10_0: + name: PM 10.0 Concentration + pm_1_0_std: + name: PM 1.0 Standard Atmospher Concentration + pm_2_5_std: + name: PM 2.5 Standard Atmospher Concentration + pm_10_0_std: + name: PM 10.0 Standard Atmospher Concentration + pm_0_3um: + name: Particulate Count >0.3um + pm_0_5um: + name: Particulate Count >0.5um + pm_1_0um: + name: Particulate Count >1.0um + pm_2_5um: + name: Particulate Count >2.5um + pm_5_0um: + name: Particulate Count >5.0um + pm_10_0um: + name: Particulate Count >10.0um + update_interval: 30s diff --git a/tests/components/pmsx003/test.esp32-c3-ard.yaml b/tests/components/pmsx003/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..58adc9390a --- /dev/null +++ b/tests/components/pmsx003/test.esp32-c3-ard.yaml @@ -0,0 +1,34 @@ +uart: + - id: uart_pmsx003 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: pmsx003 + type: PMSX003 + pm_1_0: + name: PM 1.0 Concentration + pm_2_5: + name: PM 2.5 Concentration + pm_10_0: + name: PM 10.0 Concentration + pm_1_0_std: + name: PM 1.0 Standard Atmospher Concentration + pm_2_5_std: + name: PM 2.5 Standard Atmospher Concentration + pm_10_0_std: + name: PM 10.0 Standard Atmospher Concentration + pm_0_3um: + name: Particulate Count >0.3um + pm_0_5um: + name: Particulate Count >0.5um + pm_1_0um: + name: Particulate Count >1.0um + pm_2_5um: + name: Particulate Count >2.5um + pm_5_0um: + name: Particulate Count >5.0um + pm_10_0um: + name: Particulate Count >10.0um + update_interval: 30s diff --git a/tests/components/pmsx003/test.esp32-c3-idf.yaml b/tests/components/pmsx003/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..58adc9390a --- /dev/null +++ b/tests/components/pmsx003/test.esp32-c3-idf.yaml @@ -0,0 +1,34 @@ +uart: + - id: uart_pmsx003 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: pmsx003 + type: PMSX003 + pm_1_0: + name: PM 1.0 Concentration + pm_2_5: + name: PM 2.5 Concentration + pm_10_0: + name: PM 10.0 Concentration + pm_1_0_std: + name: PM 1.0 Standard Atmospher Concentration + pm_2_5_std: + name: PM 2.5 Standard Atmospher Concentration + pm_10_0_std: + name: PM 10.0 Standard Atmospher Concentration + pm_0_3um: + name: Particulate Count >0.3um + pm_0_5um: + name: Particulate Count >0.5um + pm_1_0um: + name: Particulate Count >1.0um + pm_2_5um: + name: Particulate Count >2.5um + pm_5_0um: + name: Particulate Count >5.0um + pm_10_0um: + name: Particulate Count >10.0um + update_interval: 30s diff --git a/tests/components/pmsx003/test.esp32-idf.yaml b/tests/components/pmsx003/test.esp32-idf.yaml new file mode 100644 index 0000000000..5e7ebbbb2e --- /dev/null +++ b/tests/components/pmsx003/test.esp32-idf.yaml @@ -0,0 +1,34 @@ +uart: + - id: uart_pmsx003 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: pmsx003 + type: PMSX003 + pm_1_0: + name: PM 1.0 Concentration + pm_2_5: + name: PM 2.5 Concentration + pm_10_0: + name: PM 10.0 Concentration + pm_1_0_std: + name: PM 1.0 Standard Atmospher Concentration + pm_2_5_std: + name: PM 2.5 Standard Atmospher Concentration + pm_10_0_std: + name: PM 10.0 Standard Atmospher Concentration + pm_0_3um: + name: Particulate Count >0.3um + pm_0_5um: + name: Particulate Count >0.5um + pm_1_0um: + name: Particulate Count >1.0um + pm_2_5um: + name: Particulate Count >2.5um + pm_5_0um: + name: Particulate Count >5.0um + pm_10_0um: + name: Particulate Count >10.0um + update_interval: 30s diff --git a/tests/components/pmsx003/test.esp8266-ard.yaml b/tests/components/pmsx003/test.esp8266-ard.yaml new file mode 100644 index 0000000000..58adc9390a --- /dev/null +++ b/tests/components/pmsx003/test.esp8266-ard.yaml @@ -0,0 +1,34 @@ +uart: + - id: uart_pmsx003 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: pmsx003 + type: PMSX003 + pm_1_0: + name: PM 1.0 Concentration + pm_2_5: + name: PM 2.5 Concentration + pm_10_0: + name: PM 10.0 Concentration + pm_1_0_std: + name: PM 1.0 Standard Atmospher Concentration + pm_2_5_std: + name: PM 2.5 Standard Atmospher Concentration + pm_10_0_std: + name: PM 10.0 Standard Atmospher Concentration + pm_0_3um: + name: Particulate Count >0.3um + pm_0_5um: + name: Particulate Count >0.5um + pm_1_0um: + name: Particulate Count >1.0um + pm_2_5um: + name: Particulate Count >2.5um + pm_5_0um: + name: Particulate Count >5.0um + pm_10_0um: + name: Particulate Count >10.0um + update_interval: 30s diff --git a/tests/components/pmsx003/test.rp2040-ard.yaml b/tests/components/pmsx003/test.rp2040-ard.yaml new file mode 100644 index 0000000000..58adc9390a --- /dev/null +++ b/tests/components/pmsx003/test.rp2040-ard.yaml @@ -0,0 +1,34 @@ +uart: + - id: uart_pmsx003 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: pmsx003 + type: PMSX003 + pm_1_0: + name: PM 1.0 Concentration + pm_2_5: + name: PM 2.5 Concentration + pm_10_0: + name: PM 10.0 Concentration + pm_1_0_std: + name: PM 1.0 Standard Atmospher Concentration + pm_2_5_std: + name: PM 2.5 Standard Atmospher Concentration + pm_10_0_std: + name: PM 10.0 Standard Atmospher Concentration + pm_0_3um: + name: Particulate Count >0.3um + pm_0_5um: + name: Particulate Count >0.5um + pm_1_0um: + name: Particulate Count >1.0um + pm_2_5um: + name: Particulate Count >2.5um + pm_5_0um: + name: Particulate Count >5.0um + pm_10_0um: + name: Particulate Count >10.0um + update_interval: 30s diff --git a/tests/components/pmwcs3/test.esp32-ard.yaml b/tests/components/pmwcs3/test.esp32-ard.yaml new file mode 100644 index 0000000000..787eaca650 --- /dev/null +++ b/tests/components/pmwcs3/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_pmwcs3 + scl: 16 + sda: 17 + +sensor: + - platform: pmwcs3 + e25: + name: pmwcs3_e25 + ec: + name: pmwcs3_ec + temperature: + name: pmwcs3_temperature + vwc: + name: pmwcs3_vwc diff --git a/tests/components/pmwcs3/test.esp32-c3-ard.yaml b/tests/components/pmwcs3/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7e7e72692d --- /dev/null +++ b/tests/components/pmwcs3/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_pmwcs3 + scl: 5 + sda: 4 + +sensor: + - platform: pmwcs3 + e25: + name: pmwcs3_e25 + ec: + name: pmwcs3_ec + temperature: + name: pmwcs3_temperature + vwc: + name: pmwcs3_vwc diff --git a/tests/components/pmwcs3/test.esp32-c3-idf.yaml b/tests/components/pmwcs3/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7e7e72692d --- /dev/null +++ b/tests/components/pmwcs3/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_pmwcs3 + scl: 5 + sda: 4 + +sensor: + - platform: pmwcs3 + e25: + name: pmwcs3_e25 + ec: + name: pmwcs3_ec + temperature: + name: pmwcs3_temperature + vwc: + name: pmwcs3_vwc diff --git a/tests/components/pmwcs3/test.esp32-idf.yaml b/tests/components/pmwcs3/test.esp32-idf.yaml new file mode 100644 index 0000000000..787eaca650 --- /dev/null +++ b/tests/components/pmwcs3/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_pmwcs3 + scl: 16 + sda: 17 + +sensor: + - platform: pmwcs3 + e25: + name: pmwcs3_e25 + ec: + name: pmwcs3_ec + temperature: + name: pmwcs3_temperature + vwc: + name: pmwcs3_vwc diff --git a/tests/components/pmwcs3/test.esp8266-ard.yaml b/tests/components/pmwcs3/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7e7e72692d --- /dev/null +++ b/tests/components/pmwcs3/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_pmwcs3 + scl: 5 + sda: 4 + +sensor: + - platform: pmwcs3 + e25: + name: pmwcs3_e25 + ec: + name: pmwcs3_ec + temperature: + name: pmwcs3_temperature + vwc: + name: pmwcs3_vwc diff --git a/tests/components/pmwcs3/test.rp2040-ard.yaml b/tests/components/pmwcs3/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7e7e72692d --- /dev/null +++ b/tests/components/pmwcs3/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_pmwcs3 + scl: 5 + sda: 4 + +sensor: + - platform: pmwcs3 + e25: + name: pmwcs3_e25 + ec: + name: pmwcs3_ec + temperature: + name: pmwcs3_temperature + vwc: + name: pmwcs3_vwc diff --git a/tests/components/pn532_i2c/test.esp32-ard.yaml b/tests/components/pn532_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..a50533b1d0 --- /dev/null +++ b/tests/components/pn532_i2c/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_pn532 + scl: 16 + sda: 17 + +pn532_i2c: + id: pn532_nfcc + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_i2c/test.esp32-c3-ard.yaml b/tests/components/pn532_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..62816d2ace --- /dev/null +++ b/tests/components/pn532_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_pn532 + scl: 5 + sda: 4 + +pn532_i2c: + id: pn532_nfcc + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_i2c/test.esp32-c3-idf.yaml b/tests/components/pn532_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..62816d2ace --- /dev/null +++ b/tests/components/pn532_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_pn532 + scl: 5 + sda: 4 + +pn532_i2c: + id: pn532_nfcc + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_i2c/test.esp32-idf.yaml b/tests/components/pn532_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..a50533b1d0 --- /dev/null +++ b/tests/components/pn532_i2c/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_pn532 + scl: 16 + sda: 17 + +pn532_i2c: + id: pn532_nfcc + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_i2c/test.esp8266-ard.yaml b/tests/components/pn532_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..62816d2ace --- /dev/null +++ b/tests/components/pn532_i2c/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_pn532 + scl: 5 + sda: 4 + +pn532_i2c: + id: pn532_nfcc + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_i2c/test.rp2040-ard.yaml b/tests/components/pn532_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..62816d2ace --- /dev/null +++ b/tests/components/pn532_i2c/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_pn532 + scl: 5 + sda: 4 + +pn532_i2c: + id: pn532_nfcc + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_spi/test.esp32-ard.yaml b/tests/components/pn532_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..18a382a007 --- /dev/null +++ b/tests/components/pn532_spi/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_pn532 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +pn532_spi: + id: pn532_nfcc + cs_pin: 12 + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_spi/test.esp32-c3-ard.yaml b/tests/components/pn532_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d21d50aa5c --- /dev/null +++ b/tests/components/pn532_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_pn532 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +pn532_spi: + id: pn532_nfcc + cs_pin: 4 + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_spi/test.esp32-c3-idf.yaml b/tests/components/pn532_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d21d50aa5c --- /dev/null +++ b/tests/components/pn532_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_pn532 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +pn532_spi: + id: pn532_nfcc + cs_pin: 4 + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_spi/test.esp32-idf.yaml b/tests/components/pn532_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..18a382a007 --- /dev/null +++ b/tests/components/pn532_spi/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_pn532 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +pn532_spi: + id: pn532_nfcc + cs_pin: 12 + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_spi/test.esp8266-ard.yaml b/tests/components/pn532_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1dba38e63e --- /dev/null +++ b/tests/components/pn532_spi/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_pn532 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +pn532_spi: + id: pn532_nfcc + cs_pin: 15 + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn532_spi/test.rp2040-ard.yaml b/tests/components/pn532_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ab02b2cc47 --- /dev/null +++ b/tests/components/pn532_spi/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_pn532 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +pn532_spi: + id: pn532_nfcc + cs_pin: 6 + +binary_sensor: + - platform: pn532 + pn532_id: pn532_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/pn7150_i2c/test.esp32-ard.yaml b/tests/components/pn7150_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..23d1061608 --- /dev/null +++ b/tests/components/pn7150_i2c/test.esp32-ard.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7150 + - tag.set_format_mode: nfcc_pn7150 + - tag.set_read_mode: nfcc_pn7150 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7150 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7150 + - tag.emulation_on: nfcc_pn7150 + - tag.polling_off: nfcc_pn7150 + - tag.polling_on: nfcc_pn7150 + +i2c: + - id: i2c_pn7150 + scl: 16 + sda: 17 + +pn7150_i2c: + id: nfcc_pn7150 + irq_pin: 12 + ven_pin: 13 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7150_i2c/test.esp32-c3-ard.yaml b/tests/components/pn7150_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..aee1886cd4 --- /dev/null +++ b/tests/components/pn7150_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7150 + - tag.set_format_mode: nfcc_pn7150 + - tag.set_read_mode: nfcc_pn7150 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7150 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7150 + - tag.emulation_on: nfcc_pn7150 + - tag.polling_off: nfcc_pn7150 + - tag.polling_on: nfcc_pn7150 + +i2c: + - id: i2c_pn7150 + scl: 5 + sda: 4 + +pn7150_i2c: + id: nfcc_pn7150 + irq_pin: 2 + ven_pin: 3 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml b/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..aee1886cd4 --- /dev/null +++ b/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7150 + - tag.set_format_mode: nfcc_pn7150 + - tag.set_read_mode: nfcc_pn7150 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7150 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7150 + - tag.emulation_on: nfcc_pn7150 + - tag.polling_off: nfcc_pn7150 + - tag.polling_on: nfcc_pn7150 + +i2c: + - id: i2c_pn7150 + scl: 5 + sda: 4 + +pn7150_i2c: + id: nfcc_pn7150 + irq_pin: 2 + ven_pin: 3 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7150_i2c/test.esp32-idf.yaml b/tests/components/pn7150_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..23d1061608 --- /dev/null +++ b/tests/components/pn7150_i2c/test.esp32-idf.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7150 + - tag.set_format_mode: nfcc_pn7150 + - tag.set_read_mode: nfcc_pn7150 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7150 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7150 + - tag.emulation_on: nfcc_pn7150 + - tag.polling_off: nfcc_pn7150 + - tag.polling_on: nfcc_pn7150 + +i2c: + - id: i2c_pn7150 + scl: 16 + sda: 17 + +pn7150_i2c: + id: nfcc_pn7150 + irq_pin: 12 + ven_pin: 13 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7150_i2c/test.esp8266-ard.yaml b/tests/components/pn7150_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6017d548ca --- /dev/null +++ b/tests/components/pn7150_i2c/test.esp8266-ard.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7150 + - tag.set_format_mode: nfcc_pn7150 + - tag.set_read_mode: nfcc_pn7150 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7150 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7150 + - tag.emulation_on: nfcc_pn7150 + - tag.polling_off: nfcc_pn7150 + - tag.polling_on: nfcc_pn7150 + +i2c: + - id: i2c_pn7150 + scl: 5 + sda: 4 + +pn7150_i2c: + id: nfcc_pn7150 + irq_pin: 12 + ven_pin: 13 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7150_i2c/test.rp2040-ard.yaml b/tests/components/pn7150_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..aee1886cd4 --- /dev/null +++ b/tests/components/pn7150_i2c/test.rp2040-ard.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7150 + - tag.set_format_mode: nfcc_pn7150 + - tag.set_read_mode: nfcc_pn7150 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7150 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7150 + - tag.emulation_on: nfcc_pn7150 + - tag.polling_off: nfcc_pn7150 + - tag.polling_on: nfcc_pn7150 + +i2c: + - id: i2c_pn7150 + scl: 5 + sda: 4 + +pn7150_i2c: + id: nfcc_pn7150 + irq_pin: 2 + ven_pin: 3 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_i2c/test.esp32-ard.yaml b/tests/components/pn7160_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..d1a3cf5c77 --- /dev/null +++ b/tests/components/pn7160_i2c/test.esp32-ard.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +i2c: + - id: i2c_pn7160 + scl: 16 + sda: 17 + +pn7150_i2c: + id: nfcc_pn7160 + irq_pin: 12 + ven_pin: 13 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_i2c/test.esp32-c3-ard.yaml b/tests/components/pn7160_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d1d7947352 --- /dev/null +++ b/tests/components/pn7160_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +i2c: + - id: i2c_pn7160 + scl: 5 + sda: 4 + +pn7160_i2c: + id: nfcc_pn7160 + irq_pin: 2 + ven_pin: 3 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml b/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d1d7947352 --- /dev/null +++ b/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +i2c: + - id: i2c_pn7160 + scl: 5 + sda: 4 + +pn7160_i2c: + id: nfcc_pn7160 + irq_pin: 2 + ven_pin: 3 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_i2c/test.esp32-idf.yaml b/tests/components/pn7160_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..d1a3cf5c77 --- /dev/null +++ b/tests/components/pn7160_i2c/test.esp32-idf.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +i2c: + - id: i2c_pn7160 + scl: 16 + sda: 17 + +pn7150_i2c: + id: nfcc_pn7160 + irq_pin: 12 + ven_pin: 13 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_i2c/test.esp8266-ard.yaml b/tests/components/pn7160_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..57bd965fc9 --- /dev/null +++ b/tests/components/pn7160_i2c/test.esp8266-ard.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +i2c: + - id: i2c_pn7160 + scl: 5 + sda: 4 + +pn7150_i2c: + id: nfcc_pn7160 + irq_pin: 12 + ven_pin: 13 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_i2c/test.rp2040-ard.yaml b/tests/components/pn7160_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5224b465ed --- /dev/null +++ b/tests/components/pn7160_i2c/test.rp2040-ard.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +i2c: + - id: i2c_pn7160 + scl: 5 + sda: 4 + +pn7150_i2c: + id: nfcc_pn7160 + irq_pin: 2 + ven_pin: 3 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_spi/test.esp32-ard.yaml b/tests/components/pn7160_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..0319648f13 --- /dev/null +++ b/tests/components/pn7160_spi/test.esp32-ard.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +spi: + - id: spi_pn7160 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +pn7160_spi: + id: nfcc_pn7160 + cs_pin: 12 + irq_pin: 14 + ven_pin: 13 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_spi/test.esp32-c3-ard.yaml b/tests/components/pn7160_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fd19a53b2b --- /dev/null +++ b/tests/components/pn7160_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +spi: + - id: spi_pn7160 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +pn7160_spi: + id: nfcc_pn7160 + cs_pin: 4 + irq_pin: 2 + ven_pin: 3 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_spi/test.esp32-c3-idf.yaml b/tests/components/pn7160_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fd19a53b2b --- /dev/null +++ b/tests/components/pn7160_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +spi: + - id: spi_pn7160 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +pn7160_spi: + id: nfcc_pn7160 + cs_pin: 4 + irq_pin: 2 + ven_pin: 3 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_spi/test.esp32-idf.yaml b/tests/components/pn7160_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..0319648f13 --- /dev/null +++ b/tests/components/pn7160_spi/test.esp32-idf.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +spi: + - id: spi_pn7160 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +pn7160_spi: + id: nfcc_pn7160 + cs_pin: 12 + irq_pin: 14 + ven_pin: 13 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_spi/test.esp8266-ard.yaml b/tests/components/pn7160_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..fa356d5610 --- /dev/null +++ b/tests/components/pn7160_spi/test.esp8266-ard.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +spi: + - id: spi_pn7160 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +pn7160_spi: + id: nfcc_pn7160 + cs_pin: 15 + irq_pin: 4 + ven_pin: 5 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/pn7160_spi/test.rp2040-ard.yaml b/tests/components/pn7160_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b36650032f --- /dev/null +++ b/tests/components/pn7160_spi/test.rp2040-ard.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - tag.set_clean_mode: nfcc_pn7160 + - tag.set_format_mode: nfcc_pn7160 + - tag.set_read_mode: nfcc_pn7160 + - tag.set_write_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.set_write_mode: nfcc_pn7160 + - tag.set_emulation_message: + message: https://www.home-assistant.io/tag/pulse + include_android_app_record: false + - tag.emulation_off: nfcc_pn7160 + - tag.emulation_on: nfcc_pn7160 + - tag.polling_off: nfcc_pn7160 + - tag.polling_on: nfcc_pn7160 + +spi: + - id: spi_pn7160 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +pn7160_spi: + id: nfcc_pn7160 + cs_pin: 6 + irq_pin: 7 + ven_pin: 5 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + on_tag: + - logger.log: "Tag" + on_tag_removed: + - logger.log: "Tag removed" + on_emulated_tag_scan: + - logger.log: "Tag emulated" diff --git a/tests/components/power_supply/common.yaml b/tests/components/power_supply/common.yaml new file mode 100644 index 0000000000..3fefc4d425 --- /dev/null +++ b/tests/components/power_supply/common.yaml @@ -0,0 +1,6 @@ +power_supply: + - id: atx_power_supply + enable_time: 20ms + keep_on_time: 10s + enable_on_boot: true + pin: 4 diff --git a/tests/components/power_supply/test.esp32-ard.yaml b/tests/components/power_supply/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/power_supply/test.esp32-c3-ard.yaml b/tests/components/power_supply/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/power_supply/test.esp32-c3-idf.yaml b/tests/components/power_supply/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/power_supply/test.esp32-idf.yaml b/tests/components/power_supply/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/power_supply/test.esp8266-ard.yaml b/tests/components/power_supply/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/power_supply/test.rp2040-ard.yaml b/tests/components/power_supply/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml new file mode 100644 index 0000000000..68ef2a2f58 --- /dev/null +++ b/tests/components/prometheus/common.yaml @@ -0,0 +1,89 @@ +esphome: + name: livingroomdevice + friendly_name: Living Room Device + area: Living Room + +wifi: + ssid: MySSID + password: password1 + +sensor: + - platform: template + id: template_sensor1 + lambda: |- + if (millis() > 10000) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +text_sensor: + - platform: version + name: "ESPHome Version" + hide_timestamp: true + - platform: template + id: template_text_sensor1 + lambda: |- + if (millis() > 10000) { + return {"Hello World"}; + } else { + return {"Goodbye (cruel) World"}; + } + update_interval: 60s + +binary_sensor: + - platform: template + id: template_binary_sensor1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +switch: + - platform: template + id: template_switch1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + optimistic: true + +fan: + - platform: template + id: template_fan1 + +cover: + - platform: template + id: template_cover1 + lambda: |- + if (millis() > 10000) { + return COVER_OPEN; + } else { + return COVER_CLOSED; + } + +lock: + - platform: template + id: template_lock1 + lambda: |- + if (millis() > 10000) { + return LOCK_STATE_LOCKED; + } else { + return LOCK_STATE_UNLOCKED; + } + optimistic: true + +prometheus: + include_internal: true + relabel: + template_sensor1: + id: hellow_world + name: Hello World + template_text_sensor1: + id: hello_text + name: Text Substitution diff --git a/tests/components/prometheus/test.esp32-ard.yaml b/tests/components/prometheus/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/prometheus/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-c3-ard.yaml b/tests/components/prometheus/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/prometheus/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-c3-idf.yaml b/tests/components/prometheus/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/prometheus/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-idf.yaml b/tests/components/prometheus/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/prometheus/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/prometheus/test.esp8266-ard.yaml b/tests/components/prometheus/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/prometheus/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/common.yaml b/tests/components/psram/common.yaml new file mode 100644 index 0000000000..cfd39f77fe --- /dev/null +++ b/tests/components/psram/common.yaml @@ -0,0 +1,3 @@ +psram: + mode: octal + speed: 80MHz diff --git a/tests/components/psram/test.esp32-ard.yaml b/tests/components/psram/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-c3-ard.yaml b/tests/components/psram/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-c3-idf.yaml b/tests/components/psram/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-idf.yaml b/tests/components/psram/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/common.yaml b/tests/components/pulse_counter/common.yaml new file mode 100644 index 0000000000..556b43ee6f --- /dev/null +++ b/tests/components/pulse_counter/common.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: pulse_counter + name: Pulse Counter + pin: 4 + count_mode: + rising_edge: INCREMENT + falling_edge: DECREMENT + internal_filter: 13us + update_interval: 15s diff --git a/tests/components/pulse_counter/test.esp32-ard.yaml b/tests/components/pulse_counter/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.esp32-c3-ard.yaml b/tests/components/pulse_counter/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.esp32-c3-idf.yaml b/tests/components/pulse_counter/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.esp32-idf.yaml b/tests/components/pulse_counter/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.esp8266-ard.yaml b/tests/components/pulse_counter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.rp2040-ard.yaml b/tests/components/pulse_counter/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/common.yaml b/tests/components/pulse_meter/common.yaml new file mode 100644 index 0000000000..a83ec478bb --- /dev/null +++ b/tests/components/pulse_meter/common.yaml @@ -0,0 +1,13 @@ +sensor: + - platform: pulse_meter + id: pulse_meter_sensor + name: Pulse Meter + pin: 4 + internal_filter: 100ms + timeout: 2 min + on_value: + - pulse_meter.set_total_pulses: + id: pulse_meter_sensor + value: 12345 + total: + name: Pulse Meter Total diff --git a/tests/components/pulse_meter/test.esp32-ard.yaml b/tests/components/pulse_meter/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.esp32-c3-ard.yaml b/tests/components/pulse_meter/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.esp32-c3-idf.yaml b/tests/components/pulse_meter/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.esp32-idf.yaml b/tests/components/pulse_meter/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.esp8266-ard.yaml b/tests/components/pulse_meter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.rp2040-ard.yaml b/tests/components/pulse_meter/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/common.yaml b/tests/components/pulse_width/common.yaml new file mode 100644 index 0000000000..fbda7cda28 --- /dev/null +++ b/tests/components/pulse_width/common.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: pulse_width + name: Pulse Width + pin: 4 diff --git a/tests/components/pulse_width/test.esp32-ard.yaml b/tests/components/pulse_width/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.esp32-c3-ard.yaml b/tests/components/pulse_width/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.esp32-c3-idf.yaml b/tests/components/pulse_width/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.esp32-idf.yaml b/tests/components/pulse_width/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.esp8266-ard.yaml b/tests/components/pulse_width/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.rp2040-ard.yaml b/tests/components/pulse_width/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/common.yaml b/tests/components/pvvx_mithermometer/common.yaml new file mode 100644 index 0000000000..972f23122c --- /dev/null +++ b/tests/components/pvvx_mithermometer/common.yaml @@ -0,0 +1,44 @@ +wifi: + ssid: MySSID + password: password1 + +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: pvvx_ble_display + +display: + - platform: pvvx_mithermometer + ble_client_id: pvvx_ble_display + time_id: sntp_time + disconnect_delay: 3s + update_interval: 10min + validity_period: 20min + lambda: |- + it.print_bignum(188.8); + it.print_unit(pvvx_mithermometer::UNIT_DEG_E); + it.print_smallnum(88); + it.print_percent(true); + it.print_happy(true); + it.print_sad(true); + it.print_bracket(true); + it.print_battery(true); + +sensor: + - platform: pvvx_mithermometer + mac_address: A4:C1:38:4E:16:78 + temperature: + name: PVVX Temperature + humidity: + name: PVVX Humidity + battery_level: + name: PVVX Battery-Level + battery_voltage: + name: PVVX Battery-Voltage + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org diff --git a/tests/components/pvvx_mithermometer/test.esp32-ard.yaml b/tests/components/pvvx_mithermometer/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pvvx_mithermometer/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-c3-ard.yaml b/tests/components/pvvx_mithermometer/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pvvx_mithermometer/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml b/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-idf.yaml b/tests/components/pvvx_mithermometer/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pvvx_mithermometer/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pylontech/test.esp32-ard.yaml b/tests/components/pylontech/test.esp32-ard.yaml new file mode 100644 index 0000000000..a4c168fb47 --- /dev/null +++ b/tests/components/pylontech/test.esp32-ard.yaml @@ -0,0 +1,48 @@ +uart: + - id: uart_pylontech0 + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +pylontech: + - id: pylontech0 + - id: pylontech1 + +sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + voltage: + id: pyl01_voltage + current: + id: pyl01_current + coulomb: + id: pyl01_soc + mos_temperature: + id: pyl01_mos_temperature + - platform: pylontech + pylontech_id: pylontech1 + battery: 1 + voltage: + id: pyl13_voltage + temperature_low: + id: pyl13_temperature_low + temperature_high: + id: pyl13_temperature_high + voltage_low: + id: pyl13_voltage_low + voltage_high: + id: pyl13_voltage_high + +text_sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + base_state: + id: pyl0_base_state + voltage_state: + id: pyl0_voltage_state + current_state: + id: pyl0_current_state + temperature_state: + id: pyl0_temperature_state diff --git a/tests/components/pylontech/test.esp32-c3-ard.yaml b/tests/components/pylontech/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f7ec493422 --- /dev/null +++ b/tests/components/pylontech/test.esp32-c3-ard.yaml @@ -0,0 +1,48 @@ +uart: + - id: uart_pylontech0 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +pylontech: + - id: pylontech0 + - id: pylontech1 + +sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + voltage: + id: pyl01_voltage + current: + id: pyl01_current + coulomb: + id: pyl01_soc + mos_temperature: + id: pyl01_mos_temperature + - platform: pylontech + pylontech_id: pylontech1 + battery: 1 + voltage: + id: pyl13_voltage + temperature_low: + id: pyl13_temperature_low + temperature_high: + id: pyl13_temperature_high + voltage_low: + id: pyl13_voltage_low + voltage_high: + id: pyl13_voltage_high + +text_sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + base_state: + id: pyl0_base_state + voltage_state: + id: pyl0_voltage_state + current_state: + id: pyl0_current_state + temperature_state: + id: pyl0_temperature_state diff --git a/tests/components/pylontech/test.esp32-c3-idf.yaml b/tests/components/pylontech/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f7ec493422 --- /dev/null +++ b/tests/components/pylontech/test.esp32-c3-idf.yaml @@ -0,0 +1,48 @@ +uart: + - id: uart_pylontech0 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +pylontech: + - id: pylontech0 + - id: pylontech1 + +sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + voltage: + id: pyl01_voltage + current: + id: pyl01_current + coulomb: + id: pyl01_soc + mos_temperature: + id: pyl01_mos_temperature + - platform: pylontech + pylontech_id: pylontech1 + battery: 1 + voltage: + id: pyl13_voltage + temperature_low: + id: pyl13_temperature_low + temperature_high: + id: pyl13_temperature_high + voltage_low: + id: pyl13_voltage_low + voltage_high: + id: pyl13_voltage_high + +text_sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + base_state: + id: pyl0_base_state + voltage_state: + id: pyl0_voltage_state + current_state: + id: pyl0_current_state + temperature_state: + id: pyl0_temperature_state diff --git a/tests/components/pylontech/test.esp32-idf.yaml b/tests/components/pylontech/test.esp32-idf.yaml new file mode 100644 index 0000000000..a4c168fb47 --- /dev/null +++ b/tests/components/pylontech/test.esp32-idf.yaml @@ -0,0 +1,48 @@ +uart: + - id: uart_pylontech0 + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +pylontech: + - id: pylontech0 + - id: pylontech1 + +sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + voltage: + id: pyl01_voltage + current: + id: pyl01_current + coulomb: + id: pyl01_soc + mos_temperature: + id: pyl01_mos_temperature + - platform: pylontech + pylontech_id: pylontech1 + battery: 1 + voltage: + id: pyl13_voltage + temperature_low: + id: pyl13_temperature_low + temperature_high: + id: pyl13_temperature_high + voltage_low: + id: pyl13_voltage_low + voltage_high: + id: pyl13_voltage_high + +text_sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + base_state: + id: pyl0_base_state + voltage_state: + id: pyl0_voltage_state + current_state: + id: pyl0_current_state + temperature_state: + id: pyl0_temperature_state diff --git a/tests/components/pylontech/test.esp8266-ard.yaml b/tests/components/pylontech/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f7ec493422 --- /dev/null +++ b/tests/components/pylontech/test.esp8266-ard.yaml @@ -0,0 +1,48 @@ +uart: + - id: uart_pylontech0 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +pylontech: + - id: pylontech0 + - id: pylontech1 + +sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + voltage: + id: pyl01_voltage + current: + id: pyl01_current + coulomb: + id: pyl01_soc + mos_temperature: + id: pyl01_mos_temperature + - platform: pylontech + pylontech_id: pylontech1 + battery: 1 + voltage: + id: pyl13_voltage + temperature_low: + id: pyl13_temperature_low + temperature_high: + id: pyl13_temperature_high + voltage_low: + id: pyl13_voltage_low + voltage_high: + id: pyl13_voltage_high + +text_sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + base_state: + id: pyl0_base_state + voltage_state: + id: pyl0_voltage_state + current_state: + id: pyl0_current_state + temperature_state: + id: pyl0_temperature_state diff --git a/tests/components/pylontech/test.rp2040-ard.yaml b/tests/components/pylontech/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f7ec493422 --- /dev/null +++ b/tests/components/pylontech/test.rp2040-ard.yaml @@ -0,0 +1,48 @@ +uart: + - id: uart_pylontech0 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +pylontech: + - id: pylontech0 + - id: pylontech1 + +sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + voltage: + id: pyl01_voltage + current: + id: pyl01_current + coulomb: + id: pyl01_soc + mos_temperature: + id: pyl01_mos_temperature + - platform: pylontech + pylontech_id: pylontech1 + battery: 1 + voltage: + id: pyl13_voltage + temperature_low: + id: pyl13_temperature_low + temperature_high: + id: pyl13_temperature_high + voltage_low: + id: pyl13_voltage_low + voltage_high: + id: pyl13_voltage_high + +text_sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + base_state: + id: pyl0_base_state + voltage_state: + id: pyl0_voltage_state + current_state: + id: pyl0_current_state + temperature_state: + id: pyl0_temperature_state diff --git a/tests/components/pzem004t/test.esp32-ard.yaml b/tests/components/pzem004t/test.esp32-ard.yaml new file mode 100644 index 0000000000..23f2bd0eca --- /dev/null +++ b/tests/components/pzem004t/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_pzem004t + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +sensor: + - platform: pzem004t + voltage: + name: PZEM004T Voltage + current: + name: PZEM004T Current + power: + name: PZEM004T Power diff --git a/tests/components/pzem004t/test.esp32-c3-ard.yaml b/tests/components/pzem004t/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b9c93f8761 --- /dev/null +++ b/tests/components/pzem004t/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_pzem004t + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: pzem004t + voltage: + name: PZEM004T Voltage + current: + name: PZEM004T Current + power: + name: PZEM004T Power diff --git a/tests/components/pzem004t/test.esp32-c3-idf.yaml b/tests/components/pzem004t/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b9c93f8761 --- /dev/null +++ b/tests/components/pzem004t/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_pzem004t + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: pzem004t + voltage: + name: PZEM004T Voltage + current: + name: PZEM004T Current + power: + name: PZEM004T Power diff --git a/tests/components/pzem004t/test.esp32-idf.yaml b/tests/components/pzem004t/test.esp32-idf.yaml new file mode 100644 index 0000000000..23f2bd0eca --- /dev/null +++ b/tests/components/pzem004t/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_pzem004t + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +sensor: + - platform: pzem004t + voltage: + name: PZEM004T Voltage + current: + name: PZEM004T Current + power: + name: PZEM004T Power diff --git a/tests/components/pzem004t/test.esp8266-ard.yaml b/tests/components/pzem004t/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b9c93f8761 --- /dev/null +++ b/tests/components/pzem004t/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_pzem004t + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: pzem004t + voltage: + name: PZEM004T Voltage + current: + name: PZEM004T Current + power: + name: PZEM004T Power diff --git a/tests/components/pzem004t/test.rp2040-ard.yaml b/tests/components/pzem004t/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b9c93f8761 --- /dev/null +++ b/tests/components/pzem004t/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_pzem004t + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: pzem004t + voltage: + name: PZEM004T Voltage + current: + name: PZEM004T Current + power: + name: PZEM004T Power diff --git a/tests/components/pzemac/test.esp32-ard.yaml b/tests/components/pzemac/test.esp32-ard.yaml new file mode 100644 index 0000000000..ce431a6100 --- /dev/null +++ b/tests/components/pzemac/test.esp32-ard.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - pzemac.reset_energy: pzemac1 + +uart: + - id: uart_pzemac + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + +sensor: + - platform: pzemac + id: pzemac1 + voltage: + name: PZEMAC Voltage + current: + name: PZEMAC Current + power: + name: PZEMAC Power + energy: + name: PZEMAC Energy + frequency: + name: PZEMAC Frequency + power_factor: + name: PZEMAC Power Factor diff --git a/tests/components/pzemac/test.esp32-c3-ard.yaml b/tests/components/pzemac/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..6d9abbebe9 --- /dev/null +++ b/tests/components/pzemac/test.esp32-c3-ard.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - pzemac.reset_energy: pzemac1 + +uart: + - id: uart_pzemac + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + +sensor: + - platform: pzemac + id: pzemac1 + voltage: + name: PZEMAC Voltage + current: + name: PZEMAC Current + power: + name: PZEMAC Power + energy: + name: PZEMAC Energy + frequency: + name: PZEMAC Frequency + power_factor: + name: PZEMAC Power Factor diff --git a/tests/components/pzemac/test.esp32-c3-idf.yaml b/tests/components/pzemac/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6d9abbebe9 --- /dev/null +++ b/tests/components/pzemac/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - pzemac.reset_energy: pzemac1 + +uart: + - id: uart_pzemac + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + +sensor: + - platform: pzemac + id: pzemac1 + voltage: + name: PZEMAC Voltage + current: + name: PZEMAC Current + power: + name: PZEMAC Power + energy: + name: PZEMAC Energy + frequency: + name: PZEMAC Frequency + power_factor: + name: PZEMAC Power Factor diff --git a/tests/components/pzemac/test.esp32-idf.yaml b/tests/components/pzemac/test.esp32-idf.yaml new file mode 100644 index 0000000000..ce431a6100 --- /dev/null +++ b/tests/components/pzemac/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - pzemac.reset_energy: pzemac1 + +uart: + - id: uart_pzemac + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +modbus: + +sensor: + - platform: pzemac + id: pzemac1 + voltage: + name: PZEMAC Voltage + current: + name: PZEMAC Current + power: + name: PZEMAC Power + energy: + name: PZEMAC Energy + frequency: + name: PZEMAC Frequency + power_factor: + name: PZEMAC Power Factor diff --git a/tests/components/pzemac/test.esp8266-ard.yaml b/tests/components/pzemac/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6d9abbebe9 --- /dev/null +++ b/tests/components/pzemac/test.esp8266-ard.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - pzemac.reset_energy: pzemac1 + +uart: + - id: uart_pzemac + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + +sensor: + - platform: pzemac + id: pzemac1 + voltage: + name: PZEMAC Voltage + current: + name: PZEMAC Current + power: + name: PZEMAC Power + energy: + name: PZEMAC Energy + frequency: + name: PZEMAC Frequency + power_factor: + name: PZEMAC Power Factor diff --git a/tests/components/pzemac/test.rp2040-ard.yaml b/tests/components/pzemac/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6d9abbebe9 --- /dev/null +++ b/tests/components/pzemac/test.rp2040-ard.yaml @@ -0,0 +1,28 @@ +esphome: + on_boot: + then: + - pzemac.reset_energy: pzemac1 + +uart: + - id: uart_pzemac + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +modbus: + +sensor: + - platform: pzemac + id: pzemac1 + voltage: + name: PZEMAC Voltage + current: + name: PZEMAC Current + power: + name: PZEMAC Power + energy: + name: PZEMAC Energy + frequency: + name: PZEMAC Frequency + power_factor: + name: PZEMAC Power Factor diff --git a/tests/components/pzemdc/test.esp32-ard.yaml b/tests/components/pzemdc/test.esp32-ard.yaml new file mode 100644 index 0000000000..9cc61137de --- /dev/null +++ b/tests/components/pzemdc/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - pzemdc.reset_energy: pzemdc1 + +uart: + - id: uart_pzemdc + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + stop_bits: 2 + +sensor: + - platform: pzemdc + id: pzemdc1 + voltage: + name: PZEMDC Voltage + current: + name: PZEMDC Current + power: + name: PZEMDC Power + energy: + name: PZEMDC Energy diff --git a/tests/components/pzemdc/test.esp32-c3-ard.yaml b/tests/components/pzemdc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..02114b781d --- /dev/null +++ b/tests/components/pzemdc/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - pzemdc.reset_energy: pzemdc1 + +uart: + - id: uart_pzemdc + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + stop_bits: 2 + +sensor: + - platform: pzemdc + id: pzemdc1 + voltage: + name: PZEMDC Voltage + current: + name: PZEMDC Current + power: + name: PZEMDC Power + energy: + name: PZEMDC Energy diff --git a/tests/components/pzemdc/test.esp32-c3-idf.yaml b/tests/components/pzemdc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..02114b781d --- /dev/null +++ b/tests/components/pzemdc/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - pzemdc.reset_energy: pzemdc1 + +uart: + - id: uart_pzemdc + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + stop_bits: 2 + +sensor: + - platform: pzemdc + id: pzemdc1 + voltage: + name: PZEMDC Voltage + current: + name: PZEMDC Current + power: + name: PZEMDC Power + energy: + name: PZEMDC Energy diff --git a/tests/components/pzemdc/test.esp32-idf.yaml b/tests/components/pzemdc/test.esp32-idf.yaml new file mode 100644 index 0000000000..9cc61137de --- /dev/null +++ b/tests/components/pzemdc/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - pzemdc.reset_energy: pzemdc1 + +uart: + - id: uart_pzemdc + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + stop_bits: 2 + +sensor: + - platform: pzemdc + id: pzemdc1 + voltage: + name: PZEMDC Voltage + current: + name: PZEMDC Current + power: + name: PZEMDC Power + energy: + name: PZEMDC Energy diff --git a/tests/components/pzemdc/test.esp8266-ard.yaml b/tests/components/pzemdc/test.esp8266-ard.yaml new file mode 100644 index 0000000000..02114b781d --- /dev/null +++ b/tests/components/pzemdc/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - pzemdc.reset_energy: pzemdc1 + +uart: + - id: uart_pzemdc + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + stop_bits: 2 + +sensor: + - platform: pzemdc + id: pzemdc1 + voltage: + name: PZEMDC Voltage + current: + name: PZEMDC Current + power: + name: PZEMDC Power + energy: + name: PZEMDC Energy diff --git a/tests/components/pzemdc/test.rp2040-ard.yaml b/tests/components/pzemdc/test.rp2040-ard.yaml new file mode 100644 index 0000000000..02114b781d --- /dev/null +++ b/tests/components/pzemdc/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +esphome: + on_boot: + then: + - pzemdc.reset_energy: pzemdc1 + +uart: + - id: uart_pzemdc + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + stop_bits: 2 + +sensor: + - platform: pzemdc + id: pzemdc1 + voltage: + name: PZEMDC Voltage + current: + name: PZEMDC Current + power: + name: PZEMDC Power + energy: + name: PZEMDC Energy diff --git a/tests/components/qmc5883l/test.esp32-ard.yaml b/tests/components/qmc5883l/test.esp32-ard.yaml new file mode 100644 index 0000000000..9acd391497 --- /dev/null +++ b/tests/components/qmc5883l/test.esp32-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_qmc5883l + scl: 16 + sda: 17 + +sensor: + - platform: qmc5883l + address: 0x0D + field_strength_x: + name: QMC5883L Field Strength X + field_strength_y: + name: QMC5883L Field Strength Y + field_strength_z: + name: QMC5883L Field Strength Z + heading: + name: QMC5883L Heading + temperature: + name: QMC5883L Temperature + range: 800uT + oversampling: 256x + update_interval: 15s diff --git a/tests/components/qmc5883l/test.esp32-c3-ard.yaml b/tests/components/qmc5883l/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..841bbd5d1e --- /dev/null +++ b/tests/components/qmc5883l/test.esp32-c3-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_qmc5883l + scl: 5 + sda: 4 + +sensor: + - platform: qmc5883l + address: 0x0D + field_strength_x: + name: QMC5883L Field Strength X + field_strength_y: + name: QMC5883L Field Strength Y + field_strength_z: + name: QMC5883L Field Strength Z + heading: + name: QMC5883L Heading + temperature: + name: QMC5883L Temperature + range: 800uT + oversampling: 256x + update_interval: 15s diff --git a/tests/components/qmc5883l/test.esp32-c3-idf.yaml b/tests/components/qmc5883l/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..841bbd5d1e --- /dev/null +++ b/tests/components/qmc5883l/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_qmc5883l + scl: 5 + sda: 4 + +sensor: + - platform: qmc5883l + address: 0x0D + field_strength_x: + name: QMC5883L Field Strength X + field_strength_y: + name: QMC5883L Field Strength Y + field_strength_z: + name: QMC5883L Field Strength Z + heading: + name: QMC5883L Heading + temperature: + name: QMC5883L Temperature + range: 800uT + oversampling: 256x + update_interval: 15s diff --git a/tests/components/qmc5883l/test.esp32-idf.yaml b/tests/components/qmc5883l/test.esp32-idf.yaml new file mode 100644 index 0000000000..9acd391497 --- /dev/null +++ b/tests/components/qmc5883l/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_qmc5883l + scl: 16 + sda: 17 + +sensor: + - platform: qmc5883l + address: 0x0D + field_strength_x: + name: QMC5883L Field Strength X + field_strength_y: + name: QMC5883L Field Strength Y + field_strength_z: + name: QMC5883L Field Strength Z + heading: + name: QMC5883L Heading + temperature: + name: QMC5883L Temperature + range: 800uT + oversampling: 256x + update_interval: 15s diff --git a/tests/components/qmc5883l/test.esp8266-ard.yaml b/tests/components/qmc5883l/test.esp8266-ard.yaml new file mode 100644 index 0000000000..841bbd5d1e --- /dev/null +++ b/tests/components/qmc5883l/test.esp8266-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_qmc5883l + scl: 5 + sda: 4 + +sensor: + - platform: qmc5883l + address: 0x0D + field_strength_x: + name: QMC5883L Field Strength X + field_strength_y: + name: QMC5883L Field Strength Y + field_strength_z: + name: QMC5883L Field Strength Z + heading: + name: QMC5883L Heading + temperature: + name: QMC5883L Temperature + range: 800uT + oversampling: 256x + update_interval: 15s diff --git a/tests/components/qmc5883l/test.rp2040-ard.yaml b/tests/components/qmc5883l/test.rp2040-ard.yaml new file mode 100644 index 0000000000..841bbd5d1e --- /dev/null +++ b/tests/components/qmc5883l/test.rp2040-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_qmc5883l + scl: 5 + sda: 4 + +sensor: + - platform: qmc5883l + address: 0x0D + field_strength_x: + name: QMC5883L Field Strength X + field_strength_y: + name: QMC5883L Field Strength Y + field_strength_z: + name: QMC5883L Field Strength Z + heading: + name: QMC5883L Heading + temperature: + name: QMC5883L Temperature + range: 800uT + oversampling: 256x + update_interval: 15s diff --git a/tests/components/qmp6988/test.esp32-ard.yaml b/tests/components/qmp6988/test.esp32-ard.yaml new file mode 100644 index 0000000000..f3fbf75bbe --- /dev/null +++ b/tests/components/qmp6988/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_qmp6988 + scl: 16 + sda: 17 + +sensor: + - platform: qmp6988 + temperature: + name: QMP6988 Temperature + oversampling: 32x + pressure: + name: QMP6988 Pressure + oversampling: 2x + address: 0x70 + update_interval: 30s + iir_filter: 16x diff --git a/tests/components/qmp6988/test.esp32-c3-ard.yaml b/tests/components/qmp6988/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..bcd87ae6b8 --- /dev/null +++ b/tests/components/qmp6988/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_qmp6988 + scl: 5 + sda: 4 + +sensor: + - platform: qmp6988 + temperature: + name: QMP6988 Temperature + oversampling: 32x + pressure: + name: QMP6988 Pressure + oversampling: 2x + address: 0x70 + update_interval: 30s + iir_filter: 16x diff --git a/tests/components/qmp6988/test.esp32-c3-idf.yaml b/tests/components/qmp6988/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..bcd87ae6b8 --- /dev/null +++ b/tests/components/qmp6988/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_qmp6988 + scl: 5 + sda: 4 + +sensor: + - platform: qmp6988 + temperature: + name: QMP6988 Temperature + oversampling: 32x + pressure: + name: QMP6988 Pressure + oversampling: 2x + address: 0x70 + update_interval: 30s + iir_filter: 16x diff --git a/tests/components/qmp6988/test.esp32-idf.yaml b/tests/components/qmp6988/test.esp32-idf.yaml new file mode 100644 index 0000000000..f3fbf75bbe --- /dev/null +++ b/tests/components/qmp6988/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_qmp6988 + scl: 16 + sda: 17 + +sensor: + - platform: qmp6988 + temperature: + name: QMP6988 Temperature + oversampling: 32x + pressure: + name: QMP6988 Pressure + oversampling: 2x + address: 0x70 + update_interval: 30s + iir_filter: 16x diff --git a/tests/components/qmp6988/test.esp8266-ard.yaml b/tests/components/qmp6988/test.esp8266-ard.yaml new file mode 100644 index 0000000000..bcd87ae6b8 --- /dev/null +++ b/tests/components/qmp6988/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_qmp6988 + scl: 5 + sda: 4 + +sensor: + - platform: qmp6988 + temperature: + name: QMP6988 Temperature + oversampling: 32x + pressure: + name: QMP6988 Pressure + oversampling: 2x + address: 0x70 + update_interval: 30s + iir_filter: 16x diff --git a/tests/components/qmp6988/test.rp2040-ard.yaml b/tests/components/qmp6988/test.rp2040-ard.yaml new file mode 100644 index 0000000000..bcd87ae6b8 --- /dev/null +++ b/tests/components/qmp6988/test.rp2040-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_qmp6988 + scl: 5 + sda: 4 + +sensor: + - platform: qmp6988 + temperature: + name: QMP6988 Temperature + oversampling: 32x + pressure: + name: QMP6988 Pressure + oversampling: 2x + address: 0x70 + update_interval: 30s + iir_filter: 16x diff --git a/tests/components/qr_code/test.esp32-ard.yaml b/tests/components/qr_code/test.esp32-ard.yaml new file mode 100644 index 0000000000..8689d4d73f --- /dev/null +++ b/tests/components/qr_code/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: false + lambda: |- + // Draw a QR code in the center of the screen + auto scale = 2; + auto size = id(homepage_qr).get_size() * scale; + auto x = (it.get_width() / 2) - (size / 2); + auto y = (it.get_height() / 2) - (size / 2); + it.qr_code(x, y, id(homepage_qr), Color(255,255,255), scale); + +qr_code: + - id: homepage_qr + value: https://esphome.io/index.html diff --git a/tests/components/qr_code/test.esp32-c3-ard.yaml b/tests/components/qr_code/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..3690d2598c --- /dev/null +++ b/tests/components/qr_code/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + lambda: |- + // Draw a QR code in the center of the screen + auto scale = 2; + auto size = id(homepage_qr).get_size() * scale; + auto x = (it.get_width() / 2) - (size / 2); + auto y = (it.get_height() / 2) - (size / 2); + it.qr_code(x, y, id(homepage_qr), Color(255,255,255), scale); + +qr_code: + - id: homepage_qr + value: https://esphome.io/index.html diff --git a/tests/components/qr_code/test.esp32-c3-idf.yaml b/tests/components/qr_code/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3690d2598c --- /dev/null +++ b/tests/components/qr_code/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + lambda: |- + // Draw a QR code in the center of the screen + auto scale = 2; + auto size = id(homepage_qr).get_size() * scale; + auto x = (it.get_width() / 2) - (size / 2); + auto y = (it.get_height() / 2) - (size / 2); + it.qr_code(x, y, id(homepage_qr), Color(255,255,255), scale); + +qr_code: + - id: homepage_qr + value: https://esphome.io/index.html diff --git a/tests/components/qr_code/test.esp32-idf.yaml b/tests/components/qr_code/test.esp32-idf.yaml new file mode 100644 index 0000000000..8689d4d73f --- /dev/null +++ b/tests/components/qr_code/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: false + lambda: |- + // Draw a QR code in the center of the screen + auto scale = 2; + auto size = id(homepage_qr).get_size() * scale; + auto x = (it.get_width() / 2) - (size / 2); + auto y = (it.get_height() / 2) - (size / 2); + it.qr_code(x, y, id(homepage_qr), Color(255,255,255), scale); + +qr_code: + - id: homepage_qr + value: https://esphome.io/index.html diff --git a/tests/components/qr_code/test.esp8266-ard.yaml b/tests/components/qr_code/test.esp8266-ard.yaml new file mode 100644 index 0000000000..02dc183440 --- /dev/null +++ b/tests/components/qr_code/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 5 + dc_pin: 15 + reset_pin: 16 + invert_colors: false + lambda: |- + // Draw a QR code in the center of the screen + auto scale = 2; + auto size = id(homepage_qr).get_size() * scale; + auto x = (it.get_width() / 2) - (size / 2); + auto y = (it.get_height() / 2) - (size / 2); + it.qr_code(x, y, id(homepage_qr), Color(255,255,255), scale); + +qr_code: + - id: homepage_qr + value: https://esphome.io/index.html diff --git a/tests/components/qr_code/test.rp2040-ard.yaml b/tests/components/qr_code/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0d86f8d213 --- /dev/null +++ b/tests/components/qr_code/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_main_lcd + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 21 + reset_pin: 22 + invert_colors: false + lambda: |- + // Draw a QR code in the center of the screen + auto scale = 2; + auto size = id(homepage_qr).get_size() * scale; + auto x = (it.get_width() / 2) - (size / 2); + auto y = (it.get_height() / 2) - (size / 2); + it.qr_code(x, y, id(homepage_qr), Color(255,255,255), scale); + +qr_code: + - id: homepage_qr + value: https://esphome.io/index.html diff --git a/tests/components/qspi_dbi/common.yaml b/tests/components/qspi_dbi/common.yaml new file mode 100644 index 0000000000..655af304af --- /dev/null +++ b/tests/components/qspi_dbi/common.yaml @@ -0,0 +1,44 @@ +spi: + id: quad_spi + clk_pin: 15 + type: quad + data_pins: [14, 10, 16, 12] + +display: + - platform: qspi_dbi + model: RM690B0 + data_rate: 80MHz + spi_mode: mode0 + dimensions: + width: 450 + height: 600 + offset_width: 16 + color_order: rgb + invert_colors: false + brightness: 255 + cs_pin: 11 + reset_pin: 13 + enable_pin: 9 + + - platform: qspi_dbi + model: CUSTOM + id: main_lcd + draw_from_origin: true + dimensions: + height: 240 + width: 536 + transform: + mirror_x: true + swap_xy: true + color_order: rgb + brightness: 255 + cs_pin: 6 + reset_pin: 17 + enable_pin: 38 + init_sequence: + - [0x3A, 0x66] + - [0x11] + - delay 120ms + - [0x29] + - delay 20ms + diff --git a/tests/components/qspi_dbi/test.esp32-s3-idf.yaml b/tests/components/qspi_dbi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/qspi_dbi/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/qwiic_pir/test.esp32-ard.yaml b/tests/components/qwiic_pir/test.esp32-ard.yaml new file mode 100644 index 0000000000..da2e275cf3 --- /dev/null +++ b/tests/components/qwiic_pir/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_qwiic_pir + scl: 16 + sda: 17 + +binary_sensor: + - platform: qwiic_pir + name: Qwiic PIR Motion Sensor diff --git a/tests/components/qwiic_pir/test.esp32-c3-ard.yaml b/tests/components/qwiic_pir/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ad52ac91c5 --- /dev/null +++ b/tests/components/qwiic_pir/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_qwiic_pir + scl: 5 + sda: 4 + +binary_sensor: + - platform: qwiic_pir + name: Qwiic PIR Motion Sensor diff --git a/tests/components/qwiic_pir/test.esp32-c3-idf.yaml b/tests/components/qwiic_pir/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ad52ac91c5 --- /dev/null +++ b/tests/components/qwiic_pir/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_qwiic_pir + scl: 5 + sda: 4 + +binary_sensor: + - platform: qwiic_pir + name: Qwiic PIR Motion Sensor diff --git a/tests/components/qwiic_pir/test.esp32-idf.yaml b/tests/components/qwiic_pir/test.esp32-idf.yaml new file mode 100644 index 0000000000..da2e275cf3 --- /dev/null +++ b/tests/components/qwiic_pir/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_qwiic_pir + scl: 16 + sda: 17 + +binary_sensor: + - platform: qwiic_pir + name: Qwiic PIR Motion Sensor diff --git a/tests/components/qwiic_pir/test.esp8266-ard.yaml b/tests/components/qwiic_pir/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ad52ac91c5 --- /dev/null +++ b/tests/components/qwiic_pir/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_qwiic_pir + scl: 5 + sda: 4 + +binary_sensor: + - platform: qwiic_pir + name: Qwiic PIR Motion Sensor diff --git a/tests/components/qwiic_pir/test.rp2040-ard.yaml b/tests/components/qwiic_pir/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ad52ac91c5 --- /dev/null +++ b/tests/components/qwiic_pir/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_qwiic_pir + scl: 5 + sda: 4 + +binary_sensor: + - platform: qwiic_pir + name: Qwiic PIR Motion Sensor diff --git a/tests/components/radon_eye_ble/common.yaml b/tests/components/radon_eye_ble/common.yaml new file mode 100644 index 0000000000..85638d5c0e --- /dev/null +++ b/tests/components/radon_eye_ble/common.yaml @@ -0,0 +1,3 @@ +esp32_ble_tracker: + +radon_eye_ble: diff --git a/tests/components/radon_eye_ble/test.esp32-ard.yaml b/tests/components/radon_eye_ble/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/radon_eye_ble/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-c3-ard.yaml b/tests/components/radon_eye_ble/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/radon_eye_ble/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml b/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-idf.yaml b/tests/components/radon_eye_ble/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/radon_eye_ble/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/common.yaml b/tests/components/radon_eye_rd200/common.yaml new file mode 100644 index 0000000000..d06979be6f --- /dev/null +++ b/tests/components/radon_eye_rd200/common.yaml @@ -0,0 +1,14 @@ +esp32_ble_tracker: + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: radon_eye_blec + +sensor: + - platform: radon_eye_rd200 + ble_client_id: radon_eye_blec + radon: + name: RD200 Radon + radon_long_term: + name: RD200 Radon Long Term + update_interval: 10min diff --git a/tests/components/radon_eye_rd200/test.esp32-ard.yaml b/tests/components/radon_eye_rd200/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/radon_eye_rd200/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-c3-ard.yaml b/tests/components/radon_eye_rd200/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/radon_eye_rd200/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml b/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-idf.yaml b/tests/components/radon_eye_rd200/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/radon_eye_rd200/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/rc522_i2c/test.esp32-ard.yaml b/tests/components/rc522_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..69b7d892a4 --- /dev/null +++ b/tests/components/rc522_i2c/test.esp32-ard.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_rc522 + scl: 16 + sda: 17 + +rc522_i2c: + - id: rc522_nfcc + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: RC522 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_i2c/test.esp32-c3-ard.yaml b/tests/components/rc522_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8c8819e257 --- /dev/null +++ b/tests/components/rc522_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_rc522 + scl: 5 + sda: 4 + +rc522_i2c: + - id: rc522_nfcc + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: RC522 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_i2c/test.esp32-c3-idf.yaml b/tests/components/rc522_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8c8819e257 --- /dev/null +++ b/tests/components/rc522_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_rc522 + scl: 5 + sda: 4 + +rc522_i2c: + - id: rc522_nfcc + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: RC522 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_i2c/test.esp32-idf.yaml b/tests/components/rc522_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..69b7d892a4 --- /dev/null +++ b/tests/components/rc522_i2c/test.esp32-idf.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_rc522 + scl: 16 + sda: 17 + +rc522_i2c: + - id: rc522_nfcc + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: RC522 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_i2c/test.esp8266-ard.yaml b/tests/components/rc522_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8c8819e257 --- /dev/null +++ b/tests/components/rc522_i2c/test.esp8266-ard.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_rc522 + scl: 5 + sda: 4 + +rc522_i2c: + - id: rc522_nfcc + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: RC522 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_i2c/test.rp2040-ard.yaml b/tests/components/rc522_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..8c8819e257 --- /dev/null +++ b/tests/components/rc522_i2c/test.rp2040-ard.yaml @@ -0,0 +1,17 @@ +i2c: + - id: i2c_rc522 + scl: 5 + sda: 4 + +rc522_i2c: + - id: rc522_nfcc + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: RC522 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_spi/test.esp32-ard.yaml b/tests/components/rc522_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..5c0b698a08 --- /dev/null +++ b/tests/components/rc522_spi/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_rc522 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +rc522_spi: + id: rc522_nfcc + cs_pin: 12 + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_spi/test.esp32-c3-ard.yaml b/tests/components/rc522_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8bcab84700 --- /dev/null +++ b/tests/components/rc522_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_rc522 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +rc522_spi: + id: rc522_nfcc + cs_pin: 4 + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_spi/test.esp32-c3-idf.yaml b/tests/components/rc522_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..8bcab84700 --- /dev/null +++ b/tests/components/rc522_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_rc522 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +rc522_spi: + id: rc522_nfcc + cs_pin: 4 + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_spi/test.esp32-idf.yaml b/tests/components/rc522_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..5c0b698a08 --- /dev/null +++ b/tests/components/rc522_spi/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_rc522 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +rc522_spi: + id: rc522_nfcc + cs_pin: 12 + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_spi/test.esp8266-ard.yaml b/tests/components/rc522_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3c33311266 --- /dev/null +++ b/tests/components/rc522_spi/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_rc522 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +rc522_spi: + id: rc522_nfcc + cs_pin: 15 + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rc522_spi/test.rp2040-ard.yaml b/tests/components/rc522_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ed2827dbb9 --- /dev/null +++ b/tests/components/rc522_spi/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +spi: + - id: spi_rc522 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +rc522_spi: + id: rc522_nfcc + cs_pin: 6 + +binary_sensor: + - platform: rc522 + rc522_id: rc522_nfcc + name: PN532 NFC Tag + uid: 74-10-37-94 diff --git a/tests/components/rdm6300/test.esp32-ard.yaml b/tests/components/rdm6300/test.esp32-ard.yaml new file mode 100644 index 0000000000..4159248124 --- /dev/null +++ b/tests/components/rdm6300/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_rdm6300 + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +rdm6300: + +binary_sensor: + - platform: rdm6300 + uid: 7616525 + name: RDM6300 NFC Tag diff --git a/tests/components/rdm6300/test.esp32-c3-ard.yaml b/tests/components/rdm6300/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b92fce06e2 --- /dev/null +++ b/tests/components/rdm6300/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_rdm6300 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +rdm6300: + +binary_sensor: + - platform: rdm6300 + uid: 7616525 + name: RDM6300 NFC Tag diff --git a/tests/components/rdm6300/test.esp32-c3-idf.yaml b/tests/components/rdm6300/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b92fce06e2 --- /dev/null +++ b/tests/components/rdm6300/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_rdm6300 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +rdm6300: + +binary_sensor: + - platform: rdm6300 + uid: 7616525 + name: RDM6300 NFC Tag diff --git a/tests/components/rdm6300/test.esp32-idf.yaml b/tests/components/rdm6300/test.esp32-idf.yaml new file mode 100644 index 0000000000..4159248124 --- /dev/null +++ b/tests/components/rdm6300/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_rdm6300 + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +rdm6300: + +binary_sensor: + - platform: rdm6300 + uid: 7616525 + name: RDM6300 NFC Tag diff --git a/tests/components/rdm6300/test.esp8266-ard.yaml b/tests/components/rdm6300/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b92fce06e2 --- /dev/null +++ b/tests/components/rdm6300/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_rdm6300 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +rdm6300: + +binary_sensor: + - platform: rdm6300 + uid: 7616525 + name: RDM6300 NFC Tag diff --git a/tests/components/rdm6300/test.rp2040-ard.yaml b/tests/components/rdm6300/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b92fce06e2 --- /dev/null +++ b/tests/components/rdm6300/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_rdm6300 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +rdm6300: + +binary_sensor: + - platform: rdm6300 + uid: 7616525 + name: RDM6300 NFC Tag diff --git a/tests/components/remote_receiver/esp32-common.yaml b/tests/components/remote_receiver/esp32-common.yaml new file mode 100644 index 0000000000..7e5d2cce32 --- /dev/null +++ b/tests/components/remote_receiver/esp32-common.yaml @@ -0,0 +1,157 @@ +remote_receiver: + id: rcvr + pin: ${pin} + rmt_channel: ${rmt_channel} + dump: all + tolerance: 25% + on_abbwelcome: + then: + - logger.log: + format: "on_abbwelcome: %u" + args: ["x.data()[0]"] + on_aeha: + then: + - logger.log: + format: "on_aeha: %u %u" + args: ["x.address", "x.data.front()"] + on_byronsx: + then: + - logger.log: + format: "on_byronsx: %u %u" + args: ["x.address", "x.command"] + on_canalsat: + then: + - logger.log: + format: "on_canalsat: %u %u" + args: ["x.address", "x.command"] + # on_canalsatld: + # then: + # - logger.log: + # format: "on_canalsatld: %u %u" + # args: ["x.address", "x.command"] + on_coolix: + then: + - logger.log: + format: "on_coolix: %lu %lu" + args: ["long(x.first)", "long(x.second)"] + on_dish: + then: + - logger.log: + format: "on_dish: %u %u" + args: ["x.address", "x.command"] + on_dooya: + then: + - logger.log: + format: "on_dooya: %u %u %u" + args: ["x.channel", "x.button", "x.check"] + on_drayton: + then: + - logger.log: + format: "on_drayton: %u %u %u" + args: ["x.address", "x.channel", "x.command"] + on_jvc: + then: + - logger.log: + format: "on_jvc: %lu" + args: ["long(x.data)"] + on_keeloq: + then: + - logger.log: + format: "on_keeloq: %lu %lu %u" + args: ["long(x.encrypted)", "long(x.address)", "x.command"] + on_haier: + then: + - logger.log: + format: "on_haier: %u" + args: ["x.data.front()"] + on_lg: + then: + - logger.log: + format: "on_lg: %lu %u" + args: ["long(x.data)", "x.nbits"] + on_magiquest: + then: + - logger.log: + format: "on_magiquest: %u %lu" + args: ["x.magnitude", "long(x.wand_id)"] + on_midea: + then: + - logger.log: + format: "on_midea: %u %u" + args: ["x.size()", "x.data()[0]"] + on_nec: + then: + - logger.log: + format: "on_nec: %u %u" + args: ["x.address", "x.command"] + on_nexa: + then: + - logger.log: + format: "on_nexa: %lu %u %u %u %u" + args: ["long(x.device)", "x.group", "x.state", "x.channel", "x.level"] + on_panasonic: + then: + - logger.log: + format: "on_panasonic: %u %lu" + args: ["x.address", "long(x.command)"] + on_pioneer: + then: + - logger.log: + format: "on_pioneer: %u %u" + args: ["x.rc_code_1", "x.rc_code_2"] + on_pronto: + then: + - logger.log: + format: "on_pronto: %s" + args: ["x.data.c_str()"] + on_raw: + then: + - logger.log: + format: "on_raw: %lu" + args: ["long(x.front())"] + on_rc5: + then: + - logger.log: + format: "on_rc5: %u %u" + args: ["x.address", "x.command"] + on_rc6: + then: + - logger.log: + format: "on_rc6: %u %u" + args: ["x.address", "x.command"] + on_rc_switch: + then: + - logger.log: + format: "on_rc_switch: %llu %u" + args: ["x.code", "x.protocol"] + on_samsung: + then: + - logger.log: + format: "on_samsung: %llu %u" + args: ["x.data", "x.nbits"] + on_samsung36: + then: + - logger.log: + format: "on_samsung36: %u %lu" + args: ["x.address", "long(x.command)"] + on_sony: + then: + - logger.log: + format: "on_sony: %lu %u" + args: ["long(x.data)", "x.nbits"] + on_toshiba_ac: + then: + - logger.log: + format: "on_toshiba_ac: %llu %llu" + args: ["x.rc_code_1", "x.rc_code_2"] + on_mirage: + then: + - lambda: |- + ESP_LOGD("mirage", "Mirage data: %s", format_hex(x.data).c_str()); + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_receiver/test.esp32-ard.yaml b/tests/components/remote_receiver/test.esp32-ard.yaml new file mode 100644 index 0000000000..16d276958a --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO2 + rmt_channel: "2" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-c3-ard.yaml b/tests/components/remote_receiver/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..16d276958a --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO2 + rmt_channel: "2" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-c3-idf.yaml b/tests/components/remote_receiver/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..16d276958a --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO2 + rmt_channel: "2" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-idf.yaml b/tests/components/remote_receiver/test.esp32-idf.yaml new file mode 100644 index 0000000000..16d276958a --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO2 + rmt_channel: "2" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-s3-idf.yaml b/tests/components/remote_receiver/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..265ecda771 --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-s3-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO38 + rmt_channel: "5" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp8266-ard.yaml b/tests/components/remote_receiver/test.esp8266-ard.yaml new file mode 100644 index 0000000000..27d36d4a16 --- /dev/null +++ b/tests/components/remote_receiver/test.esp8266-ard.yaml @@ -0,0 +1,155 @@ +remote_receiver: + id: rcvr + pin: GPIO5 + dump: all + on_abbwelcome: + then: + - logger.log: + format: "on_abbwelcome: %u" + args: ["x.data()[0]"] + on_aeha: + then: + - logger.log: + format: "on_aeha: %u %u" + args: ["x.address", "x.data.front()"] + on_byronsx: + then: + - logger.log: + format: "on_byronsx: %u %u" + args: ["x.address", "x.command"] + on_canalsat: + then: + - logger.log: + format: "on_canalsat: %u %u" + args: ["x.address", "x.command"] + # on_canalsatld: + # then: + # - logger.log: + # format: "on_canalsatld: %u %u" + # args: ["x.address", "x.command"] + on_coolix: + then: + - logger.log: + format: "on_coolix: %u %u" + args: ["x.first", "x.second"] + on_dish: + then: + - logger.log: + format: "on_dish: %u %u" + args: ["x.address", "x.command"] + on_dooya: + then: + - logger.log: + format: "on_dooya: %u %u %u" + args: ["x.channel", "x.button", "x.check"] + on_drayton: + then: + - logger.log: + format: "on_drayton: %u %u %u" + args: ["x.address", "x.channel", "x.command"] + on_jvc: + then: + - logger.log: + format: "on_jvc: %u" + args: ["x.data"] + on_keeloq: + then: + - logger.log: + format: "on_keeloq: %u %u %u" + args: ["x.encrypted", "x.address", "x.command"] + on_haier: + then: + - logger.log: + format: "on_haier: %u" + args: ["x.data.front()"] + on_lg: + then: + - logger.log: + format: "on_lg: %u %u" + args: ["x.data", "x.nbits"] + on_magiquest: + then: + - logger.log: + format: "on_magiquest: %u %u" + args: ["x.magnitude", "x.wand_id"] + on_midea: + then: + - logger.log: + format: "on_midea: %u %u" + args: ["x.size()", "x.data()[0]"] + on_nec: + then: + - logger.log: + format: "on_nec: %u %u" + args: ["x.address", "x.command"] + on_nexa: + then: + - logger.log: + format: "on_nexa: %u %u %u %u %u" + args: ["x.device", "x.group", "x.state", "x.channel", "x.level"] + on_panasonic: + then: + - logger.log: + format: "on_panasonic: %u %u" + args: ["x.address", "x.command"] + on_pioneer: + then: + - logger.log: + format: "on_pioneer: %u %u" + args: ["x.rc_code_1", "x.rc_code_2"] + on_pronto: + then: + - logger.log: + format: "on_pronto: %s" + args: ["x.data.c_str()"] + on_raw: + then: + - logger.log: + format: "on_raw: %u" + args: ["x.front()"] + on_rc5: + then: + - logger.log: + format: "on_rc5: %u %u" + args: ["x.address", "x.command"] + on_rc6: + then: + - logger.log: + format: "on_rc6: %u %u" + args: ["x.address", "x.command"] + on_rc_switch: + then: + - logger.log: + format: "on_rc_switch: %llu %u" + args: ["x.code", "x.protocol"] + on_samsung: + then: + - logger.log: + format: "on_samsung: %llu %u" + args: ["x.data", "x.nbits"] + on_samsung36: + then: + - logger.log: + format: "on_samsung36: %u %u" + args: ["x.address", "x.command"] + on_sony: + then: + - logger.log: + format: "on_sony: %u %u" + args: ["x.data", "x.nbits"] + on_toshiba_ac: + then: + - logger.log: + format: "on_toshiba_ac: %llu %llu" + args: ["x.rc_code_1", "x.rc_code_2"] + on_mirage: + then: + - lambda: |- + ESP_LOGD("mirage", "Mirage data: %s", format_hex(x.data).c_str()); + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml new file mode 100644 index 0000000000..c6a2453b20 --- /dev/null +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -0,0 +1,192 @@ +button: + - platform: template + name: JVC Off + id: living_room_lights_on + on_press: + remote_transmitter.transmit_jvc: + data: 0x10EF + - platform: template + name: MagiQuest + on_press: + remote_transmitter.transmit_magiquest: + wand_id: 0x01234567 + - platform: template + name: NEC + id: living_room_lights_off + on_press: + remote_transmitter.transmit_nec: + address: 0x4242 + command: 0x8484 + - platform: template + name: LG + on_press: + remote_transmitter.transmit_lg: + data: 4294967295 + nbits: 28 + - platform: template + name: Samsung + on_press: + remote_transmitter.transmit_samsung: + data: 0xABCDEF + - platform: template + name: Samsung36 + on_press: + remote_transmitter.transmit_samsung36: + address: 0x0400 + command: 0x000E00FF + - platform: template + name: ToshibaAC + on_press: + - remote_transmitter.transmit_toshiba_ac: + rc_code_1: 0xB24DBF4050AF + rc_code_2: 0xD5660001003C + - platform: template + name: Sony + on_press: + remote_transmitter.transmit_sony: + data: 0xABCDEF + nbits: 12 + - platform: template + name: Panasonic + on_press: + remote_transmitter.transmit_panasonic: + address: 0x4004 + command: 0x1000BCD + - platform: template + name: Pioneer + on_press: + - remote_transmitter.transmit_pioneer: + rc_code_1: 0xA556 + rc_code_2: 0xA506 + repeat: + times: 2 + - platform: template + name: RC Switch Raw + on_press: + remote_transmitter.transmit_rc_switch_raw: + code: "00101001100111110101xxxx" + protocol: 1 + - platform: template + name: RC Switch Type A + on_press: + remote_transmitter.transmit_rc_switch_type_a: + group: "11001" + device: "01000" + state: true + protocol: + pulse_length: 175 + sync: [1, 31] + zero: [1, 3] + one: [3, 1] + inverted: false + - platform: template + name: RC Switch Type B + on_press: + remote_transmitter.transmit_rc_switch_type_b: + address: 4 + channel: 2 + state: true + - platform: template + name: RC Switch Type C + on_press: + remote_transmitter.transmit_rc_switch_type_c: + family: "a" + group: 1 + device: 2 + state: true + - platform: template + name: RC Switch Type D + on_press: + remote_transmitter.transmit_rc_switch_type_d: + group: "a" + device: 2 + state: true + - platform: template + name: RC5 + on_press: + remote_transmitter.transmit_rc5: + address: 0x00 + command: 0x0B + - platform: template + name: RC5 + on_press: + remote_transmitter.transmit_raw: + code: [1000, -1000] + - platform: template + name: AEHA + id: eaha_hitachi_climate_power_on + on_press: + remote_transmitter.transmit_aeha: + address: 0x8008 + carrier_frequency: 36700Hz + data: + [ + 0x00, + 0x02, + 0xFD, + 0xFF, + 0x00, + 0x33, + 0xCC, + 0x49, + 0xB6, + 0xC8, + 0x37, + 0x16, + 0xE9, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0xCA, + 0x35, + 0x8F, + 0x70, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + ] + - platform: template + name: Haier + on_press: + remote_transmitter.transmit_haier: + code: + [ + 0xA6, + 0xDA, + 0x00, + 0x00, + 0x40, + 0x40, + 0x00, + 0x80, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + ] + - platform: template + name: Mirage + on_press: + remote_transmitter.transmit_mirage: + code: [0x56, 0x77, 0x00, 0x00, 0x22, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + - platform: template + name: Dooya + on_press: + remote_transmitter.transmit_dooya: + id: 0x123456 + channel: 1 + button: 1 + check: 1 diff --git a/tests/components/remote_transmitter/esp32-common.yaml b/tests/components/remote_transmitter/esp32-common.yaml new file mode 100644 index 0000000000..3f3cd3f8c7 --- /dev/null +++ b/tests/components/remote_transmitter/esp32-common.yaml @@ -0,0 +1,8 @@ +remote_transmitter: + id: rcvr + pin: ${pin} + rmt_channel: ${rmt_channel} + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/components/remote_transmitter/test.esp32-ard.yaml b/tests/components/remote_transmitter/test.esp32-ard.yaml new file mode 100644 index 0000000000..16d276958a --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO2 + rmt_channel: "2" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..3e2dc88e5a --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO2 + rmt_channel: "1" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-c3-idf.yaml b/tests/components/remote_transmitter/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3e2dc88e5a --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO2 + rmt_channel: "1" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-idf.yaml b/tests/components/remote_transmitter/test.esp32-idf.yaml new file mode 100644 index 0000000000..16d276958a --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO2 + rmt_channel: "2" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..31851dc54c --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + pin: GPIO38 + rmt_channel: "3" + +packages: + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp8266-ard.yaml b/tests/components/remote_transmitter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..de494485f4 --- /dev/null +++ b/tests/components/remote_transmitter/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: trns + pin: GPIO5 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/components/resistance/test.esp32-ard.yaml b/tests/components/resistance/test.esp32-ard.yaml new file mode 100644 index 0000000000..b1ffc64972 --- /dev/null +++ b/tests/components/resistance/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +sensor: + - platform: adc + id: my_sensor + pin: 32 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist diff --git a/tests/components/resistance/test.esp32-c3-ard.yaml b/tests/components/resistance/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..84e23d5115 --- /dev/null +++ b/tests/components/resistance/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +sensor: + - platform: adc + id: my_sensor + pin: 4 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist diff --git a/tests/components/resistance/test.esp32-idf.yaml b/tests/components/resistance/test.esp32-idf.yaml new file mode 100644 index 0000000000..b1ffc64972 --- /dev/null +++ b/tests/components/resistance/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +sensor: + - platform: adc + id: my_sensor + pin: 32 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist diff --git a/tests/components/resistance/test.esp32-s2-ard.yaml b/tests/components/resistance/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..4ebd6b5c49 --- /dev/null +++ b/tests/components/resistance/test.esp32-s2-ard.yaml @@ -0,0 +1,12 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist diff --git a/tests/components/resistance/test.esp32-s3-ard.yaml b/tests/components/resistance/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..4ebd6b5c49 --- /dev/null +++ b/tests/components/resistance/test.esp32-s3-ard.yaml @@ -0,0 +1,12 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist diff --git a/tests/components/resistance/test.esp8266-ard.yaml b/tests/components/resistance/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f723f7c7c7 --- /dev/null +++ b/tests/components/resistance/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: adc + id: my_sensor + pin: VCC + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist diff --git a/tests/components/resistance/test.rp2040-ard.yaml b/tests/components/resistance/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5cc643926a --- /dev/null +++ b/tests/components/resistance/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +sensor: + - platform: adc + id: my_sensor + name: VSYS + pin: VCC + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist diff --git a/tests/components/restart/common.yaml b/tests/components/restart/common.yaml new file mode 100644 index 0000000000..f0d25809ac --- /dev/null +++ b/tests/components/restart/common.yaml @@ -0,0 +1,7 @@ +button: + - platform: restart + name: Restart Button + +switch: + - platform: restart + name: Restart Switch diff --git a/tests/components/restart/test.esp32-ard.yaml b/tests/components/restart/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/restart/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/restart/test.esp32-c3-ard.yaml b/tests/components/restart/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/restart/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/restart/test.esp32-c3-idf.yaml b/tests/components/restart/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/restart/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/restart/test.esp32-idf.yaml b/tests/components/restart/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/restart/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/restart/test.esp8266-ard.yaml b/tests/components/restart/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/restart/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/restart/test.rp2040-ard.yaml b/tests/components/restart/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/restart/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/rf_bridge/test.esp32-ard.yaml b/tests/components/rf_bridge/test.esp32-ard.yaml new file mode 100644 index 0000000000..9ade7f0ac0 --- /dev/null +++ b/tests/components/rf_bridge/test.esp32-ard.yaml @@ -0,0 +1,35 @@ +uart: + - id: uart_rf_bridge + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +rf_bridge: + on_code_received: + - lambda: |- + uint32_t test; + test = data.sync; + test = data.low; + test = data.high; + test = data.code; + - rf_bridge.send_code: + sync: 0x1234 + low: 0x1234 + high: 0x1234 + code: 0x123456 + - rf_bridge.learn + on_advanced_code_received: + - lambda: |- + uint32_t test; + std::string test_code; + test = data.length; + test = data.protocol; + test_code = data.code; + - rf_bridge.start_advanced_sniffing: + - rf_bridge.stop_advanced_sniffing: + - rf_bridge.send_advanced_code: + length: 0x04 + protocol: 0x01 + code: "ABC123" + - rf_bridge.send_raw: + raw: "AAA5070008001000ABC12355" diff --git a/tests/components/rf_bridge/test.esp32-c3-ard.yaml b/tests/components/rf_bridge/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..95a7aa861a --- /dev/null +++ b/tests/components/rf_bridge/test.esp32-c3-ard.yaml @@ -0,0 +1,35 @@ +uart: + - id: uart_rf_bridge + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +rf_bridge: + on_code_received: + - lambda: |- + uint32_t test; + test = data.sync; + test = data.low; + test = data.high; + test = data.code; + - rf_bridge.send_code: + sync: 0x1234 + low: 0x1234 + high: 0x1234 + code: 0x123456 + - rf_bridge.learn + on_advanced_code_received: + - lambda: |- + uint32_t test; + std::string test_code; + test = data.length; + test = data.protocol; + test_code = data.code; + - rf_bridge.start_advanced_sniffing: + - rf_bridge.stop_advanced_sniffing: + - rf_bridge.send_advanced_code: + length: 0x04 + protocol: 0x01 + code: "ABC123" + - rf_bridge.send_raw: + raw: "AAA5070008001000ABC12355" diff --git a/tests/components/rf_bridge/test.esp32-c3-idf.yaml b/tests/components/rf_bridge/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..95a7aa861a --- /dev/null +++ b/tests/components/rf_bridge/test.esp32-c3-idf.yaml @@ -0,0 +1,35 @@ +uart: + - id: uart_rf_bridge + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +rf_bridge: + on_code_received: + - lambda: |- + uint32_t test; + test = data.sync; + test = data.low; + test = data.high; + test = data.code; + - rf_bridge.send_code: + sync: 0x1234 + low: 0x1234 + high: 0x1234 + code: 0x123456 + - rf_bridge.learn + on_advanced_code_received: + - lambda: |- + uint32_t test; + std::string test_code; + test = data.length; + test = data.protocol; + test_code = data.code; + - rf_bridge.start_advanced_sniffing: + - rf_bridge.stop_advanced_sniffing: + - rf_bridge.send_advanced_code: + length: 0x04 + protocol: 0x01 + code: "ABC123" + - rf_bridge.send_raw: + raw: "AAA5070008001000ABC12355" diff --git a/tests/components/rf_bridge/test.esp32-idf.yaml b/tests/components/rf_bridge/test.esp32-idf.yaml new file mode 100644 index 0000000000..9ade7f0ac0 --- /dev/null +++ b/tests/components/rf_bridge/test.esp32-idf.yaml @@ -0,0 +1,35 @@ +uart: + - id: uart_rf_bridge + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +rf_bridge: + on_code_received: + - lambda: |- + uint32_t test; + test = data.sync; + test = data.low; + test = data.high; + test = data.code; + - rf_bridge.send_code: + sync: 0x1234 + low: 0x1234 + high: 0x1234 + code: 0x123456 + - rf_bridge.learn + on_advanced_code_received: + - lambda: |- + uint32_t test; + std::string test_code; + test = data.length; + test = data.protocol; + test_code = data.code; + - rf_bridge.start_advanced_sniffing: + - rf_bridge.stop_advanced_sniffing: + - rf_bridge.send_advanced_code: + length: 0x04 + protocol: 0x01 + code: "ABC123" + - rf_bridge.send_raw: + raw: "AAA5070008001000ABC12355" diff --git a/tests/components/rf_bridge/test.esp8266-ard.yaml b/tests/components/rf_bridge/test.esp8266-ard.yaml new file mode 100644 index 0000000000..95a7aa861a --- /dev/null +++ b/tests/components/rf_bridge/test.esp8266-ard.yaml @@ -0,0 +1,35 @@ +uart: + - id: uart_rf_bridge + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +rf_bridge: + on_code_received: + - lambda: |- + uint32_t test; + test = data.sync; + test = data.low; + test = data.high; + test = data.code; + - rf_bridge.send_code: + sync: 0x1234 + low: 0x1234 + high: 0x1234 + code: 0x123456 + - rf_bridge.learn + on_advanced_code_received: + - lambda: |- + uint32_t test; + std::string test_code; + test = data.length; + test = data.protocol; + test_code = data.code; + - rf_bridge.start_advanced_sniffing: + - rf_bridge.stop_advanced_sniffing: + - rf_bridge.send_advanced_code: + length: 0x04 + protocol: 0x01 + code: "ABC123" + - rf_bridge.send_raw: + raw: "AAA5070008001000ABC12355" diff --git a/tests/components/rf_bridge/test.rp2040-ard.yaml b/tests/components/rf_bridge/test.rp2040-ard.yaml new file mode 100644 index 0000000000..95a7aa861a --- /dev/null +++ b/tests/components/rf_bridge/test.rp2040-ard.yaml @@ -0,0 +1,35 @@ +uart: + - id: uart_rf_bridge + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +rf_bridge: + on_code_received: + - lambda: |- + uint32_t test; + test = data.sync; + test = data.low; + test = data.high; + test = data.code; + - rf_bridge.send_code: + sync: 0x1234 + low: 0x1234 + high: 0x1234 + code: 0x123456 + - rf_bridge.learn + on_advanced_code_received: + - lambda: |- + uint32_t test; + std::string test_code; + test = data.length; + test = data.protocol; + test_code = data.code; + - rf_bridge.start_advanced_sniffing: + - rf_bridge.stop_advanced_sniffing: + - rf_bridge.send_advanced_code: + length: 0x04 + protocol: 0x01 + code: "ABC123" + - rf_bridge.send_raw: + raw: "AAA5070008001000ABC12355" diff --git a/tests/components/rgb/test.esp32-ard.yaml b/tests/components/rgb/test.esp32-ard.yaml new file mode 100644 index 0000000000..2173e718be --- /dev/null +++ b/tests/components/rgb/test.esp32-ard.yaml @@ -0,0 +1,18 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + - platform: ledc + id: light_output_3 + pin: 14 + +light: + - platform: rgb + name: RGB Light + id: rgb_light + red: light_output_1 + green: light_output_2 + blue: light_output_3 diff --git a/tests/components/rgb/test.esp32-c3-ard.yaml b/tests/components/rgb/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..30ff1527b4 --- /dev/null +++ b/tests/components/rgb/test.esp32-c3-ard.yaml @@ -0,0 +1,18 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + - platform: ledc + id: light_output_3 + pin: 3 + +light: + - platform: rgb + name: RGB Light + id: rgb_light + red: light_output_1 + green: light_output_2 + blue: light_output_3 diff --git a/tests/components/rgb/test.esp32-c3-idf.yaml b/tests/components/rgb/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..30ff1527b4 --- /dev/null +++ b/tests/components/rgb/test.esp32-c3-idf.yaml @@ -0,0 +1,18 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + - platform: ledc + id: light_output_3 + pin: 3 + +light: + - platform: rgb + name: RGB Light + id: rgb_light + red: light_output_1 + green: light_output_2 + blue: light_output_3 diff --git a/tests/components/rgb/test.esp32-idf.yaml b/tests/components/rgb/test.esp32-idf.yaml new file mode 100644 index 0000000000..2173e718be --- /dev/null +++ b/tests/components/rgb/test.esp32-idf.yaml @@ -0,0 +1,18 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + - platform: ledc + id: light_output_3 + pin: 14 + +light: + - platform: rgb + name: RGB Light + id: rgb_light + red: light_output_1 + green: light_output_2 + blue: light_output_3 diff --git a/tests/components/rgb/test.esp8266-ard.yaml b/tests/components/rgb/test.esp8266-ard.yaml new file mode 100644 index 0000000000..60c5a7e04f --- /dev/null +++ b/tests/components/rgb/test.esp8266-ard.yaml @@ -0,0 +1,18 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 + - platform: esp8266_pwm + id: light_output_2 + pin: 13 + - platform: esp8266_pwm + id: light_output_3 + pin: 14 + +light: + - platform: rgb + name: RGB Light + id: rgb_light + red: light_output_1 + green: light_output_2 + blue: light_output_3 diff --git a/tests/components/rgb/test.rp2040-ard.yaml b/tests/components/rgb/test.rp2040-ard.yaml new file mode 100644 index 0000000000..fd6519707b --- /dev/null +++ b/tests/components/rgb/test.rp2040-ard.yaml @@ -0,0 +1,18 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 + - platform: rp2040_pwm + id: light_output_2 + pin: 13 + - platform: rp2040_pwm + id: light_output_3 + pin: 14 + +light: + - platform: rgb + name: RGB Light + id: rgb_light + red: light_output_1 + green: light_output_2 + blue: light_output_3 diff --git a/tests/components/rgbct/test.esp32-ard.yaml b/tests/components/rgbct/test.esp32-ard.yaml new file mode 100644 index 0000000000..d9758c9ec7 --- /dev/null +++ b/tests/components/rgbct/test.esp32-ard.yaml @@ -0,0 +1,28 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + - platform: ledc + id: light_output_3 + pin: 14 + - platform: ledc + id: light_output_4 + pin: 15 + - platform: ledc + id: light_output_5 + pin: 16 + +light: + - platform: rgbct + name: RGBCT Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + color_temperature: light_output_4 + white_brightness: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbct/test.esp32-c3-ard.yaml b/tests/components/rgbct/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..426c4b8937 --- /dev/null +++ b/tests/components/rgbct/test.esp32-c3-ard.yaml @@ -0,0 +1,28 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + - platform: ledc + id: light_output_3 + pin: 3 + - platform: ledc + id: light_output_4 + pin: 4 + - platform: ledc + id: light_output_5 + pin: 5 + +light: + - platform: rgbct + name: RGBCT Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + color_temperature: light_output_4 + white_brightness: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbct/test.esp32-c3-idf.yaml b/tests/components/rgbct/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..426c4b8937 --- /dev/null +++ b/tests/components/rgbct/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + - platform: ledc + id: light_output_3 + pin: 3 + - platform: ledc + id: light_output_4 + pin: 4 + - platform: ledc + id: light_output_5 + pin: 5 + +light: + - platform: rgbct + name: RGBCT Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + color_temperature: light_output_4 + white_brightness: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbct/test.esp32-idf.yaml b/tests/components/rgbct/test.esp32-idf.yaml new file mode 100644 index 0000000000..d9758c9ec7 --- /dev/null +++ b/tests/components/rgbct/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + - platform: ledc + id: light_output_3 + pin: 14 + - platform: ledc + id: light_output_4 + pin: 15 + - platform: ledc + id: light_output_5 + pin: 16 + +light: + - platform: rgbct + name: RGBCT Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + color_temperature: light_output_4 + white_brightness: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbct/test.esp8266-ard.yaml b/tests/components/rgbct/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b7008c9ae3 --- /dev/null +++ b/tests/components/rgbct/test.esp8266-ard.yaml @@ -0,0 +1,28 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 + - platform: esp8266_pwm + id: light_output_2 + pin: 13 + - platform: esp8266_pwm + id: light_output_3 + pin: 14 + - platform: esp8266_pwm + id: light_output_4 + pin: 15 + - platform: esp8266_pwm + id: light_output_5 + pin: 16 + +light: + - platform: rgbct + name: RGBCT Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + color_temperature: light_output_4 + white_brightness: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbct/test.rp2040-ard.yaml b/tests/components/rgbct/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e7e959b2a4 --- /dev/null +++ b/tests/components/rgbct/test.rp2040-ard.yaml @@ -0,0 +1,28 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 + - platform: rp2040_pwm + id: light_output_2 + pin: 13 + - platform: rp2040_pwm + id: light_output_3 + pin: 14 + - platform: rp2040_pwm + id: light_output_4 + pin: 15 + - platform: rp2040_pwm + id: light_output_5 + pin: 16 + +light: + - platform: rgbct + name: RGBCT Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + color_temperature: light_output_4 + white_brightness: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbw/test.esp32-ard.yaml b/tests/components/rgbw/test.esp32-ard.yaml new file mode 100644 index 0000000000..6e9e92a03c --- /dev/null +++ b/tests/components/rgbw/test.esp32-ard.yaml @@ -0,0 +1,22 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + - platform: ledc + id: light_output_3 + pin: 14 + - platform: ledc + id: light_output_4 + pin: 15 + +light: + - platform: rgbw + name: RGBW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + white: light_output_4 + color_interlock: true diff --git a/tests/components/rgbw/test.esp32-c3-ard.yaml b/tests/components/rgbw/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c5d4fceb9d --- /dev/null +++ b/tests/components/rgbw/test.esp32-c3-ard.yaml @@ -0,0 +1,22 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + - platform: ledc + id: light_output_3 + pin: 3 + - platform: ledc + id: light_output_4 + pin: 4 + +light: + - platform: rgbw + name: RGBW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + white: light_output_4 + color_interlock: true diff --git a/tests/components/rgbw/test.esp32-c3-idf.yaml b/tests/components/rgbw/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c5d4fceb9d --- /dev/null +++ b/tests/components/rgbw/test.esp32-c3-idf.yaml @@ -0,0 +1,22 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + - platform: ledc + id: light_output_3 + pin: 3 + - platform: ledc + id: light_output_4 + pin: 4 + +light: + - platform: rgbw + name: RGBW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + white: light_output_4 + color_interlock: true diff --git a/tests/components/rgbw/test.esp32-idf.yaml b/tests/components/rgbw/test.esp32-idf.yaml new file mode 100644 index 0000000000..6e9e92a03c --- /dev/null +++ b/tests/components/rgbw/test.esp32-idf.yaml @@ -0,0 +1,22 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + - platform: ledc + id: light_output_3 + pin: 14 + - platform: ledc + id: light_output_4 + pin: 15 + +light: + - platform: rgbw + name: RGBW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + white: light_output_4 + color_interlock: true diff --git a/tests/components/rgbw/test.esp8266-ard.yaml b/tests/components/rgbw/test.esp8266-ard.yaml new file mode 100644 index 0000000000..54098613e4 --- /dev/null +++ b/tests/components/rgbw/test.esp8266-ard.yaml @@ -0,0 +1,22 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 + - platform: esp8266_pwm + id: light_output_2 + pin: 13 + - platform: esp8266_pwm + id: light_output_3 + pin: 14 + - platform: esp8266_pwm + id: light_output_4 + pin: 15 + +light: + - platform: rgbw + name: RGBW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + white: light_output_4 + color_interlock: true diff --git a/tests/components/rgbw/test.rp2040-ard.yaml b/tests/components/rgbw/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6a4437b898 --- /dev/null +++ b/tests/components/rgbw/test.rp2040-ard.yaml @@ -0,0 +1,22 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 + - platform: rp2040_pwm + id: light_output_2 + pin: 13 + - platform: rp2040_pwm + id: light_output_3 + pin: 14 + - platform: rp2040_pwm + id: light_output_4 + pin: 15 + +light: + - platform: rgbw + name: RGBW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + white: light_output_4 + color_interlock: true diff --git a/tests/components/rgbww/test.esp32-ard.yaml b/tests/components/rgbww/test.esp32-ard.yaml new file mode 100644 index 0000000000..c24b6b7746 --- /dev/null +++ b/tests/components/rgbww/test.esp32-ard.yaml @@ -0,0 +1,28 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + - platform: ledc + id: light_output_3 + pin: 14 + - platform: ledc + id: light_output_4 + pin: 15 + - platform: ledc + id: light_output_5 + pin: 16 + +light: + - platform: rgbww + name: RGBWW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + cold_white: light_output_4 + warm_white: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbww/test.esp32-c3-ard.yaml b/tests/components/rgbww/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..49e9c7f331 --- /dev/null +++ b/tests/components/rgbww/test.esp32-c3-ard.yaml @@ -0,0 +1,28 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + - platform: ledc + id: light_output_3 + pin: 3 + - platform: ledc + id: light_output_4 + pin: 4 + - platform: ledc + id: light_output_5 + pin: 5 + +light: + - platform: rgbww + name: RGBWW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + cold_white: light_output_4 + warm_white: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbww/test.esp32-c3-idf.yaml b/tests/components/rgbww/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..49e9c7f331 --- /dev/null +++ b/tests/components/rgbww/test.esp32-c3-idf.yaml @@ -0,0 +1,28 @@ +output: + - platform: ledc + id: light_output_1 + pin: 1 + - platform: ledc + id: light_output_2 + pin: 2 + - platform: ledc + id: light_output_3 + pin: 3 + - platform: ledc + id: light_output_4 + pin: 4 + - platform: ledc + id: light_output_5 + pin: 5 + +light: + - platform: rgbww + name: RGBWW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + cold_white: light_output_4 + warm_white: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbww/test.esp32-idf.yaml b/tests/components/rgbww/test.esp32-idf.yaml new file mode 100644 index 0000000000..c24b6b7746 --- /dev/null +++ b/tests/components/rgbww/test.esp32-idf.yaml @@ -0,0 +1,28 @@ +output: + - platform: ledc + id: light_output_1 + pin: 12 + - platform: ledc + id: light_output_2 + pin: 13 + - platform: ledc + id: light_output_3 + pin: 14 + - platform: ledc + id: light_output_4 + pin: 15 + - platform: ledc + id: light_output_5 + pin: 16 + +light: + - platform: rgbww + name: RGBWW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + cold_white: light_output_4 + warm_white: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbww/test.esp8266-ard.yaml b/tests/components/rgbww/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4ea26e6526 --- /dev/null +++ b/tests/components/rgbww/test.esp8266-ard.yaml @@ -0,0 +1,28 @@ +output: + - platform: esp8266_pwm + id: light_output_1 + pin: 12 + - platform: esp8266_pwm + id: light_output_2 + pin: 13 + - platform: esp8266_pwm + id: light_output_3 + pin: 14 + - platform: esp8266_pwm + id: light_output_4 + pin: 15 + - platform: esp8266_pwm + id: light_output_5 + pin: 16 + +light: + - platform: rgbww + name: RGBWW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + cold_white: light_output_4 + warm_white: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rgbww/test.rp2040-ard.yaml b/tests/components/rgbww/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0986f06e78 --- /dev/null +++ b/tests/components/rgbww/test.rp2040-ard.yaml @@ -0,0 +1,28 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 12 + - platform: rp2040_pwm + id: light_output_2 + pin: 13 + - platform: rp2040_pwm + id: light_output_3 + pin: 14 + - platform: rp2040_pwm + id: light_output_4 + pin: 15 + - platform: rp2040_pwm + id: light_output_5 + pin: 16 + +light: + - platform: rgbww + name: RGBWW Light + red: light_output_1 + green: light_output_2 + blue: light_output_3 + cold_white: light_output_4 + warm_white: light_output_5 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true diff --git a/tests/components/rotary_encoder/test.esp32-ard.yaml b/tests/components/rotary_encoder/test.esp32-ard.yaml new file mode 100644 index 0000000000..da3843f82d --- /dev/null +++ b/tests/components/rotary_encoder/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +sensor: + - platform: rotary_encoder + name: Rotary Encoder + id: rotary_encoder1 + pin_a: 13 + pin_b: 14 + pin_reset: 15 + filters: + - or: + - debounce: 0.1s + - delta: 10 + resolution: 4 + min_value: -10 + max_value: 30 + on_value: + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: 10 + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: !lambda "return -1;" + on_clockwise: + - logger.log: Clockwise + on_anticlockwise: + - logger.log: Anticlockwise diff --git a/tests/components/rotary_encoder/test.esp32-c3-ard.yaml b/tests/components/rotary_encoder/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..59f8b56abf --- /dev/null +++ b/tests/components/rotary_encoder/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +sensor: + - platform: rotary_encoder + name: Rotary Encoder + id: rotary_encoder1 + pin_a: 3 + pin_b: 4 + pin_reset: 5 + filters: + - or: + - debounce: 0.1s + - delta: 10 + resolution: 4 + min_value: -10 + max_value: 30 + on_value: + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: 10 + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: !lambda "return -1;" + on_clockwise: + - logger.log: Clockwise + on_anticlockwise: + - logger.log: Anticlockwise diff --git a/tests/components/rotary_encoder/test.esp32-c3-idf.yaml b/tests/components/rotary_encoder/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..59f8b56abf --- /dev/null +++ b/tests/components/rotary_encoder/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +sensor: + - platform: rotary_encoder + name: Rotary Encoder + id: rotary_encoder1 + pin_a: 3 + pin_b: 4 + pin_reset: 5 + filters: + - or: + - debounce: 0.1s + - delta: 10 + resolution: 4 + min_value: -10 + max_value: 30 + on_value: + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: 10 + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: !lambda "return -1;" + on_clockwise: + - logger.log: Clockwise + on_anticlockwise: + - logger.log: Anticlockwise diff --git a/tests/components/rotary_encoder/test.esp32-idf.yaml b/tests/components/rotary_encoder/test.esp32-idf.yaml new file mode 100644 index 0000000000..da3843f82d --- /dev/null +++ b/tests/components/rotary_encoder/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +sensor: + - platform: rotary_encoder + name: Rotary Encoder + id: rotary_encoder1 + pin_a: 13 + pin_b: 14 + pin_reset: 15 + filters: + - or: + - debounce: 0.1s + - delta: 10 + resolution: 4 + min_value: -10 + max_value: 30 + on_value: + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: 10 + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: !lambda "return -1;" + on_clockwise: + - logger.log: Clockwise + on_anticlockwise: + - logger.log: Anticlockwise diff --git a/tests/components/rotary_encoder/test.esp8266-ard.yaml b/tests/components/rotary_encoder/test.esp8266-ard.yaml new file mode 100644 index 0000000000..da3843f82d --- /dev/null +++ b/tests/components/rotary_encoder/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +sensor: + - platform: rotary_encoder + name: Rotary Encoder + id: rotary_encoder1 + pin_a: 13 + pin_b: 14 + pin_reset: 15 + filters: + - or: + - debounce: 0.1s + - delta: 10 + resolution: 4 + min_value: -10 + max_value: 30 + on_value: + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: 10 + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: !lambda "return -1;" + on_clockwise: + - logger.log: Clockwise + on_anticlockwise: + - logger.log: Anticlockwise diff --git a/tests/components/rotary_encoder/test.rp2040-ard.yaml b/tests/components/rotary_encoder/test.rp2040-ard.yaml new file mode 100644 index 0000000000..da3843f82d --- /dev/null +++ b/tests/components/rotary_encoder/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +sensor: + - platform: rotary_encoder + name: Rotary Encoder + id: rotary_encoder1 + pin_a: 13 + pin_b: 14 + pin_reset: 15 + filters: + - or: + - debounce: 0.1s + - delta: 10 + resolution: 4 + min_value: -10 + max_value: 30 + on_value: + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: 10 + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: !lambda "return -1;" + on_clockwise: + - logger.log: Clockwise + on_anticlockwise: + - logger.log: Anticlockwise diff --git a/tests/components/rp2040_pio_led_strip/common.yaml b/tests/components/rp2040_pio_led_strip/common.yaml new file mode 100644 index 0000000000..b9b1436cdb --- /dev/null +++ b/tests/components/rp2040_pio_led_strip/common.yaml @@ -0,0 +1,18 @@ +light: + - platform: rp2040_pio_led_strip + id: led_strip + pin: 4 + num_leds: 60 + pio: 0 + rgb_order: GRB + chipset: WS2812 + - platform: rp2040_pio_led_strip + id: led_strip_custom_timings + pin: 5 + num_leds: 60 + pio: 1 + rgb_order: GRB + bit0_high: .1us + bit0_low: 1.2us + bit1_high: .69us + bit1_low: .4us diff --git a/tests/components/rp2040_pio_led_strip/test.rp2040-ard.yaml b/tests/components/rp2040_pio_led_strip/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/rp2040_pio_led_strip/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/rp2040_pwm/common.yaml b/tests/components/rp2040_pwm/common.yaml new file mode 100644 index 0000000000..45c039106f --- /dev/null +++ b/tests/components/rp2040_pwm/common.yaml @@ -0,0 +1,7 @@ +output: + - platform: rp2040_pwm + id: light_output_1 + pin: 2 + - platform: rp2040_pwm + id: light_output_2 + pin: 3 diff --git a/tests/components/rp2040_pwm/test.rp2040-ard.yaml b/tests/components/rp2040_pwm/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/rp2040_pwm/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/rpi_dpi_rgb/common.yaml b/tests/components/rpi_dpi_rgb/common.yaml new file mode 100644 index 0000000000..9ce2d9b9fd --- /dev/null +++ b/tests/components/rpi_dpi_rgb/common.yaml @@ -0,0 +1,40 @@ +psram: + mode: octal + speed: 80MHz +display: + - platform: rpi_dpi_rgb + update_interval: never + auto_clear_enabled: false + id: rpi_display + color_order: RGB + rotation: 90 + dimensions: + width: 800 + height: 480 + de_pin: + number: 40 + hsync_pin: 39 + vsync_pin: 41 + pclk_pin: 42 + data_pins: + red: + - number: 45 # r1 + ignore_strapping_warning: true + - 48 # r2 + - 47 # r3 + - 21 # r4 + - number: 14 # r5 + ignore_strapping_warning: false + green: + - 5 # g0 + - 6 # g1 + - 7 # g2 + - 15 # g3 + - 16 # g4 + - 4 # g5 + blue: + - 8 # b1 + - 3 # b2 + - 46 # b3 + - 9 # b4 + - 1 # b5 diff --git a/tests/components/rpi_dpi_rgb/test.esp32-s3-idf.yaml b/tests/components/rpi_dpi_rgb/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/rpi_dpi_rgb/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/rtttl/test.esp32-ard.yaml b/tests/components/rtttl/test.esp32-ard.yaml new file mode 100644 index 0000000000..367a670741 --- /dev/null +++ b/tests/components/rtttl/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +esphome: + on_boot: + then: + - rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e' + - rtttl.stop + +output: + - platform: ledc + id: rtttl_output + pin: 13 + frequency: 1500Hz + channel: 14 + max_power: 0.5 + +rtttl: + output: rtttl_output diff --git a/tests/components/rtttl/test.esp32-c3-ard.yaml b/tests/components/rtttl/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c525f417de --- /dev/null +++ b/tests/components/rtttl/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +esphome: + on_boot: + then: + - rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e' + - rtttl.stop + +output: + - platform: ledc + id: rtttl_output + pin: 1 + frequency: 1500Hz + channel: 14 + max_power: 0.5 + +rtttl: + output: rtttl_output diff --git a/tests/components/rtttl/test.esp32-c3-idf.yaml b/tests/components/rtttl/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c525f417de --- /dev/null +++ b/tests/components/rtttl/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +esphome: + on_boot: + then: + - rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e' + - rtttl.stop + +output: + - platform: ledc + id: rtttl_output + pin: 1 + frequency: 1500Hz + channel: 14 + max_power: 0.5 + +rtttl: + output: rtttl_output diff --git a/tests/components/rtttl/test.esp32-idf.yaml b/tests/components/rtttl/test.esp32-idf.yaml new file mode 100644 index 0000000000..367a670741 --- /dev/null +++ b/tests/components/rtttl/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +esphome: + on_boot: + then: + - rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e' + - rtttl.stop + +output: + - platform: ledc + id: rtttl_output + pin: 13 + frequency: 1500Hz + channel: 14 + max_power: 0.5 + +rtttl: + output: rtttl_output diff --git a/tests/components/rtttl/test.esp8266-ard.yaml b/tests/components/rtttl/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c3b87c0f72 --- /dev/null +++ b/tests/components/rtttl/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e' + - rtttl.stop + +output: + - platform: esp8266_pwm + id: rtttl_output + pin: 13 + frequency: 1500Hz + max_power: 0.5 + +rtttl: + output: rtttl_output diff --git a/tests/components/rtttl/test.rp2040-ard.yaml b/tests/components/rtttl/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ea240aa34d --- /dev/null +++ b/tests/components/rtttl/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e' + - rtttl.stop + +output: + - platform: rp2040_pwm + id: rtttl_output + pin: 3 + frequency: 1500Hz + max_power: 0.5 + +rtttl: + output: rtttl_output diff --git a/tests/components/ruuvi_ble/common.yaml b/tests/components/ruuvi_ble/common.yaml new file mode 100644 index 0000000000..1f155fd8e1 --- /dev/null +++ b/tests/components/ruuvi_ble/common.yaml @@ -0,0 +1,3 @@ +esp32_ble_tracker: + +ruuvi_ble: diff --git a/tests/components/ruuvi_ble/test.esp32-ard.yaml b/tests/components/ruuvi_ble/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ruuvi_ble/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-c3-ard.yaml b/tests/components/ruuvi_ble/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ruuvi_ble/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml b/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-idf.yaml b/tests/components/ruuvi_ble/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ruuvi_ble/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ruuvitag/common.yaml b/tests/components/ruuvitag/common.yaml new file mode 100644 index 0000000000..7990617710 --- /dev/null +++ b/tests/components/ruuvitag/common.yaml @@ -0,0 +1,27 @@ +esp32_ble_tracker: + +sensor: + - platform: ruuvitag + mac_address: FF:56:D3:2F:7D:E8 + humidity: + name: RuuviTag Humidity + temperature: + name: RuuviTag Temperature + pressure: + name: RuuviTag Pressure + acceleration: + name: RuuviTag Acceleration + acceleration_x: + name: RuuviTag Acceleration X + acceleration_y: + name: RuuviTag Acceleration Y + acceleration_z: + name: RuuviTag Acceleration Z + battery_voltage: + name: RuuviTag Battery Voltage + tx_power: + name: RuuviTag TX Power + movement_counter: + name: RuuviTag Movement Counter + measurement_sequence_number: + name: RuuviTag Measurement Sequence Number diff --git a/tests/components/ruuvitag/test.esp32-ard.yaml b/tests/components/ruuvitag/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ruuvitag/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-c3-ard.yaml b/tests/components/ruuvitag/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ruuvitag/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-c3-idf.yaml b/tests/components/ruuvitag/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ruuvitag/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-idf.yaml b/tests/components/ruuvitag/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ruuvitag/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/safe_mode/common-enabled.yaml b/tests/components/safe_mode/common-enabled.yaml new file mode 100644 index 0000000000..c24f49e6b6 --- /dev/null +++ b/tests/components/safe_mode/common-enabled.yaml @@ -0,0 +1,18 @@ +wifi: + ssid: MySSID + password: password1 + +safe_mode: + boot_is_good_after: 2min + num_attempts: 3 + reboot_timeout: 2min + on_safe_mode: + - logger.log: Time for safe mode + +button: + - platform: safe_mode + name: Safe Mode Button + +switch: + - platform: safe_mode + name: Safe Mode Switch diff --git a/tests/components/safe_mode/test-disabled.esp32-idf.yaml b/tests/components/safe_mode/test-disabled.esp32-idf.yaml new file mode 100644 index 0000000000..291f5a2c7c --- /dev/null +++ b/tests/components/safe_mode/test-disabled.esp32-idf.yaml @@ -0,0 +1,6 @@ +wifi: + ssid: MySSID + password: password1 + +safe_mode: + disabled: true diff --git a/tests/components/safe_mode/test-enabled.esp32-ard.yaml b/tests/components/safe_mode/test-enabled.esp32-ard.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/safe_mode/test-enabled.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/safe_mode/test-enabled.esp32-c3-ard.yaml b/tests/components/safe_mode/test-enabled.esp32-c3-ard.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/safe_mode/test-enabled.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml b/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/safe_mode/test-enabled.esp32-idf.yaml b/tests/components/safe_mode/test-enabled.esp32-idf.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/safe_mode/test-enabled.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/safe_mode/test-enabled.esp8266-ard.yaml b/tests/components/safe_mode/test-enabled.esp8266-ard.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/safe_mode/test-enabled.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/safe_mode/test-enabled.rp2040-ard.yaml b/tests/components/safe_mode/test-enabled.rp2040-ard.yaml new file mode 100644 index 0000000000..97fd63d70e --- /dev/null +++ b/tests/components/safe_mode/test-enabled.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled.yaml diff --git a/tests/components/scd30/test.esp32-ard.yaml b/tests/components/scd30/test.esp32-ard.yaml new file mode 100644 index 0000000000..b48f8054c8 --- /dev/null +++ b/tests/components/scd30/test.esp32-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd30 + scl: 16 + sda: 17 + +sensor: + - platform: scd30 + co2: + name: SCD30 CO2 + temperature: + id: scd30_temperature + name: SCD30 Temperature + humidity: + name: SCD30 Humidity + address: 0x61 + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd30/test.esp32-c3-ard.yaml b/tests/components/scd30/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..80f02a1b87 --- /dev/null +++ b/tests/components/scd30/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd30 + scl: 5 + sda: 4 + +sensor: + - platform: scd30 + co2: + name: SCD30 CO2 + temperature: + id: scd30_temperature + name: SCD30 Temperature + humidity: + name: SCD30 Humidity + address: 0x61 + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd30/test.esp32-c3-idf.yaml b/tests/components/scd30/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..80f02a1b87 --- /dev/null +++ b/tests/components/scd30/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd30 + scl: 5 + sda: 4 + +sensor: + - platform: scd30 + co2: + name: SCD30 CO2 + temperature: + id: scd30_temperature + name: SCD30 Temperature + humidity: + name: SCD30 Humidity + address: 0x61 + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd30/test.esp32-idf.yaml b/tests/components/scd30/test.esp32-idf.yaml new file mode 100644 index 0000000000..b48f8054c8 --- /dev/null +++ b/tests/components/scd30/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd30 + scl: 16 + sda: 17 + +sensor: + - platform: scd30 + co2: + name: SCD30 CO2 + temperature: + id: scd30_temperature + name: SCD30 Temperature + humidity: + name: SCD30 Humidity + address: 0x61 + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd30/test.esp8266-ard.yaml b/tests/components/scd30/test.esp8266-ard.yaml new file mode 100644 index 0000000000..80f02a1b87 --- /dev/null +++ b/tests/components/scd30/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd30 + scl: 5 + sda: 4 + +sensor: + - platform: scd30 + co2: + name: SCD30 CO2 + temperature: + id: scd30_temperature + name: SCD30 Temperature + humidity: + name: SCD30 Humidity + address: 0x61 + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd30/test.rp2040-ard.yaml b/tests/components/scd30/test.rp2040-ard.yaml new file mode 100644 index 0000000000..80f02a1b87 --- /dev/null +++ b/tests/components/scd30/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd30 + scl: 5 + sda: 4 + +sensor: + - platform: scd30 + co2: + name: SCD30 CO2 + temperature: + id: scd30_temperature + name: SCD30 Temperature + humidity: + name: SCD30 Humidity + address: 0x61 + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd4x/test.esp32-ard.yaml b/tests/components/scd4x/test.esp32-ard.yaml new file mode 100644 index 0000000000..02cec921d2 --- /dev/null +++ b/tests/components/scd4x/test.esp32-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd4x + scl: 16 + sda: 17 + +sensor: + - platform: scd4x + id: scd40 + co2: + name: SCD4X CO2 + temperature: + id: scd4x_temperature + name: SCD4X Temperature + humidity: + name: SCD4X Humidity + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd4x/test.esp32-c3-ard.yaml b/tests/components/scd4x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..353293be65 --- /dev/null +++ b/tests/components/scd4x/test.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd4x + scl: 5 + sda: 4 + +sensor: + - platform: scd4x + id: scd40 + co2: + name: SCD4X CO2 + temperature: + id: scd4x_temperature + name: SCD4X Temperature + humidity: + name: SCD4X Humidity + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd4x/test.esp32-c3-idf.yaml b/tests/components/scd4x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..353293be65 --- /dev/null +++ b/tests/components/scd4x/test.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd4x + scl: 5 + sda: 4 + +sensor: + - platform: scd4x + id: scd40 + co2: + name: SCD4X CO2 + temperature: + id: scd4x_temperature + name: SCD4X Temperature + humidity: + name: SCD4X Humidity + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd4x/test.esp32-idf.yaml b/tests/components/scd4x/test.esp32-idf.yaml new file mode 100644 index 0000000000..02cec921d2 --- /dev/null +++ b/tests/components/scd4x/test.esp32-idf.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd4x + scl: 16 + sda: 17 + +sensor: + - platform: scd4x + id: scd40 + co2: + name: SCD4X CO2 + temperature: + id: scd4x_temperature + name: SCD4X Temperature + humidity: + name: SCD4X Humidity + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd4x/test.esp8266-ard.yaml b/tests/components/scd4x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..353293be65 --- /dev/null +++ b/tests/components/scd4x/test.esp8266-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd4x + scl: 5 + sda: 4 + +sensor: + - platform: scd4x + id: scd40 + co2: + name: SCD4X CO2 + temperature: + id: scd4x_temperature + name: SCD4X Temperature + humidity: + name: SCD4X Humidity + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/scd4x/test.rp2040-ard.yaml b/tests/components/scd4x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..353293be65 --- /dev/null +++ b/tests/components/scd4x/test.rp2040-ard.yaml @@ -0,0 +1,20 @@ +i2c: + - id: i2c_scd4x + scl: 5 + sda: 4 + +sensor: + - platform: scd4x + id: scd40 + co2: + name: SCD4X CO2 + temperature: + id: scd4x_temperature + name: SCD4X Temperature + humidity: + name: SCD4X Humidity + automatic_self_calibration: true + altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C + update_interval: 15s diff --git a/tests/components/script/common.yaml b/tests/components/script/common.yaml new file mode 100644 index 0000000000..bfd5d0e7ff --- /dev/null +++ b/tests/components/script/common.yaml @@ -0,0 +1,55 @@ +esphome: + on_boot: + then: + - script.execute: my_script + - script.execute: + id: my_script_with_params + prefix: "Test" + param2: 0 + param3: true + - script.wait: my_script + - script.stop: my_script + - if: + condition: + - script.is_running: my_script + then: + - logger.log: my_script is running + +script: + - id: my_script + mode: single + then: + - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_queued + mode: queued + max_runs: 2 + then: + - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_parallel + mode: parallel + max_runs: 2 + then: + - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_restart + mode: restart + then: + - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_with_params + parameters: + prefix: string + param2: uint8_t + param3: bool + then: + - lambda: 'ESP_LOGD(prefix.c_str(), "Hello World! %u %u", param2, param3);' + - if: + condition: + for: + time: !lambda "return param2;" + condition: + script.is_running: my_script + then: + - lambda: 'ESP_LOGD("main", "API has stayed connected for at least %u minutes", param2);' + - repeat: + count: 5 + then: + - logger.log: looping! diff --git a/tests/components/script/test.bk72xx-ard.yaml b/tests/components/script/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/script/test.esp32-ard.yaml b/tests/components/script/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/script/test.esp32-c3-ard.yaml b/tests/components/script/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/script/test.esp32-c3-idf.yaml b/tests/components/script/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/script/test.esp32-idf.yaml b/tests/components/script/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/script/test.esp8266-ard.yaml b/tests/components/script/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/script/test.rp2040-ard.yaml b/tests/components/script/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sdl/common.yaml b/tests/components/sdl/common.yaml new file mode 100644 index 0000000000..0192f054b5 --- /dev/null +++ b/tests/components/sdl/common.yaml @@ -0,0 +1,12 @@ +host: + mac_address: "62:23:45:AF:B3:DD" + +display: + - platform: sdl + id: sdl_display + update_interval: 1s + auto_clear_enabled: false + show_test_card: true + dimensions: + width: 450 + height: 600 diff --git a/tests/components/sdl/test.host.yaml b/tests/components/sdl/test.host.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sdl/test.host.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sdm_meter/test.esp32-ard.yaml b/tests/components/sdm_meter/test.esp32-ard.yaml new file mode 100644 index 0000000000..eb3958db19 --- /dev/null +++ b/tests/components/sdm_meter/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sdm_meter + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: sdm_meter + phase_a: + current: + name: Phase A Current + voltage: + name: Phase A Voltage + active_power: + name: Phase A Power + power_factor: + name: Phase A Power Factor + apparent_power: + name: Phase A Apparent Power + reactive_power: + name: Phase A Reactive Power + phase_angle: + name: Phase A Phase Angle diff --git a/tests/components/sdm_meter/test.esp32-c3-ard.yaml b/tests/components/sdm_meter/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0c2144f983 --- /dev/null +++ b/tests/components/sdm_meter/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sdm_meter + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: sdm_meter + phase_a: + current: + name: Phase A Current + voltage: + name: Phase A Voltage + active_power: + name: Phase A Power + power_factor: + name: Phase A Power Factor + apparent_power: + name: Phase A Apparent Power + reactive_power: + name: Phase A Reactive Power + phase_angle: + name: Phase A Phase Angle diff --git a/tests/components/sdm_meter/test.esp32-c3-idf.yaml b/tests/components/sdm_meter/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0c2144f983 --- /dev/null +++ b/tests/components/sdm_meter/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sdm_meter + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: sdm_meter + phase_a: + current: + name: Phase A Current + voltage: + name: Phase A Voltage + active_power: + name: Phase A Power + power_factor: + name: Phase A Power Factor + apparent_power: + name: Phase A Apparent Power + reactive_power: + name: Phase A Reactive Power + phase_angle: + name: Phase A Phase Angle diff --git a/tests/components/sdm_meter/test.esp32-idf.yaml b/tests/components/sdm_meter/test.esp32-idf.yaml new file mode 100644 index 0000000000..eb3958db19 --- /dev/null +++ b/tests/components/sdm_meter/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sdm_meter + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: sdm_meter + phase_a: + current: + name: Phase A Current + voltage: + name: Phase A Voltage + active_power: + name: Phase A Power + power_factor: + name: Phase A Power Factor + apparent_power: + name: Phase A Apparent Power + reactive_power: + name: Phase A Reactive Power + phase_angle: + name: Phase A Phase Angle diff --git a/tests/components/sdm_meter/test.esp8266-ard.yaml b/tests/components/sdm_meter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0c2144f983 --- /dev/null +++ b/tests/components/sdm_meter/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sdm_meter + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: sdm_meter + phase_a: + current: + name: Phase A Current + voltage: + name: Phase A Voltage + active_power: + name: Phase A Power + power_factor: + name: Phase A Power Factor + apparent_power: + name: Phase A Apparent Power + reactive_power: + name: Phase A Reactive Power + phase_angle: + name: Phase A Phase Angle diff --git a/tests/components/sdm_meter/test.rp2040-ard.yaml b/tests/components/sdm_meter/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0c2144f983 --- /dev/null +++ b/tests/components/sdm_meter/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sdm_meter + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: sdm_meter + phase_a: + current: + name: Phase A Current + voltage: + name: Phase A Voltage + active_power: + name: Phase A Power + power_factor: + name: Phase A Power Factor + apparent_power: + name: Phase A Apparent Power + reactive_power: + name: Phase A Reactive Power + phase_angle: + name: Phase A Phase Angle diff --git a/tests/components/sdp3x/test.esp32-ard.yaml b/tests/components/sdp3x/test.esp32-ard.yaml new file mode 100644 index 0000000000..00666082eb --- /dev/null +++ b/tests/components/sdp3x/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_sdp3x + scl: 16 + sda: 17 + +sensor: + - platform: sdp3x + id: filter_pressure + name: HVAC Filter Pressure drop + accuracy_decimals: 3 + update_interval: 5s diff --git a/tests/components/sdp3x/test.esp32-c3-ard.yaml b/tests/components/sdp3x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..42b90f1b81 --- /dev/null +++ b/tests/components/sdp3x/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_sdp3x + scl: 5 + sda: 4 + +sensor: + - platform: sdp3x + id: filter_pressure + name: HVAC Filter Pressure drop + accuracy_decimals: 3 + update_interval: 5s diff --git a/tests/components/sdp3x/test.esp32-c3-idf.yaml b/tests/components/sdp3x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..42b90f1b81 --- /dev/null +++ b/tests/components/sdp3x/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_sdp3x + scl: 5 + sda: 4 + +sensor: + - platform: sdp3x + id: filter_pressure + name: HVAC Filter Pressure drop + accuracy_decimals: 3 + update_interval: 5s diff --git a/tests/components/sdp3x/test.esp32-idf.yaml b/tests/components/sdp3x/test.esp32-idf.yaml new file mode 100644 index 0000000000..00666082eb --- /dev/null +++ b/tests/components/sdp3x/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_sdp3x + scl: 16 + sda: 17 + +sensor: + - platform: sdp3x + id: filter_pressure + name: HVAC Filter Pressure drop + accuracy_decimals: 3 + update_interval: 5s diff --git a/tests/components/sdp3x/test.esp8266-ard.yaml b/tests/components/sdp3x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..42b90f1b81 --- /dev/null +++ b/tests/components/sdp3x/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_sdp3x + scl: 5 + sda: 4 + +sensor: + - platform: sdp3x + id: filter_pressure + name: HVAC Filter Pressure drop + accuracy_decimals: 3 + update_interval: 5s diff --git a/tests/components/sdp3x/test.rp2040-ard.yaml b/tests/components/sdp3x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..42b90f1b81 --- /dev/null +++ b/tests/components/sdp3x/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_sdp3x + scl: 5 + sda: 4 + +sensor: + - platform: sdp3x + id: filter_pressure + name: HVAC Filter Pressure drop + accuracy_decimals: 3 + update_interval: 5s diff --git a/tests/components/sds011/test.esp32-ard.yaml b/tests/components/sds011/test.esp32-ard.yaml new file mode 100644 index 0000000000..275390f14c --- /dev/null +++ b/tests/components/sds011/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_sdm_sds011 + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +sensor: + - platform: sds011 + pm_2_5: + name: SDS011 PM2.5 + pm_10_0: + name: SDS011 PM10.0 + rx_only: false + update_interval: 5min diff --git a/tests/components/sds011/test.esp32-c3-ard.yaml b/tests/components/sds011/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e680a62dfe --- /dev/null +++ b/tests/components/sds011/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_sdm_sds011 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: sds011 + pm_2_5: + name: SDS011 PM2.5 + pm_10_0: + name: SDS011 PM10.0 + rx_only: false + update_interval: 5min diff --git a/tests/components/sds011/test.esp32-c3-idf.yaml b/tests/components/sds011/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e680a62dfe --- /dev/null +++ b/tests/components/sds011/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_sdm_sds011 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: sds011 + pm_2_5: + name: SDS011 PM2.5 + pm_10_0: + name: SDS011 PM10.0 + rx_only: false + update_interval: 5min diff --git a/tests/components/sds011/test.esp32-idf.yaml b/tests/components/sds011/test.esp32-idf.yaml new file mode 100644 index 0000000000..275390f14c --- /dev/null +++ b/tests/components/sds011/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_sdm_sds011 + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +sensor: + - platform: sds011 + pm_2_5: + name: SDS011 PM2.5 + pm_10_0: + name: SDS011 PM10.0 + rx_only: false + update_interval: 5min diff --git a/tests/components/sds011/test.esp8266-ard.yaml b/tests/components/sds011/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e680a62dfe --- /dev/null +++ b/tests/components/sds011/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_sdm_sds011 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: sds011 + pm_2_5: + name: SDS011 PM2.5 + pm_10_0: + name: SDS011 PM10.0 + rx_only: false + update_interval: 5min diff --git a/tests/components/sds011/test.rp2040-ard.yaml b/tests/components/sds011/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e680a62dfe --- /dev/null +++ b/tests/components/sds011/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_sdm_sds011 + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +sensor: + - platform: sds011 + pm_2_5: + name: SDS011 PM2.5 + pm_10_0: + name: SDS011 PM10.0 + rx_only: false + update_interval: 5min diff --git a/tests/components/seeed_mr24hpc1/common.yaml b/tests/components/seeed_mr24hpc1/common.yaml new file mode 100644 index 0000000000..38692b3e5e --- /dev/null +++ b/tests/components/seeed_mr24hpc1/common.yaml @@ -0,0 +1,92 @@ +uart: + - id: seeed_mr24hpc1_uart + tx_pin: ${uart_tx_pin} + rx_pin: ${uart_rx_pin} + baud_rate: 115200 + parity: NONE + stop_bits: 1 + +seeed_mr24hpc1: + id: my_seeed_mr24hpc1 + uart_id: seeed_mr24hpc1_uart + +sensor: + - platform: seeed_mr24hpc1 + custom_presence_of_detection: + name: "Static Distance" + movement_signs: + name: "Body Movement Parameter" + custom_motion_distance: + name: "Motion Distance" + custom_spatial_static_value: + name: "Existence Energy" + custom_spatial_motion_value: + name: "Motion Energy" + custom_motion_speed: + name: "Motion Speed" + custom_mode_num: + name: "Current Custom Mode" + +binary_sensor: + - platform: seeed_mr24hpc1 + has_target: + name: "Presence Information" + +switch: + - platform: seeed_mr24hpc1 + underlying_open_function: + name: Underlying Open Function Info Output Switch + +text_sensor: + - platform: seeed_mr24hpc1 + heart_beat: + name: "Heartbeat" + product_model: + name: "Product Model" + product_id: + name: "Product ID" + hardware_model: + name: "Hardware Model" + hardware_version: + name: "Hardware Version" + keep_away: + name: "Active Reporting Of Proximity" + motion_status: + name: "Motion Information" + custom_mode_end: + name: "Custom Mode Status" + +number: + - platform: seeed_mr24hpc1 + sensitivity: + name: "Sensitivity" + custom_mode: + name: "Custom Mode" + existence_threshold: + name: "Existence Energy Threshold" + motion_threshold: + name: "Motion Energy Threshold" + motion_trigger: + name: "Motion Trigger Time" + motion_to_rest: + name: "Motion To Rest Time" + custom_unman_time: + name: "Time For Entering No Person State (Underlying Open Function)" + +select: + - platform: seeed_mr24hpc1 + scene_mode: + name: "Scene" + unman_time: + name: "Time For Entering No Person State (Standard Function)" + existence_boundary: + name: "Existence Boundary" + motion_boundary: + name: "Motion Boundary" + +button: + - platform: seeed_mr24hpc1 + restart: + name: "Module Restart" + custom_set_end: + name: "End Of Custom Mode Settings" diff --git a/tests/components/seeed_mr24hpc1/test.esp32-c3-ard.yaml b/tests/components/seeed_mr24hpc1/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4fb884abf4 --- /dev/null +++ b/tests/components/seeed_mr24hpc1/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + uart_tx_pin: GPIO5 + uart_rx_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml b/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4fb884abf4 --- /dev/null +++ b/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + uart_tx_pin: GPIO5 + uart_rx_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/selec_meter/test.esp32-ard.yaml b/tests/components/selec_meter/test.esp32-ard.yaml new file mode 100644 index 0000000000..648adc1757 --- /dev/null +++ b/tests/components/selec_meter/test.esp32-ard.yaml @@ -0,0 +1,45 @@ +uart: + - id: uart_selec_meter + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: selec_meter + total_active_energy: + name: SelecEM2M Total Active Energy + import_active_energy: + name: SelecEM2M Import Active Energy + export_active_energy: + name: SelecEM2M Export Active Energy + total_reactive_energy: + name: SelecEM2M Total Reactive Energy + import_reactive_energy: + name: SelecEM2M Import Reactive Energy + export_reactive_energy: + name: SelecEM2M Export Reactive Energy + apparent_energy: + name: SelecEM2M Apparent Energy + active_power: + name: SelecEM2M Active Power + reactive_power: + name: SelecEM2M Reactive Power + apparent_power: + name: SelecEM2M Apparent Power + voltage: + name: SelecEM2M Voltage + current: + name: SelecEM2M Current + power_factor: + name: SelecEM2M Power Factor + frequency: + name: SelecEM2M Frequency + maximum_demand_active_power: + name: SelecEM2M Maximum Demand Active Power + disabled_by_default: true + maximum_demand_reactive_power: + name: SelecEM2M Maximum Demand Reactive Power + disabled_by_default: true + maximum_demand_apparent_power: + name: SelecEM2M Maximum Demand Apparent Power + disabled_by_default: true diff --git a/tests/components/selec_meter/test.esp32-c3-ard.yaml b/tests/components/selec_meter/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5f6e69f96f --- /dev/null +++ b/tests/components/selec_meter/test.esp32-c3-ard.yaml @@ -0,0 +1,45 @@ +uart: + - id: uart_selec_meter + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: selec_meter + total_active_energy: + name: SelecEM2M Total Active Energy + import_active_energy: + name: SelecEM2M Import Active Energy + export_active_energy: + name: SelecEM2M Export Active Energy + total_reactive_energy: + name: SelecEM2M Total Reactive Energy + import_reactive_energy: + name: SelecEM2M Import Reactive Energy + export_reactive_energy: + name: SelecEM2M Export Reactive Energy + apparent_energy: + name: SelecEM2M Apparent Energy + active_power: + name: SelecEM2M Active Power + reactive_power: + name: SelecEM2M Reactive Power + apparent_power: + name: SelecEM2M Apparent Power + voltage: + name: SelecEM2M Voltage + current: + name: SelecEM2M Current + power_factor: + name: SelecEM2M Power Factor + frequency: + name: SelecEM2M Frequency + maximum_demand_active_power: + name: SelecEM2M Maximum Demand Active Power + disabled_by_default: true + maximum_demand_reactive_power: + name: SelecEM2M Maximum Demand Reactive Power + disabled_by_default: true + maximum_demand_apparent_power: + name: SelecEM2M Maximum Demand Apparent Power + disabled_by_default: true diff --git a/tests/components/selec_meter/test.esp32-c3-idf.yaml b/tests/components/selec_meter/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5f6e69f96f --- /dev/null +++ b/tests/components/selec_meter/test.esp32-c3-idf.yaml @@ -0,0 +1,45 @@ +uart: + - id: uart_selec_meter + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: selec_meter + total_active_energy: + name: SelecEM2M Total Active Energy + import_active_energy: + name: SelecEM2M Import Active Energy + export_active_energy: + name: SelecEM2M Export Active Energy + total_reactive_energy: + name: SelecEM2M Total Reactive Energy + import_reactive_energy: + name: SelecEM2M Import Reactive Energy + export_reactive_energy: + name: SelecEM2M Export Reactive Energy + apparent_energy: + name: SelecEM2M Apparent Energy + active_power: + name: SelecEM2M Active Power + reactive_power: + name: SelecEM2M Reactive Power + apparent_power: + name: SelecEM2M Apparent Power + voltage: + name: SelecEM2M Voltage + current: + name: SelecEM2M Current + power_factor: + name: SelecEM2M Power Factor + frequency: + name: SelecEM2M Frequency + maximum_demand_active_power: + name: SelecEM2M Maximum Demand Active Power + disabled_by_default: true + maximum_demand_reactive_power: + name: SelecEM2M Maximum Demand Reactive Power + disabled_by_default: true + maximum_demand_apparent_power: + name: SelecEM2M Maximum Demand Apparent Power + disabled_by_default: true diff --git a/tests/components/selec_meter/test.esp32-idf.yaml b/tests/components/selec_meter/test.esp32-idf.yaml new file mode 100644 index 0000000000..648adc1757 --- /dev/null +++ b/tests/components/selec_meter/test.esp32-idf.yaml @@ -0,0 +1,45 @@ +uart: + - id: uart_selec_meter + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: selec_meter + total_active_energy: + name: SelecEM2M Total Active Energy + import_active_energy: + name: SelecEM2M Import Active Energy + export_active_energy: + name: SelecEM2M Export Active Energy + total_reactive_energy: + name: SelecEM2M Total Reactive Energy + import_reactive_energy: + name: SelecEM2M Import Reactive Energy + export_reactive_energy: + name: SelecEM2M Export Reactive Energy + apparent_energy: + name: SelecEM2M Apparent Energy + active_power: + name: SelecEM2M Active Power + reactive_power: + name: SelecEM2M Reactive Power + apparent_power: + name: SelecEM2M Apparent Power + voltage: + name: SelecEM2M Voltage + current: + name: SelecEM2M Current + power_factor: + name: SelecEM2M Power Factor + frequency: + name: SelecEM2M Frequency + maximum_demand_active_power: + name: SelecEM2M Maximum Demand Active Power + disabled_by_default: true + maximum_demand_reactive_power: + name: SelecEM2M Maximum Demand Reactive Power + disabled_by_default: true + maximum_demand_apparent_power: + name: SelecEM2M Maximum Demand Apparent Power + disabled_by_default: true diff --git a/tests/components/selec_meter/test.esp8266-ard.yaml b/tests/components/selec_meter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5f6e69f96f --- /dev/null +++ b/tests/components/selec_meter/test.esp8266-ard.yaml @@ -0,0 +1,45 @@ +uart: + - id: uart_selec_meter + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: selec_meter + total_active_energy: + name: SelecEM2M Total Active Energy + import_active_energy: + name: SelecEM2M Import Active Energy + export_active_energy: + name: SelecEM2M Export Active Energy + total_reactive_energy: + name: SelecEM2M Total Reactive Energy + import_reactive_energy: + name: SelecEM2M Import Reactive Energy + export_reactive_energy: + name: SelecEM2M Export Reactive Energy + apparent_energy: + name: SelecEM2M Apparent Energy + active_power: + name: SelecEM2M Active Power + reactive_power: + name: SelecEM2M Reactive Power + apparent_power: + name: SelecEM2M Apparent Power + voltage: + name: SelecEM2M Voltage + current: + name: SelecEM2M Current + power_factor: + name: SelecEM2M Power Factor + frequency: + name: SelecEM2M Frequency + maximum_demand_active_power: + name: SelecEM2M Maximum Demand Active Power + disabled_by_default: true + maximum_demand_reactive_power: + name: SelecEM2M Maximum Demand Reactive Power + disabled_by_default: true + maximum_demand_apparent_power: + name: SelecEM2M Maximum Demand Apparent Power + disabled_by_default: true diff --git a/tests/components/selec_meter/test.rp2040-ard.yaml b/tests/components/selec_meter/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5f6e69f96f --- /dev/null +++ b/tests/components/selec_meter/test.rp2040-ard.yaml @@ -0,0 +1,45 @@ +uart: + - id: uart_selec_meter + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: selec_meter + total_active_energy: + name: SelecEM2M Total Active Energy + import_active_energy: + name: SelecEM2M Import Active Energy + export_active_energy: + name: SelecEM2M Export Active Energy + total_reactive_energy: + name: SelecEM2M Total Reactive Energy + import_reactive_energy: + name: SelecEM2M Import Reactive Energy + export_reactive_energy: + name: SelecEM2M Export Reactive Energy + apparent_energy: + name: SelecEM2M Apparent Energy + active_power: + name: SelecEM2M Active Power + reactive_power: + name: SelecEM2M Reactive Power + apparent_power: + name: SelecEM2M Apparent Power + voltage: + name: SelecEM2M Voltage + current: + name: SelecEM2M Current + power_factor: + name: SelecEM2M Power Factor + frequency: + name: SelecEM2M Frequency + maximum_demand_active_power: + name: SelecEM2M Maximum Demand Active Power + disabled_by_default: true + maximum_demand_reactive_power: + name: SelecEM2M Maximum Demand Reactive Power + disabled_by_default: true + maximum_demand_apparent_power: + name: SelecEM2M Maximum Demand Apparent Power + disabled_by_default: true diff --git a/tests/components/sen0321/test.esp32-ard.yaml b/tests/components/sen0321/test.esp32-ard.yaml new file mode 100644 index 0000000000..44f76bf5e6 --- /dev/null +++ b/tests/components/sen0321/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sen0321 + scl: 16 + sda: 17 + +sensor: + - platform: sen0321 + name: Workshop Ozone Sensor + id: sen0321_ozone + update_interval: 10s diff --git a/tests/components/sen0321/test.esp32-c3-ard.yaml b/tests/components/sen0321/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7fa461a7fa --- /dev/null +++ b/tests/components/sen0321/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sen0321 + scl: 5 + sda: 4 + +sensor: + - platform: sen0321 + name: Workshop Ozone Sensor + id: sen0321_ozone + update_interval: 10s diff --git a/tests/components/sen0321/test.esp32-c3-idf.yaml b/tests/components/sen0321/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7fa461a7fa --- /dev/null +++ b/tests/components/sen0321/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sen0321 + scl: 5 + sda: 4 + +sensor: + - platform: sen0321 + name: Workshop Ozone Sensor + id: sen0321_ozone + update_interval: 10s diff --git a/tests/components/sen0321/test.esp32-idf.yaml b/tests/components/sen0321/test.esp32-idf.yaml new file mode 100644 index 0000000000..44f76bf5e6 --- /dev/null +++ b/tests/components/sen0321/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sen0321 + scl: 16 + sda: 17 + +sensor: + - platform: sen0321 + name: Workshop Ozone Sensor + id: sen0321_ozone + update_interval: 10s diff --git a/tests/components/sen0321/test.esp8266-ard.yaml b/tests/components/sen0321/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7fa461a7fa --- /dev/null +++ b/tests/components/sen0321/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sen0321 + scl: 5 + sda: 4 + +sensor: + - platform: sen0321 + name: Workshop Ozone Sensor + id: sen0321_ozone + update_interval: 10s diff --git a/tests/components/sen0321/test.rp2040-ard.yaml b/tests/components/sen0321/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7fa461a7fa --- /dev/null +++ b/tests/components/sen0321/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sen0321 + scl: 5 + sda: 4 + +sensor: + - platform: sen0321 + name: Workshop Ozone Sensor + id: sen0321_ozone + update_interval: 10s diff --git a/tests/components/sen21231/test.esp32-ard.yaml b/tests/components/sen21231/test.esp32-ard.yaml new file mode 100644 index 0000000000..3173683f17 --- /dev/null +++ b/tests/components/sen21231/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_sen21231 + scl: 16 + sda: 17 + +sensor: + - platform: sen21231 + id: sen21231_sensor1 + name: Person Sensor diff --git a/tests/components/sen21231/test.esp32-c3-ard.yaml b/tests/components/sen21231/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..efd7843f56 --- /dev/null +++ b/tests/components/sen21231/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_sen21231 + scl: 5 + sda: 4 + +sensor: + - platform: sen21231 + id: sen21231_sensor1 + name: Person Sensor diff --git a/tests/components/sen21231/test.esp32-c3-idf.yaml b/tests/components/sen21231/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..efd7843f56 --- /dev/null +++ b/tests/components/sen21231/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_sen21231 + scl: 5 + sda: 4 + +sensor: + - platform: sen21231 + id: sen21231_sensor1 + name: Person Sensor diff --git a/tests/components/sen21231/test.esp32-idf.yaml b/tests/components/sen21231/test.esp32-idf.yaml new file mode 100644 index 0000000000..3173683f17 --- /dev/null +++ b/tests/components/sen21231/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_sen21231 + scl: 16 + sda: 17 + +sensor: + - platform: sen21231 + id: sen21231_sensor1 + name: Person Sensor diff --git a/tests/components/sen21231/test.esp8266-ard.yaml b/tests/components/sen21231/test.esp8266-ard.yaml new file mode 100644 index 0000000000..efd7843f56 --- /dev/null +++ b/tests/components/sen21231/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_sen21231 + scl: 5 + sda: 4 + +sensor: + - platform: sen21231 + id: sen21231_sensor1 + name: Person Sensor diff --git a/tests/components/sen21231/test.rp2040-ard.yaml b/tests/components/sen21231/test.rp2040-ard.yaml new file mode 100644 index 0000000000..efd7843f56 --- /dev/null +++ b/tests/components/sen21231/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_sen21231 + scl: 5 + sda: 4 + +sensor: + - platform: sen21231 + id: sen21231_sensor1 + name: Person Sensor diff --git a/tests/components/sen5x/test.esp32-ard.yaml b/tests/components/sen5x/test.esp32-ard.yaml new file mode 100644 index 0000000000..b8f89c435f --- /dev/null +++ b/tests/components/sen5x/test.esp32-ard.yaml @@ -0,0 +1,49 @@ +i2c: + - id: i2c_sen5x + scl: 16 + sda: 17 + +sensor: + - platform: sen5x + id: sen54 + temperature: + name: Temperature + accuracy_decimals: 1 + humidity: + name: Humidity + accuracy_decimals: 0 + pm_1_0: + name: PM <1µm Weight concentration + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: PM <2.5µm Weight concentration + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: PM <4µm Weight concentration + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: PM <10µm Weight concentration + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: NOx + voc: + name: VOC + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + temperature_compensation: + offset: 0 + normalized_offset_slope: 0 + time_constant: 0 + auto_cleaning_interval: 604800s + acceleration_mode: low + store_baseline: true + address: 0x69 diff --git a/tests/components/sen5x/test.esp32-c3-ard.yaml b/tests/components/sen5x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..3352a59b17 --- /dev/null +++ b/tests/components/sen5x/test.esp32-c3-ard.yaml @@ -0,0 +1,49 @@ +i2c: + - id: i2c_sen5x + scl: 5 + sda: 4 + +sensor: + - platform: sen5x + id: sen54 + temperature: + name: Temperature + accuracy_decimals: 1 + humidity: + name: Humidity + accuracy_decimals: 0 + pm_1_0: + name: PM <1µm Weight concentration + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: PM <2.5µm Weight concentration + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: PM <4µm Weight concentration + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: PM <10µm Weight concentration + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: NOx + voc: + name: VOC + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + temperature_compensation: + offset: 0 + normalized_offset_slope: 0 + time_constant: 0 + auto_cleaning_interval: 604800s + acceleration_mode: low + store_baseline: true + address: 0x69 diff --git a/tests/components/sen5x/test.esp32-c3-idf.yaml b/tests/components/sen5x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3352a59b17 --- /dev/null +++ b/tests/components/sen5x/test.esp32-c3-idf.yaml @@ -0,0 +1,49 @@ +i2c: + - id: i2c_sen5x + scl: 5 + sda: 4 + +sensor: + - platform: sen5x + id: sen54 + temperature: + name: Temperature + accuracy_decimals: 1 + humidity: + name: Humidity + accuracy_decimals: 0 + pm_1_0: + name: PM <1µm Weight concentration + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: PM <2.5µm Weight concentration + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: PM <4µm Weight concentration + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: PM <10µm Weight concentration + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: NOx + voc: + name: VOC + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + temperature_compensation: + offset: 0 + normalized_offset_slope: 0 + time_constant: 0 + auto_cleaning_interval: 604800s + acceleration_mode: low + store_baseline: true + address: 0x69 diff --git a/tests/components/sen5x/test.esp32-idf.yaml b/tests/components/sen5x/test.esp32-idf.yaml new file mode 100644 index 0000000000..b8f89c435f --- /dev/null +++ b/tests/components/sen5x/test.esp32-idf.yaml @@ -0,0 +1,49 @@ +i2c: + - id: i2c_sen5x + scl: 16 + sda: 17 + +sensor: + - platform: sen5x + id: sen54 + temperature: + name: Temperature + accuracy_decimals: 1 + humidity: + name: Humidity + accuracy_decimals: 0 + pm_1_0: + name: PM <1µm Weight concentration + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: PM <2.5µm Weight concentration + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: PM <4µm Weight concentration + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: PM <10µm Weight concentration + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: NOx + voc: + name: VOC + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + temperature_compensation: + offset: 0 + normalized_offset_slope: 0 + time_constant: 0 + auto_cleaning_interval: 604800s + acceleration_mode: low + store_baseline: true + address: 0x69 diff --git a/tests/components/sen5x/test.esp8266-ard.yaml b/tests/components/sen5x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3352a59b17 --- /dev/null +++ b/tests/components/sen5x/test.esp8266-ard.yaml @@ -0,0 +1,49 @@ +i2c: + - id: i2c_sen5x + scl: 5 + sda: 4 + +sensor: + - platform: sen5x + id: sen54 + temperature: + name: Temperature + accuracy_decimals: 1 + humidity: + name: Humidity + accuracy_decimals: 0 + pm_1_0: + name: PM <1µm Weight concentration + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: PM <2.5µm Weight concentration + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: PM <4µm Weight concentration + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: PM <10µm Weight concentration + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: NOx + voc: + name: VOC + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + temperature_compensation: + offset: 0 + normalized_offset_slope: 0 + time_constant: 0 + auto_cleaning_interval: 604800s + acceleration_mode: low + store_baseline: true + address: 0x69 diff --git a/tests/components/sen5x/test.rp2040-ard.yaml b/tests/components/sen5x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..3352a59b17 --- /dev/null +++ b/tests/components/sen5x/test.rp2040-ard.yaml @@ -0,0 +1,49 @@ +i2c: + - id: i2c_sen5x + scl: 5 + sda: 4 + +sensor: + - platform: sen5x + id: sen54 + temperature: + name: Temperature + accuracy_decimals: 1 + humidity: + name: Humidity + accuracy_decimals: 0 + pm_1_0: + name: PM <1µm Weight concentration + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: PM <2.5µm Weight concentration + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: PM <4µm Weight concentration + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: PM <10µm Weight concentration + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: NOx + voc: + name: VOC + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + temperature_compensation: + offset: 0 + normalized_offset_slope: 0 + time_constant: 0 + auto_cleaning_interval: 604800s + acceleration_mode: low + store_baseline: true + address: 0x69 diff --git a/tests/components/senseair/test.esp32-ard.yaml b/tests/components/senseair/test.esp32-ard.yaml new file mode 100644 index 0000000000..daa4645f59 --- /dev/null +++ b/tests/components/senseair/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_senseair + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: senseair + id: senseair0 + co2: + name: SenseAir CO2 Value + on_value: + then: + - senseair.background_calibration: senseair0 + - senseair.background_calibration_result: senseair0 + - senseair.abc_get_period: senseair0 + - senseair.abc_enable: senseair0 + - senseair.abc_disable: senseair0 + update_interval: 15s diff --git a/tests/components/senseair/test.esp32-c3-ard.yaml b/tests/components/senseair/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..41a441f496 --- /dev/null +++ b/tests/components/senseair/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_senseair + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: senseair + id: senseair0 + co2: + name: SenseAir CO2 Value + on_value: + then: + - senseair.background_calibration: senseair0 + - senseair.background_calibration_result: senseair0 + - senseair.abc_get_period: senseair0 + - senseair.abc_enable: senseair0 + - senseair.abc_disable: senseair0 + update_interval: 15s diff --git a/tests/components/senseair/test.esp32-c3-idf.yaml b/tests/components/senseair/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..41a441f496 --- /dev/null +++ b/tests/components/senseair/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_senseair + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: senseair + id: senseair0 + co2: + name: SenseAir CO2 Value + on_value: + then: + - senseair.background_calibration: senseair0 + - senseair.background_calibration_result: senseair0 + - senseair.abc_get_period: senseair0 + - senseair.abc_enable: senseair0 + - senseair.abc_disable: senseair0 + update_interval: 15s diff --git a/tests/components/senseair/test.esp32-idf.yaml b/tests/components/senseair/test.esp32-idf.yaml new file mode 100644 index 0000000000..daa4645f59 --- /dev/null +++ b/tests/components/senseair/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_senseair + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: senseair + id: senseair0 + co2: + name: SenseAir CO2 Value + on_value: + then: + - senseair.background_calibration: senseair0 + - senseair.background_calibration_result: senseair0 + - senseair.abc_get_period: senseair0 + - senseair.abc_enable: senseair0 + - senseair.abc_disable: senseair0 + update_interval: 15s diff --git a/tests/components/senseair/test.esp8266-ard.yaml b/tests/components/senseair/test.esp8266-ard.yaml new file mode 100644 index 0000000000..41a441f496 --- /dev/null +++ b/tests/components/senseair/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_senseair + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: senseair + id: senseair0 + co2: + name: SenseAir CO2 Value + on_value: + then: + - senseair.background_calibration: senseair0 + - senseair.background_calibration_result: senseair0 + - senseair.abc_get_period: senseair0 + - senseair.abc_enable: senseair0 + - senseair.abc_disable: senseair0 + update_interval: 15s diff --git a/tests/components/senseair/test.rp2040-ard.yaml b/tests/components/senseair/test.rp2040-ard.yaml new file mode 100644 index 0000000000..41a441f496 --- /dev/null +++ b/tests/components/senseair/test.rp2040-ard.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_senseair + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: senseair + id: senseair0 + co2: + name: SenseAir CO2 Value + on_value: + then: + - senseair.background_calibration: senseair0 + - senseair.background_calibration_result: senseair0 + - senseair.abc_get_period: senseair0 + - senseair.abc_enable: senseair0 + - senseair.abc_disable: senseair0 + update_interval: 15s diff --git a/tests/components/servo/test.esp32-ard.yaml b/tests/components/servo/test.esp32-ard.yaml new file mode 100644 index 0000000000..e769f055b4 --- /dev/null +++ b/tests/components/servo/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - servo.write: + id: test_servo + level: -100.0% + - servo.detach: test_servo + +output: + - platform: ledc + id: servo_output_1 + pin: 12 + +servo: + id: test_servo + output: servo_output_1 + restore: true + min_level: 4% + max_level: 8% diff --git a/tests/components/servo/test.esp32-c3-ard.yaml b/tests/components/servo/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..29ebea3359 --- /dev/null +++ b/tests/components/servo/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - servo.write: + id: test_servo + level: -100.0% + - servo.detach: test_servo + +output: + - platform: ledc + id: servo_output_1 + pin: 1 + +servo: + id: test_servo + output: servo_output_1 + restore: true + min_level: 4% + max_level: 8% diff --git a/tests/components/servo/test.esp32-c3-idf.yaml b/tests/components/servo/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..29ebea3359 --- /dev/null +++ b/tests/components/servo/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - servo.write: + id: test_servo + level: -100.0% + - servo.detach: test_servo + +output: + - platform: ledc + id: servo_output_1 + pin: 1 + +servo: + id: test_servo + output: servo_output_1 + restore: true + min_level: 4% + max_level: 8% diff --git a/tests/components/servo/test.esp32-idf.yaml b/tests/components/servo/test.esp32-idf.yaml new file mode 100644 index 0000000000..e769f055b4 --- /dev/null +++ b/tests/components/servo/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - servo.write: + id: test_servo + level: -100.0% + - servo.detach: test_servo + +output: + - platform: ledc + id: servo_output_1 + pin: 12 + +servo: + id: test_servo + output: servo_output_1 + restore: true + min_level: 4% + max_level: 8% diff --git a/tests/components/servo/test.esp8266-ard.yaml b/tests/components/servo/test.esp8266-ard.yaml new file mode 100644 index 0000000000..48b4421641 --- /dev/null +++ b/tests/components/servo/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - servo.write: + id: test_servo + level: -100.0% + - servo.detach: test_servo + +output: + - platform: esp8266_pwm + id: servo_output_1 + pin: 12 + +servo: + id: test_servo + output: servo_output_1 + restore: true + min_level: 4% + max_level: 8% diff --git a/tests/components/servo/test.rp2040-ard.yaml b/tests/components/servo/test.rp2040-ard.yaml new file mode 100644 index 0000000000..75efa9407e --- /dev/null +++ b/tests/components/servo/test.rp2040-ard.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - servo.write: + id: test_servo + level: -100.0% + - servo.detach: test_servo + +output: + - platform: rp2040_pwm + id: servo_output_1 + pin: 12 + +servo: + id: test_servo + output: servo_output_1 + restore: true + min_level: 4% + max_level: 8% diff --git a/tests/components/sfa30/test.esp32-ard.yaml b/tests/components/sfa30/test.esp32-ard.yaml new file mode 100644 index 0000000000..dc7e4988e5 --- /dev/null +++ b/tests/components/sfa30/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sfa30 + scl: 16 + sda: 17 + +sensor: + - platform: sfa30 + formaldehyde: + name: "SFA30 formaldehyde" + temperature: + name: "SFA30 temperature" + humidity: + name: "SFA30 humidity" + address: 0x5D + update_interval: 30s diff --git a/tests/components/sfa30/test.esp32-c3-ard.yaml b/tests/components/sfa30/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..119059e4e2 --- /dev/null +++ b/tests/components/sfa30/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sfa30 + scl: 5 + sda: 4 + +sensor: + - platform: sfa30 + formaldehyde: + name: "SFA30 formaldehyde" + temperature: + name: "SFA30 temperature" + humidity: + name: "SFA30 humidity" + address: 0x5D + update_interval: 30s diff --git a/tests/components/sfa30/test.esp32-c3-idf.yaml b/tests/components/sfa30/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..119059e4e2 --- /dev/null +++ b/tests/components/sfa30/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sfa30 + scl: 5 + sda: 4 + +sensor: + - platform: sfa30 + formaldehyde: + name: "SFA30 formaldehyde" + temperature: + name: "SFA30 temperature" + humidity: + name: "SFA30 humidity" + address: 0x5D + update_interval: 30s diff --git a/tests/components/sfa30/test.esp32-idf.yaml b/tests/components/sfa30/test.esp32-idf.yaml new file mode 100644 index 0000000000..dc7e4988e5 --- /dev/null +++ b/tests/components/sfa30/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sfa30 + scl: 16 + sda: 17 + +sensor: + - platform: sfa30 + formaldehyde: + name: "SFA30 formaldehyde" + temperature: + name: "SFA30 temperature" + humidity: + name: "SFA30 humidity" + address: 0x5D + update_interval: 30s diff --git a/tests/components/sfa30/test.esp8266-ard.yaml b/tests/components/sfa30/test.esp8266-ard.yaml new file mode 100644 index 0000000000..119059e4e2 --- /dev/null +++ b/tests/components/sfa30/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sfa30 + scl: 5 + sda: 4 + +sensor: + - platform: sfa30 + formaldehyde: + name: "SFA30 formaldehyde" + temperature: + name: "SFA30 temperature" + humidity: + name: "SFA30 humidity" + address: 0x5D + update_interval: 30s diff --git a/tests/components/sfa30/test.rp2040-ard.yaml b/tests/components/sfa30/test.rp2040-ard.yaml new file mode 100644 index 0000000000..119059e4e2 --- /dev/null +++ b/tests/components/sfa30/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sfa30 + scl: 5 + sda: 4 + +sensor: + - platform: sfa30 + formaldehyde: + name: "SFA30 formaldehyde" + temperature: + name: "SFA30 temperature" + humidity: + name: "SFA30 humidity" + address: 0x5D + update_interval: 30s diff --git a/tests/components/sgp30/test.esp32-ard.yaml b/tests/components/sgp30/test.esp32-ard.yaml new file mode 100644 index 0000000000..6ea23c25cd --- /dev/null +++ b/tests/components/sgp30/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sgp30 + scl: 16 + sda: 17 + +sensor: + - platform: sgp30 + eco2: + name: Workshop eCO2 + accuracy_decimals: 1 + tvoc: + name: Workshop TVOC + accuracy_decimals: 1 + address: 0x58 + update_interval: 5s diff --git a/tests/components/sgp30/test.esp32-c3-ard.yaml b/tests/components/sgp30/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..45de67e94b --- /dev/null +++ b/tests/components/sgp30/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sgp30 + scl: 5 + sda: 4 + +sensor: + - platform: sgp30 + eco2: + name: Workshop eCO2 + accuracy_decimals: 1 + tvoc: + name: Workshop TVOC + accuracy_decimals: 1 + address: 0x58 + update_interval: 5s diff --git a/tests/components/sgp30/test.esp32-c3-idf.yaml b/tests/components/sgp30/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..45de67e94b --- /dev/null +++ b/tests/components/sgp30/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sgp30 + scl: 5 + sda: 4 + +sensor: + - platform: sgp30 + eco2: + name: Workshop eCO2 + accuracy_decimals: 1 + tvoc: + name: Workshop TVOC + accuracy_decimals: 1 + address: 0x58 + update_interval: 5s diff --git a/tests/components/sgp30/test.esp32-idf.yaml b/tests/components/sgp30/test.esp32-idf.yaml new file mode 100644 index 0000000000..6ea23c25cd --- /dev/null +++ b/tests/components/sgp30/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sgp30 + scl: 16 + sda: 17 + +sensor: + - platform: sgp30 + eco2: + name: Workshop eCO2 + accuracy_decimals: 1 + tvoc: + name: Workshop TVOC + accuracy_decimals: 1 + address: 0x58 + update_interval: 5s diff --git a/tests/components/sgp30/test.esp8266-ard.yaml b/tests/components/sgp30/test.esp8266-ard.yaml new file mode 100644 index 0000000000..45de67e94b --- /dev/null +++ b/tests/components/sgp30/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sgp30 + scl: 5 + sda: 4 + +sensor: + - platform: sgp30 + eco2: + name: Workshop eCO2 + accuracy_decimals: 1 + tvoc: + name: Workshop TVOC + accuracy_decimals: 1 + address: 0x58 + update_interval: 5s diff --git a/tests/components/sgp30/test.rp2040-ard.yaml b/tests/components/sgp30/test.rp2040-ard.yaml new file mode 100644 index 0000000000..45de67e94b --- /dev/null +++ b/tests/components/sgp30/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_sgp30 + scl: 5 + sda: 4 + +sensor: + - platform: sgp30 + eco2: + name: Workshop eCO2 + accuracy_decimals: 1 + tvoc: + name: Workshop TVOC + accuracy_decimals: 1 + address: 0x58 + update_interval: 5s diff --git a/tests/components/sgp4x/test.esp32-ard.yaml b/tests/components/sgp4x/test.esp32-ard.yaml new file mode 100644 index 0000000000..c7380b5a10 --- /dev/null +++ b/tests/components/sgp4x/test.esp32-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_sgp4x + scl: 16 + sda: 17 + +sensor: + - platform: sgp4x + voc: + name: VOC Index + id: sgp40_voc_index + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + nox: + name: NOx + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + update_interval: 5s diff --git a/tests/components/sgp4x/test.esp32-c3-ard.yaml b/tests/components/sgp4x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b2876478bd --- /dev/null +++ b/tests/components/sgp4x/test.esp32-c3-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_sgp4x + scl: 5 + sda: 4 + +sensor: + - platform: sgp4x + voc: + name: VOC Index + id: sgp40_voc_index + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + nox: + name: NOx + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + update_interval: 5s diff --git a/tests/components/sgp4x/test.esp32-c3-idf.yaml b/tests/components/sgp4x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b2876478bd --- /dev/null +++ b/tests/components/sgp4x/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_sgp4x + scl: 5 + sda: 4 + +sensor: + - platform: sgp4x + voc: + name: VOC Index + id: sgp40_voc_index + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + nox: + name: NOx + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + update_interval: 5s diff --git a/tests/components/sgp4x/test.esp32-idf.yaml b/tests/components/sgp4x/test.esp32-idf.yaml new file mode 100644 index 0000000000..c7380b5a10 --- /dev/null +++ b/tests/components/sgp4x/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_sgp4x + scl: 16 + sda: 17 + +sensor: + - platform: sgp4x + voc: + name: VOC Index + id: sgp40_voc_index + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + nox: + name: NOx + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + update_interval: 5s diff --git a/tests/components/sgp4x/test.esp8266-ard.yaml b/tests/components/sgp4x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b2876478bd --- /dev/null +++ b/tests/components/sgp4x/test.esp8266-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_sgp4x + scl: 5 + sda: 4 + +sensor: + - platform: sgp4x + voc: + name: VOC Index + id: sgp40_voc_index + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + nox: + name: NOx + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + update_interval: 5s diff --git a/tests/components/sgp4x/test.rp2040-ard.yaml b/tests/components/sgp4x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b2876478bd --- /dev/null +++ b/tests/components/sgp4x/test.rp2040-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_sgp4x + scl: 5 + sda: 4 + +sensor: + - platform: sgp4x + voc: + name: VOC Index + id: sgp40_voc_index + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + nox: + name: NOx + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + update_interval: 5s diff --git a/tests/components/shelly_dimmer/common.yaml b/tests/components/shelly_dimmer/common.yaml new file mode 100644 index 0000000000..3acd0260d5 --- /dev/null +++ b/tests/components/shelly_dimmer/common.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_shelly_dimmer + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +light: + - platform: shelly_dimmer + name: Shelly Dimmer Light + power: + name: Shelly Dimmer Power + voltage: + name: Shelly Dimmer Voltage + current: + name: Shelly Dimmer Current + max_brightness: 500 + firmware: "51.6" + nrst_pin: 13 + boot0_pin: 14 diff --git a/tests/components/shelly_dimmer/test.esp8266-ard.yaml b/tests/components/shelly_dimmer/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/shelly_dimmer/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sht3xd/test.esp32-ard.yaml b/tests/components/sht3xd/test.esp32-ard.yaml new file mode 100644 index 0000000000..2b6ee50760 --- /dev/null +++ b/tests/components/sht3xd/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht3xd + scl: 16 + sda: 17 + +sensor: + - platform: sht3xd + temperature: + name: SHT3XD Temperature + humidity: + name: SHT3XD Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht3xd/test.esp32-c3-ard.yaml b/tests/components/sht3xd/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0409ff65c6 --- /dev/null +++ b/tests/components/sht3xd/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht3xd + scl: 5 + sda: 4 + +sensor: + - platform: sht3xd + temperature: + name: SHT3XD Temperature + humidity: + name: SHT3XD Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht3xd/test.esp32-c3-idf.yaml b/tests/components/sht3xd/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0409ff65c6 --- /dev/null +++ b/tests/components/sht3xd/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht3xd + scl: 5 + sda: 4 + +sensor: + - platform: sht3xd + temperature: + name: SHT3XD Temperature + humidity: + name: SHT3XD Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht3xd/test.esp32-idf.yaml b/tests/components/sht3xd/test.esp32-idf.yaml new file mode 100644 index 0000000000..2b6ee50760 --- /dev/null +++ b/tests/components/sht3xd/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht3xd + scl: 16 + sda: 17 + +sensor: + - platform: sht3xd + temperature: + name: SHT3XD Temperature + humidity: + name: SHT3XD Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht3xd/test.esp8266-ard.yaml b/tests/components/sht3xd/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0409ff65c6 --- /dev/null +++ b/tests/components/sht3xd/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht3xd + scl: 5 + sda: 4 + +sensor: + - platform: sht3xd + temperature: + name: SHT3XD Temperature + humidity: + name: SHT3XD Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht3xd/test.rp2040-ard.yaml b/tests/components/sht3xd/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0409ff65c6 --- /dev/null +++ b/tests/components/sht3xd/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht3xd + scl: 5 + sda: 4 + +sensor: + - platform: sht3xd + temperature: + name: SHT3XD Temperature + humidity: + name: SHT3XD Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht4x/test.esp32-ard.yaml b/tests/components/sht4x/test.esp32-ard.yaml new file mode 100644 index 0000000000..13ec524d7d --- /dev/null +++ b/tests/components/sht4x/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht4x + scl: 16 + sda: 17 + +sensor: + - platform: sht4x + temperature: + name: SHT4X Temperature + humidity: + name: SHT4X Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht4x/test.esp32-c3-ard.yaml b/tests/components/sht4x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0bcdd864f6 --- /dev/null +++ b/tests/components/sht4x/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht4x + scl: 5 + sda: 4 + +sensor: + - platform: sht4x + temperature: + name: SHT4X Temperature + humidity: + name: SHT4X Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht4x/test.esp32-c3-idf.yaml b/tests/components/sht4x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0bcdd864f6 --- /dev/null +++ b/tests/components/sht4x/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht4x + scl: 5 + sda: 4 + +sensor: + - platform: sht4x + temperature: + name: SHT4X Temperature + humidity: + name: SHT4X Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht4x/test.esp32-idf.yaml b/tests/components/sht4x/test.esp32-idf.yaml new file mode 100644 index 0000000000..13ec524d7d --- /dev/null +++ b/tests/components/sht4x/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht4x + scl: 16 + sda: 17 + +sensor: + - platform: sht4x + temperature: + name: SHT4X Temperature + humidity: + name: SHT4X Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht4x/test.esp8266-ard.yaml b/tests/components/sht4x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0bcdd864f6 --- /dev/null +++ b/tests/components/sht4x/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht4x + scl: 5 + sda: 4 + +sensor: + - platform: sht4x + temperature: + name: SHT4X Temperature + humidity: + name: SHT4X Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/sht4x/test.rp2040-ard.yaml b/tests/components/sht4x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0bcdd864f6 --- /dev/null +++ b/tests/components/sht4x/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_sht4x + scl: 5 + sda: 4 + +sensor: + - platform: sht4x + temperature: + name: SHT4X Temperature + humidity: + name: SHT4X Humidity + address: 0x44 + update_interval: 15s diff --git a/tests/components/shtcx/test.esp32-ard.yaml b/tests/components/shtcx/test.esp32-ard.yaml new file mode 100644 index 0000000000..619bac9548 --- /dev/null +++ b/tests/components/shtcx/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_shtcx + scl: 16 + sda: 17 + +sensor: + - platform: shtcx + temperature: + name: SHTCX Temperature + humidity: + name: SHTCX Humidity + address: 0x70 + update_interval: 15s diff --git a/tests/components/shtcx/test.esp32-c3-ard.yaml b/tests/components/shtcx/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c1c7a2a63f --- /dev/null +++ b/tests/components/shtcx/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_shtcx + scl: 5 + sda: 4 + +sensor: + - platform: shtcx + temperature: + name: SHTCX Temperature + humidity: + name: SHTCX Humidity + address: 0x70 + update_interval: 15s diff --git a/tests/components/shtcx/test.esp32-c3-idf.yaml b/tests/components/shtcx/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c1c7a2a63f --- /dev/null +++ b/tests/components/shtcx/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_shtcx + scl: 5 + sda: 4 + +sensor: + - platform: shtcx + temperature: + name: SHTCX Temperature + humidity: + name: SHTCX Humidity + address: 0x70 + update_interval: 15s diff --git a/tests/components/shtcx/test.esp32-idf.yaml b/tests/components/shtcx/test.esp32-idf.yaml new file mode 100644 index 0000000000..619bac9548 --- /dev/null +++ b/tests/components/shtcx/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_shtcx + scl: 16 + sda: 17 + +sensor: + - platform: shtcx + temperature: + name: SHTCX Temperature + humidity: + name: SHTCX Humidity + address: 0x70 + update_interval: 15s diff --git a/tests/components/shtcx/test.esp8266-ard.yaml b/tests/components/shtcx/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c1c7a2a63f --- /dev/null +++ b/tests/components/shtcx/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_shtcx + scl: 5 + sda: 4 + +sensor: + - platform: shtcx + temperature: + name: SHTCX Temperature + humidity: + name: SHTCX Humidity + address: 0x70 + update_interval: 15s diff --git a/tests/components/shtcx/test.rp2040-ard.yaml b/tests/components/shtcx/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c1c7a2a63f --- /dev/null +++ b/tests/components/shtcx/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_shtcx + scl: 5 + sda: 4 + +sensor: + - platform: shtcx + temperature: + name: SHTCX Temperature + humidity: + name: SHTCX Humidity + address: 0x70 + update_interval: 15s diff --git a/tests/components/shutdown/common.yaml b/tests/components/shutdown/common.yaml new file mode 100644 index 0000000000..f47e7da85d --- /dev/null +++ b/tests/components/shutdown/common.yaml @@ -0,0 +1,7 @@ +button: + - platform: shutdown + name: Shutdown Button + +switch: + - platform: shutdown + name: Shutdown Switch diff --git a/tests/components/shutdown/test.esp32-ard.yaml b/tests/components/shutdown/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/shutdown/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/shutdown/test.esp32-c3-ard.yaml b/tests/components/shutdown/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/shutdown/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/shutdown/test.esp32-c3-idf.yaml b/tests/components/shutdown/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/shutdown/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/shutdown/test.esp32-idf.yaml b/tests/components/shutdown/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/shutdown/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/shutdown/test.esp8266-ard.yaml b/tests/components/shutdown/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/shutdown/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/shutdown/test.rp2040-ard.yaml b/tests/components/shutdown/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/shutdown/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sigma_delta_output/common.yaml b/tests/components/sigma_delta_output/common.yaml new file mode 100644 index 0000000000..2a9a5d2c3b --- /dev/null +++ b/tests/components/sigma_delta_output/common.yaml @@ -0,0 +1,16 @@ +output: + - platform: sigma_delta_output + id: sddac + pin: 4 + turn_on_action: + then: + - logger.log: "Turned on" + turn_off_action: + then: + - logger.log: "Turned off" + state_change_action: + then: + - logger.log: + format: "Changed state: %d" + args: ["state"] + update_interval: 60s diff --git a/tests/components/sigma_delta_output/test.esp32-ard.yaml b/tests/components/sigma_delta_output/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sigma_delta_output/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sigma_delta_output/test.esp32-c3-ard.yaml b/tests/components/sigma_delta_output/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sigma_delta_output/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sigma_delta_output/test.esp32-c3-idf.yaml b/tests/components/sigma_delta_output/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sigma_delta_output/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sigma_delta_output/test.esp32-idf.yaml b/tests/components/sigma_delta_output/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sigma_delta_output/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sigma_delta_output/test.esp8266-ard.yaml b/tests/components/sigma_delta_output/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sigma_delta_output/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sigma_delta_output/test.rp2040-ard.yaml b/tests/components/sigma_delta_output/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sigma_delta_output/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sim800l/test.esp32-ard.yaml b/tests/components/sim800l/test.esp32-ard.yaml new file mode 100644 index 0000000000..c116548c6f --- /dev/null +++ b/tests/components/sim800l/test.esp32-ard.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - sim800l.send_sms: + recipient: '+15551234567' + message: Hello there + - sim800l.dial: + recipient: '+15551234567' + - sim800l.connect + - sim800l.disconnect + - sim800l.send_ussd: + ussd: test_ussd + +uart: + - id: uart_sim800l + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sim800l: + on_sms_received: + - lambda: |- + std::string str; + str = sender; + str = message; + - sim800l.send_sms: + message: hello you + recipient: "+1234" + - sim800l.dial: + recipient: "+1234" + on_incoming_call: + - logger.log: + format: "Incoming call from '%s'" + args: ["caller_id.c_str()"] + - sim800l.disconnect + on_ussd_received: + - logger.log: "ussd_received" diff --git a/tests/components/sim800l/test.esp32-c3-ard.yaml b/tests/components/sim800l/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7ff359d1e7 --- /dev/null +++ b/tests/components/sim800l/test.esp32-c3-ard.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - sim800l.send_sms: + recipient: '+15551234567' + message: Hello there + - sim800l.dial: + recipient: '+15551234567' + - sim800l.connect + - sim800l.disconnect + - sim800l.send_ussd: + ussd: test_ussd + +uart: + - id: uart_sim800l + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sim800l: + on_sms_received: + - lambda: |- + std::string str; + str = sender; + str = message; + - sim800l.send_sms: + message: hello you + recipient: "+1234" + - sim800l.dial: + recipient: "+1234" + on_incoming_call: + - logger.log: + format: "Incoming call from '%s'" + args: ["caller_id.c_str()"] + - sim800l.disconnect + on_ussd_received: + - logger.log: "ussd_received" diff --git a/tests/components/sim800l/test.esp32-c3-idf.yaml b/tests/components/sim800l/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7ff359d1e7 --- /dev/null +++ b/tests/components/sim800l/test.esp32-c3-idf.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - sim800l.send_sms: + recipient: '+15551234567' + message: Hello there + - sim800l.dial: + recipient: '+15551234567' + - sim800l.connect + - sim800l.disconnect + - sim800l.send_ussd: + ussd: test_ussd + +uart: + - id: uart_sim800l + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sim800l: + on_sms_received: + - lambda: |- + std::string str; + str = sender; + str = message; + - sim800l.send_sms: + message: hello you + recipient: "+1234" + - sim800l.dial: + recipient: "+1234" + on_incoming_call: + - logger.log: + format: "Incoming call from '%s'" + args: ["caller_id.c_str()"] + - sim800l.disconnect + on_ussd_received: + - logger.log: "ussd_received" diff --git a/tests/components/sim800l/test.esp32-idf.yaml b/tests/components/sim800l/test.esp32-idf.yaml new file mode 100644 index 0000000000..c116548c6f --- /dev/null +++ b/tests/components/sim800l/test.esp32-idf.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - sim800l.send_sms: + recipient: '+15551234567' + message: Hello there + - sim800l.dial: + recipient: '+15551234567' + - sim800l.connect + - sim800l.disconnect + - sim800l.send_ussd: + ussd: test_ussd + +uart: + - id: uart_sim800l + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sim800l: + on_sms_received: + - lambda: |- + std::string str; + str = sender; + str = message; + - sim800l.send_sms: + message: hello you + recipient: "+1234" + - sim800l.dial: + recipient: "+1234" + on_incoming_call: + - logger.log: + format: "Incoming call from '%s'" + args: ["caller_id.c_str()"] + - sim800l.disconnect + on_ussd_received: + - logger.log: "ussd_received" diff --git a/tests/components/sim800l/test.esp8266-ard.yaml b/tests/components/sim800l/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7ff359d1e7 --- /dev/null +++ b/tests/components/sim800l/test.esp8266-ard.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - sim800l.send_sms: + recipient: '+15551234567' + message: Hello there + - sim800l.dial: + recipient: '+15551234567' + - sim800l.connect + - sim800l.disconnect + - sim800l.send_ussd: + ussd: test_ussd + +uart: + - id: uart_sim800l + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sim800l: + on_sms_received: + - lambda: |- + std::string str; + str = sender; + str = message; + - sim800l.send_sms: + message: hello you + recipient: "+1234" + - sim800l.dial: + recipient: "+1234" + on_incoming_call: + - logger.log: + format: "Incoming call from '%s'" + args: ["caller_id.c_str()"] + - sim800l.disconnect + on_ussd_received: + - logger.log: "ussd_received" diff --git a/tests/components/sim800l/test.rp2040-ard.yaml b/tests/components/sim800l/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7ff359d1e7 --- /dev/null +++ b/tests/components/sim800l/test.rp2040-ard.yaml @@ -0,0 +1,37 @@ +esphome: + on_boot: + then: + - sim800l.send_sms: + recipient: '+15551234567' + message: Hello there + - sim800l.dial: + recipient: '+15551234567' + - sim800l.connect + - sim800l.disconnect + - sim800l.send_ussd: + ussd: test_ussd + +uart: + - id: uart_sim800l + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sim800l: + on_sms_received: + - lambda: |- + std::string str; + str = sender; + str = message; + - sim800l.send_sms: + message: hello you + recipient: "+1234" + - sim800l.dial: + recipient: "+1234" + on_incoming_call: + - logger.log: + format: "Incoming call from '%s'" + args: ["caller_id.c_str()"] + - sim800l.disconnect + on_ussd_received: + - logger.log: "ussd_received" diff --git a/tests/components/slow_pwm/common.yaml b/tests/components/slow_pwm/common.yaml new file mode 100644 index 0000000000..6bfb2f8ac5 --- /dev/null +++ b/tests/components/slow_pwm/common.yaml @@ -0,0 +1,6 @@ +output: + - platform: slow_pwm + id: test_slow_pwm + pin: 4 + period: 15s + restart_cycle_on_state_change: false diff --git a/tests/components/slow_pwm/test.esp32-ard.yaml b/tests/components/slow_pwm/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/slow_pwm/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp32-c3-ard.yaml b/tests/components/slow_pwm/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/slow_pwm/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp32-c3-idf.yaml b/tests/components/slow_pwm/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/slow_pwm/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp32-idf.yaml b/tests/components/slow_pwm/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/slow_pwm/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp8266-ard.yaml b/tests/components/slow_pwm/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/slow_pwm/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.rp2040-ard.yaml b/tests/components/slow_pwm/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/slow_pwm/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm16716/common.yaml b/tests/components/sm16716/common.yaml new file mode 100644 index 0000000000..3bf2712f4e --- /dev/null +++ b/tests/components/sm16716/common.yaml @@ -0,0 +1,16 @@ +sm16716: + clock_pin: 4 + data_pin: 5 + num_channels: 3 + num_chips: 1 + +output: + - platform: sm16716 + id: sm16716_red + channel: 1 + - platform: sm16716 + id: sm16716_green + channel: 0 + - platform: sm16716 + id: sm16716_blue + channel: 2 diff --git a/tests/components/sm16716/test.esp32-ard.yaml b/tests/components/sm16716/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm16716/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-c3-ard.yaml b/tests/components/sm16716/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm16716/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-c3-idf.yaml b/tests/components/sm16716/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm16716/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-idf.yaml b/tests/components/sm16716/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm16716/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp8266-ard.yaml b/tests/components/sm16716/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm16716/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm16716/test.rp2040-ard.yaml b/tests/components/sm16716/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm16716/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2135/common.yaml b/tests/components/sm2135/common.yaml new file mode 100644 index 0000000000..9a0de60839 --- /dev/null +++ b/tests/components/sm2135/common.yaml @@ -0,0 +1,22 @@ +sm2135: + clock_pin: 4 + data_pin: 5 + rgb_current: 20mA + cw_current: 60mA + +output: + - platform: sm2135 + id: sm2135_0 + channel: 0 + - platform: sm2135 + id: sm2135_1 + channel: 1 + - platform: sm2135 + id: sm2135_2 + channel: 2 + - platform: sm2135 + id: sm2135_3 + channel: 3 + - platform: sm2135 + id: sm2135_4 + channel: 4 diff --git a/tests/components/sm2135/test.esp32-ard.yaml b/tests/components/sm2135/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2135/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-c3-ard.yaml b/tests/components/sm2135/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2135/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-c3-idf.yaml b/tests/components/sm2135/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2135/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-idf.yaml b/tests/components/sm2135/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2135/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp8266-ard.yaml b/tests/components/sm2135/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2135/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2135/test.rp2040-ard.yaml b/tests/components/sm2135/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2135/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2235/common.yaml b/tests/components/sm2235/common.yaml new file mode 100644 index 0000000000..043d43d6f1 --- /dev/null +++ b/tests/components/sm2235/common.yaml @@ -0,0 +1,22 @@ +sm2235: + clock_pin: 4 + data_pin: 5 + max_power_color_channels: 9 + max_power_white_channels: 9 + +output: + - platform: sm2235 + id: sm2235_red + channel: 1 + - platform: sm2235 + id: sm2235_green + channel: 0 + - platform: sm2235 + id: sm2235_blue + channel: 2 + - platform: sm2235 + id: sm2235_coldwhite + channel: 4 + - platform: sm2235 + id: sm2235_warmwhite + channel: 3 diff --git a/tests/components/sm2235/test.esp32-ard.yaml b/tests/components/sm2235/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2235/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-c3-ard.yaml b/tests/components/sm2235/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2235/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-c3-idf.yaml b/tests/components/sm2235/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2235/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-idf.yaml b/tests/components/sm2235/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2235/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp8266-ard.yaml b/tests/components/sm2235/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2235/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2235/test.rp2040-ard.yaml b/tests/components/sm2235/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2235/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2335/common.yaml b/tests/components/sm2335/common.yaml new file mode 100644 index 0000000000..a5b2aedeb5 --- /dev/null +++ b/tests/components/sm2335/common.yaml @@ -0,0 +1,22 @@ +sm2335: + clock_pin: 4 + data_pin: 5 + max_power_color_channels: 9 + max_power_white_channels: 9 + +output: + - platform: sm2335 + id: sm2335_red + channel: 1 + - platform: sm2335 + id: sm2335_green + channel: 0 + - platform: sm2335 + id: sm2335_blue + channel: 2 + - platform: sm2335 + id: sm2335_coldwhite + channel: 4 + - platform: sm2335 + id: sm2335_warmwhite + channel: 3 diff --git a/tests/components/sm2335/test.esp32-ard.yaml b/tests/components/sm2335/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2335/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-c3-ard.yaml b/tests/components/sm2335/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2335/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-c3-idf.yaml b/tests/components/sm2335/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2335/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-idf.yaml b/tests/components/sm2335/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2335/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp8266-ard.yaml b/tests/components/sm2335/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2335/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm2335/test.rp2040-ard.yaml b/tests/components/sm2335/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sm2335/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sm300d2/test.esp32-ard.yaml b/tests/components/sm300d2/test.esp32-ard.yaml new file mode 100644 index 0000000000..92dba4fb3b --- /dev/null +++ b/tests/components/sm300d2/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sm300d2 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: sm300d2 + co2: + name: SM300D2 CO2 Value + formaldehyde: + name: SM300D2 Formaldehyde Value + tvoc: + name: SM300D2 TVOC Value + pm_2_5: + name: SM300D2 PM2.5 Value + pm_10_0: + name: SM300D2 PM10 Value + temperature: + name: SM300D2 Temperature Value + humidity: + name: SM300D2 Humidity Value + update_interval: 60s diff --git a/tests/components/sm300d2/test.esp32-c3-ard.yaml b/tests/components/sm300d2/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..bcd0a728b2 --- /dev/null +++ b/tests/components/sm300d2/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sm300d2 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: sm300d2 + co2: + name: SM300D2 CO2 Value + formaldehyde: + name: SM300D2 Formaldehyde Value + tvoc: + name: SM300D2 TVOC Value + pm_2_5: + name: SM300D2 PM2.5 Value + pm_10_0: + name: SM300D2 PM10 Value + temperature: + name: SM300D2 Temperature Value + humidity: + name: SM300D2 Humidity Value + update_interval: 60s diff --git a/tests/components/sm300d2/test.esp32-c3-idf.yaml b/tests/components/sm300d2/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..bcd0a728b2 --- /dev/null +++ b/tests/components/sm300d2/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sm300d2 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: sm300d2 + co2: + name: SM300D2 CO2 Value + formaldehyde: + name: SM300D2 Formaldehyde Value + tvoc: + name: SM300D2 TVOC Value + pm_2_5: + name: SM300D2 PM2.5 Value + pm_10_0: + name: SM300D2 PM10 Value + temperature: + name: SM300D2 Temperature Value + humidity: + name: SM300D2 Humidity Value + update_interval: 60s diff --git a/tests/components/sm300d2/test.esp32-idf.yaml b/tests/components/sm300d2/test.esp32-idf.yaml new file mode 100644 index 0000000000..92dba4fb3b --- /dev/null +++ b/tests/components/sm300d2/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sm300d2 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: sm300d2 + co2: + name: SM300D2 CO2 Value + formaldehyde: + name: SM300D2 Formaldehyde Value + tvoc: + name: SM300D2 TVOC Value + pm_2_5: + name: SM300D2 PM2.5 Value + pm_10_0: + name: SM300D2 PM10 Value + temperature: + name: SM300D2 Temperature Value + humidity: + name: SM300D2 Humidity Value + update_interval: 60s diff --git a/tests/components/sm300d2/test.esp8266-ard.yaml b/tests/components/sm300d2/test.esp8266-ard.yaml new file mode 100644 index 0000000000..bcd0a728b2 --- /dev/null +++ b/tests/components/sm300d2/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sm300d2 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: sm300d2 + co2: + name: SM300D2 CO2 Value + formaldehyde: + name: SM300D2 Formaldehyde Value + tvoc: + name: SM300D2 TVOC Value + pm_2_5: + name: SM300D2 PM2.5 Value + pm_10_0: + name: SM300D2 PM10 Value + temperature: + name: SM300D2 Temperature Value + humidity: + name: SM300D2 Humidity Value + update_interval: 60s diff --git a/tests/components/sm300d2/test.rp2040-ard.yaml b/tests/components/sm300d2/test.rp2040-ard.yaml new file mode 100644 index 0000000000..bcd0a728b2 --- /dev/null +++ b/tests/components/sm300d2/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +uart: + - id: uart_sm300d2 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: sm300d2 + co2: + name: SM300D2 CO2 Value + formaldehyde: + name: SM300D2 Formaldehyde Value + tvoc: + name: SM300D2 TVOC Value + pm_2_5: + name: SM300D2 PM2.5 Value + pm_10_0: + name: SM300D2 PM10 Value + temperature: + name: SM300D2 Temperature Value + humidity: + name: SM300D2 Humidity Value + update_interval: 60s diff --git a/tests/components/sml/test.esp32-ard.yaml b/tests/components/sml/test.esp32-ard.yaml new file mode 100644 index 0000000000..7217199380 --- /dev/null +++ b/tests/components/sml/test.esp32-ard.yaml @@ -0,0 +1,31 @@ +uart: + - id: uart_sml + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sml: + id: mysml + on_data: + - logger.log: "SML on_data" + +sensor: + - platform: sml + name: Total energy + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "1-0:1.8.0" + unit_of_measurement: kWh + accuracy_decimals: 1 + device_class: energy + state_class: total_increasing + filters: + - multiply: 0.0001 + +text_sensor: + - platform: sml + name: Manufacturer + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "129-129:199.130.3" + format: text diff --git a/tests/components/sml/test.esp32-c3-ard.yaml b/tests/components/sml/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..903f968c26 --- /dev/null +++ b/tests/components/sml/test.esp32-c3-ard.yaml @@ -0,0 +1,31 @@ +uart: + - id: uart_sml + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sml: + id: mysml + on_data: + - logger.log: "SML on_data" + +sensor: + - platform: sml + name: Total energy + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "1-0:1.8.0" + unit_of_measurement: kWh + accuracy_decimals: 1 + device_class: energy + state_class: total_increasing + filters: + - multiply: 0.0001 + +text_sensor: + - platform: sml + name: Manufacturer + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "129-129:199.130.3" + format: text diff --git a/tests/components/sml/test.esp32-c3-idf.yaml b/tests/components/sml/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..903f968c26 --- /dev/null +++ b/tests/components/sml/test.esp32-c3-idf.yaml @@ -0,0 +1,31 @@ +uart: + - id: uart_sml + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sml: + id: mysml + on_data: + - logger.log: "SML on_data" + +sensor: + - platform: sml + name: Total energy + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "1-0:1.8.0" + unit_of_measurement: kWh + accuracy_decimals: 1 + device_class: energy + state_class: total_increasing + filters: + - multiply: 0.0001 + +text_sensor: + - platform: sml + name: Manufacturer + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "129-129:199.130.3" + format: text diff --git a/tests/components/sml/test.esp32-idf.yaml b/tests/components/sml/test.esp32-idf.yaml new file mode 100644 index 0000000000..7217199380 --- /dev/null +++ b/tests/components/sml/test.esp32-idf.yaml @@ -0,0 +1,31 @@ +uart: + - id: uart_sml + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sml: + id: mysml + on_data: + - logger.log: "SML on_data" + +sensor: + - platform: sml + name: Total energy + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "1-0:1.8.0" + unit_of_measurement: kWh + accuracy_decimals: 1 + device_class: energy + state_class: total_increasing + filters: + - multiply: 0.0001 + +text_sensor: + - platform: sml + name: Manufacturer + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "129-129:199.130.3" + format: text diff --git a/tests/components/sml/test.esp8266-ard.yaml b/tests/components/sml/test.esp8266-ard.yaml new file mode 100644 index 0000000000..903f968c26 --- /dev/null +++ b/tests/components/sml/test.esp8266-ard.yaml @@ -0,0 +1,31 @@ +uart: + - id: uart_sml + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sml: + id: mysml + on_data: + - logger.log: "SML on_data" + +sensor: + - platform: sml + name: Total energy + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "1-0:1.8.0" + unit_of_measurement: kWh + accuracy_decimals: 1 + device_class: energy + state_class: total_increasing + filters: + - multiply: 0.0001 + +text_sensor: + - platform: sml + name: Manufacturer + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "129-129:199.130.3" + format: text diff --git a/tests/components/sml/test.rp2040-ard.yaml b/tests/components/sml/test.rp2040-ard.yaml new file mode 100644 index 0000000000..903f968c26 --- /dev/null +++ b/tests/components/sml/test.rp2040-ard.yaml @@ -0,0 +1,31 @@ +uart: + - id: uart_sml + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sml: + id: mysml + on_data: + - logger.log: "SML on_data" + +sensor: + - platform: sml + name: Total energy + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "1-0:1.8.0" + unit_of_measurement: kWh + accuracy_decimals: 1 + device_class: energy + state_class: total_increasing + filters: + - multiply: 0.0001 + +text_sensor: + - platform: sml + name: Manufacturer + sml_id: mysml + server_id: 0123456789abcdef + obis_code: "129-129:199.130.3" + format: text diff --git a/tests/components/smt100/test.esp32-ard.yaml b/tests/components/smt100/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c19f4bc45 --- /dev/null +++ b/tests/components/smt100/test.esp32-ard.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_smt100 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: smt100 + counts: + name: Counts + dielectric_constant: + name: Dielectric Constant + temperature: + name: Temperature + moisture: + name: Moisture + voltage: + name: Voltage + update_interval: 60s diff --git a/tests/components/smt100/test.esp32-c3-ard.yaml b/tests/components/smt100/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4277f2567b --- /dev/null +++ b/tests/components/smt100/test.esp32-c3-ard.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_smt100 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: smt100 + counts: + name: Counts + dielectric_constant: + name: Dielectric Constant + temperature: + name: Temperature + moisture: + name: Moisture + voltage: + name: Voltage + update_interval: 60s diff --git a/tests/components/smt100/test.esp32-c3-idf.yaml b/tests/components/smt100/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4277f2567b --- /dev/null +++ b/tests/components/smt100/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_smt100 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: smt100 + counts: + name: Counts + dielectric_constant: + name: Dielectric Constant + temperature: + name: Temperature + moisture: + name: Moisture + voltage: + name: Voltage + update_interval: 60s diff --git a/tests/components/smt100/test.esp32-idf.yaml b/tests/components/smt100/test.esp32-idf.yaml new file mode 100644 index 0000000000..7c19f4bc45 --- /dev/null +++ b/tests/components/smt100/test.esp32-idf.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_smt100 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +sensor: + - platform: smt100 + counts: + name: Counts + dielectric_constant: + name: Dielectric Constant + temperature: + name: Temperature + moisture: + name: Moisture + voltage: + name: Voltage + update_interval: 60s diff --git a/tests/components/smt100/test.esp8266-ard.yaml b/tests/components/smt100/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4277f2567b --- /dev/null +++ b/tests/components/smt100/test.esp8266-ard.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_smt100 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: smt100 + counts: + name: Counts + dielectric_constant: + name: Dielectric Constant + temperature: + name: Temperature + moisture: + name: Moisture + voltage: + name: Voltage + update_interval: 60s diff --git a/tests/components/smt100/test.rp2040-ard.yaml b/tests/components/smt100/test.rp2040-ard.yaml new file mode 100644 index 0000000000..4277f2567b --- /dev/null +++ b/tests/components/smt100/test.rp2040-ard.yaml @@ -0,0 +1,19 @@ +uart: + - id: uart_smt100 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +sensor: + - platform: smt100 + counts: + name: Counts + dielectric_constant: + name: Dielectric Constant + temperature: + name: Temperature + moisture: + name: Moisture + voltage: + name: Voltage + update_interval: 60s diff --git a/tests/components/sn74hc165/test.esp32-ard.yaml b/tests/components/sn74hc165/test.esp32-ard.yaml new file mode 100644 index 0000000000..55b06aec9b --- /dev/null +++ b/tests/components/sn74hc165/test.esp32-ard.yaml @@ -0,0 +1,14 @@ +sn74hc165: + id: sn74hc165_hub + clock_pin: 13 + data_pin: 14 + load_pin: 15 + clock_inhibit_pin: 16 + sr_count: 2 + +binary_sensor: + - platform: gpio + id: sn74hc165_pin_0 + pin: + sn74hc165: sn74hc165_hub + number: 0 diff --git a/tests/components/sn74hc165/test.esp32-c3-ard.yaml b/tests/components/sn74hc165/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f687b23c9d --- /dev/null +++ b/tests/components/sn74hc165/test.esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +sn74hc165: + id: sn74hc165_hub + clock_pin: 3 + data_pin: 4 + load_pin: 5 + clock_inhibit_pin: 6 + sr_count: 2 + +binary_sensor: + - platform: gpio + id: sn74hc165_pin_0 + pin: + sn74hc165: sn74hc165_hub + number: 0 diff --git a/tests/components/sn74hc165/test.esp32-c3-idf.yaml b/tests/components/sn74hc165/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f687b23c9d --- /dev/null +++ b/tests/components/sn74hc165/test.esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +sn74hc165: + id: sn74hc165_hub + clock_pin: 3 + data_pin: 4 + load_pin: 5 + clock_inhibit_pin: 6 + sr_count: 2 + +binary_sensor: + - platform: gpio + id: sn74hc165_pin_0 + pin: + sn74hc165: sn74hc165_hub + number: 0 diff --git a/tests/components/sn74hc165/test.esp32-idf.yaml b/tests/components/sn74hc165/test.esp32-idf.yaml new file mode 100644 index 0000000000..55b06aec9b --- /dev/null +++ b/tests/components/sn74hc165/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +sn74hc165: + id: sn74hc165_hub + clock_pin: 13 + data_pin: 14 + load_pin: 15 + clock_inhibit_pin: 16 + sr_count: 2 + +binary_sensor: + - platform: gpio + id: sn74hc165_pin_0 + pin: + sn74hc165: sn74hc165_hub + number: 0 diff --git a/tests/components/sn74hc165/test.esp8266-ard.yaml b/tests/components/sn74hc165/test.esp8266-ard.yaml new file mode 100644 index 0000000000..55b06aec9b --- /dev/null +++ b/tests/components/sn74hc165/test.esp8266-ard.yaml @@ -0,0 +1,14 @@ +sn74hc165: + id: sn74hc165_hub + clock_pin: 13 + data_pin: 14 + load_pin: 15 + clock_inhibit_pin: 16 + sr_count: 2 + +binary_sensor: + - platform: gpio + id: sn74hc165_pin_0 + pin: + sn74hc165: sn74hc165_hub + number: 0 diff --git a/tests/components/sn74hc165/test.rp2040-ard.yaml b/tests/components/sn74hc165/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f687b23c9d --- /dev/null +++ b/tests/components/sn74hc165/test.rp2040-ard.yaml @@ -0,0 +1,14 @@ +sn74hc165: + id: sn74hc165_hub + clock_pin: 3 + data_pin: 4 + load_pin: 5 + clock_inhibit_pin: 6 + sr_count: 2 + +binary_sensor: + - platform: gpio + id: sn74hc165_pin_0 + pin: + sn74hc165: sn74hc165_hub + number: 0 diff --git a/tests/components/sn74hc595/test.esp32-ard.yaml b/tests/components/sn74hc595/test.esp32-ard.yaml new file mode 100644 index 0000000000..f695395797 --- /dev/null +++ b/tests/components/sn74hc595/test.esp32-ard.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_sn74hc595 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sn74hc595: + - id: sn74hc595_hub + clock_pin: 12 + data_pin: 13 + latch_pin: 14 + oe_pin: 18 + sr_count: 2 + - id: sn74hc595_hub_2 + latch_pin: 21 + oe_pin: 22 + spi_id: spi_sn74hc595 + type: spi + sr_count: 2 + +switch: + - platform: gpio + name: SN74HC595 Pin 0 + pin: + sn74hc595: sn74hc595_hub_2 + number: 0 + inverted: false diff --git a/tests/components/sn74hc595/test.esp32-c3-ard.yaml b/tests/components/sn74hc595/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9b093899d3 --- /dev/null +++ b/tests/components/sn74hc595/test.esp32-c3-ard.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_sn74hc595 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sn74hc595: + - id: sn74hc595_hub + clock_pin: 0 + data_pin: 1 + latch_pin: 2 + oe_pin: 3 + sr_count: 2 + - id: sn74hc595_hub_2 + latch_pin: 8 + oe_pin: 9 + spi_id: spi_sn74hc595 + type: spi + sr_count: 2 + +switch: + - platform: gpio + name: SN74HC595 Pin 0 + pin: + sn74hc595: sn74hc595_hub_2 + number: 0 + inverted: false diff --git a/tests/components/sn74hc595/test.esp32-c3-idf.yaml b/tests/components/sn74hc595/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9b093899d3 --- /dev/null +++ b/tests/components/sn74hc595/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_sn74hc595 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +sn74hc595: + - id: sn74hc595_hub + clock_pin: 0 + data_pin: 1 + latch_pin: 2 + oe_pin: 3 + sr_count: 2 + - id: sn74hc595_hub_2 + latch_pin: 8 + oe_pin: 9 + spi_id: spi_sn74hc595 + type: spi + sr_count: 2 + +switch: + - platform: gpio + name: SN74HC595 Pin 0 + pin: + sn74hc595: sn74hc595_hub_2 + number: 0 + inverted: false diff --git a/tests/components/sn74hc595/test.esp32-idf.yaml b/tests/components/sn74hc595/test.esp32-idf.yaml new file mode 100644 index 0000000000..f695395797 --- /dev/null +++ b/tests/components/sn74hc595/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_sn74hc595 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +sn74hc595: + - id: sn74hc595_hub + clock_pin: 12 + data_pin: 13 + latch_pin: 14 + oe_pin: 18 + sr_count: 2 + - id: sn74hc595_hub_2 + latch_pin: 21 + oe_pin: 22 + spi_id: spi_sn74hc595 + type: spi + sr_count: 2 + +switch: + - platform: gpio + name: SN74HC595 Pin 0 + pin: + sn74hc595: sn74hc595_hub_2 + number: 0 + inverted: false diff --git a/tests/components/sn74hc595/test.esp8266-ard.yaml b/tests/components/sn74hc595/test.esp8266-ard.yaml new file mode 100644 index 0000000000..64bf5d1925 --- /dev/null +++ b/tests/components/sn74hc595/test.esp8266-ard.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_sn74hc595 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +sn74hc595: + - id: sn74hc595_hub + clock_pin: 0 + data_pin: 2 + latch_pin: 4 + oe_pin: 5 + sr_count: 2 + - id: sn74hc595_hub_2 + latch_pin: 15 + oe_pin: 16 + spi_id: spi_sn74hc595 + type: spi + sr_count: 2 + +switch: + - platform: gpio + name: SN74HC595 Pin 0 + pin: + sn74hc595: sn74hc595_hub_2 + number: 0 + inverted: false diff --git a/tests/components/sn74hc595/test.rp2040-ard.yaml b/tests/components/sn74hc595/test.rp2040-ard.yaml new file mode 100644 index 0000000000..de8e192659 --- /dev/null +++ b/tests/components/sn74hc595/test.rp2040-ard.yaml @@ -0,0 +1,27 @@ +spi: + - id: spi_sn74hc595 + clk_pin: 6 + mosi_pin: 5 + miso_pin: 4 + +sn74hc595: + - id: sn74hc595_hub + clock_pin: 0 + data_pin: 1 + latch_pin: 2 + oe_pin: 3 + sr_count: 2 + - id: sn74hc595_hub_2 + latch_pin: 8 + oe_pin: 9 + spi_id: spi_sn74hc595 + type: spi + sr_count: 2 + +switch: + - platform: gpio + name: SN74HC595 Pin 0 + pin: + sn74hc595: sn74hc595_hub_2 + number: 0 + inverted: false diff --git a/tests/components/sntp/common.yaml b/tests/components/sntp/common.yaml new file mode 100644 index 0000000000..3e9e465296 --- /dev/null +++ b/tests/components/sntp/common.yaml @@ -0,0 +1,15 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + on_time: + cron: "/30 0-30,30/5 * ? JAN-DEC MON,SAT-SUN,TUE-FRI" + then: + - lambda: 'ESP_LOGD("main", "time");' diff --git a/tests/components/sntp/test.bk72xx-ard.yaml b/tests/components/sntp/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-ard.yaml b/tests/components/sntp/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-c3-ard.yaml b/tests/components/sntp/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-c3-idf.yaml b/tests/components/sntp/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-idf.yaml b/tests/components/sntp/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sntp/test.esp8266-ard.yaml b/tests/components/sntp/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sntp/test.rp2040-ard.yaml b/tests/components/sntp/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sonoff_d1/test.esp32-ard.yaml b/tests/components/sonoff_d1/test.esp32-ard.yaml new file mode 100644 index 0000000000..dc35e3b6ac --- /dev/null +++ b/tests/components/sonoff_d1/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_sonoff_d1 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +light: + - platform: sonoff_d1 + id: d1_light + name: Sonoff D1 Dimmer + restore_mode: RESTORE_DEFAULT_OFF + use_rm433_remote: false diff --git a/tests/components/sonoff_d1/test.esp32-idf.yaml b/tests/components/sonoff_d1/test.esp32-idf.yaml new file mode 100644 index 0000000000..dc35e3b6ac --- /dev/null +++ b/tests/components/sonoff_d1/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_sonoff_d1 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +light: + - platform: sonoff_d1 + id: d1_light + name: Sonoff D1 Dimmer + restore_mode: RESTORE_DEFAULT_OFF + use_rm433_remote: false diff --git a/tests/components/sonoff_d1/test.esp8266-ard.yaml b/tests/components/sonoff_d1/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c4a62f4cb3 --- /dev/null +++ b/tests/components/sonoff_d1/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +uart: + - id: uart_sonoff_d1 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +light: + - platform: sonoff_d1 + id: d1_light + name: Sonoff D1 Dimmer + restore_mode: RESTORE_DEFAULT_OFF + use_rm433_remote: false diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml new file mode 100644 index 0000000000..396b4d95ea --- /dev/null +++ b/tests/components/speaker/test.esp32-ard.yaml @@ -0,0 +1,26 @@ +esphome: + on_boot: + then: + - speaker.mute_on: + - speaker.mute_off: + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - speaker.volume_set: 0.9 + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: + +i2s_audio: + i2s_lrclk_pin: 16 + i2s_bclk_pin: 17 + i2s_mclk_pin: 15 + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: 13 diff --git a/tests/components/speaker/test.esp32-c3-ard.yaml b/tests/components/speaker/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..636aeba766 --- /dev/null +++ b/tests/components/speaker/test.esp32-c3-ard.yaml @@ -0,0 +1,26 @@ +esphome: + on_boot: + then: + - speaker.mute_on: + - speaker.mute_off: + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - speaker.volume_set: 0.9 + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: + +i2s_audio: + i2s_lrclk_pin: 6 + i2s_bclk_pin: 7 + i2s_mclk_pin: 5 + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: 3 diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..636aeba766 --- /dev/null +++ b/tests/components/speaker/test.esp32-c3-idf.yaml @@ -0,0 +1,26 @@ +esphome: + on_boot: + then: + - speaker.mute_on: + - speaker.mute_off: + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - speaker.volume_set: 0.9 + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: + +i2s_audio: + i2s_lrclk_pin: 6 + i2s_bclk_pin: 7 + i2s_mclk_pin: 5 + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: 3 diff --git a/tests/components/speaker/test.esp32-idf.yaml b/tests/components/speaker/test.esp32-idf.yaml new file mode 100644 index 0000000000..b69440b133 --- /dev/null +++ b/tests/components/speaker/test.esp32-idf.yaml @@ -0,0 +1,35 @@ +esphome: + on_boot: + then: + - speaker.mute_on: + - speaker.mute_off: + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - speaker.volume_set: 0.9 + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: + +i2s_audio: + i2s_lrclk_pin: 16 + i2s_bclk_pin: 17 + i2s_mclk_pin: 15 + +i2c: + scl: 12 + sda: 10 + +audio_dac: + - platform: aic3204 + id: internal_dac + +speaker: + - platform: i2s_audio + id: speaker_with_audio_dac_id + audio_dac: internal_dac + dac_type: external + i2s_dout_pin: 14 diff --git a/tests/components/speed/test.esp32-ard.yaml b/tests/components/speed/test.esp32-ard.yaml new file mode 100644 index 0000000000..29a55e9edd --- /dev/null +++ b/tests/components/speed/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 diff --git a/tests/components/speed/test.esp32-c3-ard.yaml b/tests/components/speed/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fa1920676e --- /dev/null +++ b/tests/components/speed/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 2 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 diff --git a/tests/components/speed/test.esp32-c3-idf.yaml b/tests/components/speed/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fa1920676e --- /dev/null +++ b/tests/components/speed/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 2 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 diff --git a/tests/components/speed/test.esp32-idf.yaml b/tests/components/speed/test.esp32-idf.yaml new file mode 100644 index 0000000000..29a55e9edd --- /dev/null +++ b/tests/components/speed/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +output: + - platform: ledc + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 diff --git a/tests/components/speed/test.esp8266-ard.yaml b/tests/components/speed/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6ed9949cf5 --- /dev/null +++ b/tests/components/speed/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +output: + - platform: esp8266_pwm + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 diff --git a/tests/components/speed/test.rp2040-ard.yaml b/tests/components/speed/test.rp2040-ard.yaml new file mode 100644 index 0000000000..02b572db75 --- /dev/null +++ b/tests/components/speed/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +output: + - platform: rp2040_pwm + id: fan_output_1 + pin: 12 + +fan: + - platform: speed + id: fan_speed + output: fan_output_1 diff --git a/tests/components/spi/test.esp32-ard.yaml b/tests/components/spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..1cdcf461dd --- /dev/null +++ b/tests/components/spi/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +spi: + - id: spi_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 diff --git a/tests/components/spi/test.esp32-c3-ard.yaml b/tests/components/spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f49470ad07 --- /dev/null +++ b/tests/components/spi/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +spi: + - id: spi_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 diff --git a/tests/components/spi/test.esp32-c3-idf.yaml b/tests/components/spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f49470ad07 --- /dev/null +++ b/tests/components/spi/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +spi: + - id: spi_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 diff --git a/tests/components/spi/test.esp32-idf.yaml b/tests/components/spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..1cdcf461dd --- /dev/null +++ b/tests/components/spi/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +spi: + - id: spi_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 diff --git a/tests/components/spi/test.esp32-s3-idf.yaml b/tests/components/spi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..8db934023a --- /dev/null +++ b/tests/components/spi/test.esp32-s3-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_id_1 + type: single + clk_pin: + number: GPIO0 + ignore_strapping_warning: true + allow_other_uses: false + mosi_pin: GPIO6 + interface: hardware + - id: quad_spi + type: quad + clk_pin: 47 + interface: spi3 + data_pins: + - number: 40 + allow_other_uses: false + - 41 + - 42 + - 43 + - id: spi_id_3 + clk_pin: 8 + mosi_pin: 9 + interface: any + diff --git a/tests/components/spi/test.esp8266-ard.yaml b/tests/components/spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..83f110921f --- /dev/null +++ b/tests/components/spi/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +spi: + - id: spi_spi + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 diff --git a/tests/components/spi/test.rp2040-ard.yaml b/tests/components/spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..1e39d247fe --- /dev/null +++ b/tests/components/spi/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +spi: + - id: spi_spi + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 diff --git a/tests/components/spi_device/test.esp32-ard.yaml b/tests/components/spi_device/test.esp32-ard.yaml new file mode 100644 index 0000000000..b539cb3ec4 --- /dev/null +++ b/tests/components/spi_device/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_device1 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +spi_device: + id: spi_device_test + data_rate: 2MHz + spi_mode: 3 + bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-c3-ard.yaml b/tests/components/spi_device/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..99c0ac1ebb --- /dev/null +++ b/tests/components/spi_device/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_device1 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +spi_device: + id: spi_device_test + data_rate: 2MHz + spi_mode: 3 + bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-c3-idf.yaml b/tests/components/spi_device/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..99c0ac1ebb --- /dev/null +++ b/tests/components/spi_device/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_device1 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +spi_device: + id: spi_device_test + data_rate: 2MHz + spi_mode: 3 + bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-idf.yaml b/tests/components/spi_device/test.esp32-idf.yaml new file mode 100644 index 0000000000..b539cb3ec4 --- /dev/null +++ b/tests/components/spi_device/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_device1 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +spi_device: + id: spi_device_test + data_rate: 2MHz + spi_mode: 3 + bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp8266-ard.yaml b/tests/components/spi_device/test.esp8266-ard.yaml new file mode 100644 index 0000000000..988825ce2d --- /dev/null +++ b/tests/components/spi_device/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_device1 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +spi_device: + id: spi_device_test + data_rate: 2MHz + spi_mode: 3 + bit_order: lsb_first diff --git a/tests/components/spi_device/test.rp2040-ard.yaml b/tests/components/spi_device/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6020643f21 --- /dev/null +++ b/tests/components/spi_device/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +spi: + - id: spi_device1 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +spi_device: + id: spi_device_test + data_rate: 2MHz + spi_mode: 3 + bit_order: lsb_first diff --git a/tests/components/spi_led_strip/test.esp32-ard.yaml b/tests/components/spi_led_strip/test.esp32-ard.yaml new file mode 100644 index 0000000000..f4a760bf4c --- /dev/null +++ b/tests/components/spi_led_strip/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_spi_led_strip + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +light: + - platform: spi_led_strip + num_leds: 4 + color_correct: [80%, 60%, 100%] + id: rgb_led + name: "RGB LED" + data_rate: 8MHz diff --git a/tests/components/spi_led_strip/test.esp32-c3-ard.yaml b/tests/components/spi_led_strip/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..983ad2863f --- /dev/null +++ b/tests/components/spi_led_strip/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_spi_led_strip + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +light: + - platform: spi_led_strip + num_leds: 4 + color_correct: [80%, 60%, 100%] + id: rgb_led + name: "RGB LED" + data_rate: 8MHz diff --git a/tests/components/spi_led_strip/test.esp32-c3-idf.yaml b/tests/components/spi_led_strip/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..983ad2863f --- /dev/null +++ b/tests/components/spi_led_strip/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_spi_led_strip + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +light: + - platform: spi_led_strip + num_leds: 4 + color_correct: [80%, 60%, 100%] + id: rgb_led + name: "RGB LED" + data_rate: 8MHz diff --git a/tests/components/spi_led_strip/test.esp32-idf.yaml b/tests/components/spi_led_strip/test.esp32-idf.yaml new file mode 100644 index 0000000000..f4a760bf4c --- /dev/null +++ b/tests/components/spi_led_strip/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_spi_led_strip + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +light: + - platform: spi_led_strip + num_leds: 4 + color_correct: [80%, 60%, 100%] + id: rgb_led + name: "RGB LED" + data_rate: 8MHz diff --git a/tests/components/spi_led_strip/test.esp8266-ard.yaml b/tests/components/spi_led_strip/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8e76303b6a --- /dev/null +++ b/tests/components/spi_led_strip/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_spi_led_strip + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +light: + - platform: spi_led_strip + num_leds: 4 + color_correct: [80%, 60%, 100%] + id: rgb_led + name: "RGB LED" + data_rate: 8MHz diff --git a/tests/components/spi_led_strip/test.rp2040-ard.yaml b/tests/components/spi_led_strip/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9d12f1592b --- /dev/null +++ b/tests/components/spi_led_strip/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: spi_spi_led_strip + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +light: + - platform: spi_led_strip + num_leds: 4 + color_correct: [80%, 60%, 100%] + id: rgb_led + name: "RGB LED" + data_rate: 8MHz diff --git a/tests/components/sprinkler/common.yaml b/tests/components/sprinkler/common.yaml new file mode 100644 index 0000000000..f099f77729 --- /dev/null +++ b/tests/components/sprinkler/common.yaml @@ -0,0 +1,83 @@ +esphome: + on_boot: + then: + - sprinkler.start_full_cycle: yard_sprinkler_ctrlr + - sprinkler.start_from_queue: yard_sprinkler_ctrlr + - sprinkler.start_single_valve: + id: yard_sprinkler_ctrlr + valve_number: 0 + run_duration: 600s + - sprinkler.shutdown: yard_sprinkler_ctrlr + - sprinkler.next_valve: yard_sprinkler_ctrlr + - sprinkler.previous_valve: yard_sprinkler_ctrlr + - sprinkler.pause: yard_sprinkler_ctrlr + - sprinkler.resume: yard_sprinkler_ctrlr + - sprinkler.resume_or_start_full_cycle: yard_sprinkler_ctrlr + - sprinkler.queue_valve: + id: yard_sprinkler_ctrlr + valve_number: 2 + run_duration: 900s + - sprinkler.clear_queued_valves: yard_sprinkler_ctrlr + - sprinkler.set_multiplier: + id: yard_sprinkler_ctrlr + multiplier: 1.5 + - sprinkler.set_repeat: + id: yard_sprinkler_ctrlr + repeat: 2 + - sprinkler.set_divider: + id: yard_sprinkler_ctrlr + divider: 2 + - sprinkler.set_valve_run_duration: + id: yard_sprinkler_ctrlr + valve_number: 0 + run_duration: 600s + +switch: + - platform: template + id: switch1 + optimistic: true + - platform: template + id: switch2 + optimistic: true + +sprinkler: + - id: yard_sprinkler_ctrlr + main_switch: Yard Sprinklers + auto_advance_switch: Yard Sprinklers Auto Advance + reverse_switch: Yard Sprinklers Reverse + pump_start_pump_delay: 2s + pump_stop_valve_delay: 4s + pump_switch_off_during_valve_open_delay: true + valve_open_delay: 5s + valves: + - valve_switch: Yard Valve 0 + enable_switch: Enable Yard Valve 0 + pump_switch_id: switch1 + run_duration: 10s + valve_switch_id: switch2 + - valve_switch: Yard Valve 1 + enable_switch: Enable Yard Valve 1 + pump_switch_id: switch1 + run_duration: 10s + valve_switch_id: switch2 + - valve_switch: Yard Valve 2 + enable_switch: Enable Yard Valve 2 + pump_switch_id: switch1 + run_duration: 10s + valve_switch_id: switch2 + - id: garden_sprinkler_ctrlr + main_switch: Garden Sprinklers + auto_advance_switch: Garden Sprinklers Auto Advance + reverse_switch: Garden Sprinklers Reverse + valve_overlap: 5s + valves: + - valve_switch: Garden Valve 0 + enable_switch: Enable Garden Valve 0 + pump_switch_id: switch1 + run_duration: 10s + valve_switch_id: switch2 + - valve_switch: Garden Valve 1 + enable_switch: Enable Garden Valve 1 + pump_switch_id: switch1 + run_duration: 10s + valve_switch_id: switch2 diff --git a/tests/components/sprinkler/test.esp32-ard.yaml b/tests/components/sprinkler/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sprinkler/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp32-c3-ard.yaml b/tests/components/sprinkler/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sprinkler/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp32-c3-idf.yaml b/tests/components/sprinkler/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sprinkler/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp32-idf.yaml b/tests/components/sprinkler/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sprinkler/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp8266-ard.yaml b/tests/components/sprinkler/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sprinkler/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sprinkler/test.rp2040-ard.yaml b/tests/components/sprinkler/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sprinkler/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sps30/test.esp32-ard.yaml b/tests/components/sps30/test.esp32-ard.yaml new file mode 100644 index 0000000000..f9d1ee4e55 --- /dev/null +++ b/tests/components/sps30/test.esp32-ard.yaml @@ -0,0 +1,36 @@ +i2c: + - id: i2c_sps30 + scl: 16 + sda: 17 + +sensor: + - platform: sps30 + pm_1_0: + name: Workshop PM <1µm Weight concentration + id: workshop_PM_1_0 + pm_2_5: + name: Workshop PM <2.5µm Weight concentration + id: workshop_PM_2_5 + pm_4_0: + name: Workshop PM <4µm Weight concentration + id: workshop_PM_4_0 + pm_10_0: + name: Workshop PM <10µm Weight concentration + id: workshop_PM_10_0 + pmc_0_5: + name: Workshop PM <0.5µm Number concentration + id: workshop_PMC_0_5 + pmc_1_0: + name: Workshop PM <1µm Number concentration + id: workshop_PMC_1_0 + pmc_2_5: + name: Workshop PM <2.5µm Number concentration + id: workshop_PMC_2_5 + pmc_4_0: + name: Workshop PM <4µm Number concentration + id: workshop_PMC_4_0 + pmc_10_0: + name: Workshop PM <10µm Number concentration + id: workshop_PMC_10_0 + address: 0x69 + update_interval: 10s diff --git a/tests/components/sps30/test.esp32-c3-ard.yaml b/tests/components/sps30/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e071a00936 --- /dev/null +++ b/tests/components/sps30/test.esp32-c3-ard.yaml @@ -0,0 +1,36 @@ +i2c: + - id: i2c_sps30 + scl: 5 + sda: 4 + +sensor: + - platform: sps30 + pm_1_0: + name: Workshop PM <1µm Weight concentration + id: workshop_PM_1_0 + pm_2_5: + name: Workshop PM <2.5µm Weight concentration + id: workshop_PM_2_5 + pm_4_0: + name: Workshop PM <4µm Weight concentration + id: workshop_PM_4_0 + pm_10_0: + name: Workshop PM <10µm Weight concentration + id: workshop_PM_10_0 + pmc_0_5: + name: Workshop PM <0.5µm Number concentration + id: workshop_PMC_0_5 + pmc_1_0: + name: Workshop PM <1µm Number concentration + id: workshop_PMC_1_0 + pmc_2_5: + name: Workshop PM <2.5µm Number concentration + id: workshop_PMC_2_5 + pmc_4_0: + name: Workshop PM <4µm Number concentration + id: workshop_PMC_4_0 + pmc_10_0: + name: Workshop PM <10µm Number concentration + id: workshop_PMC_10_0 + address: 0x69 + update_interval: 10s diff --git a/tests/components/sps30/test.esp32-c3-idf.yaml b/tests/components/sps30/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e071a00936 --- /dev/null +++ b/tests/components/sps30/test.esp32-c3-idf.yaml @@ -0,0 +1,36 @@ +i2c: + - id: i2c_sps30 + scl: 5 + sda: 4 + +sensor: + - platform: sps30 + pm_1_0: + name: Workshop PM <1µm Weight concentration + id: workshop_PM_1_0 + pm_2_5: + name: Workshop PM <2.5µm Weight concentration + id: workshop_PM_2_5 + pm_4_0: + name: Workshop PM <4µm Weight concentration + id: workshop_PM_4_0 + pm_10_0: + name: Workshop PM <10µm Weight concentration + id: workshop_PM_10_0 + pmc_0_5: + name: Workshop PM <0.5µm Number concentration + id: workshop_PMC_0_5 + pmc_1_0: + name: Workshop PM <1µm Number concentration + id: workshop_PMC_1_0 + pmc_2_5: + name: Workshop PM <2.5µm Number concentration + id: workshop_PMC_2_5 + pmc_4_0: + name: Workshop PM <4µm Number concentration + id: workshop_PMC_4_0 + pmc_10_0: + name: Workshop PM <10µm Number concentration + id: workshop_PMC_10_0 + address: 0x69 + update_interval: 10s diff --git a/tests/components/sps30/test.esp32-idf.yaml b/tests/components/sps30/test.esp32-idf.yaml new file mode 100644 index 0000000000..f9d1ee4e55 --- /dev/null +++ b/tests/components/sps30/test.esp32-idf.yaml @@ -0,0 +1,36 @@ +i2c: + - id: i2c_sps30 + scl: 16 + sda: 17 + +sensor: + - platform: sps30 + pm_1_0: + name: Workshop PM <1µm Weight concentration + id: workshop_PM_1_0 + pm_2_5: + name: Workshop PM <2.5µm Weight concentration + id: workshop_PM_2_5 + pm_4_0: + name: Workshop PM <4µm Weight concentration + id: workshop_PM_4_0 + pm_10_0: + name: Workshop PM <10µm Weight concentration + id: workshop_PM_10_0 + pmc_0_5: + name: Workshop PM <0.5µm Number concentration + id: workshop_PMC_0_5 + pmc_1_0: + name: Workshop PM <1µm Number concentration + id: workshop_PMC_1_0 + pmc_2_5: + name: Workshop PM <2.5µm Number concentration + id: workshop_PMC_2_5 + pmc_4_0: + name: Workshop PM <4µm Number concentration + id: workshop_PMC_4_0 + pmc_10_0: + name: Workshop PM <10µm Number concentration + id: workshop_PMC_10_0 + address: 0x69 + update_interval: 10s diff --git a/tests/components/sps30/test.esp8266-ard.yaml b/tests/components/sps30/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e071a00936 --- /dev/null +++ b/tests/components/sps30/test.esp8266-ard.yaml @@ -0,0 +1,36 @@ +i2c: + - id: i2c_sps30 + scl: 5 + sda: 4 + +sensor: + - platform: sps30 + pm_1_0: + name: Workshop PM <1µm Weight concentration + id: workshop_PM_1_0 + pm_2_5: + name: Workshop PM <2.5µm Weight concentration + id: workshop_PM_2_5 + pm_4_0: + name: Workshop PM <4µm Weight concentration + id: workshop_PM_4_0 + pm_10_0: + name: Workshop PM <10µm Weight concentration + id: workshop_PM_10_0 + pmc_0_5: + name: Workshop PM <0.5µm Number concentration + id: workshop_PMC_0_5 + pmc_1_0: + name: Workshop PM <1µm Number concentration + id: workshop_PMC_1_0 + pmc_2_5: + name: Workshop PM <2.5µm Number concentration + id: workshop_PMC_2_5 + pmc_4_0: + name: Workshop PM <4µm Number concentration + id: workshop_PMC_4_0 + pmc_10_0: + name: Workshop PM <10µm Number concentration + id: workshop_PMC_10_0 + address: 0x69 + update_interval: 10s diff --git a/tests/components/sps30/test.rp2040-ard.yaml b/tests/components/sps30/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e071a00936 --- /dev/null +++ b/tests/components/sps30/test.rp2040-ard.yaml @@ -0,0 +1,36 @@ +i2c: + - id: i2c_sps30 + scl: 5 + sda: 4 + +sensor: + - platform: sps30 + pm_1_0: + name: Workshop PM <1µm Weight concentration + id: workshop_PM_1_0 + pm_2_5: + name: Workshop PM <2.5µm Weight concentration + id: workshop_PM_2_5 + pm_4_0: + name: Workshop PM <4µm Weight concentration + id: workshop_PM_4_0 + pm_10_0: + name: Workshop PM <10µm Weight concentration + id: workshop_PM_10_0 + pmc_0_5: + name: Workshop PM <0.5µm Number concentration + id: workshop_PMC_0_5 + pmc_1_0: + name: Workshop PM <1µm Number concentration + id: workshop_PMC_1_0 + pmc_2_5: + name: Workshop PM <2.5µm Number concentration + id: workshop_PMC_2_5 + pmc_4_0: + name: Workshop PM <4µm Number concentration + id: workshop_PMC_4_0 + pmc_10_0: + name: Workshop PM <10µm Number concentration + id: workshop_PMC_10_0 + address: 0x69 + update_interval: 10s diff --git a/tests/components/ssd1306_i2c/test.esp32-ard.yaml b/tests/components/ssd1306_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..dddc67309c --- /dev/null +++ b/tests/components/ssd1306_i2c/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_ssd1306_i2c + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + model: SSD1306_128X64 + reset_pin: 3 + address: 0x3C + id: display1 + contrast: 60% + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1306_i2c/test.esp32-c3-ard.yaml b/tests/components/ssd1306_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..f4a301db51 --- /dev/null +++ b/tests/components/ssd1306_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_ssd1306_i2c + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + model: SSD1306_128X64 + reset_pin: 3 + address: 0x3C + id: display1 + contrast: 60% + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml b/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..f4a301db51 --- /dev/null +++ b/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_ssd1306_i2c + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + model: SSD1306_128X64 + reset_pin: 3 + address: 0x3C + id: display1 + contrast: 60% + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1306_i2c/test.esp32-idf.yaml b/tests/components/ssd1306_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..dddc67309c --- /dev/null +++ b/tests/components/ssd1306_i2c/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_ssd1306_i2c + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + model: SSD1306_128X64 + reset_pin: 3 + address: 0x3C + id: display1 + contrast: 60% + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1306_i2c/test.esp8266-ard.yaml b/tests/components/ssd1306_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f4a301db51 --- /dev/null +++ b/tests/components/ssd1306_i2c/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_ssd1306_i2c + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + model: SSD1306_128X64 + reset_pin: 3 + address: 0x3C + id: display1 + contrast: 60% + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1306_i2c/test.rp2040-ard.yaml b/tests/components/ssd1306_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f4a301db51 --- /dev/null +++ b/tests/components/ssd1306_i2c/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_ssd1306_i2c + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + model: SSD1306_128X64 + reset_pin: 3 + address: 0x3C + id: display1 + contrast: 60% + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1306_spi/test.esp32-ard.yaml b/tests/components/ssd1306_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..b0e5e0f1a2 --- /dev/null +++ b/tests/components/ssd1306_spi/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1306_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1306_spi + model: SSD1306 128x64 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1306_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1306_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..01b2d0e4a8 --- /dev/null +++ b/tests/components/ssd1306_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1306_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1306_spi + model: SSD1306 128x64 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..01b2d0e4a8 --- /dev/null +++ b/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1306_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1306_spi + model: SSD1306 128x64 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1306_spi/test.esp32-idf.yaml b/tests/components/ssd1306_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..b0e5e0f1a2 --- /dev/null +++ b/tests/components/ssd1306_spi/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1306_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1306_spi + model: SSD1306 128x64 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1306_spi/test.esp8266-ard.yaml b/tests/components/ssd1306_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..135e364bb2 --- /dev/null +++ b/tests/components/ssd1306_spi/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1306_spi + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ssd1306_spi + model: SSD1306 128x64 + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + 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"); diff --git a/tests/components/ssd1306_spi/test.rp2040-ard.yaml b/tests/components/ssd1306_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..94c4b85158 --- /dev/null +++ b/tests/components/ssd1306_spi/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1306_spi + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ssd1306_spi + model: SSD1306 128x64 + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + 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"); diff --git a/tests/components/ssd1322_spi/test.esp32-ard.yaml b/tests/components/ssd1322_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..aa6d0fbf01 --- /dev/null +++ b/tests/components/ssd1322_spi/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1322_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1322_spi + model: SSD1322 256x64 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1322_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1322_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4fa9f86594 --- /dev/null +++ b/tests/components/ssd1322_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1322_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1322_spi + model: SSD1322 256x64 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4fa9f86594 --- /dev/null +++ b/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1322_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1322_spi + model: SSD1322 256x64 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1322_spi/test.esp32-idf.yaml b/tests/components/ssd1322_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..aa6d0fbf01 --- /dev/null +++ b/tests/components/ssd1322_spi/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1322_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1322_spi + model: SSD1322 256x64 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1322_spi/test.esp8266-ard.yaml b/tests/components/ssd1322_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a5aa565c09 --- /dev/null +++ b/tests/components/ssd1322_spi/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1322_spi + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ssd1322_spi + model: SSD1322 256x64 + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + 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"); diff --git a/tests/components/ssd1322_spi/test.rp2040-ard.yaml b/tests/components/ssd1322_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..59544e7878 --- /dev/null +++ b/tests/components/ssd1322_spi/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1322_spi + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ssd1322_spi + model: SSD1322 256x64 + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + 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"); diff --git a/tests/components/ssd1325_spi/test.esp32-ard.yaml b/tests/components/ssd1325_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..84d94eff28 --- /dev/null +++ b/tests/components/ssd1325_spi/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1325_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1325_spi + model: SSD1325 128x64 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1325_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1325_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0fa8cb6488 --- /dev/null +++ b/tests/components/ssd1325_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1325_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1325_spi + model: SSD1325 128x64 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0fa8cb6488 --- /dev/null +++ b/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1325_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1325_spi + model: SSD1325 128x64 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1325_spi/test.esp32-idf.yaml b/tests/components/ssd1325_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..84d94eff28 --- /dev/null +++ b/tests/components/ssd1325_spi/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1325_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1325_spi + model: SSD1325 128x64 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1325_spi/test.esp8266-ard.yaml b/tests/components/ssd1325_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9d7e483585 --- /dev/null +++ b/tests/components/ssd1325_spi/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1325_spi + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ssd1325_spi + model: SSD1325 128x64 + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + 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"); diff --git a/tests/components/ssd1325_spi/test.rp2040-ard.yaml b/tests/components/ssd1325_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..869663c19d --- /dev/null +++ b/tests/components/ssd1325_spi/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1325_spi + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ssd1325_spi + model: SSD1325 128x64 + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + 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"); diff --git a/tests/components/ssd1327_i2c/test.esp32-ard.yaml b/tests/components/ssd1327_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..e72a9c7b7a --- /dev/null +++ b/tests/components/ssd1327_i2c/test.esp32-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ssd1327_i2c + scl: 16 + sda: 17 + +display: + - platform: ssd1327_i2c + model: SSD1327_128x128 + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1327_i2c/test.esp32-c3-ard.yaml b/tests/components/ssd1327_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..cd28795ff1 --- /dev/null +++ b/tests/components/ssd1327_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ssd1327_i2c + scl: 5 + sda: 4 + +display: + - platform: ssd1327_i2c + model: SSD1327_128x128 + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml b/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..cd28795ff1 --- /dev/null +++ b/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ssd1327_i2c + scl: 5 + sda: 4 + +display: + - platform: ssd1327_i2c + model: SSD1327_128x128 + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1327_i2c/test.esp32-idf.yaml b/tests/components/ssd1327_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..e72a9c7b7a --- /dev/null +++ b/tests/components/ssd1327_i2c/test.esp32-idf.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ssd1327_i2c + scl: 16 + sda: 17 + +display: + - platform: ssd1327_i2c + model: SSD1327_128x128 + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1327_i2c/test.esp8266-ard.yaml b/tests/components/ssd1327_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..cd28795ff1 --- /dev/null +++ b/tests/components/ssd1327_i2c/test.esp8266-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ssd1327_i2c + scl: 5 + sda: 4 + +display: + - platform: ssd1327_i2c + model: SSD1327_128x128 + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1327_i2c/test.rp2040-ard.yaml b/tests/components/ssd1327_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..cd28795ff1 --- /dev/null +++ b/tests/components/ssd1327_i2c/test.rp2040-ard.yaml @@ -0,0 +1,24 @@ +i2c: + - id: i2c_ssd1327_i2c + scl: 5 + sda: 4 + +display: + - platform: ssd1327_i2c + model: SSD1327_128x128 + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1327_spi/test.esp32-ard.yaml b/tests/components/ssd1327_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..e103ded187 --- /dev/null +++ b/tests/components/ssd1327_spi/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1327_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1327_spi + model: SSD1327 128x128 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1327_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1327_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ec5795d716 --- /dev/null +++ b/tests/components/ssd1327_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1327_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1327_spi + model: SSD1327 128x128 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ec5795d716 --- /dev/null +++ b/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1327_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1327_spi + model: SSD1327 128x128 + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1327_spi/test.esp32-idf.yaml b/tests/components/ssd1327_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..e103ded187 --- /dev/null +++ b/tests/components/ssd1327_spi/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1327_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1327_spi + model: SSD1327 128x128 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1327_spi/test.esp8266-ard.yaml b/tests/components/ssd1327_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..30455d24ee --- /dev/null +++ b/tests/components/ssd1327_spi/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1327_spi + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ssd1327_spi + model: SSD1327 128x128 + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + 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"); diff --git a/tests/components/ssd1327_spi/test.rp2040-ard.yaml b/tests/components/ssd1327_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f819ab2c41 --- /dev/null +++ b/tests/components/ssd1327_spi/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1327_spi + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ssd1327_spi + model: SSD1327 128x128 + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + 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"); diff --git a/tests/components/ssd1331_spi/test.esp32-ard.yaml b/tests/components/ssd1331_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..e9eb8ea9ad --- /dev/null +++ b/tests/components/ssd1331_spi/test.esp32-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_ssd1331_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1331_spi + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1331_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1331_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9a35918faf --- /dev/null +++ b/tests/components/ssd1331_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_ssd1331_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1331_spi + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9a35918faf --- /dev/null +++ b/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_ssd1331_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1331_spi + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1331_spi/test.esp32-idf.yaml b/tests/components/ssd1331_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..e9eb8ea9ad --- /dev/null +++ b/tests/components/ssd1331_spi/test.esp32-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_ssd1331_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1331_spi + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1331_spi/test.esp8266-ard.yaml b/tests/components/ssd1331_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3b319ef38e --- /dev/null +++ b/tests/components/ssd1331_spi/test.esp8266-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_ssd1331_spi + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ssd1331_spi + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + 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"); diff --git a/tests/components/ssd1331_spi/test.rp2040-ard.yaml b/tests/components/ssd1331_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..947685b07a --- /dev/null +++ b/tests/components/ssd1331_spi/test.rp2040-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_ssd1331_spi + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ssd1331_spi + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + 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"); diff --git a/tests/components/ssd1351_spi/test.esp32-ard.yaml b/tests/components/ssd1351_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..8342cb972b --- /dev/null +++ b/tests/components/ssd1351_spi/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1351_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1351_spi + model: "SSD1351 128x128" + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1351_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1351_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2a7266f502 --- /dev/null +++ b/tests/components/ssd1351_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1351_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1351_spi + model: "SSD1351 128x128" + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2a7266f502 --- /dev/null +++ b/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1351_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ssd1351_spi + model: "SSD1351 128x128" + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/ssd1351_spi/test.esp32-idf.yaml b/tests/components/ssd1351_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..8342cb972b --- /dev/null +++ b/tests/components/ssd1351_spi/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1351_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ssd1351_spi + model: "SSD1351 128x128" + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/ssd1351_spi/test.esp8266-ard.yaml b/tests/components/ssd1351_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7ed9a31dde --- /dev/null +++ b/tests/components/ssd1351_spi/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1351_spi + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ssd1351_spi + model: "SSD1351 128x128" + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + 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"); diff --git a/tests/components/ssd1351_spi/test.rp2040-ard.yaml b/tests/components/ssd1351_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..72936d046b --- /dev/null +++ b/tests/components/ssd1351_spi/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_ssd1351_spi + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ssd1351_spi + model: "SSD1351 128x128" + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + 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"); diff --git a/tests/components/st7567_i2c/test.esp32-ard.yaml b/tests/components/st7567_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..b7aee61b68 --- /dev/null +++ b/tests/components/st7567_i2c/test.esp32-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_st7567_i2c + scl: 16 + sda: 17 + +display: + - platform: st7567_i2c + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7567_i2c/test.esp32-c3-ard.yaml b/tests/components/st7567_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d779040500 --- /dev/null +++ b/tests/components/st7567_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_st7567_i2c + scl: 5 + sda: 4 + +display: + - platform: st7567_i2c + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7567_i2c/test.esp32-c3-idf.yaml b/tests/components/st7567_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d779040500 --- /dev/null +++ b/tests/components/st7567_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_st7567_i2c + scl: 5 + sda: 4 + +display: + - platform: st7567_i2c + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7567_i2c/test.esp32-idf.yaml b/tests/components/st7567_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..b7aee61b68 --- /dev/null +++ b/tests/components/st7567_i2c/test.esp32-idf.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_st7567_i2c + scl: 16 + sda: 17 + +display: + - platform: st7567_i2c + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7567_i2c/test.esp8266-ard.yaml b/tests/components/st7567_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..d779040500 --- /dev/null +++ b/tests/components/st7567_i2c/test.esp8266-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_st7567_i2c + scl: 5 + sda: 4 + +display: + - platform: st7567_i2c + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7567_i2c/test.rp2040-ard.yaml b/tests/components/st7567_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..d779040500 --- /dev/null +++ b/tests/components/st7567_i2c/test.rp2040-ard.yaml @@ -0,0 +1,23 @@ +i2c: + - id: i2c_st7567_i2c + scl: 5 + sda: 4 + +display: + - platform: st7567_i2c + reset_pin: 3 + address: 0x3C + id: display1 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, 10, 10); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7567_spi/test.esp32-ard.yaml b/tests/components/st7567_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..bb4530248f --- /dev/null +++ b/tests/components/st7567_spi/test.esp32-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7567_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: st7567_spi + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/st7567_spi/test.esp32-c3-ard.yaml b/tests/components/st7567_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b799ce7302 --- /dev/null +++ b/tests/components/st7567_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7567_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: st7567_spi + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/st7567_spi/test.esp32-c3-idf.yaml b/tests/components/st7567_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b799ce7302 --- /dev/null +++ b/tests/components/st7567_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7567_spi + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: st7567_spi + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + 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"); diff --git a/tests/components/st7567_spi/test.esp32-idf.yaml b/tests/components/st7567_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..bb4530248f --- /dev/null +++ b/tests/components/st7567_spi/test.esp32-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7567_spi + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: st7567_spi + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/st7567_spi/test.esp8266-ard.yaml b/tests/components/st7567_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..bbc47e67f6 --- /dev/null +++ b/tests/components/st7567_spi/test.esp8266-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7567_spi + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: st7567_spi + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + 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"); diff --git a/tests/components/st7567_spi/test.rp2040-ard.yaml b/tests/components/st7567_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..1b491101a8 --- /dev/null +++ b/tests/components/st7567_spi/test.rp2040-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7567_spi + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: st7567_spi + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + 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"); diff --git a/tests/components/st7701s/common.yaml b/tests/components/st7701s/common.yaml new file mode 100644 index 0000000000..6e4fccc9cb --- /dev/null +++ b/tests/components/st7701s/common.yaml @@ -0,0 +1,65 @@ +psram: + mode: octal + speed: 80MHz +spi: + - id: lcd_spi + clk_pin: 41 + mosi_pin: 48 + +i2c: + sda: 39 + scl: 40 + scan: false + id: bus_a + +pca9554: + - id: p_c_a + pin_count: 16 + address: 0x20 + +display: + - platform: st7701s + spi_mode: MODE3 + color_order: RGB + dimensions: + width: 480 + height: 480 + invert_colors: true + transform: + mirror_x: true + mirror_y: true + cs_pin: + pca9554: p_c_a + number: 4 + reset_pin: + pca9554: p_c_a + number: 5 + de_pin: 18 + hsync_pin: 16 + vsync_pin: 17 + pclk_pin: 21 + init_sequence: + - 1 + - [0x23, 0xA, 0xB] + - delay 20ms + - [0x23, 0xA, 0xB] + - delay 0.2s + data_pins: + - number: 0 + ignore_strapping_warning: true + - 1 + - 2 + - 3 + - number: 4 + ignore_strapping_warning: false + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 diff --git a/tests/components/st7701s/test.esp32-s3-idf.yaml b/tests/components/st7701s/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/st7701s/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/st7735/test.esp32-ard.yaml b/tests/components/st7735/test.esp32-ard.yaml new file mode 100644 index 0000000000..fd3f6cade6 --- /dev/null +++ b/tests/components/st7735/test.esp32-ard.yaml @@ -0,0 +1,29 @@ +spi: + - id: spi_st7735 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: st7735 + model: "INITR_18BLACKTAB" + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + device_width: 128 + device_height: 160 + col_start: 0 + row_start: 0 + 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"); diff --git a/tests/components/st7735/test.esp32-c3-ard.yaml b/tests/components/st7735/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fc6c2421c4 --- /dev/null +++ b/tests/components/st7735/test.esp32-c3-ard.yaml @@ -0,0 +1,29 @@ +spi: + - id: spi_st7735 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: st7735 + model: "INITR_18BLACKTAB" + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + device_width: 128 + device_height: 160 + col_start: 0 + row_start: 0 + 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"); diff --git a/tests/components/st7735/test.esp32-c3-idf.yaml b/tests/components/st7735/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fc6c2421c4 --- /dev/null +++ b/tests/components/st7735/test.esp32-c3-idf.yaml @@ -0,0 +1,29 @@ +spi: + - id: spi_st7735 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: st7735 + model: "INITR_18BLACKTAB" + cs_pin: 2 + dc_pin: 3 + reset_pin: 4 + device_width: 128 + device_height: 160 + col_start: 0 + row_start: 0 + 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"); diff --git a/tests/components/st7735/test.esp32-idf.yaml b/tests/components/st7735/test.esp32-idf.yaml new file mode 100644 index 0000000000..fd3f6cade6 --- /dev/null +++ b/tests/components/st7735/test.esp32-idf.yaml @@ -0,0 +1,29 @@ +spi: + - id: spi_st7735 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: st7735 + model: "INITR_18BLACKTAB" + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + device_width: 128 + device_height: 160 + col_start: 0 + row_start: 0 + 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"); diff --git a/tests/components/st7735/test.esp8266-ard.yaml b/tests/components/st7735/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ea8fa93c36 --- /dev/null +++ b/tests/components/st7735/test.esp8266-ard.yaml @@ -0,0 +1,29 @@ +spi: + - id: spi_st7735 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: st7735 + model: "INITR_18BLACKTAB" + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + device_width: 128 + device_height: 160 + col_start: 0 + row_start: 0 + 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"); diff --git a/tests/components/st7735/test.rp2040-ard.yaml b/tests/components/st7735/test.rp2040-ard.yaml new file mode 100644 index 0000000000..828f9a3ae1 --- /dev/null +++ b/tests/components/st7735/test.rp2040-ard.yaml @@ -0,0 +1,29 @@ +spi: + - id: spi_st7735 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: st7735 + model: "INITR_18BLACKTAB" + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + device_width: 128 + device_height: 160 + col_start: 0 + row_start: 0 + 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"); diff --git a/tests/components/st7789v/test.esp32-ard.yaml b/tests/components/st7789v/test.esp32-ard.yaml new file mode 100644 index 0000000000..54a9db6da1 --- /dev/null +++ b/tests/components/st7789v/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_st7789v + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: st7789v + model: TTGO TDisplay 135x240 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/st7789v/test.esp32-c3-ard.yaml b/tests/components/st7789v/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..1cb8d22fcb --- /dev/null +++ b/tests/components/st7789v/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_st7789v + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: st7789v + model: TTGO TDisplay 135x240 + cs_pin: 2 + dc_pin: 3 + reset_pin: 8 + 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"); diff --git a/tests/components/st7789v/test.esp32-c3-idf.yaml b/tests/components/st7789v/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..1cb8d22fcb --- /dev/null +++ b/tests/components/st7789v/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_st7789v + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: st7789v + model: TTGO TDisplay 135x240 + cs_pin: 2 + dc_pin: 3 + reset_pin: 8 + 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"); diff --git a/tests/components/st7789v/test.esp32-idf.yaml b/tests/components/st7789v/test.esp32-idf.yaml new file mode 100644 index 0000000000..54a9db6da1 --- /dev/null +++ b/tests/components/st7789v/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_st7789v + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: st7789v + model: TTGO TDisplay 135x240 + cs_pin: 12 + dc_pin: 13 + reset_pin: 14 + 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"); diff --git a/tests/components/st7789v/test.esp8266-ard.yaml b/tests/components/st7789v/test.esp8266-ard.yaml new file mode 100644 index 0000000000..deeceb8c9a --- /dev/null +++ b/tests/components/st7789v/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_st7789v + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: st7789v + model: TTGO TDisplay 135x240 + cs_pin: 15 + dc_pin: 16 + reset_pin: 5 + 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"); diff --git a/tests/components/st7789v/test.rp2040-ard.yaml b/tests/components/st7789v/test.rp2040-ard.yaml new file mode 100644 index 0000000000..778aa2aa55 --- /dev/null +++ b/tests/components/st7789v/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +spi: + - id: spi_st7789v + clk_pin: 2 + mosi_pin: 3 + miso_pin: 8 + +display: + - platform: st7789v + model: TTGO TDisplay 135x240 + cs_pin: 5 + dc_pin: 6 + reset_pin: 7 + 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"); diff --git a/tests/components/st7920/test.esp32-ard.yaml b/tests/components/st7920/test.esp32-ard.yaml new file mode 100644 index 0000000000..cdcbc85642 --- /dev/null +++ b/tests/components/st7920/test.esp32-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7920 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: st7920 + cs_pin: 12 + height: 128 + width: 64 + 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"); diff --git a/tests/components/st7920/test.esp32-c3-ard.yaml b/tests/components/st7920/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..84ae88461f --- /dev/null +++ b/tests/components/st7920/test.esp32-c3-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7920 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: st7920 + cs_pin: 2 + height: 128 + width: 64 + 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"); diff --git a/tests/components/st7920/test.esp32-c3-idf.yaml b/tests/components/st7920/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..84ae88461f --- /dev/null +++ b/tests/components/st7920/test.esp32-c3-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7920 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: st7920 + cs_pin: 2 + height: 128 + width: 64 + 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"); diff --git a/tests/components/st7920/test.esp32-idf.yaml b/tests/components/st7920/test.esp32-idf.yaml new file mode 100644 index 0000000000..cdcbc85642 --- /dev/null +++ b/tests/components/st7920/test.esp32-idf.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7920 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: st7920 + cs_pin: 12 + height: 128 + width: 64 + 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"); diff --git a/tests/components/st7920/test.esp8266-ard.yaml b/tests/components/st7920/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0450bf1c5e --- /dev/null +++ b/tests/components/st7920/test.esp8266-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7920 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: st7920 + cs_pin: 15 + height: 128 + width: 64 + 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"); diff --git a/tests/components/st7920/test.rp2040-ard.yaml b/tests/components/st7920/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f442820e7b --- /dev/null +++ b/tests/components/st7920/test.rp2040-ard.yaml @@ -0,0 +1,24 @@ +spi: + - id: spi_st7920 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: st7920 + cs_pin: 5 + height: 128 + width: 64 + 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"); diff --git a/tests/components/statsD/common.yaml b/tests/components/statsD/common.yaml new file mode 100644 index 0000000000..5878101de8 --- /dev/null +++ b/tests/components/statsD/common.yaml @@ -0,0 +1,29 @@ +wifi: + ssid: MySSID + password: password1 + +statsd: + host: "192.168.1.1" + port: 8125 + prefix: esphome + update_interval: 60s + sensors: + id: s + name: sensors + binary_sensors: + id: bs + name: binary_sensors + +sensor: + - platform: template + id: s + name: "42.1" + lambda: |- + return 42.1f; + +binary_sensor: + - platform: template + id: bs + name: "On" + lambda: |- + return true; diff --git a/tests/components/statsD/test.bk72xx-ard.yaml b/tests/components/statsD/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-ard.yaml b/tests/components/statsD/test.esp32-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp32-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-c3-ard.yaml b/tests/components/statsD/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp32-c3-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-c3-idf.yaml b/tests/components/statsD/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp32-c3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-idf.yaml b/tests/components/statsD/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp8266-ard.yaml b/tests/components/statsD/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.rp2040-ard.yaml b/tests/components/statsD/test.rp2040-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/status/common.yaml b/tests/components/status/common.yaml new file mode 100644 index 0000000000..c14157566b --- /dev/null +++ b/tests/components/status/common.yaml @@ -0,0 +1,10 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +binary_sensor: + - platform: status + id: node_status + name: Node Status diff --git a/tests/components/status/test.esp32-ard.yaml b/tests/components/status/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status/test.esp32-c3-ard.yaml b/tests/components/status/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status/test.esp32-c3-idf.yaml b/tests/components/status/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status/test.esp32-idf.yaml b/tests/components/status/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status/test.esp8266-ard.yaml b/tests/components/status/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status/test.rp2040-ard.yaml b/tests/components/status/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status_led/common.yaml b/tests/components/status_led/common.yaml new file mode 100644 index 0000000000..ec66c219d3 --- /dev/null +++ b/tests/components/status_led/common.yaml @@ -0,0 +1,10 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +light: + - platform: status_led + name: Switch State + pin: 4 diff --git a/tests/components/status_led/test.esp32-ard.yaml b/tests/components/status_led/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status_led/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status_led/test.esp32-c3-ard.yaml b/tests/components/status_led/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status_led/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status_led/test.esp32-c3-idf.yaml b/tests/components/status_led/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status_led/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status_led/test.esp32-idf.yaml b/tests/components/status_led/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status_led/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status_led/test.esp8266-ard.yaml b/tests/components/status_led/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status_led/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/status_led/test.rp2040-ard.yaml b/tests/components/status_led/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/status_led/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/stepper/common.yaml b/tests/components/stepper/common.yaml new file mode 100644 index 0000000000..fcf5759618 --- /dev/null +++ b/tests/components/stepper/common.yaml @@ -0,0 +1,27 @@ +stepper: + - platform: a4988 + id: test_stepper + step_pin: 3 + dir_pin: 4 + sleep_pin: 5 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 + +switch: + - platform: template + name: Stepper Switch + assumed_state: true + turn_on_action: + - stepper.set_target: + id: test_stepper + target: !lambda |- + static int32_t i = 0; + i += 1000; + if (i > 5000) { + i = -5000; + } + return i; + - stepper.report_position: + id: test_stepper + position: 0 diff --git a/tests/components/stepper/test.esp32-ard.yaml b/tests/components/stepper/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/stepper/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/stepper/test.esp32-c3-ard.yaml b/tests/components/stepper/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/stepper/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/stepper/test.esp32-c3-idf.yaml b/tests/components/stepper/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/stepper/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/stepper/test.esp32-idf.yaml b/tests/components/stepper/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/stepper/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/stepper/test.esp8266-ard.yaml b/tests/components/stepper/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/stepper/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/stepper/test.rp2040-ard.yaml b/tests/components/stepper/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/stepper/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sts3x/test.esp32-ard.yaml b/tests/components/sts3x/test.esp32-ard.yaml new file mode 100644 index 0000000000..a74d61e748 --- /dev/null +++ b/tests/components/sts3x/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sts3x + scl: 16 + sda: 17 + +sensor: + - platform: sts3x + id: sts3x_sensor + name: STS3X Temperature + address: 0x4A diff --git a/tests/components/sts3x/test.esp32-c3-ard.yaml b/tests/components/sts3x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..87980ce3a7 --- /dev/null +++ b/tests/components/sts3x/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sts3x + scl: 5 + sda: 4 + +sensor: + - platform: sts3x + id: sts3x_sensor + name: STS3X Temperature + address: 0x4A diff --git a/tests/components/sts3x/test.esp32-c3-idf.yaml b/tests/components/sts3x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..87980ce3a7 --- /dev/null +++ b/tests/components/sts3x/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sts3x + scl: 5 + sda: 4 + +sensor: + - platform: sts3x + id: sts3x_sensor + name: STS3X Temperature + address: 0x4A diff --git a/tests/components/sts3x/test.esp32-idf.yaml b/tests/components/sts3x/test.esp32-idf.yaml new file mode 100644 index 0000000000..a74d61e748 --- /dev/null +++ b/tests/components/sts3x/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sts3x + scl: 16 + sda: 17 + +sensor: + - platform: sts3x + id: sts3x_sensor + name: STS3X Temperature + address: 0x4A diff --git a/tests/components/sts3x/test.esp8266-ard.yaml b/tests/components/sts3x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..87980ce3a7 --- /dev/null +++ b/tests/components/sts3x/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sts3x + scl: 5 + sda: 4 + +sensor: + - platform: sts3x + id: sts3x_sensor + name: STS3X Temperature + address: 0x4A diff --git a/tests/components/sts3x/test.rp2040-ard.yaml b/tests/components/sts3x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..87980ce3a7 --- /dev/null +++ b/tests/components/sts3x/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_sts3x + scl: 5 + sda: 4 + +sensor: + - platform: sts3x + id: sts3x_sensor + name: STS3X Temperature + address: 0x4A diff --git a/tests/components/sun/common.yaml b/tests/components/sun/common.yaml new file mode 100644 index 0000000000..e0157424a0 --- /dev/null +++ b/tests/components/sun/common.yaml @@ -0,0 +1,38 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +time: + - platform: homeassistant + id: homeassistant_time + +sun: + latitude: 48.8584° + longitude: 2.2945° + on_sunrise: + - then: + - logger.log: Good morning + - elevation: 5° + then: + - logger.log: Good morning again + on_sunset: + - then: + - logger.log: Good evening + +sensor: + - platform: sun + name: Sun Elevation + type: elevation + - platform: sun + name: Sun Azimuth + type: azimuth + +text_sensor: + - platform: sun + name: Sun Next Sunrise + type: sunrise + - platform: sun + name: Sun Next Sunset + type: sunset diff --git a/tests/components/sun/test.esp32-ard.yaml b/tests/components/sun/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sun/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sun/test.esp32-c3-ard.yaml b/tests/components/sun/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sun/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sun/test.esp32-c3-idf.yaml b/tests/components/sun/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sun/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sun/test.esp32-idf.yaml b/tests/components/sun/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sun/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sun/test.esp8266-ard.yaml b/tests/components/sun/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sun/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sun/test.rp2040-ard.yaml b/tests/components/sun/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sun/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sun_gtil2/test.esp32-ard.yaml b/tests/components/sun_gtil2/test.esp32-ard.yaml new file mode 100644 index 0000000000..ed1e68e574 --- /dev/null +++ b/tests/components/sun_gtil2/test.esp32-ard.yaml @@ -0,0 +1,44 @@ +uart: + - id: control_to_display + rx_pin: + number: 16 + baud_rate: 9600 + +sun_gtil2: + uart_id: control_to_display + +sensor: + - platform: sun_gtil2 + temperature: + id: gtil_temperature + name: "Heatsink Temperature" + filters: + - throttle_average: 30s + dc_voltage: + id: gtil_dc_voltage + name: "DC Voltage" + filters: + - throttle_average: 30s + ac_voltage: + id: gtil_ac_voltage + name: "AC Voltage" + filters: + - throttle_average: 30s + ac_power: + id: gtil_ac_power + name: "AC Power" + dc_power: + id: gtil_dc_power + name: "DC Power" + limiter_power: + id: gtil_limiter_power + internal: true + +text_sensor: + - platform: sun_gtil2 + state: + id: gtil_state + name: "State" + serial_number: + id: gtil_serial_number + internal: true diff --git a/tests/components/sun_gtil2/test.esp32-c3-ard.yaml b/tests/components/sun_gtil2/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..6ec834db98 --- /dev/null +++ b/tests/components/sun_gtil2/test.esp32-c3-ard.yaml @@ -0,0 +1,44 @@ +uart: + - id: control_to_display + rx_pin: + number: 5 + baud_rate: 9600 + +sun_gtil2: + uart_id: control_to_display + +sensor: + - platform: sun_gtil2 + temperature: + id: gtil_temperature + name: "Heatsink Temperature" + filters: + - throttle_average: 30s + dc_voltage: + id: gtil_dc_voltage + name: "DC Voltage" + filters: + - throttle_average: 30s + ac_voltage: + id: gtil_ac_voltage + name: "AC Voltage" + filters: + - throttle_average: 30s + ac_power: + id: gtil_ac_power + name: "AC Power" + dc_power: + id: gtil_dc_power + name: "DC Power" + limiter_power: + id: gtil_limiter_power + internal: true + +text_sensor: + - platform: sun_gtil2 + state: + id: gtil_state + name: "State" + serial_number: + id: gtil_serial_number + internal: true diff --git a/tests/components/sun_gtil2/test.esp32-c3-idf.yaml b/tests/components/sun_gtil2/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6ec834db98 --- /dev/null +++ b/tests/components/sun_gtil2/test.esp32-c3-idf.yaml @@ -0,0 +1,44 @@ +uart: + - id: control_to_display + rx_pin: + number: 5 + baud_rate: 9600 + +sun_gtil2: + uart_id: control_to_display + +sensor: + - platform: sun_gtil2 + temperature: + id: gtil_temperature + name: "Heatsink Temperature" + filters: + - throttle_average: 30s + dc_voltage: + id: gtil_dc_voltage + name: "DC Voltage" + filters: + - throttle_average: 30s + ac_voltage: + id: gtil_ac_voltage + name: "AC Voltage" + filters: + - throttle_average: 30s + ac_power: + id: gtil_ac_power + name: "AC Power" + dc_power: + id: gtil_dc_power + name: "DC Power" + limiter_power: + id: gtil_limiter_power + internal: true + +text_sensor: + - platform: sun_gtil2 + state: + id: gtil_state + name: "State" + serial_number: + id: gtil_serial_number + internal: true diff --git a/tests/components/sun_gtil2/test.esp32-idf.yaml b/tests/components/sun_gtil2/test.esp32-idf.yaml new file mode 100644 index 0000000000..ed1e68e574 --- /dev/null +++ b/tests/components/sun_gtil2/test.esp32-idf.yaml @@ -0,0 +1,44 @@ +uart: + - id: control_to_display + rx_pin: + number: 16 + baud_rate: 9600 + +sun_gtil2: + uart_id: control_to_display + +sensor: + - platform: sun_gtil2 + temperature: + id: gtil_temperature + name: "Heatsink Temperature" + filters: + - throttle_average: 30s + dc_voltage: + id: gtil_dc_voltage + name: "DC Voltage" + filters: + - throttle_average: 30s + ac_voltage: + id: gtil_ac_voltage + name: "AC Voltage" + filters: + - throttle_average: 30s + ac_power: + id: gtil_ac_power + name: "AC Power" + dc_power: + id: gtil_dc_power + name: "DC Power" + limiter_power: + id: gtil_limiter_power + internal: true + +text_sensor: + - platform: sun_gtil2 + state: + id: gtil_state + name: "State" + serial_number: + id: gtil_serial_number + internal: true diff --git a/tests/components/sun_gtil2/test.esp8266-ard.yaml b/tests/components/sun_gtil2/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6ec834db98 --- /dev/null +++ b/tests/components/sun_gtil2/test.esp8266-ard.yaml @@ -0,0 +1,44 @@ +uart: + - id: control_to_display + rx_pin: + number: 5 + baud_rate: 9600 + +sun_gtil2: + uart_id: control_to_display + +sensor: + - platform: sun_gtil2 + temperature: + id: gtil_temperature + name: "Heatsink Temperature" + filters: + - throttle_average: 30s + dc_voltage: + id: gtil_dc_voltage + name: "DC Voltage" + filters: + - throttle_average: 30s + ac_voltage: + id: gtil_ac_voltage + name: "AC Voltage" + filters: + - throttle_average: 30s + ac_power: + id: gtil_ac_power + name: "AC Power" + dc_power: + id: gtil_dc_power + name: "DC Power" + limiter_power: + id: gtil_limiter_power + internal: true + +text_sensor: + - platform: sun_gtil2 + state: + id: gtil_state + name: "State" + serial_number: + id: gtil_serial_number + internal: true diff --git a/tests/components/sun_gtil2/test.rp2040-ard.yaml b/tests/components/sun_gtil2/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6ec834db98 --- /dev/null +++ b/tests/components/sun_gtil2/test.rp2040-ard.yaml @@ -0,0 +1,44 @@ +uart: + - id: control_to_display + rx_pin: + number: 5 + baud_rate: 9600 + +sun_gtil2: + uart_id: control_to_display + +sensor: + - platform: sun_gtil2 + temperature: + id: gtil_temperature + name: "Heatsink Temperature" + filters: + - throttle_average: 30s + dc_voltage: + id: gtil_dc_voltage + name: "DC Voltage" + filters: + - throttle_average: 30s + ac_voltage: + id: gtil_ac_voltage + name: "AC Voltage" + filters: + - throttle_average: 30s + ac_power: + id: gtil_ac_power + name: "AC Power" + dc_power: + id: gtil_dc_power + name: "DC Power" + limiter_power: + id: gtil_limiter_power + internal: true + +text_sensor: + - platform: sun_gtil2 + state: + id: gtil_state + name: "State" + serial_number: + id: gtil_serial_number + internal: true diff --git a/tests/components/sx1509/test.esp32-ard.yaml b/tests/components/sx1509/test.esp32-ard.yaml new file mode 100644 index 0000000000..aa1d161a43 --- /dev/null +++ b/tests/components/sx1509/test.esp32-ard.yaml @@ -0,0 +1,33 @@ +i2c: + - id: i2c_sx1509 + scl: 16 + sda: 17 + +sx1509: + - id: sx1509_hub + address: 0x3E + +binary_sensor: + - platform: gpio + name: GPIO SX1509 Test + pin: + sx1509: sx1509_hub + number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.esp32-c3-ard.yaml b/tests/components/sx1509/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..0397812880 --- /dev/null +++ b/tests/components/sx1509/test.esp32-c3-ard.yaml @@ -0,0 +1,33 @@ +i2c: + - id: i2c_sx1509 + scl: 5 + sda: 4 + +sx1509: + - id: sx1509_hub + address: 0x3E + +binary_sensor: + - platform: gpio + name: GPIO SX1509 Test + pin: + sx1509: sx1509_hub + number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.esp32-c3-idf.yaml b/tests/components/sx1509/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..0397812880 --- /dev/null +++ b/tests/components/sx1509/test.esp32-c3-idf.yaml @@ -0,0 +1,33 @@ +i2c: + - id: i2c_sx1509 + scl: 5 + sda: 4 + +sx1509: + - id: sx1509_hub + address: 0x3E + +binary_sensor: + - platform: gpio + name: GPIO SX1509 Test + pin: + sx1509: sx1509_hub + number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.esp32-idf.yaml b/tests/components/sx1509/test.esp32-idf.yaml new file mode 100644 index 0000000000..aa1d161a43 --- /dev/null +++ b/tests/components/sx1509/test.esp32-idf.yaml @@ -0,0 +1,33 @@ +i2c: + - id: i2c_sx1509 + scl: 16 + sda: 17 + +sx1509: + - id: sx1509_hub + address: 0x3E + +binary_sensor: + - platform: gpio + name: GPIO SX1509 Test + pin: + sx1509: sx1509_hub + number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.esp8266-ard.yaml b/tests/components/sx1509/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0397812880 --- /dev/null +++ b/tests/components/sx1509/test.esp8266-ard.yaml @@ -0,0 +1,33 @@ +i2c: + - id: i2c_sx1509 + scl: 5 + sda: 4 + +sx1509: + - id: sx1509_hub + address: 0x3E + +binary_sensor: + - platform: gpio + name: GPIO SX1509 Test + pin: + sx1509: sx1509_hub + number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.rp2040-ard.yaml b/tests/components/sx1509/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0397812880 --- /dev/null +++ b/tests/components/sx1509/test.rp2040-ard.yaml @@ -0,0 +1,33 @@ +i2c: + - id: i2c_sx1509 + scl: 5 + sda: 4 + +sx1509: + - id: sx1509_hub + address: 0x3E + +binary_sensor: + - platform: gpio + name: GPIO SX1509 Test + pin: + sx1509: sx1509_hub + number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/t6615/test.esp32-ard.yaml b/tests/components/t6615/test.esp32-ard.yaml new file mode 100644 index 0000000000..2cfaa0ae5b --- /dev/null +++ b/tests/components/t6615/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_t6615 + tx_pin: 17 + rx_pin: 16 + baud_rate: 19200 + +sensor: + - platform: t6615 + co2: + name: CO2 Sensor diff --git a/tests/components/t6615/test.esp32-c3-ard.yaml b/tests/components/t6615/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e8690c770f --- /dev/null +++ b/tests/components/t6615/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_t6615 + tx_pin: 4 + rx_pin: 5 + baud_rate: 19200 + +sensor: + - platform: t6615 + co2: + name: CO2 Sensor diff --git a/tests/components/t6615/test.esp32-c3-idf.yaml b/tests/components/t6615/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e8690c770f --- /dev/null +++ b/tests/components/t6615/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_t6615 + tx_pin: 4 + rx_pin: 5 + baud_rate: 19200 + +sensor: + - platform: t6615 + co2: + name: CO2 Sensor diff --git a/tests/components/t6615/test.esp32-idf.yaml b/tests/components/t6615/test.esp32-idf.yaml new file mode 100644 index 0000000000..2cfaa0ae5b --- /dev/null +++ b/tests/components/t6615/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_t6615 + tx_pin: 17 + rx_pin: 16 + baud_rate: 19200 + +sensor: + - platform: t6615 + co2: + name: CO2 Sensor diff --git a/tests/components/t6615/test.esp8266-ard.yaml b/tests/components/t6615/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e8690c770f --- /dev/null +++ b/tests/components/t6615/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_t6615 + tx_pin: 4 + rx_pin: 5 + baud_rate: 19200 + +sensor: + - platform: t6615 + co2: + name: CO2 Sensor diff --git a/tests/components/t6615/test.rp2040-ard.yaml b/tests/components/t6615/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e8690c770f --- /dev/null +++ b/tests/components/t6615/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_t6615 + tx_pin: 4 + rx_pin: 5 + baud_rate: 19200 + +sensor: + - platform: t6615 + co2: + name: CO2 Sensor diff --git a/tests/components/tc74/test.esp32-ard.yaml b/tests/components/tc74/test.esp32-ard.yaml new file mode 100644 index 0000000000..ef9b40e184 --- /dev/null +++ b/tests/components/tc74/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tc74 + scl: 16 + sda: 17 + +sensor: + - platform: tc74 + name: TC74 Temperature diff --git a/tests/components/tc74/test.esp32-c3-ard.yaml b/tests/components/tc74/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..e1a373fbf4 --- /dev/null +++ b/tests/components/tc74/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tc74 + scl: 5 + sda: 4 + +sensor: + - platform: tc74 + name: TC74 Temperature diff --git a/tests/components/tc74/test.esp32-c3-idf.yaml b/tests/components/tc74/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e1a373fbf4 --- /dev/null +++ b/tests/components/tc74/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tc74 + scl: 5 + sda: 4 + +sensor: + - platform: tc74 + name: TC74 Temperature diff --git a/tests/components/tc74/test.esp32-idf.yaml b/tests/components/tc74/test.esp32-idf.yaml new file mode 100644 index 0000000000..ef9b40e184 --- /dev/null +++ b/tests/components/tc74/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tc74 + scl: 16 + sda: 17 + +sensor: + - platform: tc74 + name: TC74 Temperature diff --git a/tests/components/tc74/test.esp8266-ard.yaml b/tests/components/tc74/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e1a373fbf4 --- /dev/null +++ b/tests/components/tc74/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tc74 + scl: 5 + sda: 4 + +sensor: + - platform: tc74 + name: TC74 Temperature diff --git a/tests/components/tc74/test.rp2040-ard.yaml b/tests/components/tc74/test.rp2040-ard.yaml new file mode 100644 index 0000000000..e1a373fbf4 --- /dev/null +++ b/tests/components/tc74/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tc74 + scl: 5 + sda: 4 + +sensor: + - platform: tc74 + name: TC74 Temperature diff --git a/tests/components/tca9548a/test.esp32-ard.yaml b/tests/components/tca9548a/test.esp32-ard.yaml new file mode 100644 index 0000000000..7edb83c821 --- /dev/null +++ b/tests/components/tca9548a/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_tca9548a + scl: 16 + sda: 17 + +tca9548a: + - id: multiplex0 + address: 0x70 + channels: + - bus_id: multiplex0_chan0 + channel: 0 + i2c_id: i2c_tca9548a + - id: multiplex1 + address: 0x71 + i2c_id: multiplex0_chan0 diff --git a/tests/components/tca9548a/test.esp32-c3-ard.yaml b/tests/components/tca9548a/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2294530d14 --- /dev/null +++ b/tests/components/tca9548a/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_tca9548a + scl: 5 + sda: 4 + +tca9548a: + - id: multiplex0 + address: 0x70 + channels: + - bus_id: multiplex0_chan0 + channel: 0 + i2c_id: i2c_tca9548a + - id: multiplex1 + address: 0x71 + i2c_id: multiplex0_chan0 diff --git a/tests/components/tca9548a/test.esp32-c3-idf.yaml b/tests/components/tca9548a/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2294530d14 --- /dev/null +++ b/tests/components/tca9548a/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_tca9548a + scl: 5 + sda: 4 + +tca9548a: + - id: multiplex0 + address: 0x70 + channels: + - bus_id: multiplex0_chan0 + channel: 0 + i2c_id: i2c_tca9548a + - id: multiplex1 + address: 0x71 + i2c_id: multiplex0_chan0 diff --git a/tests/components/tca9548a/test.esp32-idf.yaml b/tests/components/tca9548a/test.esp32-idf.yaml new file mode 100644 index 0000000000..7edb83c821 --- /dev/null +++ b/tests/components/tca9548a/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_tca9548a + scl: 16 + sda: 17 + +tca9548a: + - id: multiplex0 + address: 0x70 + channels: + - bus_id: multiplex0_chan0 + channel: 0 + i2c_id: i2c_tca9548a + - id: multiplex1 + address: 0x71 + i2c_id: multiplex0_chan0 diff --git a/tests/components/tca9548a/test.esp8266-ard.yaml b/tests/components/tca9548a/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2294530d14 --- /dev/null +++ b/tests/components/tca9548a/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_tca9548a + scl: 5 + sda: 4 + +tca9548a: + - id: multiplex0 + address: 0x70 + channels: + - bus_id: multiplex0_chan0 + channel: 0 + i2c_id: i2c_tca9548a + - id: multiplex1 + address: 0x71 + i2c_id: multiplex0_chan0 diff --git a/tests/components/tca9548a/test.rp2040-ard.yaml b/tests/components/tca9548a/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2294530d14 --- /dev/null +++ b/tests/components/tca9548a/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_tca9548a + scl: 5 + sda: 4 + +tca9548a: + - id: multiplex0 + address: 0x70 + channels: + - bus_id: multiplex0_chan0 + channel: 0 + i2c_id: i2c_tca9548a + - id: multiplex1 + address: 0x71 + i2c_id: multiplex0_chan0 diff --git a/tests/components/tca9555/test.esp32-ard.yaml b/tests/components/tca9555/test.esp32-ard.yaml new file mode 100644 index 0000000000..e0c046b443 --- /dev/null +++ b/tests/components/tca9555/test.esp32-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 16 + sda: 17 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.esp32-c3-ard.yaml b/tests/components/tca9555/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5c49b27640 --- /dev/null +++ b/tests/components/tca9555/test.esp32-c3-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 5 + sda: 4 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.esp32-c3-idf.yaml b/tests/components/tca9555/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5c49b27640 --- /dev/null +++ b/tests/components/tca9555/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 5 + sda: 4 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.esp32-idf.yaml b/tests/components/tca9555/test.esp32-idf.yaml new file mode 100644 index 0000000000..e0c046b443 --- /dev/null +++ b/tests/components/tca9555/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 16 + sda: 17 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.esp8266-ard.yaml b/tests/components/tca9555/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5c49b27640 --- /dev/null +++ b/tests/components/tca9555/test.esp8266-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 5 + sda: 4 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.rp2040-ard.yaml b/tests/components/tca9555/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5c49b27640 --- /dev/null +++ b/tests/components/tca9555/test.rp2040-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 5 + sda: 4 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tcl112/test.esp32-ard.yaml b/tests/components/tcl112/test.esp32-ard.yaml new file mode 100644 index 0000000000..03c0e84fe5 --- /dev/null +++ b/tests/components/tcl112/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +sensor: + - platform: template + id: tcl112_sensor + lambda: "return 21;" + +climate: + - platform: tcl112 + name: TCL112 Climate with Sensor + supports_heat: true + supports_cool: true + sensor: tcl112_sensor diff --git a/tests/components/tcl112/test.esp32-c3-ard.yaml b/tests/components/tcl112/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..03c0e84fe5 --- /dev/null +++ b/tests/components/tcl112/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +sensor: + - platform: template + id: tcl112_sensor + lambda: "return 21;" + +climate: + - platform: tcl112 + name: TCL112 Climate with Sensor + supports_heat: true + supports_cool: true + sensor: tcl112_sensor diff --git a/tests/components/tcl112/test.esp32-c3-idf.yaml b/tests/components/tcl112/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..03c0e84fe5 --- /dev/null +++ b/tests/components/tcl112/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +sensor: + - platform: template + id: tcl112_sensor + lambda: "return 21;" + +climate: + - platform: tcl112 + name: TCL112 Climate with Sensor + supports_heat: true + supports_cool: true + sensor: tcl112_sensor diff --git a/tests/components/tcl112/test.esp32-idf.yaml b/tests/components/tcl112/test.esp32-idf.yaml new file mode 100644 index 0000000000..03c0e84fe5 --- /dev/null +++ b/tests/components/tcl112/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +sensor: + - platform: template + id: tcl112_sensor + lambda: "return 21;" + +climate: + - platform: tcl112 + name: TCL112 Climate with Sensor + supports_heat: true + supports_cool: true + sensor: tcl112_sensor diff --git a/tests/components/tcl112/test.esp8266-ard.yaml b/tests/components/tcl112/test.esp8266-ard.yaml new file mode 100644 index 0000000000..0a85536928 --- /dev/null +++ b/tests/components/tcl112/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +sensor: + - platform: template + id: tcl112_sensor + lambda: "return 21;" + +climate: + - platform: tcl112 + name: TCL112 Climate with Sensor + supports_heat: true + supports_cool: true + sensor: tcl112_sensor diff --git a/tests/components/tcs34725/test.esp32-ard.yaml b/tests/components/tcs34725/test.esp32-ard.yaml new file mode 100644 index 0000000000..86ef82962e --- /dev/null +++ b/tests/components/tcs34725/test.esp32-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_tcs34725 + scl: 16 + sda: 17 + +sensor: + - platform: tcs34725 + red_channel: + name: Red Channel + green_channel: + name: Green Channel + blue_channel: + name: Blue Channel + clear_channel: + name: Clear Channel + illuminance: + name: Illuminance + color_temperature: + name: Color Temperature + integration_time: 614ms + gain: 60x diff --git a/tests/components/tcs34725/test.esp32-c3-ard.yaml b/tests/components/tcs34725/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9b459c9104 --- /dev/null +++ b/tests/components/tcs34725/test.esp32-c3-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_tcs34725 + scl: 5 + sda: 4 + +sensor: + - platform: tcs34725 + red_channel: + name: Red Channel + green_channel: + name: Green Channel + blue_channel: + name: Blue Channel + clear_channel: + name: Clear Channel + illuminance: + name: Illuminance + color_temperature: + name: Color Temperature + integration_time: 614ms + gain: 60x diff --git a/tests/components/tcs34725/test.esp32-c3-idf.yaml b/tests/components/tcs34725/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9b459c9104 --- /dev/null +++ b/tests/components/tcs34725/test.esp32-c3-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_tcs34725 + scl: 5 + sda: 4 + +sensor: + - platform: tcs34725 + red_channel: + name: Red Channel + green_channel: + name: Green Channel + blue_channel: + name: Blue Channel + clear_channel: + name: Clear Channel + illuminance: + name: Illuminance + color_temperature: + name: Color Temperature + integration_time: 614ms + gain: 60x diff --git a/tests/components/tcs34725/test.esp32-idf.yaml b/tests/components/tcs34725/test.esp32-idf.yaml new file mode 100644 index 0000000000..86ef82962e --- /dev/null +++ b/tests/components/tcs34725/test.esp32-idf.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_tcs34725 + scl: 16 + sda: 17 + +sensor: + - platform: tcs34725 + red_channel: + name: Red Channel + green_channel: + name: Green Channel + blue_channel: + name: Blue Channel + clear_channel: + name: Clear Channel + illuminance: + name: Illuminance + color_temperature: + name: Color Temperature + integration_time: 614ms + gain: 60x diff --git a/tests/components/tcs34725/test.esp8266-ard.yaml b/tests/components/tcs34725/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9b459c9104 --- /dev/null +++ b/tests/components/tcs34725/test.esp8266-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_tcs34725 + scl: 5 + sda: 4 + +sensor: + - platform: tcs34725 + red_channel: + name: Red Channel + green_channel: + name: Green Channel + blue_channel: + name: Blue Channel + clear_channel: + name: Clear Channel + illuminance: + name: Illuminance + color_temperature: + name: Color Temperature + integration_time: 614ms + gain: 60x diff --git a/tests/components/tcs34725/test.rp2040-ard.yaml b/tests/components/tcs34725/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9b459c9104 --- /dev/null +++ b/tests/components/tcs34725/test.rp2040-ard.yaml @@ -0,0 +1,21 @@ +i2c: + - id: i2c_tcs34725 + scl: 5 + sda: 4 + +sensor: + - platform: tcs34725 + red_channel: + name: Red Channel + green_channel: + name: Green Channel + blue_channel: + name: Blue Channel + clear_channel: + name: Clear Channel + illuminance: + name: Illuminance + color_temperature: + name: Color Temperature + integration_time: 614ms + gain: 60x diff --git a/tests/components/tee501/test.esp32-ard.yaml b/tests/components/tee501/test.esp32-ard.yaml new file mode 100644 index 0000000000..acf6fed4bf --- /dev/null +++ b/tests/components/tee501/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tee501 + scl: 16 + sda: 17 + +sensor: + - platform: tee501 + name: TEE501 Temperature + address: 0x48 diff --git a/tests/components/tee501/test.esp32-c3-ard.yaml b/tests/components/tee501/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..11991a6153 --- /dev/null +++ b/tests/components/tee501/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tee501 + scl: 5 + sda: 4 + +sensor: + - platform: tee501 + name: TEE501 Temperature + address: 0x48 diff --git a/tests/components/tee501/test.esp32-c3-idf.yaml b/tests/components/tee501/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..11991a6153 --- /dev/null +++ b/tests/components/tee501/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tee501 + scl: 5 + sda: 4 + +sensor: + - platform: tee501 + name: TEE501 Temperature + address: 0x48 diff --git a/tests/components/tee501/test.esp32-idf.yaml b/tests/components/tee501/test.esp32-idf.yaml new file mode 100644 index 0000000000..acf6fed4bf --- /dev/null +++ b/tests/components/tee501/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tee501 + scl: 16 + sda: 17 + +sensor: + - platform: tee501 + name: TEE501 Temperature + address: 0x48 diff --git a/tests/components/tee501/test.esp8266-ard.yaml b/tests/components/tee501/test.esp8266-ard.yaml new file mode 100644 index 0000000000..11991a6153 --- /dev/null +++ b/tests/components/tee501/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tee501 + scl: 5 + sda: 4 + +sensor: + - platform: tee501 + name: TEE501 Temperature + address: 0x48 diff --git a/tests/components/tee501/test.rp2040-ard.yaml b/tests/components/tee501/test.rp2040-ard.yaml new file mode 100644 index 0000000000..11991a6153 --- /dev/null +++ b/tests/components/tee501/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tee501 + scl: 5 + sda: 4 + +sensor: + - platform: tee501 + name: TEE501 Temperature + address: 0x48 diff --git a/tests/components/teleinfo/test.esp32-ard.yaml b/tests/components/teleinfo/test.esp32-ard.yaml new file mode 100644 index 0000000000..a5bd176143 --- /dev/null +++ b/tests/components/teleinfo/test.esp32-ard.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_teleinfo + tx_pin: 17 + rx_pin: 16 + baud_rate: 1200 + parity: EVEN + +button: + - platform: template + name: Poller component suspend test + on_press: + - component.suspend: test_teleinfo + - delay: 20s + - component.update: test_teleinfo + - delay: 20s + - component.resume: test_teleinfo + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: 2s + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: !lambda return 2500; + +teleinfo: + id: test_teleinfo + historical_mode: true + update_interval: 60s + +sensor: + - platform: teleinfo + name: hchc + tag_name: HCHC + teleinfo_id: test_teleinfo + unit_of_measurement: Wh + +text_sensor: + - platform: teleinfo + name: optarif + tag_name: OPTARIF + teleinfo_id: test_teleinfo diff --git a/tests/components/teleinfo/test.esp32-c3-ard.yaml b/tests/components/teleinfo/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..55641e1e01 --- /dev/null +++ b/tests/components/teleinfo/test.esp32-c3-ard.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_teleinfo + tx_pin: 4 + rx_pin: 5 + baud_rate: 1200 + parity: EVEN + +button: + - platform: template + name: Poller component suspend test + on_press: + - component.suspend: test_teleinfo + - delay: 20s + - component.update: test_teleinfo + - delay: 20s + - component.resume: test_teleinfo + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: 2s + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: !lambda return 2500; + +teleinfo: + id: test_teleinfo + historical_mode: true + update_interval: 60s + +sensor: + - platform: teleinfo + name: hchc + tag_name: HCHC + teleinfo_id: test_teleinfo + unit_of_measurement: Wh + +text_sensor: + - platform: teleinfo + name: optarif + tag_name: OPTARIF + teleinfo_id: test_teleinfo diff --git a/tests/components/teleinfo/test.esp32-c3-idf.yaml b/tests/components/teleinfo/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..55641e1e01 --- /dev/null +++ b/tests/components/teleinfo/test.esp32-c3-idf.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_teleinfo + tx_pin: 4 + rx_pin: 5 + baud_rate: 1200 + parity: EVEN + +button: + - platform: template + name: Poller component suspend test + on_press: + - component.suspend: test_teleinfo + - delay: 20s + - component.update: test_teleinfo + - delay: 20s + - component.resume: test_teleinfo + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: 2s + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: !lambda return 2500; + +teleinfo: + id: test_teleinfo + historical_mode: true + update_interval: 60s + +sensor: + - platform: teleinfo + name: hchc + tag_name: HCHC + teleinfo_id: test_teleinfo + unit_of_measurement: Wh + +text_sensor: + - platform: teleinfo + name: optarif + tag_name: OPTARIF + teleinfo_id: test_teleinfo diff --git a/tests/components/teleinfo/test.esp32-idf.yaml b/tests/components/teleinfo/test.esp32-idf.yaml new file mode 100644 index 0000000000..a5bd176143 --- /dev/null +++ b/tests/components/teleinfo/test.esp32-idf.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_teleinfo + tx_pin: 17 + rx_pin: 16 + baud_rate: 1200 + parity: EVEN + +button: + - platform: template + name: Poller component suspend test + on_press: + - component.suspend: test_teleinfo + - delay: 20s + - component.update: test_teleinfo + - delay: 20s + - component.resume: test_teleinfo + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: 2s + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: !lambda return 2500; + +teleinfo: + id: test_teleinfo + historical_mode: true + update_interval: 60s + +sensor: + - platform: teleinfo + name: hchc + tag_name: HCHC + teleinfo_id: test_teleinfo + unit_of_measurement: Wh + +text_sensor: + - platform: teleinfo + name: optarif + tag_name: OPTARIF + teleinfo_id: test_teleinfo diff --git a/tests/components/teleinfo/test.esp8266-ard.yaml b/tests/components/teleinfo/test.esp8266-ard.yaml new file mode 100644 index 0000000000..55641e1e01 --- /dev/null +++ b/tests/components/teleinfo/test.esp8266-ard.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_teleinfo + tx_pin: 4 + rx_pin: 5 + baud_rate: 1200 + parity: EVEN + +button: + - platform: template + name: Poller component suspend test + on_press: + - component.suspend: test_teleinfo + - delay: 20s + - component.update: test_teleinfo + - delay: 20s + - component.resume: test_teleinfo + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: 2s + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: !lambda return 2500; + +teleinfo: + id: test_teleinfo + historical_mode: true + update_interval: 60s + +sensor: + - platform: teleinfo + name: hchc + tag_name: HCHC + teleinfo_id: test_teleinfo + unit_of_measurement: Wh + +text_sensor: + - platform: teleinfo + name: optarif + tag_name: OPTARIF + teleinfo_id: test_teleinfo diff --git a/tests/components/teleinfo/test.rp2040-ard.yaml b/tests/components/teleinfo/test.rp2040-ard.yaml new file mode 100644 index 0000000000..55641e1e01 --- /dev/null +++ b/tests/components/teleinfo/test.rp2040-ard.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_teleinfo + tx_pin: 4 + rx_pin: 5 + baud_rate: 1200 + parity: EVEN + +button: + - platform: template + name: Poller component suspend test + on_press: + - component.suspend: test_teleinfo + - delay: 20s + - component.update: test_teleinfo + - delay: 20s + - component.resume: test_teleinfo + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: 2s + - delay: 20s + - component.resume: + id: test_teleinfo + update_interval: !lambda return 2500; + +teleinfo: + id: test_teleinfo + historical_mode: true + update_interval: 60s + +sensor: + - platform: teleinfo + name: hchc + tag_name: HCHC + teleinfo_id: test_teleinfo + unit_of_measurement: Wh + +text_sensor: + - platform: teleinfo + name: optarif + tag_name: OPTARIF + teleinfo_id: test_teleinfo diff --git a/tests/components/tem3200/common.yaml b/tests/components/tem3200/common.yaml new file mode 100644 index 0000000000..392c853bf4 --- /dev/null +++ b/tests/components/tem3200/common.yaml @@ -0,0 +1,16 @@ +i2c: + id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + frequency: 200kHz + +sensor: + - platform: tem3200 + update_interval: 1s + i2c_id: i2c_bus + + temperature: + name: water temperature + + raw_pressure: + name: water pressure diff --git a/tests/components/tem3200/test.esp32-ard.yaml b/tests/components/tem3200/test.esp32-ard.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/tem3200/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-idf.yaml b/tests/components/tem3200/test.esp32-idf.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/tem3200/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-s3-ard.yaml b/tests/components/tem3200/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/tem3200/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-s3-idf.yaml b/tests/components/tem3200/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/tem3200/test.esp32-s3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/tem3200/test.esp8266-ard.yaml b/tests/components/tem3200/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3be5e53dcb --- /dev/null +++ b/tests/components/tem3200/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO05 + sda_pin: GPIO04 + +<<: !include common.yaml diff --git a/tests/components/template/common.yaml b/tests/components/template/common.yaml new file mode 100644 index 0000000000..3565926933 --- /dev/null +++ b/tests/components/template/common.yaml @@ -0,0 +1,228 @@ +sensor: + - platform: template + name: "Template Sensor" + id: template_sens + lambda: |- + if (id(some_binary_sensor).state) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +esphome: + on_boot: + - sensor.template.publish: + id: template_sens + state: 42.0 + + # Templated + - sensor.template.publish: + id: template_sens + state: !lambda "return 42.0;" + + - datetime.date.set: + id: test_date + date: + year: 2021 + month: 1 + day: 1 + - datetime.date.set: + id: test_date + date: !lambda "return {.day_of_month = 1, .month = 1, .year = 2021};" + - datetime.date.set: + id: test_date + date: "2021-01-01" + +binary_sensor: + - platform: template + id: some_binary_sensor + name: "Garage Door Open" + lambda: |- + if (id(template_sens).state > 30) { + // Garage Door is open. + return true; + } else { + // Garage Door is closed. + return false; + } + - platform: template + id: other_binary_sensor + name: "Garage Door Closed" + condition: + sensor.in_range: + id: template_sens + below: 30.0 + +output: + - platform: template + id: outputsplit + type: float + write_action: + - logger.log: "write_action" + +switch: + - platform: template + name: "Template Switch" + lambda: |- + if (id(some_binary_sensor).state) { + return true; + } else { + return false; + } + turn_on_action: + - logger.log: "turn_on_action" + turn_off_action: + - logger.log: "turn_off_action" + +button: + - platform: template + name: "Template Button" + on_press: + - logger.log: Button Pressed + +cover: + - platform: template + name: "Template Cover" + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } else { + return COVER_CLOSED; + } + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + +number: + - platform: template + name: "Template number" + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: "Template select" + optimistic: true + options: + - one + - two + - three + initial_option: two + +lock: + - platform: template + name: "Template Lock" + lambda: |- + if (id(some_binary_sensor).state) { + return LOCK_STATE_LOCKED; + } else { + return LOCK_STATE_UNLOCKED; + } + lock_action: + - logger.log: lock_action + unlock_action: + - logger.log: unlock_action + open_action: + - logger.log: open_action + +valve: + - platform: template + name: "Template Valve" + lambda: |- + if (id(some_binary_sensor).state) { + return VALVE_OPEN; + } else { + return VALVE_CLOSED; + } + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + +text: + - platform: template + name: "Template text" + optimistic: true + min_length: 0 + max_length: 100 + mode: text + - platform: template + name: "Template text lambda" + mode: text + update_interval: 1s + lambda: | + return std::string{"Hello!"}; + set_action: + then: + - logger.log: + format: Template Text set to %s + args: ["x.c_str()"] + +alarm_control_panel: + - platform: template + name: Alarm Panel + codes: + - "1234" + +datetime: + - platform: template + name: Date + id: test_date + type: date + initial_value: "2000-1-2" + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "Date: %04d-%02d-%02d" + args: + - x.year + - x.month + - x.day_of_month + - platform: template + name: Time + id: test_time + type: time + initial_value: "12:34:56am" + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "Time: %02d:%02d:%02d" + args: + - x.hour + - x.minute + - x.second + - platform: template + name: DateTime + id: test_datetime + type: datetime + initial_value: "2000-1-2 12:34:56" + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "DateTime: %04d-%02d-%02d %02d:%02d:%02d" + args: + - x.year + - x.month + - x.day_of_month + - x.hour + - x.minute + - x.second + +time: + - platform: sntp # Required for datetime + +wifi: # Required for sntp time + ap: diff --git a/tests/components/template/test.bk72xx-ard.yaml b/tests/components/template/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-ard.yaml b/tests/components/template/test.esp32-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-c3-ard.yaml b/tests/components/template/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-c3-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-c3-idf.yaml b/tests/components/template/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-c3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-idf.yaml b/tests/components/template/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp32-s3-idf.yaml b/tests/components/template/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp32-s3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.esp8266-ard.yaml b/tests/components/template/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/template/test.rp2040-ard.yaml b/tests/components/template/test.rp2040-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/thermostat/common.yaml b/tests/components/thermostat/common.yaml new file mode 100644 index 0000000000..d630a93efc --- /dev/null +++ b/tests/components/thermostat/common.yaml @@ -0,0 +1,93 @@ +sensor: + - platform: template + id: thermostat_sensor + lambda: "return 21;" + +climate: + - platform: thermostat + name: Test Thermostat + sensor: thermostat_sensor + humidity_sensor: thermostat_sensor + preset: + - name: Default Preset + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + - name: Away + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C + idle_action: + - logger.log: idle_action + cool_action: + - logger.log: cool_action + supplemental_cooling_action: + - logger.log: supplemental_cooling_action + heat_action: + - logger.log: heat_action + supplemental_heating_action: + - logger.log: supplemental_heating_action + dry_action: + - logger.log: dry_action + fan_only_action: + - logger.log: fan_only_action + auto_mode: + - logger.log: auto_mode + off_mode: + - logger.log: off_mode + heat_mode: + - logger.log: heat_mode + cool_mode: + - logger.log: cool_mode + dry_mode: + - logger.log: dry_mode + fan_only_mode: + - logger.log: fan_only_mode + fan_mode_auto_action: + - logger.log: fan_mode_auto_action + fan_mode_on_action: + - logger.log: fan_mode_on_action + fan_mode_off_action: + - logger.log: fan_mode_off_action + fan_mode_low_action: + - logger.log: fan_mode_low_action + fan_mode_medium_action: + - logger.log: fan_mode_medium_action + fan_mode_high_action: + - logger.log: fan_mode_high_action + fan_mode_middle_action: + - logger.log: fan_mode_middle_action + fan_mode_focus_action: + - logger.log: fan_mode_focus_action + fan_mode_diffuse_action: + - logger.log: fan_mode_diffuse_action + fan_mode_quiet_action: + - logger.log: fan_mode_quiet_action + swing_off_action: + - logger.log: swing_off_action + swing_horizontal_action: + - logger.log: swing_horizontal_action + swing_vertical_action: + - logger.log: swing_vertical_action + swing_both_action: + - logger.log: swing_both_action + startup_delay: true + supplemental_cooling_delta: 2.0 + cool_deadband: 0.5 + cool_overrun: 0.5 + min_cooling_off_time: 300s + min_cooling_run_time: 300s + max_cooling_run_time: 600s + supplemental_heating_delta: 2.0 + heat_deadband: 0.5 + heat_overrun: 0.5 + min_heating_off_time: 300s + min_heating_run_time: 300s + max_heating_run_time: 600s + min_fanning_off_time: 30s + min_fanning_run_time: 30s + min_fan_mode_switching_time: 15s + min_idle_time: 30s + set_point_minimum_differential: 0.5 + fan_only_action_uses_fan_mode_timer: true + fan_only_cooling: true + fan_with_cooling: true + fan_with_heating: true diff --git a/tests/components/thermostat/test.esp32-ard.yaml b/tests/components/thermostat/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/thermostat/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/thermostat/test.esp32-c3-ard.yaml b/tests/components/thermostat/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/thermostat/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/thermostat/test.esp32-c3-idf.yaml b/tests/components/thermostat/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/thermostat/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/thermostat/test.esp32-idf.yaml b/tests/components/thermostat/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/thermostat/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/thermostat/test.esp8266-ard.yaml b/tests/components/thermostat/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/thermostat/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/thermostat/test.rp2040-ard.yaml b/tests/components/thermostat/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/thermostat/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time/common.yaml b/tests/components/time/common.yaml new file mode 100644 index 0000000000..465be045db --- /dev/null +++ b/tests/components/time/common.yaml @@ -0,0 +1,10 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +time: + - platform: homeassistant + - platform: sntp + id: sntp_time diff --git a/tests/components/time/test.esp32-ard.yaml b/tests/components/time/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time/test.esp32-c3-ard.yaml b/tests/components/time/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time/test.esp32-c3-idf.yaml b/tests/components/time/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time/test.esp32-idf.yaml b/tests/components/time/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time/test.esp8266-ard.yaml b/tests/components/time/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time/test.rp2040-ard.yaml b/tests/components/time/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time_based/common.yaml b/tests/components/time_based/common.yaml new file mode 100644 index 0000000000..48c86de90f --- /dev/null +++ b/tests/components/time_based/common.yaml @@ -0,0 +1,12 @@ +cover: + - platform: time_based + name: Time Based Cover + id: time_based_cover + stop_action: + - logger.log: stop_action + open_action: + - logger.log: open_action + open_duration: 5min + close_action: + - logger.log: close_action + close_duration: 4.5min diff --git a/tests/components/time_based/test.esp32-ard.yaml b/tests/components/time_based/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time_based/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time_based/test.esp32-c3-ard.yaml b/tests/components/time_based/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time_based/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time_based/test.esp32-c3-idf.yaml b/tests/components/time_based/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time_based/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time_based/test.esp32-idf.yaml b/tests/components/time_based/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time_based/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time_based/test.esp8266-ard.yaml b/tests/components/time_based/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time_based/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time_based/test.rp2040-ard.yaml b/tests/components/time_based/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/time_based/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tlc59208f/test.esp32-ard.yaml b/tests/components/tlc59208f/test.esp32-ard.yaml new file mode 100644 index 0000000000..2639de3b3d --- /dev/null +++ b/tests/components/tlc59208f/test.esp32-ard.yaml @@ -0,0 +1,50 @@ +i2c: + - id: i2c_tlc59208f + scl: 16 + sda: 17 + +tlc59208f: + - address: 0x20 + id: tlc59208f_1 + - address: 0x22 + id: tlc59208f_2 + - address: 0x24 + id: tlc59208f_3 + +output: + - platform: tlc59208f + id: tlc_0 + channel: 0 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_1 + channel: 1 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_2 + channel: 2 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_3 + channel: 0 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_4 + channel: 1 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_5 + channel: 2 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_6 + channel: 0 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_7 + channel: 1 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_8 + channel: 2 + tlc59208f_id: tlc59208f_3 diff --git a/tests/components/tlc59208f/test.esp32-c3-ard.yaml b/tests/components/tlc59208f/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..923ea4b4a4 --- /dev/null +++ b/tests/components/tlc59208f/test.esp32-c3-ard.yaml @@ -0,0 +1,50 @@ +i2c: + - id: i2c_tlc59208f + scl: 5 + sda: 4 + +tlc59208f: + - address: 0x20 + id: tlc59208f_1 + - address: 0x22 + id: tlc59208f_2 + - address: 0x24 + id: tlc59208f_3 + +output: + - platform: tlc59208f + id: tlc_0 + channel: 0 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_1 + channel: 1 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_2 + channel: 2 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_3 + channel: 0 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_4 + channel: 1 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_5 + channel: 2 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_6 + channel: 0 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_7 + channel: 1 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_8 + channel: 2 + tlc59208f_id: tlc59208f_3 diff --git a/tests/components/tlc59208f/test.esp32-c3-idf.yaml b/tests/components/tlc59208f/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..923ea4b4a4 --- /dev/null +++ b/tests/components/tlc59208f/test.esp32-c3-idf.yaml @@ -0,0 +1,50 @@ +i2c: + - id: i2c_tlc59208f + scl: 5 + sda: 4 + +tlc59208f: + - address: 0x20 + id: tlc59208f_1 + - address: 0x22 + id: tlc59208f_2 + - address: 0x24 + id: tlc59208f_3 + +output: + - platform: tlc59208f + id: tlc_0 + channel: 0 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_1 + channel: 1 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_2 + channel: 2 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_3 + channel: 0 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_4 + channel: 1 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_5 + channel: 2 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_6 + channel: 0 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_7 + channel: 1 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_8 + channel: 2 + tlc59208f_id: tlc59208f_3 diff --git a/tests/components/tlc59208f/test.esp32-idf.yaml b/tests/components/tlc59208f/test.esp32-idf.yaml new file mode 100644 index 0000000000..2639de3b3d --- /dev/null +++ b/tests/components/tlc59208f/test.esp32-idf.yaml @@ -0,0 +1,50 @@ +i2c: + - id: i2c_tlc59208f + scl: 16 + sda: 17 + +tlc59208f: + - address: 0x20 + id: tlc59208f_1 + - address: 0x22 + id: tlc59208f_2 + - address: 0x24 + id: tlc59208f_3 + +output: + - platform: tlc59208f + id: tlc_0 + channel: 0 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_1 + channel: 1 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_2 + channel: 2 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_3 + channel: 0 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_4 + channel: 1 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_5 + channel: 2 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_6 + channel: 0 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_7 + channel: 1 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_8 + channel: 2 + tlc59208f_id: tlc59208f_3 diff --git a/tests/components/tlc59208f/test.esp8266-ard.yaml b/tests/components/tlc59208f/test.esp8266-ard.yaml new file mode 100644 index 0000000000..923ea4b4a4 --- /dev/null +++ b/tests/components/tlc59208f/test.esp8266-ard.yaml @@ -0,0 +1,50 @@ +i2c: + - id: i2c_tlc59208f + scl: 5 + sda: 4 + +tlc59208f: + - address: 0x20 + id: tlc59208f_1 + - address: 0x22 + id: tlc59208f_2 + - address: 0x24 + id: tlc59208f_3 + +output: + - platform: tlc59208f + id: tlc_0 + channel: 0 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_1 + channel: 1 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_2 + channel: 2 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_3 + channel: 0 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_4 + channel: 1 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_5 + channel: 2 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_6 + channel: 0 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_7 + channel: 1 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_8 + channel: 2 + tlc59208f_id: tlc59208f_3 diff --git a/tests/components/tlc59208f/test.rp2040-ard.yaml b/tests/components/tlc59208f/test.rp2040-ard.yaml new file mode 100644 index 0000000000..923ea4b4a4 --- /dev/null +++ b/tests/components/tlc59208f/test.rp2040-ard.yaml @@ -0,0 +1,50 @@ +i2c: + - id: i2c_tlc59208f + scl: 5 + sda: 4 + +tlc59208f: + - address: 0x20 + id: tlc59208f_1 + - address: 0x22 + id: tlc59208f_2 + - address: 0x24 + id: tlc59208f_3 + +output: + - platform: tlc59208f + id: tlc_0 + channel: 0 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_1 + channel: 1 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_2 + channel: 2 + tlc59208f_id: tlc59208f_1 + - platform: tlc59208f + id: tlc_3 + channel: 0 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_4 + channel: 1 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_5 + channel: 2 + tlc59208f_id: tlc59208f_2 + - platform: tlc59208f + id: tlc_6 + channel: 0 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_7 + channel: 1 + tlc59208f_id: tlc59208f_3 + - platform: tlc59208f + id: tlc_8 + channel: 2 + tlc59208f_id: tlc59208f_3 diff --git a/tests/components/tlc5947/common.yaml b/tests/components/tlc5947/common.yaml new file mode 100644 index 0000000000..89588f3c76 --- /dev/null +++ b/tests/components/tlc5947/common.yaml @@ -0,0 +1,13 @@ +tlc5947: + clock_pin: ${clock_pin} + data_pin: ${data_pin} + lat_pin: ${lat_pin} + +output: + - platform: tlc5947 + id: output_1 + channel: 0 + max_power: 0.8 + - platform: tlc5947 + id: output_2 + channel: 1 diff --git a/tests/components/tlc5947/test.esp32-ard.yaml b/tests/components/tlc5947/test.esp32-ard.yaml new file mode 100644 index 0000000000..52411bc1e9 --- /dev/null +++ b/tests/components/tlc5947/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO15 + data_pin: GPIO14 + lat_pin: GPIO13 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5947/test.esp32-c3-ard.yaml b/tests/components/tlc5947/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4694c43642 --- /dev/null +++ b/tests/components/tlc5947/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO5 + data_pin: GPIO4 + lat_pin: GPIO3 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5947/test.esp32-c3-idf.yaml b/tests/components/tlc5947/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4694c43642 --- /dev/null +++ b/tests/components/tlc5947/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO5 + data_pin: GPIO4 + lat_pin: GPIO3 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5947/test.esp32-idf.yaml b/tests/components/tlc5947/test.esp32-idf.yaml new file mode 100644 index 0000000000..52411bc1e9 --- /dev/null +++ b/tests/components/tlc5947/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO15 + data_pin: GPIO14 + lat_pin: GPIO13 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5947/test.esp8266-ard.yaml b/tests/components/tlc5947/test.esp8266-ard.yaml new file mode 100644 index 0000000000..44da5a07b3 --- /dev/null +++ b/tests/components/tlc5947/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO5 + data_pin: GPIO4 + lat_pin: GPIO13 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5947/test.rp2040-ard.yaml b/tests/components/tlc5947/test.rp2040-ard.yaml new file mode 100644 index 0000000000..4694c43642 --- /dev/null +++ b/tests/components/tlc5947/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO5 + data_pin: GPIO4 + lat_pin: GPIO3 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5971/common.yaml b/tests/components/tlc5971/common.yaml new file mode 100644 index 0000000000..fe7fe25f0e --- /dev/null +++ b/tests/components/tlc5971/common.yaml @@ -0,0 +1,12 @@ +tlc5971: + clock_pin: ${clock_pin} + data_pin: ${data_pin} + +output: + - platform: tlc5971 + id: output_1 + channel: 0 + max_power: 0.8 + - platform: tlc5971 + id: output_2 + channel: 1 diff --git a/tests/components/tlc5971/test.esp32-ard.yaml b/tests/components/tlc5971/test.esp32-ard.yaml new file mode 100644 index 0000000000..52411bc1e9 --- /dev/null +++ b/tests/components/tlc5971/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO15 + data_pin: GPIO14 + lat_pin: GPIO13 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-c3-ard.yaml b/tests/components/tlc5971/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d898a21d46 --- /dev/null +++ b/tests/components/tlc5971/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + clock_pin: GPIO5 + data_pin: GPIO4 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-c3-idf.yaml b/tests/components/tlc5971/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d898a21d46 --- /dev/null +++ b/tests/components/tlc5971/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + clock_pin: GPIO5 + data_pin: GPIO4 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-idf.yaml b/tests/components/tlc5971/test.esp32-idf.yaml new file mode 100644 index 0000000000..e8943a572a --- /dev/null +++ b/tests/components/tlc5971/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +substitutions: + clock_pin: GPIO15 + data_pin: GPIO14 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-s2-ard.yaml b/tests/components/tlc5971/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..7bba0b0117 --- /dev/null +++ b/tests/components/tlc5971/test.esp32-s2-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + clock_pin: GPIO36 + data_pin: GPIO35 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp8266-ard.yaml b/tests/components/tlc5971/test.esp8266-ard.yaml new file mode 100644 index 0000000000..52411bc1e9 --- /dev/null +++ b/tests/components/tlc5971/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO15 + data_pin: GPIO14 + lat_pin: GPIO13 + +packages: + common: !include common.yaml diff --git a/tests/components/tlc5971/test.rp2040-ard.yaml b/tests/components/tlc5971/test.rp2040-ard.yaml new file mode 100644 index 0000000000..4694c43642 --- /dev/null +++ b/tests/components/tlc5971/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clock_pin: GPIO5 + data_pin: GPIO4 + lat_pin: GPIO3 + +packages: + common: !include common.yaml diff --git a/tests/components/tm1621/test.esp32-ard.yaml b/tests/components/tm1621/test.esp32-ard.yaml new file mode 100644 index 0000000000..8eab46f000 --- /dev/null +++ b/tests/components/tm1621/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +display: + - platform: tm1621 + id: tm1621_display + cs_pin: 15 + data_pin: 14 + read_pin: 12 + write_pin: 13 + lambda: |- + it.printf(0, "%.1f", 20.0); + it.display_celsius(true); + it.printf(1, "%.1f", 20.0); + it.display_humidity(true); diff --git a/tests/components/tm1621/test.esp32-c3-ard.yaml b/tests/components/tm1621/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..cddd64f31f --- /dev/null +++ b/tests/components/tm1621/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +display: + - platform: tm1621 + id: tm1621_display + cs_pin: 7 + data_pin: 4 + read_pin: 5 + write_pin: 6 + lambda: |- + it.printf(0, "%.1f", 20.0); + it.display_celsius(true); + it.printf(1, "%.1f", 20.0); + it.display_humidity(true); diff --git a/tests/components/tm1621/test.esp32-c3-idf.yaml b/tests/components/tm1621/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..cddd64f31f --- /dev/null +++ b/tests/components/tm1621/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +display: + - platform: tm1621 + id: tm1621_display + cs_pin: 7 + data_pin: 4 + read_pin: 5 + write_pin: 6 + lambda: |- + it.printf(0, "%.1f", 20.0); + it.display_celsius(true); + it.printf(1, "%.1f", 20.0); + it.display_humidity(true); diff --git a/tests/components/tm1621/test.esp32-idf.yaml b/tests/components/tm1621/test.esp32-idf.yaml new file mode 100644 index 0000000000..8eab46f000 --- /dev/null +++ b/tests/components/tm1621/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +display: + - platform: tm1621 + id: tm1621_display + cs_pin: 15 + data_pin: 14 + read_pin: 12 + write_pin: 13 + lambda: |- + it.printf(0, "%.1f", 20.0); + it.display_celsius(true); + it.printf(1, "%.1f", 20.0); + it.display_humidity(true); diff --git a/tests/components/tm1621/test.esp8266-ard.yaml b/tests/components/tm1621/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8eab46f000 --- /dev/null +++ b/tests/components/tm1621/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +display: + - platform: tm1621 + id: tm1621_display + cs_pin: 15 + data_pin: 14 + read_pin: 12 + write_pin: 13 + lambda: |- + it.printf(0, "%.1f", 20.0); + it.display_celsius(true); + it.printf(1, "%.1f", 20.0); + it.display_humidity(true); diff --git a/tests/components/tm1621/test.rp2040-ard.yaml b/tests/components/tm1621/test.rp2040-ard.yaml new file mode 100644 index 0000000000..cddd64f31f --- /dev/null +++ b/tests/components/tm1621/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +display: + - platform: tm1621 + id: tm1621_display + cs_pin: 7 + data_pin: 4 + read_pin: 5 + write_pin: 6 + lambda: |- + it.printf(0, "%.1f", 20.0); + it.display_celsius(true); + it.printf(1, "%.1f", 20.0); + it.display_humidity(true); diff --git a/tests/components/tm1637/test.esp32-ard.yaml b/tests/components/tm1637/test.esp32-ard.yaml new file mode 100644 index 0000000000..bf5f331cca --- /dev/null +++ b/tests/components/tm1637/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +display: + - platform: tm1637 + clk_pin: 15 + dio_pin: 14 + intensity: 3 + lambda: |- + it.print("1234"); diff --git a/tests/components/tm1637/test.esp32-c3-ard.yaml b/tests/components/tm1637/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..fa4c95b443 --- /dev/null +++ b/tests/components/tm1637/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +display: + - platform: tm1637 + clk_pin: 5 + dio_pin: 4 + intensity: 3 + lambda: |- + it.print("1234"); diff --git a/tests/components/tm1637/test.esp32-c3-idf.yaml b/tests/components/tm1637/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..fa4c95b443 --- /dev/null +++ b/tests/components/tm1637/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +display: + - platform: tm1637 + clk_pin: 5 + dio_pin: 4 + intensity: 3 + lambda: |- + it.print("1234"); diff --git a/tests/components/tm1637/test.esp32-idf.yaml b/tests/components/tm1637/test.esp32-idf.yaml new file mode 100644 index 0000000000..bf5f331cca --- /dev/null +++ b/tests/components/tm1637/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +display: + - platform: tm1637 + clk_pin: 15 + dio_pin: 14 + intensity: 3 + lambda: |- + it.print("1234"); diff --git a/tests/components/tm1637/test.esp8266-ard.yaml b/tests/components/tm1637/test.esp8266-ard.yaml new file mode 100644 index 0000000000..fa4c95b443 --- /dev/null +++ b/tests/components/tm1637/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +display: + - platform: tm1637 + clk_pin: 5 + dio_pin: 4 + intensity: 3 + lambda: |- + it.print("1234"); diff --git a/tests/components/tm1637/test.rp2040-ard.yaml b/tests/components/tm1637/test.rp2040-ard.yaml new file mode 100644 index 0000000000..fa4c95b443 --- /dev/null +++ b/tests/components/tm1637/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +display: + - platform: tm1637 + clk_pin: 5 + dio_pin: 4 + intensity: 3 + lambda: |- + it.print("1234"); diff --git a/tests/components/tm1638/common.yaml b/tests/components/tm1638/common.yaml new file mode 100644 index 0000000000..b0c5cef528 --- /dev/null +++ b/tests/components/tm1638/common.yaml @@ -0,0 +1,118 @@ +display: + - platform: tm1638 + id: tm1638_display + stb_pin: 2 + clk_pin: 5 + dio_pin: 4 + update_interval: 5s + intensity: 5 + lambda: |- + it.print("81818181"); + +binary_sensor: + - platform: tm1638 + id: Button0 + key: 0 + filters: + - delayed_on: 10ms + on_press: + then: + - switch.turn_on: Led0 + on_release: + then: + - switch.turn_off: Led0 + - platform: tm1638 + id: Button1 + key: 1 + on_press: + then: + - switch.turn_on: Led1 + on_release: + then: + - switch.turn_off: Led1 + - platform: tm1638 + id: Button2 + key: 2 + on_press: + then: + - switch.turn_on: Led2 + on_release: + then: + - switch.turn_off: Led2 + - platform: tm1638 + id: Button3 + key: 3 + on_press: + then: + - switch.turn_on: Led3 + on_release: + then: + - switch.turn_off: Led3 + - platform: tm1638 + id: Button4 + key: 4 + on_press: + then: + - output.turn_on: Led4 + on_release: + then: + - output.turn_off: Led4 + - platform: tm1638 + id: Button5 + key: 5 + on_press: + then: + - output.turn_on: Led5 + on_release: + then: + - output.turn_off: Led5 + - platform: tm1638 + id: Button6 + key: 6 + on_press: + then: + - output.turn_on: Led6 + on_release: + then: + - output.turn_off: Led6 + - platform: tm1638 + id: Button7 + key: 7 + on_press: + then: + - output.turn_on: Led7 + on_release: + then: + - output.turn_off: Led7 + +switch: + - platform: tm1638 + id: Led0 + led: 0 + name: TM1638Led0 + - platform: tm1638 + id: Led1 + led: 1 + name: TM1638Led1 + - platform: tm1638 + id: Led2 + led: 2 + name: TM1638Led2 + - platform: tm1638 + id: Led3 + led: 3 + name: TM1638Led3 + +output: + - platform: tm1638 + id: Led4 + led: 4 + - platform: tm1638 + id: Led5 + led: 5 + - platform: tm1638 + id: Led6 + led: 6 + - platform: tm1638 + id: Led7 + led: 7 diff --git a/tests/components/tm1638/test.esp32-ard.yaml b/tests/components/tm1638/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1638/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1638/test.esp32-c3-ard.yaml b/tests/components/tm1638/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1638/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1638/test.esp32-c3-idf.yaml b/tests/components/tm1638/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1638/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1638/test.esp32-idf.yaml b/tests/components/tm1638/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1638/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1638/test.esp8266-ard.yaml b/tests/components/tm1638/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1638/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1638/test.rp2040-ard.yaml b/tests/components/tm1638/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1638/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1651/common.yaml b/tests/components/tm1651/common.yaml new file mode 100644 index 0000000000..667648f4d6 --- /dev/null +++ b/tests/components/tm1651/common.yaml @@ -0,0 +1,21 @@ +tm1651: + id: tm1651_battery + clk_pin: 5 + dio_pin: 4 + +esphome: + on_boot: + then: + - tm1651.set_level_percent: + id: tm1651_battery + level_percent: 50 + - tm1651.set_level: + id: tm1651_battery + level: 5 + - tm1651.set_brightness: + id: tm1651_battery + brightness: 2 + - tm1651.turn_on: + id: tm1651_battery + - tm1651.turn_off: + id: tm1651_battery diff --git a/tests/components/tm1651/test.esp32-ard.yaml b/tests/components/tm1651/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1651/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp32-c3-ard.yaml b/tests/components/tm1651/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1651/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp8266-ard.yaml b/tests/components/tm1651/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1651/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1651/test.rp2040-ard.yaml b/tests/components/tm1651/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1651/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tmp102/test.esp32-ard.yaml b/tests/components/tmp102/test.esp32-ard.yaml new file mode 100644 index 0000000000..840bf7edb3 --- /dev/null +++ b/tests/components/tmp102/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tmp102 + scl: 16 + sda: 17 + +sensor: + - platform: tmp102 + name: TMP102 Temperature diff --git a/tests/components/tmp102/test.esp32-c3-ard.yaml b/tests/components/tmp102/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c1d35fca3f --- /dev/null +++ b/tests/components/tmp102/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tmp102 + scl: 5 + sda: 4 + +sensor: + - platform: tmp102 + name: TMP102 Temperature diff --git a/tests/components/tmp102/test.esp32-c3-idf.yaml b/tests/components/tmp102/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c1d35fca3f --- /dev/null +++ b/tests/components/tmp102/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tmp102 + scl: 5 + sda: 4 + +sensor: + - platform: tmp102 + name: TMP102 Temperature diff --git a/tests/components/tmp102/test.esp32-idf.yaml b/tests/components/tmp102/test.esp32-idf.yaml new file mode 100644 index 0000000000..840bf7edb3 --- /dev/null +++ b/tests/components/tmp102/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tmp102 + scl: 16 + sda: 17 + +sensor: + - platform: tmp102 + name: TMP102 Temperature diff --git a/tests/components/tmp102/test.esp8266-ard.yaml b/tests/components/tmp102/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c1d35fca3f --- /dev/null +++ b/tests/components/tmp102/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tmp102 + scl: 5 + sda: 4 + +sensor: + - platform: tmp102 + name: TMP102 Temperature diff --git a/tests/components/tmp102/test.rp2040-ard.yaml b/tests/components/tmp102/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c1d35fca3f --- /dev/null +++ b/tests/components/tmp102/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +i2c: + - id: i2c_tmp102 + scl: 5 + sda: 4 + +sensor: + - platform: tmp102 + name: TMP102 Temperature diff --git a/tests/components/tmp1075/test.esp32-ard.yaml b/tests/components/tmp1075/test.esp32-ard.yaml new file mode 100644 index 0000000000..6c50d0da77 --- /dev/null +++ b/tests/components/tmp1075/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_tmp1075 + scl: 16 + sda: 17 + +sensor: + - platform: tmp1075 + name: Temperature TMP1075 + conversion_rate: 27.5ms + alert: + limit_low: 50 + limit_high: 75 + fault_count: 1 + polarity: active_high + function: comparator + update_interval: 10s diff --git a/tests/components/tmp1075/test.esp32-c3-ard.yaml b/tests/components/tmp1075/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..99433aa655 --- /dev/null +++ b/tests/components/tmp1075/test.esp32-c3-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_tmp1075 + scl: 5 + sda: 4 + +sensor: + - platform: tmp1075 + name: Temperature TMP1075 + conversion_rate: 27.5ms + alert: + limit_low: 50 + limit_high: 75 + fault_count: 1 + polarity: active_high + function: comparator + update_interval: 10s diff --git a/tests/components/tmp1075/test.esp32-c3-idf.yaml b/tests/components/tmp1075/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..99433aa655 --- /dev/null +++ b/tests/components/tmp1075/test.esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_tmp1075 + scl: 5 + sda: 4 + +sensor: + - platform: tmp1075 + name: Temperature TMP1075 + conversion_rate: 27.5ms + alert: + limit_low: 50 + limit_high: 75 + fault_count: 1 + polarity: active_high + function: comparator + update_interval: 10s diff --git a/tests/components/tmp1075/test.esp32-idf.yaml b/tests/components/tmp1075/test.esp32-idf.yaml new file mode 100644 index 0000000000..6c50d0da77 --- /dev/null +++ b/tests/components/tmp1075/test.esp32-idf.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_tmp1075 + scl: 16 + sda: 17 + +sensor: + - platform: tmp1075 + name: Temperature TMP1075 + conversion_rate: 27.5ms + alert: + limit_low: 50 + limit_high: 75 + fault_count: 1 + polarity: active_high + function: comparator + update_interval: 10s diff --git a/tests/components/tmp1075/test.esp8266-ard.yaml b/tests/components/tmp1075/test.esp8266-ard.yaml new file mode 100644 index 0000000000..99433aa655 --- /dev/null +++ b/tests/components/tmp1075/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_tmp1075 + scl: 5 + sda: 4 + +sensor: + - platform: tmp1075 + name: Temperature TMP1075 + conversion_rate: 27.5ms + alert: + limit_low: 50 + limit_high: 75 + fault_count: 1 + polarity: active_high + function: comparator + update_interval: 10s diff --git a/tests/components/tmp1075/test.rp2040-ard.yaml b/tests/components/tmp1075/test.rp2040-ard.yaml new file mode 100644 index 0000000000..99433aa655 --- /dev/null +++ b/tests/components/tmp1075/test.rp2040-ard.yaml @@ -0,0 +1,16 @@ +i2c: + - id: i2c_tmp1075 + scl: 5 + sda: 4 + +sensor: + - platform: tmp1075 + name: Temperature TMP1075 + conversion_rate: 27.5ms + alert: + limit_low: 50 + limit_high: 75 + fault_count: 1 + polarity: active_high + function: comparator + update_interval: 10s diff --git a/tests/components/tmp117/test.esp32-ard.yaml b/tests/components/tmp117/test.esp32-ard.yaml new file mode 100644 index 0000000000..03e0dd4e8e --- /dev/null +++ b/tests/components/tmp117/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tmp117 + scl: 16 + sda: 17 + +sensor: + - platform: tmp117 + name: TMP117 Temperature + update_interval: 5s diff --git a/tests/components/tmp117/test.esp32-c3-ard.yaml b/tests/components/tmp117/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..61fc2cc03d --- /dev/null +++ b/tests/components/tmp117/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tmp117 + scl: 5 + sda: 4 + +sensor: + - platform: tmp117 + name: TMP117 Temperature + update_interval: 5s diff --git a/tests/components/tmp117/test.esp32-c3-idf.yaml b/tests/components/tmp117/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..61fc2cc03d --- /dev/null +++ b/tests/components/tmp117/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tmp117 + scl: 5 + sda: 4 + +sensor: + - platform: tmp117 + name: TMP117 Temperature + update_interval: 5s diff --git a/tests/components/tmp117/test.esp32-idf.yaml b/tests/components/tmp117/test.esp32-idf.yaml new file mode 100644 index 0000000000..03e0dd4e8e --- /dev/null +++ b/tests/components/tmp117/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tmp117 + scl: 16 + sda: 17 + +sensor: + - platform: tmp117 + name: TMP117 Temperature + update_interval: 5s diff --git a/tests/components/tmp117/test.esp8266-ard.yaml b/tests/components/tmp117/test.esp8266-ard.yaml new file mode 100644 index 0000000000..61fc2cc03d --- /dev/null +++ b/tests/components/tmp117/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tmp117 + scl: 5 + sda: 4 + +sensor: + - platform: tmp117 + name: TMP117 Temperature + update_interval: 5s diff --git a/tests/components/tmp117/test.rp2040-ard.yaml b/tests/components/tmp117/test.rp2040-ard.yaml new file mode 100644 index 0000000000..61fc2cc03d --- /dev/null +++ b/tests/components/tmp117/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tmp117 + scl: 5 + sda: 4 + +sensor: + - platform: tmp117 + name: TMP117 Temperature + update_interval: 5s diff --git a/tests/components/tof10120/test.esp32-ard.yaml b/tests/components/tof10120/test.esp32-ard.yaml new file mode 100644 index 0000000000..74541ecde8 --- /dev/null +++ b/tests/components/tof10120/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tof10120 + scl: 16 + sda: 17 + +sensor: + - platform: tof10120 + name: Distance sensor + update_interval: 5s diff --git a/tests/components/tof10120/test.esp32-c3-ard.yaml b/tests/components/tof10120/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..01cde0df6a --- /dev/null +++ b/tests/components/tof10120/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tof10120 + scl: 5 + sda: 4 + +sensor: + - platform: tof10120 + name: Distance sensor + update_interval: 5s diff --git a/tests/components/tof10120/test.esp32-c3-idf.yaml b/tests/components/tof10120/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..01cde0df6a --- /dev/null +++ b/tests/components/tof10120/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tof10120 + scl: 5 + sda: 4 + +sensor: + - platform: tof10120 + name: Distance sensor + update_interval: 5s diff --git a/tests/components/tof10120/test.esp32-idf.yaml b/tests/components/tof10120/test.esp32-idf.yaml new file mode 100644 index 0000000000..74541ecde8 --- /dev/null +++ b/tests/components/tof10120/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tof10120 + scl: 16 + sda: 17 + +sensor: + - platform: tof10120 + name: Distance sensor + update_interval: 5s diff --git a/tests/components/tof10120/test.esp8266-ard.yaml b/tests/components/tof10120/test.esp8266-ard.yaml new file mode 100644 index 0000000000..01cde0df6a --- /dev/null +++ b/tests/components/tof10120/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tof10120 + scl: 5 + sda: 4 + +sensor: + - platform: tof10120 + name: Distance sensor + update_interval: 5s diff --git a/tests/components/tof10120/test.rp2040-ard.yaml b/tests/components/tof10120/test.rp2040-ard.yaml new file mode 100644 index 0000000000..01cde0df6a --- /dev/null +++ b/tests/components/tof10120/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_tof10120 + scl: 5 + sda: 4 + +sensor: + - platform: tof10120 + name: Distance sensor + update_interval: 5s diff --git a/tests/components/toshiba/test.esp32-ard.yaml b/tests/components/toshiba/test.esp32-ard.yaml new file mode 100644 index 0000000000..c134c7f5bd --- /dev/null +++ b/tests/components/toshiba/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: toshiba + name: Toshiba Climate diff --git a/tests/components/toshiba/test.esp32-c3-ard.yaml b/tests/components/toshiba/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c134c7f5bd --- /dev/null +++ b/tests/components/toshiba/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: toshiba + name: Toshiba Climate diff --git a/tests/components/toshiba/test.esp32-c3-idf.yaml b/tests/components/toshiba/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c134c7f5bd --- /dev/null +++ b/tests/components/toshiba/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: toshiba + name: Toshiba Climate diff --git a/tests/components/toshiba/test.esp32-idf.yaml b/tests/components/toshiba/test.esp32-idf.yaml new file mode 100644 index 0000000000..c134c7f5bd --- /dev/null +++ b/tests/components/toshiba/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: toshiba + name: Toshiba Climate diff --git a/tests/components/toshiba/test.esp8266-ard.yaml b/tests/components/toshiba/test.esp8266-ard.yaml new file mode 100644 index 0000000000..8730a5d4ab --- /dev/null +++ b/tests/components/toshiba/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: toshiba + name: Toshiba Climate diff --git a/tests/components/total_daily_energy/test.esp32-ard.yaml b/tests/components/total_daily_energy/test.esp32-ard.yaml new file mode 100644 index 0000000000..34d452aae5 --- /dev/null +++ b/tests/components/total_daily_energy/test.esp32-ard.yaml @@ -0,0 +1,32 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + +sensor: + - platform: hlw8012 + sel_pin: 15 + cf_pin: 14 + cf1_pin: 13 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE + model: hlw8012 + - platform: total_daily_energy + name: HLW8012 Total Daily Energy + power_id: hlw8012_power diff --git a/tests/components/total_daily_energy/test.esp32-c3-ard.yaml b/tests/components/total_daily_energy/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..71afa45ed5 --- /dev/null +++ b/tests/components/total_daily_energy/test.esp32-c3-ard.yaml @@ -0,0 +1,32 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + +sensor: + - platform: hlw8012 + sel_pin: 5 + cf_pin: 4 + cf1_pin: 3 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE + model: hlw8012 + - platform: total_daily_energy + name: HLW8012 Total Daily Energy + power_id: hlw8012_power diff --git a/tests/components/total_daily_energy/test.esp32-c3-idf.yaml b/tests/components/total_daily_energy/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..71afa45ed5 --- /dev/null +++ b/tests/components/total_daily_energy/test.esp32-c3-idf.yaml @@ -0,0 +1,32 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + +sensor: + - platform: hlw8012 + sel_pin: 5 + cf_pin: 4 + cf1_pin: 3 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE + model: hlw8012 + - platform: total_daily_energy + name: HLW8012 Total Daily Energy + power_id: hlw8012_power diff --git a/tests/components/total_daily_energy/test.esp32-idf.yaml b/tests/components/total_daily_energy/test.esp32-idf.yaml new file mode 100644 index 0000000000..34d452aae5 --- /dev/null +++ b/tests/components/total_daily_energy/test.esp32-idf.yaml @@ -0,0 +1,32 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + +sensor: + - platform: hlw8012 + sel_pin: 15 + cf_pin: 14 + cf1_pin: 13 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE + model: hlw8012 + - platform: total_daily_energy + name: HLW8012 Total Daily Energy + power_id: hlw8012_power diff --git a/tests/components/total_daily_energy/test.esp8266-ard.yaml b/tests/components/total_daily_energy/test.esp8266-ard.yaml new file mode 100644 index 0000000000..34d452aae5 --- /dev/null +++ b/tests/components/total_daily_energy/test.esp8266-ard.yaml @@ -0,0 +1,32 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + +sensor: + - platform: hlw8012 + sel_pin: 15 + cf_pin: 14 + cf1_pin: 13 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE + model: hlw8012 + - platform: total_daily_energy + name: HLW8012 Total Daily Energy + power_id: hlw8012_power diff --git a/tests/components/total_daily_energy/test.rp2040-ard.yaml b/tests/components/total_daily_energy/test.rp2040-ard.yaml new file mode 100644 index 0000000000..71afa45ed5 --- /dev/null +++ b/tests/components/total_daily_energy/test.rp2040-ard.yaml @@ -0,0 +1,32 @@ +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + +sensor: + - platform: hlw8012 + sel_pin: 5 + cf_pin: 4 + cf1_pin: 3 + current: + name: HLW8012 Current + voltage: + name: HLW8012 Voltage + power: + name: HLW8012 Power + id: hlw8012_power + energy: + name: HLW8012 Energy + id: hlw8012_energy + update_interval: 15s + current_resistor: 0.001 ohm + voltage_divider: 2351 + change_mode_every: "never" + initial_mode: VOLTAGE + model: hlw8012 + - platform: total_daily_energy + name: HLW8012 Total Daily Energy + power_id: hlw8012_power diff --git a/tests/components/tsl2561/test.esp32-ard.yaml b/tests/components/tsl2561/test.esp32-ard.yaml new file mode 100644 index 0000000000..8d43c62414 --- /dev/null +++ b/tests/components/tsl2561/test.esp32-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_tsl2561 + scl: 16 + sda: 17 + +sensor: + - platform: tsl2561 + name: TSL2561 Ambient Light + address: 0x39 + is_cs_package: true + integration_time: 402ms + gain: 16x + update_interval: 15s diff --git a/tests/components/tsl2561/test.esp32-c3-ard.yaml b/tests/components/tsl2561/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..1ea768c5d9 --- /dev/null +++ b/tests/components/tsl2561/test.esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_tsl2561 + scl: 5 + sda: 4 + +sensor: + - platform: tsl2561 + name: TSL2561 Ambient Light + address: 0x39 + is_cs_package: true + integration_time: 402ms + gain: 16x + update_interval: 15s diff --git a/tests/components/tsl2561/test.esp32-c3-idf.yaml b/tests/components/tsl2561/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..1ea768c5d9 --- /dev/null +++ b/tests/components/tsl2561/test.esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_tsl2561 + scl: 5 + sda: 4 + +sensor: + - platform: tsl2561 + name: TSL2561 Ambient Light + address: 0x39 + is_cs_package: true + integration_time: 402ms + gain: 16x + update_interval: 15s diff --git a/tests/components/tsl2561/test.esp32-idf.yaml b/tests/components/tsl2561/test.esp32-idf.yaml new file mode 100644 index 0000000000..8d43c62414 --- /dev/null +++ b/tests/components/tsl2561/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_tsl2561 + scl: 16 + sda: 17 + +sensor: + - platform: tsl2561 + name: TSL2561 Ambient Light + address: 0x39 + is_cs_package: true + integration_time: 402ms + gain: 16x + update_interval: 15s diff --git a/tests/components/tsl2561/test.esp8266-ard.yaml b/tests/components/tsl2561/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1ea768c5d9 --- /dev/null +++ b/tests/components/tsl2561/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_tsl2561 + scl: 5 + sda: 4 + +sensor: + - platform: tsl2561 + name: TSL2561 Ambient Light + address: 0x39 + is_cs_package: true + integration_time: 402ms + gain: 16x + update_interval: 15s diff --git a/tests/components/tsl2561/test.rp2040-ard.yaml b/tests/components/tsl2561/test.rp2040-ard.yaml new file mode 100644 index 0000000000..1ea768c5d9 --- /dev/null +++ b/tests/components/tsl2561/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_tsl2561 + scl: 5 + sda: 4 + +sensor: + - platform: tsl2561 + name: TSL2561 Ambient Light + address: 0x39 + is_cs_package: true + integration_time: 402ms + gain: 16x + update_interval: 15s diff --git a/tests/components/tsl2591/test.esp32-ard.yaml b/tests/components/tsl2591/test.esp32-ard.yaml new file mode 100644 index 0000000000..14f9311ae6 --- /dev/null +++ b/tests/components/tsl2591/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tsl2591 + scl: 16 + sda: 17 + +sensor: + - platform: tsl2591 + id: test_tsl2591 + address: 0x29 + integration_time: 600ms + gain: high + visible: + name: tsl2591 visible + id: tsl2591_vis + unit_of_measurement: pH + infrared: + name: tsl2591 infrared + id: tsl2591_ir + full_spectrum: + name: tsl2591 full_spectrum + id: tsl2591_fs + calculated_lux: + name: tsl2591 calculated_lux + id: tsl2591_cl + update_interval: 15s diff --git a/tests/components/tsl2591/test.esp32-c3-ard.yaml b/tests/components/tsl2591/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..de57ef548a --- /dev/null +++ b/tests/components/tsl2591/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tsl2591 + scl: 5 + sda: 4 + +sensor: + - platform: tsl2591 + id: test_tsl2591 + address: 0x29 + integration_time: 600ms + gain: high + visible: + name: tsl2591 visible + id: tsl2591_vis + unit_of_measurement: pH + infrared: + name: tsl2591 infrared + id: tsl2591_ir + full_spectrum: + name: tsl2591 full_spectrum + id: tsl2591_fs + calculated_lux: + name: tsl2591 calculated_lux + id: tsl2591_cl + update_interval: 15s diff --git a/tests/components/tsl2591/test.esp32-c3-idf.yaml b/tests/components/tsl2591/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..de57ef548a --- /dev/null +++ b/tests/components/tsl2591/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tsl2591 + scl: 5 + sda: 4 + +sensor: + - platform: tsl2591 + id: test_tsl2591 + address: 0x29 + integration_time: 600ms + gain: high + visible: + name: tsl2591 visible + id: tsl2591_vis + unit_of_measurement: pH + infrared: + name: tsl2591 infrared + id: tsl2591_ir + full_spectrum: + name: tsl2591 full_spectrum + id: tsl2591_fs + calculated_lux: + name: tsl2591 calculated_lux + id: tsl2591_cl + update_interval: 15s diff --git a/tests/components/tsl2591/test.esp32-idf.yaml b/tests/components/tsl2591/test.esp32-idf.yaml new file mode 100644 index 0000000000..14f9311ae6 --- /dev/null +++ b/tests/components/tsl2591/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tsl2591 + scl: 16 + sda: 17 + +sensor: + - platform: tsl2591 + id: test_tsl2591 + address: 0x29 + integration_time: 600ms + gain: high + visible: + name: tsl2591 visible + id: tsl2591_vis + unit_of_measurement: pH + infrared: + name: tsl2591 infrared + id: tsl2591_ir + full_spectrum: + name: tsl2591 full_spectrum + id: tsl2591_fs + calculated_lux: + name: tsl2591 calculated_lux + id: tsl2591_cl + update_interval: 15s diff --git a/tests/components/tsl2591/test.esp8266-ard.yaml b/tests/components/tsl2591/test.esp8266-ard.yaml new file mode 100644 index 0000000000..de57ef548a --- /dev/null +++ b/tests/components/tsl2591/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tsl2591 + scl: 5 + sda: 4 + +sensor: + - platform: tsl2591 + id: test_tsl2591 + address: 0x29 + integration_time: 600ms + gain: high + visible: + name: tsl2591 visible + id: tsl2591_vis + unit_of_measurement: pH + infrared: + name: tsl2591 infrared + id: tsl2591_ir + full_spectrum: + name: tsl2591 full_spectrum + id: tsl2591_fs + calculated_lux: + name: tsl2591 calculated_lux + id: tsl2591_cl + update_interval: 15s diff --git a/tests/components/tsl2591/test.rp2040-ard.yaml b/tests/components/tsl2591/test.rp2040-ard.yaml new file mode 100644 index 0000000000..de57ef548a --- /dev/null +++ b/tests/components/tsl2591/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tsl2591 + scl: 5 + sda: 4 + +sensor: + - platform: tsl2591 + id: test_tsl2591 + address: 0x29 + integration_time: 600ms + gain: high + visible: + name: tsl2591 visible + id: tsl2591_vis + unit_of_measurement: pH + infrared: + name: tsl2591 infrared + id: tsl2591_ir + full_spectrum: + name: tsl2591 full_spectrum + id: tsl2591_fs + calculated_lux: + name: tsl2591 calculated_lux + id: tsl2591_cl + update_interval: 15s diff --git a/tests/components/tt21100/test.esp32-ard.yaml b/tests/components/tt21100/test.esp32-ard.yaml new file mode 100644 index 0000000000..2419b0ad6a --- /dev/null +++ b/tests/components/tt21100/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tt21100 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: tt21100 + display: ssd1306_display + interrupt_pin: 14 + reset_pin: 15 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 diff --git a/tests/components/tt21100/test.esp32-c3-ard.yaml b/tests/components/tt21100/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..17b8c8065a --- /dev/null +++ b/tests/components/tt21100/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tt21100 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: tt21100 + display: ssd1306_display + interrupt_pin: 6 + reset_pin: 7 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 diff --git a/tests/components/tt21100/test.esp32-c3-idf.yaml b/tests/components/tt21100/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..17b8c8065a --- /dev/null +++ b/tests/components/tt21100/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tt21100 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: tt21100 + display: ssd1306_display + interrupt_pin: 6 + reset_pin: 7 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 diff --git a/tests/components/tt21100/test.esp32-idf.yaml b/tests/components/tt21100/test.esp32-idf.yaml new file mode 100644 index 0000000000..2419b0ad6a --- /dev/null +++ b/tests/components/tt21100/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tt21100 + scl: 16 + sda: 17 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: tt21100 + display: ssd1306_display + interrupt_pin: 14 + reset_pin: 15 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 diff --git a/tests/components/tt21100/test.esp32-s2-ard.yaml b/tests/components/tt21100/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..86b9e7530d --- /dev/null +++ b/tests/components/tt21100/test.esp32-s2-ard.yaml @@ -0,0 +1,44 @@ +i2c: + sda: GPIO8 + scl: GPIO18 + +spi: + clk_pin: 7 + mosi_pin: 11 + miso_pin: 9 + +display: + - platform: ili9xxx + id: my_display + model: ili9341 + cs_pin: 5 + dc_pin: 12 + reset_pin: 33 + auto_clear_enabled: false + data_rate: 40MHz + dimensions: 320x240 + update_interval: never + invert_colors: false + transform: + mirror_y: false + mirror_x: false + swap_xy: true + +touchscreen: + - platform: tt21100 + address: 0x24 + interrupt_pin: GPIO3 + on_touch: + - logger.log: "Touchscreen:: Touched" + +binary_sensor: + - platform: tt21100 + index: 0 + name: "Home" + + - platform: touchscreen + name: FanLo + x_min: 0 + x_max: 105 + y_min: 0 + y_max: 80 diff --git a/tests/components/tt21100/test.esp8266-ard.yaml b/tests/components/tt21100/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1393019417 --- /dev/null +++ b/tests/components/tt21100/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tt21100 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 13 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: tt21100 + display: ssd1306_display + interrupt_pin: 14 + reset_pin: 15 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 diff --git a/tests/components/tt21100/test.rp2040-ard.yaml b/tests/components/tt21100/test.rp2040-ard.yaml new file mode 100644 index 0000000000..17b8c8065a --- /dev/null +++ b/tests/components/tt21100/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_tt21100 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 3 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: tt21100 + display: ssd1306_display + interrupt_pin: 6 + reset_pin: 7 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 diff --git a/tests/components/ttp229_bsf/test.esp32-ard.yaml b/tests/components/ttp229_bsf/test.esp32-ard.yaml new file mode 100644 index 0000000000..edee6d164e --- /dev/null +++ b/tests/components/ttp229_bsf/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +ttp229_bsf: + scl_pin: 16 + sdo_pin: 17 + +binary_sensor: + - platform: ttp229_bsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_bsf/test.esp32-c3-ard.yaml b/tests/components/ttp229_bsf/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2006061c6e --- /dev/null +++ b/tests/components/ttp229_bsf/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +ttp229_bsf: + scl_pin: 5 + sdo_pin: 4 + +binary_sensor: + - platform: ttp229_bsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml b/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2006061c6e --- /dev/null +++ b/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +ttp229_bsf: + scl_pin: 5 + sdo_pin: 4 + +binary_sensor: + - platform: ttp229_bsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_bsf/test.esp32-idf.yaml b/tests/components/ttp229_bsf/test.esp32-idf.yaml new file mode 100644 index 0000000000..edee6d164e --- /dev/null +++ b/tests/components/ttp229_bsf/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +ttp229_bsf: + scl_pin: 16 + sdo_pin: 17 + +binary_sensor: + - platform: ttp229_bsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_bsf/test.esp8266-ard.yaml b/tests/components/ttp229_bsf/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2006061c6e --- /dev/null +++ b/tests/components/ttp229_bsf/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +ttp229_bsf: + scl_pin: 5 + sdo_pin: 4 + +binary_sensor: + - platform: ttp229_bsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_bsf/test.rp2040-ard.yaml b/tests/components/ttp229_bsf/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2006061c6e --- /dev/null +++ b/tests/components/ttp229_bsf/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +ttp229_bsf: + scl_pin: 5 + sdo_pin: 4 + +binary_sensor: + - platform: ttp229_bsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_lsf/test.esp32-ard.yaml b/tests/components/ttp229_lsf/test.esp32-ard.yaml new file mode 100644 index 0000000000..81fb965883 --- /dev/null +++ b/tests/components/ttp229_lsf/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_ttp229_lsf + scl: 16 + sda: 17 + +ttp229_lsf: + +binary_sensor: + - platform: ttp229_lsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_lsf/test.esp32-c3-ard.yaml b/tests/components/ttp229_lsf/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..3927aff40e --- /dev/null +++ b/tests/components/ttp229_lsf/test.esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_ttp229_lsf + scl: 5 + sda: 4 + +ttp229_lsf: + +binary_sensor: + - platform: ttp229_lsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml b/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..3927aff40e --- /dev/null +++ b/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_ttp229_lsf + scl: 5 + sda: 4 + +ttp229_lsf: + +binary_sensor: + - platform: ttp229_lsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_lsf/test.esp32-idf.yaml b/tests/components/ttp229_lsf/test.esp32-idf.yaml new file mode 100644 index 0000000000..81fb965883 --- /dev/null +++ b/tests/components/ttp229_lsf/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_ttp229_lsf + scl: 16 + sda: 17 + +ttp229_lsf: + +binary_sensor: + - platform: ttp229_lsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_lsf/test.esp8266-ard.yaml b/tests/components/ttp229_lsf/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3927aff40e --- /dev/null +++ b/tests/components/ttp229_lsf/test.esp8266-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_ttp229_lsf + scl: 5 + sda: 4 + +ttp229_lsf: + +binary_sensor: + - platform: ttp229_lsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/ttp229_lsf/test.rp2040-ard.yaml b/tests/components/ttp229_lsf/test.rp2040-ard.yaml new file mode 100644 index 0000000000..3927aff40e --- /dev/null +++ b/tests/components/ttp229_lsf/test.rp2040-ard.yaml @@ -0,0 +1,11 @@ +i2c: + - id: i2c_ttp229_lsf + scl: 5 + sda: 4 + +ttp229_lsf: + +binary_sensor: + - platform: ttp229_lsf + name: TTP229 Channel 0 + channel: 0 diff --git a/tests/components/tuya/test.esp32-ard.yaml b/tests/components/tuya/test.esp32-ard.yaml new file mode 100644 index 0000000000..9105522dcd --- /dev/null +++ b/tests/components/tuya/test.esp32-ard.yaml @@ -0,0 +1,78 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_tuya + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +tuya: + status_pin: + number: 15 + inverted: true + on_datapoint_update: + - sensor_datapoint: 6 + datapoint_type: raw + then: + - logger.log: Datapoint 6 updated + +binary_sensor: + - platform: tuya + id: tuya_binary_sensor + sensor_datapoint: 1 + +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 + reports_fahrenheit: true + +cover: + - platform: tuya + id: tuya_cover + position_datapoint: 2 + +light: + - platform: tuya + id: tuya_light + switch_datapoint: 1 + dimmer_datapoint: 2 + min_value_datapoint: 3 + color_temperature_datapoint: 4 + min_value: 1 + max_value: 100 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + gamma_correct: 1 + +number: + - platform: tuya + id: tuya_number + number_datapoint: 102 + min_value: 0 + max_value: 17 + step: 1 + +select: + - platform: tuya + id: tuya_select + enum_datapoint: 42 + options: + 0: Internal + 1: Floor + 2: Both + +sensor: + - platform: tuya + id: tuya_sensor + sensor_datapoint: 1 + +switch: + - platform: tuya + id: tuya_switch + switch_datapoint: 1 diff --git a/tests/components/tuya/test.esp32-c3-ard.yaml b/tests/components/tuya/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4892e807b1 --- /dev/null +++ b/tests/components/tuya/test.esp32-c3-ard.yaml @@ -0,0 +1,78 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_tuya + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +tuya: + status_pin: + number: 6 + inverted: true + on_datapoint_update: + - sensor_datapoint: 6 + datapoint_type: raw + then: + - logger.log: Datapoint 6 updated + +binary_sensor: + - platform: tuya + id: tuya_binary_sensor + sensor_datapoint: 1 + +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 + reports_fahrenheit: true + +cover: + - platform: tuya + id: tuya_cover + position_datapoint: 2 + +light: + - platform: tuya + id: tuya_light + switch_datapoint: 1 + dimmer_datapoint: 2 + min_value_datapoint: 3 + color_temperature_datapoint: 4 + min_value: 1 + max_value: 100 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + gamma_correct: 1 + +number: + - platform: tuya + id: tuya_number + number_datapoint: 102 + min_value: 0 + max_value: 17 + step: 1 + +select: + - platform: tuya + id: tuya_select + enum_datapoint: 42 + options: + 0: Internal + 1: Floor + 2: Both + +sensor: + - platform: tuya + id: tuya_sensor + sensor_datapoint: 1 + +switch: + - platform: tuya + id: tuya_switch + switch_datapoint: 1 diff --git a/tests/components/tuya/test.esp32-c3-idf.yaml b/tests/components/tuya/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4892e807b1 --- /dev/null +++ b/tests/components/tuya/test.esp32-c3-idf.yaml @@ -0,0 +1,78 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_tuya + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +tuya: + status_pin: + number: 6 + inverted: true + on_datapoint_update: + - sensor_datapoint: 6 + datapoint_type: raw + then: + - logger.log: Datapoint 6 updated + +binary_sensor: + - platform: tuya + id: tuya_binary_sensor + sensor_datapoint: 1 + +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 + reports_fahrenheit: true + +cover: + - platform: tuya + id: tuya_cover + position_datapoint: 2 + +light: + - platform: tuya + id: tuya_light + switch_datapoint: 1 + dimmer_datapoint: 2 + min_value_datapoint: 3 + color_temperature_datapoint: 4 + min_value: 1 + max_value: 100 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + gamma_correct: 1 + +number: + - platform: tuya + id: tuya_number + number_datapoint: 102 + min_value: 0 + max_value: 17 + step: 1 + +select: + - platform: tuya + id: tuya_select + enum_datapoint: 42 + options: + 0: Internal + 1: Floor + 2: Both + +sensor: + - platform: tuya + id: tuya_sensor + sensor_datapoint: 1 + +switch: + - platform: tuya + id: tuya_switch + switch_datapoint: 1 diff --git a/tests/components/tuya/test.esp32-idf.yaml b/tests/components/tuya/test.esp32-idf.yaml new file mode 100644 index 0000000000..9105522dcd --- /dev/null +++ b/tests/components/tuya/test.esp32-idf.yaml @@ -0,0 +1,78 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_tuya + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +tuya: + status_pin: + number: 15 + inverted: true + on_datapoint_update: + - sensor_datapoint: 6 + datapoint_type: raw + then: + - logger.log: Datapoint 6 updated + +binary_sensor: + - platform: tuya + id: tuya_binary_sensor + sensor_datapoint: 1 + +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 + reports_fahrenheit: true + +cover: + - platform: tuya + id: tuya_cover + position_datapoint: 2 + +light: + - platform: tuya + id: tuya_light + switch_datapoint: 1 + dimmer_datapoint: 2 + min_value_datapoint: 3 + color_temperature_datapoint: 4 + min_value: 1 + max_value: 100 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + gamma_correct: 1 + +number: + - platform: tuya + id: tuya_number + number_datapoint: 102 + min_value: 0 + max_value: 17 + step: 1 + +select: + - platform: tuya + id: tuya_select + enum_datapoint: 42 + options: + 0: Internal + 1: Floor + 2: Both + +sensor: + - platform: tuya + id: tuya_sensor + sensor_datapoint: 1 + +switch: + - platform: tuya + id: tuya_switch + switch_datapoint: 1 diff --git a/tests/components/tuya/test.esp8266-ard.yaml b/tests/components/tuya/test.esp8266-ard.yaml new file mode 100644 index 0000000000..56177fb982 --- /dev/null +++ b/tests/components/tuya/test.esp8266-ard.yaml @@ -0,0 +1,78 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_tuya + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +tuya: + status_pin: + number: 16 + inverted: true + on_datapoint_update: + - sensor_datapoint: 6 + datapoint_type: raw + then: + - logger.log: Datapoint 6 updated + +binary_sensor: + - platform: tuya + id: tuya_binary_sensor + sensor_datapoint: 1 + +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 + reports_fahrenheit: true + +cover: + - platform: tuya + id: tuya_cover + position_datapoint: 2 + +light: + - platform: tuya + id: tuya_light + switch_datapoint: 1 + dimmer_datapoint: 2 + min_value_datapoint: 3 + color_temperature_datapoint: 4 + min_value: 1 + max_value: 100 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + gamma_correct: 1 + +number: + - platform: tuya + id: tuya_number + number_datapoint: 102 + min_value: 0 + max_value: 17 + step: 1 + +select: + - platform: tuya + id: tuya_select + enum_datapoint: 42 + options: + 0: Internal + 1: Floor + 2: Both + +sensor: + - platform: tuya + id: tuya_sensor + sensor_datapoint: 1 + +switch: + - platform: tuya + id: tuya_switch + switch_datapoint: 1 diff --git a/tests/components/tuya/test.rp2040-ard.yaml b/tests/components/tuya/test.rp2040-ard.yaml new file mode 100644 index 0000000000..4892e807b1 --- /dev/null +++ b/tests/components/tuya/test.rp2040-ard.yaml @@ -0,0 +1,78 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uart_tuya + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +tuya: + status_pin: + number: 6 + inverted: true + on_datapoint_update: + - sensor_datapoint: 6 + datapoint_type: raw + then: + - logger.log: Datapoint 6 updated + +binary_sensor: + - platform: tuya + id: tuya_binary_sensor + sensor_datapoint: 1 + +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 + reports_fahrenheit: true + +cover: + - platform: tuya + id: tuya_cover + position_datapoint: 2 + +light: + - platform: tuya + id: tuya_light + switch_datapoint: 1 + dimmer_datapoint: 2 + min_value_datapoint: 3 + color_temperature_datapoint: 4 + min_value: 1 + max_value: 100 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + gamma_correct: 1 + +number: + - platform: tuya + id: tuya_number + number_datapoint: 102 + min_value: 0 + max_value: 17 + step: 1 + +select: + - platform: tuya + id: tuya_select + enum_datapoint: 42 + options: + 0: Internal + 1: Floor + 2: Both + +sensor: + - platform: tuya + id: tuya_sensor + sensor_datapoint: 1 + +switch: + - platform: tuya + id: tuya_switch + switch_datapoint: 1 diff --git a/tests/components/tx20/common.yaml b/tests/components/tx20/common.yaml new file mode 100644 index 0000000000..d826059320 --- /dev/null +++ b/tests/components/tx20/common.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: tx20 + wind_speed: + name: Windspeed + wind_direction_degrees: + name: Winddirection Degrees + pin: 4 diff --git a/tests/components/tx20/test.esp32-ard.yaml b/tests/components/tx20/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tx20/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tx20/test.esp32-c3-ard.yaml b/tests/components/tx20/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tx20/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tx20/test.esp32-c3-idf.yaml b/tests/components/tx20/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tx20/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tx20/test.esp32-idf.yaml b/tests/components/tx20/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tx20/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tx20/test.esp8266-ard.yaml b/tests/components/tx20/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tx20/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tx20/test.rp2040-ard.yaml b/tests/components/tx20/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tx20/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-c3-ard.yaml b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2a73826c51 --- /dev/null +++ b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-c3-ard.yaml @@ -0,0 +1,30 @@ +<<: !include ../logger/common-usb_cdc.yaml + +esphome: + on_boot: + then: + - uart.write: + id: uart_1 + data: 'Hello World' + - uart.write: + id: uart_1 + data: [0x00, 0x20, 0x42] + +uart: + - id: uart_1 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 + + - id: uart_2 + tx_pin: 6 + rx_pin: 7 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-ard.yaml b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-ard.yaml new file mode 100644 index 0000000000..2a73826c51 --- /dev/null +++ b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-ard.yaml @@ -0,0 +1,30 @@ +<<: !include ../logger/common-usb_cdc.yaml + +esphome: + on_boot: + then: + - uart.write: + id: uart_1 + data: 'Hello World' + - uart.write: + id: uart_1 + data: [0x00, 0x20, 0x42] + +uart: + - id: uart_1 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 + + - id: uart_2 + tx_pin: 6 + rx_pin: 7 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-idf.yaml b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-idf.yaml new file mode 100644 index 0000000000..2a73826c51 --- /dev/null +++ b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-idf.yaml @@ -0,0 +1,30 @@ +<<: !include ../logger/common-usb_cdc.yaml + +esphome: + on_boot: + then: + - uart.write: + id: uart_1 + data: 'Hello World' + - uart.write: + id: uart_1 + data: [0x00, 0x20, 0x42] + +uart: + - id: uart_1 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 + + - id: uart_2 + tx_pin: 6 + rx_pin: 7 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s3-ard.yaml b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s3-ard.yaml new file mode 100644 index 0000000000..2a73826c51 --- /dev/null +++ b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s3-ard.yaml @@ -0,0 +1,30 @@ +<<: !include ../logger/common-usb_cdc.yaml + +esphome: + on_boot: + then: + - uart.write: + id: uart_1 + data: 'Hello World' + - uart.write: + id: uart_1 + data: [0x00, 0x20, 0x42] + +uart: + - id: uart_1 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 + + - id: uart_2 + tx_pin: 6 + rx_pin: 7 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-c3-idf.yaml b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-c3-idf.yaml new file mode 100644 index 0000000000..e0a07dde91 --- /dev/null +++ b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-c3-idf.yaml @@ -0,0 +1,30 @@ +<<: !include ../logger/common-usb_serial_jtag.yaml + +esphome: + on_boot: + then: + - uart.write: + id: uart_1 + data: 'Hello World' + - uart.write: + id: uart_1 + data: [0x00, 0x20, 0x42] + +uart: + - id: uart_1 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 + + - id: uart_2 + tx_pin: 6 + rx_pin: 7 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml new file mode 100644 index 0000000000..e0a07dde91 --- /dev/null +++ b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml @@ -0,0 +1,30 @@ +<<: !include ../logger/common-usb_serial_jtag.yaml + +esphome: + on_boot: + then: + - uart.write: + id: uart_1 + data: 'Hello World' + - uart.write: + id: uart_1 + data: [0x00, 0x20, 0x42] + +uart: + - id: uart_1 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 + + - id: uart_2 + tx_pin: 6 + rx_pin: 7 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test.esp32-ard.yaml b/tests/components/uart/test.esp32-ard.yaml new file mode 100644 index 0000000000..bef5b460ab --- /dev/null +++ b/tests/components/uart/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - uart.write: 'Hello World' + - uart.write: [0x00, 0x20, 0x42] + +uart: + - id: uart_uart + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test.esp32-c3-ard.yaml b/tests/components/uart/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..09178f1663 --- /dev/null +++ b/tests/components/uart/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - uart.write: 'Hello World' + - uart.write: [0x00, 0x20, 0x42] + +uart: + - id: uart_uart + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test.esp32-c3-idf.yaml b/tests/components/uart/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..09178f1663 --- /dev/null +++ b/tests/components/uart/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - uart.write: 'Hello World' + - uart.write: [0x00, 0x20, 0x42] + +uart: + - id: uart_uart + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml new file mode 100644 index 0000000000..bef5b460ab --- /dev/null +++ b/tests/components/uart/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - uart.write: 'Hello World' + - uart.write: [0x00, 0x20, 0x42] + +uart: + - id: uart_uart + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test.esp8266-ard.yaml b/tests/components/uart/test.esp8266-ard.yaml new file mode 100644 index 0000000000..09178f1663 --- /dev/null +++ b/tests/components/uart/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - uart.write: 'Hello World' + - uart.write: [0x00, 0x20, 0x42] + +uart: + - id: uart_uart + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test.host.yaml b/tests/components/uart/test.host.yaml new file mode 100644 index 0000000000..63f0ade084 --- /dev/null +++ b/tests/components/uart/test.host.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - uart.write: 'Hello World' + - uart.write: [0x00, 0x20, 0x42] + +uart: + - id: uart_uart + port: "/dev/ttyS0" + baud_rate: 9600 + data_bits: 8 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/uart/test.rp2040-ard.yaml b/tests/components/uart/test.rp2040-ard.yaml new file mode 100644 index 0000000000..09178f1663 --- /dev/null +++ b/tests/components/uart/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - uart.write: 'Hello World' + - uart.write: [0x00, 0x20, 0x42] + +uart: + - id: uart_uart + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + data_bits: 8 + rx_buffer_size: 512 + parity: EVEN + stop_bits: 2 diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml new file mode 100644 index 0000000000..3bdc19ece5 --- /dev/null +++ b/tests/components/udp/common.yaml @@ -0,0 +1,35 @@ +wifi: + ssid: MySSID + password: password1 + +udp: + update_interval: 5s + encryption: "our key goes here" + rolling_code_enable: true + ping_pong_enable: true + binary_sensors: + - binary_sensor_id1 + - id: binary_sensor_id1 + broadcast_id: other_id + sensors: + - sensor_id1 + - id: sensor_id1 + broadcast_id: other_id + providers: + - name: some-device-name + encryption: "their key goes here" + +sensor: + - platform: template + id: sensor_id1 + - platform: udp + provider: some-device-name + id: our_id + remote_id: some_sensor_id + +binary_sensor: + - platform: udp + provider: unencrypted-device + id: other_binary_sensor_id + - platform: template + id: binary_sensor_id1 diff --git a/tests/components/udp/test.bk72xx-ard.yaml b/tests/components/udp/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-ard.yaml b/tests/components/udp/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-c3-ard.yaml b/tests/components/udp/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-c3-idf.yaml b/tests/components/udp/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-idf.yaml b/tests/components/udp/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp8266-ard.yaml b/tests/components/udp/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.host.yaml b/tests/components/udp/test.host.yaml new file mode 100644 index 0000000000..e735c37e4d --- /dev/null +++ b/tests/components/udp/test.host.yaml @@ -0,0 +1,4 @@ +packages: + common: !include common.yaml + +wifi: !remove diff --git a/tests/components/udp/test.rp2040-ard.yaml b/tests/components/udp/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ufire_ec/test.esp32-ard.yaml b/tests/components/ufire_ec/test.esp32-ard.yaml new file mode 100644 index 0000000000..5e6a0daa9e --- /dev/null +++ b/tests/components/ufire_ec/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ec.calibrate_probe: + id: ufire_ec_board + solution: 0.146 + temperature: !lambda "return id(test_sensor).state;" + - ufire_ec.reset: + +i2c: + - id: i2c_ufire_ec + scl: 16 + sda: 17 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ec + id: ufire_ec_board + ec: + name: Ufire EC + temperature_sensor: test_sensor + temperature_compensation: 20.0 + temperature_coefficient: 0.019 diff --git a/tests/components/ufire_ec/test.esp32-c3-ard.yaml b/tests/components/ufire_ec/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..aa72c992b8 --- /dev/null +++ b/tests/components/ufire_ec/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ec.calibrate_probe: + id: ufire_ec_board + solution: 0.146 + temperature: !lambda "return id(test_sensor).state;" + - ufire_ec.reset: + +i2c: + - id: i2c_ufire_ec + scl: 5 + sda: 4 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ec + id: ufire_ec_board + ec: + name: Ufire EC + temperature_sensor: test_sensor + temperature_compensation: 20.0 + temperature_coefficient: 0.019 diff --git a/tests/components/ufire_ec/test.esp32-c3-idf.yaml b/tests/components/ufire_ec/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..aa72c992b8 --- /dev/null +++ b/tests/components/ufire_ec/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ec.calibrate_probe: + id: ufire_ec_board + solution: 0.146 + temperature: !lambda "return id(test_sensor).state;" + - ufire_ec.reset: + +i2c: + - id: i2c_ufire_ec + scl: 5 + sda: 4 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ec + id: ufire_ec_board + ec: + name: Ufire EC + temperature_sensor: test_sensor + temperature_compensation: 20.0 + temperature_coefficient: 0.019 diff --git a/tests/components/ufire_ec/test.esp32-idf.yaml b/tests/components/ufire_ec/test.esp32-idf.yaml new file mode 100644 index 0000000000..5e6a0daa9e --- /dev/null +++ b/tests/components/ufire_ec/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ec.calibrate_probe: + id: ufire_ec_board + solution: 0.146 + temperature: !lambda "return id(test_sensor).state;" + - ufire_ec.reset: + +i2c: + - id: i2c_ufire_ec + scl: 16 + sda: 17 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ec + id: ufire_ec_board + ec: + name: Ufire EC + temperature_sensor: test_sensor + temperature_compensation: 20.0 + temperature_coefficient: 0.019 diff --git a/tests/components/ufire_ec/test.esp8266-ard.yaml b/tests/components/ufire_ec/test.esp8266-ard.yaml new file mode 100644 index 0000000000..aa72c992b8 --- /dev/null +++ b/tests/components/ufire_ec/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ec.calibrate_probe: + id: ufire_ec_board + solution: 0.146 + temperature: !lambda "return id(test_sensor).state;" + - ufire_ec.reset: + +i2c: + - id: i2c_ufire_ec + scl: 5 + sda: 4 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ec + id: ufire_ec_board + ec: + name: Ufire EC + temperature_sensor: test_sensor + temperature_compensation: 20.0 + temperature_coefficient: 0.019 diff --git a/tests/components/ufire_ec/test.rp2040-ard.yaml b/tests/components/ufire_ec/test.rp2040-ard.yaml new file mode 100644 index 0000000000..aa72c992b8 --- /dev/null +++ b/tests/components/ufire_ec/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ec.calibrate_probe: + id: ufire_ec_board + solution: 0.146 + temperature: !lambda "return id(test_sensor).state;" + - ufire_ec.reset: + +i2c: + - id: i2c_ufire_ec + scl: 5 + sda: 4 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ec + id: ufire_ec_board + ec: + name: Ufire EC + temperature_sensor: test_sensor + temperature_compensation: 20.0 + temperature_coefficient: 0.019 diff --git a/tests/components/ufire_ise/test.esp32-ard.yaml b/tests/components/ufire_ise/test.esp32-ard.yaml new file mode 100644 index 0000000000..9ed0ac433a --- /dev/null +++ b/tests/components/ufire_ise/test.esp32-ard.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ise.calibrate_probe_high: + id: ufire_ise_sensor + solution: 7.0 + - ufire_ise.calibrate_probe_low: + id: ufire_ise_sensor + solution: 4.0 + - ufire_ise.reset: + +i2c: + - id: i2c_ufire_ise + scl: 16 + sda: 17 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ise + id: ufire_ise_sensor + temperature_sensor: test_sensor + ph: + name: Ufire pH diff --git a/tests/components/ufire_ise/test.esp32-c3-ard.yaml b/tests/components/ufire_ise/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..36aec73113 --- /dev/null +++ b/tests/components/ufire_ise/test.esp32-c3-ard.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ise.calibrate_probe_high: + id: ufire_ise_sensor + solution: 7.0 + - ufire_ise.calibrate_probe_low: + id: ufire_ise_sensor + solution: 4.0 + - ufire_ise.reset: + +i2c: + - id: i2c_ufire_ise + scl: 5 + sda: 4 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ise + id: ufire_ise_sensor + temperature_sensor: test_sensor + ph: + name: Ufire pH diff --git a/tests/components/ufire_ise/test.esp32-c3-idf.yaml b/tests/components/ufire_ise/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..36aec73113 --- /dev/null +++ b/tests/components/ufire_ise/test.esp32-c3-idf.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ise.calibrate_probe_high: + id: ufire_ise_sensor + solution: 7.0 + - ufire_ise.calibrate_probe_low: + id: ufire_ise_sensor + solution: 4.0 + - ufire_ise.reset: + +i2c: + - id: i2c_ufire_ise + scl: 5 + sda: 4 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ise + id: ufire_ise_sensor + temperature_sensor: test_sensor + ph: + name: Ufire pH diff --git a/tests/components/ufire_ise/test.esp32-idf.yaml b/tests/components/ufire_ise/test.esp32-idf.yaml new file mode 100644 index 0000000000..9ed0ac433a --- /dev/null +++ b/tests/components/ufire_ise/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ise.calibrate_probe_high: + id: ufire_ise_sensor + solution: 7.0 + - ufire_ise.calibrate_probe_low: + id: ufire_ise_sensor + solution: 4.0 + - ufire_ise.reset: + +i2c: + - id: i2c_ufire_ise + scl: 16 + sda: 17 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ise + id: ufire_ise_sensor + temperature_sensor: test_sensor + ph: + name: Ufire pH diff --git a/tests/components/ufire_ise/test.esp8266-ard.yaml b/tests/components/ufire_ise/test.esp8266-ard.yaml new file mode 100644 index 0000000000..36aec73113 --- /dev/null +++ b/tests/components/ufire_ise/test.esp8266-ard.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ise.calibrate_probe_high: + id: ufire_ise_sensor + solution: 7.0 + - ufire_ise.calibrate_probe_low: + id: ufire_ise_sensor + solution: 4.0 + - ufire_ise.reset: + +i2c: + - id: i2c_ufire_ise + scl: 5 + sda: 4 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ise + id: ufire_ise_sensor + temperature_sensor: test_sensor + ph: + name: Ufire pH diff --git a/tests/components/ufire_ise/test.rp2040-ard.yaml b/tests/components/ufire_ise/test.rp2040-ard.yaml new file mode 100644 index 0000000000..36aec73113 --- /dev/null +++ b/tests/components/ufire_ise/test.rp2040-ard.yaml @@ -0,0 +1,25 @@ +esphome: + on_boot: + then: + - ufire_ise.calibrate_probe_high: + id: ufire_ise_sensor + solution: 7.0 + - ufire_ise.calibrate_probe_low: + id: ufire_ise_sensor + solution: 4.0 + - ufire_ise.reset: + +i2c: + - id: i2c_ufire_ise + scl: 5 + sda: 4 + +sensor: + - platform: template + id: test_sensor + lambda: "return 21;" + - platform: ufire_ise + id: ufire_ise_sensor + temperature_sensor: test_sensor + ph: + name: Ufire pH diff --git a/tests/components/uln2003/test.esp32-ard.yaml b/tests/components/uln2003/test.esp32-ard.yaml new file mode 100644 index 0000000000..61a6e94396 --- /dev/null +++ b/tests/components/uln2003/test.esp32-ard.yaml @@ -0,0 +1,29 @@ +esphome: + on_boot: + then: + - stepper.report_position: + id: uln2003_stepper + position: 250 + - stepper.set_target: + id: uln2003_stepper + target: 250 + - stepper.set_acceleration: + id: uln2003_stepper + acceleration: 250 steps/s^2 + - stepper.set_deceleration: + id: uln2003_stepper + deceleration: 250 steps/s^2 + - stepper.set_speed: + id: uln2003_stepper + speed: 250 steps/s + +stepper: + - platform: uln2003 + id: uln2003_stepper + pin_a: 12 + pin_b: 13 + pin_c: 14 + pin_d: 15 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/uln2003/test.esp32-c3-ard.yaml b/tests/components/uln2003/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2d19d4dba3 --- /dev/null +++ b/tests/components/uln2003/test.esp32-c3-ard.yaml @@ -0,0 +1,29 @@ +esphome: + on_boot: + then: + - stepper.report_position: + id: uln2003_stepper + position: 250 + - stepper.set_target: + id: uln2003_stepper + target: 250 + - stepper.set_acceleration: + id: uln2003_stepper + acceleration: 250 steps/s^2 + - stepper.set_deceleration: + id: uln2003_stepper + deceleration: 250 steps/s^2 + - stepper.set_speed: + id: uln2003_stepper + speed: 250 steps/s + +stepper: + - platform: uln2003 + id: uln2003_stepper + pin_a: 0 + pin_b: 1 + pin_c: 2 + pin_d: 3 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/uln2003/test.esp32-c3-idf.yaml b/tests/components/uln2003/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2d19d4dba3 --- /dev/null +++ b/tests/components/uln2003/test.esp32-c3-idf.yaml @@ -0,0 +1,29 @@ +esphome: + on_boot: + then: + - stepper.report_position: + id: uln2003_stepper + position: 250 + - stepper.set_target: + id: uln2003_stepper + target: 250 + - stepper.set_acceleration: + id: uln2003_stepper + acceleration: 250 steps/s^2 + - stepper.set_deceleration: + id: uln2003_stepper + deceleration: 250 steps/s^2 + - stepper.set_speed: + id: uln2003_stepper + speed: 250 steps/s + +stepper: + - platform: uln2003 + id: uln2003_stepper + pin_a: 0 + pin_b: 1 + pin_c: 2 + pin_d: 3 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/uln2003/test.esp32-idf.yaml b/tests/components/uln2003/test.esp32-idf.yaml new file mode 100644 index 0000000000..61a6e94396 --- /dev/null +++ b/tests/components/uln2003/test.esp32-idf.yaml @@ -0,0 +1,29 @@ +esphome: + on_boot: + then: + - stepper.report_position: + id: uln2003_stepper + position: 250 + - stepper.set_target: + id: uln2003_stepper + target: 250 + - stepper.set_acceleration: + id: uln2003_stepper + acceleration: 250 steps/s^2 + - stepper.set_deceleration: + id: uln2003_stepper + deceleration: 250 steps/s^2 + - stepper.set_speed: + id: uln2003_stepper + speed: 250 steps/s + +stepper: + - platform: uln2003 + id: uln2003_stepper + pin_a: 12 + pin_b: 13 + pin_c: 14 + pin_d: 15 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/uln2003/test.esp8266-ard.yaml b/tests/components/uln2003/test.esp8266-ard.yaml new file mode 100644 index 0000000000..61a6e94396 --- /dev/null +++ b/tests/components/uln2003/test.esp8266-ard.yaml @@ -0,0 +1,29 @@ +esphome: + on_boot: + then: + - stepper.report_position: + id: uln2003_stepper + position: 250 + - stepper.set_target: + id: uln2003_stepper + target: 250 + - stepper.set_acceleration: + id: uln2003_stepper + acceleration: 250 steps/s^2 + - stepper.set_deceleration: + id: uln2003_stepper + deceleration: 250 steps/s^2 + - stepper.set_speed: + id: uln2003_stepper + speed: 250 steps/s + +stepper: + - platform: uln2003 + id: uln2003_stepper + pin_a: 12 + pin_b: 13 + pin_c: 14 + pin_d: 15 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/uln2003/test.rp2040-ard.yaml b/tests/components/uln2003/test.rp2040-ard.yaml new file mode 100644 index 0000000000..2d19d4dba3 --- /dev/null +++ b/tests/components/uln2003/test.rp2040-ard.yaml @@ -0,0 +1,29 @@ +esphome: + on_boot: + then: + - stepper.report_position: + id: uln2003_stepper + position: 250 + - stepper.set_target: + id: uln2003_stepper + target: 250 + - stepper.set_acceleration: + id: uln2003_stepper + acceleration: 250 steps/s^2 + - stepper.set_deceleration: + id: uln2003_stepper + deceleration: 250 steps/s^2 + - stepper.set_speed: + id: uln2003_stepper + speed: 250 steps/s + +stepper: + - platform: uln2003 + id: uln2003_stepper + pin_a: 0 + pin_b: 1 + pin_c: 2 + pin_d: 3 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 diff --git a/tests/components/ultrasonic/common.yaml b/tests/components/ultrasonic/common.yaml new file mode 100644 index 0000000000..f1f673d918 --- /dev/null +++ b/tests/components/ultrasonic/common.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: ultrasonic + id: ultrasonic_sensor1 + name: Ultrasonic Sensor + echo_pin: 4 + trigger_pin: 5 + timeout: 5.5m diff --git a/tests/components/ultrasonic/test.esp32-ard.yaml b/tests/components/ultrasonic/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ultrasonic/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp32-c3-ard.yaml b/tests/components/ultrasonic/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ultrasonic/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp32-c3-idf.yaml b/tests/components/ultrasonic/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ultrasonic/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp32-idf.yaml b/tests/components/ultrasonic/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ultrasonic/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp8266-ard.yaml b/tests/components/ultrasonic/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ultrasonic/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.rp2040-ard.yaml b/tests/components/ultrasonic/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ultrasonic/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml new file mode 100644 index 0000000000..dcb4f42527 --- /dev/null +++ b/tests/components/update/common.yaml @@ -0,0 +1,28 @@ +substitutions: + verify_ssl: "true" + +esphome: + on_boot: + then: + - if: + condition: + update.is_available: + then: + - logger.log: "Update available" + - update.perform: + force_update: true + +wifi: + ssid: MySSID + password: password1 + +http_request: + verify_ssl: ${verify_ssl} + +ota: + - platform: http_request + +update: + - platform: http_request + name: Firmware Update + source: http://example.com/manifest.json diff --git a/tests/components/update/test.esp32-ard.yaml b/tests/components/update/test.esp32-ard.yaml new file mode 100644 index 0000000000..c1937b5a10 --- /dev/null +++ b/tests/components/update/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "false" + +<<: !include common.yaml diff --git a/tests/components/update/test.esp32-idf.yaml b/tests/components/update/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/update/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/update/test.esp8266-ard.yaml b/tests/components/update/test.esp8266-ard.yaml new file mode 100644 index 0000000000..c1937b5a10 --- /dev/null +++ b/tests/components/update/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "false" + +<<: !include common.yaml diff --git a/tests/components/update/test.rp2040-ard.yaml b/tests/components/update/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c1937b5a10 --- /dev/null +++ b/tests/components/update/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "false" + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/common.yaml b/tests/components/uponor_smatrix/common.yaml new file mode 100644 index 0000000000..cfdbacaa4c --- /dev/null +++ b/tests/components/uponor_smatrix/common.yaml @@ -0,0 +1,38 @@ +wifi: + ssid: MySSID + password: password1 + +uart: + - id: uponor_uart + baud_rate: 19200 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + +time: + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + +uponor_smatrix: + uart_id: uponor_uart + address: 0x110B + time_id: sntp_time + time_device_address: 0xDE13 + +climate: + - platform: uponor_smatrix + address: 0xDE13 + name: Thermostat Living Room + +sensor: + - platform: uponor_smatrix + address: 0xDE13 + humidity: + name: Thermostat Humidity Living Room + temperature: + name: Thermostat Temperature Living Room + external_temperature: + name: Thermostat Floor Temperature Living Room diff --git a/tests/components/uponor_smatrix/test.esp32-ard.yaml b/tests/components/uponor_smatrix/test.esp32-ard.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-c3-ard.yaml b/tests/components/uponor_smatrix/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml b/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-idf.yaml b/tests/components/uponor_smatrix/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp8266-ard.yaml b/tests/components/uponor_smatrix/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/uponor_smatrix/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.rp2040-ard.yaml b/tests/components/uponor_smatrix/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/uponor_smatrix/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml new file mode 100644 index 0000000000..f63f80b050 --- /dev/null +++ b/tests/components/uptime/common.yaml @@ -0,0 +1,15 @@ +wifi: + ap: + +time: + - platform: sntp + +sensor: + - platform: uptime + name: Uptime Sensor + - platform: uptime + name: Uptime Sensor Seconds + type: seconds + - platform: uptime + name: Uptime Sensor Timestamp + type: timestamp diff --git a/tests/components/uptime/test.esp32-ard.yaml b/tests/components/uptime/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/uptime/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/uptime/test.esp32-c3-ard.yaml b/tests/components/uptime/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/uptime/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/uptime/test.esp32-c3-idf.yaml b/tests/components/uptime/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/uptime/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/uptime/test.esp32-idf.yaml b/tests/components/uptime/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/uptime/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/uptime/test.esp8266-ard.yaml b/tests/components/uptime/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/uptime/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/uptime/test.rp2040-ard.yaml b/tests/components/uptime/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/uptime/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/vbus/test.esp32-ard.yaml b/tests/components/vbus/test.esp32-ard.yaml new file mode 100644 index 0000000000..a0e5ca42cc --- /dev/null +++ b/tests/components/vbus/test.esp32-ard.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_vbus + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +vbus: + +binary_sensor: + - platform: vbus + model: deltasol_bs_plus + relay1: + name: Relay 1 On + relay2: + name: Relay 2 On + sensor1_error: + name: Sensor 1 Error + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + binary_sensors: + - id: vcustom_b + name: VBus Custom Binary Sensor + lambda: return x[0] & 1; + +sensor: + - platform: vbus + model: deltasol c + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + temperature_3: + name: Temperature 3 + operating_hours_1: + name: Operating Hours 1 + heat_quantity: + name: Heat Quantity + time: + name: System Time diff --git a/tests/components/vbus/test.esp32-c3-ard.yaml b/tests/components/vbus/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..67ee542031 --- /dev/null +++ b/tests/components/vbus/test.esp32-c3-ard.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_vbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +vbus: + +binary_sensor: + - platform: vbus + model: deltasol_bs_plus + relay1: + name: Relay 1 On + relay2: + name: Relay 2 On + sensor1_error: + name: Sensor 1 Error + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + binary_sensors: + - id: vcustom_b + name: VBus Custom Binary Sensor + lambda: return x[0] & 1; + +sensor: + - platform: vbus + model: deltasol c + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + temperature_3: + name: Temperature 3 + operating_hours_1: + name: Operating Hours 1 + heat_quantity: + name: Heat Quantity + time: + name: System Time diff --git a/tests/components/vbus/test.esp32-c3-idf.yaml b/tests/components/vbus/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..67ee542031 --- /dev/null +++ b/tests/components/vbus/test.esp32-c3-idf.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_vbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +vbus: + +binary_sensor: + - platform: vbus + model: deltasol_bs_plus + relay1: + name: Relay 1 On + relay2: + name: Relay 2 On + sensor1_error: + name: Sensor 1 Error + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + binary_sensors: + - id: vcustom_b + name: VBus Custom Binary Sensor + lambda: return x[0] & 1; + +sensor: + - platform: vbus + model: deltasol c + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + temperature_3: + name: Temperature 3 + operating_hours_1: + name: Operating Hours 1 + heat_quantity: + name: Heat Quantity + time: + name: System Time diff --git a/tests/components/vbus/test.esp32-idf.yaml b/tests/components/vbus/test.esp32-idf.yaml new file mode 100644 index 0000000000..a0e5ca42cc --- /dev/null +++ b/tests/components/vbus/test.esp32-idf.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_vbus + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +vbus: + +binary_sensor: + - platform: vbus + model: deltasol_bs_plus + relay1: + name: Relay 1 On + relay2: + name: Relay 2 On + sensor1_error: + name: Sensor 1 Error + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + binary_sensors: + - id: vcustom_b + name: VBus Custom Binary Sensor + lambda: return x[0] & 1; + +sensor: + - platform: vbus + model: deltasol c + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + temperature_3: + name: Temperature 3 + operating_hours_1: + name: Operating Hours 1 + heat_quantity: + name: Heat Quantity + time: + name: System Time diff --git a/tests/components/vbus/test.esp8266-ard.yaml b/tests/components/vbus/test.esp8266-ard.yaml new file mode 100644 index 0000000000..67ee542031 --- /dev/null +++ b/tests/components/vbus/test.esp8266-ard.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_vbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +vbus: + +binary_sensor: + - platform: vbus + model: deltasol_bs_plus + relay1: + name: Relay 1 On + relay2: + name: Relay 2 On + sensor1_error: + name: Sensor 1 Error + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + binary_sensors: + - id: vcustom_b + name: VBus Custom Binary Sensor + lambda: return x[0] & 1; + +sensor: + - platform: vbus + model: deltasol c + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + temperature_3: + name: Temperature 3 + operating_hours_1: + name: Operating Hours 1 + heat_quantity: + name: Heat Quantity + time: + name: System Time diff --git a/tests/components/vbus/test.rp2040-ard.yaml b/tests/components/vbus/test.rp2040-ard.yaml new file mode 100644 index 0000000000..67ee542031 --- /dev/null +++ b/tests/components/vbus/test.rp2040-ard.yaml @@ -0,0 +1,42 @@ +uart: + - id: uart_vbus + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +vbus: + +binary_sensor: + - platform: vbus + model: deltasol_bs_plus + relay1: + name: Relay 1 On + relay2: + name: Relay 2 On + sensor1_error: + name: Sensor 1 Error + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + binary_sensors: + - id: vcustom_b + name: VBus Custom Binary Sensor + lambda: return x[0] & 1; + +sensor: + - platform: vbus + model: deltasol c + temperature_1: + name: Temperature 1 + temperature_2: + name: Temperature 2 + temperature_3: + name: Temperature 3 + operating_hours_1: + name: Operating Hours 1 + heat_quantity: + name: Heat Quantity + time: + name: System Time diff --git a/tests/components/veml3235/test.esp32-ard.yaml b/tests/components/veml3235/test.esp32-ard.yaml new file mode 100644 index 0000000000..3442fa4317 --- /dev/null +++ b/tests/components/veml3235/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_veml3235 + scl: 16 + sda: 17 + +sensor: + - platform: veml3235 + id: veml3235_sensor + name: VEML3235 Light Sensor + auto_gain: true + auto_gain_threshold_high: 90% + auto_gain_threshold_low: 15% + digital_gain: 1X + gain: 1X + integration_time: 50ms diff --git a/tests/components/veml3235/test.esp32-c3-ard.yaml b/tests/components/veml3235/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..1f79c5f50c --- /dev/null +++ b/tests/components/veml3235/test.esp32-c3-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_veml3235 + scl: 5 + sda: 4 + +sensor: + - platform: veml3235 + id: veml3235_sensor + name: VEML3235 Light Sensor + auto_gain: true + auto_gain_threshold_high: 90% + auto_gain_threshold_low: 15% + digital_gain: 1X + gain: 1X + integration_time: 50ms diff --git a/tests/components/veml3235/test.esp32-c3-idf.yaml b/tests/components/veml3235/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..1f79c5f50c --- /dev/null +++ b/tests/components/veml3235/test.esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_veml3235 + scl: 5 + sda: 4 + +sensor: + - platform: veml3235 + id: veml3235_sensor + name: VEML3235 Light Sensor + auto_gain: true + auto_gain_threshold_high: 90% + auto_gain_threshold_low: 15% + digital_gain: 1X + gain: 1X + integration_time: 50ms diff --git a/tests/components/veml3235/test.esp32-idf.yaml b/tests/components/veml3235/test.esp32-idf.yaml new file mode 100644 index 0000000000..3442fa4317 --- /dev/null +++ b/tests/components/veml3235/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_veml3235 + scl: 16 + sda: 17 + +sensor: + - platform: veml3235 + id: veml3235_sensor + name: VEML3235 Light Sensor + auto_gain: true + auto_gain_threshold_high: 90% + auto_gain_threshold_low: 15% + digital_gain: 1X + gain: 1X + integration_time: 50ms diff --git a/tests/components/veml3235/test.esp8266-ard.yaml b/tests/components/veml3235/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1f79c5f50c --- /dev/null +++ b/tests/components/veml3235/test.esp8266-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_veml3235 + scl: 5 + sda: 4 + +sensor: + - platform: veml3235 + id: veml3235_sensor + name: VEML3235 Light Sensor + auto_gain: true + auto_gain_threshold_high: 90% + auto_gain_threshold_low: 15% + digital_gain: 1X + gain: 1X + integration_time: 50ms diff --git a/tests/components/veml3235/test.rp2040-ard.yaml b/tests/components/veml3235/test.rp2040-ard.yaml new file mode 100644 index 0000000000..1f79c5f50c --- /dev/null +++ b/tests/components/veml3235/test.rp2040-ard.yaml @@ -0,0 +1,15 @@ +i2c: + - id: i2c_veml3235 + scl: 5 + sda: 4 + +sensor: + - platform: veml3235 + id: veml3235_sensor + name: VEML3235 Light Sensor + auto_gain: true + auto_gain_threshold_high: 90% + auto_gain_threshold_low: 15% + digital_gain: 1X + gain: 1X + integration_time: 50ms diff --git a/tests/components/veml7700/common.yaml b/tests/components/veml7700/common.yaml new file mode 100644 index 0000000000..6620c8d7e1 --- /dev/null +++ b/tests/components/veml7700/common.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: veml7700 + address: 0x10 + i2c_id: i2c_veml7700 + ambient_light: Ambient light + ambient_light_counts: Ambient light counts + full_spectrum: Full spectrum + full_spectrum_counts: Full spectrum counts + actual_integration_time: Actual integration time + actual_gain: Actual gain diff --git a/tests/components/veml7700/test.esp32-ard.yaml b/tests/components/veml7700/test.esp32-ard.yaml new file mode 100644 index 0000000000..4b812a1392 --- /dev/null +++ b/tests/components/veml7700/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_veml7700 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp32-c3-ard.yaml b/tests/components/veml7700/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ce0fa0125b --- /dev/null +++ b/tests/components/veml7700/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_veml7700 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp32-c3-idf.yaml b/tests/components/veml7700/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ce0fa0125b --- /dev/null +++ b/tests/components/veml7700/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_veml7700 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp32-idf.yaml b/tests/components/veml7700/test.esp32-idf.yaml new file mode 100644 index 0000000000..4b812a1392 --- /dev/null +++ b/tests/components/veml7700/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_veml7700 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp8266-ard.yaml b/tests/components/veml7700/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ce0fa0125b --- /dev/null +++ b/tests/components/veml7700/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_veml7700 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/veml7700/test.rp2040-ard.yaml b/tests/components/veml7700/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ce0fa0125b --- /dev/null +++ b/tests/components/veml7700/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_veml7700 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/version/common.yaml b/tests/components/version/common.yaml new file mode 100644 index 0000000000..7713afc37c --- /dev/null +++ b/tests/components/version/common.yaml @@ -0,0 +1,3 @@ +text_sensor: + - platform: version + name: "ESPHome Version" diff --git a/tests/components/version/test.esp32-ard.yaml b/tests/components/version/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.esp32-c3-ard.yaml b/tests/components/version/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.esp32-c3-idf.yaml b/tests/components/version/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.esp32-idf.yaml b/tests/components/version/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.esp8266-ard.yaml b/tests/components/version/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.rp2040-ard.yaml b/tests/components/version/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/vl53l0x/test.esp32-ard.yaml b/tests/components/vl53l0x/test.esp32-ard.yaml new file mode 100644 index 0000000000..8f35de0e72 --- /dev/null +++ b/tests/components/vl53l0x/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_vl53l0x + scl: 16 + sda: 17 + +sensor: + - platform: vl53l0x + name: VL53L0x Distance + address: 0x29 + enable_pin: 3 + timeout: 200us + update_interval: 60s diff --git a/tests/components/vl53l0x/test.esp32-c3-ard.yaml b/tests/components/vl53l0x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..832f7dcfbc --- /dev/null +++ b/tests/components/vl53l0x/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_vl53l0x + scl: 5 + sda: 4 + +sensor: + - platform: vl53l0x + name: VL53L0x Distance + address: 0x29 + enable_pin: 3 + timeout: 200us + update_interval: 60s diff --git a/tests/components/vl53l0x/test.esp32-c3-idf.yaml b/tests/components/vl53l0x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..832f7dcfbc --- /dev/null +++ b/tests/components/vl53l0x/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_vl53l0x + scl: 5 + sda: 4 + +sensor: + - platform: vl53l0x + name: VL53L0x Distance + address: 0x29 + enable_pin: 3 + timeout: 200us + update_interval: 60s diff --git a/tests/components/vl53l0x/test.esp32-idf.yaml b/tests/components/vl53l0x/test.esp32-idf.yaml new file mode 100644 index 0000000000..8f35de0e72 --- /dev/null +++ b/tests/components/vl53l0x/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_vl53l0x + scl: 16 + sda: 17 + +sensor: + - platform: vl53l0x + name: VL53L0x Distance + address: 0x29 + enable_pin: 3 + timeout: 200us + update_interval: 60s diff --git a/tests/components/vl53l0x/test.esp8266-ard.yaml b/tests/components/vl53l0x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..832f7dcfbc --- /dev/null +++ b/tests/components/vl53l0x/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_vl53l0x + scl: 5 + sda: 4 + +sensor: + - platform: vl53l0x + name: VL53L0x Distance + address: 0x29 + enable_pin: 3 + timeout: 200us + update_interval: 60s diff --git a/tests/components/vl53l0x/test.rp2040-ard.yaml b/tests/components/vl53l0x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..832f7dcfbc --- /dev/null +++ b/tests/components/vl53l0x/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_vl53l0x + scl: 5 + sda: 4 + +sensor: + - platform: vl53l0x + name: VL53L0x Distance + address: 0x29 + enable_pin: 3 + timeout: 200us + update_interval: 60s diff --git a/tests/components/voice_assistant/test.esp32-ard.yaml b/tests/components/voice_assistant/test.esp32-ard.yaml new file mode 100644 index 0000000000..cbf9460087 --- /dev/null +++ b/tests/components/voice_assistant/test.esp32-ard.yaml @@ -0,0 +1,57 @@ +esphome: + on_boot: + then: + - voice_assistant.start + - voice_assistant.start_continuous + - voice_assistant.stop + +wifi: + ssid: MySSID + password: password1 + +api: + +i2s_audio: + i2s_lrclk_pin: 16 + i2s_bclk_pin: 17 + i2s_mclk_pin: 15 + +microphone: + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: 13 + adc_type: external + pdm: false + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: 12 + +voice_assistant: + microphone: mic_id_external + speaker: speaker_id + conversation_timeout: 60s + on_listening: + - logger.log: "Voice assistant microphone listening" + on_start: + - logger.log: "Voice assistant started" + on_stt_end: + - logger.log: + format: "Voice assistant STT ended with result %s" + args: [x.c_str()] + on_tts_start: + - logger.log: + format: "Voice assistant TTS started with text %s" + args: [x.c_str()] + on_tts_end: + - logger.log: + format: "Voice assistant TTS ended with url %s" + args: [x.c_str()] + on_end: + - logger.log: "Voice assistant ended" + on_error: + - logger.log: + format: "Voice assistant error - code %s, message: %s" + args: [code.c_str(), message.c_str()] diff --git a/tests/components/voice_assistant/test.esp32-c3-ard.yaml b/tests/components/voice_assistant/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..86357fad36 --- /dev/null +++ b/tests/components/voice_assistant/test.esp32-c3-ard.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - voice_assistant.start + - voice_assistant.start_continuous + - voice_assistant.stop + +wifi: + ssid: MySSID + password: password1 + +api: + +i2s_audio: + i2s_lrclk_pin: 6 + i2s_bclk_pin: 7 + i2s_mclk_pin: 5 + +microphone: + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: 3 + adc_type: external + pdm: false + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: 2 + +voice_assistant: + microphone: mic_id_external + speaker: speaker_id + on_listening: + - logger.log: "Voice assistant microphone listening" + on_start: + - logger.log: "Voice assistant started" + on_stt_end: + - logger.log: + format: "Voice assistant STT ended with result %s" + args: [x.c_str()] + on_tts_start: + - logger.log: + format: "Voice assistant TTS started with text %s" + args: [x.c_str()] + on_tts_end: + - logger.log: + format: "Voice assistant TTS ended with url %s" + args: [x.c_str()] + on_end: + - logger.log: "Voice assistant ended" + on_error: + - logger.log: + format: "Voice assistant error - code %s, message: %s" + args: [code.c_str(), message.c_str()] diff --git a/tests/components/voice_assistant/test.esp32-c3-idf.yaml b/tests/components/voice_assistant/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..86357fad36 --- /dev/null +++ b/tests/components/voice_assistant/test.esp32-c3-idf.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - voice_assistant.start + - voice_assistant.start_continuous + - voice_assistant.stop + +wifi: + ssid: MySSID + password: password1 + +api: + +i2s_audio: + i2s_lrclk_pin: 6 + i2s_bclk_pin: 7 + i2s_mclk_pin: 5 + +microphone: + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: 3 + adc_type: external + pdm: false + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: 2 + +voice_assistant: + microphone: mic_id_external + speaker: speaker_id + on_listening: + - logger.log: "Voice assistant microphone listening" + on_start: + - logger.log: "Voice assistant started" + on_stt_end: + - logger.log: + format: "Voice assistant STT ended with result %s" + args: [x.c_str()] + on_tts_start: + - logger.log: + format: "Voice assistant TTS started with text %s" + args: [x.c_str()] + on_tts_end: + - logger.log: + format: "Voice assistant TTS ended with url %s" + args: [x.c_str()] + on_end: + - logger.log: "Voice assistant ended" + on_error: + - logger.log: + format: "Voice assistant error - code %s, message: %s" + args: [code.c_str(), message.c_str()] diff --git a/tests/components/voice_assistant/test.esp32-idf.yaml b/tests/components/voice_assistant/test.esp32-idf.yaml new file mode 100644 index 0000000000..da9b50721f --- /dev/null +++ b/tests/components/voice_assistant/test.esp32-idf.yaml @@ -0,0 +1,56 @@ +esphome: + on_boot: + then: + - voice_assistant.start + - voice_assistant.start_continuous + - voice_assistant.stop + +wifi: + ssid: MySSID + password: password1 + +api: + +i2s_audio: + i2s_lrclk_pin: 16 + i2s_bclk_pin: 17 + i2s_mclk_pin: 15 + +microphone: + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: 13 + adc_type: external + pdm: false + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: 12 + +voice_assistant: + microphone: mic_id_external + speaker: speaker_id + on_listening: + - logger.log: "Voice assistant microphone listening" + on_start: + - logger.log: "Voice assistant started" + on_stt_end: + - logger.log: + format: "Voice assistant STT ended with result %s" + args: [x.c_str()] + on_tts_start: + - logger.log: + format: "Voice assistant TTS started with text %s" + args: [x.c_str()] + on_tts_end: + - logger.log: + format: "Voice assistant TTS ended with url %s" + args: [x.c_str()] + on_end: + - logger.log: "Voice assistant ended" + on_error: + - logger.log: + format: "Voice assistant error - code %s, message: %s" + args: [code.c_str(), message.c_str()] diff --git a/tests/components/wake_on_lan/common.yaml b/tests/components/wake_on_lan/common.yaml new file mode 100644 index 0000000000..6a5351b624 --- /dev/null +++ b/tests/components/wake_on_lan/common.yaml @@ -0,0 +1,9 @@ +wifi: + ssid: MySSID + password: password1 + +button: + - platform: wake_on_lan + id: wol_1 + name: wol_test_1 + target_mac_address: 12:34:56:78:90:ab diff --git a/tests/components/wake_on_lan/test.esp32-ard.yaml b/tests/components/wake_on_lan/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wake_on_lan/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.esp32-c3-ard.yaml b/tests/components/wake_on_lan/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wake_on_lan/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.esp32-c3-idf.yaml b/tests/components/wake_on_lan/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wake_on_lan/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.esp32-idf.yaml b/tests/components/wake_on_lan/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wake_on_lan/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.esp8266-ard.yaml b/tests/components/wake_on_lan/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wake_on_lan/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.rp2040-ard.yaml b/tests/components/wake_on_lan/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wake_on_lan/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.esp32-ard.yaml b/tests/components/waveshare_epaper/test.esp32-ard.yaml new file mode 100644 index 0000000000..2f06c5c51b --- /dev/null +++ b/tests/components/waveshare_epaper/test.esp32-ard.yaml @@ -0,0 +1,190 @@ +--- +spi: + - id: spi_id_1 + clk_pin: + number: GPIO18 + mosi_pin: + number: GPIO23 + miso_pin: + number: GPIO19 + interface: hardware + +display: + - platform: waveshare_epaper + model: 2.13in-ttgo-b1 + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.13in-ttgo-b74 + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.90in + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + full_update_every: 30 + reset_duration: 200ms + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.90inv2 + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.90in-dke + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + full_update_every: 1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.70in-b + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.70in-bv2 + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 1.54in-m5coreink-m09 + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.13inv3 + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.13inv2 + spi_id: spi_id_1 + cs_pin: + allow_other_uses: true + number: GPIO25 + dc_pin: + allow_other_uses: true + number: GPIO26 + busy_pin: + allow_other_uses: true + number: GPIO27 + reset_pin: + allow_other_uses: true + number: GPIO32 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml b/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..1c4547b7b4 --- /dev/null +++ b/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml @@ -0,0 +1,107 @@ +spi: + - id: spi_waveshare_epaper + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.13in-ttgo-b1 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.90in + full_update_every: 30 + reset_duration: 200ms + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.90inv2 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.70in-b + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.70in-bv2 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 1.54in-m5coreink-m09 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml b/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..1c4547b7b4 --- /dev/null +++ b/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml @@ -0,0 +1,107 @@ +spi: + - id: spi_waveshare_epaper + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.13in-ttgo-b1 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.90in + full_update_every: 30 + reset_duration: 200ms + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.90inv2 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.70in-b + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.70in-bv2 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 1.54in-m5coreink-m09 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/waveshare_epaper/test.esp32-idf.yaml b/tests/components/waveshare_epaper/test.esp32-idf.yaml new file mode 100644 index 0000000000..b6082fcfbf --- /dev/null +++ b/tests/components/waveshare_epaper/test.esp32-idf.yaml @@ -0,0 +1,107 @@ +spi: + - id: spi_bme280 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.13in-ttgo-b1 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.90in + full_update_every: 30 + reset_duration: 200ms + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.90inv2 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.70in-b + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.70in-bv2 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 1.54in-m5coreink-m09 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/waveshare_epaper/test.esp8266-ard.yaml b/tests/components/waveshare_epaper/test.esp8266-ard.yaml new file mode 100644 index 0000000000..1f076a67be --- /dev/null +++ b/tests/components/waveshare_epaper/test.esp8266-ard.yaml @@ -0,0 +1,107 @@ +spi: + - id: spi_bme280 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.13in-ttgo-b1 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.90in + full_update_every: 30 + reset_duration: 200ms + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.90inv2 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.70in-b + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 2.70in-bv2 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 4 + dc_pin: + allow_other_uses: true + number: 4 + busy_pin: + allow_other_uses: true + number: 4 + reset_pin: + allow_other_uses: true + number: 4 + model: 1.54in-m5coreink-m09 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/waveshare_epaper/test.rp2040-ard.yaml b/tests/components/waveshare_epaper/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6050062d7e --- /dev/null +++ b/tests/components/waveshare_epaper/test.rp2040-ard.yaml @@ -0,0 +1,107 @@ +spi: + - id: spi_bme280 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 5 + dc_pin: + allow_other_uses: true + number: 5 + busy_pin: + allow_other_uses: true + number: 5 + reset_pin: + allow_other_uses: true + number: 5 + model: 2.13in-ttgo-b1 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 5 + dc_pin: + allow_other_uses: true + number: 5 + busy_pin: + allow_other_uses: true + number: 5 + reset_pin: + allow_other_uses: true + number: 5 + model: 2.90in + full_update_every: 30 + reset_duration: 200ms + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 5 + dc_pin: + allow_other_uses: true + number: 5 + busy_pin: + allow_other_uses: true + number: 5 + reset_pin: + allow_other_uses: true + number: 5 + model: 2.90inv2 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 5 + dc_pin: + allow_other_uses: true + number: 5 + busy_pin: + allow_other_uses: true + number: 5 + reset_pin: + allow_other_uses: true + number: 5 + model: 2.70in-b + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 5 + dc_pin: + allow_other_uses: true + number: 5 + busy_pin: + allow_other_uses: true + number: 5 + reset_pin: + allow_other_uses: true + number: 5 + model: 2.70in-bv2 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: + allow_other_uses: true + number: 5 + dc_pin: + allow_other_uses: true + number: 5 + busy_pin: + allow_other_uses: true + number: 5 + reset_pin: + allow_other_uses: true + number: 5 + model: 1.54in-m5coreink-m09 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/web_server/common.yaml b/tests/components/web_server/common.yaml new file mode 100644 index 0000000000..eb768eeb91 --- /dev/null +++ b/tests/components/web_server/common.yaml @@ -0,0 +1,38 @@ +wifi: + ssid: MySSID + password: password1 + +binary_sensor: +cover: +fan: +light: +sensor: +switch: +button: +text_sensor: +climate: +number: +text: +select: +lock: +valve: +alarm_control_panel: +api: +time: + - platform: homeassistant + id: homeassistant_time +datetime: + - platform: template + id: my_datetime_date + type: date + optimistic: yes + - platform: template + id: my_datetime_time + type: time + optimistic: yes + - platform: template + id: my_datetime + type: datetime + optimistic: yes +event: +update: diff --git a/tests/components/web_server/common_v1.yaml b/tests/components/web_server/common_v1.yaml new file mode 100644 index 0000000000..3c51f894b8 --- /dev/null +++ b/tests/components/web_server/common_v1.yaml @@ -0,0 +1,6 @@ +packages: + device_base: !include common.yaml + +web_server: + port: 8080 + version: 1 diff --git a/tests/components/web_server/common_v2.yaml b/tests/components/web_server/common_v2.yaml new file mode 100644 index 0000000000..2af5ceca44 --- /dev/null +++ b/tests/components/web_server/common_v2.yaml @@ -0,0 +1,6 @@ +packages: + device_base: !include common.yaml + +web_server: + port: 8080 + version: 2 diff --git a/tests/components/web_server/common_v3.yaml b/tests/components/web_server/common_v3.yaml new file mode 100644 index 0000000000..bdacaaddbe --- /dev/null +++ b/tests/components/web_server/common_v3.yaml @@ -0,0 +1,45 @@ +packages: + device_base: !include common.yaml + +web_server: + port: 8080 + version: 3 + sorting_groups: + - id: sorting_group_1 + name: "Group 1 Diplayed Last" + sorting_weight: 40 + - id: sorting_group_2 + name: "Group 2 Displayed Third" + sorting_weight: 30 + - id: sorting_group_3 + name: "Group 3 Displayed Second" + sorting_weight: 20 + - id: sorting_group_4 + name: "Group 4 Displayed First" + sorting_weight: 10 + +number: + - platform: template + name: "Template number" + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + web_server: + sorting_group_id: sorting_group_1 + sorting_weight: -1 +switch: + - platform: template + name: "Template Switch" + optimistic: true + web_server: + sorting_group_id: sorting_group_2 + sorting_weight: -10 +datetime: + - platform: template + name: Pick a Date + type: datetime + optimistic: yes + web_server: + sorting_group_id: sorting_group_3 + sorting_weight: -5 diff --git a/tests/components/web_server/test.esp32-ard.yaml b/tests/components/web_server/test.esp32-ard.yaml new file mode 100644 index 0000000000..7e6658e20e --- /dev/null +++ b/tests/components/web_server/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common_v2.yaml diff --git a/tests/components/web_server/test.esp32-c3-ard.yaml b/tests/components/web_server/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7e6658e20e --- /dev/null +++ b/tests/components/web_server/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common_v2.yaml diff --git a/tests/components/web_server/test.esp32-c3-idf.yaml b/tests/components/web_server/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7e6658e20e --- /dev/null +++ b/tests/components/web_server/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common_v2.yaml diff --git a/tests/components/web_server/test.esp32-idf.yaml b/tests/components/web_server/test.esp32-idf.yaml new file mode 100644 index 0000000000..7e6658e20e --- /dev/null +++ b/tests/components/web_server/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common_v2.yaml diff --git a/tests/components/web_server/test.esp8266-ard.yaml b/tests/components/web_server/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7e6658e20e --- /dev/null +++ b/tests/components/web_server/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common_v2.yaml diff --git a/tests/components/web_server/test_v1.esp32-ard.yaml b/tests/components/web_server/test_v1.esp32-ard.yaml new file mode 100644 index 0000000000..389a930284 --- /dev/null +++ b/tests/components/web_server/test_v1.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common_v1.yaml diff --git a/tests/components/web_server/test_v3.esp32-ard.yaml b/tests/components/web_server/test_v3.esp32-ard.yaml new file mode 100644 index 0000000000..00d05521e4 --- /dev/null +++ b/tests/components/web_server/test_v3.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common_v3.yaml diff --git a/tests/components/whirlpool/test.esp32-ard.yaml b/tests/components/whirlpool/test.esp32-ard.yaml new file mode 100644 index 0000000000..4d0afc39a4 --- /dev/null +++ b/tests/components/whirlpool/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: whirlpool + name: Whirlpool Climate diff --git a/tests/components/whirlpool/test.esp32-c3-ard.yaml b/tests/components/whirlpool/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4d0afc39a4 --- /dev/null +++ b/tests/components/whirlpool/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: whirlpool + name: Whirlpool Climate diff --git a/tests/components/whirlpool/test.esp32-c3-idf.yaml b/tests/components/whirlpool/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4d0afc39a4 --- /dev/null +++ b/tests/components/whirlpool/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: whirlpool + name: Whirlpool Climate diff --git a/tests/components/whirlpool/test.esp32-idf.yaml b/tests/components/whirlpool/test.esp32-idf.yaml new file mode 100644 index 0000000000..4d0afc39a4 --- /dev/null +++ b/tests/components/whirlpool/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: whirlpool + name: Whirlpool Climate diff --git a/tests/components/whirlpool/test.esp8266-ard.yaml b/tests/components/whirlpool/test.esp8266-ard.yaml new file mode 100644 index 0000000000..efd530c160 --- /dev/null +++ b/tests/components/whirlpool/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: whirlpool + name: Whirlpool Climate diff --git a/tests/components/whynter/test.esp32-ard.yaml b/tests/components/whynter/test.esp32-ard.yaml new file mode 100644 index 0000000000..dc8fb9584d --- /dev/null +++ b/tests/components/whynter/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: whynter + name: Whynter Climate diff --git a/tests/components/whynter/test.esp32-c3-ard.yaml b/tests/components/whynter/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dc8fb9584d --- /dev/null +++ b/tests/components/whynter/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: whynter + name: Whynter Climate diff --git a/tests/components/whynter/test.esp32-c3-idf.yaml b/tests/components/whynter/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dc8fb9584d --- /dev/null +++ b/tests/components/whynter/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: whynter + name: Whynter Climate diff --git a/tests/components/whynter/test.esp32-idf.yaml b/tests/components/whynter/test.esp32-idf.yaml new file mode 100644 index 0000000000..dc8fb9584d --- /dev/null +++ b/tests/components/whynter/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: whynter + name: Whynter Climate diff --git a/tests/components/whynter/test.esp8266-ard.yaml b/tests/components/whynter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a656a7427d --- /dev/null +++ b/tests/components/whynter/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: whynter + name: Whynter Climate diff --git a/tests/components/wiegand/common.yaml b/tests/components/wiegand/common.yaml new file mode 100644 index 0000000000..4e15a44b89 --- /dev/null +++ b/tests/components/wiegand/common.yaml @@ -0,0 +1,10 @@ +wiegand: + - id: test_wiegand + d0: 5 + d1: 4 + on_key: + - lambda: ESP_LOGI("KEY", "Received key %d", x); + on_tag: + - lambda: ESP_LOGI("TAG", "Received tag %s", x.c_str()); + on_raw: + - lambda: ESP_LOGI("RAW", "Received raw %d bits, value %llx", bits, value); diff --git a/tests/components/wiegand/test.esp32-ard.yaml b/tests/components/wiegand/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wiegand/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wiegand/test.esp32-c3-ard.yaml b/tests/components/wiegand/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wiegand/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wiegand/test.esp32-c3-idf.yaml b/tests/components/wiegand/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wiegand/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wiegand/test.esp32-idf.yaml b/tests/components/wiegand/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wiegand/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wiegand/test.esp8266-ard.yaml b/tests/components/wiegand/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wiegand/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wiegand/test.rp2040-ard.yaml b/tests/components/wiegand/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wiegand/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/common-eap.yaml b/tests/components/wifi/common-eap.yaml new file mode 100644 index 0000000000..779cd6b49a --- /dev/null +++ b/tests/components/wifi/common-eap.yaml @@ -0,0 +1,7 @@ +wifi: + networks: + - ssid: MySSID + eap: + username: username + password: password + identity: identity diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml new file mode 100644 index 0000000000..003f6347be --- /dev/null +++ b/tests/components/wifi/common.yaml @@ -0,0 +1,9 @@ +esphome: + on_boot: + then: + - wifi.disable + - wifi.enable + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/wifi/test-eap.esp32-ard.yaml b/tests/components/wifi/test-eap.esp32-ard.yaml new file mode 100644 index 0000000000..9177e5de10 --- /dev/null +++ b/tests/components/wifi/test-eap.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-eap.yaml diff --git a/tests/components/wifi/test-eap.esp32-idf.yaml b/tests/components/wifi/test-eap.esp32-idf.yaml new file mode 100644 index 0000000000..9177e5de10 --- /dev/null +++ b/tests/components/wifi/test-eap.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-eap.yaml diff --git a/tests/components/wifi/test.esp32-ard.yaml b/tests/components/wifi/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.esp32-c3-ard.yaml b/tests/components/wifi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.esp32-c3-idf.yaml b/tests/components/wifi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.rp2040-ard.yaml b/tests/components/wifi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_info/common.yaml b/tests/components/wifi_info/common.yaml new file mode 100644 index 0000000000..cf5ea563ba --- /dev/null +++ b/tests/components/wifi_info/common.yaml @@ -0,0 +1,18 @@ +wifi: + ssid: MySSID + password: password1 + +text_sensor: + - platform: wifi_info + scan_results: + name: Scan Results + ip_address: + name: IP Address + ssid: + name: SSID + bssid: + name: BSSID + mac_address: + name: Mac Address + dns_address: + name: DNS ADdress diff --git a/tests/components/wifi_info/test.esp32-ard.yaml b/tests/components/wifi_info/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_info/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-c3-ard.yaml b/tests/components/wifi_info/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_info/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-c3-idf.yaml b/tests/components/wifi_info/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_info/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-idf.yaml b/tests/components/wifi_info/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_info/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp8266-ard.yaml b/tests/components/wifi_info/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_info/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_info/test.rp2040-ard.yaml b/tests/components/wifi_info/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_info/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_signal/common.yaml b/tests/components/wifi_signal/common.yaml new file mode 100644 index 0000000000..58d1cab244 --- /dev/null +++ b/tests/components/wifi_signal/common.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +sensor: + - platform: wifi_signal + name: WiFi Signal Sensor + update_interval: 15s diff --git a/tests/components/wifi_signal/test.esp32-ard.yaml b/tests/components/wifi_signal/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_signal/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp32-c3-ard.yaml b/tests/components/wifi_signal/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_signal/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp32-c3-idf.yaml b/tests/components/wifi_signal/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_signal/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp32-idf.yaml b/tests/components/wifi_signal/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_signal/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp8266-ard.yaml b/tests/components/wifi_signal/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_signal/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.rp2040-ard.yaml b/tests/components/wifi_signal/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi_signal/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wireguard/common.yaml b/tests/components/wireguard/common.yaml new file mode 100644 index 0000000000..cd7ab1075e --- /dev/null +++ b/tests/components/wireguard/common.yaml @@ -0,0 +1,59 @@ +wifi: + ssid: "MySSID1" + password: "password1" + +time: + - platform: sntp + +wireguard: + address: 172.16.34.100 + netmask: 255.255.255.0 + # NEVER use the following keys for your VPN -- they are now public! + private_key: wPBMxtNYH3mChicrbpsRpZIasIdPq3yZuthn23FbGG8= + peer_public_key: Hs2JfikvYU03/Kv3YoAs1hrUIPPTEkpsZKSPUljE9yc= + peer_preshared_key: 20fjM5GRnSolGPC5SRj9ljgIUyQfruv0B0bvLl3Yt60= + peer_endpoint: wg.server.example + peer_persistent_keepalive: 25s + peer_allowed_ips: + - 172.16.34.0/24 + - 192.168.4.0/24 + +binary_sensor: + - platform: wireguard + status: + name: 'WireGuard Status' + enabled: + name: 'WireGuard Enabled' + +sensor: + - platform: wireguard + latest_handshake: + name: 'WireGuard Latest Handshake' + +text_sensor: + - platform: wireguard + address: + name: 'WireGuard Address' + +button: + - platform: template + name: 'Toggle WireGuard' + entity_category: config + on_press: + - if: + condition: wireguard.enabled + then: + - wireguard.disable: + else: + - wireguard.enable: + + - platform: template + name: 'Log WireGuard status' + entity_category: config + on_press: + - if: + condition: wireguard.peer_online + then: + - logger.log: 'wireguard remote peer is online' + else: + - logger.log: 'wireguard remote peer is offline' diff --git a/tests/components/wireguard/test.bk72xx-ard.yaml b/tests/components/wireguard/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wireguard/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wireguard/test.esp32-ard.yaml b/tests/components/wireguard/test.esp32-ard.yaml new file mode 100644 index 0000000000..2798f8e566 --- /dev/null +++ b/tests/components/wireguard/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +<<: !include common.yaml + +network: + enable_ipv6: true diff --git a/tests/components/wireguard/test.esp32-c3-ard.yaml b/tests/components/wireguard/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wireguard/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wireguard/test.esp32-c3-idf.yaml b/tests/components/wireguard/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wireguard/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wireguard/test.esp32-idf.yaml b/tests/components/wireguard/test.esp32-idf.yaml new file mode 100644 index 0000000000..2798f8e566 --- /dev/null +++ b/tests/components/wireguard/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +<<: !include common.yaml + +network: + enable_ipv6: true diff --git a/tests/components/wireguard/test.esp8266-ard.yaml b/tests/components/wireguard/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2798f8e566 --- /dev/null +++ b/tests/components/wireguard/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +<<: !include common.yaml + +network: + enable_ipv6: true diff --git a/tests/components/wk2132_i2c/common.yaml b/tests/components/wk2132_i2c/common.yaml new file mode 100644 index 0000000000..f9c8ab756d --- /dev/null +++ b/tests/components/wk2132_i2c/common.yaml @@ -0,0 +1,20 @@ +i2c: + id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true + frequency: 600kHz + +wk2132_i2c: + - id: wk2132_i2c_id + address: 0x70 + i2c_id: i2c_bus + uart: + - id: wk2132_id_0 + channel: 0 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: wk2132_id_1 + channel: 1 + baud_rate: 19200 diff --git a/tests/components/wk2132_i2c/test.esp32-ard.yaml b/tests/components/wk2132_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/wk2132_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/wk2132_i2c/test.esp32-idf.yaml b/tests/components/wk2132_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/wk2132_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/wk2132_i2c/test.esp32-s3-ard.yaml b/tests/components/wk2132_i2c/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/wk2132_i2c/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/wk2132_spi/common.yaml b/tests/components/wk2132_spi/common.yaml new file mode 100644 index 0000000000..b21e89120c --- /dev/null +++ b/tests/components/wk2132_spi/common.yaml @@ -0,0 +1,21 @@ +spi: + id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +wk2132_spi: + - id: wk2132_spi_id + cs_pin: ${cs_pin} + spi_id: spi_bus + crystal: 11059200 + data_rate: 1MHz + uart: + - id: wk2132_spi_id0 + channel: 0 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: wk2132_spi_id1 + channel: 1 + baud_rate: 921600 diff --git a/tests/components/wk2132_spi/test.esp32-ard.yaml b/tests/components/wk2132_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..76e7138ab0 --- /dev/null +++ b/tests/components/wk2132_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO18 + miso_pin: GPIO19 + mosi_pin: GPIO23 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-idf.yaml b/tests/components/wk2132_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..76e7138ab0 --- /dev/null +++ b/tests/components/wk2132_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO18 + miso_pin: GPIO19 + mosi_pin: GPIO23 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-s3-ard.yaml b/tests/components/wk2132_spi/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..b0aadf620a --- /dev/null +++ b/tests/components/wk2132_spi/test.esp32-s3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO40 + miso_pin: GPIO41 + mosi_pin: GPIO6 + cs_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml b/tests/components/wk2132_spi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..b0aadf620a --- /dev/null +++ b/tests/components/wk2132_spi/test.esp32-s3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO40 + miso_pin: GPIO41 + mosi_pin: GPIO6 + cs_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/common.yaml b/tests/components/wk2168_i2c/common.yaml new file mode 100644 index 0000000000..fe4689d6db --- /dev/null +++ b/tests/components/wk2168_i2c/common.yaml @@ -0,0 +1,63 @@ +i2c: + id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true + frequency: 600kHz + +# component declaration +wk2168_i2c: + - id: bridge_i2c + i2c_id: i2c_bus + address: 0x70 + uart: + - id: id0 + channel: 0 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: id1 + channel: 1 + baud_rate: 115200 + - id: id2 + channel: 2 + baud_rate: 115200 + - id: id3 + channel: 3 + baud_rate: 115200 + +# individual binary_sensor inputs +binary_sensor: + - platform: gpio + name: "pin_0" + pin: + wk2168_i2c: bridge_i2c + number: 0 + mode: + input: true + - platform: gpio + name: "pin_1" + pin: + wk2168_i2c: bridge_i2c + number: 1 + mode: + input: true + inverted: true + +# Individual binary outputs +switch: + - platform: gpio + name: "pin_2" + pin: + wk2168_i2c: bridge_i2c + number: 2 + mode: + output: true + - platform: gpio + name: "pin_3" + pin: + wk2168_i2c: bridge_i2c + number: 3 + mode: + output: true + inverted: true diff --git a/tests/components/wk2168_i2c/test.esp32-ard.yaml b/tests/components/wk2168_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/wk2168_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-idf.yaml b/tests/components/wk2168_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/wk2168_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-s3-ard.yaml b/tests/components/wk2168_i2c/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/wk2168_i2c/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/wk2168_spi/common.yaml b/tests/components/wk2168_spi/common.yaml new file mode 100644 index 0000000000..7626e18df6 --- /dev/null +++ b/tests/components/wk2168_spi/common.yaml @@ -0,0 +1,63 @@ +spi: + id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +wk2168_spi: + - id: bridge_spi + cs_pin: ${cs_pin} + spi_id: spi_bus + crystal: 11059200 + data_rate: 1MHz + uart: + - id: id0 + channel: 0 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: id1 + channel: 1 + baud_rate: 115200 + - id: id2 + channel: 2 + baud_rate: 115200 + - id: id3 + channel: 3 + baud_rate: 115200 + +# individual binary_sensor inputs +binary_sensor: + - platform: gpio + name: "pin_0" + pin: + wk2168_spi: bridge_spi + number: 0 + mode: + input: true + - platform: gpio + name: "pin_1" + pin: + wk2168_spi: bridge_spi + number: 1 + mode: + input: true + inverted: true + +# Individual binary outputs +switch: + - platform: gpio + name: "pin_2" + pin: + wk2168_spi: bridge_spi + number: 2 + mode: + output: true + - platform: gpio + name: "pin_3" + pin: + wk2168_spi: bridge_spi + number: 3 + mode: + output: true + inverted: true diff --git a/tests/components/wk2168_spi/test.esp32-ard.yaml b/tests/components/wk2168_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..76e7138ab0 --- /dev/null +++ b/tests/components/wk2168_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO18 + miso_pin: GPIO19 + mosi_pin: GPIO23 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-idf.yaml b/tests/components/wk2168_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..76e7138ab0 --- /dev/null +++ b/tests/components/wk2168_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO18 + miso_pin: GPIO19 + mosi_pin: GPIO23 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-s3-ard.yaml b/tests/components/wk2168_spi/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..b0aadf620a --- /dev/null +++ b/tests/components/wk2168_spi/test.esp32-s3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO40 + miso_pin: GPIO41 + mosi_pin: GPIO6 + cs_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml b/tests/components/wk2168_spi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..b0aadf620a --- /dev/null +++ b/tests/components/wk2168_spi/test.esp32-s3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO40 + miso_pin: GPIO41 + mosi_pin: GPIO6 + cs_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/common.yaml b/tests/components/wk2204_i2c/common.yaml new file mode 100644 index 0000000000..80f636c690 --- /dev/null +++ b/tests/components/wk2204_i2c/common.yaml @@ -0,0 +1,28 @@ +i2c: + id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true + frequency: 600kHz + +wk2204_i2c: + - id: wk2204_i2c_id + i2c_id: i2c_bus + address: 0x70 + uart: + - id: wk2204_id_0 + channel: 0 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: wk2204_id_1 + channel: 1 + baud_rate: 19200 + - id: wk2204_id_2 + channel: 2 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: wk2204_id_3 + channel: 3 + baud_rate: 19200 diff --git a/tests/components/wk2204_i2c/test.esp32-ard.yaml b/tests/components/wk2204_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/wk2204_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-idf.yaml b/tests/components/wk2204_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/wk2204_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-s3-ard.yaml b/tests/components/wk2204_i2c/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/wk2204_i2c/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/wk2204_spi/common.yaml b/tests/components/wk2204_spi/common.yaml new file mode 100644 index 0000000000..3bae9c9a6d --- /dev/null +++ b/tests/components/wk2204_spi/common.yaml @@ -0,0 +1,29 @@ +spi: + id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +wk2204_spi: + - id: wk2204_spi_id + cs_pin: ${cs_pin} + spi_id: spi_bus + crystal: 11059200 + data_rate: 1MHz + uart: + - id: wk2204_spi_id0 + channel: 0 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: wk2204_spi_id1 + channel: 1 + baud_rate: 921600 + - id: wk2204_spi_id2 + channel: 2 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: wk2204_spi_id3 + channel: 3 + baud_rate: 921600 diff --git a/tests/components/wk2204_spi/test.esp32-ard.yaml b/tests/components/wk2204_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..76e7138ab0 --- /dev/null +++ b/tests/components/wk2204_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO18 + miso_pin: GPIO19 + mosi_pin: GPIO23 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-idf.yaml b/tests/components/wk2204_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..76e7138ab0 --- /dev/null +++ b/tests/components/wk2204_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO18 + miso_pin: GPIO19 + mosi_pin: GPIO23 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-s3-ard.yaml b/tests/components/wk2204_spi/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..b0aadf620a --- /dev/null +++ b/tests/components/wk2204_spi/test.esp32-s3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO40 + miso_pin: GPIO41 + mosi_pin: GPIO6 + cs_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml b/tests/components/wk2204_spi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..b0aadf620a --- /dev/null +++ b/tests/components/wk2204_spi/test.esp32-s3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO40 + miso_pin: GPIO41 + mosi_pin: GPIO6 + cs_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/common.yaml b/tests/components/wk2212_i2c/common.yaml new file mode 100644 index 0000000000..2e891c5520 --- /dev/null +++ b/tests/components/wk2212_i2c/common.yaml @@ -0,0 +1,59 @@ +i2c: + id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true + frequency: 600kHz + +# component declaration +wk2212_i2c: + - id: bridge_i2c + i2c_id: i2c_bus + address: 0x70 + uart: + - id: uart_i2c_id0 + channel: 0 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: uart_i2c_id1 + channel: 1 + baud_rate: 115200 + stop_bits: 1 + parity: none + +# individual binary_sensor inputs +binary_sensor: + - platform: gpio + name: "pin_0" + pin: + wk2212_i2c: bridge_i2c + number: 0 + mode: + input: true + - platform: gpio + name: "pin_1" + pin: + wk2212_i2c: bridge_i2c + number: 1 + mode: + input: true + inverted: true + +# Individual binary outputs +switch: + - platform: gpio + name: "pin_2" + pin: + wk2212_i2c: bridge_i2c + number: 2 + mode: + output: true + - platform: gpio + name: "pin_3" + pin: + wk2212_i2c: bridge_i2c + number: 3 + mode: + output: true + inverted: true diff --git a/tests/components/wk2212_i2c/test.esp32-ard.yaml b/tests/components/wk2212_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/wk2212_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-idf.yaml b/tests/components/wk2212_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..3b761d3fc1 --- /dev/null +++ b/tests/components/wk2212_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-s3-ard.yaml b/tests/components/wk2212_i2c/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/wk2212_i2c/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..4942e3c2b3 --- /dev/null +++ b/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO40 + sda_pin: GPIO41 + +<<: !include common.yaml diff --git a/tests/components/wk2212_spi/common.yaml b/tests/components/wk2212_spi/common.yaml new file mode 100644 index 0000000000..ad9f11d9e8 --- /dev/null +++ b/tests/components/wk2212_spi/common.yaml @@ -0,0 +1,58 @@ +spi: + id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +wk2212_spi: + - id: bridge_spi + cs_pin: ${cs_pin} + spi_id: spi_bus + crystal: 11059200 + data_rate: 1MHz + uart: + - id: id0 + channel: 0 + baud_rate: 115200 + stop_bits: 1 + parity: none + - id: id1 + channel: 1 + baud_rate: 115200 + +# individual binary_sensor inputs +binary_sensor: + - platform: gpio + name: "pin_0" + pin: + wk2212_spi: bridge_spi + number: 0 + mode: + input: true + - platform: gpio + name: "pin_1" + pin: + wk2212_spi: bridge_spi + number: 1 + mode: + input: true + inverted: true + +# Individual binary outputs +switch: + - platform: gpio + name: "pin_2" + pin: + wk2212_spi: bridge_spi + number: 2 + mode: + output: true + - platform: gpio + name: "pin_3" + pin: + wk2212_spi: bridge_spi + number: 3 + mode: + output: true + inverted: true + diff --git a/tests/components/wk2212_spi/test.esp32-ard.yaml b/tests/components/wk2212_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..76e7138ab0 --- /dev/null +++ b/tests/components/wk2212_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO18 + miso_pin: GPIO19 + mosi_pin: GPIO23 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-idf.yaml b/tests/components/wk2212_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..76e7138ab0 --- /dev/null +++ b/tests/components/wk2212_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO18 + miso_pin: GPIO19 + mosi_pin: GPIO23 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-s3-ard.yaml b/tests/components/wk2212_spi/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..b0aadf620a --- /dev/null +++ b/tests/components/wk2212_spi/test.esp32-s3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO40 + miso_pin: GPIO41 + mosi_pin: GPIO6 + cs_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml b/tests/components/wk2212_spi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..b0aadf620a --- /dev/null +++ b/tests/components/wk2212_spi/test.esp32-s3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO40 + miso_pin: GPIO41 + mosi_pin: GPIO6 + cs_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/wl_134/test.esp32-ard.yaml b/tests/components/wl_134/test.esp32-ard.yaml new file mode 100644 index 0000000000..d517889d28 --- /dev/null +++ b/tests/components/wl_134/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_wl_134 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +text_sensor: + - platform: wl_134 + name: Transponder Code + reset: true diff --git a/tests/components/wl_134/test.esp32-c3-ard.yaml b/tests/components/wl_134/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..7cda1ba060 --- /dev/null +++ b/tests/components/wl_134/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_wl_134 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +text_sensor: + - platform: wl_134 + name: Transponder Code + reset: true diff --git a/tests/components/wl_134/test.esp32-c3-idf.yaml b/tests/components/wl_134/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..7cda1ba060 --- /dev/null +++ b/tests/components/wl_134/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_wl_134 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +text_sensor: + - platform: wl_134 + name: Transponder Code + reset: true diff --git a/tests/components/wl_134/test.esp32-idf.yaml b/tests/components/wl_134/test.esp32-idf.yaml new file mode 100644 index 0000000000..d517889d28 --- /dev/null +++ b/tests/components/wl_134/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_wl_134 + tx_pin: 17 + rx_pin: 16 + baud_rate: 9600 + +text_sensor: + - platform: wl_134 + name: Transponder Code + reset: true diff --git a/tests/components/wl_134/test.esp8266-ard.yaml b/tests/components/wl_134/test.esp8266-ard.yaml new file mode 100644 index 0000000000..7cda1ba060 --- /dev/null +++ b/tests/components/wl_134/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_wl_134 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +text_sensor: + - platform: wl_134 + name: Transponder Code + reset: true diff --git a/tests/components/wl_134/test.rp2040-ard.yaml b/tests/components/wl_134/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7cda1ba060 --- /dev/null +++ b/tests/components/wl_134/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +uart: + - id: uart_wl_134 + tx_pin: 4 + rx_pin: 5 + baud_rate: 9600 + +text_sensor: + - platform: wl_134 + name: Transponder Code + reset: true diff --git a/tests/components/wled/test.esp32-ard.yaml b/tests/components/wled/test.esp32-ard.yaml new file mode 100644 index 0000000000..a24f28e154 --- /dev/null +++ b/tests/components/wled/test.esp32-ard.yaml @@ -0,0 +1,17 @@ +wifi: + ssid: MySSID + password: password1 + +wled: + +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + effects: + - wled: diff --git a/tests/components/wled/test.esp32-c3-ard.yaml b/tests/components/wled/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a24f28e154 --- /dev/null +++ b/tests/components/wled/test.esp32-c3-ard.yaml @@ -0,0 +1,17 @@ +wifi: + ssid: MySSID + password: password1 + +wled: + +light: + - platform: esp32_rmt_led_strip + id: led_matrix_32x8 + default_transition_length: 500ms + chipset: ws2812 + rgb_order: GRB + num_leds: 256 + pin: 2 + rmt_channel: 0 + effects: + - wled: diff --git a/tests/components/wled/test.esp8266-ard.yaml b/tests/components/wled/test.esp8266-ard.yaml new file mode 100644 index 0000000000..e291af927c --- /dev/null +++ b/tests/components/wled/test.esp8266-ard.yaml @@ -0,0 +1,17 @@ +wifi: + ssid: MySSID + password: password1 + +wled: + +light: + - platform: neopixelbus + id: addr + name: Neopixelbus Light + method: esp8266_uart + num_leds: 5 + pin: 2 + type: GRBW + variant: SK6812 + effects: + - wled: diff --git a/tests/components/x9c/test.esp32-ard.yaml b/tests/components/x9c/test.esp32-ard.yaml new file mode 100644 index 0000000000..f587b69b4f --- /dev/null +++ b/tests/components/x9c/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +output: + - platform: x9c + id: test_x9c + cs_pin: 13 + inc_pin: 14 + ud_pin: 15 + initial_value: 0.5 + step_delay: 50us diff --git a/tests/components/x9c/test.esp32-c3-ard.yaml b/tests/components/x9c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..972c743fcd --- /dev/null +++ b/tests/components/x9c/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +output: + - platform: x9c + id: test_x9c + cs_pin: 3 + inc_pin: 4 + ud_pin: 5 + initial_value: 0.5 + step_delay: 50us diff --git a/tests/components/x9c/test.esp32-c3-idf.yaml b/tests/components/x9c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..972c743fcd --- /dev/null +++ b/tests/components/x9c/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +output: + - platform: x9c + id: test_x9c + cs_pin: 3 + inc_pin: 4 + ud_pin: 5 + initial_value: 0.5 + step_delay: 50us diff --git a/tests/components/x9c/test.esp32-idf.yaml b/tests/components/x9c/test.esp32-idf.yaml new file mode 100644 index 0000000000..f587b69b4f --- /dev/null +++ b/tests/components/x9c/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +output: + - platform: x9c + id: test_x9c + cs_pin: 13 + inc_pin: 14 + ud_pin: 15 + initial_value: 0.5 + step_delay: 50us diff --git a/tests/components/x9c/test.esp8266-ard.yaml b/tests/components/x9c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..f587b69b4f --- /dev/null +++ b/tests/components/x9c/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +output: + - platform: x9c + id: test_x9c + cs_pin: 13 + inc_pin: 14 + ud_pin: 15 + initial_value: 0.5 + step_delay: 50us diff --git a/tests/components/x9c/test.rp2040-ard.yaml b/tests/components/x9c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..972c743fcd --- /dev/null +++ b/tests/components/x9c/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +output: + - platform: x9c + id: test_x9c + cs_pin: 3 + inc_pin: 4 + ud_pin: 5 + initial_value: 0.5 + step_delay: 50us diff --git a/tests/components/xgzp68xx/test.esp32-ard.yaml b/tests/components/xgzp68xx/test.esp32-ard.yaml new file mode 100644 index 0000000000..fb55421123 --- /dev/null +++ b/tests/components/xgzp68xx/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_xgzp68xx + scl: 16 + sda: 17 + +sensor: + - platform: xgzp68xx + k_value: 4096 + temperature: + name: Pressure Temperature + pressure: + name: Differential pressure diff --git a/tests/components/xgzp68xx/test.esp32-c3-ard.yaml b/tests/components/xgzp68xx/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..25df8cc225 --- /dev/null +++ b/tests/components/xgzp68xx/test.esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_xgzp68xx + scl: 5 + sda: 4 + +sensor: + - platform: xgzp68xx + k_value: 4096 + temperature: + name: Pressure Temperature + pressure: + name: Differential pressure diff --git a/tests/components/xgzp68xx/test.esp32-c3-idf.yaml b/tests/components/xgzp68xx/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25df8cc225 --- /dev/null +++ b/tests/components/xgzp68xx/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_xgzp68xx + scl: 5 + sda: 4 + +sensor: + - platform: xgzp68xx + k_value: 4096 + temperature: + name: Pressure Temperature + pressure: + name: Differential pressure diff --git a/tests/components/xgzp68xx/test.esp32-idf.yaml b/tests/components/xgzp68xx/test.esp32-idf.yaml new file mode 100644 index 0000000000..fb55421123 --- /dev/null +++ b/tests/components/xgzp68xx/test.esp32-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_xgzp68xx + scl: 16 + sda: 17 + +sensor: + - platform: xgzp68xx + k_value: 4096 + temperature: + name: Pressure Temperature + pressure: + name: Differential pressure diff --git a/tests/components/xgzp68xx/test.esp8266-ard.yaml b/tests/components/xgzp68xx/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25df8cc225 --- /dev/null +++ b/tests/components/xgzp68xx/test.esp8266-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_xgzp68xx + scl: 5 + sda: 4 + +sensor: + - platform: xgzp68xx + k_value: 4096 + temperature: + name: Pressure Temperature + pressure: + name: Differential pressure diff --git a/tests/components/xgzp68xx/test.rp2040-ard.yaml b/tests/components/xgzp68xx/test.rp2040-ard.yaml new file mode 100644 index 0000000000..25df8cc225 --- /dev/null +++ b/tests/components/xgzp68xx/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_xgzp68xx + scl: 5 + sda: 4 + +sensor: + - platform: xgzp68xx + k_value: 4096 + temperature: + name: Pressure Temperature + pressure: + name: Differential pressure diff --git a/tests/components/xiaomi_ble/common.yaml b/tests/components/xiaomi_ble/common.yaml new file mode 100644 index 0000000000..9d10393177 --- /dev/null +++ b/tests/components/xiaomi_ble/common.yaml @@ -0,0 +1,3 @@ +esp32_ble_tracker: + +xiaomi_ble: diff --git a/tests/components/xiaomi_ble/test.esp32-ard.yaml b/tests/components/xiaomi_ble/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_ble/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-c3-ard.yaml b/tests/components/xiaomi_ble/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_ble/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml b/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-idf.yaml b/tests/components/xiaomi_ble/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_ble/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/common.yaml b/tests/components/xiaomi_cgd1/common.yaml new file mode 100644 index 0000000000..94ed09e8f2 --- /dev/null +++ b/tests/components/xiaomi_cgd1/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_cgd1 + mac_address: A4:C1:38:D1:61:7D + bindkey: c99d2313182473b38001086febf781bd + temperature: + name: Xiaomi CGD1 Temperature + humidity: + name: Xiaomi CGD1 Humidity + battery_level: + name: Xiaomi CGD1 Battery Level diff --git a/tests/components/xiaomi_cgd1/test.esp32-ard.yaml b/tests/components/xiaomi_cgd1/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgd1/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-c3-ard.yaml b/tests/components/xiaomi_cgd1/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgd1/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-idf.yaml b/tests/components/xiaomi_cgd1/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgd1/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/common.yaml b/tests/components/xiaomi_cgdk2/common.yaml new file mode 100644 index 0000000000..dddca56222 --- /dev/null +++ b/tests/components/xiaomi_cgdk2/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_cgdk2 + mac_address: A4:C1:38:D1:61:7D + bindkey: c99d2313182473b38001086febf781bd + temperature: + name: Xiaomi CGD1 Temperature + humidity: + name: Xiaomi CGD1 Humidity + battery_level: + name: Xiaomi CGD1 Battery Level diff --git a/tests/components/xiaomi_cgdk2/test.esp32-ard.yaml b/tests/components/xiaomi_cgdk2/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgdk2/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-c3-ard.yaml b/tests/components/xiaomi_cgdk2/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgdk2/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-idf.yaml b/tests/components/xiaomi_cgdk2/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgdk2/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/common.yaml b/tests/components/xiaomi_cgg1/common.yaml new file mode 100644 index 0000000000..170aebfbde --- /dev/null +++ b/tests/components/xiaomi_cgg1/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_cgg1 + mac_address: A4:C1:38:D1:61:7D + bindkey: c99d2313182473b38001086febf781bd + temperature: + name: Xiaomi CGD1 Temperature + humidity: + name: Xiaomi CGD1 Humidity + battery_level: + name: Xiaomi CGD1 Battery Level diff --git a/tests/components/xiaomi_cgg1/test.esp32-ard.yaml b/tests/components/xiaomi_cgg1/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgg1/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-c3-ard.yaml b/tests/components/xiaomi_cgg1/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgg1/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-idf.yaml b/tests/components/xiaomi_cgg1/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgg1/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/common.yaml b/tests/components/xiaomi_cgpr1/common.yaml new file mode 100644 index 0000000000..48082a886c --- /dev/null +++ b/tests/components/xiaomi_cgpr1/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: xiaomi_cgpr1 + name: CGPR1 Motion + mac_address: "12:34:56:12:34:56" + bindkey: 48403ebe2d385db8d0c187f81e62cb64 + battery_level: + name: CGPR1 battery Level + idle_time: + name: CGPR1 Idle Time + illuminance: + name: CGPR1 Illuminance diff --git a/tests/components/xiaomi_cgpr1/test.esp32-ard.yaml b/tests/components/xiaomi_cgpr1/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgpr1/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-c3-ard.yaml b/tests/components/xiaomi_cgpr1/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgpr1/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-idf.yaml b/tests/components/xiaomi_cgpr1/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_cgpr1/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/common.yaml b/tests/components/xiaomi_gcls002/common.yaml new file mode 100644 index 0000000000..32990708cc --- /dev/null +++ b/tests/components/xiaomi_gcls002/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_gcls002 + mac_address: 94:2B:FF:5C:91:61 + temperature: + name: GCLS02 Temperature + moisture: + name: GCLS02 Moisture + conductivity: + name: GCLS02 Soil Conductivity + illuminance: + name: GCLS02 Illuminance diff --git a/tests/components/xiaomi_gcls002/test.esp32-ard.yaml b/tests/components/xiaomi_gcls002/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_gcls002/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-c3-ard.yaml b/tests/components/xiaomi_gcls002/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_gcls002/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml b/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-idf.yaml b/tests/components/xiaomi_gcls002/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_gcls002/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/common.yaml b/tests/components/xiaomi_hhccjcy01/common.yaml new file mode 100644 index 0000000000..0def909488 --- /dev/null +++ b/tests/components/xiaomi_hhccjcy01/common.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_hhccjcy01 + mac_address: 94:2B:FF:5C:91:61 + temperature: + name: Xiaomi HHCCJCY01 Temperature + moisture: + name: Xiaomi HHCCJCY01 Moisture + illuminance: + name: Xiaomi HHCCJCY01 Illuminance + conductivity: + name: Xiaomi HHCCJCY01 Soil Conductivity + battery_level: + name: Xiaomi HHCCJCY01 Battery Level diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-ard.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_hhccjcy01/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-ard.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_hhccjcy01/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-idf.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_hhccjcy01/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/common.yaml b/tests/components/xiaomi_hhccpot002/common.yaml new file mode 100644 index 0000000000..2e5fa14620 --- /dev/null +++ b/tests/components/xiaomi_hhccpot002/common.yaml @@ -0,0 +1,9 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_hhccpot002 + mac_address: 94:2B:FF:5C:91:61 + moisture: + name: HHCCPOT002 Moisture + conductivity: + name: HHCCPOT002 Soil Conductivity diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-ard.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_hhccpot002/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-c3-ard.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_hhccpot002/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-idf.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_hhccpot002/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/common.yaml b/tests/components/xiaomi_jqjcy01ym/common.yaml new file mode 100644 index 0000000000..54c4b33dcd --- /dev/null +++ b/tests/components/xiaomi_jqjcy01ym/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_jqjcy01ym + mac_address: 7A:80:8E:19:36:BA + temperature: + name: JQJCY01YM Temperature + humidity: + name: JQJCY01YM Humidity + formaldehyde: + name: JQJCY01YM Formaldehyde + battery_level: + name: JQJCY01YM Battery Level diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-ard.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_jqjcy01ym/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-ard.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-idf.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_jqjcy01ym/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/common.yaml b/tests/components/xiaomi_lywsd02/common.yaml new file mode 100644 index 0000000000..3e40ab8d70 --- /dev/null +++ b/tests/components/xiaomi_lywsd02/common.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_lywsd02 + mac_address: 3F:5B:7D:82:58:4E + temperature: + name: Xiaomi LYWSD02 Temperature + humidity: + name: Xiaomi LYWSD02 Humidity + battery_level: + name: Xiaomi LYWSD02 Battery Level diff --git a/tests/components/xiaomi_lywsd02/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd02/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd02/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd02/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/common.yaml b/tests/components/xiaomi_lywsd02mmc/common.yaml new file mode 100644 index 0000000000..e63f585830 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_lywsd02mmc + mac_address: A4:C1:38:54:5E:18 + bindkey: 2529d8e0d23150a588675cc54ad48400 + temperature: + name: Xiaomi LYWSD02MMC Temperature + humidity: + name: Xiaomi LYWSD02MMC Humidity + battery_level: + name: Xiaomi LYWSD02MMC Battery Level diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/common.yaml b/tests/components/xiaomi_lywsd03mmc/common.yaml new file mode 100644 index 0000000000..d10a859c56 --- /dev/null +++ b/tests/components/xiaomi_lywsd03mmc/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_lywsd03mmc + mac_address: A4:C1:38:4E:16:78 + bindkey: e9efaa6873f9f9c87a5e75a5f814801c + temperature: + name: Xiaomi LYWSD03MMC Temperature + humidity: + name: Xiaomi LYWSD03MMC Humidity + battery_level: + name: Xiaomi LYWSD03MMC Battery Level diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd03mmc/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd03mmc/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/common.yaml b/tests/components/xiaomi_lywsdcgq/common.yaml new file mode 100644 index 0000000000..d8422b4c0c --- /dev/null +++ b/tests/components/xiaomi_lywsdcgq/common.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_lywsdcgq + mac_address: 7A:80:8E:19:36:BA + temperature: + name: Xiaomi LYWSDCGQ Temperature + humidity: + name: Xiaomi LYWSDCGQ Humidity + battery_level: + name: Xiaomi LYWSDCGQ Battery Level diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-ard.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsdcgq/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsdcgq/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-idf.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsdcgq/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/common.yaml b/tests/components/xiaomi_mhoc303/common.yaml new file mode 100644 index 0000000000..e4353d3c6a --- /dev/null +++ b/tests/components/xiaomi_mhoc303/common.yaml @@ -0,0 +1,11 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_mhoc303 + mac_address: E7:50:59:32:A0:1C + temperature: + name: MHO-C303 Temperature + humidity: + name: MHO-C303 Humidity + battery_level: + name: MHO-C303 Battery Level diff --git a/tests/components/xiaomi_mhoc303/test.esp32-ard.yaml b/tests/components/xiaomi_mhoc303/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mhoc303/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-c3-ard.yaml b/tests/components/xiaomi_mhoc303/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mhoc303/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-idf.yaml b/tests/components/xiaomi_mhoc303/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mhoc303/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/common.yaml b/tests/components/xiaomi_mhoc401/common.yaml new file mode 100644 index 0000000000..ae378f5604 --- /dev/null +++ b/tests/components/xiaomi_mhoc401/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_mhoc401 + mac_address: E7:50:59:32:A0:1C + bindkey: "eef418daf699a0c188f3bfd17e4565d9" + temperature: + name: MHO-C303 Temperature + humidity: + name: MHO-C303 Humidity + battery_level: + name: MHO-C303 Battery Level diff --git a/tests/components/xiaomi_mhoc401/test.esp32-ard.yaml b/tests/components/xiaomi_mhoc401/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mhoc401/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-c3-ard.yaml b/tests/components/xiaomi_mhoc401/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mhoc401/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-idf.yaml b/tests/components/xiaomi_mhoc401/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mhoc401/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale copy/common.yaml b/tests/components/xiaomi_miscale copy/common.yaml new file mode 100644 index 0000000000..89f32ad199 --- /dev/null +++ b/tests/components/xiaomi_miscale copy/common.yaml @@ -0,0 +1,9 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_miscale + mac_address: '5C:CA:D3:70:D4:A2' + weight: + name: "Xiaomi Mi Scale Weight" + impedance: + name: "Xiaomi Mi Scale Impedance" diff --git a/tests/components/xiaomi_miscale copy/test.esp32-ard.yaml b/tests/components/xiaomi_miscale copy/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_miscale copy/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale copy/test.esp32-c3-ard.yaml b/tests/components/xiaomi_miscale copy/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_miscale copy/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale copy/test.esp32-c3-idf.yaml b/tests/components/xiaomi_miscale copy/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_miscale copy/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale copy/test.esp32-idf.yaml b/tests/components/xiaomi_miscale copy/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_miscale copy/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/common.yaml b/tests/components/xiaomi_miscale/common.yaml new file mode 100644 index 0000000000..89f32ad199 --- /dev/null +++ b/tests/components/xiaomi_miscale/common.yaml @@ -0,0 +1,9 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_miscale + mac_address: '5C:CA:D3:70:D4:A2' + weight: + name: "Xiaomi Mi Scale Weight" + impedance: + name: "Xiaomi Mi Scale Impedance" diff --git a/tests/components/xiaomi_miscale/test.esp32-ard.yaml b/tests/components/xiaomi_miscale/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_miscale/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-c3-ard.yaml b/tests/components/xiaomi_miscale/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_miscale/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml b/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-idf.yaml b/tests/components/xiaomi_miscale/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_miscale/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/common.yaml b/tests/components/xiaomi_mjyd02yla/common.yaml new file mode 100644 index 0000000000..dffcef84c4 --- /dev/null +++ b/tests/components/xiaomi_mjyd02yla/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: xiaomi_mjyd02yla + name: MJYD02YL-A Motion + mac_address: 50:EC:50:CD:32:02 + bindkey: 48403ebe2d385db8d0c187f81e62cb64 + idle_time: + name: MJYD02YL-A Idle Time + light: + name: MJYD02YL-A Light Status + battery_level: + name: MJYD02YL-A Battery Level diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-ard.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mjyd02yla/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-ard.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mjyd02yla/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-idf.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mjyd02yla/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/common.yaml b/tests/components/xiaomi_mue4094rt/common.yaml new file mode 100644 index 0000000000..4f0e5ccbae --- /dev/null +++ b/tests/components/xiaomi_mue4094rt/common.yaml @@ -0,0 +1,7 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: xiaomi_mue4094rt + name: MUE4094RT Motion + mac_address: 7A:80:8E:19:36:BA + timeout: 5s diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-ard.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mue4094rt/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-c3-ard.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mue4094rt/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-idf.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_mue4094rt/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/common.yaml b/tests/components/xiaomi_rtcgq02lm/common.yaml new file mode 100644 index 0000000000..a2e0c66ba5 --- /dev/null +++ b/tests/components/xiaomi_rtcgq02lm/common.yaml @@ -0,0 +1,22 @@ +esp32_ble_tracker: + +xiaomi_rtcgq02lm: + - id: motion_rtcgq02lm + mac_address: 01:02:03:04:05:06 + bindkey: "48403ebe2d385db8d0c187f81e62cb64" + +binary_sensor: + - platform: xiaomi_rtcgq02lm + id: motion_rtcgq02lm + motion: + name: Mi Motion Sensor 2 + light: + name: Mi Motion Sensor 2 Light + button: + name: Mi Motion Sensor 2 Button + +sensor: + - platform: xiaomi_rtcgq02lm + id: motion_rtcgq02lm + battery_level: + name: Mi Motion Sensor 2 Battery level diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-ard.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_rtcgq02lm/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-ard.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-idf.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_rtcgq02lm/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/common.yaml b/tests/components/xiaomi_wx08zm/common.yaml new file mode 100644 index 0000000000..3e83ad3e95 --- /dev/null +++ b/tests/components/xiaomi_wx08zm/common.yaml @@ -0,0 +1,10 @@ +esp32_ble_tracker: + +binary_sensor: + - platform: xiaomi_wx08zm + name: WX08ZM Activation State + mac_address: 74:a3:4a:b5:07:34 + tablet: + name: WX08ZM Tablet Resource + battery_level: + name: WX08ZM Battery Level diff --git a/tests/components/xiaomi_wx08zm/test.esp32-ard.yaml b/tests/components/xiaomi_wx08zm/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_wx08zm/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-c3-ard.yaml b/tests/components/xiaomi_wx08zm/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_wx08zm/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml b/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-idf.yaml b/tests/components/xiaomi_wx08zm/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_wx08zm/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xl9535/test.esp32-ard.yaml b/tests/components/xl9535/test.esp32-ard.yaml new file mode 100644 index 0000000000..a65aae890e --- /dev/null +++ b/tests/components/xl9535/test.esp32-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_xl9535 + scl: 16 + sda: 17 + +xl9535: + - id: xl9535_hub + address: 0x20 + +binary_sensor: + - platform: gpio + name: XL9535 Pin 0 + pin: + xl9535: xl9535_hub + number: 0 + mode: + input: true + inverted: false + - platform: gpio + name: XL9535 Pin 17 + pin: + xl9535: xl9535_hub + number: 17 + mode: + input: true + inverted: false diff --git a/tests/components/xl9535/test.esp32-c3-ard.yaml b/tests/components/xl9535/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..178adf870e --- /dev/null +++ b/tests/components/xl9535/test.esp32-c3-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_xl9535 + scl: 5 + sda: 4 + +xl9535: + - id: xl9535_hub + address: 0x20 + +binary_sensor: + - platform: gpio + name: XL9535 Pin 0 + pin: + xl9535: xl9535_hub + number: 0 + mode: + input: true + inverted: false + - platform: gpio + name: XL9535 Pin 17 + pin: + xl9535: xl9535_hub + number: 17 + mode: + input: true + inverted: false diff --git a/tests/components/xl9535/test.esp32-c3-idf.yaml b/tests/components/xl9535/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..178adf870e --- /dev/null +++ b/tests/components/xl9535/test.esp32-c3-idf.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_xl9535 + scl: 5 + sda: 4 + +xl9535: + - id: xl9535_hub + address: 0x20 + +binary_sensor: + - platform: gpio + name: XL9535 Pin 0 + pin: + xl9535: xl9535_hub + number: 0 + mode: + input: true + inverted: false + - platform: gpio + name: XL9535 Pin 17 + pin: + xl9535: xl9535_hub + number: 17 + mode: + input: true + inverted: false diff --git a/tests/components/xl9535/test.esp32-idf.yaml b/tests/components/xl9535/test.esp32-idf.yaml new file mode 100644 index 0000000000..a65aae890e --- /dev/null +++ b/tests/components/xl9535/test.esp32-idf.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_xl9535 + scl: 16 + sda: 17 + +xl9535: + - id: xl9535_hub + address: 0x20 + +binary_sensor: + - platform: gpio + name: XL9535 Pin 0 + pin: + xl9535: xl9535_hub + number: 0 + mode: + input: true + inverted: false + - platform: gpio + name: XL9535 Pin 17 + pin: + xl9535: xl9535_hub + number: 17 + mode: + input: true + inverted: false diff --git a/tests/components/xl9535/test.esp8266-ard.yaml b/tests/components/xl9535/test.esp8266-ard.yaml new file mode 100644 index 0000000000..178adf870e --- /dev/null +++ b/tests/components/xl9535/test.esp8266-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_xl9535 + scl: 5 + sda: 4 + +xl9535: + - id: xl9535_hub + address: 0x20 + +binary_sensor: + - platform: gpio + name: XL9535 Pin 0 + pin: + xl9535: xl9535_hub + number: 0 + mode: + input: true + inverted: false + - platform: gpio + name: XL9535 Pin 17 + pin: + xl9535: xl9535_hub + number: 17 + mode: + input: true + inverted: false diff --git a/tests/components/xl9535/test.rp2040-ard.yaml b/tests/components/xl9535/test.rp2040-ard.yaml new file mode 100644 index 0000000000..178adf870e --- /dev/null +++ b/tests/components/xl9535/test.rp2040-ard.yaml @@ -0,0 +1,26 @@ +i2c: + - id: i2c_xl9535 + scl: 5 + sda: 4 + +xl9535: + - id: xl9535_hub + address: 0x20 + +binary_sensor: + - platform: gpio + name: XL9535 Pin 0 + pin: + xl9535: xl9535_hub + number: 0 + mode: + input: true + inverted: false + - platform: gpio + name: XL9535 Pin 17 + pin: + xl9535: xl9535_hub + number: 17 + mode: + input: true + inverted: false diff --git a/tests/components/xpt2046/test.esp32-ard.yaml b/tests/components/xpt2046/test.esp32-ard.yaml new file mode 100644 index 0000000000..9e305791e0 --- /dev/null +++ b/tests/components/xpt2046/test.esp32-ard.yaml @@ -0,0 +1,35 @@ +spi: + - id: spi_xpt2046 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: xpt_display + dimensions: 320x240 + model: TFT 2.4 + cs_pin: 13 + dc_pin: 14 + reset_pin: 21 + invert_colors: false + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: xpt2046 + id: xpt_touchscreen + cs_pin: 18 + interrupt_pin: 19 + display: xpt_display + update_interval: 50ms + threshold: 400 + calibration: + x_min: 280 + x_max: 3860 + y_min: 340 + y_max: 3860 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/xpt2046/test.esp32-c3-ard.yaml b/tests/components/xpt2046/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c03fd6b345 --- /dev/null +++ b/tests/components/xpt2046/test.esp32-c3-ard.yaml @@ -0,0 +1,35 @@ +spi: + - id: spi_xpt2046 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: xpt_display + dimensions: 320x240 + model: TFT 2.4 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: xpt2046 + id: xpt_touchscreen + cs_pin: 4 + interrupt_pin: 3 + display: xpt_display + update_interval: 50ms + threshold: 400 + calibration: + x_min: 28 + x_max: 280 + y_min: 340 + y_max: 3860 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/xpt2046/test.esp32-c3-idf.yaml b/tests/components/xpt2046/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..787ca9b1ed --- /dev/null +++ b/tests/components/xpt2046/test.esp32-c3-idf.yaml @@ -0,0 +1,35 @@ +spi: + - id: spi_xpt2046 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: xpt_display + dimensions: 320x240 + model: TFT 2.4 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: xpt2046 + id: xpt_touchscreen + cs_pin: 4 + interrupt_pin: 3 + display: xpt_display + update_interval: 50ms + threshold: 400 + calibration: + x_min: 50 + x_max: 280 + y_min: 340 + y_max: 3860 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/xpt2046/test.esp32-idf.yaml b/tests/components/xpt2046/test.esp32-idf.yaml new file mode 100644 index 0000000000..e79997146b --- /dev/null +++ b/tests/components/xpt2046/test.esp32-idf.yaml @@ -0,0 +1,35 @@ +spi: + - id: spi_xpt2046 + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: xpt_display + dimensions: 320x240 + model: TFT 2.4 + cs_pin: 13 + dc_pin: 14 + reset_pin: 21 + invert_colors: false + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: xpt2046 + id: xpt_touchscreen + cs_pin: 18 + interrupt_pin: 19 + display: xpt_display + update_interval: 50ms + threshold: 400 + calibration: + x_min: 50 + x_max: 280 + y_min: 340 + y_max: 3860 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/xpt2046/test.esp32-s2-ard.yaml b/tests/components/xpt2046/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..df2a99b4f5 --- /dev/null +++ b/tests/components/xpt2046/test.esp32-s2-ard.yaml @@ -0,0 +1,38 @@ +spi: + clk_pin: 7 + mosi_pin: 11 + miso_pin: 9 + +display: + - platform: ili9xxx + id: my_display + model: ili9341 + cs_pin: 5 + dc_pin: 12 + reset_pin: 33 + auto_clear_enabled: false + data_rate: 40MHz + dimensions: 320x240 + update_interval: never + invert_colors: false + transform: + mirror_y: false + mirror_x: false + swap_xy: true + +touchscreen: + - platform: xpt2046 + display: my_display + id: my_toucher + update_interval: 50ms + cs_pin: 18 + threshold: 300 + calibration: + x_min: 210 + x_max: 3890 + y_min: 170 + y_max: 3730 + transform: + mirror_x: false + mirror_y: true + swap_xy: true diff --git a/tests/components/xpt2046/test.esp8266-ard.yaml b/tests/components/xpt2046/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ab71f7b8bc --- /dev/null +++ b/tests/components/xpt2046/test.esp8266-ard.yaml @@ -0,0 +1,35 @@ +spi: + - id: spi_xpt2046 + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: xpt_display + dimensions: 320x240 + model: TFT 2.4 + cs_pin: 15 + dc_pin: 4 + reset_pin: 5 + invert_colors: false + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: xpt2046 + id: xpt_touchscreen + cs_pin: 0 + interrupt_pin: 16 + display: xpt_display + update_interval: 50ms + threshold: 400 + calibration: + x_min: 50 + x_max: 280 + y_min: 340 + y_max: 3860 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/xpt2046/test.rp2040-ard.yaml b/tests/components/xpt2046/test.rp2040-ard.yaml new file mode 100644 index 0000000000..622e69ac98 --- /dev/null +++ b/tests/components/xpt2046/test.rp2040-ard.yaml @@ -0,0 +1,35 @@ +spi: + - id: spi_xpt2046 + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ili9xxx + id: xpt_display + dimensions: 320x240 + model: TFT 2.4 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: xpt2046 + id: xpt_touchscreen + cs_pin: 5 + interrupt_pin: 6 + display: xpt_display + update_interval: 50ms + threshold: 400 + calibration: + x_min: 280 + x_max: 3860 + y_min: 340 + y_max: 3860 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] diff --git a/tests/components/yashima/test.esp32-ard.yaml b/tests/components/yashima/test.esp32-ard.yaml new file mode 100644 index 0000000000..4b6d6daee4 --- /dev/null +++ b/tests/components/yashima/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: yashima + name: Yashima Climate diff --git a/tests/components/yashima/test.esp32-c3-ard.yaml b/tests/components/yashima/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4b6d6daee4 --- /dev/null +++ b/tests/components/yashima/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: yashima + name: Yashima Climate diff --git a/tests/components/yashima/test.esp32-c3-idf.yaml b/tests/components/yashima/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4b6d6daee4 --- /dev/null +++ b/tests/components/yashima/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: yashima + name: Yashima Climate diff --git a/tests/components/yashima/test.esp32-idf.yaml b/tests/components/yashima/test.esp32-idf.yaml new file mode 100644 index 0000000000..4b6d6daee4 --- /dev/null +++ b/tests/components/yashima/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: yashima + name: Yashima Climate diff --git a/tests/components/yashima/test.esp8266-ard.yaml b/tests/components/yashima/test.esp8266-ard.yaml new file mode 100644 index 0000000000..296a7ede25 --- /dev/null +++ b/tests/components/yashima/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: yashima + name: Yashima Climate diff --git a/tests/components/zhlt01/test.esp32-ard.yaml b/tests/components/zhlt01/test.esp32-ard.yaml new file mode 100644 index 0000000000..d1dc3b4926 --- /dev/null +++ b/tests/components/zhlt01/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: zhlt01 + name: ZH/LT-01 Climate diff --git a/tests/components/zhlt01/test.esp32-c3-ard.yaml b/tests/components/zhlt01/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..d1dc3b4926 --- /dev/null +++ b/tests/components/zhlt01/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: zhlt01 + name: ZH/LT-01 Climate diff --git a/tests/components/zhlt01/test.esp32-c3-idf.yaml b/tests/components/zhlt01/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..d1dc3b4926 --- /dev/null +++ b/tests/components/zhlt01/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: zhlt01 + name: ZH/LT-01 Climate diff --git a/tests/components/zhlt01/test.esp32-idf.yaml b/tests/components/zhlt01/test.esp32-idf.yaml new file mode 100644 index 0000000000..d1dc3b4926 --- /dev/null +++ b/tests/components/zhlt01/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 2 + carrier_duty_percent: 50% + +climate: + - platform: zhlt01 + name: ZH/LT-01 Climate diff --git a/tests/components/zhlt01/test.esp8266-ard.yaml b/tests/components/zhlt01/test.esp8266-ard.yaml new file mode 100644 index 0000000000..40a00bc458 --- /dev/null +++ b/tests/components/zhlt01/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + pin: 5 + carrier_duty_percent: 50% + +climate: + - platform: zhlt01 + name: ZH/LT-01 Climate diff --git a/tests/components/zio_ultrasonic/test.esp32-ard.yaml b/tests/components/zio_ultrasonic/test.esp32-ard.yaml new file mode 100644 index 0000000000..ad4050307e --- /dev/null +++ b/tests/components/zio_ultrasonic/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_zio_ultrasonic + scl: 16 + sda: 17 + +sensor: + - platform: zio_ultrasonic + name: "Distance" + update_interval: 60s diff --git a/tests/components/zio_ultrasonic/test.esp32-c3-ard.yaml b/tests/components/zio_ultrasonic/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..36e1697a38 --- /dev/null +++ b/tests/components/zio_ultrasonic/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_zio_ultrasonic + scl: 5 + sda: 4 + +sensor: + - platform: zio_ultrasonic + name: "Distance" + update_interval: 60s diff --git a/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml b/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..36e1697a38 --- /dev/null +++ b/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_zio_ultrasonic + scl: 5 + sda: 4 + +sensor: + - platform: zio_ultrasonic + name: "Distance" + update_interval: 60s diff --git a/tests/components/zio_ultrasonic/test.esp32-idf.yaml b/tests/components/zio_ultrasonic/test.esp32-idf.yaml new file mode 100644 index 0000000000..ad4050307e --- /dev/null +++ b/tests/components/zio_ultrasonic/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_zio_ultrasonic + scl: 16 + sda: 17 + +sensor: + - platform: zio_ultrasonic + name: "Distance" + update_interval: 60s diff --git a/tests/components/zio_ultrasonic/test.esp8266-ard.yaml b/tests/components/zio_ultrasonic/test.esp8266-ard.yaml new file mode 100644 index 0000000000..36e1697a38 --- /dev/null +++ b/tests/components/zio_ultrasonic/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_zio_ultrasonic + scl: 5 + sda: 4 + +sensor: + - platform: zio_ultrasonic + name: "Distance" + update_interval: 60s diff --git a/tests/components/zio_ultrasonic/test.rp2040-ard.yaml b/tests/components/zio_ultrasonic/test.rp2040-ard.yaml new file mode 100644 index 0000000000..36e1697a38 --- /dev/null +++ b/tests/components/zio_ultrasonic/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_zio_ultrasonic + scl: 5 + sda: 4 + +sensor: + - platform: zio_ultrasonic + name: "Distance" + update_interval: 60s diff --git a/tests/components/zyaura/test.esp32-ard.yaml b/tests/components/zyaura/test.esp32-ard.yaml new file mode 100644 index 0000000000..29116a978b --- /dev/null +++ b/tests/components/zyaura/test.esp32-ard.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: zyaura + clock_pin: 16 + data_pin: 17 + co2: + name: ZyAura CO2 + temperature: + name: ZyAura Temperature + humidity: + name: ZyAura Humidity diff --git a/tests/components/zyaura/test.esp32-c3-ard.yaml b/tests/components/zyaura/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..90205c468c --- /dev/null +++ b/tests/components/zyaura/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: zyaura + clock_pin: 5 + data_pin: 4 + co2: + name: ZyAura CO2 + temperature: + name: ZyAura Temperature + humidity: + name: ZyAura Humidity diff --git a/tests/components/zyaura/test.esp32-c3-idf.yaml b/tests/components/zyaura/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..90205c468c --- /dev/null +++ b/tests/components/zyaura/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: zyaura + clock_pin: 5 + data_pin: 4 + co2: + name: ZyAura CO2 + temperature: + name: ZyAura Temperature + humidity: + name: ZyAura Humidity diff --git a/tests/components/zyaura/test.esp32-idf.yaml b/tests/components/zyaura/test.esp32-idf.yaml new file mode 100644 index 0000000000..29116a978b --- /dev/null +++ b/tests/components/zyaura/test.esp32-idf.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: zyaura + clock_pin: 16 + data_pin: 17 + co2: + name: ZyAura CO2 + temperature: + name: ZyAura Temperature + humidity: + name: ZyAura Humidity diff --git a/tests/components/zyaura/test.esp8266-ard.yaml b/tests/components/zyaura/test.esp8266-ard.yaml new file mode 100644 index 0000000000..90205c468c --- /dev/null +++ b/tests/components/zyaura/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: zyaura + clock_pin: 5 + data_pin: 4 + co2: + name: ZyAura CO2 + temperature: + name: ZyAura Temperature + humidity: + name: ZyAura Humidity diff --git a/tests/components/zyaura/test.rp2040-ard.yaml b/tests/components/zyaura/test.rp2040-ard.yaml new file mode 100644 index 0000000000..90205c468c --- /dev/null +++ b/tests/components/zyaura/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: zyaura + clock_pin: 5 + data_pin: 4 + co2: + name: ZyAura CO2 + temperature: + name: ZyAura Temperature + humidity: + name: ZyAura Humidity diff --git a/tests/dashboard/__init__.py b/tests/dashboard/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dashboard/common.py b/tests/dashboard/common.py new file mode 100644 index 0000000000..f84c03aad8 --- /dev/null +++ b/tests/dashboard/common.py @@ -0,0 +1,6 @@ +import pathlib + + +def get_fixture_path(filename: str) -> pathlib.Path: + """Get path of fixture.""" + return pathlib.Path(__file__).parent.joinpath("fixtures", filename) diff --git a/tests/dashboard/fixtures/conf/pico.yaml b/tests/dashboard/fixtures/conf/pico.yaml new file mode 100644 index 0000000000..cf5b5b75bf --- /dev/null +++ b/tests/dashboard/fixtures/conf/pico.yaml @@ -0,0 +1,47 @@ +substitutions: + name: picoproxy + friendly_name: Pico Proxy + +esphome: + name: ${name} + friendly_name: ${friendly_name} + project: + name: esphome.bluetooth-proxy + version: "1.0" + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ap: + +api: +logger: +ota: +improv_serial: + +dashboard_import: + package_import_url: github://esphome/firmware/bluetooth-proxy/esp32-generic.yaml@main + +button: + - platform: factory_reset + id: resetf + - platform: safe_mode + name: Safe Mode Boot + entity_category: diagnostic + +sensor: + - platform: template + id: pm11 + name: "pm 1.0µm" + lambda: return 1.0; + - platform: template + id: pm251 + name: "pm 2.5µm" + lambda: return 2.5; + - platform: template + id: pm101 + name: "pm 10µm" + lambda: return 10; diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py new file mode 100644 index 0000000000..a61850abf3 --- /dev/null +++ b/tests/dashboard/test_web_server.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import asyncio +import json +import os +from unittest.mock import Mock + +import pytest +import pytest_asyncio +from tornado.httpclient import AsyncHTTPClient, HTTPResponse +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from tornado.testing import bind_unused_port + +from esphome.dashboard import web_server +from esphome.dashboard.core import DASHBOARD + +from .common import get_fixture_path + + +class DashboardTestHelper: + def __init__(self, io_loop: IOLoop, client: AsyncHTTPClient, port: int) -> None: + self.io_loop = io_loop + self.client = client + self.port = port + + async def fetch(self, path: str, **kwargs) -> HTTPResponse: + """Get a response for the given path.""" + if path.lower().startswith(("http://", "https://")): + url = path + else: + url = f"http://127.0.0.1:{self.port}{path}" + future = self.client.fetch(url, raise_error=True, **kwargs) + result = await future + return result + + +@pytest_asyncio.fixture() +async def dashboard() -> DashboardTestHelper: + sock, port = bind_unused_port() + args = Mock( + ha_addon=True, + configuration=get_fixture_path("conf"), + port=port, + ) + DASHBOARD.settings.parse_args(args) + app = web_server.make_app() + http_server = HTTPServer(app) + http_server.add_sockets([sock]) + await DASHBOARD.async_setup() + os.environ["DISABLE_HA_AUTHENTICATION"] = "1" + assert DASHBOARD.settings.using_password is False + assert DASHBOARD.settings.on_ha_addon is True + assert DASHBOARD.settings.using_auth is False + task = asyncio.create_task(DASHBOARD.async_run()) + client = AsyncHTTPClient() + io_loop = IOLoop(make_current=False) + yield DashboardTestHelper(io_loop, client, port) + task.cancel() + sock.close() + client.close() + io_loop.close() + + +@pytest.mark.asyncio +async def test_main_page(dashboard: DashboardTestHelper) -> None: + response = await dashboard.fetch("/") + assert response.code == 200 + + +@pytest.mark.asyncio +async def test_devices_page(dashboard: DashboardTestHelper) -> None: + response = await dashboard.fetch("/devices") + assert response.code == 200 + assert response.headers["content-type"] == "application/json" + json_data = json.loads(response.body.decode()) + configured_devices = json_data["configured"] + first_device = configured_devices[0] + assert first_device["name"] == "pico" + assert first_device["configuration"] == "pico.yaml" diff --git a/tests/dashboard/util/__init__.py b/tests/dashboard/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dashboard/util/test_file.py b/tests/dashboard/util/test_file.py new file mode 100644 index 0000000000..51ba10b328 --- /dev/null +++ b/tests/dashboard/util/test_file.py @@ -0,0 +1,56 @@ +import os +from pathlib import Path +from unittest.mock import patch + +import py +import pytest + +from esphome.dashboard.util.file import write_file, write_utf8_file + + +def test_write_utf8_file(tmp_path: Path) -> None: + write_utf8_file(tmp_path.joinpath("foo.txt"), "foo") + assert tmp_path.joinpath("foo.txt").read_text() == "foo" + + with pytest.raises(OSError): + write_utf8_file(Path("/dev/not-writable"), "bar") + + +def test_write_file(tmp_path: Path) -> None: + write_file(tmp_path.joinpath("foo.txt"), b"foo") + assert tmp_path.joinpath("foo.txt").read_text() == "foo" + + +def test_write_utf8_file_fails_at_rename( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test that if rename fails not not remove, we do not log the failed cleanup.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with ( + pytest.raises(OSError), + patch("esphome.dashboard.util.file.os.replace", side_effect=OSError), + ): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert not os.path.exists(test_file) + + assert "File replacement cleanup failed" not in caplog.text + + +def test_write_utf8_file_fails_at_rename_and_remove( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test that if rename and remove both fail, we log the failed cleanup.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with ( + pytest.raises(OSError), + patch("esphome.dashboard.util.file.os.remove", side_effect=OSError), + patch("esphome.dashboard.util.file.os.replace", side_effect=OSError), + ): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert "File replacement cleanup failed" in caplog.text diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 236b9f5fc2..3ba4c8bd07 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -5,14 +5,14 @@ #include #include -#include +#include #include #include using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false); + App.pre_setup("livingroom", "LivingRoom", "LivingRoomArea", "comment", __DATE__ ", " __TIME__, false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); @@ -25,7 +25,7 @@ void setup() { ap.set_password("password1"); wifi->add_sta(ap); - auto *ota = new ota::OTAComponent(); // NOLINT + auto *ota = new esphome::ESPHomeOTAComponent(); // NOLINT ota->set_port(8266); App.setup(); diff --git a/tests/test1.yaml b/tests/test1.yaml deleted file mode 100644 index bee0d93faf..0000000000 --- a/tests/test1.yaml +++ /dev/null @@ -1,3373 +0,0 @@ ---- -substitutions: - devicename: test1 - sensorname: my - textname: template - roomname: living_room - -esphome: - name: test1 - name_add_mac_suffix: true - platform: ESP32 - board: nodemcu-32s - platformio_options: - board_build.partitions: huge_app.csv - on_boot: - priority: 150.0 - then: - - lambda: >- - ESP_LOGD("main", "ON BOOT!"); - on_shutdown: - then: - - lambda: >- - ESP_LOGD("main", "ON SHUTDOWN!"); - on_loop: - then: - - lambda: >- - ESP_LOGV("main", "ON LOOP!"); - - light.addressable_set: - id: addr1 - range_from: 0 - range_to: 100 - red: 100% - green: !lambda "return 255;" - blue: 0% - white: 100% - - http_request.get: - url: https://esphome.io - headers: - Content-Type: application/json - verify_ssl: false - - http_request.post: - url: https://esphome.io - verify_ssl: false - json: - key: !lambda |- - return id(${textname}_text).state; - greeting: Hello World - - http_request.send: - method: PUT - url: https://esphome.io - headers: - Content-Type: application/json - body: Some data - verify_ssl: false - on_response: - then: - - logger.log: - format: "Response status: %d" - args: - - status_code - build_path: build/test1 - -packages: - wifi: !include test_packages/test_packages_package_wifi.yaml - pkg_test: !include test_packages/test_packages_package1.yaml - -wifi: - networks: - - ssid: "MySSID" - password: "password1" - - ssid: "MySSID2" - password: "" - channel: 14 - bssid: "A1:63:95:47:D3:1D" - manual_ip: - static_ip: 192.168.178.230 - gateway: 192.168.178.1 - subnet: 255.255.255.0 - dns1: 1.1.1.1 - dns2: 1.2.2.1 - domain: .local - reboot_timeout: 120s - power_save_mode: light - -mdns: - disabled: false - -http_request: - useragent: esphome/device - timeout: 10s - -mqtt: - broker: "192.168.178.84" - port: 1883 - username: "debug" - password: "debug" - client_id: someclient - use_abbreviations: false - discovery: true - discovery_retain: false - discovery_prefix: discovery - discovery_unique_id_generator: legacy - topic_prefix: helloworld - log_topic: - topic: helloworld/hi - level: INFO - birth_message: - will_message: - shutdown_message: - topic: topic/to/send/to - payload: hi - qos: 2 - retain: true - keepalive: 60s - reboot_timeout: 60s - on_message: - - topic: my/custom/topic - qos: 0 - then: - - lambda: >- - ESP_LOGD("main", "Got message %s", x.c_str()); - - topic: livingroom/ota_mode - then: - - deep_sleep.prevent - - deep_sleep.allow - - topic: livingroom/ota_mode - then: - - deep_sleep.enter: - on_json_message: - topic: the/topic - then: - - if: - condition: - - wifi.connected: - - mqtt.connected: - - light.is_on: kitchen - - light.is_off: kitchen - - fan.is_on: fan_speed - - fan.is_off: fan_speed - then: - - lambda: |- - int data = x["my_data"]; - ESP_LOGD("main", "The data is: %d", data); - - light.turn_on: - id: ${roomname}_lights - brightness: !lambda |- - float brightness = 1.0; - if (x.containsKey("brightness")) - brightness = x["brightness"]; - return brightness; - effect: !lambda |- - const char *effect = "None"; - if (x.containsKey("effect")) - effect = x["effect"]; - return effect; - - light.control: - id: ${roomname}_lights - # yamllint disable-line rule:line-length - brightness: !lambda "return id(${roomname}_lights).current_values.get_brightness() + 0.5;" - - light.dim_relative: - id: ${roomname}_lights - relative_brightness: 5% - - uart.write: - id: uart_0 - data: Hello World - - uart.write: - id: uart_0 - data: [0x00, 0x20, 0x30] - - uart.write: - id: uart_0 - data: !lambda |- - return {}; - on_connect: - - light.turn_on: ${roomname}_lights - - mqtt.publish: - topic: some/topic - payload: Hello - on_disconnect: - - light.turn_off: ${roomname}_lights - -i2c: - sda: 21 - scl: 22 - scan: true - frequency: 100kHz - setup_priority: -100 - id: i2c_bus - -spi: - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 - -uart: - - tx_pin: - number: GPIO22 - inverted: true - rx_pin: - number: GPIO23 - inverted: true - baud_rate: 115200 - id: uart_0 - parity: NONE - data_bits: 8 - stop_bits: 1 - rx_buffer_size: 512 - debug: - dummy_receiver: true - direction: both - after: - bytes: 50 - timeout: 500ms - delimiter: "\r\n" - sequence: - - lambda: UARTDebug::log_hex(direction, bytes, ':'); - - lambda: UARTDebug::log_string(direction, bytes); - - lambda: UARTDebug::log_int(direction, bytes, ','); - - lambda: UARTDebug::log_binary(direction, bytes, ';'); - - - id: adalight_uart - tx_pin: GPIO25 - rx_pin: GPIO26 - baud_rate: 115200 - rx_buffer_size: 1024 - - id: ld2410_uart - tx_pin: 18 - rx_pin: 23 - baud_rate: 256000 - parity: NONE - stop_bits: 1 - -ota: - safe_mode: true - password: "superlongpasswordthatnoonewillknow" - port: 3286 - reboot_timeout: 2min - num_attempts: 5 - on_state_change: - then: - lambda: >- - ESP_LOGD("ota", "State %d", state); - on_begin: - then: - logger.log: OTA begin - on_progress: - then: - lambda: >- - ESP_LOGD("ota", "Got progress %f", x); - on_end: - then: - logger.log: OTA end - on_error: - then: - lambda: >- - ESP_LOGD("ota", "Got error code %d", x); - -logger: - baud_rate: 0 - level: VERBOSE - logs: - mqtt.component: DEBUG - mqtt.client: ERROR - -web_server: - port: 8080 - version: 2 - -power_supply: - id: atx_power_supply - enable_time: 20ms - keep_on_time: 10s - pin: - number: 13 - inverted: true - -deep_sleep: - run_duration: 20s - sleep_duration: 50s - wakeup_pin: GPIO2 - wakeup_pin_mode: INVERT_WAKEUP - -ads1115: - address: 0x48 - i2c_id: i2c_bus - -dallas: - pin: GPIO23 - -as3935_spi: - cs_pin: GPIO12 - irq_pin: GPIO13 - -wled: - -adalight: - -esp32_ble: - io_capability: keyboard_only - -esp32_ble_tracker: - -ble_client: - - mac_address: AA:BB:CC:DD:EE:FF - id: ble_foo - - mac_address: 11:22:33:44:55:66 - id: ble_blah - on_connect: - then: - - switch.turn_on: ble1_status - on_disconnect: - then: - - switch.turn_on: ble1_status - on_passkey_request: - then: - - ble_client.passkey_reply: - id: ble_blah - passkey: 123456 - on_passkey_notification: - then: - - logger.log: "Passkey notification received" - on_numeric_comparison_request: - then: - - ble_client.numeric_comparison_reply: - id: ble_blah - accept: True - - mac_address: C4:4F:33:11:22:33 - id: my_bedjet_ble_client -bedjet: - - ble_client_id: my_bedjet_ble_client - id: my_bedjet_client - time_id: sntp_time -mcp23s08: - - id: mcp23s08_hub - cs_pin: GPIO12 - deviceaddress: 0 - -mcp23s17: - - id: mcp23s17_hub - cs_pin: GPIO12 - deviceaddress: 1 - -sensor: - - platform: internal_temperature - name: Internal Temperature - - platform: ble_client - type: characteristic - ble_client_id: ble_foo - name: Green iTag btn - service_uuid: ffe0 - characteristic_uuid: ffe1 - descriptor_uuid: ffe2 - notify: true - update_interval: never - lambda: |- - ESP_LOGD("main", "Length of data is %i", x.size()); - return x[0]; - on_notify: - then: - - lambda: |- - ESP_LOGD("green_btn", "Button was pressed, val%f", x); - - platform: ble_client - type: rssi - ble_client_id: ble_foo - name: Green iTag RSSI - update_interval: 15s - - platform: adc - pin: A0 - name: Living Room Brightness - update_interval: "1:01" - attenuation: 2.5db - unit_of_measurement: "°C" - icon: "mdi:water-percent" - accuracy_decimals: 5 - expire_after: 120s - setup_priority: -100 - force_update: true - filters: - - offset: 2.0 - - multiply: 1.2 - - calibrate_linear: - - 0.0 -> 0.0 - - 40.0 -> 45.0 - - 100.0 -> 102.5 - - filter_out: 42.0 - - filter_out: nan - - median: - window_size: 5 - send_every: 5 - send_first_at: 3 - - min: - window_size: 5 - send_every: 5 - send_first_at: 3 - - max: - window_size: 5 - send_every: 5 - send_first_at: 3 - - sliding_window_moving_average: - window_size: 15 - send_every: 15 - send_first_at: 15 - - exponential_moving_average: - alpha: 0.1 - send_every: 15 - send_first_at: 15 - - throttle_average: 60s - - throttle: 1s - - heartbeat: 5s - - debounce: 0.1s - - delta: 5.0 - - delta: 1% - - or: - - throttle: 1s - - delta: 5.0 - - lambda: return x * (9.0/5.0) + 32.0; - on_value: - then: - # yamllint disable rule:line-length - - lambda: |- - ESP_LOGD("main", "Got value %f", x); - id(${sensorname}_sensor).publish_state(42.0); - ESP_LOGI("main", "Value of my sensor: %f", id(${sensorname}_sensor).state); - ESP_LOGI("main", "Raw Value of my sensor: %f", id(${sensorname}_sensor).state); - # yamllint enable rule:line-length - on_value_range: - above: 5 - below: 10 - then: - - lambda: >- - ESP_LOGD("main", "Got value range %f", x); - - wait_until: wifi.connected - - wait_until: - condition: - binary_sensor.is_on: binary_sensor1 - timeout: 1s - on_raw_value: - - lambda: >- - ESP_LOGD("main", "Got raw value %f", x); - - logger.log: - level: DEBUG - format: Got raw value %f - args: ["x"] - - logger.log: Got raw value NAN - - mqtt.publish: - topic: some/topic - payload: Hello - qos: 2 - retain: true - - platform: esp32_hall - name: ESP32 Hall Sensor - - platform: ads1115 - multiplexer: A0_A1 - gain: 1.024 - id: ${sensorname}_sensor - filters: - state_topic: hi/me - retain: false - availability: - - platform: as7341 - update_interval: 15s - gain: X8 - atime: 120 - astep: 99 - f1: - name: F1 - f2: - name: F2 - f3: - name: F3 - f4: - name: F4 - f5: - name: F5 - f6: - name: F6 - f7: - name: F7 - f8: - name: F8 - clear: - name: Clear - nir: - name: NIR - i2c_id: i2c_bus - - platform: atm90e32 - cs_pin: 5 - phase_a: - voltage: - name: EMON Line Voltage A - current: - name: EMON CT1 Current - power: - name: EMON Active Power CT1 - reactive_power: - name: EMON Reactive Power CT1 - power_factor: - name: EMON Power Factor CT1 - gain_voltage: 7305 - gain_ct: 27961 - phase_b: - current: - name: EMON CT2 Current - power: - name: EMON Active Power CT2 - reactive_power: - name: EMON Reactive Power CT2 - power_factor: - name: EMON Power Factor CT2 - gain_voltage: 7305 - gain_ct: 27961 - phase_c: - current: - name: EMON CT3 Current - power: - name: EMON Active Power CT3 - reactive_power: - name: EMON Reactive Power CT3 - power_factor: - name: EMON Power Factor CT3 - gain_voltage: 7305 - gain_ct: 27961 - frequency: - name: EMON Line Frequency - chip_temperature: - name: EMON Chip Temp A - line_frequency: 60Hz - current_phases: 3 - gain_pga: 2X - - platform: bh1750 - name: Living Room Brightness 3 - internal: true - address: 0x23 - update_interval: 30s - retain: false - availability: - state_topic: livingroom/custom_state_topic - i2c_id: i2c_bus - - platform: max44009 - name: Outside Brightness 1 - internal: true - address: 0x4A - update_interval: 30s - mode: low_power - i2c_id: i2c_bus - - platform: bme280 - temperature: - name: Outside Temperature - oversampling: 16x - pressure: - name: Outside Pressure - oversampling: none - humidity: - name: Outside Humidity - oversampling: 8x - address: 0x77 - iir_filter: 16x - update_interval: 15s - i2c_id: i2c_bus - - platform: bme680 - temperature: - name: Outside Temperature - oversampling: 16x - pressure: - name: Outside Pressure - humidity: - name: Outside Humidity - gas_resistance: - name: Outside Gas Sensor - address: 0x77 - heater: - temperature: 320 - duration: 150ms - update_interval: 15s - i2c_id: i2c_bus - - platform: bmp085 - temperature: - name: Outside Temperature - pressure: - name: Outside Pressure - filters: - - lambda: >- - return x / powf(1.0 - (x / 44330.0), 5.255); - update_interval: 15s - i2c_id: i2c_bus - - platform: bmp280 - temperature: - name: Outside Temperature - oversampling: 16x - pressure: - name: Outside Pressure - address: 0x77 - update_interval: 15s - iir_filter: 16x - i2c_id: i2c_bus - - platform: dallas - address: 0x1C0000031EDD2A28 - name: Living Room Temperature - resolution: 9 - - platform: dallas - index: 1 - name: Living Room Temperature 2 - - platform: dht - pin: GPIO26 - temperature: - id: dht_temperature - name: Living Room Temperature 3 - humidity: - id: dht_humidity - name: Living Room Humidity 3 - model: AM2302 - update_interval: 15s - - platform: dht12 - temperature: - name: Living Room Temperature 4 - humidity: - name: Living Room Humidity 4 - update_interval: 15s - i2c_id: i2c_bus - - platform: duty_cycle - pin: GPIO25 - name: Duty Cycle Sensor - - platform: ee895 - co2: - name: Office CO2 1 - temperature: - name: Office Temperature 1 - pressure: - name: Office Pressure 1 - address: 0x5F - i2c_id: i2c_bus - - platform: esp32_hall - name: ESP32 Hall Sensor - update_interval: 15s - - platform: ens210 - temperature: - name: Living Room Temperature 5 - humidity: - name: Living Room Humidity 5 - update_interval: 15s - i2c_id: i2c_bus - - platform: hdc1080 - temperature: - name: Living Room Temperature 6 - humidity: - name: Living Room Humidity 5 - update_interval: 15s - i2c_id: i2c_bus - - platform: hlw8012 - sel_pin: 5 - cf_pin: 14 - cf1_pin: 13 - current: - name: HLW8012 Current - voltage: - name: HLW8012 Voltage - power: - name: HLW8012 Power - id: hlw8012_power - energy: - name: HLW8012 Energy - id: hlw8012_energy - update_interval: 15s - current_resistor: 0.001 ohm - voltage_divider: 2351 - change_mode_every: 16 - initial_mode: VOLTAGE - model: hlw8012 - - platform: total_daily_energy - power_id: hlw8012_power - name: HLW8012 Total Daily Energy - - platform: integration - sensor: hlw8012_power - name: Integration Sensor - time_unit: s - - platform: integration - sensor: hlw8012_power - name: Integration Sensor lazy - time_unit: s - - platform: hmc5883l - address: 0x68 - field_strength_x: - name: HMC5883L Field Strength X - field_strength_y: - name: HMC5883L Field Strength Y - field_strength_z: - name: HMC5883L Field Strength Z - heading: - name: HMC5883L Heading - range: 130uT - oversampling: 8x - update_interval: 15s - i2c_id: i2c_bus - - platform: honeywellabp - pressure: - name: Honeywell pressure - min_pressure: 0 - max_pressure: 15 - temperature: - name: Honeywell temperature - cs_pin: GPIO5 - - platform: hte501 - temperature: - name: Office Temperature 2 - humidity: - name: Office Humidity 1 - address: 0x40 - i2c_id: i2c_bus - - platform: qmc5883l - address: 0x0D - field_strength_x: - name: QMC5883L Field Strength X - field_strength_y: - name: QMC5883L Field Strength Y - field_strength_z: - name: QMC5883L Field Strength Z - heading: - name: QMC5883L Heading - range: 800uT - oversampling: 256x - update_interval: 15s - i2c_id: i2c_bus - - platform: hx711 - name: HX711 Value - dout_pin: GPIO23 - clk_pin: GPIO25 - gain: 128 - update_interval: 15s - - platform: ina219 - address: 0x40 - shunt_resistance: 0.1 ohm - current: - name: INA219 Current - power: - name: INA219 Power - bus_voltage: - name: INA219 Bus Voltage - shunt_voltage: - name: INA219 Shunt Voltage - max_voltage: 32.0V - max_current: 3.2A - update_interval: 15s - i2c_id: i2c_bus - - platform: ina226 - address: 0x40 - shunt_resistance: 0.1 ohm - current: - name: INA226 Current - power: - name: INA226 Power - bus_voltage: - name: INA226 Bus Voltage - shunt_voltage: - name: INA226 Shunt Voltage - max_current: 3.2A - update_interval: 15s - i2c_id: i2c_bus - - platform: ina3221 - address: 0x40 - channel_1: - shunt_resistance: 0.1 ohm - current: - name: INA3221 Channel 1 Current - power: - name: INA3221 Channel 1 Power - bus_voltage: - name: INA3221 Channel 1 Bus Voltage - shunt_voltage: - name: INA3221 Channel 1 Shunt Voltage - update_interval: 15s - i2c_id: i2c_bus - - platform: kalman_combinator - name: Kalman-filtered temperature - process_std_dev: 0.00139 - sources: - - source: scd30_temperature - error: !lambda |- - return 0.4 + std::abs(x - 25) * 0.023; - - source: scd4x_temperature - error: 1.5 - - platform: htu21d - temperature: - name: Living Room Temperature 6 - humidity: - name: Living Room Humidity 6 - update_interval: 15s - i2c_id: i2c_bus - - platform: max6675 - name: Living Room Temperature - cs_pin: GPIO23 - update_interval: 15s - - platform: max31855 - name: Den Temperature - cs_pin: GPIO23 - update_interval: 15s - reference_temperature: - name: MAX31855 Internal Temperature - - platform: max31856 - name: BBQ Temperature - cs_pin: GPIO17 - update_interval: 15s - mains_filter: 50Hz - - platform: max31865 - name: Water Tank Temperature - cs_pin: GPIO23 - update_interval: 15s - reference_resistance: 430 Ω - rtd_nominal_resistance: 100 Ω - - platform: mhz19 - uart_id: uart_0 - co2: - name: MH-Z19 CO2 Value - temperature: - name: MH-Z19 Temperature - update_interval: 15s - automatic_baseline_calibration: false - - platform: mpu6050 - address: 0x68 - accel_x: - name: MPU6050 Accel X - accel_y: - name: MPU6050 Accel Y - accel_z: - name: MPU6050 Accel z - gyro_x: - name: MPU6050 Gyro X - gyro_y: - name: MPU6050 Gyro Y - gyro_z: - name: MPU6050 Gyro z - temperature: - name: MPU6050 Temperature - i2c_id: i2c_bus - - platform: mpu6886 - address: 0x68 - accel_x: - name: MPU6886 Accel X - accel_y: - name: MPU6886 Accel Y - accel_z: - name: MPU6886 Accel z - gyro_x: - name: MPU6886 Gyro X - gyro_y: - name: MPU6886 Gyro Y - gyro_z: - name: MPU6886 Gyro z - temperature: - name: MPU6886 Temperature - i2c_id: i2c_bus - - platform: mmc5603 - address: 0x30 - field_strength_x: - name: HMC5883L Field Strength X - field_strength_y: - name: HMC5883L Field Strength Y - field_strength_z: - name: HMC5883L Field Strength Z - i2c_id: i2c_bus - - platform: dps310 - temperature: - name: DPS310 Temperature - pressure: - name: DPS310 Pressure - address: 0x77 - update_interval: 15s - i2c_id: i2c_bus - - platform: ms5611 - temperature: - name: Outside Temperature - pressure: - name: Outside Pressure - address: 0x77 - update_interval: 15s - i2c_id: i2c_bus - - platform: pmsa003i - pm_1_0: - name: PMSA003i PM1.0 - pm_2_5: - name: PMSA003i PM2.5 - pm_10_0: - name: PMSA003i PM10.0 - pmc_0_3: - name: PMSA003i PMC <0.3µm - pmc_0_5: - name: PMSA003i PMC <0.5µm - pmc_1_0: - name: PMSA003i PMC <1µm - pmc_2_5: - name: PMSA003i PMC <2.5µm - pmc_5_0: - name: PMSA003i PMC <5µm - pmc_10_0: - name: PMSA003i PMC <10µm - address: 0x12 - standard_units: true - i2c_id: i2c_bus - - platform: pulse_counter - name: Pulse Counter - pin: GPIO12 - count_mode: - rising_edge: INCREMENT - falling_edge: DECREMENT - internal_filter: 13us - update_interval: 15s - - platform: pulse_meter - name: Pulse Meter - id: pulse_meter_sensor - pin: GPIO12 - internal_filter: 100ms - timeout: 2 min - on_value: - - pulse_meter.set_total_pulses: - id: pulse_meter_sensor - value: 12345 - total: - name: Pulse Meter Total - - platform: qmp6988 - temperature: - name: Living Temperature QMP - oversampling: 32x - pressure: - name: Living Pressure QMP - oversampling: 2x - address: 0x70 - update_interval: 30s - iir_filter: 16x - i2c_id: i2c_bus - - platform: rotary_encoder - name: Rotary Encoder - id: rotary_encoder1 - pin_a: GPIO23 - pin_b: GPIO25 - pin_reset: GPIO25 - filters: - - or: - - debounce: 0.1s - - delta: 10 - resolution: 4 - min_value: -10 - max_value: 30 - on_value: - - sensor.rotary_encoder.set_value: - id: rotary_encoder1 - value: 10 - - sensor.rotary_encoder.set_value: - id: rotary_encoder1 - value: !lambda "return -1;" - on_clockwise: - - logger.log: Clockwise - - display_menu.down: - on_anticlockwise: - - logger.log: Anticlockwise - - display_menu.up: - - platform: pulse_width - name: Pulse Width - pin: GPIO12 - - platform: sm300d2 - uart_id: uart_0 - co2: - name: SM300D2 CO2 Value - formaldehyde: - name: SM300D2 Formaldehyde Value - tvoc: - name: SM300D2 TVOC Value - pm_2_5: - name: SM300D2 PM2.5 Value - pm_10_0: - name: SM300D2 PM10 Value - temperature: - name: SM300D2 Temperature Value - humidity: - name: SM300D2 Humidity Value - update_interval: 60s - - platform: sht3xd - temperature: - name: Living Room Temperature 8 - humidity: - name: Living Room Humidity 8 - address: 0x44 - i2c_id: i2c_bus - update_interval: 15s - - platform: sts3x - name: Living Room Temperature 9 - address: 0x4A - i2c_id: i2c_bus - - platform: scd30 - co2: - name: Living Room CO2 9 - temperature: - id: scd30_temperature - name: Living Room Temperature 9 - humidity: - name: Living Room Humidity 9 - address: 0x61 - update_interval: 15s - automatic_self_calibration: true - altitude_compensation: 10m - ambient_pressure_compensation: 961mBar - temperature_offset: 4.2C - i2c_id: i2c_bus - - platform: scd4x - id: scd40 - co2: - name: SCD4X CO2 - temperature: - id: scd4x_temperature - name: SCD4X Temperature - humidity: - name: SCD4X Humidity - update_interval: 15s - automatic_self_calibration: true - altitude_compensation: 10m - ambient_pressure_compensation: 961mBar - temperature_offset: 4.2C - i2c_id: i2c_bus - - platform: sgp30 - eco2: - name: Workshop eCO2 - accuracy_decimals: 1 - tvoc: - name: Workshop TVOC - accuracy_decimals: 1 - address: 0x58 - update_interval: 5s - i2c_id: i2c_bus - - platform: sps30 - pm_1_0: - name: Workshop PM <1µm Weight concentration - id: workshop_PM_1_0 - pm_2_5: - name: Workshop PM <2.5µm Weight concentration - id: workshop_PM_2_5 - pm_4_0: - name: Workshop PM <4µm Weight concentration - id: workshop_PM_4_0 - pm_10_0: - name: Workshop PM <10µm Weight concentration - id: workshop_PM_10_0 - pmc_0_5: - name: Workshop PM <0.5µm Number concentration - id: workshop_PMC_0_5 - pmc_1_0: - name: Workshop PM <1µm Number concentration - id: workshop_PMC_1_0 - pmc_2_5: - name: Workshop PM <2.5µm Number concentration - id: workshop_PMC_2_5 - pmc_4_0: - name: Workshop PM <4µm Number concentration - id: workshop_PMC_4_0 - pmc_10_0: - name: Workshop PM <10µm Number concentration - id: workshop_PMC_10_0 - address: 0x69 - update_interval: 10s - i2c_id: i2c_bus - - platform: sht4x - temperature: - name: SHT4X Temperature - humidity: - name: SHT4X Humidity - address: 0x44 - update_interval: 15s - i2c_id: i2c_bus - - platform: shtcx - temperature: - name: Living Room Temperature 10 - humidity: - name: Living Room Humidity 10 - address: 0x70 - update_interval: 15s - i2c_id: i2c_bus - - platform: template - name: Template Sensor - state_class: measurement - id: template_sensor - lambda: |- - if (id(ultrasonic_sensor1).state > 1) { - return 42.0; - } else { - return {}; - } - update_interval: 15s - on_value: - - sensor.template.publish: - id: template_sensor - state: 43.0 - - sensor.template.publish: - id: template_sensor - state: !lambda "return NAN;" - - platform: tsl2561 - name: TSL2561 Ambient Light - address: 0x39 - update_interval: 15s - is_cs_package: true - integration_time: 402ms - gain: 16x - i2c_id: i2c_bus - - platform: tsl2591 - id: this_little_light_of_mine - address: 0x29 - update_interval: 15s - integration_time: 600ms - gain: high - visible: - name: tsl2591 visible - id: tsl2591_vis - unit_of_measurement: pH - infrared: - name: tsl2591 infrared - id: tsl2591_ir - full_spectrum: - name: tsl2591 full_spectrum - id: tsl2591_fs - calculated_lux: - name: tsl2591 calculated_lux - id: tsl2591_cl - i2c_id: i2c_bus - - platform: tee501 - name: Office Temperature 3 - address: 0x48 - i2c_id: i2c_bus - - platform: ultrasonic - trigger_pin: GPIO25 - echo_pin: - number: GPIO23 - inverted: true - name: Ultrasonic Sensor - timeout: 5.5m - id: ultrasonic_sensor1 - - platform: uptime - name: Uptime Sensor - - id: !extend ${devicename}_uptime_pcg - unit_of_measurement: s - - platform: wifi_signal - name: WiFi Signal Sensor - update_interval: 15s - - platform: mqtt_subscribe - name: MQTT Subscribe Sensor 1 - topic: mqtt/topic - id: the_sensor - qos: 2 - on_value: - - mqtt.publish_json: - topic: the/topic - payload: |- - root["key"] = id(the_sensor).state; - root["greeting"] = "Hello World"; - - platform: sds011 - uart_id: uart_0 - pm_2_5: - name: SDS011 PM2.5 - pm_10_0: - name: SDS011 PM10.0 - update_interval: 5min - rx_only: false - - platform: ccs811 - eco2: - name: CCS811 eCO2 - tvoc: - name: CCS811 TVOC - update_interval: 30s - baseline: 0x4242 - i2c_id: i2c_bus - - platform: tx20 - wind_speed: - name: Windspeed - wind_direction_degrees: - name: Winddirection Degrees - pin: - number: GPIO04 - mode: INPUT - - platform: zyaura - clock_pin: GPIO5 - data_pin: GPIO4 - co2: - name: ZyAura CO2 - temperature: - name: ZyAura Temperature - humidity: - name: ZyAura Humidity - - platform: as3935 - lightning_energy: - name: Lightning Energy - distance: - name: Distance Storm - - platform: tmp117 - name: TMP117 Temperature - update_interval: 5s - i2c_id: i2c_bus - - platform: hm3301 - pm_1_0: - name: PM1.0 - pm_2_5: - name: PM2.5 - pm_10_0: - name: PM10.0 - aqi: - name: AQI - calculation_type: CAQI - i2c_id: i2c_bus - - platform: teleinfo - tag_name: HCHC - name: hchc - unit_of_measurement: Wh - icon: mdi:flash - teleinfo_id: myteleinfo - - platform: mcp9808 - name: MCP9808 Temperature - update_interval: 15s - i2c_id: i2c_bus - - platform: ezo - id: ph_ezo - address: 99 - unit_of_measurement: pH - i2c_id: i2c_bus - - platform: sdp3x - name: HVAC Filter Pressure drop - id: filter_pressure - update_interval: 5s - accuracy_decimals: 3 - i2c_id: i2c_bus - - platform: cs5460a - id: cs5460a1 - current: - name: Socket current - voltage: - name: Mains voltage - power: - name: Socket power - on_value: - then: - cs5460a.restart: cs5460a1 - samples: 1600 - pga_gain: 10X - current_gain: 0.01 - voltage_gain: 0.000573 - current_hpf: true - voltage_hpf: true - phase_offset: 20 - pulse_energy: 0.01 kWh - cs_pin: - mcp23xxx: mcp23017_hub - number: 14 - - platform: max9611 - i2c_id: i2c_bus - shunt_resistance: 0.2 ohm - gain: 1X - voltage: - name: Max9611 Voltage - current: - name: Max9611 Current - power: - name: Max9611 Watts - temperature: - name: Max9611 Temp - update_interval: 1s - - platform: mlx90614 - i2c_id: i2c_bus - ambient: - name: Ambient - object: - name: Object - emissivity: 1.0 - - platform: mpl3115a2 - i2c_id: i2c_bus - temperature: - name: "MPL3115A2 Temperature" - pressure: - name: "MPL3115A2 Pressure" - update_interval: 10s - - platform: ld2410 - moving_distance: - name: "Moving distance (cm)" - still_distance: - name: "Still Distance (cm)" - moving_energy: - name: "Move Energy" - still_energy: - name: "Still Energy" - detection_distance: - name: "Distance Detection" - - platform: sen21231 - name: "Person Sensor" - i2c_id: i2c_bus - - platform: fs3000 - name: "Air Velocity" - model: 1005 - update_interval: 60s - i2c_id: i2c_bus - - platform: absolute_humidity - name: DHT Absolute Humidity - temperature: dht_temperature - humidity: dht_humidity - - platform: hyt271 - i2c_id: i2c_bus - temperature: - name: "Temperature hyt271" - id: temp_etuve - humidity: - name: "Humidity hyt271" - - platform: tmp1075 - name: "Temperature TMP1075" - update_interval: 10s - i2c_id: i2c_bus - conversion_rate: 27.5ms - alert: - limit_low: 50 - limit_high: 75 - fault_count: 1 - polarity: active_high - function: comparator - -esp32_touch: - setup_mode: false - iir_filter: 10ms - sleep_duration: 27ms - measurement_duration: 8ms - low_voltage_reference: 0.5V - high_voltage_reference: 2.7V - voltage_attenuation: 1.5V - -binary_sensor: - - platform: gpio - name: "MCP23S08 Pin #1" - pin: - mcp23xxx: mcp23s08_hub - # Use pin number 1 - number: 1 - # One of INPUT or INPUT_PULLUP - mode: INPUT_PULLUP - inverted: false - - platform: gpio - name: "MCP23S17 Pin #1" - pin: - mcp23xxx: mcp23s17_hub - # Use pin number 1 - number: 1 - # One of INPUT or INPUT_PULLUP - mode: INPUT_PULLUP - inverted: false - - platform: gpio - name: "MCP23S17 Pin #1 with interrupt" - pin: - mcp23xxx: mcp23s17_hub - # Use pin number 1 - number: 1 - # One of INPUT or INPUT_PULLUP - mode: INPUT_PULLUP - inverted: false - interrupt: FALLING - - platform: gpio - pin: GPIO9 - name: Living Room Window - device_class: window - filters: - - invert: - - delayed_on: 40ms - - delayed_off: 40ms - on_press: - then: - - lambda: >- - ESP_LOGD("main", "Pressed"); - on_release: - then: - - lambda: >- - ESP_LOGD("main", "Released"); - on_click: - - min_length: 50ms - max_length: 350ms - then: - - lambda: >- - ESP_LOGD("main", "Clicked"); - - then: - - lambda: >- - ESP_LOGD("main", "Clicked"); - on_double_click: - - min_length: 50ms - max_length: 350ms - then: - - lambda: >- - ESP_LOGD("main", "Double Clicked"); - - then: - - lambda: >- - ESP_LOGD("main", "Double Clicked"); - on_multi_click: - - timing: - - ON for at most 1s - - OFF for at most 1s - - ON for at most 1s - - OFF for at least 0.2s - then: - - logger.log: - format: Multi Clicked TWO - level: warn - - timing: - - OFF for 1s to 2s - - ON for 1s to 2s - - OFF for at least 0.5s - then: - - logger.log: - format: Multi Clicked LONG SINGLE - level: warn - - timing: - - ON for at most 1s - - OFF for at least 0.5s - then: - - logger.log: - format: Multi Clicked SINGLE - level: warn - id: binary_sensor1 - - platform: gpio - pin: - number: GPIO9 - mode: INPUT_PULLUP - name: Living Room Window 2 - - platform: gpio - pin: - number: GPIO9 - mode: INPUT_OUTPUT_OPEN_DRAIN - name: Living Room Button - - platform: status - name: Living Room Status - - platform: esp32_touch - name: ESP32 Touch Pad GPIO27 - pin: GPIO27 - threshold: 1000 - id: btn_left - on_press: - - if: - condition: - display_menu.is_active: - then: - - display_menu.enter: - else: - - display_menu.left: - - display_menu.right: - - display_menu.show: - - platform: template - name: Garage Door Open - id: garage_door - lambda: |- - if (isnan(id(${sensorname}_sensor).state)) { - // isnan checks if the ultrasonic sensor echo - // has timed out, resulting in a NaN (not a number) state - // in that case, return {} to indicate that we don't know. - return {}; - } else if (id(${sensorname}_sensor).state > 30) { - // Garage Door is open. - return true; - } else { - // Garage Door is closed. - return false; - } - on_press: - - binary_sensor.template.publish: - id: garage_door - state: false - - output.ledc.set_frequency: - id: gpio_19 - frequency: 500.0Hz - - output.ledc.set_frequency: - id: gpio_19 - frequency: !lambda "return 500.0;" - - platform: pn532 - pn532_id: pn532_bs - uid: 74-10-37-94 - name: PN532 NFC Tag - - platform: rdm6300 - uid: 7616525 - name: RDM6300 NFC Tag - - platform: gpio - name: PCF binary sensor - pin: - pcf8574: pcf8574_hub - number: 1 - mode: INPUT - inverted: true - - platform: gpio - name: PCA9554 binary sensor - pin: - pca9554: pca9554_hub - number: 1 - mode: INPUT - inverted: true - - platform: gpio - name: PCA6416A binary sensor - pin: - pca6416a: pca6416a_hub - number: 15 - mode: INPUT - inverted: true - - platform: gpio - name: MCP21 binary sensor - pin: - mcp23xxx: mcp23017_hub - number: 1 - mode: INPUT - inverted: true - - platform: gpio - name: MCP22 binary sensor - pin: - mcp23xxx: mcp23008_hub - number: 7 - mode: INPUT_PULLUP - inverted: false - - platform: gpio - name: MCP23 binary sensor - pin: - mcp23016: mcp23016_hub - number: 7 - mode: INPUT - inverted: false - - - platform: remote_receiver - name: Raw Remote Receiver Test - raw: - code: - [ - 5685, - -4252, - 1711, - -2265, - 1712, - -2265, - 1711, - -2264, - 1712, - -2266, - 3700, - -2263, - 1712, - -4254, - 1711, - -4249, - 1715, - -2266, - 1710, - -2267, - 1709, - -2265, - 3704, - -4250, - 1712, - -4254, - 3700, - -2260, - 1714, - -2265, - 1712, - -2262, - 1714, - -2267, - 1709, - ] - - platform: as3935 - name: Storm Alert - - platform: analog_threshold - name: Analog Trheshold 1 - sensor_id: template_sensor - threshold: - upper: 110 - lower: 90 - filters: - - delayed_on: 0s - - delayed_off: 10s - - platform: analog_threshold - name: Analog Trheshold 2 - sensor_id: template_sensor - threshold: 100 - filters: - - invert: - - platform: template - id: open_endstop_sensor - - platform: template - id: open_sensor - - platform: template - id: open_obstacle_sensor - - - platform: template - id: close_endstop_sensor - - platform: template - id: close_sensor - - platform: template - id: close_obstacle_sensor - - platform: ld2410 - has_target: - name: presence - has_moving_target: - name: movement - has_still_target: - name: still - -pca9685: - frequency: 500 - address: 0x0 - i2c_id: i2c_bus - -tlc59208f: - - address: 0x20 - id: tlc59208f_1 - i2c_id: i2c_bus - - address: 0x22 - id: tlc59208f_2 - i2c_id: i2c_bus - - address: 0x24 - id: tlc59208f_3 - i2c_id: i2c_bus - -my9231: - data_pin: GPIO12 - clock_pin: GPIO14 - num_channels: 6 - num_chips: 2 - bit_depth: 16 - -sm2235: - data_pin: GPIO4 - clock_pin: GPIO5 - max_power_color_channels: 9 - max_power_white_channels: 9 - -sm2335: - data_pin: GPIO4 - clock_pin: GPIO5 - max_power_color_channels: 9 - max_power_white_channels: 9 - -bp1658cj: - data_pin: GPIO3 - clock_pin: GPIO5 - max_power_color_channels: 4 - max_power_white_channels: 6 - -bp5758d: - data_pin: GPIO3 - clock_pin: GPIO5 - -output: - - platform: gpio - pin: GPIO26 - id: gpio_26 - power_supply: atx_power_supply - inverted: false - - platform: ledc - pin: 19 - id: gpio_19 - frequency: 1500Hz - channel: 14 - max_power: 0.5 - - platform: pca9685 - id: pca_0 - channel: 0 - - platform: pca9685 - id: pca_1 - channel: 1 - - platform: pca9685 - id: pca_2 - channel: 2 - - platform: pca9685 - id: pca_3 - channel: 3 - - platform: pca9685 - id: pca_4 - channel: 4 - - platform: pca9685 - id: pca_5 - channel: 5 - - platform: pca9685 - id: pca_6 - channel: 6 - - platform: pca9685 - id: pca_7 - channel: 7 - - platform: tlc59208f - id: tlc_0 - channel: 0 - tlc59208f_id: tlc59208f_1 - - platform: tlc59208f - id: tlc_1 - channel: 1 - tlc59208f_id: tlc59208f_1 - - platform: tlc59208f - id: tlc_2 - channel: 2 - tlc59208f_id: tlc59208f_1 - - platform: tlc59208f - id: tlc_3 - channel: 0 - tlc59208f_id: tlc59208f_2 - - platform: tlc59208f - id: tlc_4 - channel: 1 - tlc59208f_id: tlc59208f_2 - - platform: tlc59208f - id: tlc_5 - channel: 2 - tlc59208f_id: tlc59208f_2 - - platform: tlc59208f - id: tlc_6 - channel: 0 - tlc59208f_id: tlc59208f_3 - - platform: tlc59208f - id: tlc_7 - channel: 1 - tlc59208f_id: tlc59208f_3 - - platform: tlc59208f - id: tlc_8 - channel: 2 - tlc59208f_id: tlc59208f_3 - - platform: gpio - id: id2 - pin: - pcf8574: pcf8574_hub - number: 0 - mode: OUTPUT - inverted: false - - platform: gpio - id: id26 - pin: - pca9554: pca9554_hub - number: 0 - mode: OUTPUT - inverted: false - - platform: gpio - id: id22 - pin: - mcp23xxx: mcp23017_hub - number: 0 - mode: OUTPUT - inverted: false - - platform: gpio - id: id23 - pin: - mcp23xxx: mcp23008_hub - number: 0 - mode: OUTPUT - inverted: false - - platform: gpio - id: id25 - pin: - mcp23016: mcp23016_hub - number: 0 - mode: OUTPUT - inverted: false - - platform: my9231 - id: my_0 - channel: 0 - - platform: my9231 - id: my_1 - channel: 1 - - platform: my9231 - id: my_2 - channel: 2 - - platform: my9231 - id: my_3 - channel: 3 - - platform: my9231 - id: my_4 - channel: 4 - - platform: my9231 - id: my_5 - channel: 5 - - platform: sm2235 - id: sm2235_red - channel: 1 - - platform: sm2235 - id: sm2235_green - channel: 0 - - platform: sm2235 - id: sm2235_blue - channel: 2 - - platform: sm2235 - id: sm2235_coldwhite - channel: 4 - - platform: sm2235 - id: sm2235_warmwhite - channel: 3 - - platform: sm2335 - id: sm2335_red - channel: 1 - - platform: sm2335 - id: sm2335_green - channel: 0 - - platform: sm2335 - id: sm2335_blue - channel: 2 - - platform: sm2335 - id: sm2335_coldwhite - channel: 4 - - platform: sm2335 - id: sm2335_warmwhite - channel: 3 - - platform: slow_pwm - id: id24 - pin: GPIO26 - period: 15s - - platform: ac_dimmer - id: dimmer1 - gate_pin: GPIO5 - zero_cross_pin: GPIO26 - - platform: esp32_dac - pin: GPIO25 - id: dac_output - - platform: mcp4725 - id: mcp4725_dac_output - i2c_id: i2c_bus - - platform: mcp4728 - id: mcp4728_dac_output_a - channel: A - vref: vdd - power_down: normal - - platform: mcp4728 - id: mcp4728_dac_output_b - channel: B - vref: internal - gain: X1 - power_down: gnd_1k - - platform: mcp4728 - id: mcp4728_dac_output_c - channel: C - vref: vdd - power_down: gnd_100k - - platform: mcp4728 - id: mcp4728_dac_output_d - channel: D - vref: internal - gain: X2 - power_down: gnd_500k - - platform: bp1658cj - id: bp1658cj_red - channel: 1 - - platform: bp1658cj - id: bp1658cj_green - channel: 2 - - platform: bp1658cj - id: bp1658cj_blue - channel: 0 - - platform: bp1658cj - id: bp1658cj_coldwhite - channel: 3 - - platform: bp1658cj - id: bp1658cj_warmwhite - channel: 4 - - platform: bp5758d - id: bp5758d_red - channel: 2 - current: 10 - - platform: bp5758d - id: bp5758d_green - channel: 3 - current: 10 - - platform: bp5758d - id: bp5758d_blue - channel: 1 - current: 10 - - platform: bp5758d - id: bp5758d_coldwhite - channel: 5 - current: 10 - - platform: bp5758d - id: bp5758d_warmwhite - channel: 4 - current: 10 - - platform: x9c - id: test_x9c - cs_pin: GPIO25 - inc_pin: GPIO26 - ud_pin: GPIO27 - initial_value: 0.5 - -e131: - -light: - - platform: binary - name: Desk Lamp - output: gpio_26 - effects: - - strobe: - - strobe: - name: My Strobe - colors: - - state: true - duration: 250ms - - state: false - duration: 250ms - on_turn_on: - - switch.template.publish: - id: livingroom_lights - state: true - on_turn_off: - - switch.template.publish: - id: livingroom_lights - state: true - - platform: monochromatic - name: Kitchen Lights - id: kitchen - output: gpio_19 - gamma_correct: 2.8 - default_transition_length: 2s - effects: - - strobe: - - flicker: - - flicker: - name: My Flicker - alpha: 98% - intensity: 1.5% - - lambda: - name: My Custom Effect - update_interval: 1s - lambda: |- - static int state = 0; - state += 1; - if (state == 4) - state = 0; - - platform: rgb - name: Living Room Lights - id: ${roomname}_lights - red: pca_0 - green: pca_1 - blue: pca_2 - - platform: rgbw - name: Living Room Lights 2 - red: pca_3 - green: pca_4 - blue: pca_5 - white: pca_6 - color_interlock: true - - platform: rgbww - name: Living Room Lights 2 - red: pca_3 - green: pca_4 - blue: pca_5 - cold_white: pca_6 - warm_white: pca_6 - cold_white_color_temperature: 153 mireds - warm_white_color_temperature: 500 mireds - color_interlock: true - - platform: rgbct - name: Living Room Lights 2 - red: pca_3 - green: pca_4 - blue: pca_5 - color_temperature: pca_6 - white_brightness: pca_6 - cold_white_color_temperature: 153 mireds - warm_white_color_temperature: 500 mireds - color_interlock: true - - platform: cwww - name: Living Room Lights 2 - cold_white: pca_6 - warm_white: pca_6 - cold_white_color_temperature: 153 mireds - warm_white_color_temperature: 500 mireds - constant_brightness: true - - platform: color_temperature - name: Living Room Lights 2 - color_temperature: pca_6 - brightness: pca_6 - cold_white_color_temperature: 153 mireds - warm_white_color_temperature: 500 mireds - - platform: fastled_clockless - id: addr1 - chipset: WS2811 - pin: GPIO23 - num_leds: 60 - rgb_order: BRG - max_refresh_rate: 20ms - power_supply: atx_power_supply - color_correct: [75%, 100%, 50%] - name: FastLED WS2811 Light - effects: - - addressable_color_wipe: - - addressable_color_wipe: - name: Color Wipe Effect With Custom Values - colors: - - red: 100% - green: 100% - blue: 100% - num_leds: 1 - - red: 0% - green: 0% - blue: 0% - num_leds: 1 - add_led_interval: 100ms - reverse: false - - addressable_scan: - - addressable_scan: - name: Scan Effect With Custom Values - move_interval: 100ms - - addressable_twinkle: - - addressable_twinkle: - name: Twinkle Effect With Custom Values - twinkle_probability: 5% - progress_interval: 4ms - - addressable_random_twinkle: - - addressable_random_twinkle: - name: Random Twinkle Effect With Custom Values - twinkle_probability: 5% - progress_interval: 32ms - - addressable_fireworks: - - addressable_fireworks: - name: Fireworks Effect With Custom Values - update_interval: 32ms - spark_probability: 10% - use_random_color: false - fade_out_rate: 120 - - addressable_flicker: - - addressable_flicker: - name: Flicker Effect With Custom Values - update_interval: 16ms - intensity: 5% - - addressable_lambda: - name: Test For Custom Lambda Effect - lambda: |- - if (initial_run) { - it[0] = current_color; - } - - - wled: - port: 11111 - - - adalight: - uart_id: adalight_uart - - - automation: - name: Custom Effect - sequence: - - light.addressable_set: - id: addr1 - red: 100% - green: 100% - blue: 0% - - delay: 100ms - - light.addressable_set: - id: addr1 - red: 0% - green: 100% - blue: 0% - - e131: - universe: 1 - - platform: fastled_spi - id: addr2 - chipset: WS2801 - data_pin: GPIO23 - clock_pin: GPIO22 - data_rate: 2MHz - num_leds: 60 - rgb_order: BRG - name: FastLED SPI Light - - platform: neopixelbus - id: addr3 - name: Neopixelbus Light - gamma_correct: 2.8 - color_correct: [0.0, 0.0, 0.0, 0.0] - default_transition_length: 10s - power_supply: atx_power_supply - effects: - - addressable_flicker: - name: Flicker Effect With Custom Values - update_interval: 16ms - intensity: 5% - type: GRBW - variant: SK6812 - method: ESP32_I2S_0 - num_leds: 60 - pin: GPIO23 - - platform: partition - name: Partition Light - segments: - - id: addr1 - from: 0 - to: 0 - - id: addr2 - from: 1 - to: 10 - - id: addr2 - from: 20 - to: 25 - - single_light_id: ${roomname}_lights - -remote_transmitter: - - pin: 32 - carrier_duty_percent: 100% - -climate: - - platform: tcl112 - name: TCL112 Climate With Sensor - supports_heat: true - supports_cool: true - sensor: ${sensorname}_sensor - - platform: tcl112 - name: TCL112 Climate - action_state_topic: action/state/topic - away_command_topic: away/command/topic - away_state_topic: away/state/topic - current_temperature_state_topic: current/temperature/state/topic - fan_mode_command_topic: fan_mode/mode/command/topic - fan_mode_state_topic: fan_mode/mode/state/topic - mode_command_topic: mode/command/topic - mode_state_topic: mode/state/topic - swing_mode_command_topic: swing_mode/command/topic - swing_mode_state_topic: swing_mode/state/topic - target_temperature_command_topic: target/temperature/command/topic - target_temperature_high_command_topic: target/temperature/high/command/topic - target_temperature_high_state_topic: target/temperature/high/state/topic - target_temperature_low_command_topic: target/temperature/low/command/topic - target_temperature_low_state_topic: target/temperature/low/state/topic - target_temperature_state_topic: target/temperature/state/topic - - platform: coolix - name: Coolix Climate With Sensor - supports_heat: true - supports_cool: true - sensor: ${sensorname}_sensor - - platform: coolix - name: Coolix Climate - - platform: fujitsu_general - name: Fujitsu General Climate - - platform: daikin - name: Daikin Climate - - platform: daikin_brc - name: Daikin BRC Climate - use_fahrenheit: true - - platform: delonghi - name: Delonghi Climate - - platform: yashima - name: Yashima Climate - - platform: mitsubishi - name: Mitsubishi - - platform: whirlpool - name: Whirlpool Climate - - platform: climate_ir_lg - name: LG Climate - - platform: toshiba - name: Toshiba Climate - - platform: hitachi_ac344 - name: Hitachi Climate - - platform: heatpumpir - protocol: mitsubishi_heavy_zm - horizontal_default: left - vertical_default: up - name: HeatpumpIR Climate - min_temperature: 18 - max_temperature: 30 - - platform: midea_ir - name: Midea IR - use_fahrenheit: true - - platform: midea - on_control: - logger.log: Control message received! - on_state: - logger.log: State changed! - id: midea_unit - uart_id: uart_0 - name: Midea Climate - transmitter_id: - period: 1s - num_attempts: 5 - timeout: 2s - beeper: false - autoconf: true - visual: - min_temperature: 17 °C - max_temperature: 30 °C - temperature_step: 0.5 °C - supported_modes: - - FAN_ONLY - - HEAT_COOL - - COOL - - HEAT - - DRY - custom_fan_modes: - - SILENT - - TURBO - supported_presets: - - ECO - - BOOST - - SLEEP - custom_presets: - - FREEZE_PROTECTION - supported_swing_modes: - - VERTICAL - - HORIZONTAL - - BOTH - outdoor_temperature: - name: Temp - power_usage: - name: Power - humidity_setpoint: - name: Humidity - - platform: anova - name: Anova cooker - ble_client_id: ble_blah - unit_of_measurement: c - icon: mdi:stove - - platform: bedjet - name: My Bedjet - bedjet_id: my_bedjet_client - heat_mode: extended - - platform: whynter - name: Whynter - -script: - - id: climate_custom - then: - - climate.control: - id: midea_unit - custom_preset: FREEZE_PROTECTION - custom_fan_mode: SILENT - - id: climate_preset - then: - - climate.control: - id: midea_unit - preset: SLEEP - -switch: - - platform: template - name: MIDEA_AC_BEEPER_CONTROL - optimistic: true - turn_on_action: - midea_ac.beeper_on: - turn_off_action: - midea_ac.beeper_off: - - platform: template - name: MIDEA_RAW - turn_on_action: - remote_transmitter.transmit_midea: - code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] - - platform: gpio - name: "MCP23S08 Pin #0" - pin: - mcp23xxx: mcp23s08_hub - # Use pin number 0 - number: 0 - mode: OUTPUT - inverted: false - - platform: gpio - name: "MCP23S17 Pin #0" - pin: - mcp23xxx: mcp23s17_hub - # Use pin number 0 - number: 1 - mode: OUTPUT - inverted: false - - platform: gpio - pin: GPIO25 - name: Living Room Dehumidifier - icon: "mdi:restart" - inverted: true - command_topic: custom_command_topic - command_retain: true - restore_mode: ALWAYS_OFF - - platform: template - name: JVC Off - id: living_room_lights_on - turn_on_action: - remote_transmitter.transmit_jvc: - data: 0x10EF - - platform: template - name: MagiQuest - turn_on_action: - remote_transmitter.transmit_magiquest: - wand_id: 0x01234567 - - platform: template - name: NEC - id: living_room_lights_off - turn_on_action: - remote_transmitter.transmit_nec: - address: 0x4242 - command: 0x8484 - - platform: template - name: LG - turn_on_action: - remote_transmitter.transmit_lg: - data: 4294967295 - nbits: 28 - - platform: template - name: Samsung - turn_on_action: - remote_transmitter.transmit_samsung: - data: 0xABCDEF - - platform: template - name: Samsung36 - turn_on_action: - remote_transmitter.transmit_samsung36: - address: 0x0400 - command: 0x000E00FF - - platform: template - name: ToshibaAC - turn_on_action: - - remote_transmitter.transmit_toshiba_ac: - rc_code_1: 0xB24DBF4050AF - rc_code_2: 0xD5660001003C - - platform: template - name: Sony - turn_on_action: - remote_transmitter.transmit_sony: - data: 0xABCDEF - nbits: 12 - - platform: template - name: Panasonic - turn_on_action: - remote_transmitter.transmit_panasonic: - address: 0x4004 - command: 0x1000BCD - - platform: template - name: Pioneer - turn_on_action: - - remote_transmitter.transmit_pioneer: - rc_code_1: 0xA556 - rc_code_2: 0xA506 - repeat: - times: 2 - - platform: template - name: RC Switch Raw - turn_on_action: - remote_transmitter.transmit_rc_switch_raw: - code: "00101001100111110101xxxx" - protocol: 1 - - platform: template - name: RC Switch Type A - turn_on_action: - remote_transmitter.transmit_rc_switch_type_a: - group: "11001" - device: "01000" - state: true - protocol: - pulse_length: 175 - sync: [1, 31] - zero: [1, 3] - one: [3, 1] - inverted: false - - platform: template - name: RC Switch Type B - turn_on_action: - remote_transmitter.transmit_rc_switch_type_b: - address: 4 - channel: 2 - state: true - - platform: template - name: RC Switch Type C - turn_on_action: - remote_transmitter.transmit_rc_switch_type_c: - family: "a" - group: 1 - device: 2 - state: true - - platform: template - name: RC Switch Type D - turn_on_action: - remote_transmitter.transmit_rc_switch_type_d: - group: "a" - device: 2 - state: true - - platform: template - name: RC5 - turn_on_action: - remote_transmitter.transmit_rc5: - address: 0x00 - command: 0x0B - - platform: template - name: RC5 - turn_on_action: - remote_transmitter.transmit_raw: - code: [1000, -1000] - - platform: template - name: AEHA - id: eaha_hitachi_climate_power_on - turn_on_action: - remote_transmitter.transmit_aeha: - address: 0x8008 - data: - [ - 0x00, - 0x02, - 0xFD, - 0xFF, - 0x00, - 0x33, - 0xCC, - 0x49, - 0xB6, - 0xC8, - 0x37, - 0x16, - 0xE9, - 0x00, - 0xFF, - 0x00, - 0xFF, - 0x00, - 0xFF, - 0x00, - 0xFF, - 0x00, - 0xFF, - 0xCA, - 0x35, - 0x8F, - 0x70, - 0x00, - 0xFF, - 0x00, - 0xFF, - 0x00, - 0xFF, - 0x00, - 0xFF, - ] - - platform: template - name: Living Room Lights - id: livingroom_lights - optimistic: true - assumed_state: true - turn_on_action: - - switch.turn_on: living_room_lights_on - - output.set_level: - id: gpio_19 - level: 50% - - output.set_level: - id: gpio_19 - level: !lambda "return 0.5;" - - output.set_level: - id: dac_output - level: 50% - - output.set_level: - id: dac_output - level: !lambda "return 0.5;" - - output.set_level: - id: mcp4725_dac_output - level: !lambda "return 0.5;" - - output.set_level: - id: mcp4728_dac_output_a - level: !lambda "return 0.5;" - turn_off_action: - - switch.turn_on: living_room_lights_off - restore_state: false - on_turn_on: - - switch.template.publish: - id: livingroom_lights - state: true - - platform: restart - name: Living Room Restart - - platform: safe_mode - name: Living Room Restart (Safe Mode) - - platform: factory_reset - name: Living Room Restart (Factory Default Settings) - - platform: shutdown - name: Living Room Shutdown - - platform: output - name: Generic Output - output: pca_6 - - platform: template - name: Template Switch - id: my_switch - lambda: |- - if (id(binary_sensor1).state) { - return true; - } else { - return {}; - } - id(my_switch).publish_state(false); - id(my_switch).publish_state(true); - if (id(my_switch).state) { - // Switch is ON, do something here - id(my_switch).turn_off(); - id(my_switch).turn_on(); - } else { - // Switch is OFF, do something else here - } - optimistic: true - assumed_state: false - restore_state: true - on_turn_off: - - switch.template.publish: - id: my_switch - state: !lambda "return false;" - - platform: uart - uart_id: uart_0 - name: UART String Output - data: DataToSend - - platform: uart - uart_id: uart_0 - name: UART Bytes Output - data: [0xDE, 0xAD, 0xBE, 0xEF] - - platform: uart - uart_id: uart_0 - name: UART Recurring Output - data: [0xDE, 0xAD, 0xBE, 0xEF] - send_every: 1s - - platform: template - assumed_state: true - name: Stepper Switch - turn_on_action: - - stepper.set_target: - id: my_stepper - target: !lambda |- - static int32_t i = 0; - i += 1000; - if (i > 5000) { - i = -5000; - } - return i; - - stepper.report_position: - id: my_stepper - position: 0 - - - platform: gpio - name: "SN74HC595 Pin #0" - pin: - sn74hc595: sn74hc595_hub - # Use pin number 0 - number: 0 - inverted: false - - platform: template - id: ble1_status - optimistic: true - - platform: template - id: outlet_switch - optimistic: true - device_class: outlet - -fan: - - platform: binary - output: gpio_26 - name: Living Room Fan 1 - oscillation_output: gpio_19 - direction_output: gpio_26 - - platform: speed - id: fan_speed - icon: mdi:weather-windy - output: pca_6 - speed_count: 10 - name: Living Room Fan 2 - oscillation_output: gpio_19 - direction_output: gpio_26 - oscillation_state_topic: oscillation/state/topic - oscillation_command_topic: oscillation/command/topic - speed_level_state_topic: speed_level/state/topic - speed_level_command_topic: speed_level/command/topic - speed_state_topic: speed/state/topic - speed_command_topic: speed/command/topic - on_speed_set: - then: - - logger.log: Fan speed was changed! - - platform: bedjet - name: My Bedjet fan - bedjet_id: my_bedjet_client - - platform: copy - source_id: fan_speed - name: Fan Speed Copy - -interval: - - interval: 10s - then: - - display.page.show: !lambda |- - if (true) return id(page1); else return id(page2); - - display.page.show_next: display1 - - display.page.show_previous: display1 - - interval: 2s - then: - # yamllint disable rule:line-length - - lambda: |- - static uint16_t btn_left_state = id(btn_left)->get_value(); - - ESP_LOGD("adaptive touch", "___ Touch Pad '%s' (T%u): val: %u state: %u tres:%u", id(btn_left)->get_name().c_str(), id(btn_left)->get_touch_pad(), id(btn_left)->get_value(), btn_left_state, id(btn_left)->get_threshold()); - - btn_left_state = ((uint32_t) id(btn_left)->get_value() + 63 * (uint32_t)btn_left_state) >> 6; - - id(btn_left)->set_threshold(btn_left_state * 0.9); - # yamllint enable rule:line-length - - if: - condition: - display.is_displaying_page: - id: display1 - page_id: page1 - then: - - logger.log: Seeing page 1 - -color: - - id: kbx_red - red: 100% - green_int: 123 - blue: 2% - - id: kbx_blue - red: 0% - green: 1% - blue: 100% - - id: kbx_green - hex: "3DEC55" - -display: - - platform: lcd_gpio - id: my_lcd_gpio - dimensions: 18x4 - data_pins: - - GPIO19 - - GPIO21 - - GPIO22 - - GPIO23 - enable_pin: GPIO23 - rs_pin: GPIO25 - lambda: |- - it.print("Hello World!"); - - platform: lcd_pcf8574 - dimensions: 18x4 - address: 0x3F - user_characters: - - position: 0 - data: - - 0b00000 - - 0b01010 - - 0b00000 - - 0b00100 - - 0b00100 - - 0b10001 - - 0b01110 - - 0b00000 - lambda: |- - it.print("Hello World!"); - i2c_id: i2c_bus - - platform: max7219 - cs_pin: GPIO23 - num_chips: 1 - lambda: |- - it.print("01234567"); - - platform: tm1637 - clk_pin: GPIO23 - dio_pin: GPIO25 - intensity: 3 - lambda: |- - it.print("1234"); - - platform: tm1637 - clk_pin: - mcp23xxx: mcp23017_hub - number: 1 - dio_pin: - mcp23xxx: mcp23017_hub - number: 2 - intensity: 3 - inverted: true - length: 4 - lambda: |- - it.print("1234"); - - platform: pcd8544 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - contrast: 60 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ssd1306_i2c - model: SSD1306_128X64 - reset_pin: GPIO23 - address: 0x3C - id: display1 - contrast: 60% - pages: - - id: page1 - lambda: |- - it.qr_code(0, 0, id(homepage_qr)); - it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 - lambda: |- - // Nothing - on_page_change: - from: page1 - to: page2 - then: - lambda: |- - ESP_LOGD("display", "1 -> 2"); - i2c_id: i2c_bus - - platform: ssd1306_spi - model: SSD1306 128x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ssd1322_spi - model: SSD1322 256x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ssd1325_spi - model: SSD1325 128x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ssd1327_i2c - model: SSD1327 128X128 - reset_pin: GPIO23 - address: 0x3D - id: display1327 - brightness: 60% - pages: - - id: page13271 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page13272 - lambda: |- - // Nothing - i2c_id: i2c_bus - - platform: ssd1327_spi - model: SSD1327 128x128 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ssd1331_spi - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ssd1351_spi - model: SSD1351 128x128 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: st7789v - model: TTGO TDisplay 135x240 - cs_pin: GPIO5 - dc_pin: GPIO16 - reset_pin: GPIO23 - backlight_pin: GPIO4 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: st7920 - width: 128 - height: 64 - cs_pin: - number: GPIO23 - inverted: true - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: st7735 - model: INITR_BLACKTAB - cs_pin: GPIO5 - dc_pin: GPIO16 - reset_pin: GPIO23 - rotation: 0 - device_width: 128 - device_height: 160 - col_start: 0 - row_start: 0 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ili9xxx - model: TFT 2.4 - cs_pin: GPIO5 - dc_pin: GPIO4 - reset_pin: GPIO22 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ili9xxx - model: TFT 2.4 - cs_pin: GPIO5 - dc_pin: GPIO4 - reset_pin: GPIO22 - auto_clear_enabled: false - rotation: 90 - lambda: |- - if (!id(glob_bool_processed)) { - it.fill(Color::WHITE); - id(glob_bool_processed) = true; - } - - platform: pvvx_mithermometer - ble_client_id: ble_foo - time_id: sntp_time - disconnect_delay: 3s - update_interval: 10min - validity_period: 20min - lambda: |- - it.print_bignum(188.8); - it.print_unit(pvvx_mithermometer::UNIT_DEG_E); - it.print_smallnum(88); - it.print_percent(true); - it.print_happy(true); - it.print_sad(true); - it.print_bracket(true); - it.print_battery(true); - - platform: tm1621 - id: tm1621_display - cs_pin: GPIO17 - data_pin: GPIO5 - read_pin: GPIO23 - write_pin: GPIO18 - lambda: |- - it.printf(0, "%.1f", id(dht_temperature).state); - it.display_celsius(true); - it.printf(1, "%.1f", id(dht_humidity).state); - it.display_humidity(true); - -tm1651: - id: tm1651_battery - clk_pin: GPIO23 - dio_pin: GPIO23 - -remote_receiver: - pin: GPIO32 - dump: all - -status_led: - pin: GPIO2 - -pn532_spi: - id: pn532_bs - cs_pin: GPIO23 - update_interval: 1s - on_tag: - - lambda: |- - ESP_LOGD("main", "Found tag %s", x.c_str()); - - mqtt.publish: - topic: the/topic - payload: !lambda "return x;" - on_tag_removed: - - lambda: |- - ESP_LOGD("main", "Removed tag %s", x.c_str()); - - mqtt.publish: - topic: the/topic - payload: !lambda "return x;" - -pn532_i2c: - i2c_id: i2c_bus - -rdm6300: - uart_id: uart_0 - -rc522_spi: - cs_pin: GPIO23 - update_interval: 1s - on_tag: - - lambda: |- - ESP_LOGD("main", "Found tag %s", x.c_str()); - -rc522_i2c: - - update_interval: 1s - on_tag: - - lambda: |- - ESP_LOGD("main", "Found tag %s", x.c_str()); - i2c_id: i2c_bus - - - update_interval: 1s - on_tag: - - lambda: |- - ESP_LOGD("main", "Found tag %s", x.c_str()); - i2c_id: i2c_bus - -mcp4728: - - id: mcp4728_dac - store_in_eeprom: false - address: 0x60 - i2c_id: i2c_bus - -gps: - uart_id: uart_0 - -time: - - platform: sntp - id: sntp_time - servers: - - 0.pool.ntp.org - - 1.pool.ntp.org - - 192.168.178.1 - on_time: - cron: "/30 0-30,30/5 * ? JAN-DEC MON,SAT-SUN,TUE-FRI" - then: - - lambda: 'ESP_LOGD("main", "time");' - - platform: gps - on_time_sync: - then: - ds1307.write_time: - id: ds1307_time - - platform: ds1307 - id: ds1307_time - update_interval: never - on_time: - seconds: 0 - then: ds1307.read_time - i2c_id: i2c_bus - -cover: - - platform: template - name: Template Cover - id: template_cover - lambda: |- - if (id(binary_sensor1).state) { - return COVER_OPEN; - } else { - return {}; - } - optimistic: true - open_action: - - cover.template.publish: - id: template_cover - state: CLOSED - assumed_state: false - has_position: true - position_state_topic: position/state/topic - position_command_topic: position/command/topic - tilt_lambda: !lambda "return 0.5;" - tilt_state_topic: tilt/state/topic - tilt_command_topic: tilt/command/topic - on_open: - then: - - lambda: 'ESP_LOGD("cover", "open");' - on_closed: - then: - - lambda: 'ESP_LOGD("cover", "closed");' - - platform: am43 - name: Test AM43 - id: am43_test - ble_client_id: ble_foo - icon: mdi:blinds - - platform: feedback - name: Feedback Cover - id: gate - device_class: gate - - infer_endstop_from_movement: false - has_built_in_endstop: false - max_duration: 30s - direction_change_wait_time: 300ms - acceleration_wait_time: 150ms - obstacle_rollback: 10% - - open_duration: 22.1s - open_endstop: open_endstop_sensor - open_sensor: open_sensor - open_obstacle_sensor: open_obstacle_sensor - - close_duration: 22.4s - close_endstop: close_endstop_sensor - close_sensor: close_sensor - close_obstacle_sensor: close_obstacle_sensor - - open_action: - - logger.log: Open Action - - close_action: - - logger.log: Close Action - - stop_action: - - logger.log: Stop Action - -debug: - -tca9548a: - - address: 0x70 - id: multiplex0 - channels: - - bus_id: multiplex0_chan0 - channel: 0 - i2c_id: i2c_bus - - address: 0x71 - id: multiplex1 - i2c_id: multiplex0_chan0 - -pcf8574: - - id: pcf8574_hub - address: 0x21 - pcf8575: false - i2c_id: i2c_bus - -pca9554: - - id: pca9554_hub - address: 0x3F - i2c_id: i2c_bus - -pca6416a: - - id: pca6416a_hub - address: 0x21 - i2c_id: i2c_bus - -mcp23017: - - id: mcp23017_hub - open_drain_interrupt: true - i2c_id: i2c_bus - -mcp23008: - - id: mcp23008_hub - address: 0x22 - open_drain_interrupt: true - i2c_id: i2c_bus - -mcp23016: - - id: mcp23016_hub - address: 0x23 - i2c_id: i2c_bus - -stepper: - - platform: a4988 - id: my_stepper - step_pin: GPIO23 - dir_pin: GPIO25 - sleep_pin: GPIO25 - max_speed: 250 steps/s - acceleration: 100 steps/s^2 - deceleration: 200 steps/s^2 - -globals: - - id: glob_int - type: int - restore_value: true - initial_value: "0" - - id: glob_float - type: float - restore_value: true - initial_value: "0.0f" - - id: glob_bool - type: bool - restore_value: false - initial_value: "true" - - id: glob_string - type: std::string - restore_value: false - # initial_value: "" - - id: glob_bool_processed - type: bool - restore_value: false - initial_value: "false" - -text_sensor: - - platform: ble_client - ble_client_id: ble_foo - name: Sensor Location - service_uuid: "180d" - characteristic_uuid: "2a38" - descriptor_uuid: "2902" - notify: true - update_interval: never - on_notify: - then: - - lambda: |- - ESP_LOGD("green_btn", "Location changed: %s", x.c_str()); - - platform: mqtt_subscribe - name: MQTT Subscribe Text - topic: "the/topic" - qos: 2 - on_value: - - text_sensor.template.publish: - id: ${textname}_text - state: Hello World - - text_sensor.template.publish: - id: ${textname}_text - state: |- - return "Hello World2"; - - globals.set: - id: glob_int - value: "0" - - canbus.send: - canbus_id: mcp2515_can - can_id: 23 - data: [0x10, 0x20, 0x30] - - canbus.send: - canbus_id: esp32_internal_can - can_id: 23 - data: [0x10, 0x20, 0x30] - - canbus.send: - canbus_id: mcp2515_can - can_id: 24 - remote_transmission_request: true - data: [] - - canbus.send: - canbus_id: esp32_internal_can - can_id: 24 - remote_transmission_request: true - data: [] - - platform: template - name: Template Text Sensor - id: ${textname}_text - - platform: wifi_info - scan_results: - name: Scan Results - ip_address: - name: IP Address - ssid: - name: SSID - bssid: - name: BSSID - mac_address: - name: Mac Address - dns_address: - name: DNS ADdress - - platform: version - name: ESPHome Version No Timestamp - hide_timestamp: true - - platform: teleinfo - tag_name: OPTARIF - name: optarif - teleinfo_id: myteleinfo - -sn74hc595: - - id: sn74hc595_hub - data_pin: GPIO21 - clock_pin: GPIO23 - latch_pin: GPIO22 - oe_pin: GPIO32 - sr_count: 2 - -rtttl: - output: gpio_19 - -canbus: - - platform: mcp2515 - id: mcp2515_can - cs_pin: GPIO17 - can_id: 4 - bit_rate: 50kbps - on_frame: - - can_id: 500 - then: - - lambda: |- - std::string b(x.begin(), x.end()); - ESP_LOGD("canid 500", "%s", b.c_str()); - - can_id: 23 - then: - - if: - condition: - lambda: "return x[0] == 0x11;" - then: - light.toggle: ${roomname}_lights - - can_id: 0b00000000000000000000001000000 - can_id_mask: 0b11111000000000011111111000000 - use_extended_id: true - then: - - lambda: |- - auto pdo_id = can_id >> 14; - switch (pdo_id) - { - case 117: - ESP_LOGD("canbus", "exhaust_fan_duty"); - break; - case 118: - ESP_LOGD("canbus", "supply_fan_duty"); - break; - case 119: - ESP_LOGD("canbus", "supply_fan_flow"); - break; - // to be continued... - } - - platform: esp32_can - id: esp32_internal_can - rx_pin: GPIO04 - tx_pin: GPIO05 - can_id: 4 - bit_rate: 50kbps - on_frame: - - can_id: 500 - then: - - lambda: |- - std::string b(x.begin(), x.end()); - ESP_LOGD("canid 500", "%s", b.c_str() ); - - can_id: 23 - then: - - if: - condition: - lambda: "return x[0] == 0x11;" - then: - light.toggle: ${roomname}_lights - - can_id: 0b00000000000000000000001000000 - can_id_mask: 0b11111000000000011111111000000 - use_extended_id: true - then: - - lambda: |- - auto pdo_id = can_id >> 14; - switch (pdo_id) - { - case 117: - ESP_LOGD("canbus", "exhaust_fan_duty"); - break; - case 118: - ESP_LOGD("canbus", "supply_fan_duty"); - break; - case 119: - ESP_LOGD("canbus", "supply_fan_flow"); - break; - // to be continued... - } - -teleinfo: - id: myteleinfo - uart_id: uart_0 - update_interval: 60s - historical_mode: true - -number: - - platform: template - id: test_number - state_topic: livingroom/custom_state_topic - command_topic: livingroom/custom_command_topic - min_value: 0 - step: 1 - max_value: 10 - optimistic: true - -select: - - platform: template - id: test_select - state_topic: livingroom/custom_state_topic - command_topic: livingroom/custom_command_topic - options: - - one - - two - optimistic: true - - platform: copy - source_id: test_select - name: Test Select Copy - -qr_code: - - id: homepage_qr - value: https://esphome.io/index.html - -lock: - - platform: template - id: test_lock1 - name: Template Switch - lambda: |- - if (id(binary_sensor1).state) { - return LOCK_STATE_LOCKED; - }else{ - return LOCK_STATE_UNLOCKED; - } - optimistic: true - assumed_state: false - on_unlock: - - lock.template.publish: - id: test_lock1 - state: !lambda "return LOCK_STATE_UNLOCKED;" - on_lock: - - lock.template.publish: - id: test_lock1 - state: !lambda "return LOCK_STATE_LOCKED;" - - platform: output - name: Generic Output Lock - id: test_lock2 - output: pca_6 - - platform: copy - source_id: test_lock2 - name: Generic Output Lock Copy - -button: - - platform: template - name: Start calibration - on_press: - - scd4x.perform_forced_calibration: - value: 419 - id: scd40 - - scd4x.factory_reset: - id: scd40 - - platform: template - name: Midea Display Toggle - on_press: - midea_ac.display_toggle: - - platform: template - name: Midea Swing Step - on_press: - midea_ac.swing_step: - - platform: template - name: Midea Power On - on_press: - midea_ac.power_on: - - platform: template - name: Midea Power Off - on_press: - midea_ac.power_off: - - platform: template - name: Midea Power Inverse - on_press: - midea_ac.power_toggle: - -ld2410: - id: my_ld2410 - uart_id: ld2410_uart - timeout: 150s - max_move_distance: 6m - max_still_distance: 0.75m - g0_move_threshold: 10 - g0_still_threshold: 20 - g2_move_threshold: 20 - g2_still_threshold: 21 - g8_move_threshold: 80 - g8_still_threshold: 81 - -lcd_menu: - display_id: my_lcd_gpio - mark_back: 0x5e - mark_selected: 0x3e - mark_editing: 0x2a - mark_submenu: 0x7e - active: false - mode: rotary - on_enter: - then: - lambda: 'ESP_LOGI("lcd_menu", "root enter");' - on_leave: - then: - lambda: 'ESP_LOGI("lcd_menu", "root leave");' - items: - - type: back - text: Back - - type: label - - type: menu - text: Submenu 1 - items: - - type: back - text: Back - - type: menu - text: Submenu 21 - items: - - type: back - text: Back - - type: command - text: Show Main - on_value: - then: - - display_menu.show_main: - - type: select - text: Enum Item - immediate_edit: true - select: test_select - on_enter: - then: - lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' - on_leave: - then: - lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' - on_value: - then: - lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' - - type: number - text: Number - number: test_number - on_enter: - then: - lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' - on_leave: - then: - lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' - on_value: - then: - lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' - - type: command - text: Hide - on_value: - then: - - display_menu.hide: - - type: switch - text: Switch - switch: my_switch - on_text: Bright - off_text: Dark - immediate_edit: false - on_value: - then: - lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());' - - type: custom - text: !lambda 'return "Custom";' - value_lambda: 'return "Val";' - on_next: - then: - lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());' - on_prev: - then: - lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/test2.yaml b/tests/test2.yaml deleted file mode 100644 index 8f56336848..0000000000 --- a/tests/test2.yaml +++ /dev/null @@ -1,704 +0,0 @@ ---- -esphome: - name: $devicename - platform: ESP32 - board: nodemcu-32s - build_path: build/test2 - -substitutions: - devicename: test2 - -ethernet: - type: LAN8720 - mdc_pin: GPIO23 - mdio_pin: GPIO25 - clk_mode: GPIO0_IN - phy_addr: 0 - power_pin: GPIO25 - manual_ip: - static_ip: 192.168.178.56 - gateway: 192.168.178.1 - subnet: 255.255.255.0 - domain: .local - -mdns: - disabled: true - -api: - -i2c: - sda: 21 - scl: 22 - scan: false - -spi: - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 - -uart: - tx_pin: GPIO22 - rx_pin: GPIO23 - baud_rate: 115200 - # Specifically added for testing debug with no after: definition. - debug: - dummy_receiver: false - direction: rx - sequence: - - lambda: UARTDebug::log_hex(direction, bytes, ':'); - -ota: - safe_mode: true - port: 3286 - num_attempts: 15 - -logger: - level: DEBUG - -deep_sleep: - run_duration: - default: 20s - gpio_wakeup_reason: 10s - touch_wakeup_reason: 15s - sleep_duration: 50s - wakeup_pin: GPIO2 - wakeup_pin_mode: INVERT_WAKEUP - -as3935_i2c: - irq_pin: GPIO12 - -mcp3008: - - id: mcp3008_hub - cs_pin: GPIO12 - -output: - - platform: ac_dimmer - id: dimmer1 - gate_pin: GPIO5 - zero_cross_pin: GPIO12 - -sensor: - - platform: homeassistant - entity_id: sensor.hello_world - id: ha_hello_world - - platform: homeassistant - entity_id: climate.living_room - attribute: temperature - id: ha_hello_world_temperature - - platform: ble_rssi - mac_address: AC:37:43:77:5F:4C - name: BLE Google Home Mini RSSI value - - platform: ble_rssi - service_uuid: 11aa - name: BLE Test Service 16 - - platform: ble_rssi - service_uuid: "11223344" - name: BLE Test Service 32 - - platform: ble_rssi - service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 - name: BLE Test Service 128 - - platform: ble_rssi - service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 - name: BLE Test iBeacon UUID - - platform: b_parasite - mac_address: F0:CA:F0:CA:01:01 - humidity: - name: b-parasite Air Humidity - temperature: - name: b-parasite Air Temperature - moisture: - name: b-parasite Soil Moisture - battery_voltage: - name: b-parasite Battery Voltage - illuminance: - name: b-parasite Illuminance - - platform: senseair - id: senseair0 - co2: - name: SenseAir CO2 Value - on_value: - then: - - senseair.background_calibration: senseair0 - - senseair.background_calibration_result: senseair0 - - senseair.abc_get_period: senseair0 - - senseair.abc_enable: senseair0 - - senseair.abc_disable: senseair0 - update_interval: 15s - - platform: ruuvitag - mac_address: FF:56:D3:2F:7D:E8 - humidity: - name: RuuviTag Humidity - temperature: - name: RuuviTag Temperature - pressure: - name: RuuviTag Pressure - acceleration_x: - name: RuuviTag Acceleration X - acceleration_y: - name: RuuviTag Acceleration Y - acceleration_z: - name: RuuviTag Acceleration Z - battery_voltage: - name: RuuviTag Battery Voltage - tx_power: - name: RuuviTag TX Power - movement_counter: - name: RuuviTag Movement Counter - measurement_sequence_number: - name: RuuviTag Measurement Sequence Number - - platform: as3935 - lightning_energy: - name: Lightning Energy - distance: - name: Distance Storm - - platform: xiaomi_hhccjcy01 - mac_address: 94:2B:FF:5C:91:61 - temperature: - name: Xiaomi HHCCJCY01 Temperature - moisture: - name: Xiaomi HHCCJCY01 Moisture - illuminance: - name: Xiaomi HHCCJCY01 Illuminance - conductivity: - name: Xiaomi HHCCJCY01 Soil Conductivity - battery_level: - name: Xiaomi HHCCJCY01 Battery Level - - platform: xiaomi_lywsdcgq - mac_address: 7A:80:8E:19:36:BA - temperature: - name: Xiaomi LYWSDCGQ Temperature - humidity: - name: Xiaomi LYWSDCGQ Humidity - battery_level: - name: Xiaomi LYWSDCGQ Battery Level - - platform: xiaomi_lywsd02 - mac_address: 3F:5B:7D:82:58:4E - temperature: - name: Xiaomi LYWSD02 Temperature - humidity: - name: Xiaomi LYWSD02 Humidity - battery_level: - name: Xiaomi LYWSD02 Battery Level - - platform: xiaomi_cgg1 - mac_address: 7A:80:8E:19:36:BA - temperature: - name: Xiaomi CGG1 Temperature - humidity: - name: Xiaomi CGG1 Humidity - battery_level: - name: Xiaomi CGG1 Battery Level - - platform: xiaomi_gcls002 - mac_address: 94:2B:FF:5C:91:61 - temperature: - name: GCLS02 Temperature - moisture: - name: GCLS02 Moisture - conductivity: - name: GCLS02 Soil Conductivity - illuminance: - name: GCLS02 Illuminance - - platform: xiaomi_hhccpot002 - mac_address: 94:2B:FF:5C:91:61 - moisture: - name: HHCCPOT002 Moisture - conductivity: - name: HHCCPOT002 Soil Conductivity - - platform: xiaomi_lywsd03mmc - mac_address: A4:C1:38:4E:16:78 - bindkey: e9efaa6873f9f9c87a5e75a5f814801c - temperature: - name: Xiaomi LYWSD03MMC Temperature - humidity: - name: Xiaomi LYWSD03MMC Humidity - battery_level: - name: Xiaomi LYWSD03MMC Battery Level - - platform: xiaomi_cgd1 - mac_address: A4:C1:38:D1:61:7D - bindkey: c99d2313182473b38001086febf781bd - temperature: - name: Xiaomi CGD1 Temperature - humidity: - name: Xiaomi CGD1 Humidity - battery_level: - name: Xiaomi CGD1 Battery Level - - platform: xiaomi_jqjcy01ym - mac_address: 7A:80:8E:19:36:BA - temperature: - name: JQJCY01YM Temperature - humidity: - name: JQJCY01YM Humidity - formaldehyde: - name: JQJCY01YM Formaldehyde - battery_level: - name: JQJCY01YM Battery Level - - platform: xiaomi_mhoc303 - mac_address: E7:50:59:32:A0:1C - temperature: - name: MHO-C303 Temperature - humidity: - name: MHO-C303 Humidity - battery_level: - name: MHO-C303 Battery Level - - platform: atc_mithermometer - mac_address: A4:C1:38:4E:16:78 - temperature: - name: ATC Temperature - humidity: - name: ATC Humidity - battery_level: - name: ATC Battery-Level - battery_voltage: - name: ATC Battery-Voltage - - platform: pvvx_mithermometer - mac_address: A4:C1:38:4E:16:78 - temperature: - name: PVVX Temperature - humidity: - name: PVVX Humidity - battery_level: - name: PVVX Battery-Level - battery_voltage: - name: PVVX Battery-Voltage - - platform: inkbird_ibsth1_mini - mac_address: 38:81:D7:0A:9C:11 - temperature: - name: Inkbird IBS-TH1 Temperature - humidity: - name: Inkbird IBS-TH1 Humidity - battery_level: - name: Inkbird IBS-TH1 Battery Level - - platform: xiaomi_rtcgq02lm - id: motion_rtcgq02lm - battery_level: - name: Mi Motion Sensor 2 Battery level - - platform: ltr390 - uv: - name: LTR390 UV - uv_index: - name: LTR390 UVI - light: - name: LTR390 Light - ambient_light: - name: LTR390 ALS - gain: X3 - resolution: 18 - window_correction_factor: 1.0 - address: 0x53 - update_interval: 60s - - platform: sgp4x - voc: - name: VOC Index - id: sgp40_voc_index - algorithm_tuning: - index_offset: 100 - learning_time_offset_hours: 12 - learning_time_gain_hours: 12 - gating_max_duration_minutes: 180 - std_initial: 50 - gain_factor: 230 - nox: - name: NOx - algorithm_tuning: - index_offset: 100 - learning_time_offset_hours: 12 - learning_time_gain_hours: 12 - gating_max_duration_minutes: 180 - std_initial: 50 - gain_factor: 230 - update_interval: 5s - - platform: mcp3008 - update_interval: 5s - mcp3008_id: mcp3008_hub - id: freezer_temp_source - reference_voltage: 3.19 - number: 0 - - platform: airthings_wave_plus - ble_client_id: airthings01 - update_interval: 5min - temperature: - name: Wave Plus Temperature - radon: - name: Wave Plus Radon - radon_long_term: - name: Wave Plus Radon Long Term - pressure: - name: Wave Plus Pressure - humidity: - name: Wave Plus Humidity - co2: - name: Wave Plus CO2 - tvoc: - name: Wave Plus VOC - - platform: airthings_wave_mini - ble_client_id: airthingsmini01 - update_interval: 5min - temperature: - name: Wave Mini Temperature - humidity: - name: Wave Mini Humidity - pressure: - name: Wave Mini Pressure - tvoc: - name: Wave Mini VOC - - platform: ina260 - address: 0x40 - current: - name: INA260 Current - power: - name: INA260 Power - bus_voltage: - name: INA260 Voltage - update_interval: 60s - - platform: radon_eye_rd200 - ble_client_id: radon_eye_ble_id - update_interval: 10min - radon: - name: RD200 Radon - radon_long_term: - name: RD200 Radon Long Term - - platform: mopeka_pro_check - mac_address: D3:75:F2:DC:16:91 - tank_type: CUSTOM - custom_distance_full: 40cm - custom_distance_empty: 10mm - temperature: - name: Propane test temp - level: - name: Propane test level - distance: - name: Propane test distance - battery_level: - name: Propane test battery level - - platform: ufire_ec - id: ufire_ec_board - ec: - name: Ufire EC - temperature_sensor: ha_hello_world_temperature - temperature_compensation: 20.0 - temperature_coefficient: 0.019 - - platform: ufire_ise - id: ufire_ise_board - temperature_sensor: ha_hello_world_temperature - ph: - name: Ufire pH - - platform: mics_4514 - update_interval: 60s - nitrogen_dioxide: - name: MICS-4514 NO2 - carbon_monoxide: - name: MICS-4514 CO - methane: - name: MICS-4514 CH4 - hydrogen: - name: MICS-4514 H2 - ethanol: - name: MICS-4514 C2H5OH - ammonia: - name: MICS-4514 NH3 - -time: - - platform: homeassistant - on_time: - - at: "16:00:00" - then: - - logger.log: It's 16:00 - -esp32_touch: - setup_mode: true - -binary_sensor: - - platform: homeassistant - entity_id: binary_sensor.hello_world - id: ha_hello_world_binary - - platform: homeassistant - entity_id: binary_sensor.hello - attribute: world - id: ha_hello_world_binary_attribute - - platform: ble_presence - mac_address: AC:37:43:77:5F:4C - name: ESP32 BLE Tracker Google Home Mini - - platform: ble_presence - service_uuid: 11aa - name: BLE Test Service 16 Presence - - platform: ble_presence - service_uuid: "11223344" - name: BLE Test Service 32 Presence - - platform: ble_presence - service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 - name: BLE Test Service 128 Presence - - platform: ble_presence - ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00 - ibeacon_major: 100 - ibeacon_minor: 1 - name: BLE Test iBeacon Presence - - platform: esp32_touch - name: ESP32 Touch Pad GPIO27 - pin: GPIO27 - threshold: 1000 - - platform: as3935 - name: Storm Alert - - platform: xiaomi_mue4094rt - name: MUE4094RT Motion - mac_address: 7A:80:8E:19:36:BA - timeout: 5s - - platform: xiaomi_mjyd02yla - name: MJYD02YL-A Motion - mac_address: 50:EC:50:CD:32:02 - bindkey: 48403ebe2d385db8d0c187f81e62cb64 - idle_time: - name: MJYD02YL-A Idle Time - light: - name: MJYD02YL-A Light Status - battery_level: - name: MJYD02YL-A Battery Level - - platform: xiaomi_wx08zm - name: WX08ZM Activation State - mac_address: 74:a3:4a:b5:07:34 - tablet: - name: WX08ZM Tablet Resource - battery_level: - name: WX08ZM Battery Level - - platform: xiaomi_cgpr1 - name: CGPR1 Motion - mac_address: "12:34:56:12:34:56" - bindkey: 48403ebe2d385db8d0c187f81e62cb64 - battery_level: - name: CGPR1 battery Level - idle_time: - name: CGPR1 Idle Time - illuminance: - name: CGPR1 Illuminance - - platform: xiaomi_rtcgq02lm - id: motion_rtcgq02lm - motion: - name: Mi Motion Sensor 2 - light: - name: Mi Motion Sensor 2 Light - button: - name: Mi Motion Sensor 2 Button - - platform: gpio - id: gpio_set_retry_test - pin: GPIO9 - on_press: - then: - - lambda: |- - App.scheduler.set_retry(id(gpio_set_retry_test), "set_retry_test", 100, 3, [](const uint8_t remaining) { - return remaining ? RetryResult::RETRY : RetryResult::DONE; // just to reference both symbols - }, 5.0f); - -esp32_ble_tracker: - on_ble_advertise: - - mac_address: AC:37:43:77:5F:4C - then: - # yamllint disable rule:line-length - - lambda: !lambda |- - ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); - # yamllint enable rule:line-length - - then: - # yamllint disable rule:line-length - - lambda: !lambda |- - ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); - # yamllint enable rule:line-length - on_ble_service_data_advertise: - - service_uuid: ABCD - then: - - lambda: !lambda |- - ESP_LOGD("main", "Length of service data is %i", x.size()); - on_ble_manufacturer_data_advertise: - - manufacturer_id: ABCD - then: - - lambda: !lambda |- - ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); - -ble_client: - - mac_address: 01:02:03:04:05:06 - id: airthings01 - - mac_address: 01:02:03:04:05:06 - id: airthingsmini01 - - mac_address: 01:02:03:04:05:06 - id: radon_eye_ble_id - -airthings_ble: - -radon_eye_ble: - -ruuvi_ble: - -xiaomi_ble: - -mopeka_ble: - -bluetooth_proxy: - active: true - -xiaomi_rtcgq02lm: - - id: motion_rtcgq02lm - mac_address: 01:02:03:04:05:06 - bindkey: "48403ebe2d385db8d0c187f81e62cb64" - -status_led: - pin: GPIO2 - -text_sensor: - - platform: version - name: ESPHome Version - icon: mdi:icon - id: version_sensor - on_value: - - if: - condition: - - api.connected: - then: - # yamllint disable rule:line-length - - lambda: !lambda |- - ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); - # yamllint enable rule:line-length - - script.execute: my_script - - script.execute: - id: my_script_with_params - prefix: Running my_script_with_params - param2: 100 - param3: true - - script.execute: - id: my_script_with_params - prefix: Running my_script_with_params using lambda parameters - param2: !lambda return 200; - param3: !lambda return true; - - homeassistant.service: - service: notify.html5 - data: - title: New Humidity - data_template: - message: The humidity is {{ my_variable }}%. - variables: - my_variable: |- - return id(version_sensor).state; - my_variable_str: |- - return "Hello World"; - - homeassistant.service: - service: light.turn_on - data: - entity_id: light.my_light - - homeassistant.tag_scanned: - tag: 1234-abcd - - homeassistant.tag_scanned: 1234-abcd - - deep_sleep.enter: - sleep_duration: 30min - - deep_sleep.enter: - sleep_duration: !lambda "return 30 * 60 * 1000;" - - platform: template - name: Template Text Sensor - lambda: |- - return {"Hello World"}; - filters: - - to_upper: - - to_lower: - - append: xyz - - prepend: abcd - - substitute: - - Hello -> Goodbye - - map: - - red -> green - - lambda: 'return {"1234"};' - - platform: homeassistant - entity_id: sensor.hello_world2 - id: ha_hello_world2 - - platform: homeassistant - entity_id: sensor.hello_world3 - id: ha_hello_world3 - attribute: some_attribute - - platform: ble_scanner - name: Scanner - -script: - - id: my_script - mode: single - then: - - lambda: 'ESP_LOGD("main", "Hello World!");' - - id: my_script_queued - mode: queued - max_runs: 2 - then: - - lambda: 'ESP_LOGD("main", "Hello World!");' - - id: my_script_parallel - mode: parallel - max_runs: 2 - then: - - lambda: 'ESP_LOGD("main", "Hello World!");' - - id: my_script_restart - mode: restart - then: - - lambda: 'ESP_LOGD("main", "Hello World!");' - - id: my_script_with_params - parameters: - prefix: string - param2: int - param3: bool - then: - - lambda: 'ESP_LOGD("main", (prefix + " Hello World!" + to_string(param2) + " " + to_string(param3)).c_str());' - -stepper: - - platform: uln2003 - id: my_stepper - pin_a: GPIO23 - pin_b: GPIO27 - pin_c: GPIO25 - pin_d: GPIO26 - sleep_when_done: false - step_mode: HALF_STEP - max_speed: 250 steps/s - - # Optional: - acceleration: inf - deceleration: inf - -interval: - interval: 5s - then: - - logger.log: Interval Run - -display: - -image: - - id: binary_image - file: pnglogo.png - type: BINARY - dither: FloydSteinberg - - id: transparent_transparent_image - file: pnglogo.png - type: TRANSPARENT_BINARY - - id: rgba_image - file: pnglogo.png - type: RGBA - resize: 50x50 - - id: rgb24_image - file: pnglogo.png - type: RGB24 - use_transparency: yes - - id: rgb565_image - file: pnglogo.png - type: RGB565 - use_transparency: no - -cap1188: - id: cap1188_component - address: 0x29 - touch_threshold: 0x20 - allow_multiple_touches: true - reset_pin: 14 - -switch: - - platform: template - name: Test BLE Write Action - turn_on_action: - - ble_client.ble_write: - id: airthings01 - service_uuid: F61E3BE9-2826-A81B-970A-4D4DECFABBAE - characteristic_uuid: 6490FAFE-0734-732C-8705-91B653A081FC - value: [0x01, 0xab, 0xff] - - ble_client.ble_write: - id: airthings01 - service_uuid: F61E3BE9-2826-A81B-970A-4D4DECFABBAE - characteristic_uuid: 6490FAFE-0734-732C-8705-91B653A081FC - value: !lambda |- - return {0x13, 0x37}; diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml deleted file mode 100644 index 0daf4e9671..0000000000 --- a/tests/test3.1.yaml +++ /dev/null @@ -1,602 +0,0 @@ ---- -esphome: - name: $device_name - comment: $device_comment - build_path: build/test3.1 - includes: - - custom.h - -esp8266: - board: d1_mini - -substitutions: - device_name: test3-1 - device_comment: test3-1 device - min_sub: "0.03" - max_sub: "12.0%" - -api: - -wifi: - ssid: "MySSID" - password: "password1" - -i2c: - sda: 4 - scl: 5 - scan: false - -spi: - clk_pin: GPIO12 - mosi_pin: GPIO13 - miso_pin: GPIO14 - -ota: - -logger: - -sensor: - - platform: apds9960 - type: proximity - name: APDS9960 Proximity - - platform: vl53l0x - name: VL53L0x Distance - address: 0x29 - update_interval: 60s - enable_pin: GPIO13 - timeout: 200us - - platform: apds9960 - type: clear - name: APDS9960 Clear - - platform: apds9960 - type: red - name: APDS9960 Red - - platform: apds9960 - type: green - name: APDS9960 Green - - platform: apds9960 - type: blue - name: APDS9960 Blue - - - platform: aht10 - temperature: - name: Temperature - humidity: - name: Humidity - - platform: am2320 - temperature: - name: Temperature - humidity: - name: Humidity - - platform: adc - pin: VCC - id: my_sensor - filters: - - offset: 5.0 - - multiply: 2.0 - - filter_out: NAN - - sliding_window_moving_average: - - exponential_moving_average: - - quantile: - window_size: 5 - send_every: 5 - send_first_at: 3 - quantile: .8 - - lambda: "return 0;" - - delta: 100 - - throttle: 100ms - - debounce: 500s - - calibrate_linear: - - 0 -> 0 - - 100 -> 100 - - calibrate_polynomial: - degree: 3 - datapoints: - - 0 -> 0 - - 100 -> 200 - - 400 -> 500 - - -50 -> -1000 - - -100 -> -10000 - - platform: cd74hc4067 - id: cd74hc4067_0 - number: 0 - sensor: my_sensor - - platform: resistance - sensor: my_sensor - configuration: DOWNSTREAM - resistor: 10kΩ - reference_voltage: 3.3V - name: Resistance - id: resist - - platform: ntc - sensor: resist - name: NTC Sensor - calibration: - b_constant: 3950 - reference_resistance: 10k - reference_temperature: 25°C - - platform: ntc - sensor: resist - name: NTC Sensor2 - calibration: - - 10.0kOhm -> 25°C - - 27.219kOhm -> 0°C - - 14.674kOhm -> 15°C - - platform: ct_clamp - sensor: my_sensor - name: CT Clamp - sample_duration: 500ms - update_interval: 5s - - - platform: tcs34725 - red_channel: - name: Red Channel - green_channel: - name: Green Channel - blue_channel: - name: Blue Channel - clear_channel: - name: Clear Channel - illuminance: - name: Illuminance - color_temperature: - name: Color Temperature - integration_time: 614ms - gain: 60x - - platform: custom - lambda: |- - auto s = new CustomSensor(); - App.register_component(s); - return {s}; - sensors: - - id: custom_sensor - name: Custom Sensor - - - platform: ade7953 - irq_pin: GPIO16 - voltage: - name: ADE7953 Voltage - id: ade7953_voltage - current_a: - name: ADE7953 Current A - id: ade7953_current_a - current_b: - name: ADE7953 Current B - id: ade7953_current_b - active_power_a: - name: ADE7953 Active Power A - id: ade7953_active_power_a - active_power_b: - name: ADE7953 Active Power B - id: ade7953_active_power_b - - - platform: tmp102 - name: TMP102 Temperature - - platform: hm3301 - pm_1_0: - name: PM1.0 - pm_2_5: - name: PM2.5 - pm_10_0: - name: PM10.0 - aqi: - name: AQI - calculation_type: AQI - - platform: ezo - id: ph_ezo - address: 99 - unit_of_measurement: pH - - platform: tof10120 - name: Distance sensor - update_interval: 5s - - - platform: mlx90393 - oversampling: 1 - filter: 0 - gain: 3X - x_axis: - name: mlxxaxis - y_axis: - name: mlxyaxis - z_axis: - name: mlxzaxis - resolution: 17BIT - temperature: - name: mlxtemp - oversampling: 2 - - - platform: adc128s102 - id: adc128s102_channel_0 - channel: 0 - -apds9960: - address: 0x20 - update_interval: 60s - -mpr121: - id: mpr121_first - address: 0x5A - -binary_sensor: - - platform: apds9960 - direction: up - name: APDS9960 Up - device_class: motion - filters: - - invert - - delayed_on: 20ms - - delayed_off: 20ms - - lambda: "return false;" - on_state: - - logger.log: New state - id: my_binary_sensor - - platform: apds9960 - direction: down - name: APDS9960 Down - - platform: apds9960 - direction: left - name: APDS9960 Left - - platform: apds9960 - direction: right - name: APDS9960 Right - - - platform: mpr121 - id: touchkey0 - channel: 0 - name: touchkey0 - - platform: mpr121 - channel: 1 - name: touchkey1 - id: bin1 - - platform: mpr121 - channel: 2 - name: touchkey2 - id: bin2 - - platform: mpr121 - channel: 3 - name: touchkey3 - id: bin3 - on_press: - then: - - switch.toggle: mpr121_toggle - - platform: ttp229_lsf - channel: 1 - name: TTP229 LSF Test - - platform: ttp229_bsf - channel: 1 - name: TTP229 BSF Test - - platform: custom - lambda: |- - auto s = new CustomBinarySensor(); - App.register_component(s); - return {s}; - binary_sensors: - - id: custom_binary_sensor - name: Custom Binary Sensor - - - platform: template - id: cover_toggle - on_press: - then: - - cover.toggle: time_based_cover - - cover.toggle: endstop_cover - - cover.toggle: current_based_cover - -globals: - - id: my_global_string - type: std::string - initial_value: '""' - -text_sensor: - - platform: custom - lambda: |- - auto s = new CustomTextSensor(); - App.register_component(s); - return {s}; - text_sensors: - - id: custom_text_sensor - name: Custom Text Sensor - -sm2135: - data_pin: GPIO12 - clock_pin: GPIO14 - rgb_current: 20mA - cw_current: 60mA - -switch: - - platform: template - name: mpr121_toggle - id: mpr121_toggle - optimistic: true - - platform: gpio - id: gpio_switch1 - pin: - mcp23xxx: mcp23017_hub - number: 0 - mode: OUTPUT - interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] - - platform: gpio - id: gpio_switch2 - pin: - mcp23xxx: mcp23008_hub - number: 0 - mode: OUTPUT - interlock: *interlock - - platform: gpio - id: gpio_switch3 - pin: GPIO1 - interlock: *interlock - - platform: custom - lambda: |- - auto s = new CustomSwitch(); - return {s}; - switches: - - id: custom_switch - name: Custom Switch - on_turn_on: - - http_request.get: - url: https://esphome.io - headers: - Content-Type: application/json - verify_ssl: false - - http_request.post: - url: https://esphome.io - verify_ssl: false - json: - key: !lambda |- - return id(custom_text_sensor).state; - greeting: Hello World - - http_request.send: - method: PUT - url: https://esphome.io - headers: - Content-Type: application/json - body: Some data - verify_ssl: false - - -custom_component: - lambda: |- - auto s = new CustomComponent(); - s->set_update_interval(15000); - return {s}; - -stepper: - - platform: uln2003 - id: my_stepper - pin_a: GPIO12 - pin_b: GPIO13 - pin_c: GPIO14 - pin_d: GPIO15 - sleep_when_done: false - step_mode: HALF_STEP - max_speed: 250 steps/s - acceleration: inf - deceleration: inf - - platform: a4988 - id: my_stepper2 - step_pin: GPIO1 - dir_pin: GPIO2 - max_speed: 0.1 steps/s - acceleration: 10 steps/s^2 - deceleration: 10 steps/s^2 - -interval: - interval: 5s - then: - - logger.log: Interval Run - - stepper.set_target: - id: my_stepper2 - target: 500 - - stepper.set_target: - id: my_stepper - target: !lambda "return 0;" - - stepper.report_position: - id: my_stepper2 - position: 0 - - stepper.report_position: - id: my_stepper - position: !lambda "return 50/100.0;" - -cover: - - platform: endstop - name: Endstop Cover - id: endstop_cover - stop_action: - - switch.turn_on: gpio_switch1 - open_endstop: my_binary_sensor - open_action: - - switch.turn_on: gpio_switch1 - open_duration: 5min - close_endstop: my_binary_sensor - close_action: - - switch.turn_on: gpio_switch2 - - output.set_level: - id: out - level: 50% - - output.esp8266_pwm.set_frequency: - id: out - frequency: 500.0Hz - - output.esp8266_pwm.set_frequency: - id: out - frequency: !lambda "return 500.0;" - - servo.write: - id: my_servo - level: -100% - - servo.write: - id: my_servo - level: !lambda "return -1.0;" - - delay: 2s - - servo.detach: my_servo - close_duration: 4.5min - max_duration: 10min - - platform: time_based - name: Time Based Cover - id: time_based_cover - stop_action: - - switch.turn_on: gpio_switch1 - open_action: - - switch.turn_on: gpio_switch1 - open_duration: 5min - close_action: - - switch.turn_on: gpio_switch2 - close_duration: 4.5min - - platform: current_based - name: Current Based Cover - id: current_based_cover - open_sensor: ade7953_current_a - open_moving_current_threshold: 0.5 - open_obstacle_current_threshold: 0.8 - open_duration: 12s - open_action: - - switch.turn_on: gpio_switch1 - close_sensor: ade7953_current_b - close_moving_current_threshold: 0.5 - close_obstacle_current_threshold: 0.8 - close_duration: 10s - close_action: - - switch.turn_on: gpio_switch2 - stop_action: - - switch.turn_off: gpio_switch1 - - switch.turn_off: gpio_switch2 - obstacle_rollback: 30% - start_sensing_delay: 0.8s - malfunction_detection: true - malfunction_action: - then: - - logger.log: Malfunction Detected - - platform: template - name: Template Cover with Tilt - tilt_lambda: "return 0.5;" - tilt_action: - - output.set_level: - id: out - level: !lambda "return tilt;" - position_action: - - output.set_level: - id: out - level: !lambda "return pos;" - -output: - - platform: esp8266_pwm - id: out - pin: D3 - frequency: 50Hz - - platform: esp8266_pwm - id: out2 - pin: D4 - - platform: custom - type: binary - lambda: |- - auto s = new CustomBinaryOutput(); - App.register_component(s); - return {s}; - outputs: - - id: custom_binary - - platform: sigma_delta_output - id: sddac - update_interval: 60s - pin: D4 - turn_on_action: - then: - - logger.log: "Turned on" - turn_off_action: - then: - - logger.log: "Turned off" - state_change_action: - then: - - logger.log: - format: "Changed state: %d" - args: ["state"] - - platform: custom - type: float - lambda: |- - auto s = new CustomFloatOutput(); - App.register_component(s); - return {s}; - outputs: - - id: custom_float - - platform: slow_pwm - pin: GPIO5 - id: my_slow_pwm - period: 15s - restart_cycle_on_state_change: false - - platform: sm2135 - id: sm2135_0 - channel: 0 - - platform: sm2135 - id: sm2135_1 - channel: 1 - - platform: sm2135 - id: sm2135_2 - channel: 2 - - platform: sm2135 - id: sm2135_3 - channel: 3 - - platform: sm2135 - id: sm2135_4 - channel: 4 - -mcp23017: - id: mcp23017_hub - -mcp23008: - id: mcp23008_hub - - -light: - - platform: hbridge - name: Icicle Lights - pin_a: out - pin_b: out2 - -servo: - id: my_servo - output: out - restore: true - min_level: $min_sub - max_level: $max_sub - -ttp229_lsf: - -ttp229_bsf: - sdo_pin: D2 - scl_pin: D1 - - -display: - - platform: max7219digit - cs_pin: GPIO15 - num_chips: 4 - rotate_chip: 0 - intensity: 10 - scroll_mode: STOP - id: my_matrix - lambda: |- - it.printdigit("hello"); - - -http_request: - useragent: esphome/device - timeout: 10s - -button: - - platform: output - id: output_button - output: out - duration: 100ms - - platform: wake_on_lan - target_mac_address: 12:34:56:78:90:ab - name: wol_test_1 - id: wol_1 - - platform: factory_reset - name: Restart Button (Factory Default Settings) - -cd74hc4067: - pin_s0: GPIO12 - pin_s1: GPIO13 - pin_s2: GPIO14 - pin_s3: GPIO15 - -adc128s102: - cs_pin: GPIO12 diff --git a/tests/test3.yaml b/tests/test3.yaml deleted file mode 100644 index c4847725e8..0000000000 --- a/tests/test3.yaml +++ /dev/null @@ -1,1173 +0,0 @@ ---- -esphome: - name: $device_name - comment: $device_comment - build_path: build/test3 - on_boot: - - if: - condition: - - api.connected - - wifi.connected - - time.has_time - then: - - logger.log: Have time - -esp8266: - board: d1_mini - early_pin_init: true - -substitutions: - device_name: test3 - device_comment: test3 device - -api: - port: 8000 - password: pwd - reboot_timeout: 0min - encryption: - key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= - services: - - service: hello_world - variables: - name: string - then: - - logger.log: - format: Hello World %s! - args: - - name.c_str() - - service: empty_service - then: - - logger.log: Service Called - - service: all_types - variables: - bool_: bool - int_: int - float_: float - string_: string - then: - - logger.log: Something happened - - service: array_types - variables: - bool_arr: bool[] - int_arr: int[] - float_arr: float[] - string_arr: string[] - then: - - logger.log: - # yamllint disable rule:line-length - format: "Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)" - # yamllint enable rule:line-length - args: - - YESNO(bool_arr[0]) - - bool_arr.size() - - int_arr[0] - - int_arr.size() - - float_arr[0] - - float_arr.size() - - string_arr[0].c_str() - - string_arr.size() - - service: dfplayer_next - then: - - dfplayer.play_next: - - service: dfplayer_previous - then: - - dfplayer.play_previous: - - service: dfplayer_play - variables: - file: int - then: - - dfplayer.play: !lambda "return file;" - - service: dfplayer_play_loop - variables: - file: int - loop_: bool - then: - - dfplayer.play: - file: !lambda "return file;" - loop: !lambda "return loop_;" - - service: dfplayer_play_folder - variables: - folder: int - file: int - then: - - dfplayer.play_folder: - folder: !lambda "return folder;" - file: !lambda "return file;" - - - service: dfplayer_play_loo_folder - variables: - folder: int - then: - - dfplayer.play_folder: - folder: !lambda "return folder;" - loop: true - - - service: dfplayer_set_device - variables: - device: int - then: - - dfplayer.set_device: - device: TF_CARD - - - service: dfplayer_set_volume - variables: - volume: int - then: - - dfplayer.set_volume: !lambda "return volume;" - - service: dfplayer_set_eq - variables: - preset: int - then: - # yamllint disable rule:line-length - - dfplayer.set_eq: !lambda "return static_cast(preset);" - # yamllint enable rule:line-length - - - service: dfplayer_sleep - then: - - dfplayer.sleep - - - service: dfplayer_reset - then: - - dfplayer.reset - - - service: dfplayer_start - then: - - dfplayer.start - - - service: dfplayer_pause - then: - - dfplayer.pause - - - service: dfplayer_stop - then: - - dfplayer.stop - - - service: dfplayer_random - then: - - dfplayer.random - - - service: dfplayer_volume_up - then: - - dfplayer.volume_up - - - service: dfplayer_volume_down - then: - - dfplayer.volume_down - - - service: battery_level_percent - variables: - level_percent: int - then: - - tm1651.set_level_percent: - id: tm1651_battery - level_percent: !lambda "return level_percent;" - - service: battery_level - variables: - level: int - then: - - tm1651.set_level: - id: tm1651_battery - level: !lambda "return level;" - - service: battery_brightness - variables: - brightness: int - then: - - tm1651.set_brightness: - id: tm1651_battery - brightness: !lambda "return brightness;" - - service: battery_turn_on - then: - - tm1651.turn_on: - id: tm1651_battery - - service: battery_turn_on - then: - - tm1651.turn_off: - id: tm1651_battery - - service: pid_set_control_parameters - then: - - climate.pid.set_control_parameters: - id: pid_climate - kp: 1.0 - kd: 1.0 - ki: 1.0 - - service: fingerprint_grow_enroll - variables: - finger_id: int - num_scans: int - then: - - fingerprint_grow.enroll: - finger_id: !lambda "return finger_id;" - num_scans: !lambda "return num_scans;" - - service: fingerprint_grow_cancel_enroll - then: - - fingerprint_grow.cancel_enroll: - - service: fingerprint_grow_delete - variables: - finger_id: int - then: - - fingerprint_grow.delete: - finger_id: !lambda "return finger_id;" - - service: fingerprint_grow_delete_all - then: - - fingerprint_grow.delete_all: - -wifi: - ssid: "MySSID" - password: "password1" - -uart: - - id: uart_1 - tx_pin: - number: GPIO1 - inverted: true - rx_pin: GPIO3 - baud_rate: 115200 - - id: uart_2 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 9600 - - id: uart_3 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 4800 - - id: uart_4 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 9600 - - id: uart_5 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 9600 - - id: uart_6 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 9600 - - id: uart_7 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 38400 - - id: uart_8 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 4800 - parity: NONE - stop_bits: 2 - # Specifically added for testing debug with no options at all. - debug: - - id: uart_9 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 9600 - - id: uart_10 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 9600 - - id: uart_11 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 9600 - - id: uart_12 - tx_pin: GPIO4 - rx_pin: GPIO5 - baud_rate: 9600 - -modbus: - uart_id: uart_1 - -vbus: - uart_id: uart_4 - -ota: - safe_mode: true - port: 3286 - reboot_timeout: 15min - -logger: - hardware_uart: UART1 - level: DEBUG - esp8266_store_log_strings_in_flash: true - -improv_serial: - next_url: https://esphome.io/?name={{device_name}}&version={{esphome_version}}&ip={{ip_address}} - -deep_sleep: - run_duration: 20s - sleep_duration: 50s - -wled: - -adalight: - -sensor: - - platform: daly_bms - voltage: - name: Battery Voltage - current: - name: Battery Current - battery_level: - name: Battery Level - max_cell_voltage: - name: Max Cell Voltage - max_cell_voltage_number: - name: Max Cell Voltage Number - min_cell_voltage: - name: Min Cell Voltage - min_cell_voltage_number: - name: Min Cell Voltage Number - max_temperature: - name: Max Temperature - max_temperature_probe_number: - name: Max Temperature Probe Number - min_temperature: - name: Min Temperature - min_temperature_probe_number: - name: Min Temperature Probe Number - remaining_capacity: - name: Remaining Capacity - cells_number: - name: Cells Number - temperature_1: - name: Temperature 1 - temperature_2: - name: Temperature 2 - - - platform: homeassistant - entity_id: sensor.hello_world - id: ha_hello_world - - - platform: hydreon_rgxx - model: RG 9 - uart_id: uart_6 - id: hydreon_rg9 - moisture: - name: hydreon_rain - id: hydreon_rain - - platform: hydreon_rgxx - model: RG_15 - uart_id: uart_6 - acc: - name: hydreon_acc - event_acc: - name: hydreon_event_acc - total_acc: - name: hydreon_total_acc - r_int: - name: hydreon_r_int - - platform: adc - pin: VCC - id: my_sensor - - - platform: binary_sensor_map - name: Binary Sensor Map - type: group - channels: - - binary_sensor: bin1 - value: 10.0 - - binary_sensor: bin2 - value: 15.0 - - binary_sensor: bin3 - value: 100.0 - - - platform: binary_sensor_map - name: Binary Sensor Map - type: sum - channels: - - binary_sensor: bin1 - value: 10.0 - - binary_sensor: bin2 - value: 15.0 - - binary_sensor: bin3 - value: 100.0 - - - platform: binary_sensor_map - name: Binary Sensor Map - type: bayesian - prior: 0.4 - observations: - - binary_sensor: bin1 - prob_given_true: 0.9 - prob_given_false: 0.4 - - binary_sensor: bin2 - prob_given_true: 0.7 - prob_given_false: 0.05 - - binary_sensor: bin3 - prob_given_true: 0.8 - prob_given_false: 0.2 - - - platform: bl0939 - uart_id: uart_8 - voltage: - name: BL0939 Voltage - current_1: - name: BL0939 Current 1 - current_2: - name: BL0939 Current 2 - active_power_1: - name: BL0939 Active Power 1 - active_power_2: - name: BL0939 Active Power 2 - energy_1: - name: BL0939 Energy 1 - energy_2: - name: BL0939 Energy 2 - energy_total: - name: BL0939 Total energy - - platform: bl0940 - uart_id: uart_3 - voltage: - name: BL0940 Voltage - current: - name: BL0940 Current - power: - name: BL0940 Power - energy: - name: BL0940 Energy - internal_temperature: - name: BL0940 Internal temperature - external_temperature: - name: BL0940 External temperature - - platform: bl0942 - uart_id: uart_3 - voltage: - name: BL0942 Voltage - current: - name: BL0942 Current - power: - name: BL0942 Power - energy: - name: BL0942 Energy - frequency: - name: BL0942 Frequency - - platform: pzem004t - uart_id: uart_3 - voltage: - name: PZEM004T Voltage - current: - name: PZEM004T Current - power: - name: PZEM004T Power - - platform: pzemac - id: pzemac1 - voltage: - name: PZEMAC Voltage - current: - name: PZEMAC Current - power: - name: PZEMAC Power - energy: - name: PZEMAC Energy - frequency: - name: PZEMAC Frequency - power_factor: - name: PZEMAC Power Factor - - platform: pzemdc - id: pzemdc1 - voltage: - name: PZEMDC Voltage - current: - name: PZEMDC Current - power: - name: PZEMDC Power - energy: - name: PZEMDC Energy - - - platform: pmsx003 - uart_id: uart_9 - type: PMSX003 - pm_1_0: - name: PM 1.0 Concentration - pm_2_5: - name: PM 2.5 Concentration - pm_10_0: - name: PM 10.0 Concentration - pm_1_0_std: - name: PM 1.0 Standard Atmospher Concentration - pm_2_5_std: - name: PM 2.5 Standard Atmospher Concentration - pm_10_0_std: - name: PM 10.0 Standard Atmospher Concentration - pm_0_3um: - name: Particulate Count >0.3um - pm_0_5um: - name: Particulate Count >0.5um - pm_1_0um: - name: Particulate Count >1.0um - pm_2_5um: - name: Particulate Count >2.5um - pm_5_0um: - name: Particulate Count >5.0um - pm_10_0um: - name: Particulate Count >10.0um - update_interval: 30s - - platform: pmsx003 - uart_id: uart_5 - type: PMS5003T - pm_1_0: - name: PM 1.0 Concentration - pm_2_5: - name: PM 2.5 Concentration - pm_10_0: - name: PM 10.0 Concentration - pm_1_0_std: - name: PM 1.0 Standard Atmospher Concentration - pm_2_5_std: - name: PM 2.5 Standard Atmospher Concentration - pm_10_0_std: - name: PM 10.0 Standard Atmospher Concentration - pm_0_3um: - name: Particulate Count >0.3um - pm_0_5um: - name: Particulate Count >0.5um - pm_1_0um: - name: Particulate Count >1.0um - pm_2_5um: - name: Particulate Count >2.5um - temperature: - name: PMS Temperature - humidity: - name: PMS Humidity - - platform: pmsx003 - uart_id: uart_6 - type: PMS5003ST - pm_1_0: - name: PM 1.0 Concentration - pm_2_5: - name: PM 2.5 Concentration - pm_10_0: - name: PM 10.0 Concentration - pm_1_0_std: - name: PM 1.0 Standard Atmospher Concentration - pm_2_5_std: - name: PM 2.5 Standard Atmospher Concentration - pm_10_0_std: - name: PM 10.0 Standard Atmospher Concentration - pm_0_3um: - name: Particulate Count >0.3um - pm_0_5um: - name: Particulate Count >0.5um - pm_1_0um: - name: Particulate Count >1.0um - pm_2_5um: - name: Particulate Count >2.5um - pm_5_0um: - name: Particulate Count >5.0um - pm_10_0um: - name: Particulate Count >10.0um - temperature: - name: PMS Temperature - humidity: - name: PMS Humidity - formaldehyde: - name: PMS Formaldehyde Concentration - - platform: cse7761 - uart_id: uart_7 - voltage: - name: CSE7761 Voltage - current_1: - name: CSE7761 Current 1 - current_2: - name: CSE7761 Current 2 - active_power_1: - name: CSE7761 Active Power 1 - active_power_2: - name: CSE7761 Active Power 2 - - platform: cse7766 - uart_id: uart_3 - voltage: - name: CSE7766 Voltage - current: - name: CSE7766 Current - power: - name: CSE776 Power - - - platform: fingerprint_grow - fingerprint_count: - name: Fingerprint Count - status: - name: Fingerprint Status - capacity: - name: Fingerprint Capacity - security_level: - name: Fingerprint Security Level - last_finger_id: - name: Fingerprint Last Finger ID - last_confidence: - name: Fingerprint Last Confidence - - platform: sdm_meter - phase_a: - current: - name: Phase A Current - voltage: - name: Phase A Voltage - active_power: - name: Phase A Power - power_factor: - name: Phase A Power Factor - apparent_power: - name: Phase A Apparent Power - reactive_power: - name: Phase A Reactive Power - phase_angle: - name: Phase A Phase Angle - phase_b: - current: - name: Phase B Current - voltage: - name: Phase B Voltage - active_power: - name: Phase B Power - power_factor: - name: Phase B Power Factor - apparent_power: - name: Phase B Apparent Power - reactive_power: - name: Phase B Reactive Power - phase_angle: - name: Phase B Phase Angle - phase_c: - current: - name: Phase C Current - voltage: - name: Phase C Voltage - active_power: - name: Phase C Power - power_factor: - name: Phase C Power Factor - apparent_power: - name: Phase C Apparent Power - reactive_power: - name: Phase C Reactive Power - phase_angle: - name: Phase C Phase Angle - frequency: - name: Frequency - import_active_energy: - name: Import Active Energy - export_active_energy: - name: Export Active Energy - import_reactive_energy: - name: Import Reactive Energy - export_reactive_energy: - name: Export Reactive Energy - - platform: dsmr - energy_delivered_tariff1: - name: dsmr_energy_delivered_tariff1 - - - platform: nextion - id: testnumber - name: testnumber - variable_name: testnumber - - platform: nextion - id: testwave - name: testwave - component_id: 2 - wave_channel_id: 1 - - platform: smt100 - uart_id: uart_10 - counts: - name: Counts - dielectric_constant: - name: Dielectric Constant - temperature: - name: Temperature - moisture: - name: Moisture - voltage: - name: Voltage - update_interval: 60s - - - platform: vbus - model: deltasol c - temperature_1: - name: Temperature 1 - - - platform: kuntze - ph: - name: Kuntze pH - temperature: - name: Kuntze temperature - -time: - - platform: homeassistant - -binary_sensor: - - platform: daly_bms - charging_mos_enabled: - name: Charging MOS - discharging_mos_enabled: - name: Discharging MOS - - - platform: homeassistant - entity_id: binary_sensor.hello_world - id: ha_hello_world_binary - - - platform: fingerprint_grow - name: Fingerprint Enrolling - - platform: nextion - page_id: 0 - component_id: 2 - name: Nextion Component 2 Touch - - platform: nextion - id: r0_sensor - name: R0 Sensor - component_name: page0.r0 - - - platform: hydreon_rgxx - hydreon_rgxx_id: hydreon_rg9 - too_cold: - name: rg9_toocold - em_sat: - name: rg9_emsat - lens_bad: - name: rg9_lens_bad - - platform: template - id: pzemac_reset_energy - on_press: - then: - - pzemac.reset_energy: pzemac1 - - platform: template - id: pzemdc_reset_energy - on_press: - then: - - pzemdc.reset_energy: pzemdc1 - - - platform: vbus - model: deltasol_bs_plus - relay1: - name: Relay 1 On - - - platform: gpio - id: bin1 - pin: 1 - - platform: gpio - id: bin2 - pin: 2 - - platform: gpio - id: bin3 - pin: 3 - -globals: - - id: my_global_string - type: std::string - initial_value: '""' - -remote_receiver: - pin: GPIO12 - dump: [] - -status_led: - pin: GPIO2 - -text_sensor: - - platform: daly_bms - status: - name: BMS Status - - platform: version - name: ESPHome Version - icon: mdi:icon - id: version_sensor - on_value: - # yamllint disable rule:line-length - - lambda: !lambda |- - ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); - # yamllint enable rule:line-length - - script.execute: my_script - - script.wait: my_script - - script.stop: my_script - - homeassistant.service: - service: notify.html5 - data: - title: New Humidity - data_template: - message: The humidity is {{ my_variable }}%. - variables: - my_variable: |- - return id(version_sensor).state; - - platform: template - name: Template Text Sensor - lambda: |- - return {"Hello World"}; - - platform: homeassistant - entity_id: sensor.hello_world2 - id: ha_hello_world2 - - platform: nextion - name: text0 - id: text0 - update_interval: 4s - component_name: text0 - - platform: dsmr - identification: - name: dsmr_identification - p1_version: - name: dsmr_p1_version - -script: - - id: my_script - then: - - lambda: 'ESP_LOGD("main", "Hello World!");' - -switch: - - platform: gpio - id: gpio_switch1 - pin: 1 - - platform: gpio - id: gpio_switch2 - pin: 2 - - platform: gpio - id: gpio_switch3 - pin: 3 - - - platform: nextion - id: r0 - name: R0 Switch - component_name: page0.r0 - - -climate: - - platform: bang_bang - name: Bang Bang Climate - sensor: ha_hello_world - default_target_temperature_low: 18°C - default_target_temperature_high: 24°C - idle_action: - - switch.turn_on: gpio_switch1 - cool_action: - - switch.turn_on: gpio_switch2 - heat_action: - - switch.turn_on: gpio_switch1 - away_config: - default_target_temperature_low: 16°C - default_target_temperature_high: 20°C - - platform: thermostat - name: Thermostat Climate - sensor: ha_hello_world - preset: - - name: Default Preset - default_target_temperature_low: 18°C - default_target_temperature_high: 24°C - - name: Away - default_target_temperature_low: 16°C - default_target_temperature_high: 20°C - idle_action: - - switch.turn_on: gpio_switch1 - cool_action: - - switch.turn_on: gpio_switch2 - supplemental_cooling_action: - - switch.turn_on: gpio_switch3 - heat_action: - - switch.turn_on: gpio_switch1 - supplemental_heating_action: - - switch.turn_on: gpio_switch3 - dry_action: - - switch.turn_on: gpio_switch2 - fan_only_action: - - switch.turn_on: gpio_switch1 - auto_mode: - - switch.turn_on: gpio_switch2 - off_mode: - - switch.turn_on: gpio_switch1 - heat_mode: - - switch.turn_on: gpio_switch2 - cool_mode: - - switch.turn_on: gpio_switch1 - dry_mode: - - switch.turn_on: gpio_switch2 - fan_only_mode: - - switch.turn_on: gpio_switch1 - fan_mode_auto_action: - - switch.turn_on: gpio_switch2 - fan_mode_on_action: - - switch.turn_on: gpio_switch1 - fan_mode_off_action: - - switch.turn_on: gpio_switch2 - fan_mode_low_action: - - switch.turn_on: gpio_switch1 - fan_mode_medium_action: - - switch.turn_on: gpio_switch2 - fan_mode_high_action: - - switch.turn_on: gpio_switch1 - fan_mode_middle_action: - - switch.turn_on: gpio_switch2 - fan_mode_focus_action: - - switch.turn_on: gpio_switch1 - fan_mode_diffuse_action: - - switch.turn_on: gpio_switch2 - fan_mode_quiet_action: - - switch.turn_on: gpio_switch1 - swing_off_action: - - switch.turn_on: gpio_switch2 - swing_horizontal_action: - - switch.turn_on: gpio_switch1 - swing_vertical_action: - - switch.turn_on: gpio_switch2 - swing_both_action: - - switch.turn_on: gpio_switch1 - startup_delay: true - supplemental_cooling_delta: 2.0 - cool_deadband: 0.5 - cool_overrun: 0.5 - min_cooling_off_time: 300s - min_cooling_run_time: 300s - max_cooling_run_time: 600s - supplemental_heating_delta: 2.0 - heat_deadband: 0.5 - heat_overrun: 0.5 - min_heating_off_time: 300s - min_heating_run_time: 300s - max_heating_run_time: 600s - min_fanning_off_time: 30s - min_fanning_run_time: 30s - min_fan_mode_switching_time: 15s - min_idle_time: 30s - set_point_minimum_differential: 0.5 - fan_only_action_uses_fan_mode_timer: true - fan_only_cooling: true - fan_with_cooling: true - fan_with_heating: true - - platform: pid - id: pid_climate - name: PID Climate Controller - sensor: ha_hello_world - default_target_temperature: 21°C - heat_output: my_slow_pwm - control_parameters: - kp: 0.0 - ki: 0.0 - kd: 0.0 - max_integral: 0.0 - output_averaging_samples: 1 - derivative_averaging_samples: 1 - deadband_parameters: - threshold_high: 0.4 - threshold_low: -2.0 - kp_multiplier: 0.0 - ki_multiplier: 0.0 - kd_multiplier: 0.0 - deadband_output_averaging_samples: 1 - - platform: haier - name: Haier AC - supported_swing_modes: - - vertical - - horizontal - - both - update_interval: 10s - uart_id: uart_12 - -sprinkler: - - id: yard_sprinkler_ctrlr - main_switch: Yard Sprinklers - auto_advance_switch: Yard Sprinklers Auto Advance - reverse_switch: Yard Sprinklers Reverse - pump_start_pump_delay: 2s - pump_stop_valve_delay: 4s - pump_switch_off_during_valve_open_delay: true - valve_open_delay: 5s - valves: - - valve_switch: Yard Valve 0 - enable_switch: Enable Yard Valve 0 - pump_switch_id: gpio_switch1 - run_duration: 10s - valve_switch_id: gpio_switch2 - - valve_switch: Yard Valve 1 - enable_switch: Enable Yard Valve 1 - pump_switch_id: gpio_switch1 - run_duration: 10s - valve_switch_id: gpio_switch2 - - valve_switch: Yard Valve 2 - enable_switch: Enable Yard Valve 2 - pump_switch_id: gpio_switch1 - run_duration: 10s - valve_switch_id: gpio_switch2 - - id: garden_sprinkler_ctrlr - main_switch: Garden Sprinklers - auto_advance_switch: Garden Sprinklers Auto Advance - reverse_switch: Garden Sprinklers Reverse - valve_overlap: 5s - valves: - - valve_switch: Garden Valve 0 - enable_switch: Enable Garden Valve 0 - pump_switch_id: gpio_switch1 - run_duration: 10s - valve_switch_id: gpio_switch2 - - valve_switch: Garden Valve 1 - enable_switch: Enable Garden Valve 1 - pump_switch_id: gpio_switch1 - run_duration: 10s - valve_switch_id: gpio_switch2 - -output: - - platform: esp8266_pwm - id: out - pin: D3 - frequency: 50Hz - - platform: esp8266_pwm - id: out2 - pin: D4 - - platform: slow_pwm - pin: GPIO5 - id: my_slow_pwm - period: 15s - restart_cycle_on_state_change: false - -e131: - -light: - - platform: neopixelbus - name: Neopixelbus Light - pin: GPIO1 - type: GRBW - variant: SK6812 - method: ESP8266_UART0 - num_leds: 100 - effects: - - wled: - - adalight: - uart_id: uart_3 - - e131: - universe: 1 - - platform: hbridge - name: Icicle Lights - pin_a: out - pin_b: out2 - - platform: sonoff_d1 - uart_id: uart_2 - use_rm433_remote: false - name: Sonoff D1 Dimmer - id: d1_light - restore_mode: RESTORE_DEFAULT_OFF - - platform: shelly_dimmer - name: "Shelly Dimmer Light" - power: - name: "Shelly Dimmer Power" - voltage: - name: "Shelly Dimmer Voltage" - current: - name: "Shelly Dimmer Current" - max_brightness: 500 - firmware: "51.6" - uart_id: uart_11 - -sim800l: - uart_id: uart_4 - on_sms_received: - - lambda: |- - std::string str; - str = sender; - str = message; - - sim800l.send_sms: - message: hello you - recipient: "+1234" - - sim800l.dial: - recipient: "+1234" - -dfplayer: - uart_id: uart_5 - on_finished_playback: - then: - if: - condition: - not: dfplayer.is_playing - then: - logger.log: Playback finished event -tm1651: - id: tm1651_battery - clk_pin: D6 - dio_pin: D5 - -rf_bridge: - uart_id: uart_5 - on_code_received: - - lambda: |- - uint32_t test; - test = data.sync; - test = data.low; - test = data.high; - test = data.code; - - rf_bridge.send_code: - sync: 0x1234 - low: 0x1234 - high: 0x1234 - code: 0x123456 - - rf_bridge.learn - - on_advanced_code_received: - - lambda: |- - uint32_t test; - std::string test_code; - test = data.length; - test = data.protocol; - test_code = data.code; - - rf_bridge.start_advanced_sniffing: - - rf_bridge.stop_advanced_sniffing: - - rf_bridge.send_advanced_code: - length: 0x04 - protocol: 0x01 - code: "ABC123" - - rf_bridge.send_raw: - raw: "AAA5070008001000ABC12355" - - -display: - - platform: nextion - uart_id: uart_1 - tft_url: http://esphome.io/default35.tft - update_interval: 5s - on_sleep: - then: - lambda: 'ESP_LOGD("display","Display went to sleep");' - on_wake: - then: - lambda: 'ESP_LOGD("display","Display woke up");' - on_setup: - then: - lambda: 'ESP_LOGD("display","Display setup completed");' - on_page: - then: - lambda: 'ESP_LOGD("display","Display shows new page %u", x);' - -fingerprint_grow: - sensing_pin: 4 - password: 0x12FE37DC - new_password: 0xA65B9840 - on_finger_scan_matched: - - homeassistant.event: - event: esphome.${device_name}_fingerprint_grow_finger_scan_matched - data: - finger_id: !lambda "return finger_id;" - confidence: !lambda "return confidence;" - on_finger_scan_unmatched: - - homeassistant.event: - event: esphome.${device_name}_fingerprint_grow_finger_scan_unmatched - on_enrollment_scan: - - homeassistant.event: - event: esphome.${device_name}_fingerprint_grow_enrollment_scan - data: - finger_id: !lambda "return finger_id;" - scan_num: !lambda "return scan_num;" - on_enrollment_done: - - homeassistant.event: - event: esphome.${device_name}_fingerprint_grow_node_enrollment_done - data: - finger_id: !lambda "return finger_id;" - on_enrollment_failed: - - homeassistant.event: - event: esphome.${device_name}_fingerprint_grow_enrollment_failed - data: - finger_id: !lambda "return finger_id;" - uart_id: uart_6 - -dsmr: - decryption_key: 00112233445566778899aabbccddeeff - uart_id: uart_6 - max_telegram_length: 1000 - request_pin: D5 - request_interval: 20s - receive_timeout: 100ms - -daly_bms: - update_interval: 20s - uart_id: uart_1 - -qr_code: - - id: homepage_qr - value: https://esphome.io/index.html diff --git a/tests/test4.yaml b/tests/test4.yaml deleted file mode 100644 index 8e76a5fd66..0000000000 --- a/tests/test4.yaml +++ /dev/null @@ -1,747 +0,0 @@ ---- -esphome: - name: $devicename - platform: ESP32 - board: nodemcu-32s - build_path: build/test4 - -substitutions: - devicename: test-4 - -ethernet: - type: LAN8720 - mdc_pin: GPIO23 - mdio_pin: GPIO25 - clk_mode: GPIO0_IN - phy_addr: 0 - power_pin: GPIO25 - manual_ip: - static_ip: 192.168.178.56 - gateway: 192.168.178.1 - subnet: 255.255.255.0 - domain: .local - -api: - -i2c: - sda: 21 - scl: 22 - scan: false - -spi: - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 - -uart: - tx_pin: GPIO22 - rx_pin: GPIO23 - baud_rate: 115200 - -ota: - safe_mode: true - port: 3286 - -logger: - level: DEBUG - -web_server: - ota: false - auth: - username: admin - password: admin - include_internal: true - -time: - - platform: sntp - id: sntp_time - -tuya: - time_id: sntp_time - status_pin: - number: 14 - inverted: true - -select: - - platform: tuya - id: tuya_select - enum_datapoint: 42 - options: - 0: Internal - 1: Floor - 2: Both - -pipsolar: - id: inverter0 - -sx1509: - - id: sx1509_hub - address: 0x3E - -mcp3204: - cs_pin: GPIO23 - -dac7678: - address: 0x4A - id: dac7678_hub1 - internal_reference: true - -sensor: - - platform: homeassistant - entity_id: sensor.hello_world - id: ha_hello_world - - platform: tuya - id: tuya_sensor - sensor_datapoint: 1 - - platform: pipsolar - pipsolar_id: inverter0 - grid_rating_voltage: - id: inverter0_grid_rating_voltage - name: inverter0_grid_rating_voltage - grid_rating_current: - id: inverter0_grid_rating_current - name: inverter0_grid_rating_current - ac_output_rating_voltage: - id: inverter0_ac_output_rating_voltage - name: inverter0_ac_output_rating_voltage - ac_output_rating_frequency: - id: inverter0_ac_output_rating_frequency - name: inverter0_ac_output_rating_frequency - ac_output_rating_current: - id: inverter0_ac_output_rating_current - name: inverter0_ac_output_rating_current - ac_output_rating_apparent_power: - id: inverter0_ac_output_rating_apparent_power - name: inverter0_ac_output_rating_apparent_power - ac_output_rating_active_power: - id: inverter0_ac_output_rating_active_power - name: inverter0_ac_output_rating_active_power - battery_rating_voltage: - id: inverter0_battery_rating_voltage - name: inverter0_battery_rating_voltage - battery_recharge_voltage: - id: inverter0_battery_recharge_voltage - name: inverter0_battery_recharge_voltage - battery_under_voltage: - id: inverter0_battery_under_voltage - name: inverter0_battery_under_voltage - battery_bulk_voltage: - id: inverter0_battery_bulk_voltage - name: inverter0_battery_bulk_voltage - battery_float_voltage: - id: inverter0_battery_float_voltage - name: inverter0_battery_float_voltage - battery_type: - id: inverter0_battery_type - name: inverter0_battery_type - current_max_ac_charging_current: - id: inverter0_current_max_ac_charging_current - name: inverter0_current_max_ac_charging_current - current_max_charging_current: - id: inverter0_current_max_charging_current - name: inverter0_current_max_charging_current - input_voltage_range: - id: inverter0_input_voltage_range - name: inverter0_input_voltage_range - output_source_priority: - id: inverter0_output_source_priority - name: inverter0_output_source_priority - charger_source_priority: - id: inverter0_charger_source_priority - name: inverter0_charger_source_priority - parallel_max_num: - id: inverter0_parallel_max_num - name: inverter0_parallel_max_num - machine_type: - id: inverter0_machine_type - name: inverter0_machine_type - topology: - id: inverter0_topology - name: inverter0_topology - output_mode: - id: inverter0_output_mode - name: inverter0_output_mode - battery_redischarge_voltage: - id: inverter0_battery_redischarge_voltage - name: inverter0_battery_redischarge_voltage - pv_ok_condition_for_parallel: - id: inverter0_pv_ok_condition_for_parallel - name: inverter0_pv_ok_condition_for_parallel - pv_power_balance: - id: inverter0_pv_power_balance - name: inverter0_pv_power_balance - grid_voltage: - id: inverter0_grid_voltage - name: inverter0_grid_voltage - grid_frequency: - id: inverter0_grid_frequency - name: inverter0_grid_frequency - ac_output_voltage: - id: inverter0_ac_output_voltage - name: inverter0_ac_output_voltage - ac_output_frequency: - id: inverter0_ac_output_frequency - name: inverter0_ac_output_frequency - ac_output_apparent_power: - id: inverter0_ac_output_apparent_power - name: inverter0_ac_output_apparent_power - ac_output_active_power: - id: inverter0_ac_output_active_power - name: inverter0_ac_output_active_power - output_load_percent: - id: inverter0_output_load_percent - name: inverter0_output_load_percent - bus_voltage: - id: inverter0_bus_voltage - name: inverter0_bus_voltage - battery_voltage: - id: inverter0_battery_voltage - name: inverter0_battery_voltage - battery_charging_current: - id: inverter0_battery_charging_current - name: inverter0_battery_charging_current - battery_capacity_percent: - id: inverter0_battery_capacity_percent - name: inverter0_battery_capacity_percent - inverter_heat_sink_temperature: - id: inverter0_inverter_heat_sink_temperature - name: inverter0_inverter_heat_sink_temperature - pv_input_current_for_battery: - id: inverter0_pv_input_current_for_battery - name: inverter0_pv_input_current_for_battery - pv_input_voltage: - id: inverter0_pv_input_voltage - name: inverter0_pv_input_voltage - battery_voltage_scc: - id: inverter0_battery_voltage_scc - name: inverter0_battery_voltage_scc - battery_discharge_current: - id: inverter0_battery_discharge_current - name: inverter0_battery_discharge_current - battery_voltage_offset_for_fans_on: - id: inverter0_battery_voltage_offset_for_fans_on - name: inverter0_battery_voltage_offset_for_fans_on - eeprom_version: - id: inverter0_eeprom_version - name: inverter0_eeprom_version - pv_charging_power: - id: inverter0_pv_charging_power - name: inverter0_pv_charging_power - - platform: hrxl_maxsonar_wr - name: Rainwater Tank Level - filters: - - sliding_window_moving_average: - window_size: 12 - send_every: 12 - - or: - - throttle: 20min - - delta: 0.02 - - platform: mcp3204 - name: MCP3204 Pin 1 - number: 1 - id: mcp_sensor - - platform: copy - source_id: mcp_sensor - name: MCP binary sensor copy - - platform: ufire_ec - id: ufire_ec_board - temperature: - name: Ufire Temperature - ec: - name: Ufire EC - temperature_compensation: 20.0 - temperature_coefficient: 0.019 - - platform: ufire_ise - id: ufire_ise_board - temperature: - name: Ufire Temperature - ph: - name: Ufire pH - -# -# platform sensor.apds9960 requires component apds9960 -# -# - platform: apds9960 -# type: proximity -# name: APDS9960 Proximity -# - platform: apds9960 -# type: clear -# name: APDS9960 Clear -# - platform: apds9960 -# type: red -# name: APDS9960 Red -# - platform: apds9960 -# type: green -# name: APDS9960 Green -# - platform: apds9960 -# type: blue -# name: APDS9960 Blue - -binary_sensor: - - platform: tuya - id: tuya_binary_sensor - sensor_datapoint: 1 - - platform: pipsolar - pipsolar_id: inverter0 - add_sbu_priority_version: - id: inverter0_add_sbu_priority_version - name: inverter0_add_sbu_priority_version - configuration_status: - id: inverter0_configuration_status - name: inverter0_configuration_status - scc_firmware_version: - id: inverter0_scc_firmware_version - name: inverter0_scc_firmware_version - load_status: - id: inverter0_load_status - name: inverter0_load_status - battery_voltage_to_steady_while_charging: - id: inverter0_battery_voltage_to_steady_while_charging - name: inverter0_battery_voltage_to_steady_while_charging - charging_status: - id: inverter0_charging_status - name: inverter0_charging_status - scc_charging_status: - id: inverter0_scc_charging_status - name: inverter0_scc_charging_status - ac_charging_status: - id: inverter0_ac_charging_status - name: inverter0_ac_charging_status - charging_to_floating_mode: - id: inverter0_charging_to_floating_mode - name: inverter0_charging_to_floating_mode - switch_on: - id: inverter0_switch_on - name: inverter0_switch_on - dustproof_installed: - id: inverter0_dustproof_installed - name: inverter0_dustproof_installed - silence_buzzer_open_buzzer: - id: inverter0_silence_buzzer_open_buzzer - name: inverter0_silence_buzzer_open_buzzer - overload_bypass_function: - id: inverter0_overload_bypass_function - name: inverter0_overload_bypass_function - lcd_escape_to_default: - id: inverter0_lcd_escape_to_default - name: inverter0_lcd_escape_to_default - overload_restart_function: - id: inverter0_overload_restart_function - name: inverter0_overload_restart_function - over_temperature_restart_function: - id: inverter0_over_temperature_restart_function - name: inverter0_over_temperature_restart_function - backlight_on: - id: inverter0_backlight_on - name: inverter0_backlight_on - - platform: template - id: ar1 - lambda: "return {};" - filters: - - autorepeat: - - delay: 2s - time_off: 100ms - time_on: 900ms - - delay: 4s - time_off: 100ms - time_on: 400ms - on_state: - then: - - lambda: 'ESP_LOGI("ar1:", "%d", x);' - - platform: touchscreen - touchscreen_id: xpt_touchscreen - id: touch_key0 - x_min: 80 - x_max: 160 - y_min: 106 - y_max: 212 - on_press: - - logger.log: Touched - - - platform: gpio - name: GPIO SX1509 test - pin: - sx1509: sx1509_hub - number: 3 - - - platform: touchscreen - touchscreen_id: lilygo_touchscreen - id: touch_key1 - x_min: 0 - x_max: 100 - y_min: 0 - y_max: 100 - on_press: - - logger.log: Touched - - - platform: gpio - name: MaxIn Pin 4 - pin: - max6956: max6956_1 - number: 4 - mode: - input: true - pullup: true - inverted: false - -climate: - - platform: tuya - id: tuya_climate - switch_datapoint: 1 - target_temperature_datapoint: 3 - current_temperature_multiplier: 0.5 - target_temperature_multiplier: 0.5 - reports_fahrenheit: true - -switch: - - platform: tuya - id: tuya_switch - switch_datapoint: 1 - - platform: pipsolar - pipsolar_id: inverter0 - output_source_priority_utility: - name: inverter0_output_source_priority_utility - output_source_priority_solar: - name: inverter0_output_source_priority_solar - output_source_priority_battery: - name: inverter0_output_source_priority_battery - input_voltage_range: - name: inverter0_input_voltage_range - pv_ok_condition_for_parallel: - name: inverter0_pv_ok_condition_for_parallel - pv_power_balance: - name: inverter0_pv_power_balance - - platform: copy - source_id: tuya_switch - name: Tuya Switch Copy - -light: - - platform: fastled_clockless - id: led_matrix_32x8 - name: led_matrix_32x8 - chipset: WS2812B - pin: GPIO15 - num_leds: 256 - rgb_order: GRB - default_transition_length: 0s - color_correct: [50%, 50%, 50%] - - platform: tuya - id: tuya_light - switch_datapoint: 1 - dimmer_datapoint: 2 - min_value_datapoint: 3 - color_temperature_datapoint: 4 - min_value: 1 - max_value: 100 - cold_white_color_temperature: 153 mireds - warm_white_color_temperature: 500 mireds - gamma_correct: 1 - -cover: - - platform: tuya - id: tuya_cover - position_datapoint: 2 - - platform: copy - source_id: tuya_cover - name: Tuya Cover copy - -display: - - platform: addressable_light - id: led_matrix_32x8_display - addressable_light_id: led_matrix_32x8 - width: 32 - height: 8 - pixel_mapper: |- - if (x % 2 == 0) { - return (x * 8) + y; - } - return (x * 8) + (7 - y); - lambda: |- - Color red = Color(0xFF0000); - Color green = Color(0x00FF00); - Color blue = Color(0x0000FF); - it.rectangle(0, 0, it.get_width(), it.get_height(), red); - it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); - it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); - it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); - rotation: 0° - update_interval: 16ms - - - platform: waveshare_epaper - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 - model: 2.13in-ttgo-b1 - full_update_every: 30 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: waveshare_epaper - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 - model: 2.90in - full_update_every: 30 - reset_duration: 200ms - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: waveshare_epaper - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 - model: 2.90inv2 - full_update_every: 30 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: inkplate6 - id: inkplate_display - greyscale: false - partial_updating: false - update_interval: 60s - - ckv_pin: GPIO1 - sph_pin: GPIO1 - gmod_pin: GPIO1 - gpio0_enable_pin: GPIO1 - oe_pin: GPIO1 - spv_pin: GPIO1 - powerup_pin: GPIO1 - wakeup_pin: GPIO1 - vcom_pin: GPIO1 - -number: - - platform: tuya - id: tuya_number - number_datapoint: 102 - min_value: 0 - max_value: 17 - step: 1 - - platform: copy - source_id: tuya_number - name: Tuya Number Copy - -text_sensor: - - platform: pipsolar - pipsolar_id: inverter0 - device_mode: - id: inverter0_device_mode - name: inverter0_device_mode - last_qpigs: - id: inverter0_last_qpigs - name: inverter0_last_qpigs - last_qpiri: - id: inverter0_last_qpiri - name: inverter0_last_qpiri - last_qmod: - id: inverter0_last_qmod - name: inverter0_last_qmod - last_qflag: - id: inverter0_last_qflag - name: inverter0_last_qflag - - platform: copy - source_id: inverter0_device_mode - name: Inverter Text Sensor Copy - - platform: ethernet_info - ip_address: - name: IP Address - -output: - - platform: pipsolar - pipsolar_id: inverter0 - battery_recharge_voltage: - id: inverter0_battery_recharge_voltage_out - - platform: dac7678 - dac7678_id: dac7678_hub1 - channel: 0 - id: dac7678_1_ch0 - - platform: dac7678 - dac7678_id: dac7678_hub1 - channel: 1 - id: dac7678_1_ch1 - - platform: dac7678 - dac7678_id: dac7678_hub1 - channel: 2 - id: dac7678_1_ch2 - - platform: dac7678 - dac7678_id: dac7678_hub1 - channel: 3 - id: dac7678_1_ch3 - - platform: dac7678 - dac7678_id: dac7678_hub1 - channel: 4 - id: dac7678_1_ch4 - - platform: dac7678 - dac7678_id: dac7678_hub1 - channel: 5 - id: dac7678_1_ch5 - - platform: dac7678 - dac7678_id: dac7678_hub1 - channel: 6 - id: dac7678_1_ch6 - - platform: dac7678 - dac7678_id: dac7678_hub1 - channel: 7 - id: dac7678_1_ch7 -esp32_camera: - name: ESP-32 Camera - data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19] - vsync_pin: GPIO22 - href_pin: GPIO26 - pixel_clock_pin: GPIO21 - external_clock: - pin: GPIO27 - frequency: 20MHz - i2c_pins: - sda: GPIO25 - scl: GPIO23 - reset_pin: GPIO15 - power_down_pin: GPIO1 - resolution: 640x480 - jpeg_quality: 10 - -esp32_camera_web_server: - - port: 8080 - mode: stream - - port: 8081 - mode: snapshot - -external_components: - - source: github://esphome/esphome@dev - refresh: 1d - components: [bh1750] - - source: ../esphome/components - components: [sntp] - -button: - - platform: restart - name: Restart Button - - platform: safe_mode - name: Safe Mode Button - - platform: shutdown - name: Shutdown Button - id: shutdown_btn - - platform: copy - source_id: shutdown_btn - name: Shutdown Button Copy - -touchscreen: - - platform: ektf2232 - interrupt_pin: GPIO36 - rts_pin: GPIO5 - display: inkplate_display - on_touch: - - logger.log: - format: Touch at (%d, %d) - args: [touch.x, touch.y] - - - platform: xpt2046 - id: xpt_touchscreen - cs_pin: 17 - interrupt_pin: 16 - display: inkplate_display - update_interval: 50ms - report_interval: 1s - threshold: 400 - calibration_x_min: 3860 - calibration_x_max: 280 - calibration_y_min: 340 - calibration_y_max: 3860 - swap_x_y: false - on_touch: - - logger.log: - format: Touch at (%d, %d) - args: [touch.x, touch.y] - - - platform: lilygo_t5_47 - id: lilygo_touchscreen - interrupt_pin: GPIO36 - display: inkplate_display - on_touch: - - logger.log: - format: Touch at (%d, %d) - args: [touch.x, touch.y] - -i2s_audio: - i2s_lrclk_pin: GPIO26 - i2s_bclk_pin: GPIO27 - i2s_mclk_pin: GPIO25 - -media_player: - - platform: i2s_audio - name: None - dac_type: external - i2s_dout_pin: GPIO25 - mute_pin: GPIO14 - on_state: - - media_player.play: - - media_player.play_media: http://localhost/media.mp3 - - media_player.play_media: !lambda 'return "http://localhost/media.mp3";' - on_idle: - - media_player.pause: - on_play: - - media_player.stop: - on_pause: - - media_player.toggle: - - wait_until: - media_player.is_idle: - - wait_until: - media_player.is_playing: - - media_player.volume_up: - - media_player.volume_down: - - media_player.volume_set: 50% - -prometheus: - include_internal: true - relabel: - ha_hello_world: - id: hellow_world - name: Hello World - -microphone: - - platform: i2s_audio - id: mic_id_adc - adc_pin: GPIO35 - adc_type: internal - - - platform: i2s_audio - id: mic_id_external - i2s_din_pin: GPIO23 - adc_type: external - pdm: false - -speaker: - - platform: i2s_audio - id: speaker_id - dac_type: external - i2s_dout_pin: GPIO25 - mode: mono - - -voice_assistant: - microphone: mic_id_external - on_start: - - logger.log: "Voice assistant started" - on_stt_end: - - logger.log: - format: "Voice assistant STT ended with result %s" - args: [x.c_str()] - on_tts_start: - - logger.log: - format: "Voice assistant TTS started with text %s" - args: [x.c_str()] - on_tts_end: - - logger.log: - format: "Voice assistant TTS ended with url %s" - args: [x.c_str()] - on_end: - - logger.log: "Voice assistant ended" - on_error: - - logger.log: - format: "Voice assistant error - code %s, message: %s" - args: [code.c_str(), message.c_str()] - -max6956: - - id: max6956_1 - address: 0x40 diff --git a/tests/test5.yaml b/tests/test5.yaml deleted file mode 100644 index cb4b559b06..0000000000 --- a/tests/test5.yaml +++ /dev/null @@ -1,666 +0,0 @@ ---- -esphome: - name: test5 - build_path: build/test5 - project: - name: esphome.test5_project - version: "1.0.0" - -esp32: - board: nodemcu-32s - framework: - type: esp-idf - advanced: - ignore_efuse_mac_crc: true - -wifi: - networks: - - ssid: "MySSID" - password: "password1" - manual_ip: - static_ip: 192.168.1.23 - gateway: 192.168.1.1 - subnet: 255.255.255.0 - -api: - -ota: - -logger: - -uart: - - id: uart_1 - tx_pin: 1 - rx_pin: 3 - baud_rate: 9600 - - id: uart_2 - tx_pin: 17 - rx_pin: 16 - baud_rate: 19200 - -i2c: - frequency: 100khz - -modbus: - uart_id: uart_1 - flow_control_pin: 5 - id: mod_bus1 - -modbus_controller: - - id: modbus_controller_test - address: 0x2 - modbus_id: mod_bus1 - -mqtt: - broker: test.mosquitto.org - port: 1883 - discovery: true - discovery_prefix: homeassistant - idf_send_async: false - on_message: - topic: testing/sensor/testing_sensor/state - qos: 0 - then: - # yamllint disable rule:line-length - - lambda: |- - ESP_LOGD("Mqtt Test", "testing/sensor/testing_sensor/state=[%s]", x.c_str()); - # yamllint enable rule:line-length - -vbus: - - uart_id: uart_2 - -binary_sensor: - - platform: gpio - pin: GPIO0 - id: io0_button - icon: mdi:gesture-tap-button - - - platform: modbus_controller - modbus_controller_id: modbus_controller_test - id: modbus_binsensortest - register_type: read - address: 0x3200 - bitmask: 0x80 # (bit 8) - lambda: "return x;" - - - platform: tm1638 - id: Button0 - key: 0 - filters: - - delayed_on: 10ms - on_press: - then: - - switch.turn_on: Led0 - on_release: - then: - - switch.turn_off: Led0 - - - platform: tm1638 - id: Button1 - key: 1 - on_press: - then: - - switch.turn_on: Led1 - on_release: - then: - - switch.turn_off: Led1 - - - platform: tm1638 - id: Button2 - key: 2 - on_press: - then: - - switch.turn_on: Led2 - on_release: - then: - - switch.turn_off: Led2 - - - platform: tm1638 - id: Button3 - key: 3 - on_press: - then: - - switch.turn_on: Led3 - on_release: - then: - - switch.turn_off: Led3 - - - platform: tm1638 - id: Button4 - key: 4 - on_press: - then: - - output.turn_on: Led4 - on_release: - then: - - output.turn_off: Led4 - - - platform: tm1638 - id: Button5 - key: 5 - on_press: - then: - - output.turn_on: Led5 - on_release: - then: - - output.turn_off: Led5 - - - platform: tm1638 - id: Button6 - key: 6 - on_press: - then: - - output.turn_on: Led6 - on_release: - then: - - output.turn_off: Led6 - - - platform: tm1638 - id: Button7 - key: 7 - on_press: - then: - - output.turn_on: Led7 - on_release: - then: - - output.turn_off: Led7 - - - platform: gpio - id: sn74hc165_pin_0 - pin: - sn74hc165: sn74hc165_hub - number: 0 - - - platform: ezo_pmp - pump_state: - name: "Pump State" - is_paused: - name: "Is Paused" - - - platform: matrix_keypad - keypad_id: keypad - id: key4 - row: 1 - col: 1 - - platform: matrix_keypad - id: key1 - key: 1 - - - platform: vbus - model: deltasol_bs_plus - relay2: - name: Relay 2 On - sensor1_error: - name: Sensor 1 Error - - - platform: vbus - model: custom - command: 0x100 - source: 0x1234 - dest: 0x10 - binary_sensors: - - id: vcustom_b - name: VBus Custom Binary Sensor - lambda: return x[0] & 1; - -tlc5947: - data_pin: GPIO12 - clock_pin: GPIO14 - lat_pin: GPIO15 - -gp8403: - - id: gp8403_5v - voltage: 5V - - id: gp8403_10v - voltage: 10V - -output: - - platform: gpio - pin: GPIO2 - id: built_in_led - - - platform: tlc5947 - id: output_red - channel: 0 - max_power: 0.8 - - - platform: mcp47a1 - id: output_mcp47a1 - - - platform: modbus_controller - modbus_controller_id: modbus_controller_test - id: modbus_output_test - lambda: |- - return x * 1.0 ; - address: 0x9001 - value_type: U_WORD - - - platform: tm1638 - id: Led4 - led: 4 - - - platform: tm1638 - id: Led5 - led: 5 - - - platform: tm1638 - id: Led6 - led: 6 - - - platform: tm1638 - id: Led7 - led: 7 - - - platform: gp8403 - id: gp8403_output_0 - gp8403_id: gp8403_5v - channel: 0 - - platform: gp8403 - gp8403_id: gp8403_10v - id: gp8403_output_1 - channel: 1 - -demo: - -esp32_ble: - -esp32_ble_server: - manufacturer: ESPHome - model: Test5 - -esp32_improv: - authorizer: io0_button - authorized_duration: 1min - status_indicator: built_in_led - -ezo_pmp: - id: hcl_pump - update_interval: 1s - -number: - - platform: template - name: My template number - id: template_number_id - optimistic: true - max_value: 100 - min_value: 0 - step: 5 - unit_of_measurement: "%" - mode: slider - device_class: humidity - on_value: - - logger.log: - format: Number changed to %f - args: [x] - set_action: - - logger.log: - format: Template Number set to %f - args: [x] - - number.set: - id: template_number_id - value: 50 - - number.to_min: template_number_id - - number.to_min: - id: template_number_id - - number.to_max: template_number_id - - number.to_max: - id: template_number_id - - number.increment: template_number_id - - number.increment: - id: template_number_id - cycle: false - - number.decrement: template_number_id - - number.decrement: - id: template_number_id - cycle: false - - number.operation: - id: template_number_id - operation: Increment - cycle: false - - number.operation: - id: template_number_id - operation: !lambda "return NUMBER_OP_INCREMENT;" - cycle: !lambda "return false;" - - - id: modbus_numbertest - platform: modbus_controller - modbus_controller_id: modbus_controller_test - name: ModbusNumber - address: 0x9002 - value_type: U_WORD - lambda: "return x * 1.0;" - write_lambda: |- - return x * 1.0 ; - multiply: 1.0 - -select: - - platform: template - name: My template select - id: template_select_id - optimistic: true - initial_option: two - restore_value: true - on_value: - - logger.log: - format: Select changed to %s (index %d)" - args: ["x.c_str()", "i"] - set_action: - - logger.log: - format: Template Select set to %s - args: ["x.c_str()"] - - select.set: - id: template_select_id - option: two - - select.first: template_select_id - - select.last: - id: template_select_id - - select.previous: template_select_id - - select.next: - id: template_select_id - cycle: false - - select.operation: - id: template_select_id - operation: Previous - cycle: false - - select.operation: - id: template_select_id - operation: !lambda "return SELECT_OP_PREVIOUS;" - cycle: !lambda "return true;" - - select.set_index: - id: template_select_id - index: 1 - - select.set_index: - id: template_select_id - index: !lambda "return 1 + 1;" - options: - - one - - two - - three - - - platform: modbus_controller - name: Modbus Select Register 1000 - address: 1000 - value_type: U_WORD - optionsmap: - "Zero": 0 - "One": 1 - "Two": 2 - "Three": 3 - -sensor: - - platform: internal_temperature - name: Internal Temperature - - platform: selec_meter - total_active_energy: - name: SelecEM2M Total Active Energy - import_active_energy: - name: SelecEM2M Import Active Energy - export_active_energy: - name: SelecEM2M Export Active Energy - total_reactive_energy: - name: SelecEM2M Total Reactive Energy - import_reactive_energy: - name: SelecEM2M Import Reactive Energy - export_reactive_energy: - name: SelecEM2M Export Reactive Energy - apparent_energy: - name: SelecEM2M Apparent Energy - active_power: - name: SelecEM2M Active Power - reactive_power: - name: SelecEM2M Reactive Power - apparent_power: - name: SelecEM2M Apparent Power - voltage: - name: SelecEM2M Voltage - current: - name: SelecEM2M Current - power_factor: - name: SelecEM2M Power Factor - frequency: - name: SelecEM2M Frequency - maximum_demand_active_power: - name: SelecEM2M Maximum Demand Active Power - disabled_by_default: true - maximum_demand_reactive_power: - name: SelecEM2M Maximum Demand Reactive Power - disabled_by_default: true - maximum_demand_apparent_power: - name: SelecEM2M Maximum Demand Apparent Power - disabled_by_default: true - - - id: modbus_sensortest - platform: modbus_controller - modbus_controller_id: modbus_controller_test - address: 0x331A - register_type: read - value_type: U_WORD - - - platform: t6615 - uart_id: uart_2 - co2: - name: CO2 Sensor - - - platform: bmp3xx - temperature: - name: BMP Temperature - oversampling: 16x - pressure: - name: BMP Pressure - address: 0x77 - iir_filter: 2X - - - platform: sen5x - id: sen54 - temperature: - name: Temperature - accuracy_decimals: 1 - humidity: - name: Humidity - accuracy_decimals: 0 - pm_1_0: - name: PM <1µm Weight concentration - id: pm_1_0 - accuracy_decimals: 1 - pm_2_5: - name: PM <2.5µm Weight concentration - id: pm_2_5 - accuracy_decimals: 1 - pm_4_0: - name: PM <4µm Weight concentration - id: pm_4_0 - accuracy_decimals: 1 - pm_10_0: - name: PM <10µm Weight concentration - id: pm_10_0 - accuracy_decimals: 1 - nox: - name: NOx - voc: - name: VOC - algorithm_tuning: - index_offset: 100 - learning_time_offset_hours: 12 - learning_time_gain_hours: 12 - gating_max_duration_minutes: 180 - std_initial: 50 - gain_factor: 230 - temperature_compensation: - offset: 0 - normalized_offset_slope: 0 - time_constant: 0 - auto_cleaning_interval: 604800s - acceleration_mode: low - store_baseline: true - address: 0x69 - - platform: mcp9600 - thermocouple_type: K - hot_junction: - name: Thermocouple Temperature - cold_junction: - name: Ambient Temperature - - - platform: ezo_pmp - current_volume_dosed: - name: Current Volume Dosed - total_volume_dosed: - name: Total Volume Dosed - absolute_total_volume_dosed: - name: Absolute Total Volume Dosed - pump_voltage: - name: Pump Voltage - last_volume_requested: - name: Last Volume Requested - max_flow_rate: - name: Max Flow Rate - - - platform: vbus - model: deltasol c - temperature_3: - name: Temperature 3 - operating_hours_1: - name: Operating Hours 1 - heat_quantity: - name: Heat Quantity - time: - name: System Time - - - platform: vbus - model: custom - command: 0x100 - source: 0x1234 - dest: 0x10 - sensors: - - id: vcustom - name: VBus Custom Sensor - lambda: return x[0] / 10.0; - - - platform: kuntze - ph: - name: Kuntze pH - temperature: - name: Kuntze temperature - -script: - - id: automation_test - then: - - repeat: - count: 5 - then: - - logger.log: looping! - -switch: - - platform: modbus_controller - modbus_controller_id: modbus_controller_test - id: modbus_switch_test - register_type: coil - address: 2 - bitmask: 1 - - - platform: tm1638 - id: Led0 - led: 0 - name: TM1638Led0 - - - platform: tm1638 - id: Led1 - led: 1 - name: TM1638Led1 - - - platform: tm1638 - id: Led2 - led: 2 - name: TM1638Led2 - - - platform: tm1638 - id: Led3 - led: 3 - name: TM1638Led3 - -display: - - platform: tm1638 - id: primarydisplay - stb_pin: 5 #TM1638 STB - clk_pin: 18 #TM1638 CLK - dio_pin: 23 #TM1638 DIO - update_interval: 5s - intensity: 5 - lambda: |- - it.print("81818181"); - -time: - - platform: pcf85063 - -text_sensor: - - platform: ezo_pmp - dosing_mode: - name: Dosing Mode - calibration_status: - name: Calibration Status - on_value: - - ezo_pmp.dose_volume: - id: hcl_pump - volume: 10 - - ezo_pmp.dose_volume_over_time: - id: hcl_pump - volume: 10 - duration: 2 - - ezo_pmp.dose_with_constant_flow_rate: - id: hcl_pump - volume_per_minute: 10 - duration: 2 - - ezo_pmp.set_calibration_volume: - id: hcl_pump - volume: 10 - - ezo_pmp.find: hcl_pump - - ezo_pmp.dose_continuously: hcl_pump - - ezo_pmp.clear_total_volume_dosed: hcl_pump - - ezo_pmp.clear_calibration: hcl_pump - - ezo_pmp.pause_dosing: hcl_pump - - ezo_pmp.stop_dosing: hcl_pump - - ezo_pmp.arbitrary_command: - id: hcl_pump - command: D,? - -sn74hc165: - id: sn74hc165_hub - data_pin: GPIO12 - clock_pin: GPIO14 - load_pin: GPIO27 - clock_inhibit_pin: GPIO26 - sr_count: 4 - -matrix_keypad: - id: keypad - rows: - - pin: 21 - - pin: 19 - columns: - - pin: 17 - - pin: 16 - keys: "1234" - -key_collector: - - id: reader - source_id: keypad - min_length: 4 - max_length: 4 - -light: - - platform: esp32_rmt_led_strip - id: led_strip - pin: 13 - num_leds: 60 - rmt_channel: 6 - rgb_order: GRB - chipset: ws2812 - - platform: esp32_rmt_led_strip - id: led_strip2 - pin: 15 - num_leds: 60 - rmt_channel: 2 - rgb_order: RGB - bit0_high: 100us - bit0_low: 100us - bit1_high: 100us - bit1_low: 100us diff --git a/tests/test6.yaml b/tests/test6.yaml deleted file mode 100644 index 6d956aa9c8..0000000000 --- a/tests/test6.yaml +++ /dev/null @@ -1,62 +0,0 @@ ---- -esphome: - name: test6 - project: - name: esphome.test6_project - version: "1.0.0" - -rp2040: - board: rpipicow - framework: - # Waiting for https://github.com/platformio/platform-raspberrypi/pull/36 - platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git - -wifi: - networks: - - ssid: "MySSID" - password: "password1" - -api: - -ota: - -logger: - -binary_sensor: - - platform: gpio - pin: GPIO5 - id: pin_5_button - -output: - - platform: gpio - pin: GPIO4 - id: pin_4 - -switch: - - platform: output - output: pin_4 - id: pin_4_switch - -light: - - platform: rp2040_pio_led_strip - id: led_strip - pin: GPIO13 - num_leds: 60 - pio: 0 - rgb_order: GRB - chipset: WS2812 - - platform: rp2040_pio_led_strip - id: led_strip_custom_timings - pin: GPIO13 - num_leds: 60 - pio: 1 - rgb_order: GRB - bit0_high: .1us - bit0_low: 1.2us - bit1_high: .69us - bit1_low: .4us - - -sensor: - - platform: internal_temperature - name: Internal Temperature diff --git a/tests/test7.yaml b/tests/test7.yaml deleted file mode 100644 index 10e1b035ab..0000000000 --- a/tests/test7.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Tests for ESP32-C3 boards which use toolchain-riscv32-esp ---- -wifi: - ssid: 'ssid' - -esp32: - board: lolin_c3_mini - framework: - type: arduino - -esphome: - name: 'on-response-test' - on_boot: - then: - - http_request.send: - method: PUT - url: https://esphome.io - headers: - Content-Type: application/json - body: Some data - verify_ssl: false - on_response: - then: - - logger.log: - format: "Response status: %d" - args: - - status_code - -logger: - -http_request: - useragent: esphome/tagreader - timeout: 10s diff --git a/tests/test8.yaml b/tests/test8.yaml deleted file mode 100644 index 2430a0d1e6..0000000000 --- a/tests/test8.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Tests for ESP32-S3 boards ---- -wifi: - ssid: "ssid" - -esp32: - board: esp32s3box - variant: ESP32S3 - framework: - type: arduino - -esphome: - name: esp32-s3-test - -logger: - -light: - - platform: neopixelbus - type: GRB - variant: WS2812 - pin: 33 - num_leds: 1 - id: neopixel - method: esp32_rmt - name: neopixel-enable - internal: false - restore_mode: ALWAYS_OFF diff --git a/tests/test_build_components/build_components_base.bk72xx-ard.yaml b/tests/test_build_components/build_components_base.bk72xx-ard.yaml new file mode 100644 index 0000000000..9a4e15d5cf --- /dev/null +++ b/tests/test_build_components/build_components_base.bk72xx-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestespbk72xx + friendly_name: $component_name + +bk72xx: + board: generic-bk7231n-qfn32-tuya + +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.esp32-ard.yaml b/tests/test_build_components/build_components_base.esp32-ard.yaml new file mode 100644 index 0000000000..31b7067acc --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-ard.yaml @@ -0,0 +1,17 @@ +esphome: + name: componenttestesp32ard + friendly_name: $component_name + +esp32: + board: nodemcu-32s + framework: + type: arduino + +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.esp32-c3-ard.yaml b/tests/test_build_components/build_components_base.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8aad447693 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c3-ard.yaml @@ -0,0 +1,17 @@ +esphome: + name: componenttestesp32c3ard + friendly_name: $component_name + +esp32: + board: lolin_c3_mini + framework: + type: arduino + +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.esp32-c3-idf-51.yaml b/tests/test_build_components/build_components_base.esp32-c3-idf-51.yaml new file mode 100644 index 0000000000..eb5b23a4ec --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c3-idf-51.yaml @@ -0,0 +1,19 @@ +esphome: + name: componenttestesp32c3idf51 + friendly_name: $component_name + +esp32: + board: lolin_c3_mini + framework: + type: esp-idf + version: 5.1.2 + platform_version: 6.5.0 + +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.esp32-c3-idf.yaml b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml new file mode 100644 index 0000000000..18584497f4 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml @@ -0,0 +1,17 @@ +esphome: + name: componenttestesp32c3idf + friendly_name: $component_name + +esp32: + board: lolin_c3_mini + 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.esp32-idf-51.yaml b/tests/test_build_components/build_components_base.esp32-idf-51.yaml new file mode 100644 index 0000000000..b5e3dd6d83 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-idf-51.yaml @@ -0,0 +1,19 @@ +esphome: + name: componenttestesp32idf51 + friendly_name: $component_name + +esp32: + board: nodemcu-32s + framework: + type: esp-idf + version: 5.1.2 + platform_version: 6.5.0 + +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.esp32-idf.yaml b/tests/test_build_components/build_components_base.esp32-idf.yaml new file mode 100644 index 0000000000..a62a995e68 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-idf.yaml @@ -0,0 +1,17 @@ +esphome: + name: componenttestesp32idf + friendly_name: $component_name + +esp32: + board: nodemcu-32s + 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.esp32-s2-ard.yaml b/tests/test_build_components/build_components_base.esp32-s2-ard.yaml new file mode 100644 index 0000000000..b8f2639127 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s2-ard.yaml @@ -0,0 +1,18 @@ +esphome: + name: componenttestesp32s2ard + friendly_name: $component_name + +esp32: + board: esp32-s2-saola-1 + variant: ESP32S2 + framework: + type: arduino + +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.esp32-s2-idf-51.yaml b/tests/test_build_components/build_components_base.esp32-s2-idf-51.yaml new file mode 100644 index 0000000000..11b077509e --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s2-idf-51.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32s2idf51 + friendly_name: $component_name + +esp32: + board: esp32-s2-saola-1 + variant: ESP32S2 + framework: + type: esp-idf + version: 5.1.2 + platform_version: 6.5.0 + +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.esp32-s2-idf.yaml b/tests/test_build_components/build_components_base.esp32-s2-idf.yaml new file mode 100644 index 0000000000..484906e8df --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s2-idf.yaml @@ -0,0 +1,18 @@ +esphome: + name: componenttestesp32s2idf + friendly_name: $component_name + +esp32: + board: esp32-s2-saola-1 + variant: ESP32S2 + 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.esp32-s3-ard.yaml b/tests/test_build_components/build_components_base.esp32-s3-ard.yaml new file mode 100644 index 0000000000..25cad038b6 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s3-ard.yaml @@ -0,0 +1,18 @@ +esphome: + name: componenttestesp32s3ard + friendly_name: $component_name + +esp32: + board: esp32s3box + variant: ESP32S3 + framework: + type: arduino + +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.esp32-s3-idf-51.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf-51.yaml new file mode 100644 index 0000000000..4357b3581b --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s3-idf-51.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32s3idf51 + friendly_name: $component_name + +esp32: + board: esp32s3box + variant: ESP32S3 + framework: + type: esp-idf + version: 5.1.2 + platform_version: 6.5.0 + +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.esp32-s3-idf.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml new file mode 100644 index 0000000000..ee209000e9 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml @@ -0,0 +1,18 @@ +esphome: + name: componenttestesp32s3idf + friendly_name: $component_name + +esp32: + board: esp32s3box + variant: ESP32S3 + 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.esp8266-ard.yaml b/tests/test_build_components/build_components_base.esp8266-ard.yaml new file mode 100644 index 0000000000..e4d6607c86 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp8266-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestesp8266ard + friendly_name: $component_name + +esp8266: + board: d1_mini + +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.host.yaml b/tests/test_build_components/build_components_base.host.yaml new file mode 100644 index 0000000000..5492cfddd2 --- /dev/null +++ b/tests/test_build_components/build_components_base.host.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttesthost + friendly_name: $component_name + +host: + mac_address: "62:23:45:AF:B3:DD" + +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.rp2040-ard.yaml b/tests/test_build_components/build_components_base.rp2040-ard.yaml new file mode 100644 index 0000000000..4fb8d51333 --- /dev/null +++ b/tests/test_build_components/build_components_base.rp2040-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestrp2040ard + friendly_name: $component_name + +rp2040: + board: rpipicow + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 41d0f3dadb..d61c4a442a 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -8,6 +8,7 @@ If adding unit tests ensure that they are fast. Slower integration tests should not be part of a unit test suite. """ + import sys import pytest diff --git a/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml b/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml new file mode 100644 index 0000000000..aaca55b807 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml @@ -0,0 +1,18 @@ +--- +substitutions: + name: original + +wifi: !include + file: includes/broken_included.yaml.txt + vars: + name: my_custom_ssid + +esphome: + # should be substituted as 'original', + # not overwritten by vars in the !include above + name: ${name} + name_add_mac_suffix: true + platform: esp8266 + board: !include {file: includes/scalar.yaml, vars: {var1: nodemcu}} + + libraries: !include {file: includes/list.yaml, vars: {var1: Wire}} diff --git a/tests/unit_tests/fixtures/yaml_util/includes/broken_included.yaml.txt b/tests/unit_tests/fixtures/yaml_util/includes/broken_included.yaml.txt new file mode 100644 index 0000000000..6e53395c86 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includes/broken_included.yaml.txt @@ -0,0 +1,5 @@ +--- +# yamllint disable-line + ssid: ${name} +# yamllint disable-line + fdf: error diff --git a/tests/unit_tests/fixtures/yaml_util/missing_comp.yaml b/tests/unit_tests/fixtures/yaml_util/missing_comp.yaml new file mode 100644 index 0000000000..d065901ed9 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/missing_comp.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ap: ~ + +image: + - id: its_a_bug + file: "mdi:bug" diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 9a15bf0b9c..2860486efe 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -1,7 +1,7 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip_addresses +from hypothesis.strategies import ip_addresses from strategies import mac_addr_strings from esphome import core, const @@ -116,14 +116,16 @@ class TestTimePeriod: assert actual == expected - def test_init__microseconds_with_fraction(self): - with pytest.raises(ValueError, match="Maximum precision is microseconds"): - core.TimePeriod(microseconds=1.1) + def test_init__nanoseconds_with_fraction(self): + with pytest.raises(ValueError, match="Maximum precision is nanoseconds"): + core.TimePeriod(nanoseconds=1.1) @pytest.mark.parametrize( "kwargs, expected", ( ({}, "0s"), + ({"nanoseconds": 1}, "1ns"), + ({"nanoseconds": 1.0001}, "1ns"), ({"microseconds": 1}, "1us"), ({"microseconds": 1.0001}, "1us"), ({"milliseconds": 2}, "2ms"), diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 331c500c04..6f4b5a40bc 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -1,4 +1,4 @@ -from typing import Iterator +from collections.abc import Iterator import math diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index ad234250ce..497b3966fb 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -1,5 +1,5 @@ import pytest -from mock import Mock +from unittest.mock import Mock from esphome import cpp_helpers as ch from esphome import const diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index b98838024f..26ebdcf6af 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,7 +1,7 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip_addresses +from hypothesis.strategies import ip_addresses from esphome import helpers @@ -108,6 +108,10 @@ def test_is_ip_address__valid(value): ("FOO", None, False, False), ("FOO", None, True, True), ("FOO", "", False, False), + ("FOO", "False", False, False), + ("FOO", "True", False, True), + ("FOO", "FALSE", True, False), + ("FOO", "fAlSe", True, False), ("FOO", "Yes", False, True), ("FOO", "123", False, True), ), @@ -254,9 +258,10 @@ def test_snake_case(text, expected): "text, expected", ( ("foo_bar", "foo_bar"), - ('!"§$%&/()=?foo_bar', "foo_bar"), - ('foo_!"§$%&/()=?bar', "foo_bar"), - ('foo_bar!"§$%&/()=?', "foo_bar"), + ('!"§$%&/()=?foo_bar', "___________foo_bar"), + ('foo_!"§$%&/()=?bar', "foo____________bar"), + ('foo_bar!"§$%&/()=?', "foo_bar___________"), + ('foo-bar!"§$%&/()=?', "foo-bar___________"), ), ) def test_sanitize(text, expected): diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 79a5894075..9260629ec3 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -1,8 +1,14 @@ """Tests for the wizard.py file.""" +import os + import esphome.wizard as wz import pytest +from esphome.core import CORE from esphome.components.esp8266.boards import ESP8266_BOARD_PINS +from esphome.components.esp32.boards import ESP32_BOARD_PINS +from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS +from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS from unittest.mock import MagicMock @@ -107,6 +113,7 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): # Given del default_config["platform"] monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) # When wz.wizard_write(tmp_path, **default_config) @@ -127,6 +134,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266( default_config["board"] = [*ESP8266_BOARD_PINS][0] monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) # When wz.wizard_write(tmp_path, **default_config) @@ -140,13 +148,14 @@ def test_wizard_write_defaults_platform_from_board_esp32( default_config, tmp_path, monkeypatch ): """ - If the platform is not explicitly set, use "ESP32" if the board is not one of the ESP8266 boards + If the platform is not explicitly set, use "ESP32" if the board is one of the ESP32 boards """ # Given del default_config["platform"] - default_config["board"] = "foo" + default_config["board"] = [*ESP32_BOARD_PINS][0] monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) # When wz.wizard_write(tmp_path, **default_config) @@ -156,6 +165,48 @@ def test_wizard_write_defaults_platform_from_board_esp32( assert "esp32:" in generated_config +def test_wizard_write_defaults_platform_from_board_bk72xx( + default_config, tmp_path, monkeypatch +): + """ + If the platform is not explicitly set, use "BK72XX" if the board is one of BK72XX boards + """ + # Given + del default_config["platform"] + default_config["board"] = [*BK72XX_BOARD_PINS][0] + + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "bk72xx:" in generated_config + + +def test_wizard_write_defaults_platform_from_board_rtl87xx( + default_config, tmp_path, monkeypatch +): + """ + If the platform is not explicitly set, use "RTL87XX" if the board is one of RTL87XX boards + """ + # Given + del default_config["platform"] + default_config["board"] = [*RTL87XX_BOARD_PINS][0] + + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "rtl87xx:" in generated_config + + def test_safe_print_step_prints_step_number_and_description(monkeypatch): """ The safe_print_step function prints the step number and the passed description @@ -186,7 +237,7 @@ def test_default_input_uses_default_if_no_input_supplied(monkeypatch): """ # Given - monkeypatch.setattr("builtins.input", lambda _: "") + monkeypatch.setattr("builtins.input", lambda _=None: "") default_string = "foobar" # When @@ -203,7 +254,7 @@ def test_default_input_uses_user_supplied_value(monkeypatch): # Given user_input = "A value" - monkeypatch.setattr("builtins.input", lambda _: user_input) + monkeypatch.setattr("builtins.input", lambda _=None: user_input) default_string = "foobar" # When @@ -269,7 +320,7 @@ def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answ config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -291,7 +342,7 @@ def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answer config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -321,7 +372,7 @@ def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -344,7 +395,7 @@ def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -366,7 +417,7 @@ def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -388,7 +439,7 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 8ee991f5b3..9178726247 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1,5 +1,6 @@ from esphome import yaml_util from esphome.components import substitutions +from esphome.core import EsphomeError def test_include_with_vars(fixture_path): @@ -11,3 +12,33 @@ def test_include_with_vars(fixture_path): assert actual["esphome"]["libraries"][0] == "Wire" assert actual["esphome"]["board"] == "nodemcu" assert actual["wifi"]["ssid"] == "my_custom_ssid" + + +def test_loading_a_broken_yaml_file(fixture_path): + """Ensure we fallback to pure python to give good errors.""" + yaml_file = fixture_path / "yaml_util" / "broken_includetest.yaml" + + try: + yaml_util.load_yaml(yaml_file) + except EsphomeError as err: + assert "broken_included.yaml" in str(err) + + +def test_loading_a_yaml_file_with_a_missing_component(fixture_path): + """Ensure we show the filename for a yaml file with a missing component.""" + yaml_file = fixture_path / "yaml_util" / "missing_comp.yaml" + + try: + yaml_util.load_yaml(yaml_file) + except EsphomeError as err: + assert "missing_comp.yaml" in str(err) + + +def test_loading_a_missing_file(fixture_path): + """We throw EsphomeError when loading a missing file.""" + yaml_file = fixture_path / "yaml_util" / "missing.yaml" + + try: + yaml_util.load_yaml(yaml_file) + except EsphomeError as err: + assert "missing.yaml" in str(err)